4 Commits

Author SHA1 Message Date
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
13 changed files with 373 additions and 20 deletions

View File

@@ -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
- split-hosts returns the entire original subnet as an unallocated subnet if hosts exceed the number of assignable addrs.

View File

@@ -5,8 +5,9 @@ type Args struct {
SplitCIDR SplitCIDRArgs `command:"split-cidr" alias:"se" description:"Split a network into as many equal subnets of prefix size N as possible." 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"` 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"`
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"`
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 +17,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,7 +27,11 @@ 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."`
Network Net `positional-args:"yes" required:"true" description:"The network/parent prefix to operate on." validate:"required"` }
type common struct {
commonBase
Network Net `positional-args:"yes" required:"true" description:"The network/parent prefix to operate on." validate:"required"`
} }
type reservedArgs struct { type reservedArgs struct {
@@ -44,8 +49,17 @@ 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 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"`
} `positional-args:"yes" required:"2" validate:"required"`
}
type ParseArgs struct { type ParseArgs struct {
splitArgs 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=-"`
} }
@@ -88,9 +102,11 @@ 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."`
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."` 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."`
Sizes []uint8 `short:"s" long:"size" required:"true" description:"Prefix lengths. May be specified multiple times." validate:"required"` // 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

@@ -236,7 +236,9 @@ func printNets(orig *netip.Prefix, origNet *net.IPNet, nets []*netip.Prefix, rem
if args == nil { if args == nil {
args = &common{ args = &common{
Separator: "\n", commonBase: commonBase{
Separator: "\n",
},
} }
} }
fmts = sectFmts[args.Plain] fmts = sectFmts[args.Plain]
@@ -471,7 +473,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
} }

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

@@ -26,14 +26,18 @@ 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 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)
@@ -77,8 +81,47 @@ func main() {
log.Panicln(err) log.Panicln(err)
} }
return return
case "num-nets":
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 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":
// 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 {
@@ -116,7 +159,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)
} }
@@ -165,15 +213,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,
Explicit: args.VLSM.Explicit, Explicit: args.VLSM.Explicit,
PrefixLengths: args.VLSM.Sizes, PrefixLengths: vlsmSizes,
BaseSplitter: new(netsplit.BaseSplitter), BaseSplitter: new(netsplit.BaseSplitter),
} }
default: default:

View File

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

View File

@@ -8,6 +8,12 @@ import (
`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

View File

@@ -4,6 +4,7 @@ import (
"encoding/json" "encoding/json"
"encoding/xml" "encoding/xml"
"fmt" "fmt"
`math`
"net" "net"
"net/netip" "net/netip"
"strings" "strings"
@@ -375,6 +376,38 @@ func MaskInvert(mask net.IPMask) (inverted net.IPMask) {
return 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
}
if network > maxBitsv4 {
ipv6Only = true
}
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

@@ -31,7 +31,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
} }
@@ -48,10 +48,10 @@ 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 = sub
nets = append(nets, subPtr)
} }
subPtr = new(netip.Prefix)
*subPtr = sub
nets = append(nets, subPtr)
} }
return return