Compare commits

...

12 Commits

Author SHA1 Message Date
brent saner
860ad5842b
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!)
2025-04-13 18:25:32 -04:00
brent saner
c05f9c4d47
v0.2.4
FIXED:
* IPv6 split-nets splitter didn't work. It does not.
  https://github.com/projectdiscovery/mapcidr/issues/628
  Noticed.
  As such, this library/program has *completely removed*
  ALL use of the mapcidr library as it cannot be expected
  to be accurate.
2025-04-13 03:52:43 -04:00
brent saner
4ab83c9069
v0.2.3
ADDED:
* Additional diagram under table subcommand for the IPv6 CIDR segments
2025-04-07 16:03:20 -04:00
brent saner
a00442c204
v0.2.2
FIXED:
* Validations for all were missing. They aren't now.
* Fixed a busted validator for explicit net
2025-04-06 22:46:42 -04:00
brent saner
3c1bc832c0
v0.2.1
FIXED:
* host splitter wasn't working quite correctly; this has been fixed.
2025-04-06 18:26:18 -04:00
brent saner
fd344f3b8e
checking in- working on fix for numhost subnetting 2025-04-06 14:37:57 -04:00
brent saner
3c239a4d09
v0.2.0
ADDED:
* num-nets subcommand' this is MUCH much faster than actually splitting
  if you're trying to figure out how many times a given subnet fits into
  a network.
2025-04-06 01:31:51 -04:00
brent saner
701b598b1c
update TODO with bug to look into 2025-04-04 14:33:39 -04:00
brent saner
32297d1bba
v0.1.2
FIXED:
* Missing reservation checker
2025-04-04 14:29:07 -04:00
brent saner
d37aa3eb6b
v0.1.1
ADDED:
* The -s/--size argument to the VLSM splitter may now be a
  comma-delimited list of sizes instead of needing to specify -s/--size
  for each size.
  This change is backwards-compatible.
2025-04-04 11:53:55 -04:00
brent saner
0c8577f149
v0.1.0
ADDED:
* Explicit subnet ordering option for VLSM
2025-04-03 18:35:35 -04:00
brent saner
166fb3be23
v0.0.2
FIXED:
* Ooops, fix go mod
2025-03-10 10:07:51 -04:00
25 changed files with 1310 additions and 276 deletions

8
ACKNOWLEDGEMENTS Normal file
View File

@ -0,0 +1,8 @@
The "IPv6 Segment Reference Diagram" output as rendered in the `table`
subcommand is from:
https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing
as fetched on April 07, 2025.
It is licensed under Creative Commons "CC BY-SA 4.0";
see:
https://creativecommons.org/licenses/by-sa/4.0/
https://creativecommons.org/licenses/by-sa/4.0/legalcode.en)

View File

@ -39,7 +39,7 @@ A tool to assist in design of segregate/segment/split/subnet networks.
** For IPv4 addresses, it will be `true` if it is an APIPA (_Automatic Private IP Addressing_) address ({rfc}3927[RFC 3927^]) (in the `169.254.0.0/16` range). ** For IPv4 addresses, it will be `true` if it is an APIPA (_Automatic Private IP Addressing_) address ({rfc}3927[RFC 3927^]) (in the `169.254.0.0/16` range).
* `First` and `Last` refer to the first and last "usable" ("host"/assignable) addresses in a subnet/network. * `First` and `Last` refer to the first and last "usable" ("host"/assignable) addresses in a subnet/network.
** Note that for IPv6, the first address (`x::`) in a subnet *may* or *may not* be assignable/"usable". If it is assigned to a device, that device *must* be a router for anycast. See {rfc}4291#section-2.6.1[RFC 4291 § 2.6.1^] for details. In the interest of convenience, `subnetter` will report this address as *not usable/addressable* in ranges for this reason as it is technically not a "host" address. ** Note that for IPv6, the first address (`x::`) in a subnet *may* or *may not* be assignable/"usable". If it is assigned to a device, that device *must* be a router for anycast. See {rfc}4291#section-2.6.1[RFC 4291 § 2.6.1^] for details. In the interest of convenience, `subnetter` will report this address as *not usable/addressable* in ranges for this reason as it is technically not a "host" address.
** Note that for IPv6, some subnetting calculators erroneously report the last address for /64's (e.g. `x:ffff:ffff:ffff:ffff/64`) as usable. They are actually reserved in strictly RFC-compliant networks for EUI-64 reasons (per {rfc}2526[RFC 2526^]). For this reason, *if and only if* a prefix is a /64 *exactly*, `subnetter` will use `x:ffff:ffff:ffff:fffe` as the last host address. ** Note that for IPv6, some subnetting calculators erroneously report the last address as usable. They are reserved in strictly RFC-compliant networks for anycast reasons (per {rfc}2526[RFC 2526^]). Subnetter follows RFC as closely as possible, and any deviation from RFC is considered a bug -- as such, the last address of IPv6 subnets is considered *not usable/addressable*.
** There are additional restrictions for /64 subnets, but they fall earlier in the range. These are *not explicitly excluded* in the usable host range, nor are they excluded from the total host count. ** There are additional restrictions for /64 subnets, but they fall earlier in the range. These are *not explicitly excluded* in the usable host range, nor are they excluded from the total host count.
* Private networks ({rfc}1918[RFC 1918^]), ULA prefixes ({rfc}4193[RFC 4193^]), and documentation prefixes ({rfc}3849[RFC 3849^], {rfc}5737[RFC 5737^], {rfc}9637[RFC 9637^]) are treated as "normal" networks (in that it is allowed to subnet them). * Private networks ({rfc}1918[RFC 1918^]), ULA prefixes ({rfc}4193[RFC 4193^]), and documentation prefixes ({rfc}3849[RFC 3849^], {rfc}5737[RFC 5737^], {rfc}9637[RFC 9637^]) are treated as "normal" networks (in that it is allowed to subnet them).
* Various other reserved IPv4 and IPv6 addresses/networks will print warnings with their corresponding RFC(s) (unless `-R`/`--allow-reserved` is specified) if they are specified as/included in the initial prefix/network. ({rfc}6890[RFC 6890^] and its update via {rfc}8190[RFC 8190^] are useful summaries.) Note that for checking to function, an Internet connection is required as it pulls it directly from IANA live to ensure the data is accurate to standards. This may be cached locally if `-c`/`--cache-reservations` is specified, in which case a locally-cached copy will be used if present and populated then used if not. * Various other reserved IPv4 and IPv6 addresses/networks will print warnings with their corresponding RFC(s) (unless `-R`/`--allow-reserved` is specified) if they are specified as/included in the initial prefix/network. ({rfc}6890[RFC 6890^] and its update via {rfc}8190[RFC 8190^] are useful summaries.) Note that for checking to function, an Internet connection is required as it pulls it directly from IANA live to ensure the data is accurate to standards. This may be cached locally if `-c`/`--cache-reservations` is specified, in which case a locally-cached copy will be used if present and populated then used if not.
@ -50,7 +50,7 @@ A tool to assist in design of segregate/segment/split/subnet networks.
**** If the `XDG_CACHE_HOME` environment variable is not present... **** If the `XDG_CACHE_HOME` environment variable is not present...
***** On macOS, an explicit fallback of `~/Library/Caches/subnetter/` will be used. (To my knowledge/understanding, this is the standard user cache directory and cannot be changed.) This usually evaluates to `/Users/<username>/Library/Caches/subnetter/`. ***** On macOS, an explicit fallback of `~/Library/Caches/subnetter/` will be used. (To my knowledge/understanding, this is the standard user cache directory and cannot be changed.) This usually evaluates to `/Users/<username>/Library/Caches/subnetter/`.
***** On all others, an explicit fallback of `~/.cache/subnetter` will be used. ***** On all others, an explicit fallback of `~/.cache/subnetter` will be used.
****** On most non-macOS \*NIX-like systems , this is usually `/home/<username>/.cache/subetter/`, provided normal user homes. On http://p9f.org/[Plan9^] platforms (e.g. https://9p.io/plan9/index.html[Plan 9 4th Ed.^], https://9front.org/[9front^], http://9legacy.org/[9legacy^]), the `/env/home` environment variable (`$home`) will be used, the `./lib/` subdirectory under there (which typically/should already exist) will be appended to it, and that appended with `./cache/subnetter/` (this usually evaluates to `/usr/<username>/lib/cache/subnetter/`). ****** On most non-macOS/*NIX-like systems , this is usually `/home/<username>/.cache/subetter/`, provided normal user homes. On http://p9f.org/[Plan9^] platforms (e.g. https://9p.io/plan9/index.html[Plan 9 4th Ed.^], https://9front.org/[9front^], http://9legacy.org/[9legacy^]), the `/env/home` environment variable (`$home`) will be used, the `./lib/` subdirectory under there (which typically/should already exist) will be appended to it, and that appended with `./cache/subnetter/` (this usually evaluates to `/usr/<username>/lib/cache/subnetter/`).
*** For Windows systems... *** For Windows systems...
**** If https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#constants[the `LOCALAPPDATA` environment variable^] is present, it will be `%LOCALAPPDATA%\Cache\subnetter\` (or `${env:LOCALAPPDATA}\Cache\subnetter\` in Powershell syntax). This usually evaluates to `C:\Users\<username>\AppData\Local\Cache\subnetter\`. **** If https://learn.microsoft.com/en-us/windows/win32/shell/knownfolderid#constants[the `LOCALAPPDATA` environment variable^] is present, it will be `%LOCALAPPDATA%\Cache\subnetter\` (or `${env:LOCALAPPDATA}\Cache\subnetter\` in Powershell syntax). This usually evaluates to `C:\Users\<username>\AppData\Local\Cache\subnetter\`.


@ -61,3 +61,5 @@ This program in general draws inspiration from `ipcalc` (http://jodies.de/ipcalc
The `table` subcommand is inspired by `iptab` from https://metacpan.org/pod/Net::IP[Perl Net-IP^]. The `table` subcommand is inspired by `iptab` from https://metacpan.org/pod/Net::IP[Perl Net-IP^].


Additional notes for certain contexts are primarily taken from https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing[the Wikipedia article on _Classless Inter-Domain Routing_^] (as of _Jan 28, 2025_). Additional notes for certain contexts are primarily taken from https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing[the Wikipedia article on _Classless Inter-Domain Routing_^] (as of _Jan 28, 2025_).

Reservations are pulled/cached directly from the IANA registries (https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml[IPv4^], https://www.iana.org/assignments/iana-ipv6-special-registry/iana-ipv6-special-registry.xhtml[IPv6^]).

6
TODO
View File

@ -1 +1,7 @@
- add table rendering for reserved networks? - add table rendering for reserved networks?

- when checking/rendering reserved networks, currently the footnotes aren't returned.
-- netsplit.IANARegistryFootnote
-- encapsulated in the IANARegistry.Footnotes

- add new interface, Getter? different formatting?

View File

@ -1,2 +1,2 @@
{{- /*gotype: subnetter/cmd/subnetter.ReservedResults*/ -}} {{- /*gotype: r00t2.io/subnetter/cmd/subnetter.ReservedResults*/ -}}
{{- $opts := . -}} {{- $opts := . -}}

View File

@ -1,4 +1,4 @@
{{- /*gotype: subnetter/cmd/subnetter.TableArgs*/ -}} {{- /*gotype: r00t2.io/subnetter/cmd/subnetter.TableArgs*/ -}}
{{- $opts := . -}} {{- $opts := . -}}
{{- $numRows := 0 -}} {{- $numRows := 0 -}}
{{- if not $opts.NoIpv4 }} {{- if not $opts.NoIpv4 }}
@ -47,13 +47,268 @@ IPV4:
{{- end }} {{- end }}


{{- if not $opts.NoIpv6 }} {{- if not $opts.NoIpv6 }}

{{- if $opts.Plain }} {{- if $opts.Plain }}
IPV6: IPV6:
{{- else }} {{- else }}
{{ bold "IPv6:"}} {{ bold "IPv6:"}}
{{- end }} {{- end }}
{{- if not $opts.NoIpv6Seg }}
{{- if $opts.Plain }} {{- if $opts.Plain }}
IPv6 Segment Reference Diagram:
{{- if $opts.VertSeg }}

Example: 2001:0db8:0123:4567:89ab:cdef:1234:5678
{{- if not $opts.VertInvert }}
2 4
0 8
0 12
1 16
:
0 20
d 24
b 28
8 32
:
0 36
1 40
2 44
3 48
:
4 52
5 56
6 60
7 64
:
8 68
9 72
a 76
b 80
:
c 84
d 88
e 92
f 96
:
1 100
2 104
3 108
4 112
:
5 116
6 120
7 124
8 127 or 128
{{- else }}
4 ____ 2
8 ____ 0
12 ___ 0
16 ___ 1
___ :
20 _____ 0
24 _____ d
28 _____ b
32 _____ 8
_____ :
36 _______ 0
40 _______ 1
44 _______ 2
48 _______ 3
_______ :
52 _________ 4
56 _________ 5
60 _________ 6
64 _________ 7
_________ :
68 ___________ 8
72 ___________ 9
76 ___________ a
80 ___________ b
___________ :
84 _____________ c
88 _____________ d
92 _____________ e
96 _____________ f
_____________ :
100 ______________ 1
104 ______________ 2
108 ______________ 3
112 ______________ 4
______________ :
116 ________________ 5
120 ________________ 6
124 ________________ 7
127 ________________ (8)
128 ________________ (8)
{{- end }}
{{- else }}

2001:0db8:0123:4567:89ab:cdef:1234:5678
|||| |||| |||| |||| |||| |||| |||| ||||
|||| |||| |||| |||| |||| |||| |||| |||128
|||| |||| |||| |||| |||| |||| |||| |||127
|||| |||| |||| |||| |||| |||| |||| ||124
|||| |||| |||| |||| |||| |||| |||| |120
|||| |||| |||| |||| |||| |||| |||| 116
|||| |||| |||| |||| |||| |||| |||112
|||| |||| |||| |||| |||| |||| ||108
|||| |||| |||| |||| |||| |||| |104
|||| |||| |||| |||| |||| |||| 100
|||| |||| |||| |||| |||| |||96
|||| |||| |||| |||| |||| ||92
|||| |||| |||| |||| |||| |88
|||| |||| |||| |||| |||| 84
|||| |||| |||| |||| |||80
|||| |||| |||| |||| ||76
|||| |||| |||| |||| |72
|||| |||| |||| |||| 68
|||| |||| |||| |||64
|||| |||| |||| ||60
|||| |||| |||| |56
|||| |||| |||| 52
|||| |||| |||48
|||| |||| ||44
|||| |||| |40
|||| |||| 36
|||| |||32
|||| ||28
|||| |24
|||| 20
|||16
||12
|8
4
{{- end }}
{{- else }}
{{ bold "IPv6 Segment Reference Diagram:" }}
{{- if $opts.VertSeg }}

{{ bold "Example:"}} 2001:0db8:0123:4567:89ab:cdef:1234:5678
{{- if not $opts.VertInvert }}
2 4
0 8
0 12
1 16
:
0 20
d 24
b 28
8 32
:
0 36
1 40
2 44
3 48
:
4 52
5 56
6 60
7 64
:
8 68
9 72
a 76
b 80
:
c 84
d 88
e 92
f 96
:
1 100
2 104
3 108
4 112
:
5 116
6 120
7 124
8 127 or 128
{{- else }}
4 ━━━━ 2
8 ━━━━ 0
12 ━━━ 0
16 ━━━ 1
━━━ :
20 ━━━━━ 0
24 ━━━━━ d
28 ━━━━━ b
32 ━━━━━ 8
━━━━━ :
36 ━━━━━━━ 0
40 ━━━━━━━ 1
44 ━━━━━━━ 2
48 ━━━━━━━ 3
━━━━━━━ :
52 ━━━━━━━━━ 4
56 ━━━━━━━━━ 5
60 ━━━━━━━━━ 6
64 ━━━━━━━━━ 7
━━━━━━━━━ :
68 ━━━━━━━━━━━ 8
72 ━━━━━━━━━━━ 9
76 ━━━━━━━━━━━ a
80 ━━━━━━━━━━━ b
━━━━━━━━━━━ :
84 ━━━━━━━━━━━━━ c
88 ━━━━━━━━━━━━━ d
92 ━━━━━━━━━━━━━ e
96 ━━━━━━━━━━━━━ f
━━━━━━━━━━━━━ :
100 ━━━━━━━━━━━━━━ 1
104 ━━━━━━━━━━━━━━ 2
108 ━━━━━━━━━━━━━━ 3
112 ━━━━━━━━━━━━━━ 4
━━━━━━━━━━━━━━ :
116 ━━━━━━━━━━━━━━━━ 5
120 ━━━━━━━━━━━━━━━━ 6
124 ━━━━━━━━━━━━━━━━ 7
127 ━━━━━━━━━━━━━━━━ (8)
128 ━━━━━━━━━━━━━━━━ (8)
{{- end }}
{{- else }}

{{ bold "2001:0db8:0123:4567:89ab:cdef:1234:5678" }}
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃128
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃127
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃124
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃120
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ 116
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃112
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃108
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃104
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ 100
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃96
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃92
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃88
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ 84
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃80
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃76
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃72
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃┃ 68
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃┃64
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃┃60
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ ┃56
┃┃┃┃ ┃┃┃┃ ┃┃┃┃ 52
┃┃┃┃ ┃┃┃┃ ┃┃┃48
┃┃┃┃ ┃┃┃┃ ┃┃44
┃┃┃┃ ┃┃┃┃ ┃40
┃┃┃┃ ┃┃┃┃ 36
┃┃┃┃ ┃┃┃32
┃┃┃┃ ┃┃28
┃┃┃┃ ┃24
┃┃┃┃ 20
┃┃┃16
┃┃12
┃8
4
{{- end }}
{{- end }}
{{- end }}


{{- if $opts.Plain }}

CIDR: CIDR:
{{- else }} {{- else }}
{{ bold "CIDR:" }} {{ bold "CIDR:" }}

View File

@ -2,11 +2,14 @@ package main


type Args struct { type Args struct {
Version verArgs `command:"version" alias:"v" description:"Show version information." validate:"omitempty"` 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"` 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"`
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. (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"` 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:"vlsm" alias:"sv" description:"Use VLSM (Variable-Length Subnet Masks) to split a network into differently sized subnets." 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" description:"Print information about an explicit network address." 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"` 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"` Table TableArgs `command:"table" alias:"t" alias:"tab" alias:"tbl" description:"Show prefix summaries (by default both IPv4 and IPv6)." validate:"omitempty"`
Check CheckArgs `command:"reserved" alias:"r" description:"Check if a subnet is reserved per IANA/RFC." validate:"omitempty"` Check CheckArgs `command:"reserved" alias:"r" description:"Check if a subnet is reserved per IANA/RFC." validate:"omitempty"`
@ -16,7 +19,7 @@ type verArgs struct {
DetailVersion bool `short:"V" long:"detail" description:"Print detailed version info and exit."` DetailVersion bool `short:"V" long:"detail" description:"Print detailed version info and exit."`
} }


type common struct { type commonBase struct {
cacheArgs cacheArgs
SuppressRemaining bool `short:"r" long:"no-remaining" description:"Don't show leftover/unallocated/remaining space."` SuppressRemaining bool `short:"r" long:"no-remaining" description:"Don't show leftover/unallocated/remaining space."`
Plain bool `short:"p" long:"plain" description:"Show plain output instead of unicode (only used if -f/--format=pretty)."` Plain bool `short:"p" long:"plain" description:"Show plain output instead of unicode (only used if -f/--format=pretty)."`
@ -26,13 +29,17 @@ type common struct {
AllowReserved bool `short:"R" long:"allow-reserved" description:"If specified, do not warn about reserved IP addresses/networks."` AllowReserved bool `short:"R" long:"allow-reserved" description:"If specified, do not warn about reserved IP addresses/networks."`
reservedArgs reservedArgs
AllowHostNet bool `short:"H" long:"allow-host" description:"If specified, do not warn about host bits. Host bits are always removed for subnetting (as otherwise there would be errors); this is only used only for output."` AllowHostNet bool `short:"H" long:"allow-host" description:"If specified, do not warn about host bits. Host bits are always removed for subnetting (as otherwise there would be errors); this is only used only for output."`
}

type common struct {
commonBase
Network Net `positional-args:"yes" required:"true" description:"The network/parent prefix to operate on." validate:"required"` Network Net `positional-args:"yes" required:"true" description:"The network/parent prefix to operate on." validate:"required"`
} }


type reservedArgs 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."` 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."` 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 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."` 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 { type splitArgs struct {
@ -44,8 +51,32 @@ type cacheArgs struct {
DoResCache bool `short:"c" long:"cache-reservations" env:"SBNTR_RSVCACHE" description:"Enable caching/cache lookup for reservation data."` DoResCache bool `short:"c" long:"cache-reservations" env:"SBNTR_RSVCACHE" description:"Enable caching/cache lookup for reservation data."`
} }


type ParseArgs struct { 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 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:"<prefix length>" 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)."`
Sizes struct {
SubnetSize uint8 `positional-arg-name:"<subnet prefix>" required:"1" validate:"lte=128,gtefield=NetworkSize"`
NetworkSize uint8 `positional-arg-name:"<parent prefix>" required:"1" validate:"lte=128,ltefield=SubnetSize"`
} `positional-args:"yes" required:"2" validate:"required"`
}

type ParseArgs struct {
commonBase
InFile string `short:"i" long:"input" default:"-" description:"Input file to parse. Default is '-' (for STDIN)." required:"true" validate:"required,filepath|eq=-"` InFile string `short:"i" long:"input" default:"-" description:"Input file to parse. Default is '-' (for STDIN)." required:"true" validate:"required,filepath|eq=-"`
} }


@ -55,13 +86,15 @@ type SplitCIDRArgs struct {
} }


type SplitHostArgs struct { type SplitHostArgs struct {
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."` 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"` Hosts uint `short:"n" long:"num-hosts" required:"true" description:"Number of hosts (usable addresses) per subnet." validate:"required"`
splitArgs splitArgs
} }


type SplitSubnetArgs struct { type SplitSubnetArgs struct {
Strict bool `short:"t" long:"strict" description:"If specified, an error will occur if the number of possible equally-sized subnets is not exactly -n/--num-nets."` Strict bool `short:"t" long:"strict" description:"If specified, an error will occur if the number of subnets is not exactly -n/--num-nets."`
NumNets uint `short:"n" long:"num-nets" required:"true" description:"Number of networks." validate:"required"` NumNets uint `short:"n" long:"num-nets" required:"true" description:"Number of networks." validate:"required"`
splitArgs splitArgs
} }
@ -73,6 +106,9 @@ type TableArgs struct {
NoV4Mask bool `short:"M" long:"no-mask" description:"Do not include netmasks for IPv4."` NoV4Mask bool `short:"M" long:"no-mask" description:"Do not include netmasks for IPv4."`
NoIpv6 bool `short:"4" long:"ipv4" description:"Only show IPv4 table(s)."` NoIpv6 bool `short:"4" long:"ipv4" description:"Only show IPv4 table(s)."`
NoIpv4 bool `short:"6" long:"ipv6" description:"Only show IPv6 table(s)."` NoIpv4 bool `short:"6" long:"ipv6" description:"Only show IPv6 table(s)."`
NoIpv6Seg bool `short:"D" long:"no-ipv6-seg" description:"Do not show the IPv6 Segment Reference Diagram (ignored if -4/--ipv4 is specified)."`
VertSeg bool `short:"V" long:"vert-ipv6-seg" description:"If specified, display the IPv6 Segment Reference Diagram vertically-aligned instead of horizontally."`
VertInvert bool `short:"I" long:"vert-invert" description:"When printing a vertical-aligned IPv6 Segment Reference Diagram, flip so the prefix length is on the left. This takes up less width and is recommended for smaller terminals, and may be easier to read in general."`
} }


type CheckArgs struct { type CheckArgs struct {
@ -89,7 +125,10 @@ type XNetArgs struct {


type VLSMArgs struct { type VLSMArgs struct {
Asc bool `short:"A" long:"ascending" description:"If specified, place smaller networks (larger prefixes) at the beginning. You almost assuredly do not want to do this."` Asc bool `short:"A" long:"ascending" description:"If specified, place smaller networks (larger prefixes) at the beginning. You almost assuredly do not want to do this."`
Sizes []uint8 `short:"s" long:"size" required:"true" description:"Prefix lengths. May be specified multiple times." validate:"required"` Explicit bool `short:"O" long:"explicit-order" description:"If specified, ignore -A/--ascending and do no reordering of prefix sizes whatsoever, instead using the order given. This is EXTREMELY suboptimal and can lead to drastic addressing waste."`
// Custom type for now; see https://github.com/jessevdk/go-flags/issues/245
// Sizes []uint8 `short:"s" long:"size" required:"true" description:"Prefix lengths. May be specified multiple times." validate:"required"`
Sizes []vlsmSize `short:"s" long:"size" required:"true" description:"Prefix lengths. May be specified multiple times or as a comma-delimited list." validate:"required"`
splitArgs splitArgs
} }



View File

@ -5,5 +5,6 @@ import (
) )


var ( var (
ErrBadFmt error = errors.New("unknown output format")
errBadNet error = errors.New("bad inet/addr family/version") errBadNet error = errors.New("bad inet/addr family/version")
) )

View File

@ -10,15 +10,14 @@ import (
"net" "net"
"net/netip" "net/netip"
"os" "os"
`sort` "sort"
"strings" "strings"
"time" "time"


"github.com/goccy/go-yaml" "github.com/goccy/go-yaml"
"github.com/projectdiscovery/mapcidr"
"go4.org/netipx" "go4.org/netipx"
"subnetter/netsplit" "r00t2.io/subnetter/netsplit"
`subnetter/version` "r00t2.io/subnetter/version"
) )


func printHostPrefix(label string, pfx *netip.Prefix, verb, indent int, indentStr string) (out string) { func printHostPrefix(label string, pfx *netip.Prefix, verb, indent int, indentStr string) (out string) {
@ -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, "%sBits:\t\t%d\n", pre2, pfx.Bits())
fmt.Fprintf(sb, "%sFirst:\t\t%s\n", pre2, first.String()) 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, "%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, "%sAddresses:\t%d\n", pre2, netsplit.NumAddrsPfx(pfx.Masked(), true, true))
fmt.Fprintf(sb, "%sHosts:\t\t%d\n", pre2, mapcidr.CountIPsInCIDR(false, false, netipx.PrefixIPNet(pfx.Masked()))) fmt.Fprintf(sb, "%sHosts:\t\t%d\n", pre2, netsplit.NumAddrsPfx(pfx.Masked(), false, false))
if verb >= 2 { if verb >= 2 {
fmt.Fprintf(sb, "%sExpanded:\t%s\n", pre2, netsplit.MaskExpand(mask, pfx.Addr().Is6())) 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()) fmt.Fprintf(sb, "%sHex:\t\t0x%s\n", pre2, mask.String())
@ -236,7 +235,9 @@ func printNets(orig *netip.Prefix, origNet *net.IPNet, nets []*netip.Prefix, rem


if args == nil { if args == nil {
args = &common{ args = &common{
commonBase: commonBase{
Separator: "\n", Separator: "\n",
},
} }
} }
fmts = sectFmts[args.Plain] fmts = sectFmts[args.Plain]
@ -399,7 +400,9 @@ func printNets(orig *netip.Prefix, origNet *net.IPNet, nets []*netip.Prefix, rem


// Remaining // Remaining
if !args.SuppressRemaining { if !args.SuppressRemaining {
if verb >= 1 { if verb <= 0 {
fmt.Println("#")
} else {
fmt.Println() fmt.Println()
fmt.Println(sectSep1) fmt.Println(sectSep1)
fmt.Println("Remaining/Left Over/Unallocated:") fmt.Println("Remaining/Left Over/Unallocated:")
@ -471,7 +474,180 @@ func printNets(orig *netip.Prefix, origNet *net.IPNet, nets []*netip.Prefix, rem
return return
} }


func printReserved(nets []*netip.Prefix, remaining *netipx.IPSet, args *common) (err error) { func printReserved(records map[netip.Prefix]*netsplit.IANAAddrNetResRecord, origNet netip.Prefix, plain bool, fmtType string) (err error) {

var b []byte
var idx int
var pfx netip.Prefix
var rec *netsplit.IANAAddrNetResRecord
var sortedKeys []netip.Prefix
var sb = new(strings.Builder)

switch fmtType {
case "json":
if b, err = json.MarshalIndent(records, "", " "); err != nil {
return
}
fmt.Println(string(b))
return
case "xml":
if b, err = xml.MarshalIndent(records, "", " "); err != nil {
return
}
fmt.Println(string(b))
return
case "yml", "yaml":
if b, err = yaml.Marshal(records); err != nil {
return
}
fmt.Println(string(b))
return
}

if fmtType != "pretty" {
err = ErrBadFmt
return
}

if records == nil || len(records) == 0 {
fmt.Println("No IANA/IETF/RFC-reserved subnet(s) found.")
return
}

sortedKeys = make([]netip.Prefix, len(records))
idx = 0
for pfx, _ = range records {
sortedKeys[idx] = pfx
idx++
}
sort.SliceStable(
sortedKeys,
func(i, j int) (isBefore bool) {
isBefore = (netipx.ComparePrefix(sortedKeys[i], sortedKeys[j])) <= 0
return
},
)

fmt.Fprintf(sb, "= %s =\n", origNet.String())
for _, pfx = range sortedKeys {
rec = records[pfx]
fmt.Fprint(sb, sectFmts[plain][0]+"\n")
// Name
fmt.Fprintf(sb, "Reservation Name:\t%s\n", rec.Name)
fmt.Fprint(sb, "\t"+sectFmts[plain][1]+"\n")
// Networks
fmt.Fprint(sb, "\tCanonical Reserved Networks:")
if rec.Networks != nil {
fmt.Fprint(sb, "\n")
for _, recPfx := range rec.Networks.Prefixes {
fmt.Fprint(sb, "\t\t"+sectFmts[plain][2]+"\n")
fmt.Fprintf(sb, "\t\t%s\n", recPfx.String())
// TODO: Print footnotes/refs!
}
} else {
fmt.Fprint(sb, "\t(N/A)\n")
}
fmt.Fprint(sb, "\t"+sectFmts[plain][1]+"\n")
// Reference/Specification
fmt.Fprint(sb, "\tSpecification:")
if rec.Spec != nil {
fmt.Fprint(sb, "\n")
for _, line := range strings.Split(rec.Spec.Text, "\n") {
fmt.Fprint(sb, "\t\t"+line+"\n")
}
if rec.Spec.References != nil {
fmt.Fprintf(sb, "\t\t%s\n", sectFmts[plain][2])
for rIdx, recref := range rec.Spec.References {
if recref != nil {
fmt.Fprintf(sb, "\t\t[%d] (%s) %s\n", rIdx, recref.Type, recref.Reference)
}
}
}
} else {
fmt.Fprint(sb, "\t(None)\n")
}
fmt.Fprint(sb, "\t"+sectFmts[plain][1]+"\n")
// Allocated (always present)
fmt.Fprintf(sb, "\tAllocated:\t%s\n", time.Time(rec.Allocation).String())
// fmt.Fprint(sb, "\t"+sectFmts[plain][1]+"\n")
// Updated
fmt.Fprint(sb, "\tUpdated:\t")
if rec.Updated != nil {
fmt.Fprintf(sb, "%s\n", time.Time(*rec.Updated).String())
} else {
fmt.Fprint(sb, "(N/A)\n")
}
// fmt.Fprint(sb, "\t"+sectFmts[plain][1]+"\n")
// Termination
fmt.Fprint(sb, "\tTerminated:\t")
if rec.Termination != nil {
fmt.Fprintf(sb, "%s\n", time.Time(*rec.Termination).String())
} else {
fmt.Fprint(sb, "(N/A)\n")
}
fmt.Fprint(sb, "\t"+sectFmts[plain][1]+"\n")
// Source
fmt.Fprint(sb, "\tValid Source:\t\t\t")
if rec.Source != nil {
if rec.Source.Applicable != nil && !bool(*rec.Source.Applicable) {
fmt.Fprint(sb, "(N/A)\n")
} else {
fmt.Fprintf(sb, "%v\n", bool(*rec.Source.Evaluated))
}
} else {
fmt.Fprint(sb, "(N/A)\n")
}
// fmt.Fprint(sb, "\t"+sectFmts[plain][1]+"\n")
// Destination
fmt.Fprint(sb, "\tValid Destination:\t\t")
if rec.Dest != nil {
if rec.Dest.Applicable != nil && !bool(*rec.Dest.Applicable) {
fmt.Fprint(sb, "(N/A)\n")
} else {
fmt.Fprintf(sb, "%v\n", bool(*rec.Dest.Evaluated))
}
} else {
fmt.Fprint(sb, "(N/A)\n")
}
// fmt.Fprint(sb, "\t"+sectFmts[plain][1]+"\n")
// Forwardable
fmt.Fprint(sb, "\tForwardable:\t\t\t")
if rec.Forwardable != nil {
if rec.Forwardable.Applicable != nil && !bool(*rec.Forwardable.Applicable) {
fmt.Fprint(sb, "(N/A)\n")
} else {
fmt.Fprintf(sb, "%v\n", bool(*rec.Forwardable.Evaluated))
}
} else {
fmt.Fprint(sb, "(N/A)\n")
}
// fmt.Fprint(sb, "\t"+sectFmts[plain][1]+"\n")
// Globally reachable
fmt.Fprint(sb, "\tGlobally Routable/Reachable:\t")
if rec.GlobalReach != nil {
if rec.GlobalReach.Applicable != nil && !bool(*rec.GlobalReach.Applicable) {
fmt.Fprint(sb, "(N/A)\n")
} else {
fmt.Fprintf(sb, "%v\n", bool(*rec.GlobalReach.Evaluated))
}
} else {
fmt.Fprint(sb, "(N/A)\n")
}
// fmt.Fprint(sb, "\t"+sectFmts[plain][1]+"\n")
// Reserved by Protocol
fmt.Fprint(sb, "\tReserved by Protocol:\t\t")
if rec.ProtoReserved != nil {
if rec.ProtoReserved.Applicable != nil && !bool(*rec.ProtoReserved.Applicable) {
fmt.Fprint(sb, "(N/A)\n")
} else {
fmt.Fprintf(sb, "%v\n", bool(*rec.ProtoReserved.Evaluated))
}
} else {
fmt.Fprint(sb, "(N/A)\n")
}
}

fmt.Print(sb.String())


return return
} }
@ -485,7 +661,7 @@ func printSplitErr(e *netsplit.SplitErr) {
os.Stderr.WriteString("\n!! ERROR !!!\n") os.Stderr.WriteString("\n!! ERROR !!!\n")


os.Stderr.WriteString("\t" + e.Wrapped.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 { if e.Nets == nil {
os.Stderr.WriteString("Nets:\t\t\t(N/A)\n") os.Stderr.WriteString("Nets:\t\t\t(N/A)\n")
} else { } else {
@ -494,7 +670,7 @@ func printSplitErr(e *netsplit.SplitErr) {
fmt.Fprintf(os.Stderr, "\t%s\n", n.String()) 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") os.Stderr.WriteString("Remaining:\t\t(N/A)\n")
} else { } else {
os.Stderr.WriteString("Remaining:\n") os.Stderr.WriteString("Remaining:\n")
@ -503,7 +679,7 @@ func printSplitErr(e *netsplit.SplitErr) {
} }
} }
if e.LastSubnet == nil { if e.LastSubnet == nil {
os.Stderr.WriteString("Last Subnet:\t\t(N/A)") os.Stderr.WriteString("Last Subnet:\t\t(N/A)\n")
} else { } else {
fmt.Fprintf(os.Stderr, "Last Subnet:\t\t%s\n", e.LastSubnet.String()) fmt.Fprintf(os.Stderr, "Last Subnet:\t\t%s\n", e.LastSubnet.String())
} }

View File

@ -10,9 +10,8 @@ import (
`strings` `strings`


`github.com/TwiN/go-color` `github.com/TwiN/go-color`
`github.com/projectdiscovery/mapcidr`
`go4.org/netipx` `go4.org/netipx`
`subnetter/netsplit` `r00t2.io/subnetter/netsplit`
) )


// renderHdr renders a header. Note that the first line does *not* include the indent, but subsequent ones do. // renderHdr renders a header. Note that the first line does *not* include the indent, but subsequent ones do.
@ -423,8 +422,8 @@ func tplTablePrefixes(ipVer uint8, indent string, plain bool) (out string, err e
return return
} }
dummyNet = netipx.PrefixIPNet(rows[idx].NetPrefix.Masked()) dummyNet = netipx.PrefixIPNet(rows[idx].NetPrefix.Masked())
rows[idx].Addresses = mapcidr.CountIPsInCIDR(true, true, dummyNet) rows[idx].Addresses = netsplit.NumAddrsNet(dummyNet, true, true)
rows[idx].Hosts = mapcidr.CountIPsInCIDR(false, false, dummyNet) rows[idx].Hosts = netsplit.NumAddrsNet(dummyNet, false, false)
} }


colFields, colTitles, colSizes = sizeStructs(rows) colFields, colTitles, colSizes = sizeStructs(rows)

View File

@ -0,0 +1,26 @@
package main

/*
AllSizes returns a properly parsed and consolidated slice
of all specified sizes, as it takes two valid syntaxes (`-s 32 -s 32`, `-s 32,32`)
that can be mixed together.
*/
func (v *VLSMArgs) AllSizes() (sizes []uint8, err error) {

var sizeSlice []uint8

if v == nil {
return
}
if v.Sizes == nil || len(v.Sizes) == 0 {
return
}
for _, s := range v.Sizes {
if sizeSlice, err = s.Sizes(); err != nil {
return
}
sizes = append(sizes, sizeSlice...)
}

return
}

View File

@ -0,0 +1,33 @@
package main

import (
`strconv`
`strings`
)

// Sizes returns a parsed/split slice of uint8s from a vlsmSize.
func (v *vlsmSize) Sizes() (sizes []uint8, err error) {

var s []string
var u uint64

if v == nil {
return
}
s = strings.Split(string(*v), ",")
for idx, i := range s {
s[idx] = strings.TrimSpace(i)
}

sizes = make([]uint8, len(s))

// No validation is performed since we don't have access to the addr inet family; that's up to the parsers.
for idx, i := range s {
if u, err = strconv.ParseUint(i, 10, 8); err != nil {
return
}
sizes[idx] = uint8(u)
}

return
}

View File

@ -3,17 +3,18 @@ package main
import ( import (
"bytes" "bytes"
"errors" "errors"
`fmt` "fmt"
"io" "io"
"log" "log"
`math/big`
"net" "net"
"net/netip" "net/netip"
"os" "os"
"strings" "strings"


"go4.org/netipx" "go4.org/netipx"
"subnetter/netsplit" "r00t2.io/subnetter/netsplit"
`subnetter/version` "r00t2.io/subnetter/version"


"github.com/jessevdk/go-flags" "github.com/jessevdk/go-flags"
"r00t2.io/sysutils/paths" "r00t2.io/sysutils/paths"
@ -26,14 +27,19 @@ func main() {
var pfx *net.IPNet var pfx *net.IPNet
var resPfx *netip.Prefix var resPfx *netip.Prefix
var origPfx netip.Prefix var origPfx netip.Prefix
var vlsmSizes []uint8
var splitter netsplit.NetSplitter var splitter netsplit.NetSplitter
var cmnArgs common var cmnArgs common
var nets []*netip.Prefix var nets []*netip.Prefix
var remaining *netipx.IPSet var remaining *netipx.IPSet
var buf *bytes.Buffer var buf *bytes.Buffer
var res *netsplit.StructuredResults var res *netsplit.StructuredResults
var numNets uint
var numAddrs *big.Int
var v6Only bool
var noStrict bool var noStrict bool
var strictErr error var strictErr error
var reservations map[netip.Prefix]*netsplit.IANAAddrNetResRecord
var splitErr *netsplit.SplitErr = new(netsplit.SplitErr) var splitErr *netsplit.SplitErr = new(netsplit.SplitErr)
var parser *flags.Parser = flags.NewParser(args, flags.Default) var parser *flags.Parser = flags.NewParser(args, flags.Default)


@ -65,6 +71,9 @@ func main() {
return return
} }
case "net": case "net":
if err = validate.Struct(args.ExplicitNetwork); err != nil {
log.Panicln(err)
}
if origPfx, err = netip.ParsePrefix(args.ExplicitNetwork.Network.Network); err != nil { if origPfx, err = netip.ParsePrefix(args.ExplicitNetwork.Network.Network); err != nil {
log.Panicln(err) log.Panicln(err)
} }
@ -77,9 +86,69 @@ func main() {
log.Panicln(err) log.Panicln(err)
} }
return 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)
}
if numNets, v6Only, err = netsplit.NumNets(
args.NumNets.Sizes.SubnetSize,
args.NumNets.Sizes.NetworkSize,
); err != nil {
log.Panicln(err)
}
if !args.NumNets.Verbose {
fmt.Printf("%d\n", numNets)
if !args.NumNets.NoV6Check {
fmt.Println(v6Only)
}
} else {
fmt.Printf("Network Size:\t\t\t%d\n", args.NumNets.Sizes.NetworkSize)
fmt.Printf("Subnet Size:\t\t\t%d\n", args.NumNets.Sizes.SubnetSize)
fmt.Printf("Number of Subnets:\t\t%d\n", numNets)
if !args.NumNets.NoV6Check {
fmt.Printf("Subnetting is IPv6-Only:\t%v\n", v6Only)
}
}
return
case "reserved": case "reserved":
// TODO 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)
}
nets = make([]*netip.Prefix, 1)
nets[0] = new(netip.Prefix)
*nets[0] = origPfx
if err = netsplit.SetCachePath(args.Check.CacheDir); err != nil {
log.Panicln(err)
}
if err = netsplit.EnableCache(args.Check.DoResCache); err != nil {
log.Panicln(err)
}
if reservations, err = netsplit.CheckReserved(nets, !args.Check.NoRevRecursive, !args.Check.NoRecursive, !args.Check.NoPrivate); err != nil {
log.Panicln(err)
}
if err = printReserved(reservations, origPfx, args.Check.Plain, args.Check.Fmt); err != nil {
log.Panicln(err)
}
return
case "table": case "table":
if err = validate.Struct(args.Table); err != nil {
log.Panicln(err)
}
// Account for a weird redundant CLI condition. // Account for a weird redundant CLI condition.
if args.Table.NoIpv4 && args.Table.NoIpv6 { if args.Table.NoIpv4 && args.Table.NoIpv6 {
args.Table.NoIpv6 = false args.Table.NoIpv6 = false
@ -92,6 +161,9 @@ func main() {
os.Stdout.Write(buf.Bytes()) os.Stdout.Write(buf.Bytes())
return return
case "parse": case "parse":
if err = validate.Struct(args.Parse); err != nil {
log.Panicln(err)
}
if strings.TrimSpace(args.Parse.InFile) == "-" { if strings.TrimSpace(args.Parse.InFile) == "-" {
buf = new(bytes.Buffer) buf = new(bytes.Buffer)
if _, err = io.Copy(buf, os.Stdin); err != nil { if _, err = io.Copy(buf, os.Stdin); err != nil {
@ -116,7 +188,12 @@ func main() {
origPfx = *resPfx origPfx = *resPfx
} }
pfx = netipx.PrefixIPNet(origPfx.Masked()) pfx = netipx.PrefixIPNet(origPfx.Masked())
cmnArgs = args.Parse.common cmnArgs = common{
commonBase: args.Parse.commonBase,
Network: Net{
Network: res.Original.String(),
},
}
if err = printNets(&origPfx, pfx, nets, remaining, &cmnArgs, res.GetSplitter()); err != nil { if err = printNets(&origPfx, pfx, nets, remaining, &cmnArgs, res.GetSplitter()); err != nil {
log.Panicln(err) log.Panicln(err)
} }
@ -132,12 +209,26 @@ func main() {
These are all handily-dandily enclosed in a `common` struct type. These are all handily-dandily enclosed in a `common` struct type.
*/ */
switch parser.Active.Name { 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": case "split-hosts":
if err = validate.Struct(args.SplitHost); err != nil { if err = validate.Struct(args.SplitHost); err != nil {
log.Panicln(err) log.Panicln(err)
} }
cmnArgs = args.SplitHost.common cmnArgs = args.SplitHost.common
splitter = &netsplit.HostSplitter{ splitter = &netsplit.HostSplitter{
InclNetAddr: args.SplitHost.InclNetAddr,
InclBcastAddr: args.SplitHost.InclBcastAddr,
NumberHosts: args.SplitHost.Hosts, NumberHosts: args.SplitHost.Hosts,
Strict: args.SplitHost.Strict, Strict: args.SplitHost.Strict,
BaseSplitter: new(netsplit.BaseSplitter), BaseSplitter: new(netsplit.BaseSplitter),
@ -165,14 +256,18 @@ func main() {
PrefixLength: args.SplitCIDR.Prefix, PrefixLength: args.SplitCIDR.Prefix,
BaseSplitter: new(netsplit.BaseSplitter), BaseSplitter: new(netsplit.BaseSplitter),
} }
case "vlsm": case "split-vlsm":
if err = validate.Struct(args.VLSM); err != nil { if err = validate.Struct(args.VLSM); err != nil {
log.Panicln(err) log.Panicln(err)
} }
cmnArgs = args.VLSM.common cmnArgs = args.VLSM.common
if vlsmSizes, err = args.VLSM.AllSizes(); err != nil {
log.Panicln(err)
}
splitter = &netsplit.VLSMSplitter{ splitter = &netsplit.VLSMSplitter{
Ascending: args.VLSM.Asc, Ascending: args.VLSM.Asc,
PrefixLengths: args.VLSM.Sizes, Explicit: args.VLSM.Explicit,
PrefixLengths: vlsmSizes,
BaseSplitter: new(netsplit.BaseSplitter), BaseSplitter: new(netsplit.BaseSplitter),
} }
default: default:

View File

@ -1,11 +1,11 @@
package main package main


import ( import (
`math/big` "math/big"
`net` "net"
"net/netip" "net/netip"


`subnetter/netsplit` "r00t2.io/subnetter/netsplit"
) )


// subnetResult is only used for human/"pretty" printing. // subnetResult is only used for human/"pretty" printing.
@ -96,6 +96,9 @@ type tableFormatter struct {
NoBoldTitle bool NoBoldTitle bool
} }


// vlsmSize is a custom type to let us specify multiple sizes as a repeated or consolidated argument.
type vlsmSize string

type ReservedResults struct { type ReservedResults struct {
Opts CheckArgs Opts CheckArgs
Reserved map[netip.Prefix]*netsplit.IANAAddrNetResRecord Reserved map[netip.Prefix]*netsplit.IANAAddrNetResRecord

34
go.mod
View File

@ -1,39 +1,29 @@
module subnetter module r00t2.io/subnetter


go 1.23.2 go 1.24.0

toolchain go1.23.5


require ( require (
github.com/TwiN/go-color v1.4.1 github.com/TwiN/go-color v1.4.1
github.com/davecgh/go-spew v1.1.1 github.com/go-playground/validator/v10 v10.25.0
github.com/go-playground/validator/v10 v10.24.0
github.com/go-resty/resty/v2 v2.16.5 github.com/go-resty/resty/v2 v2.16.5
github.com/goccy/go-yaml v1.15.16 github.com/goccy/go-yaml v1.15.23
github.com/jessevdk/go-flags v1.6.1 github.com/jessevdk/go-flags v1.6.1
github.com/projectdiscovery/mapcidr v1.1.34
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba go4.org/netipx v0.0.0-20231129151722-fdeea329fbba
golang.org/x/mod v0.22.0 golang.org/x/mod v0.24.0
r00t2.io/goutils v1.8.1
r00t2.io/sysutils v1.12.0 r00t2.io/sysutils v1.12.0
) )


require ( require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/djherbis/times v1.6.0 // indirect github.com/djherbis/times v1.6.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.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/leodido/go-urn v1.4.0 // indirect
github.com/microcosm-cc/bluemonday v1.0.27 // indirect github.com/stretchr/testify v1.9.0 // indirect
github.com/pkg/errors v0.9.1 // indirect golang.org/x/crypto v0.36.0 // indirect
github.com/projectdiscovery/blackrock v0.0.1 // indirect golang.org/x/net v0.37.0 // indirect
github.com/projectdiscovery/utils v0.4.9 // indirect golang.org/x/sync v0.12.0 // indirect
github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d // indirect golang.org/x/sys v0.31.0 // indirect
golang.org/x/crypto v0.32.0 // indirect golang.org/x/text v0.23.0 // indirect
golang.org/x/net v0.34.0 // indirect
golang.org/x/sync v0.10.0 // indirect
golang.org/x/sys v0.29.0 // indirect
golang.org/x/text v0.21.0 // indirect
r00t2.io/goutils v1.7.2 // indirect
) )

52
go.sum
View File

@ -1,7 +1,5 @@
github.com/TwiN/go-color v1.4.1 h1:mqG0P/KBgHKVqmtL5ye7K0/Gr4l6hTksPgTgMk3mUzc= 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/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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@ -15,57 +13,43 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.24.0 h1:KHQckvo8G6hlWnrPX4NJJ+aBfWNAE/HH+qdL2cBpCmg= github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
github.com/go-playground/validator/v10 v10.24.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM=
github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/goccy/go-yaml v1.15.16 h1:PMTVcGI9uNPIn7KLs0H7KC1rE+51yPl5YNh4i8rGuRA= github.com/goccy/go-yaml v1.15.23 h1:WS0GAX1uNPDLUvLkNU2vXq6oTnsmfVFocjQ/4qA48qo=
github.com/goccy/go-yaml v1.15.16/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 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/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
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 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4=
github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= 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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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.4.9 h1:GzYKy5iiCWEZZPGxrtgTOnRTZYiIAiCditGufp0nhGU=
github.com/projectdiscovery/utils v0.4.9/go.mod h1:/68d0OHGgYF4aW4X7kS1qlFlYOnZxgtFDN85iH732JI=
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 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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 h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y= 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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 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.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 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.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw=
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 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-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.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U=
golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
r00t2.io/goutils v1.7.2 h1:dJ+pzY/U1yVi2V6eKoxe/4roM+Tb3d0umMEL9Dx4+Lw= r00t2.io/goutils v1.8.1 h1:TQcUycPKsYn0QI4uCqb56utmvu/vVSxlblBg98iXStg=
r00t2.io/goutils v1.7.2/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk= r00t2.io/goutils v1.8.1/go.mod h1:9ObJI9S71wDLTOahwoOPs19DhZVYrOh4LEHmQ8SW4Lk=
r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o= r00t2.io/sysutils v1.1.1/go.mod h1:Wlfi1rrJpoKBOjWiYM9rw2FaiZqraD6VpXyiHgoDo/o=
r00t2.io/sysutils v1.12.0 h1:Ce3qUOyLixE1ZtFT/+SVwOT5kSkzg5+l1VloGeGugrU= r00t2.io/sysutils v1.12.0 h1:Ce3qUOyLixE1ZtFT/+SVwOT5kSkzg5+l1VloGeGugrU=
r00t2.io/sysutils v1.12.0/go.mod h1:bNTKNBk9MnUhj9coG9JBNicSi5FrtJHEM645um85pyw= r00t2.io/sysutils v1.12.0/go.mod h1:bNTKNBk9MnUhj9coG9JBNicSi5FrtJHEM645um85pyw=

View File

@ -5,9 +5,16 @@ import (
`net/netip` `net/netip`
`sync` `sync`


`github.com/go-playground/validator/v10`
`github.com/go-resty/resty/v2` `github.com/go-resty/resty/v2`
) )


const (
maxBitsv4 uint8 = 32
maxBitsv6 uint8 = 128
maxBits uint8 = maxBitsv6
)

const ( const (
cachedirEnvName string = "SBNTR_RSVCACHE_DIR" cachedirEnvName string = "SBNTR_RSVCACHE_DIR"
// https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml // https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml
@ -33,8 +40,12 @@ var (
) )


var ( var (
// TODO // validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled(), validator.WithPrivateFieldValidation())
cacheLock sync.RWMutex validate *validator.Validate = validator.New(validator.WithRequiredStructEnabled())
)

var (
cacheLock sync.RWMutex // TODO
cacheClient *resty.Client cacheClient *resty.Client
// 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
// 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

View File

@ -5,9 +5,11 @@ import "errors"
var ( var (
ErrBadBoundary error = errors.New("subnet does not align on bit boundary") ErrBadBoundary error = errors.New("subnet does not align on bit boundary")
ErrBadNumHosts error = errors.New("bad number of hosts; cannot split into prefix exactly") ErrBadNumHosts error = errors.New("bad number of hosts; cannot split into prefix exactly")
ErrBadNumNets error = errors.New("bad number of nets; cannot split into prefix exactly")
ErrBadPrefix error = errors.New("prefix is invalid") ErrBadPrefix error = errors.New("prefix is invalid")
ErrBadPrefixLen error = errors.New("prefix length exceeds maximum possible for prefix's inet family") ErrBadPrefixLen error = errors.New("prefix length exceeds maximum possible for prefix's inet family")
ErrBadSplitter error = errors.New("invalid or unknown splitter when containing") ErrBadSplitter error = errors.New("invalid or unknown splitter when containing")
ErrBigPrefix error = errors.New("prefix length exceeds remaining network space") ErrBigPrefix error = errors.New("prefix length exceeds remaining network space")
ErrNoNetSpace error = errors.New("reached end of network space before splitting finished") ErrNoNetSpace error = errors.New("reached end of network space before splitting finished")
ErrUnknownPos error = errors.New("unknown subnet position")
) )

View File

@ -1,9 +1,12 @@
package netsplit package netsplit


import ( import (
`bytes`
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
"math"
"math/big"
"net" "net"
"net/netip" "net/netip"
"strings" "strings"
@ -191,6 +194,7 @@ func CheckReserved(nets []*netip.Prefix, revRecursive, recursive, excludePrivate
reservations = make(map[netip.Prefix]*IANAAddrNetResRecord) reservations = make(map[netip.Prefix]*IANAAddrNetResRecord)
} }
reservations[*n] = res reservations[*n] = res
}
if !revRecursive && !recursive { if !revRecursive && !recursive {
continue continue
} }
@ -204,7 +208,8 @@ func CheckReserved(nets []*netip.Prefix, revRecursive, recursive, excludePrivate
reservations = make(map[netip.Prefix]*IANAAddrNetResRecord) reservations = make(map[netip.Prefix]*IANAAddrNetResRecord)
} }
reservations[p] = r reservations[p] = r
} else if recursive && n.Contains(p.Addr()) { }
if recursive && n.Bits() < p.Bits() && n.Contains(p.Addr()) {
if reservations == nil { if reservations == nil {
reservations = make(map[netip.Prefix]*IANAAddrNetResRecord) reservations = make(map[netip.Prefix]*IANAAddrNetResRecord)
} }
@ -212,7 +217,6 @@ func CheckReserved(nets []*netip.Prefix, revRecursive, recursive, excludePrivate
} }
} }
} }
}


return return
} }
@ -221,7 +225,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) { func Contain(origPfx *netip.Prefix, nets []*netip.Prefix, remaining *netipx.IPSet, splitter NetSplitter) (s *StructuredResults, err error) {


var rem []netip.Prefix var rem []netip.Prefix
var reserved map[netip.Prefix]*IANAAddrNetResRecord // var reserved map[netip.Prefix]*IANAAddrNetResRecord
var sr = StructuredResults{ var sr = StructuredResults{
Original: origPfx, Original: origPfx,
} }
@ -274,6 +278,7 @@ func Contain(origPfx *netip.Prefix, nets []*netip.Prefix, remaining *netipx.IPSe
} }
} }


/*
if nets != nil { if nets != nil {
if reserved, err = CheckReserved(nets, true, true, false); err != nil { if reserved, err = CheckReserved(nets, true, true, false); err != nil {
return return
@ -287,12 +292,120 @@ func Contain(origPfx *netip.Prefix, nets []*netip.Prefix, remaining *netipx.IPSe
} }
} }
} }
*/


s = &sr s = &sr


return 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. MaskExpand expands a net.IPMask's string format.
Like AddrExpand but for netmasks. Like AddrExpand but for netmasks.
@ -375,6 +488,134 @@ func MaskInvert(mask net.IPMask) (inverted net.IPMask) {
return 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(pfxLen uint8, isIpv6, inclNet, inclBcast bool) (numAddrs *big.Int, err error) {

var numBits uint
var numRemoved int64
var maxBitLen uint8 = maxBitsv4

if isIpv6 || pfxLen > maxBitsv4 {
maxBitLen = maxBitsv6
}
if pfxLen > maxBitLen {
err = ErrBadPrefixLen
return
}
if pfxLen == maxBitLen {
numAddrs = big.NewInt(1)
return
}
if (pfxLen + 1) == maxBitLen {
numAddrs = big.NewInt(2)
return
}

numBits = uint(maxBitLen - pfxLen)

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 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, inclBcast bool) (numAddrs *big.Int) {

var nPfx netip.Prefix
var ok bool

if pfx == nil {
return
}
if nPfx, ok = netipx.FromStdIPNet(pfx); !ok {
return
}
if !nPfx.IsValid() {
return
}

numAddrs = NumAddrsPfx(nPfx, inclNet, inclBcast)

return
}

// 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 err error

if !pfx.IsValid() {
return
}

// 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
}

/*
NumNets returns the number of times prefix size subnet fits into prefix size network.

It will error if network is larger than 128 or if subnet is smaller than network.

This is MUCH more performant than splitting out an actual network into explicit subnets,
and does not require an actual network.
*/
func NumNets(subnet, network uint8) (numNets uint, ipv6Only bool, err error) {

var x float64

// network cannot be higher than 128, as that's the maximum for IPv6.
if network > maxBits {
err = ErrBadPrefixLen
return
}
if subnet < network {
err = ErrBigPrefix
return
}
ipv6Only = (network > maxBitsv4) || (subnet > maxBitsv4)

x = float64(subnet - network)

numNets = uint(math.Pow(2, x))

return
}

// Parse parses b for JSON/XML/YAML and tries to return a StructuredResults from it. // Parse parses b for JSON/XML/YAML and tries to return a StructuredResults from it.
func Parse(b []byte) (s *StructuredResults, err error) { func Parse(b []byte) (s *StructuredResults, err error) {



View File

@ -195,6 +195,7 @@ func SetCachePath(cacheDirPath string) (err error) {
} }


if cacheDirPath != oldPath { if cacheDirPath != oldPath {
cacheDir = cacheDirPath
if err = os.MkdirAll(cacheDir, cacheDirPerms); err != nil { if err = os.MkdirAll(cacheDir, cacheDirPerms); err != nil {
return return
} }

View File

@ -22,6 +22,10 @@ func (c *CIDRSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e
return return
} }


if err = validate.Struct(c); err != nil {
return
}

if base, ok = netipx.FromStdIPNet(c.network); !ok { if base, ok = netipx.FromStdIPNet(c.network); !ok {
err = ErrBadBoundary err = ErrBadBoundary
return return
@ -31,7 +35,7 @@ func (c *CIDRSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e
return return
} }


if c.PrefixLength > uint8(base.Bits()) { if c.PrefixLength < uint8(base.Bits()) {
err = ErrBigPrefix err = ErrBigPrefix
return return
} }
@ -41,6 +45,15 @@ func (c *CIDRSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e
return 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 { for {
if sub, remaining, ok = remaining.RemoveFreePrefix(c.PrefixLength); !ok { if sub, remaining, ok = remaining.RemoveFreePrefix(c.PrefixLength); !ok {
if !sub.IsValid() { if !sub.IsValid() {
@ -48,11 +61,11 @@ func (c *CIDRSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e
// We just hit the end of the prefix. // We just hit the end of the prefix.
break break
} }
}
subPtr = new(netip.Prefix) subPtr = new(netip.Prefix)
*subPtr = sub *subPtr = sub
nets = append(nets, subPtr) nets = append(nets, subPtr)
} }
}


return return
} }

View File

@ -1,11 +1,9 @@
package netsplit package netsplit


import ( import (
`math/big` "math/big"
`net`
"net/netip" "net/netip"


`github.com/projectdiscovery/mapcidr`
"go4.org/netipx" "go4.org/netipx"
) )


@ -15,52 +13,88 @@ import (


remaining may or may not be nil depending on if the number of hosts can fit cleanly within equal network sizes on boundaries. 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) { func (h *HostSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) {


var pfx netip.Prefix
var tgt *big.Int var tgt *big.Int
var splitCidr int
var hosts *big.Int var hosts *big.Int
var sub netip.Prefix var found bool
var subPtr *netip.Prefix var cs *CIDRSplitter
var split []*net.IPNet
var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder)


if h == nil || h.NumberHosts == 0 || h.BaseSplitter == nil || h.network == nil { if h == nil || h.NumberHosts == 0 || h.BaseSplitter == nil || h.network == nil {
return return
} }


if split, err = mapcidr.SplitIPNetByNumber(h.network, int(h.NumberHosts)); err != nil { if err = validate.Struct(h); err != nil {
return return
} }


tgt = big.NewInt(0) pfx, _ = netipx.FromStdIPNet(h.network)
tgt = new(big.Int)
tgt.SetUint64(uint64(h.NumberHosts)) tgt.SetUint64(uint64(h.NumberHosts))


nets = make([]*netip.Prefix, len(split)) if NumAddrsPfx(pfx, h.InclNetAddr, h.InclBcastAddr).Cmp(tgt) < 0 {
for idx, n := range split { // The number of hosts per-subnet exceeds the number of addresses in the specified network.
sub, _ = netipx.FromStdIPNet(n) err = ErrNoNetSpace
hosts = mapcidr.CountIPsInCIDR(false, false, n) return
if hosts == nil || tgt.Cmp(hosts) != 0 { }
/*
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
}

// 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{ err = &SplitErr{
Wrapped: ErrBadNumHosts, Wrapped: ErrBadNumHosts,
Nets: nets, Nets: nets,
Remaining: remaining, Remaining: remaining,
LastSubnet: &sub, LastSubnet: nil,
RequestedPrefixLen: uint8(sub.Bits()), RequestedPrefixLen: uint8(splitCidr),
} }
ipsb.AddPrefix(sub) return
} else {
subPtr = new(netip.Prefix)
*subPtr = sub
nets = append(nets, subPtr)
} }


nets[idx] = new(netip.Prefix) // Then we check the cidr we split on, and check its number of hosts.
*nets[idx] = sub if hosts.Cmp(tgt) != 0 {
err = &SplitErr{
Wrapped: ErrBadNumHosts,
Nets: nets,
Remaining: remaining,
LastSubnet: nil,
RequestedPrefixLen: uint8(splitCidr),
} }

if remaining, err = ipsb.IPSet(); err != nil {
return return
} }



View File

@ -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
}

View File

@ -1,10 +1,9 @@
package netsplit package netsplit


import ( import (
`net` `math`
"net/netip" "net/netip"


`github.com/projectdiscovery/mapcidr`
"go4.org/netipx" "go4.org/netipx"
) )


@ -22,15 +21,16 @@ func (s *SubnetSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet,
var ok bool var ok bool
var pfxLen int var pfxLen int
var base netip.Prefix var base netip.Prefix
var sub netip.Prefix var vlsm *VLSMSplitter
var subPtr *netip.Prefix
var split []*net.IPNet
var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder)


if s == nil || s.BaseSplitter == nil || s.network == nil || s.NumberSubnets == 0 { if s == nil || s.BaseSplitter == nil || s.network == nil || s.NumberSubnets == 0 {
return return
} }


if err = validate.Struct(s); err != nil {
return
}

if base, ok = netipx.FromStdIPNet(s.network); !ok { if base, ok = netipx.FromStdIPNet(s.network); !ok {
err = ErrBadBoundary err = ErrBadBoundary
return return
@ -40,65 +40,48 @@ func (s *SubnetSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet,
return return
} }


if split, err = mapcidr.SplitIPNetIntoN(s.network, int(s.NumberSubnets)); err != nil { // Previously, this used (github.com/projectdiscovery/mapcidr).SplitIPNetIntoN.
return // It no longer does: https://github.com/projectdiscovery/mapcidr/issues/628
} // I am Noticing.


for _, n := range split { // First the number of bits needed is calculated.
if sub, ok = netipx.FromStdIPNet(n); !ok { pfxLen = int(math.Ceil(math.Log2(float64(s.NumberSubnets))))
// We bail early on this error. // And this is then added to the original prefix length to get the new prefix size.
err = &SplitErr{ pfxLen = pfxLen + base.Bits()
Wrapped: ErrBadBoundary, // I don't know how this would happen, but it'd be bad if it did.
Nets: nets, if pfxLen < base.Bits() {
Remaining: remaining, err = ErrBigPrefix
LastSubnet: subPtr,
RequestedPrefixLen: 0,
}
err = ErrBadBoundary
return return
} }
if sub.String() == base.String() { // Likewise.
continue if base.Addr().Is6() {
} ok = pfxLen <= int(maxBitsv6)
if pfxLen == 0 {
pfxLen = sub.Bits()
if nets == nil {
nets = make([]*netip.Prefix, 0)
}
subPtr = new(netip.Prefix)
*subPtr = sub
nets = append(nets, subPtr)
} else { } else {
if sub.Bits() != pfxLen { ok = pfxLen <= int(maxBitsv4)
if err == nil {
// Return this err but don't return early; wait for the populate.
err = &SplitErr{
Wrapped: ErrNoNetSpace,
Nets: nets,
Remaining: remaining,
LastSubnet: subPtr,
RequestedPrefixLen: uint8(pfxLen),
} }
} if !ok {
ipsb.AddPrefix(sub) err = ErrBadPrefix
} else {
subPtr = new(netip.Prefix)
*subPtr = sub
nets = append(nets, subPtr)
}
}
}

if remaining, err = ipsb.IPSet(); err != nil {
return return
} }


if len(nets) < int(s.NumberSubnets) { // We can now VLSM.
err = &SplitErr{ vlsm = &VLSMSplitter{
Wrapped: ErrNoNetSpace, // Ascenting and Explicit are pointless to set as all defined sizes are the same.
Nets: nets, Ascending: false,
Remaining: remaining, Explicit: false,
PrefixLengths: make([]uint8, s.NumberSubnets),
BaseSplitter: s.BaseSplitter,
} }
for i := 0; i < int(s.NumberSubnets); i++ {
vlsm.PrefixLengths[i] = uint8(pfxLen)
}

if nets, remaining, err = vlsm.Split(); err != nil {
return
}

if s.Strict && remaining != nil && remaining.Prefixes() != nil && len(remaining.Prefixes()) > 0 {
err = ErrBadNumNets
return return
} }



View File

@ -2,7 +2,7 @@ package netsplit


import ( import (
"net/netip" "net/netip"
"sort" `sort`


"go4.org/netipx" "go4.org/netipx"
) )
@ -21,7 +21,15 @@ func (v *VLSMSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e
var base netip.Prefix var base netip.Prefix
var sub netip.Prefix var sub netip.Prefix
var subPtr *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 { if err = ValidateSizes(v.network, v.PrefixLengths...); err != nil {
return return
@ -38,10 +46,7 @@ func (v *VLSMSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e
But, as I expected, netipx ftw again. But, as I expected, netipx ftw again.
*/ */


if v == nil || v.PrefixLengths == nil || len(v.PrefixLengths) == 0 || v.BaseSplitter == nil || v.network == nil { if !v.Explicit {
return
}

sort.SliceStable( sort.SliceStable(
v.PrefixLengths, v.PrefixLengths,
func(i, j int) (isBefore bool) { // We use a reverse sorting by default so we get larger prefixes at the beginning. func(i, j int) (isBefore bool) { // We use a reverse sorting by default so we get larger prefixes at the beginning.
@ -53,6 +58,7 @@ func (v *VLSMSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, e
return return
}, },
) )
}


pfxLen, _ = v.network.Mask.Size() pfxLen, _ = v.network.Mask.Size()
pfxLen8 = uint8(pfxLen) pfxLen8 = uint8(pfxLen)

View File

@ -31,7 +31,16 @@ type NetSplitter interface {


// BaseSplitter is used to encapsulate the "parent" network to be split. // BaseSplitter is used to encapsulate the "parent" network to be split.
type BaseSplitter struct { 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 { type CIDRSplitter struct {
// PrefixLength specifies the CIDR/prefix length of the subnets to split out. // 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"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"`
} }


@ -50,7 +63,11 @@ It attempts to evenly distribute addresses amoungs subnets.
*/ */
type HostSplitter struct { type HostSplitter struct {
// NumberHosts is the number of hosts to be placed in each subnet to split out. // 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.
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, 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"` Strict bool `json:"strict" xml:"strict,attr,omitempty" yaml:"Strictly Equal Hosts Per Subnet"`
*BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"`
@ -62,7 +79,7 @@ as cleanly as poossible.
*/ */
type SubnetSplitter struct { type SubnetSplitter struct {
// NumberSubnets indicates the number of subnets to split the network into. // 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, 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"` Strict bool `json:"strict" xml:"strict,attr,omitempty" yaml:"Strictly Equal Subnet Sizes"`
*BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"`
@ -78,9 +95,16 @@ type VLSMSplitter struct {
(ascending order) instead of larger networks/smaller prefixes (descending order). (ascending order) instead of larger networks/smaller prefixes (descending order).
You almost assuredly do not want to do this. 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 `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 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"` *BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"`
} }