checking in- needs some refinement then done

This commit is contained in:
2025-02-09 23:07:25 -05:00
parent 64b669edc3
commit d8469533a7
22 changed files with 1110 additions and 115 deletions

View File

@@ -1,15 +1,47 @@
package netsplit
import (
`io/fs`
`net/netip`
`sync`
`github.com/go-resty/resty/v2`
)
const (
cachedirEnvName string = "SBNTR_RSVCACHE_DIR"
// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
ianaSpecial4 string = "https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xml"
// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
ianaSpecial6 string = "https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xml"
ianaDateTfmt string = "2006-01-02"
ianaMonthTfmt string = "2006-01"
ianaTrue string = "True"
ianaFalse string = "False"
ianaNA string = "N/A"
ianaSpecial4Cache string = "iana_reserved_4.json"
ianaSpecial6Cache string = "iana_reserved_6.json"
cacheDirPerms fs.FileMode = 0o0750
cacheFilePerms fs.FileMode = 0o0640
)
var (
ReservedNets map[netip.Prefix]string
cacheDir string
isCaching bool
ianaReserved4 *IANARegistry
ianaReserved6 *IANARegistry
)
var (
// TODO
cacheLock sync.RWMutex
cacheClient *resty.Client
// IPv4: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml#iana-ipv4-special-registry-1
// IPv6: https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
reservedNets map[netip.Prefix]*IANAAddrNetResRecord
// Up to date as of Feb 2, 2025
reservedNetsOrig map[string]string = map[string]string{
// IPv6
// https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
// IPv6: https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml
"::/128": "Unspecified Address (RFC 4291 § 2.5.2)",
"::1/128": "Loopback Address (RFC 4291 § 2.5.3)",
"ff00::/8": "Multicast (RFC 4291 § 2.7)",
@@ -34,8 +66,29 @@ var (
"5f00::/16": "Segment Routing (SRv6) SIDs (RFC 9602)",
"fc00::/7": "Unique-Local Addressing (RFC 4193)", // private/LAN
"fe80::/10": "Link-Local Unicast (RFC 4291 § 2.5.6)",
// IPv4
// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml#iana-ipv4-special-registry-1
"": "",
// IPv4: https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml#iana-ipv4-special-registry-1
"0.0.0.0/8": "\"This Network\" (RFC 791 § 3.2)",
"0.0.0.0/32": "\"This Host on This Network\" (RFC 1122 § 3.2.1.3)",
"10.0.0.0/8": "Private Use (RFC 1918)", // private/LAN
"100.64.0.0/10": "Shared Address Space (CGNAT) (RFC 6598)",
"127.0.0.0/8": "Loopback (RFC 1122 § 3.2.1.3)",
"169.254.0.0/16": "Link-Local (RFC 3927)",
"172.16.0.0/12": "Private Use (RFC 1918)", // private/LAN
"192.0.0.0/24": "IETF Protocol Assignments (RFC 6890 § 2.1)",
"192.0.0.0/29": "IPv4 Service Community Prefix (RFC 7335)",
"192.0.0.8/32": "IPv4 Dummy Address (RFC 7600)",
"192.0.0.9/32": "Port Control Protocol Anycast (RFC 7723)",
"192.0.0.10/32": "Traversal Using Relays Around NAT Anycast (RFC 8155)",
"192.0.0.170/32": "NAT64/DNS64 Discovery (RFC 7050 § 2.2) (RFC 8880)",
"192.0.0.171/32": "NAT64/DNS64 Discovery (RFC 7050 § 2.2) (RFC 8880)",
"192.0.2.0/24": "Documentation (TEST-NET-1) (RFC 5737)",
"192.31.196.0/24": "AS112-v4 (RFC 7535)",
"192.52.193.0/24": "AMT (RFC 7450)",
"192.168.0.0/16": "Private Use (RFC 1918)", // private/LAN
"192.175.48.0/24": "Direct Delegation AS112 Service (RFC 7534)",
"198.18.0.0/15": "Benchmarking (RFC 2544)",
"198.51.100.0/24": "Documentation (TEST-NET-2) (RFC 5737)",
"240.0.0.0/24": "Reserved for Future Use (RFC 1112 § 4)",
"255.255.255.255/32": "Limited Broadcast (RFC 919 § 7) (RFC 8190)",
}
)

View File

@@ -150,8 +150,10 @@ func AddrInvert(ip netip.Addr) (inverted netip.Addr) {
}
/*
CheckReserved checks nets for any reserved prefixes, either directly or included within the prefix depending on recurse.
CheckReserved checks nets for any reserved prefixes; either directly/explicitly,
included *within* a reserved prefix (revRecursive), or *including* a reserved prefix (recursive).
excludePrivate indicates if LAN networks should be considered as "reserved" or not.
If a network is found via revRecursive/recursive, the matching prefix - not the specified one - will be in reservations.
Any found will be returned in reservations.
@@ -159,18 +161,68 @@ func AddrInvert(ip netip.Addr) (inverted netip.Addr) {
Note that prefix-specific broadcasts (e.g. x.255.255.255/8, x.x.x.255/24, ::/64, x:ffff:ffff:ffff:ffff/64, etc.)
will *not* be considered as "reserved" as they are considered normal addresses expected for functionality.
This primarily focuses on prefixes/subnets for this reason.
Additionally, all of nets will be aligned to their proper boundary range/CIDR/subnet.
*/
func CheckReserved(nets []*netip.Prefix, recurse, excludePrivate bool) (reservations map[netip.Prefix]string, err error) {
func CheckReserved(nets []*netip.Prefix, revRecursive, recursive, excludePrivate bool) (reservations map[netip.Prefix]*IANAAddrNetResRecord, err error) {
// TODO
var ok bool
var res *IANAAddrNetResRecord
var reserved map[netip.Prefix]*IANAAddrNetResRecord
if nets == nil || len(nets) == 0 {
return
}
if _, _, reserved, err = RetrieveReserved(); err != nil {
return
}
for _, n := range nets {
if n == nil {
continue
}
if n.Addr().IsPrivate() && excludePrivate {
continue
}
*n = n.Masked()
if res, ok = reserved[*n]; ok {
if reservations == nil {
reservations = make(map[netip.Prefix]*IANAAddrNetResRecord)
}
reservations[*n] = res
if !revRecursive && !recursive {
continue
}
for p, r := range reserved {
// This... *should* be safe? I don't think any reservations overlap.
// Anyways, revRecursive works because n.Addr() returns the network address, which should be the canonical boundary.
// recursive works for the same reason, just the other end.
// Math!
if revRecursive && p.Contains(n.Addr()) {
if reservations == nil {
reservations = make(map[netip.Prefix]*IANAAddrNetResRecord)
}
reservations[p] = r
} else if recursive && n.Contains(p.Addr()) {
if reservations == nil {
reservations = make(map[netip.Prefix]*IANAAddrNetResRecord)
}
reservations[p] = r
}
}
}
}
return
}
// Contain takes the results of a NetSplitter and returns a StructuredResults.
// Contain takes the results of a NetSplitter and returns a StructuredResults. The reservations are only checked against nets.
func Contain(origPfx *netip.Prefix, nets []*netip.Prefix, remaining *netipx.IPSet, splitter NetSplitter) (s *StructuredResults, err error) {
var idx int
var r *IANAAddrNetResRecord
var rem []netip.Prefix
var reserved map[netip.Prefix]*IANAAddrNetResRecord
var sr = StructuredResults{
Original: origPfx,
}
@@ -223,6 +275,18 @@ func Contain(origPfx *netip.Prefix, nets []*netip.Prefix, remaining *netipx.IPSe
}
}
if nets != nil {
if reserved, err = CheckReserved(nets, true, true, false); err != nil {
return
}
if reserved != nil && len(reserved) > 0 {
s.Reservations = make([]*IANAAddrNetResRecord, len(reserved))
for idx, r = range reserved {
s.Reservations[idx] = r
}
}
}
s = &sr
return

383
netsplit/funcs_cache.go Normal file
View File

@@ -0,0 +1,383 @@
package netsplit
import (
`encoding/json`
`errors`
`io/fs`
`net/http`
`net/netip`
`os`
`path/filepath`
`sync`
`github.com/go-resty/resty/v2`
`r00t2.io/goutils/multierr`
`r00t2.io/sysutils/envs`
`r00t2.io/sysutils/paths`
)
/*
CacheReserved caches the IANA address/network reservations to disk
(and updates ianaReserved4, ianaReserved6, and reservedNets).
It is up to the caller to schedule periodic CacheReserved calls
according to their needs for long-lived processes.
It *shouldn't* cause any memory leaks, but this has not been tested/confirmed.
If caching is not enabled, CacheReserved exits withouth any retrieval, parsing, etc.
*/
func CacheReserved() (err error) {
var dat4 []byte
var dat6 []byte
if !isCaching {
return
}
if cacheDir == "" {
if cacheDir, err = getDefCachePath(); err != nil {
return
}
}
if dat4, dat6, err = getLive(); err != nil {
return
}
if err = writeCache(dat4, dat6); err != nil {
return
}
return
}
// CleanCache clears the cache completely.
func CleanCache() (err error) {
if cacheDir == "" {
if cacheDir, err = getDefCachePath(); err != nil {
return
}
}
if err = os.RemoveAll(cacheDir); err != nil {
return
}
return
}
/*
RetrieveReserved returns the current reservations and (re-)populates reservedNets.
It returns a copy of reservedNets and the current reservations that are safe to use concurrently/separately
from subnetter.
If caching is enabled, then:
1.) First the local cache will be checked.
2.) If no cache exists, subnetter will attempt to populate it (CacheReserved());
otherwise the data there will be used.
2.b.) If an error occurs while parsing the cached data, the cache will be invalidated
and attempt to be updated.
3.) If no cache exists and the live resource is unavailable, an error will be returned.
If not:
1.) The live resource will be fetched. If it is unavailable, an error will be returned.
*/
func RetrieveReserved() (ipv4, ipv6 IANARegistry, reserved map[netip.Prefix]*IANAAddrNetResRecord, err error) {
var b []byte
var dat4 []byte
var dat6 []byte
var hasCache bool
if isCaching {
if hasCache, err = checkCache(); err != nil {
return
}
if !hasCache {
if err = CacheReserved(); err != nil {
return
}
}
if dat4, dat6, err = readCache(); err != nil {
return
}
} else {
if dat4, dat6, err = getLive(); err != nil {
return
}
}
if err = json.Unmarshal(dat4, &ipv4); err != nil {
return
}
if err = json.Unmarshal(dat6, &ipv6); err != nil {
return
}
ianaReserved4 = new(IANARegistry)
ianaReserved4 = new(IANARegistry)
if err = json.Unmarshal(dat4, ianaReserved4); err != nil {
return
}
if err = json.Unmarshal(dat6, ianaReserved6); err != nil {
return
}
reservedNets = make(map[netip.Prefix]*IANAAddrNetResRecord)
for _, reg := range []*IANARegistry{
ianaReserved4, ianaReserved6,
} {
for _, rec := range reg.Notice.Records {
for _, n := range rec.Networks.Prefixes {
reservedNets[*n] = rec
}
}
}
if b, err = json.Marshal(reservedNets); err != nil {
return
}
reserved = make(map[netip.Prefix]*IANAAddrNetResRecord)
if err = json.Unmarshal(b, &reserved); err != nil {
return
}
return
}
// GetCacheConfig returns the current state and path of subnetter's cache.
func GetCacheConfig() (enabled bool, cacheDirPath string) {
enabled = isCaching
cacheDirPath = cacheDir
return
}
// EnableCache enables or disables subnetter's caching.
func EnableCache(enable bool) (err error) {
var oldVal bool = isCaching
isCaching = enable
if isCaching && (oldVal != isCaching) {
if err = os.MkdirAll(cacheDir, 0o0640); err != nil {
return
}
}
return
}
/*
SetCachePath sets the cache path. Use an empty cacheDirPath to use the default path.
If the cache dir was changed from its previous value, subnetter will attempt to create it.
*/
func SetCachePath(cacheDirPath string) (err error) {
var oldPath string = cacheDir
if cacheDirPath == "" {
if cacheDirPath, err = getDefCachePath(); err != nil {
return
}
} else {
if err = paths.RealPath(&cacheDirPath); err != nil {
return
}
}
if cacheDirPath != oldPath {
if err = os.MkdirAll(cacheDir, cacheDirPerms); err != nil {
return
}
}
return
}
func checkCache() (hasCache bool, err error) {
var numCached uint8
var cacheDirEntries []fs.DirEntry
if cacheDir == "" {
if cacheDir, err = getDefCachePath(); err != nil {
return
}
}
if cacheDirEntries, err = os.ReadDir(cacheDir); err != nil {
return
}
for _, entry := range cacheDirEntries {
if entry.IsDir() {
continue
}
switch entry.Name() {
case ianaSpecial4Cache, ianaSpecial6Cache:
numCached++
}
if numCached >= 2 {
break
}
}
hasCache = numCached > 2 && ianaReserved4 != nil && ianaReserved6 != nil
return
}
func getDefCachePath() (val string, err error) {
if envs.HasEnv(cachedirEnvName) {
val = os.Getenv(cachedirEnvName)
} else {
if val, err = os.UserCacheDir(); err != nil {
return
}
}
if err = paths.RealPath(&val); err != nil {
return
}
return
}
func getLive() (dat4, dat6 []byte, err error) {
var wg sync.WaitGroup
var errChan chan error
var doneChan chan bool
var mErr *multierr.MultiError
var numJobs int = 2
doneChan = make(chan bool, 1)
mErr = multierr.NewMultiError(nil)
wg.Add(numJobs)
errChan = make(chan error, numJobs)
if cacheClient == nil {
cacheClient = resty.New()
}
// IPv4
go func() {
var rErr error
var req *resty.Request
var resp *resty.Response
var dat *IANARegistry = new(IANARegistry)
defer wg.Done()
req = cacheClient.R()
req.SetResult(dat)
if resp, rErr = req.Get(ianaSpecial4); rErr != nil {
errChan <- rErr
return
}
if resp.StatusCode() != http.StatusOK {
errChan <- errors.New(resp.Status())
return
}
ianaReserved4 = new(IANARegistry)
*ianaReserved4 = *dat
if dat4, rErr = json.Marshal(dat); rErr != nil {
errChan <- rErr
return
}
}()
// IPv6
go func() {
var rErr error
var req *resty.Request
var resp *resty.Response
var dat *IANARegistry = new(IANARegistry)
defer wg.Done()
req = cacheClient.R()
req.SetResult(dat)
if resp, rErr = req.Get(ianaSpecial6); rErr != nil {
errChan <- rErr
return
}
if resp.StatusCode() != http.StatusOK {
errChan <- errors.New(resp.Status())
return
}
if dat6, rErr = json.Marshal(dat); rErr != nil {
errChan <- rErr
return
}
}()
go func() {
wg.Wait()
close(errChan)
doneChan <- true
}()
<-doneChan
for i := 0; i < numJobs; i++ {
if err = <-errChan; err != nil {
mErr.AddError(err)
err = nil
}
}
if !mErr.IsEmpty() {
err = mErr
return
}
return
}
func readCache() (dat4, dat6 []byte, err error) {
if dat4, err = os.ReadFile(filepath.Join(cacheDir, ianaSpecial4Cache)); err != nil {
return
}
if dat6, err = os.ReadFile(filepath.Join(cacheDir, ianaSpecial6Cache)); err != nil {
return
}
return
}
func writeCache(dat4, dat6 []byte) (err error) {
if err = os.WriteFile(
filepath.Join(cacheDir, ianaSpecial4Cache),
dat4,
cacheFilePerms,
); err != nil {
return
}
if err = os.WriteFile(
filepath.Join(cacheDir, ianaSpecial6Cache),
dat6,
cacheFilePerms,
); err != nil {
return
}
return
}

View File

@@ -0,0 +1,47 @@
package netsplit
import (
`encoding/xml`
`errors`
`io`
`strings`
)
func (i *IANABool) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) {
var tok xml.Token
for {
if tok, err = d.Token(); err != nil {
if errors.Is(err, io.EOF) {
err = nil
break
}
return
}
switch t := tok.(type) {
case xml.CharData:
switch strings.TrimSpace(string(t)) {
case ianaTrue:
i.Evaluated = new(bool)
*i.Evaluated = true
case ianaFalse:
i.Evaluated = new(bool)
*i.Evaluated = false
case ianaNA:
i.Applicable = new(bool)
*i.Applicable = false
/*
default:
fmt.Printf("Unknown bool: %s\n", tok)
*/
}
/*
default:
spew.Dump(t)
*/
}
}
return
}

View File

@@ -0,0 +1,53 @@
package netsplit
import (
`strings`
`time`
)
// MarshalText lets an IANADate conform to an encoding.TextMarshaler.
func (i *IANADate) MarshalText() (text []byte, err error) {
if i == nil {
return
}
if time.Time(*i).Day() == 0 {
text = []byte(time.Time(*i).Format(ianaMonthTfmt))
} else {
text = []byte(time.Time(*i).Format(ianaDateTfmt))
}
return
}
// UnmarshalText lets an IANADate conform to an encoding.TextUnmarshaler.
func (i *IANADate) UnmarshalText(text []byte) (err error) {
var t time.Time
if text == nil {
return
}
switch len(string(text)) {
case 7: // no day
if t, err = time.Parse(
ianaMonthTfmt,
strings.TrimSpace(string(text)),
); err != nil {
return
}
default: // TECHNICALLY should be 10 but we'll let the error here catch it otherwise.
if t, err = time.Parse(
ianaDateTfmt,
strings.TrimSpace(string(text)),
); err != nil {
return
}
}
*i = IANADate(t)
return
}

View File

@@ -0,0 +1,65 @@
package netsplit
import (
`encoding/xml`
`errors`
`fmt`
`io`
`net/netip`
`strings`
)
// UnmarshalXML conforms an IANAPrefix to (encoding/xml).Unmarshaler.
func (i *IANAPrefix) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) {
var tok xml.Token
var nextRefIdx int
var ref *IANARef
var pfxStr []string
var sb *strings.Builder = new(strings.Builder)
for {
if tok, err = d.Token(); err != nil {
if errors.Is(err, io.EOF) {
err = nil
break
}
return
}
switch t := tok.(type) {
case xml.CharData:
sb.Write(t)
case xml.StartElement:
switch t.Name.Local {
case "xref":
ref = new(IANARef)
if err = d.DecodeElement(ref, &t); err != nil {
return
}
if i.References == nil {
i.References = make([]*IANARef, 0)
}
i.References = append(i.References, ref)
// No reference for these; they should be only network addrs.
// fmt.Fprintf(sb, "[REF %d]", nextRefIdx)
nextRefIdx++
default:
fmt.Println(t.Name.Local)
continue
}
}
}
pfxStr = strings.Split(sb.String(), ",")
i.Prefixes = make([]*netip.Prefix, len(pfxStr))
for idx, p := range pfxStr {
i.Prefixes[idx] = new(netip.Prefix)
if *i.Prefixes[idx], err = netip.ParsePrefix(
strings.TrimSpace(p),
); err != nil {
return
}
}
return
}

View File

@@ -0,0 +1,27 @@
package netsplit
import (
`encoding/xml`
`strconv`
)
func (i *IANARegistryFootnote) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) {
var u64 uint64
for _, a := range start.Attr {
switch a.Name.Local {
case "anchor":
if u64, err = strconv.ParseUint(a.Value, 10, 64); err != nil {
return
}
i.ReferenceIdx = uint(u64)
}
}
if err = d.DecodeElement(&i.Note, &start); err != nil {
return
}
return
}

View File

@@ -0,0 +1,51 @@
package netsplit
import (
`encoding/xml`
`errors`
`fmt`
`io`
`strings`
)
func (i *IANAString) UnmarshalXML(d *xml.Decoder, start xml.StartElement) (err error) {
var tok xml.Token
var nextRefIdx int
var ref *IANARef
var sb *strings.Builder = new(strings.Builder)
for {
if tok, err = d.Token(); err != nil {
if errors.Is(err, io.EOF) {
err = nil
break
}
return
}
switch t := tok.(type) {
case xml.CharData:
sb.Write(t)
case xml.StartElement:
switch t.Name.Local {
case "xref":
ref = new(IANARef)
if err = d.DecodeElement(ref, &t); err != nil {
return
}
if i.References == nil {
i.References = make([]*IANARef, 0)
}
i.References = append(i.References, ref)
fmt.Fprintf(sb, "[REF %d]", nextRefIdx)
nextRefIdx++
default:
continue
}
}
}
i.Text = strings.TrimSpace(sb.String())
return
}

View File

@@ -1,20 +0,0 @@
package netsplit
import (
`log`
`net/netip`
)
func init() {
var err error
var pfx netip.Prefix
ReservedNets = make(map[netip.Prefix]string)
for np, reason := range reservedNetsOrig {
if pfx, err = netip.ParsePrefix(np); err != nil {
log.Panicln(err)
}
ReservedNets[pfx] = reason
}
}

37
netsplit/tcache_test.go Normal file
View File

@@ -0,0 +1,37 @@
package netsplit
import (
`fmt`
`net/netip`
"testing"
)
func TestCache(t *testing.T) {
var err error
var ip4 IANARegistry
var ip6 IANARegistry
var reserved map[netip.Prefix]*IANAAddrNetResRecord
if err = SetCachePath("/tmp/subnetter_cache_test"); err != nil {
return
}
if err = EnableCache(true); err != nil {
return
}
if ip4, ip6, reserved, err = RetrieveReserved(); err != nil {
t.Fatal(err)
}
fmt.Printf("IPv4: '%s'\n", ip4.Title)
fmt.Printf("IPv6: '%s'\n", ip6.Title)
fmt.Printf("IPv4 (Internal): '%s'\n", ianaReserved4.Title)
fmt.Printf("IPv6 (Internal): '%s'\n", ianaReserved6.Title)
fmt.Printf("%d Reserved Networks\n", len(reserved))
fmt.Printf("%d Reserved Networks (Internal)\n", len(reservedNets))
if err = CleanCache(); err != nil {
t.Fatal(err)
}
}

55
netsplit/tiana_test.go Normal file
View File

@@ -0,0 +1,55 @@
package netsplit
import (
`encoding/json`
`fmt`
`net/http`
"testing"
`github.com/go-resty/resty/v2`
`github.com/goccy/go-yaml`
)
func TestIANA(t *testing.T) {
var err error
var b []byte
var req *resty.Request
var resp *resty.Response
var reg *IANARegistry
var client *resty.Client = resty.New()
// IPv4
req = client.R()
reg = new(IANARegistry)
req.SetResult(reg)
if resp, err = req.Get(ianaSpecial4); err != nil {
t.Fatal(err)
}
if resp.StatusCode() != http.StatusOK {
t.Fatal(resp.Status())
}
if b, err = json.MarshalIndent(reg, "", " "); err != nil {
t.Fatal(err)
}
fmt.Println(string(b))
// IPv6
req = client.R()
reg = new(IANARegistry)
req.SetResult(reg)
if resp, err = req.Get(ianaSpecial6); err != nil {
t.Fatal(err)
}
if resp.StatusCode() != http.StatusOK {
t.Fatal(resp.Status())
}
if b, err = yaml.Marshal(reg); err != nil {
t.Fatal(err)
}
fmt.Println(string(b))
_ = b
}

View File

@@ -4,6 +4,7 @@ import (
"encoding/xml"
"net"
"net/netip"
`time`
"go4.org/netipx"
)
@@ -100,6 +101,8 @@ type StructuredResults struct {
Allocated []*ContainedResult `json:"subnets" xml:"subnets>subnet,omitempty" yaml:"Subnets"`
// Unallocated contains subnets from Original that did not meet the splitting criteria or were left over from the split operation.
Unallocated []*ContainedResult `json:"remaining" xml:"remaining>subnet,omitempty" yaml:"Remaining/Unallocated/Left Over,omitempty"`
// Reservations contains any reserved addresses/prefixes within this set that are considered "special usage" and thus are likely to not be usable.
Reservations []*IANAAddrNetResRecord `json:"reserved,omitempty" xml:"reserved>reservation,omitempty" yaml:"Matching Reserved Subnets,omitempty"`
}
type SplitOpts struct {
@@ -115,3 +118,76 @@ type ContainedResult struct {
XMLName xml.Name `json:"-" yaml:"-" xml:"subnet"`
Network *netip.Prefix `json:"net" xml:"net,attr,omitempty" yaml:"network,omitempty"`
}
type IANADate time.Time
/*
WHAT a PITA.
IANA publishes their reservations in XML (YAY!) ... except they use inner XML all over the place
in their text.
So.
*/
type IANARegistry struct {
XMLName xml.Name `json:"-" yaml:"-" xml:"registry"`
Title string `json:"title" yaml:"Title" xml:"title"`
Category string `json:"category,omitempty" yaml:"Category,omitempty" xml:"category,omitempty"`
Created IANADate `json:"created" yaml:"Created" xml:"created"`
Updated *IANADate `json:"updated,omitempty" yaml:"Updated,omitempty" xml:"updated,omitempty"`
Notice *IANARegistryData `json:"notice" yaml:"Notice" xml:"registry"`
Footnotes []*IANARegistryFootnote `json:"footnotes,omitempty" yaml:"Footnotes,omitempty" xml:"footnote,omitempty"`
}
type IANAPrefix struct {
// IANA may include multiple prefixes in the same record.
Prefixes []*netip.Prefix `json:"prefix" yaml:"prefix" xml:"prefixes>prefix,attr"`
References []*IANARef `json:"refs,omitempty" yaml:"References,omitempty" xml:"refs,omitempty"`
}
type IANAString struct {
Text string `json:"text" yaml:"Text" xml:"text"`
References []*IANARef `json:"refs" yaml:"References" xml:"references"`
}
type IANARegistryFootnote struct {
XMLName xml.Name `json:"-" yaml:"-" xml:"footnote"`
ReferenceIdx uint `json:"ref" yaml:"Reference Index/ID" xml:"anchor,attr"`
Note *IANAString `json:"note" yaml:"Note" xml:"node"`
}
type IANARegistryData struct {
XMLName xml.Name `json:"-" yaml:"-" xml:"registry"`
ID string `json:"id" yaml:"ID" xml:"id,attr"`
Title string `json:"title" yaml:"Title" xml:"title"`
Refs []*IANARef `json:"refs" yaml:"Referencess" xml:"xref"`
Rule string `json:"rule" yaml:"Registration Rule" xml:"registration_rule"`
Note *IANAString `json:"note,omitempty" yaml:"Note,omitempty" xml:"note,omitempty"`
Records []*IANAAddrNetResRecord `json:"records,omitempty" yaml:"Records,omitempty" xml:"record,omitempty"`
}
// IANARef is used to hold references to RFCs etc.
type IANARef struct {
XMLName xml.Name `json:"-" yaml:"-" xml:"xref"`
Type string `json:"type" yaml:"Type" xml:"type,attr"`
// TODO: This may have inner XML.
Reference string `json:"ref" yaml:"Reference ID" xml:"data,attr"`
}
type IANABool struct {
Applicable *bool `json:"applicable,omitempty" yaml:"Is Applicable,omitempty" xml:"applicable,attr,omitempty"`
Evaluated *bool `json:"bool,omitempty" yaml:"As Boolean,omitempty" xml:"evaluated,attr,omitempty"`
}
type IANAAddrNetResRecord struct {
XMLName xml.Name `json:"-" yaml:"-" xml:"record"`
Updated *IANADate `json:"updated,omitempty" xml:"updated,omitempty"`
Networks *IANAPrefix `json:"net,omitempty" yaml:"Address/Network,omitempty" xml:"address,omitempty"`
Name string `json:"name" yaml:"Name" xml:"name"`
// TODO: This has inner XML.
Spec *IANAString `json:"spec" yaml:"Spec" xml:"spec"`
Allocation IANADate `json:"alloc" yaml:"Allocation Month" xml:"allocation"`
Termination *IANADate `json:"term,omitempty" yaml:"Termination,omitempty" xml:"termination,omitempty"`
Source *IANABool `json:"source,omitempty" yaml:"Is Source,omitempty" xml:"source,omitempty"`
Dest *IANABool `json:"dest,omitempty" yaml:"Is Destination,omitempty" xml:"dest,omitempty"`
Forwardable *IANABool `json:"forwardable,omitempty" yaml:"Is Forwardable,omitempty" xml:"forwardable,omitempty"`
GlobalReach *IANABool `json:"global,omitempty" yaml:"Is Globally Reachable,omitempty" xml:"global,omitempty"`
ProtoReserved *IANABool `json:"reserved,omitempty" yaml:"Is Reserved by Protocol,omitempty" xml:"reserved,omitempty"`
}