From 3c1bc832c0c70718bfe6471e3d8c8d45b25800e9 Mon Sep 17 00:00:00 2001 From: brent saner Date: Sun, 6 Apr 2025 18:26:18 -0400 Subject: [PATCH] v0.2.1 FIXED: * host splitter wasn't working quite correctly; this has been fixed. --- TODO | 2 - cmd/subnetter/args.go | 18 ++-- cmd/subnetter/funcs.go | 10 ++- cmd/subnetter/main.go | 23 ++++- netsplit/funcs.go | 153 +++++++++++++++++++++++++-------- netsplit/funcs_hostsplitter.go | 97 +++++++++++++-------- netsplit/types.go | 8 +- 7 files changed, 221 insertions(+), 90 deletions(-) diff --git a/TODO b/TODO index 49a306d..cd28efe 100644 --- a/TODO +++ b/TODO @@ -3,5 +3,3 @@ - when checking/rendering reserved networks, currently the footnotes aren't returned. -- netsplit.IANARegistryFootnote -- encapsulated in the IANARegistry.Footnotes - -- split-hosts returns the entire original subnet as an unallocated subnet if hosts exceed the number of assignable addrs. diff --git a/cmd/subnetter/args.go b/cmd/subnetter/args.go index 7573efd..0d97e35 100644 --- a/cmd/subnetter/args.go +++ b/cmd/subnetter/args.go @@ -2,8 +2,8 @@ package main type Args struct { Version verArgs `command:"version" alias:"v" description:"Show version information." validate:"omitempty"` - SplitCIDR SplitCIDRArgs `command:"split-cidr" alias:"se" description:"Split a network into as many equal subnets of prefix size N as possible." 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. (VERY easy to run out of memory for IPv6 prefixes; be sure to specify very small network!)" 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"` + 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"` @@ -36,8 +36,8 @@ type common struct { type reservedArgs struct { NoRecursive bool `short:"u" long:"no-recursive" description:"Don't show reservations that are children subnets of the subnet(s). Only if -f/--format=pretty, always false for other formats."` - NoRevRecursive bool `short:"U" long:"no-rev-recursive" description:"Don't show reservations that are parents of the subnet(s). Only if -f/--format=pretty, always false for other formats."` - NoPrivate bool `short:"e" long:"no-private" description:"Consider private subnets of the subnet(s) to be reserved. If you are subnetting private address space, you probably want to leave this disabled. Only if -f/--format=pretty, always true otherwise."` + NoRevRecursive bool `short:"U" long:"no-rev-recursive" description:"Don't show reservations that are parents of the subnet(s) -- you almost definitely don't want to suppress this. Only if -f/--format=pretty, always false for other formats."` + NoPrivate bool `short:"e" long:"no-private" description:"Consider private subnets to be reserved. If you are subnetting private address space, you probably want to leave this disabled. Only if -f/--format=pretty, always true otherwise."` } type splitArgs struct { @@ -53,8 +53,8 @@ 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)."` Sizes struct { - SubnetSize uint8 `required:"1" validate:"required,le=128,gtefield=NetworkSize"` - NetworkSize uint8 `required:"1" validate:"required,le=128,ltefield=SubnetSize"` + SubnetSize uint8 `positional-arg-name:"" required:"1" validate:"lte=128,gtefield=NetworkSize"` + NetworkSize uint8 `positional-arg-name:"" required:"1" validate:"lte=128,ltefield=SubnetSize"` } `positional-args:"yes" required:"2" validate:"required"` } @@ -69,8 +69,10 @@ type SplitCIDRArgs struct { } type SplitHostArgs struct { - Strict bool `short:"t" long:"strict" description:"If specified, an error will occur if the number of hosts/assignable addresses in a subnet is not exactly -n/--num-hosts."` - Hosts uint `short:"n" long:"num-hosts" required:"true" description:"Number of hosts (usable addresses) per subnet." validate:"required"` + InclNetAddr bool `short:"N" long:"incl-net" description:"If specified, -n/--num-hosts is interpreted to include the network address in the count."` + InclBcastAddr bool `short:"B" long:"incl-bcast" description:"If specified, -n/--num-hosts is interpreted to include the broadcast/reserved broadcast address in the count."` + Strict bool `short:"t" long:"strict" description:"If specified, an error will occur if the number of hosts/assignable addresses in a subnet is not exactly -n/--num-hosts."` + Hosts uint `short:"n" long:"num-hosts" required:"true" description:"Number of hosts (usable addresses) per subnet." validate:"required"` splitArgs } diff --git a/cmd/subnetter/funcs.go b/cmd/subnetter/funcs.go index 843f45b..c59c434 100644 --- a/cmd/subnetter/funcs.go +++ b/cmd/subnetter/funcs.go @@ -401,7 +401,9 @@ func printNets(orig *netip.Prefix, origNet *net.IPNet, nets []*netip.Prefix, rem // Remaining if !args.SuppressRemaining { - if verb >= 1 { + if verb <= 0 { + fmt.Println("#") + } else { fmt.Println() fmt.Println(sectSep1) fmt.Println("Remaining/Left Over/Unallocated:") @@ -660,7 +662,7 @@ func printSplitErr(e *netsplit.SplitErr) { os.Stderr.WriteString("\n!! ERROR !!!\n") os.Stderr.WriteString("\t" + e.Wrapped.Error() + "\n") - os.Stderr.WriteString("\nnetwork Iteration Details\n(when error was encountered):\n\n") + os.Stderr.WriteString("\nNetwork Iteration Details\n(when error was encountered):\n\n") if e.Nets == nil { os.Stderr.WriteString("Nets:\t\t\t(N/A)\n") } else { @@ -669,7 +671,7 @@ func printSplitErr(e *netsplit.SplitErr) { fmt.Fprintf(os.Stderr, "\t%s\n", n.String()) } } - if e.Remaining == nil { + if e.Remaining == nil || e.Remaining.Prefixes() == nil || len(e.Remaining.Prefixes()) == 0 { os.Stderr.WriteString("Remaining:\t\t(N/A)\n") } else { os.Stderr.WriteString("Remaining:\n") @@ -678,7 +680,7 @@ func printSplitErr(e *netsplit.SplitErr) { } } if e.LastSubnet == nil { - os.Stderr.WriteString("Last Subnet:\t\t(N/A)") + os.Stderr.WriteString("Last Subnet:\t\t(N/A)\n") } else { fmt.Fprintf(os.Stderr, "Last Subnet:\t\t%s\n", e.LastSubnet.String()) } diff --git a/cmd/subnetter/main.go b/cmd/subnetter/main.go index 0ea9390..a0744d6 100644 --- a/cmd/subnetter/main.go +++ b/cmd/subnetter/main.go @@ -69,6 +69,9 @@ func main() { return } case "net": + if err = validate.Struct(args.ExplicitNetwork.Network.Network); err != nil { + log.Panicln(err) + } if origPfx, err = netip.ParsePrefix(args.ExplicitNetwork.Network.Network); err != nil { log.Panicln(err) } @@ -82,6 +85,9 @@ func main() { } return case "num-nets": + if err = validate.Struct(args.NumNets); err != nil { + log.Panicln(err) + } if numNets, v6Only, err = netsplit.NumNets( args.NumNets.Sizes.SubnetSize, args.NumNets.Sizes.NetworkSize, @@ -103,6 +109,9 @@ func main() { } return case "reserved": + if err = validate.Struct(args.Check); err != nil { + log.Panicln(err) + } if origPfx, err = netip.ParsePrefix(args.Check.Network.Network); err != nil { log.Panicln(err) } @@ -123,6 +132,9 @@ func main() { } return case "table": + if err = validate.Struct(args.Table); err != nil { + log.Panicln(err) + } // Account for a weird redundant CLI condition. if args.Table.NoIpv4 && args.Table.NoIpv6 { args.Table.NoIpv6 = false @@ -135,6 +147,9 @@ func main() { os.Stdout.Write(buf.Bytes()) return case "parse": + if err = validate.Struct(args.Parse); err != nil { + log.Panicln(err) + } if strings.TrimSpace(args.Parse.InFile) == "-" { buf = new(bytes.Buffer) if _, err = io.Copy(buf, os.Stdin); err != nil { @@ -186,9 +201,11 @@ func main() { } cmnArgs = args.SplitHost.common splitter = &netsplit.HostSplitter{ - NumberHosts: args.SplitHost.Hosts, - Strict: args.SplitHost.Strict, - BaseSplitter: new(netsplit.BaseSplitter), + InclNetAddr: args.SplitHost.InclNetAddr, + InclBcastAddr: args.SplitHost.InclBcastAddr, + NumberHosts: args.SplitHost.Hosts, + Strict: args.SplitHost.Strict, + BaseSplitter: new(netsplit.BaseSplitter), } noStrict = !args.SplitHost.Strict strictErr = netsplit.ErrBadNumHosts diff --git a/netsplit/funcs.go b/netsplit/funcs.go index a9264f7..de84a51 100644 --- a/netsplit/funcs.go +++ b/netsplit/funcs.go @@ -193,25 +193,26 @@ func CheckReserved(nets []*netip.Prefix, revRecursive, recursive, excludePrivate 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 + } + 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 + } + if recursive && n.Bits() < p.Bits() && n.Contains(p.Addr()) { + if reservations == nil { + reservations = make(map[netip.Prefix]*IANAAddrNetResRecord) + } + reservations[p] = r } } } @@ -223,7 +224,7 @@ func CheckReserved(nets []*netip.Prefix, revRecursive, recursive, excludePrivate func Contain(origPfx *netip.Prefix, nets []*netip.Prefix, remaining *netipx.IPSet, splitter NetSplitter) (s *StructuredResults, err error) { var rem []netip.Prefix - var reserved map[netip.Prefix]*IANAAddrNetResRecord + // var reserved map[netip.Prefix]*IANAAddrNetResRecord var sr = StructuredResults{ Original: origPfx, } @@ -276,19 +277,21 @@ 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)) - idx := 0 - for _, r := range reserved { - s.Reservations[idx] = r - idx++ + /* + 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)) + idx := 0 + for _, r := range reserved { + s.Reservations[idx] = r + idx++ + } } } - } + */ s = &sr @@ -377,27 +380,105 @@ func MaskInvert(mask net.IPMask) (inverted net.IPMask) { return } +/* + NumAddrsIn returns the number of addresses in a given prefix length + and inet family. + + If isIpv6 is false, it is assumed to be IPv4 (...duh). + + inclNet and inclBcast have the same meanings as in NumAddrsNet and NumAddrsPfx. + + 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) { + + var numBits uint + var numRemoved int64 + var maxBitLen uint8 = maxBitsv4 + + if isIpv6 { + maxBitLen = maxBitsv6 + } + if prefixLen > maxBitLen { + err = ErrBadPrefixLen + return + } + if prefixLen == maxBitLen { + numAddrs = big.NewInt(1) + return + } + if (prefixLen + 1) == maxBitLen { + numAddrs = big.NewInt(2) + return + } + + numBits = uint(maxBitLen - prefixLen) + + numAddrs = new(big.Int).Lsh(big.NewInt(1), numBits) + + if !inclNet { + numRemoved++ + } + if !inclBcast { + numRemoved++ + } + if numRemoved > 0 { + _ = numAddrs.Sub(numAddrs, big.NewInt(numRemoved)) + } + + return +} + /* NumAddrsNet returns the number of IP addresses in a net.IPNet. -# The network address is included in the count if inclNet is true, otherwise it is excluded +The network address is included in the count if inclNet is true, otherwise it is excluded. -Only assignable addresses ("hosts") are considered if hostsOnly is true, -otherwise all addresses are counted (depending on inclNet). +The broadcast (or reserved broadcast, in the case of IPv6) address will be included in +the count if inclBcast is true, otherwise it is excluded. + +numAddrs will be nil if pfx is nil or invalid. */ -func NumAddrsNet(pfx *net.IPNet, inclNet, hostsOnly bool) (numAddrs *big.Int) { +func NumAddrsNet(pfx *net.IPNet, inclNet, inclBcast bool) (numAddrs *big.Int) { + + var nPfx netip.Prefix + var ok bool if pfx == nil { return } + if nPfx, ok = netipx.FromStdIPNet(pfx); !ok { + return + } - // TODO + numAddrs = NumAddrsPfx(nPfx, inclNet, inclBcast) return } // NumAddrsPfx is the exact same as NumAddrsNet but for a net/netip.Prefix instead. -// TODO +func NumAddrsPfx(pfx netip.Prefix, inclNet, inclBcast bool) (numAddrs *big.Int) { + + var numBits uint + var numRemoved int64 + + numBits = uint(pfx.Addr().BitLen() - pfx.Bits()) + + numAddrs = new(big.Int).Lsh(big.NewInt(1), numBits) + + if !inclNet { + numRemoved++ + } + if !inclBcast { + numRemoved++ + } + if numRemoved > 0 { + _ = numAddrs.Sub(numAddrs, big.NewInt(numRemoved)) + } + + return +} /* NumNets returns the number of times prefix size subnet fits into prefix size network. diff --git a/netsplit/funcs_hostsplitter.go b/netsplit/funcs_hostsplitter.go index e3657ef..15073fc 100644 --- a/netsplit/funcs_hostsplitter.go +++ b/netsplit/funcs_hostsplitter.go @@ -1,12 +1,9 @@ package netsplit import ( - "fmt" "math/big" - "net" "net/netip" - "github.com/projectdiscovery/mapcidr" "go4.org/netipx" ) @@ -16,54 +13,84 @@ This strategy attempts to split the network into subnets of equal number of host remaining may or may not be nil depending on if the number of hosts can fit cleanly within equal network sizes on boundaries. -An ErrBadNumHosts will be returned if the number of hosts does not match the *addressable* range in a prefix. +An ErrBadNumHosts will be returned if the number of hosts does not match the *exact* number of addresses per spec in a prefix. */ func (h *HostSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) { + var pfx netip.Prefix var tgt *big.Int + var splitCidr int var hosts *big.Int - var sub netip.Prefix - var subPtr *netip.Prefix - var split []*net.IPNet - var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder) + var found bool + var cs *CIDRSplitter if h == nil || h.NumberHosts == 0 || h.BaseSplitter == nil || h.network == nil { return } - if split, err = mapcidr.SplitIPNetByNumber(h.network, int(h.NumberHosts)); err != nil { + pfx, _ = netipx.FromStdIPNet(h.network) + tgt = new(big.Int) + tgt.SetUint64(uint64(h.NumberHosts)) + + if NumAddrsPfx(pfx, h.InclNetAddr, h.InclBcastAddr).Cmp(tgt) < 0 { + // The number of hosts per-subnet exceeds the number of addresses in the specified network. + err = ErrNoNetSpace + return + } + /* + Iterate up through prefix lengths for the inet family's maximum length, getting larger and larger, + until we reach the first prefix that can contain tgt. + If we reach h.network.Bits(), we are forced to use that. + (Any case otherwise should be handled by the above checks.) + */ + for splitCidr = pfx.Addr().BitLen(); splitCidr >= pfx.Bits(); splitCidr-- { + if hosts, err = NumAddrsIn(uint8(splitCidr), pfx.Addr().Is6(), h.InclNetAddr, h.InclBcastAddr); err != nil { + return + } + if hosts.Cmp(tgt) >= 0 { + found = true + break + } + } + if !found { + // Pragmatically, we should never be able to get to this code. + err = ErrNoNetSpace return } - fmt.Println(split) - - tgt = big.NewInt(0) - tgt.SetUint64(uint64(h.NumberHosts)) - - nets = make([]*netip.Prefix, len(split)) - for idx, n := range split { - sub, _ = netipx.FromStdIPNet(n) - hosts = mapcidr.CountIPsInCIDR(false, false, n) - if hosts == nil || tgt.Cmp(hosts) != 0 { - err = &SplitErr{ - Wrapped: ErrBadNumHosts, - Nets: nets, - Remaining: remaining, - LastSubnet: &sub, - RequestedPrefixLen: uint8(sub.Bits()), - } - ipsb.AddPrefix(sub) - } else { - subPtr = new(netip.Prefix) - *subPtr = sub - nets = append(nets, subPtr) + // Now that we have an appropriate prefix length for splitting, we can offload a huge portion of that to a CIDRSplitter. + cs = &CIDRSplitter{ + PrefixLength: uint8(splitCidr), + BaseSplitter: h.BaseSplitter, + } + if nets, remaining, err = cs.Split(); err != nil { + return + } + // If strict mode is enabled, we then need to match the number of hosts exactly in the subnet. + if !h.Strict { + return + } + // First off, if remaining is not nil/empty, that immediately fails strict. + if remaining != nil && remaining.Prefixes() != nil && len(remaining.Prefixes()) != 0 { + err = &SplitErr{ + Wrapped: ErrBadNumHosts, + Nets: nets, + Remaining: remaining, + LastSubnet: nil, + RequestedPrefixLen: uint8(splitCidr), } - - nets[idx] = new(netip.Prefix) - *nets[idx] = sub + return } - if remaining, err = ipsb.IPSet(); err != nil { + // Then we check the cidr we split on, and check its number of hosts. + if hosts.Cmp(tgt) != 0 { + err = &SplitErr{ + Wrapped: ErrBadNumHosts, + Nets: nets, + Remaining: remaining, + LastSubnet: nil, + RequestedPrefixLen: uint8(splitCidr), + } return } diff --git a/netsplit/types.go b/netsplit/types.go index 636d9b8..7115b75 100644 --- a/netsplit/types.go +++ b/netsplit/types.go @@ -51,6 +51,10 @@ 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"` + // 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. + InclBcastAddr bool `json:"bcast_addr" xml:"bcast,attr,omitempty" yaml:"Broadcast Address Included,omitempty"` // Strict, if true, will return an error from Split if the network cannot split into subnets of NumberHosts-addressable networks exactly. Strict bool `json:"strict" xml:"strict,attr,omitempty" yaml:"Strictly Equal Hosts Per Subnet"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"` @@ -78,14 +82,14 @@ type VLSMSplitter struct { (ascending order) instead of larger networks/smaller prefixes (descending order). You almost assuredly do not want to do this. */ - Ascending bool + Ascending bool `json:"asc,omitempty" xml:"asc,attr,omitempty" yaml:"Ascending Order,omitempty"` /* Explicit, if true, will ignore Ascending completely and split in the explicit order of PrefixLengths. This has the potential to be *extremely* wasteful of addressing space as the resulting blocks are VERY unoptimized. */ - Explicit bool + 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"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"`