From 860ad5842b6f17a5f4aa81854eedd02f5979bedd Mon Sep 17 00:00:00 2001 From: brent saner Date: Sun, 13 Apr 2025 18:25:32 -0400 Subject: [PATCH] v0.2.5 ADDED: * num-addrs subcommand, to get the number of hosts/addresses in a given prefix length * get-net subcommand, to more easily get a single subnet from either the beginning or the end of a prefix. (MUCH FASTER than CIDR-splitting!) --- TODO | 2 + cmd/subnetter/args.go | 19 +++- cmd/subnetter/funcs.go | 5 +- cmd/subnetter/funcs_tpl.go | 5 +- cmd/subnetter/main.go | 26 ++++++ go.mod | 9 +- go.sum | 32 ------- netsplit/conts.go | 9 +- netsplit/errs.go | 1 + netsplit/funcs.go | 144 +++++++++++++++++++++++++++---- netsplit/funcs_cidrsplitter.go | 13 +++ netsplit/funcs_hostsplitter.go | 4 + netsplit/funcs_prefixgetter.go | 102 ++++++++++++++++++++++ netsplit/funcs_subnetsplitter.go | 4 + netsplit/funcs_vlsmsplitter.go | 14 +-- netsplit/types.go | 23 +++-- 16 files changed, 334 insertions(+), 78 deletions(-) create mode 100644 netsplit/funcs_prefixgetter.go diff --git a/TODO b/TODO index cd28efe..1494721 100644 --- a/TODO +++ b/TODO @@ -3,3 +3,5 @@ - when checking/rendering reserved networks, currently the footnotes aren't returned. -- netsplit.IANARegistryFootnote -- encapsulated in the IANARegistry.Footnotes + +- add new interface, Getter? different formatting? \ No newline at end of file diff --git a/cmd/subnetter/args.go b/cmd/subnetter/args.go index 22db66e..dd70978 100644 --- a/cmd/subnetter/args.go +++ b/cmd/subnetter/args.go @@ -2,11 +2,13 @@ package main type Args struct { Version verArgs `command:"version" alias:"v" description:"Show version information." validate:"omitempty"` - SplitCIDR SplitCIDRArgs `command:"split-cidr" alias:"sc" description:"Split a network into as many equal subnets of prefix size N as possible." validate:"omitempty"` + GetPfx GetPfxArgs `command:"get-net" alias:"gn" description:"Get a single subnet from a network at either the beginning or end. (You'll probably want to enable -r/--no-remaining.)" validate:"omitempty"` + SplitCIDR SplitCIDRArgs `command:"split-cidr" alias:"sc" description:"Split a network into as many equal subnets of prefix size N as possible. (This may have issues/take a very long time if you are splitting a very large network into many very small subnets.)" validate:"omitempty"` SplitHost SplitHostArgs `command:"split-hosts" alias:"sh" description:"Split a network into N total number of hosts *per subnet* as cleanly/evenly as possible." validate:"omitempty"` SplitSubnets SplitSubnetArgs `command:"split-nets" alias:"sn" description:"Split a network into N number of subnets as cleanly as possible." validate:"omitempty"` VLSM VLSMArgs `command:"split-vlsm" alias:"sv" alias:"vlsm" description:"Use VLSM (Variable-Length Subnet Masks) to split a network into differently sized subnets." validate:"omitempty"` ExplicitNetwork XNetArgs `command:"net" alias:"xn" alias:"net" description:"Print information about an explicit network address." validate:"omitempty"` + NumAddrs NAddrArgs `command:"num-addrs" alias:"na" alias:"addrs" description:"Return the number of addresses/hosts in a given network size. (This saves needing to lookup from the table subcommand.)" validate:"omitempty"` NumNets NNetArgs `command:"num-nets" alias:"nn" alias:"nets" description:"Return the number of subnets of a given size that can fit into a given network size. This is MUCH, MUCH FASTER than splitting (if you do not need addressing)." validate:"omitempty"` Parse ParseArgs `command:"parse" alias:"p" alias:"read" alias:"convert" description:"Parse/convert output from a previous subnetter run." validate:"omitempty"` Table TableArgs `command:"table" alias:"t" alias:"tab" alias:"tbl" description:"Show prefix summaries (by default both IPv4 and IPv6)." validate:"omitempty"` @@ -49,6 +51,21 @@ type cacheArgs struct { DoResCache bool `short:"c" long:"cache-reservations" env:"SBNTR_RSVCACHE" description:"Enable caching/cache lookup for reservation data."` } +type GetPfxArgs struct { + Position string `short:"P" long:"position" choice:"first" choice:"last" default:"first" description:"The position of the subnet within the network." validate:"required,oneof=first last"` + Prefix uint8 `short:"s" long:"size" required:"true" description:"Prefix length/network size in bits (as CIDR number)." validate:"required"` + splitArgs +} + +type NAddrArgs struct { + Isv6 bool `short:"6" long:"v6" description:"If the prefix given is <=32, specify this flag to indicate that it is an IPv6 prefix and not IPv4. (If it is >32, it is automatically treated as an IPv6 prefix for obvious reasons.)"` + InclNetAddr bool `short:"N" long:"incl-net" description:"If specified, include the network address in the count."` + InclBcastAddr bool `short:"B" long:"incl-bcast" description:"If specified, include the broadcast/reserved broadcast address in the count."` + Size struct { + PrefixSize uint8 `positional-arg-name:"" required:"1" validate:"lte=128"` + } `positional-args:"yes" required:"1" validate:"required"` +} + type NNetArgs struct { Verbose bool `short:"v" long:"verbose" description:"Be verbose (more ideal for logging)."` NoV6Check bool `short:"6" long:"no-v6" description:"If specified, do not indicate if the subnetting is IPv6 only (true) or not (false; dual-stack/IPv4 supported)."` diff --git a/cmd/subnetter/funcs.go b/cmd/subnetter/funcs.go index c59c434..5f54a83 100644 --- a/cmd/subnetter/funcs.go +++ b/cmd/subnetter/funcs.go @@ -15,7 +15,6 @@ import ( "time" "github.com/goccy/go-yaml" - "github.com/projectdiscovery/mapcidr" "go4.org/netipx" "r00t2.io/subnetter/netsplit" "r00t2.io/subnetter/version" @@ -159,8 +158,8 @@ func printMask(label string, pfx netip.Prefix, verb, indent int, indentStr strin fmt.Fprintf(sb, "%sBits:\t\t%d\n", pre2, pfx.Bits()) fmt.Fprintf(sb, "%sFirst:\t\t%s\n", pre2, first.String()) fmt.Fprintf(sb, "%sLast:\t\t%s\n", pre2, last.String()) - fmt.Fprintf(sb, "%sAddresses:\t%d\n", pre2, mapcidr.CountIPsInCIDR(true, true, netipx.PrefixIPNet(pfx.Masked()))) - fmt.Fprintf(sb, "%sHosts:\t\t%d\n", pre2, mapcidr.CountIPsInCIDR(false, false, netipx.PrefixIPNet(pfx.Masked()))) + fmt.Fprintf(sb, "%sAddresses:\t%d\n", pre2, netsplit.NumAddrsPfx(pfx.Masked(), true, true)) + fmt.Fprintf(sb, "%sHosts:\t\t%d\n", pre2, netsplit.NumAddrsPfx(pfx.Masked(), false, false)) if verb >= 2 { fmt.Fprintf(sb, "%sExpanded:\t%s\n", pre2, netsplit.MaskExpand(mask, pfx.Addr().Is6())) fmt.Fprintf(sb, "%sHex:\t\t0x%s\n", pre2, mask.String()) diff --git a/cmd/subnetter/funcs_tpl.go b/cmd/subnetter/funcs_tpl.go index fb7e02f..e3db217 100644 --- a/cmd/subnetter/funcs_tpl.go +++ b/cmd/subnetter/funcs_tpl.go @@ -10,7 +10,6 @@ import ( `strings` `github.com/TwiN/go-color` - `github.com/projectdiscovery/mapcidr` `go4.org/netipx` `r00t2.io/subnetter/netsplit` ) @@ -423,8 +422,8 @@ func tplTablePrefixes(ipVer uint8, indent string, plain bool) (out string, err e return } dummyNet = netipx.PrefixIPNet(rows[idx].NetPrefix.Masked()) - rows[idx].Addresses = mapcidr.CountIPsInCIDR(true, true, dummyNet) - rows[idx].Hosts = mapcidr.CountIPsInCIDR(false, false, dummyNet) + rows[idx].Addresses = netsplit.NumAddrsNet(dummyNet, true, true) + rows[idx].Hosts = netsplit.NumAddrsNet(dummyNet, false, false) } colFields, colTitles, colSizes = sizeStructs(rows) diff --git a/cmd/subnetter/main.go b/cmd/subnetter/main.go index 2594f73..e89e832 100644 --- a/cmd/subnetter/main.go +++ b/cmd/subnetter/main.go @@ -6,6 +6,7 @@ import ( "fmt" "io" "log" + `math/big` "net" "net/netip" "os" @@ -34,6 +35,7 @@ func main() { var buf *bytes.Buffer var res *netsplit.StructuredResults var numNets uint + var numAddrs *big.Int var v6Only bool var noStrict bool var strictErr error @@ -84,6 +86,18 @@ func main() { log.Panicln(err) } return + case "num-addrs": + if err = validate.Struct(args.NumAddrs); err != nil { + log.Panicln(err) + } + if args.NumAddrs.Size.PrefixSize > 32 { + args.NumAddrs.Isv6 = true + } + if numAddrs, err = netsplit.NumAddrsIn(args.NumAddrs.Size.PrefixSize, args.NumAddrs.Isv6, args.NumAddrs.InclNetAddr, args.NumAddrs.InclBcastAddr); err != nil { + log.Panicln(err) + } + fmt.Println(numAddrs.String()) + return case "num-nets": if err = validate.Struct(args.NumNets); err != nil { log.Panicln(err) @@ -195,6 +209,18 @@ func main() { These are all handily-dandily enclosed in a `common` struct type. */ switch parser.Active.Name { + case "get-net": // Not a *true* splitter, per se; splitting is required but only for functional reasons. + if err = validate.Struct(args.GetPfx); err != nil { + log.Panicln(err) + } + cmnArgs = args.GetPfx.common + splitter = &netsplit.PrefixGetter{ + Pos: args.GetPfx.Position, + PrefixLength: args.GetPfx.Prefix, + BaseSplitter: new(netsplit.BaseSplitter), + } + noStrict = false + strictErr = netsplit.ErrBadNumHosts // dummy case "split-hosts": if err = validate.Struct(args.SplitHost); err != nil { log.Panicln(err) diff --git a/go.mod b/go.mod index d4fcd6c..242a766 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,6 @@ require ( github.com/go-resty/resty/v2 v2.16.5 github.com/goccy/go-yaml v1.15.23 github.com/jessevdk/go-flags v1.6.1 - github.com/projectdiscovery/mapcidr v1.1.34 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba golang.org/x/mod v0.24.0 r00t2.io/goutils v1.8.1 @@ -16,18 +15,12 @@ require ( ) require ( - github.com/aymerick/douceur v0.2.0 // indirect github.com/djherbis/times v1.6.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/gorilla/css v1.0.1 // indirect github.com/leodido/go-urn v1.4.0 // indirect - github.com/microcosm-cc/bluemonday v1.0.27 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/projectdiscovery/blackrock v0.0.1 // indirect - github.com/projectdiscovery/utils v0.4.14 // indirect - github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect + github.com/stretchr/testify v1.9.0 // indirect golang.org/x/crypto v0.36.0 // indirect golang.org/x/net v0.37.0 // indirect golang.org/x/sync v0.12.0 // indirect diff --git a/go.sum b/go.sum index 8396365..2aa67a1 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc= github.com/TwiN/go-color v1.4.1/go.mod h1:WcPf/jtiW95WBIsEeY1Lc/b8aaWoiqQpu5cf8WFxu+s= -github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= -github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -22,58 +20,28 @@ github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ github.com/goccy/go-yaml v1.15.23 h1:WS0GAX1uNPDLUvLkNU2vXq6oTnsmfVFocjQ/4qA48qo= github.com/goccy/go-yaml v1.15.23/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= -github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= -github.com/gorilla/css v1.0.1 h1:ntNaBIghp6JmvWnxbZKANoLyuXTPZ4cAMlo6RyhlbO8= -github.com/gorilla/css v1.0.1/go.mod h1:BvnYkspnSzMmwRK+b8/xgNPLiIuNZr6vbZBTPQ2A3b0= github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= -github.com/microcosm-cc/bluemonday v1.0.25 h1:4NEwSfiJ+Wva0VxN5B8OwMicaJvD8r9tlJWm9rtloEg= -github.com/microcosm-cc/bluemonday v1.0.25/go.mod h1:ZIOjCQp1OrzBBPIJmfX4qDYFuhU02nx4bn030ixfHLE= -github.com/microcosm-cc/bluemonday v1.0.27 h1:MpEUotklkwCSLeH+Qdx1VJgNqLlpY2KXwXFM08ygZfk= -github.com/microcosm-cc/bluemonday v1.0.27/go.mod h1:jFi9vgW+H7c3V0lb6nR74Ib/DIB5OBs92Dimizgw2cA= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/projectdiscovery/blackrock v0.0.1 h1:lHQqhaaEFjgf5WkuItbpeCZv2DUIE45k0VbGJyft6LQ= -github.com/projectdiscovery/blackrock v0.0.1/go.mod h1:ANUtjDfaVrqB453bzToU+YB4cUbvBRpLvEwoWIwlTss= -github.com/projectdiscovery/mapcidr v1.1.34 h1:udr83vQ7oz3kEOwlsU6NC6o08leJzSDQtls1wmXN/kM= -github.com/projectdiscovery/mapcidr v1.1.34/go.mod h1:1+1R6OkKSAKtWDXE9RvxXtXPoajXTYX0eiEdkqlhQqQ= -github.com/projectdiscovery/utils v0.0.85 h1:JpCVc9GJwJLNHy1MBPmAHJcE6rs7bRv72Trb3u84OHE= -github.com/projectdiscovery/utils v0.0.85/go.mod h1:ttiPgS2LmLFd+VRBUdgfLKMMdrF98zX7z5W+K71MX40= -github.com/projectdiscovery/utils v0.4.14 h1:BrEfO4f4P+Hu58jNfjho2aRt/Y4jxKhTVQqs2Ei4670= -github.com/projectdiscovery/utils v0.4.14/go.mod h1:y5gnpQn802iEWqf0djTRNskJlS62P5eqe1VS1+ah0tk= -github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= -github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= diff --git a/netsplit/conts.go b/netsplit/conts.go index 067c086..ebb5ecc 100644 --- a/netsplit/conts.go +++ b/netsplit/conts.go @@ -5,6 +5,7 @@ import ( `net/netip` `sync` + `github.com/go-playground/validator/v10` `github.com/go-resty/resty/v2` ) @@ -39,8 +40,12 @@ var ( ) var ( - // TODO - cacheLock sync.RWMutex + // validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled(), validator.WithPrivateFieldValidation()) + validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled()) +) + +var ( + cacheLock sync.RWMutex // TODO 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 diff --git a/netsplit/errs.go b/netsplit/errs.go index 7281d84..cb8d5af 100644 --- a/netsplit/errs.go +++ b/netsplit/errs.go @@ -11,4 +11,5 @@ var ( ErrBadSplitter error = errors.New("invalid or unknown splitter when containing") ErrBigPrefix error = errors.New("prefix length exceeds remaining network space") ErrNoNetSpace error = errors.New("reached end of network space before splitting finished") + ErrUnknownPos error = errors.New("unknown subnet position") ) diff --git a/netsplit/funcs.go b/netsplit/funcs.go index de84a51..60ad020 100644 --- a/netsplit/funcs.go +++ b/netsplit/funcs.go @@ -1,6 +1,7 @@ package netsplit import ( + `bytes` "encoding/json" "encoding/xml" "fmt" @@ -298,6 +299,113 @@ func Contain(origPfx *netip.Prefix, nets []*netip.Prefix, remaining *netipx.IPSe return } +/* +LastSubnetNet returns the last subnet of prefix length pfxLen from network pfx. + +lastSubnet will be nil if pfx is nil or invalid. +*/ +func LastSubnetNet(pfx *net.IPNet, pfxLen uint8) (lastSubnet *net.IPNet, err error) { + + var nPfx netip.Prefix + var ok bool + + if pfx == nil { + return + } + if nPfx, ok = netipx.FromStdIPNet(pfx); !ok { + return + } + if !nPfx.IsValid() { + return + } + + if nPfx, err = LastSubnetPfx(nPfx, pfxLen); err != nil { + return + } + if !nPfx.IsValid() { + return + } + + lastSubnet = netipx.PrefixIPNet(nPfx) + + return +} + +// LastSubnetPfx is exactly like LastSubnetNet but using netip.Prefix instead. +func LastSubnetPfx(pfx netip.Prefix, pfxLen uint8) (lastSubnet netip.Prefix, err error) { + + var ok bool + var pfxBytes []byte + var bitOffset uint + var offset *big.Int + var ipInt *big.Int + var ipBytes []byte + var byteLen int + var lastNet *net.IPNet + var maxBitLen uint8 = maxBitsv4 + + if !pfx.IsValid() { + return + } + + pfx = pfx.Masked() + + if pfx.Addr().Is6() || pfxLen > maxBitsv4 { + maxBitLen = maxBitsv6 + } + if pfxLen > maxBitLen { + err = ErrBadPrefixLen + return + } + if pfxLen < uint8(pfx.Bits()) { + err = ErrBigPrefix + return + } + + if pfxBytes, err = pfx.Addr().MarshalBinary(); err != nil { + return + } + + byteLen = pfx.Addr().BitLen() / 8 + bitOffset = uint(pfxLen - uint8(pfx.Bits())) + + offset = new(big.Int).Lsh(big.NewInt(1), bitOffset) + offset.Sub(offset, big.NewInt(1)) + + // Cast the prefix (as represented as bytes) into a *big.Int for some number magic. + ipInt = new(big.Int).SetBytes(pfxBytes) + // Shift to the first "address" in the prefix/mask it. + offset.Lsh(offset, uint(pfx.Addr().BitLen()-int(pfxLen))) + // Now add the offset to the base network. + ipInt.Add(ipInt, offset) + + // If the base address starts at the "0 address" (e.g. 0.0.0.0 or :: etc....), + // this can cause some strange behavior when casting the *big.Int to bytes. + // So it gets left-null-padded to the appropriate length for the inet family. + ipBytes = ipInt.Bytes() + if len(ipBytes) < byteLen { + ipBytes = append( + bytes.Repeat([]byte{0x00}, byteLen-len(ipBytes)), + ipBytes..., + ) + } + + // Create an explicit net.IPNet... + lastNet = &net.IPNet{ + IP: net.IP(ipBytes), + Mask: net.CIDRMask(int(pfxLen), pfx.Addr().BitLen()), + } + // And then make it a netip.Prefix. + if lastSubnet, ok = netipx.FromStdIPNet(lastNet); !ok { + return + } + if !lastSubnet.IsValid() { + return + } + + return +} + /* MaskExpand expands a net.IPMask's string format. Like AddrExpand but for netmasks. @@ -391,29 +499,29 @@ func MaskInvert(mask net.IPMask) (inverted net.IPMask) { Note that for the single-host prefix (/32 for IPv4, /128 for IPv6), numAddrs will *always* be 1. For point-to-point prefix (IPv4 /31, IPv6 /127), numAddrs will *ALWAYS* be 2. */ -func NumAddrsIn(prefixLen uint8, isIpv6, inclNet, inclBcast bool) (numAddrs *big.Int, err error) { +func NumAddrsIn(pfxLen uint8, isIpv6, inclNet, inclBcast bool) (numAddrs *big.Int, err error) { var numBits uint var numRemoved int64 var maxBitLen uint8 = maxBitsv4 - if isIpv6 { + if isIpv6 || pfxLen > maxBitsv4 { maxBitLen = maxBitsv6 } - if prefixLen > maxBitLen { + if pfxLen > maxBitLen { err = ErrBadPrefixLen return } - if prefixLen == maxBitLen { + if pfxLen == maxBitLen { numAddrs = big.NewInt(1) return } - if (prefixLen + 1) == maxBitLen { + if (pfxLen + 1) == maxBitLen { numAddrs = big.NewInt(2) return } - numBits = uint(maxBitLen - prefixLen) + numBits = uint(maxBitLen - pfxLen) numAddrs = new(big.Int).Lsh(big.NewInt(1), numBits) @@ -451,6 +559,9 @@ func NumAddrsNet(pfx *net.IPNet, inclNet, inclBcast bool) (numAddrs *big.Int) { if nPfx, ok = netipx.FromStdIPNet(pfx); !ok { return } + if !nPfx.IsValid() { + return + } numAddrs = NumAddrsPfx(nPfx, inclNet, inclBcast) @@ -460,21 +571,16 @@ func NumAddrsNet(pfx *net.IPNet, inclNet, inclBcast bool) (numAddrs *big.Int) { // NumAddrsPfx is the exact same as NumAddrsNet but for a net/netip.Prefix instead. func NumAddrsPfx(pfx netip.Prefix, inclNet, inclBcast bool) (numAddrs *big.Int) { - var numBits uint - var numRemoved int64 + var err error - numBits = uint(pfx.Addr().BitLen() - pfx.Bits()) - - numAddrs = new(big.Int).Lsh(big.NewInt(1), numBits) - - if !inclNet { - numRemoved++ + if !pfx.IsValid() { + return } - if !inclBcast { - numRemoved++ - } - if numRemoved > 0 { - _ = numAddrs.Sub(numAddrs, big.NewInt(numRemoved)) + + // Since we're dealing with existing prefixes/networks, we should never get an error. + if numAddrs, err = NumAddrsIn(uint8(pfx.Bits()), pfx.Addr().Is6(), inclNet, inclBcast); err != nil { + // But *somehow* in case we do... + panic(err) } return diff --git a/netsplit/funcs_cidrsplitter.go b/netsplit/funcs_cidrsplitter.go index 31cb047..e71fbb1 100644 --- a/netsplit/funcs_cidrsplitter.go +++ b/netsplit/funcs_cidrsplitter.go @@ -22,6 +22,10 @@ func (c *CIDRSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e return } + if err = validate.Struct(c); err != nil { + return + } + if base, ok = netipx.FromStdIPNet(c.network); !ok { err = ErrBadBoundary return @@ -41,6 +45,15 @@ func (c *CIDRSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e return } + /* + TODO: This is *super* slow for tiny subnets in a large net. + For future optimzation, first off benchmark to make sure it makes a difference, but + chunk out the network (how to find appropriate length?) of n < x < y, where n is the network pfx len, + and goroutine each split into a channel. + Because this splits *on CIDR boundaries* and we aren't VLSM-ing, remaining never has to be considered- + it'll always be clean splitting. + See CIDRSplitter.LenSwitch. + */ for { if sub, remaining, ok = remaining.RemoveFreePrefix(c.PrefixLength); !ok { if !sub.IsValid() { diff --git a/netsplit/funcs_hostsplitter.go b/netsplit/funcs_hostsplitter.go index 15073fc..4253e51 100644 --- a/netsplit/funcs_hostsplitter.go +++ b/netsplit/funcs_hostsplitter.go @@ -28,6 +28,10 @@ func (h *HostSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e return } + if err = validate.Struct(h); err != nil { + return + } + pfx, _ = netipx.FromStdIPNet(h.network) tgt = new(big.Int) tgt.SetUint64(uint64(h.NumberHosts)) diff --git a/netsplit/funcs_prefixgetter.go b/netsplit/funcs_prefixgetter.go new file mode 100644 index 0000000..1fd1027 --- /dev/null +++ b/netsplit/funcs_prefixgetter.go @@ -0,0 +1,102 @@ +package netsplit + +import ( + `fmt` + `net/netip` + + `go4.org/netipx` +) + +// Split is to conform to a NetSplitter, though a PrefixGetter is *technically* not a splitter. +func (p *PrefixGetter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) { + + var ok bool + var base netip.Prefix + var vlsmS *VLSMSplitter + var addr netip.Addr + var maxPfxLen uint8 = maxBitsv4 + var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder) + + if p == nil || p.PrefixLength == 0 || p.BaseSplitter == nil || p.network == nil { + return + } + + if err = validate.Struct(p); err != nil { + return + } + + // If the position is "first", we can simply call a VLSM. + if p.Pos == "first" { + vlsmS = &VLSMSplitter{ + Ascending: false, + Explicit: false, + PrefixLengths: []uint8{p.PrefixLength}, + BaseSplitter: p.BaseSplitter, + } + if nets, remaining, err = vlsmS.Split(); err != nil { + return + } + return + } + + if p.Pos != "last" { + err = ErrUnknownPos + return + } + + // Otherwise this gets... messy. + if base, ok = netipx.FromStdIPNet(p.network); !ok { + err = ErrBadBoundary + return + } + if !base.IsValid() { + err = ErrBadBoundary + return + } + ipsb = new(netipx.IPSetBuilder) + ipsb.AddPrefix(base.Masked()) + + // First if it's a single host prefix, ezpz gg no re. + if base.Addr().Is6() { + maxPfxLen = maxBitsv6 + } + if p.PrefixLength == maxPfxLen { + nets = make([]*netip.Prefix, 1) + nets[0] = new(netip.Prefix) + addr = netipx.PrefixLastIP(base) + fmt.Println(addr.String()) + if *nets[0], err = addr.Prefix(int(p.PrefixLength)); err != nil { + return + } + ipsb.Remove(addr) + if remaining, err = ipsb.IPSet(); err != nil { + return + } + return + } + + // Otherwise this gets... interesting. + /* + For IPv4, performance NORMALLY would be "fine" on modern hardware with: + 1. straight CIDR-splitting + 2. grabbing the last prefix + 3. condensing the leading prefixes to a new IPSet + But even this can take a long time (see CIDRSplitter.Split comments). + + In almost all cases (unless subnetting like, n+12 prefix length), + IPv6 takes WAY too long. + + So use the same function (LastSubnetPfx) for both cases. + */ + nets = make([]*netip.Prefix, 1) + nets[0] = new(netip.Prefix) + if *nets[0], err = LastSubnetPfx(base, p.PrefixLength); err != nil { + return + } + ipsb.RemovePrefix(*nets[0]) + if remaining, err = ipsb.IPSet(); err != nil { + return + } + + return +} diff --git a/netsplit/funcs_subnetsplitter.go b/netsplit/funcs_subnetsplitter.go index 47ebead..4e78311 100644 --- a/netsplit/funcs_subnetsplitter.go +++ b/netsplit/funcs_subnetsplitter.go @@ -27,6 +27,10 @@ func (s *SubnetSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, return } + if err = validate.Struct(s); err != nil { + return + } + if base, ok = netipx.FromStdIPNet(s.network); !ok { err = ErrBadBoundary return diff --git a/netsplit/funcs_vlsmsplitter.go b/netsplit/funcs_vlsmsplitter.go index 94704ba..31032be 100644 --- a/netsplit/funcs_vlsmsplitter.go +++ b/netsplit/funcs_vlsmsplitter.go @@ -21,7 +21,15 @@ func (v *VLSMSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e var base netip.Prefix var sub netip.Prefix var subPtr *netip.Prefix - var ipsb = new(netipx.IPSetBuilder) + var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder) + + if v == nil || v.PrefixLengths == nil || len(v.PrefixLengths) == 0 || v.BaseSplitter == nil || v.network == nil { + return + } + + if err = validate.Struct(v); err != nil { + return + } if err = ValidateSizes(v.network, v.PrefixLengths...); err != nil { return @@ -38,10 +46,6 @@ func (v *VLSMSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e But, as I expected, netipx ftw again. */ - if v == nil || v.PrefixLengths == nil || len(v.PrefixLengths) == 0 || v.BaseSplitter == nil || v.network == nil { - return - } - if !v.Explicit { sort.SliceStable( v.PrefixLengths, diff --git a/netsplit/types.go b/netsplit/types.go index 7115b75..34a03d5 100644 --- a/netsplit/types.go +++ b/netsplit/types.go @@ -31,7 +31,16 @@ type NetSplitter interface { // BaseSplitter is used to encapsulate the "parent" network to be split. type BaseSplitter struct { - network *net.IPNet + network *net.IPNet `validate:"required"` +} + +// PrefixGetter is a "pseudo-splitter"; it splits according to a given prefix but only returns a specfic subnet (either the first or the last). +type PrefixGetter struct { + // Pos is the position in BaseSplitter.network for the selected PrefixLength. + Pos string `json:"pos" xml:"pos,attr" yaml:"Position" validate:"required,oneof=first last"` + // PrefixLength specifies the CIDR/prefix length of the subnet. + PrefixLength uint8 `json:"prefix" xml:"prefix,attr" yaml:"network Prefix Length" validate:"required,lte=128"` + *BaseSplitter } /* @@ -40,7 +49,11 @@ It attemps to split the network into as many networks of size PrefixLength as cl */ type CIDRSplitter struct { // PrefixLength specifies the CIDR/prefix length of the subnets to split out. - PrefixLength uint8 `json:"prefix" xml:"prefix,attr" yaml:"network Prefix Length"` + PrefixLength uint8 `json:"prefix" xml:"prefix,attr" yaml:"network Prefix Length" validate:"required,lte=128"` + // TODO: See CIDRSplitter.Split for future optimization using this. + // LenSwitch specifies the threshold bit offset after which (inclusive) it switches from a repeated prefix removal to manual recursive binary split. + // If 0, 12 is the default. + // LenSwitch uint8 `json:"switch_at" xml:"switchAt,attr" yaml:"Switch Offset Threshold"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"` } @@ -50,7 +63,7 @@ It attempts to evenly distribute addresses amoungs subnets. */ type HostSplitter struct { // NumberHosts is the number of hosts to be placed in each subnet to split out. - NumberHosts uint `json:"hosts" xml:"hosts,attr" yaml:"Number of Hosts Per Subnet"` + NumberHosts uint `json:"hosts" xml:"hosts,attr" yaml:"Number of Hosts Per Subnet" validate:"required"` // InclNetAddr, if true, specifies that NumberHosts includes the network address. InclNetAddr bool `json:"net_addr" xml:"netAddr,attr,omitempty" yaml:"Network Address Included,omitempty"` // InclBcastAddr, if true, specifies that NumberHosts includes the broadcast address. @@ -66,7 +79,7 @@ as cleanly as poossible. */ type SubnetSplitter struct { // NumberSubnets indicates the number of subnets to split the network into. - NumberSubnets uint `json:"nets" xml:"nets,attr" yaml:"Number of Target Subnets"` + NumberSubnets uint `json:"nets" xml:"nets,attr" yaml:"Number of Target Subnets" validate:"required"` // Strict, if true, will return an error from Split if the network sizes cannot split into equally-sized networks. Strict bool `json:"strict" xml:"strict,attr,omitempty" yaml:"Strictly Equal Subnet Sizes"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"` @@ -91,7 +104,7 @@ type VLSMSplitter struct { */ Explicit bool `json:"explicit,omitempty" xml:"explicit,attr,omitempty" yaml:"Explicit Ordering,omitempty"` // PrefixLengths contains the prefix lengths of each subnet to split out from the network. - PrefixLengths []uint8 `json:"prefixes" xml:"prefixes>prefix" yaml:"Prefix Lengths"` + PrefixLengths []uint8 `json:"prefixes" xml:"prefixes>prefix" yaml:"Prefix Lengths" validate:"required,dive,lte=128"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"` }