almost done ackshually

This commit is contained in:
2025-01-31 17:18:35 -05:00
parent 6dcf5b9e2e
commit b09cb83017
21 changed files with 1646 additions and 3 deletions

12
netsplit/errs.go Normal file
View File

@@ -0,0 +1,12 @@
package netsplit
import "errors"
var (
ErrBadBoundary error = errors.New("subnet does not align on bit boundary")
ErrBadPrefix error = errors.New("prefix is invalid")
ErrBadPrefixLen error = errors.New("prefix length exceeds maximum possible for prefix's inet family")
ErrBadSplitter error = errors.New("invalid or unknown splitter when containing")
ErrBigPrefix error = errors.New("prefix length exceeds remaining network space")
ErrNoNetSpace error = errors.New("reached end of network space before splitting finished")
)

356
netsplit/funcs.go Normal file
View File

@@ -0,0 +1,356 @@
package netsplit
import (
"encoding/json"
"encoding/xml"
"fmt"
"github.com/goccy/go-yaml"
"go4.org/netipx"
"net"
"net/netip"
"strings"
)
/*
AddrExpand expands a netip.Addr's string format.
Like netip.Addr.StringExpanded() but for IPv4 too.
*/
func AddrExpand(ip netip.Addr) (s string) {
var sb *strings.Builder
if ip.IsUnspecified() || !ip.IsValid() {
return
}
if ip.Is6() {
s = ip.StringExpanded()
} else {
// IPv4 we have to do by hand.
sb = new(strings.Builder)
for idx, b := range ip.AsSlice() {
sb.WriteString(fmt.Sprintf("%03d", b))
if idx != net.IPv4len-1 {
sb.WriteString(".")
}
}
s = sb.String()
}
return
}
/*
AddrCompress returns the shortest possible CIDR representation as a string from a netip.Prefix.
Note that IPv6 netip.Prefix.String() already does this automatically, as IPv6 has special condensing rules.
*/
func AddrCompress(pfx *netip.Prefix) (s string) {
var sl []string
var lastNonzero int
if pfx == nil || !pfx.IsValid() || !pfx.Addr().IsValid() {
return
}
if pfx.Addr().Is6() {
s = pfx.String()
return
}
sl = strings.Split(pfx.Addr().String(), ".")
for idx, oct := range sl {
if oct != "0" {
lastNonzero = idx
}
}
s = fmt.Sprintf("%s/%d", strings.Join(sl[:lastNonzero+1], "."), pfx.Bits())
return
}
/*
AddrFmt provides a string representation for an IP (as a netip.Addr).
`f` is the string formatter to use (without the %). For IPv4, you generally want `d`,
for IPv6, you generally want `x`.
`sep` indicates a character to insert every `every` bytes of the mask.
For IPv4, you probably want `.`,
for IPv6 there isn't really a standard representation; CIDR notation is preferred.
Thus for IPv6 you probably want to set sep as blank and/or set `every` to 0.
`segSep` indicates a character sequence to use for segmenting the string.
Specify as an empty string and/or set `everySeg` to 0 to disable.
`every` indicates how many bytes should pass before sep is inserted.
For IPv4, this should be 1.
For IPv6, there isn't really a standard indication but it's recommended to do 2.
Set as 0 or `sep` to an empty string to do no separation characters.
`everySeg` indicates how many *seperations* should pass before segSep is inserted.
Set as 0 or `segSep` to an empty string to do no string segmentation.
*/
func AddrFmt(ip netip.Addr, f, sep, segSep string, every, everySeg uint) (s string) {
var numSegs int
var doSep bool = every > 0
var fs string = "%" + f
var sb *strings.Builder = new(strings.Builder)
if ip.IsUnspecified() || !ip.IsValid() {
return
}
for idx, b := range ip.AsSlice() {
if doSep && idx > 0 {
if idx%int(every) == 0 {
sb.WriteString(sep)
numSegs++
}
if everySeg > 0 {
if numSegs >= int(everySeg) {
sb.WriteString(segSep)
numSegs = 0
}
}
}
fmt.Fprintf(sb, fs, b)
}
s = strings.TrimSpace(sb.String())
return
}
/*
AddrInvert returns an inverted form of netip.Addr as another netip.Addr.
Note that it doesn't really make sense to use this for IPv6.
*/
func AddrInvert(ip netip.Addr) (inverted netip.Addr) {
var b []byte
if !ip.IsValid() {
return
}
b = make([]byte, len([]byte(ip.AsSlice())))
for idx, i := range []byte(ip.AsSlice()) {
b[idx] = ^i
}
inverted, _ = netip.AddrFromSlice(b)
return
}
// Contain takes the results of a NetSplitter and returns a StructuredResults.
func Contain(origPfx *netip.Prefix, nets []*netip.Prefix, remaining *netipx.IPSet, splitter NetSplitter) (s *StructuredResults, err error) {
var rem []netip.Prefix
var sr StructuredResults = StructuredResults{
Original: origPfx,
}
if origPfx == nil {
return
}
if origPfx.Addr() != origPfx.Masked().Addr() {
sr.Canonical = new(netip.Prefix)
*sr.Canonical = origPfx.Masked()
sr.HostAddr = new(netip.Addr)
*sr.HostAddr = origPfx.Addr()
}
if splitter != nil {
sr.Splitter = new(SplitOpts)
switch t := splitter.(type) {
case *CIDRSplitter:
sr.Splitter.CIDR = t
case *HostSplitter:
sr.Splitter.Host = t
case *SubnetSplitter:
sr.Splitter.Subnet = t
case *VLSMSplitter:
sr.Splitter.VLSM = t
default:
err = ErrBadSplitter
return
}
}
if nets != nil {
sr.Allocated = make([]*ContainedResult, len(nets))
for idx, n := range nets {
sr.Allocated[idx] = &ContainedResult{
Network: n,
}
}
}
if remaining != nil {
rem = remaining.Prefixes()
sr.Unallocated = make([]*ContainedResult, len(rem))
for idx, i := range rem {
sr.Unallocated[idx] = &ContainedResult{
Network: new(netip.Prefix),
}
*sr.Unallocated[idx].Network = i
}
}
s = &sr
return
}
/*
MaskExpand expands a net.IPMask's string format.
Like AddrExpand but for netmasks.
*/
func MaskExpand(mask net.IPMask, isIpv6 bool) (s string) {
var sb *strings.Builder
// IPv6 is always expanded in string format, but not split out.
if isIpv6 {
s = MaskFmt(mask, "02x", ":", "", 2, 0)
return
}
sb = new(strings.Builder)
for idx, b := range mask {
sb.WriteString(fmt.Sprintf("%03d", b))
if idx != net.IPv4len-1 {
sb.WriteString(".")
}
}
s = sb.String()
return
}
/*
MaskFmt provides a string representation for a netmask (as a net.IPMask).
Its parameters hold the same significance as in AddrFmt.
*/
func MaskFmt(mask net.IPMask, f, sep, segSep string, every, everySeg uint) (s string) {
var numSegs int
var doSep bool = every > 0
var fs string = "%" + f
var sb *strings.Builder = new(strings.Builder)
if mask == nil || len(mask) == 0 {
return
}
for idx, b := range mask {
if doSep && idx > 0 {
if idx%int(every) == 0 {
sb.WriteString(sep)
numSegs++
}
if everySeg > 0 {
if numSegs >= int(everySeg) {
sb.WriteString(segSep)
numSegs = 0
}
}
}
fmt.Fprintf(sb, fs, b)
}
s = strings.TrimSpace(sb.String())
return
}
/*
MaskInvert returns an inverted form of net.IPMask as another net.IPMask.
Note that it doesn't really make sense to use this for IPv6.
*/
func MaskInvert(mask net.IPMask) (inverted net.IPMask) {
var b []byte
b = make([]byte, len([]byte(mask)))
for idx, i := range []byte(mask) {
b[idx] = ^i
}
inverted = net.IPMask(b)
return
}
// Parse parses b for JSON/XML/YAML and tries to return a StructuredResults from it.
func Parse(b []byte) (s *StructuredResults, err error) {
if b == nil {
return
}
if err = json.Unmarshal(b, &s); err != nil {
if err = xml.Unmarshal(b, &s); err != nil {
if err = yaml.Unmarshal(b, &s); err != nil {
return
} else {
return
}
} else {
return
}
}
return
}
/*
ValidateSizes ensures that none of the prefix lengths in sizes exceeds the maximum possible in pfx.
No-ops with nil error if pfx is nil, sizes is nil, or sizes is empty.
err is also nil if validation succeeds.
If validation fails on a prefix length size, the error will be a SplitErr
with only Wrapped and RequestedPrefixLen fields populated *for the first failing size only*.
*/
func ValidateSizes(pfx *net.IPNet, sizes ...uint8) (err error) {
var ok bool
var addr netip.Addr
var familyMax uint8
if pfx == nil || sizes == nil || len(sizes) == 0 {
return
}
if addr, ok = netipx.FromStdIP(pfx.IP); !ok {
err = ErrBadPrefix
return
}
if addr.Is4() {
familyMax = 32
} else {
familyMax = 128
}
for _, size := range sizes {
if size > familyMax {
err = &SplitErr{
Wrapped: ErrBadPrefixLen,
Nets: nil,
Remaining: nil,
LastSubnet: nil,
RequestedPrefixLen: size,
}
return
}
}
return
}

View File

@@ -0,0 +1,51 @@
package netsplit
import (
"net"
)
// SetParent sets the net.IPNet for a Splitter.
func (b *BaseSplitter) SetParent(pfx net.IPNet) {
b.network = &pfx
}
// MarshalText lets a BaseSplitter conform to an encoding.TextMarshaler.
func (b *BaseSplitter) MarshalText() (text []byte, err error) {
if b == nil || b.network == nil {
return
}
text = []byte(b.network.String())
return
}
/*
UnmarshalText lets a BaseSplitter conform to an encoding.TextUnmarshaler.
This is a potentially lossy operation! Any host bits set in the prefix's address will be lost.
They will not be set if the output was originally generated by `subnetter`.
*/
func (b *BaseSplitter) UnmarshalText(text []byte) (err error) {
var s string
var n *net.IPNet
if text == nil {
return
}
s = string(text)
if _, n, err = net.ParseCIDR(s); err != nil {
return
}
*b = BaseSplitter{
network: n,
}
return
}

View File

@@ -0,0 +1,14 @@
package netsplit
import (
"go4.org/netipx"
"net/netip"
)
// Split splits the network defined in a CIDRSplitter alongside its configuration and performs the subnetting.
func (c *CIDRSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) {
// TODO
return
}

View File

@@ -0,0 +1,14 @@
package netsplit
import (
"go4.org/netipx"
"net/netip"
)
// Split splits the network defined in a HostSplitter alongside its configuration and performs the subnetting.
func (h *HostSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) {
// TODO
return
}

View File

@@ -0,0 +1,14 @@
package netsplit
// Error makes a SplitErr conform to error.
func (s *SplitErr) Error() (errStr string) {
if s == nil {
errStr = "(error unknown; nil error)"
return
}
errStr = s.Wrapped.Error()
return
}

View File

@@ -0,0 +1,73 @@
package netsplit
import (
"go4.org/netipx"
"net/netip"
)
/*
GetSplitter returns the first (should be *only*) non-nill NetSplitter on a StructuredResults.
If none is found, splitter will be nil but no panic/error will occur.
*/
func (s *StructuredResults) GetSplitter() (splitter NetSplitter) {
if s == nil || s.Splitter == nil {
return
}
/*
TODO(?): It'd be nice if I could just reflect .Interface() this
to a NetSplitter but I think I'd then have to typeswitch
into the real type regardless, which is lame.
*/
if s.Splitter.CIDR != nil {
splitter = s.Splitter.CIDR
} else if s.Splitter.Host != nil {
splitter = s.Splitter.Host
} else if s.Splitter.Subnet != nil {
splitter = s.Splitter.Subnet
} else if s.Splitter.VLSM != nil {
splitter = s.Splitter.VLSM
}
return
}
/*
Uncontain returns a set of values that "unstructure" a StructuredResults.
(Essentially the opposite procedure of Contain().)
*/
func (s *StructuredResults) Uncontain() (origPfx *netip.Prefix, nets []*netip.Prefix, remaining *netipx.IPSet, splitter NetSplitter, err error) {
var ipsb *netipx.IPSetBuilder
if s == nil {
return
}
origPfx = s.Original
if s.Allocated != nil {
nets = make([]*netip.Prefix, len(s.Allocated))
for idx, i := range s.Allocated {
nets[idx] = i.Network
}
}
if s.Unallocated != nil {
ipsb = new(netipx.IPSetBuilder)
for _, i := range s.Unallocated {
if i.Network != nil {
ipsb.AddPrefix(*i.Network)
}
}
if remaining, err = ipsb.IPSet(); err != nil {
return
}
}
splitter = s.GetSplitter()
return
}

View File

@@ -0,0 +1,14 @@
package netsplit
import (
"go4.org/netipx"
"net/netip"
)
// Split splits the network defined in a SubnetSplitter alongside its configuration and performs the subnetting.
func (s *SubnetSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) {
// TODO
return
}

View File

@@ -0,0 +1,97 @@
package netsplit
import (
"go4.org/netipx"
"net/netip"
"sort"
)
// Split splits the network defined in a VLSMSplitter alongside its configuration and performs the subnetting.
func (v *VLSMSplitter) Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error) {
var ok bool
var pfxLen int
var pfxLen8 uint8
var base netip.Prefix
var sub netip.Prefix
var subPtr *netip.Prefix
var ipsb *netipx.IPSetBuilder = new(netipx.IPSetBuilder)
if err = ValidateSizes(v.network, v.PrefixLengths...); err != nil {
return
}
/*
I thought about using the following:
* https://pkg.go.dev/net/netip
* https://pkg.go.dev/github.com/sacloud/packages-go/cidr
* https://pkg.go.dev/github.com/projectdiscovery/mapcidr
* https://pkg.go.dev/github.com/EvilSuperstars/go-cidrman
But, as I expected, netipx ftw again.
*/
if v == nil || v.PrefixLengths == nil || len(v.PrefixLengths) == 0 || v.BaseSplitter == nil || v.network == nil {
return
}
sort.SliceStable(
v.PrefixLengths,
func(i, j int) (isBefore bool) { // We use a reverse sorting by default so we get larger prefixes at the beginning.
if v.Ascending {
isBefore = v.PrefixLengths[i] > v.PrefixLengths[j]
} else {
isBefore = v.PrefixLengths[i] < v.PrefixLengths[j]
}
return
},
)
pfxLen, _ = v.network.Mask.Size()
pfxLen8 = uint8(pfxLen)
if base, ok = netipx.FromStdIPNet(v.network); !ok {
err = ErrBadBoundary
return
}
if !base.IsValid() {
err = ErrBadBoundary
return
}
ipsb.AddPrefix(base)
if remaining, err = ipsb.IPSet(); err != nil {
return
}
for _, size := range v.PrefixLengths {
if size < pfxLen8 {
err = &SplitErr{
Wrapped: ErrBigPrefix,
Nets: nets,
Remaining: remaining,
LastSubnet: &sub,
RequestedPrefixLen: size,
}
return
}
if sub, remaining, ok = remaining.RemoveFreePrefix(size); !ok {
err = &SplitErr{
Wrapped: ErrNoNetSpace,
Nets: nets,
Remaining: remaining,
LastSubnet: &sub,
RequestedPrefixLen: size,
}
return
}
subPtr = new(netip.Prefix)
*subPtr = sub
nets = append(nets, subPtr)
}
return
}

112
netsplit/types.go Normal file
View File

@@ -0,0 +1,112 @@
package netsplit
import (
"encoding/xml"
"go4.org/netipx"
"net"
"net/netip"
)
// SplitErr is used to wrap an error with context surrounding when/how that error was encountered.
type SplitErr struct {
// Wrapped is the originating error during a split (or other parsing operation).
Wrapped error
// Nets are the subnets parsed out/collected so far.
Nets []*netip.Prefix
// Remaining is an IPSet of subnets/addresses that haven't been, or were unable to be, split out.
Remaining *netipx.IPSet
// LastSubnet is the most recently split out subnet.
LastSubnet *netip.Prefix
// RequestedPrefixLen is the network prefix length size, if relevant, that was attempted to be split out of Remaining.
RequestedPrefixLen uint8
}
// NetSplitter is used to split a network into multiple nets (and any remaining prefixes/addresses that didn't fit).
type NetSplitter interface {
SetParent(pfx net.IPNet)
Split() (nets []*netip.Prefix, remaining *netipx.IPSet, err error)
}
// BaseSplitter is used to encapsulate the "parent" network to be split.
type BaseSplitter struct {
network *net.IPNet
}
/*
CIDRSplitter is used to split a network based on a fixed prefix size.
It attemps to split the network into as many networks of size PrefixLength as cleanly as possible.
*/
type CIDRSplitter struct {
// PrefixLength specifies the CIDR/prefix length of the subnets to split out.
PrefixLength uint8 `json:"prefix" xml:"prefix,attr" yaml:"network Prefix Length"`
*BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"`
}
/*
HostSplitter is used to split a network based on total number of hosts.
It attempts to evenly distribute addresses amoungs subnets.
*/
type HostSplitter struct {
// NumberHosts is the number of hosts to be placed in each subnet to split out.
NumberHosts uint `json:"hosts" xml:"hosts,attr" yaml:"Number of Hosts Per Subnet"`
*BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"`
}
/*
SubnetSplitter is used to split a network into a specific number of subnets of equal prefix lengths
as cleanly as poossible.
*/
type SubnetSplitter struct {
// NumberSubnets indicates the number of subnets to split the network into.
NumberSubnets uint `json:"nets" xml:"nets,attr" yaml:"Number of Target Subnets"`
*BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"`
}
/*
VLSMSplitter is used to split a network via VLSM (Variable-Length Subnet Masks) into multiple PrefixLengths,
in which there are multiple desired subnets of varying lengths.
*/
type VLSMSplitter struct {
/*
Ascending, if true, will subnet smaller networks/larger prefixes near the beginning
(ascending order) instead of larger networks/smaller prefixes (descending order).
You almost assuredly do not want to do this.
*/
Ascending bool
// PrefixLengths contains the prefix lengths of each subnet to split out from the network.
PrefixLengths []uint8 `json:"prefixes" xml:"prefixes>prefix" yaml:"Prefix Lengths"`
*BaseSplitter `json:"net" xml:"net,omitempty" yaml:"network,omitempty"`
}
/*
StructuredResults is used for serializing prefixes into a structured/defined data format.
*/
type StructuredResults struct {
XMLName xml.Name `json:"-" xml:"results" yaml:"-"`
// Original is the provided parent network/prefix.
Original *netip.Prefix `json:"orig" xml:"orig,attr,omitempty" yaml:"Original/Parent network"`
// HostAddr is nil if Original falls on a network prefix boundary, otherwise it is the specified host address.
HostAddr *netip.Addr `json:"host" xml:"host,attr,omitempty" yaml:"Host Address,omitempty"`
// Canonical is the canonical network of Original (e.g. with host bits masked out). It is nil if Original.Addr() falls on the (lower) boundary.
Canonical *netip.Prefix `json:"masked" xml:"masked,attr,omitempty" yaml:"Bound Original/Parent network"`
// Splitter contains the spplitter and its options used to split the network.
Splitter *SplitOpts `json:"splitter" xml:"splitter,omitempty" yaml:"Splitter,omitempty"`
// Allocated contains valid subnet(s) in Original per the user-specified subnetting rules.
Allocated []*ContainedResult `json:"subnets" xml:"subnets>subnet,omitempty" yaml:"Subnets"`
// Unallocated contains subnets from Original that did not meet the splitting criteria or were left over from the split operation.
Unallocated []*ContainedResult `json:"remaining" xml:"remaining>subnet,omitempty" yaml:"Remaining/Unallocated/Left Over,omitempty"`
}
type SplitOpts struct {
XMLName xml.Name `json:"-" xml:"splitter" yaml:"-"`
CIDR *CIDRSplitter `json:"cidr,omitempty" xml:"cidr,omitempty" yaml:"CIDR Splitter,omitempty"`
Host *HostSplitter `json:"host,omitempty" xml:"host,omitempty" yaml:"Host Splitter,omitempty"`
Subnet *SubnetSplitter `json:"subnet,omitempty" xml:"subnet,omitempty" yaml:"Subnet Splitter,omitempty"`
VLSM *VLSMSplitter `json:"vlsm,omitempty" xml:"vlsm,omitempty" yaml:"VLSM Splitter,omitempty"`
}
// ContainedResult is a single Network (either an allocated subnet or a remaining block).
type ContainedResult struct {
XMLName xml.Name `json:"-" yaml:"-" xml:"subnet"`
Network *netip.Prefix `json:"net" xml:"net,attr,omitempty" yaml:"network,omitempty"`
}