checking in- needs some refinement then done
This commit is contained in:
@@ -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)",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
383
netsplit/funcs_cache.go
Normal 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
|
||||
}
|
||||
47
netsplit/funcs_ianabool.go
Normal file
47
netsplit/funcs_ianabool.go
Normal 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
|
||||
}
|
||||
53
netsplit/funcs_ianadate.go
Normal file
53
netsplit/funcs_ianadate.go
Normal 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
|
||||
}
|
||||
65
netsplit/funcs_ianaprefix.go
Normal file
65
netsplit/funcs_ianaprefix.go
Normal 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
|
||||
}
|
||||
27
netsplit/funcs_ianaregistryfootnote.go
Normal file
27
netsplit/funcs_ianaregistryfootnote.go
Normal 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
|
||||
}
|
||||
51
netsplit/funcs_ianastring.go
Normal file
51
netsplit/funcs_ianastring.go
Normal 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
|
||||
}
|
||||
@@ -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
37
netsplit/tcache_test.go
Normal 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
55
netsplit/tiana_test.go
Normal 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
|
||||
}
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user