diff --git a/docs-src/content/functions/net.yml b/docs-src/content/functions/net.yml index 67b7664a9..45a9b1ec1 100644 --- a/docs-src/content/functions/net.yml +++ b/docs-src/content/functions/net.yml @@ -126,7 +126,27 @@ funcs: [ "v=spf1 -all" ] + - name: net.ParseAddr + description: | + Parse the given string as an IP address (a + [`netip.Addr`](https://pkg.go.dev/net/netip#Addr)). + + Any of `netip.Addr`'s methods may be called on the resulting value. See + [the docs](https://pkg.go.dev/net/netip#Addr) for details. + pipeline: true + arguments: + - name: addr + required: true + description: The IP string to parse. It must be either an IPv4 or IPv6 address. + examples: + - | + $ gomplate -i '{{ (net.ParseAddr "192.168.0.1").IsPrivate }}' + true + $ gomplate -i '{{ $ip := net.ParseAddr (net.LookupIP "example.com") -}} + {{ $ip.Prefix 12 }}' + 93.176.0.0/12 - name: net.ParseIP + deprecated: Use [`net.ParseAddr`](#net-parseaddr) instead. description: | Parse the given string as an IP address (a `netaddr.IP` from the [`inet.af/netaddr`](https://pkg.go.dev/inet.af/netaddr) package). @@ -145,7 +165,31 @@ funcs: $ gomplate -i '{{ $ip := net.ParseIP (net.LookupIP "example.com") -}} {{ $ip.Prefix 12 }}' 93.176.0.0/12 + - name: net.ParsePrefix + description: | + Parse the given string as an IP address prefix (CIDR) representing an IP + network (a [`netip.Prefix`](https://pkg.go.dev/net/netip#Prefix)). + + The string can be in the form `"192.168.1.0/24"` or `"2001::db8::/32"`, + the CIDR notations defined in [RFC 4632][] and [RFC 4291][]. + + Any of `netip.Prefix`'s methods may be called on the resulting value. See + [the docs](https://pkg.go.dev/net/netip#Prefix) for details. + pipeline: true + arguments: + - name: prefix + required: true + description: The IP address prefix to parse. It must represent either an IPv4 or IPv6 prefix, containing a `/`. + examples: + - | + $ gomplate -i '{{ (net.ParsePrefix "192.168.0.0/24").Range }}' + 192.168.0.0-192.168.0.255 + $ gomplate -i '{{ $ip := net.ParseAddr (net.LookupIP "example.com") -}} + {{ $net := net.ParsePrefix "93.184.0.0/16" -}} + {{ $net.Contains $ip }}' + true - name: net.ParseIPPrefix + deprecated: Use [`net.ParsePrefix`](#net-parseprefix) instead. description: | Parse the given string as an IP address prefix (CIDR) representing an IP network (a `netaddr.IPPrefix` from the @@ -172,7 +216,36 @@ funcs: $ gomplate -i '{{ $net := net.ParseIPPrefix "93.184.0.0/12" -}} {{ $net.Range }}' 93.176.0.0-93.191.255.255 + - name: net.ParseRange + experimental: true + description: | + Parse the given string as an inclusive range of IP addresses from the same + address family (a [`netipx.IPRange`](https://pkg.go.dev/go4.org/netipx#IPRange) + from the [`go4.org/netipx`](https://pkg.go.dev/go4.org/netipx) module). + + The string must contain a hyphen (`-`). + + Any of `netipx.IPRange`'s methods may be called on the resulting value. + See [the docs](https://pkg.go.dev/go4.org/netipx#IPRange) for details. + + Note that this function is experimental for now, because it uses a + [third-party module](https://pkg.go.dev/go4.org/netipx) which may be + brought into the standard library in the future, which may require + breaking changes to this function. + pipeline: true + arguments: + - name: iprange + required: true + description: The IP address range to parse. It must represent either an IPv4 or IPv6 range, containing a `-`. + examples: + - | + $ gomplate -i '{{ (net.ParseRange "192.168.0.0-192.168.0.255").To }}' + 192.168.0.255 + $ gomplate -i '{{ $range := net.ParseRange "1.2.3.0-1.2.3.233" -}} + {{ $range.Prefixes }}' + [1.2.3.0/25 1.2.3.128/26 1.2.3.192/27 1.2.3.224/29 1.2.3.232/31] - name: net.ParseIPRange + deprecated: Use [`net.ParseRange`](#net-parserange) instead. description: | Parse the given string as an inclusive range of IP addresses from the same address family (a `netaddr.IPRange` from the [`inet.af/netaddr`][] package). diff --git a/docs/content/functions/net.md b/docs/content/functions/net.md index ca0d8da1d..371c5be48 100644 --- a/docs/content/functions/net.md +++ b/docs/content/functions/net.md @@ -213,7 +213,41 @@ $ gomplate -i '{{net.LookupTXT "example.com" | data.ToJSONPretty " " }}' ] ``` -## `net.ParseIP` +## `net.ParseAddr` + +Parse the given string as an IP address (a +[`netip.Addr`](https://pkg.go.dev/net/netip#Addr)). + +Any of `netip.Addr`'s methods may be called on the resulting value. See +[the docs](https://pkg.go.dev/net/netip#Addr) for details. + +### Usage + +```go +net.ParseAddr addr +``` +```go +addr | net.ParseAddr +``` + +### Arguments + +| name | description | +|------|-------------| +| `addr` | _(required)_ The IP string to parse. It must be either an IPv4 or IPv6 address. | + +### Examples + +```console +$ gomplate -i '{{ (net.ParseAddr "192.168.0.1").IsPrivate }}' +true +$ gomplate -i '{{ $ip := net.ParseAddr (net.LookupIP "example.com") -}} + {{ $ip.Prefix 12 }}' +93.176.0.0/12 +``` + +## `net.ParseIP` _(deprecated)_ +**Deprecation Notice:** Use [`net.ParseAddr`](#net-parseaddr) instead. Parse the given string as an IP address (a `netaddr.IP` from the [`inet.af/netaddr`](https://pkg.go.dev/inet.af/netaddr) package). @@ -246,7 +280,45 @@ $ gomplate -i '{{ $ip := net.ParseIP (net.LookupIP "example.com") -}} 93.176.0.0/12 ``` -## `net.ParseIPPrefix` +## `net.ParsePrefix` + +Parse the given string as an IP address prefix (CIDR) representing an IP +network (a [`netip.Prefix`](https://pkg.go.dev/net/netip#Prefix)). + +The string can be in the form `"192.168.1.0/24"` or `"2001::db8::/32"`, +the CIDR notations defined in [RFC 4632][] and [RFC 4291][]. + +Any of `netip.Prefix`'s methods may be called on the resulting value. See +[the docs](https://pkg.go.dev/net/netip#Prefix) for details. + +### Usage + +```go +net.ParsePrefix prefix +``` +```go +prefix | net.ParsePrefix +``` + +### Arguments + +| name | description | +|------|-------------| +| `prefix` | _(required)_ The IP address prefix to parse. It must represent either an IPv4 or IPv6 prefix, containing a `/`. | + +### Examples + +```console +$ gomplate -i '{{ (net.ParsePrefix "192.168.0.0/24").Range }}' +192.168.0.0-192.168.0.255 +$ gomplate -i '{{ $ip := net.ParseAddr (net.LookupIP "example.com") -}} + {{ $net := net.ParsePrefix "93.184.0.0/16" -}} + {{ $net.Contains $ip }}' +true +``` + +## `net.ParseIPPrefix` _(deprecated)_ +**Deprecation Notice:** Use [`net.ParsePrefix`](#net-parseprefix) instead. Parse the given string as an IP address prefix (CIDR) representing an IP network (a `netaddr.IPPrefix` from the @@ -287,7 +359,52 @@ $ gomplate -i '{{ $net := net.ParseIPPrefix "93.184.0.0/12" -}} 93.176.0.0-93.191.255.255 ``` -## `net.ParseIPRange` +## `net.ParseRange` _(experimental)_ +**Experimental:** This function is [_experimental_][experimental] and may be enabled with the [`--experimental`][experimental] flag. + +[experimental]: ../config/#experimental + +Parse the given string as an inclusive range of IP addresses from the same +address family (a [`netipx.IPRange`](https://pkg.go.dev/go4.org/netipx#IPRange) +from the [`go4.org/netipx`](https://pkg.go.dev/go4.org/netipx) module). + +The string must contain a hyphen (`-`). + +Any of `netipx.IPRange`'s methods may be called on the resulting value. +See [the docs](https://pkg.go.dev/go4.org/netipx#IPRange) for details. + +Note that this function is experimental for now, because it uses a +[third-party module](https://pkg.go.dev/go4.org/netipx) which may be +brought into the standard library in the future, which may require +breaking changes to this function. + +### Usage + +```go +net.ParseRange iprange +``` +```go +iprange | net.ParseRange +``` + +### Arguments + +| name | description | +|------|-------------| +| `iprange` | _(required)_ The IP address range to parse. It must represent either an IPv4 or IPv6 range, containing a `-`. | + +### Examples + +```console +$ gomplate -i '{{ (net.ParseRange "192.168.0.0-192.168.0.255").To }}' +192.168.0.255 +$ gomplate -i '{{ $range := net.ParseRange "1.2.3.0-1.2.3.233" -}} + {{ $range.Prefixes }}' +[1.2.3.0/25 1.2.3.128/26 1.2.3.192/27 1.2.3.224/29 1.2.3.232/31] +``` + +## `net.ParseIPRange` _(deprecated)_ +**Deprecation Notice:** Use [`net.ParseRange`](#net-parserange) instead. Parse the given string as an inclusive range of IP addresses from the same address family (a `netaddr.IPRange` from the [`inet.af/netaddr`][] package). diff --git a/funcs/net.go b/funcs/net.go index 0d87b98aa..ab692cc1f 100644 --- a/funcs/net.go +++ b/funcs/net.go @@ -2,14 +2,16 @@ package funcs import ( "context" + "fmt" "math/big" stdnet "net" "net/netip" - "github.com/apparentlymart/go-cidr/cidr" "github.com/hairyhenderson/gomplate/v3/conv" + "github.com/hairyhenderson/gomplate/v3/internal/cidr" + "github.com/hairyhenderson/gomplate/v3/internal/deprecated" "github.com/hairyhenderson/gomplate/v3/net" - "github.com/pkg/errors" + "go4.org/netipx" "inet.af/netaddr" ) @@ -73,46 +75,76 @@ func (f NetFuncs) LookupTXT(name interface{}) ([]string, error) { } // ParseIP - -func (f NetFuncs) ParseIP(ip interface{}) (netaddr.IP, error) { +// +// Deprecated: use [ParseAddr] instead +func (f *NetFuncs) ParseIP(ip interface{}) (netaddr.IP, error) { + deprecated.WarnDeprecated(f.ctx, "net.ParseIP is deprecated - use net.ParseAddr instead") return netaddr.ParseIP(conv.ToString(ip)) } // ParseIPPrefix - -func (f NetFuncs) ParseIPPrefix(ipprefix interface{}) (netaddr.IPPrefix, error) { +// +// Deprecated: use [ParsePrefix] instead +func (f *NetFuncs) ParseIPPrefix(ipprefix interface{}) (netaddr.IPPrefix, error) { + deprecated.WarnDeprecated(f.ctx, "net.ParseIPPrefix is deprecated - use net.ParsePrefix instead") return netaddr.ParseIPPrefix(conv.ToString(ipprefix)) } // ParseIPRange - -func (f NetFuncs) ParseIPRange(iprange interface{}) (netaddr.IPRange, error) { +// +// Deprecated: use [ParseRange] instead +func (f *NetFuncs) ParseIPRange(iprange interface{}) (netaddr.IPRange, error) { + deprecated.WarnDeprecated(f.ctx, "net.ParseIPRange is deprecated - use net.ParseRange instead") return netaddr.ParseIPRange(conv.ToString(iprange)) } -func (f NetFuncs) parseStdnetIPNet(prefix interface{}) (*stdnet.IPNet, error) { - switch p := prefix.(type) { - case *stdnet.IPNet: - return p, nil - case netaddr.IPPrefix: - return p.Masked().IPNet(), nil - case netip.Prefix: - net := &stdnet.IPNet{ - IP: p.Masked().Addr().AsSlice(), - Mask: stdnet.CIDRMask(p.Bits(), p.Addr().BitLen()), - } - return net, nil - default: - _, network, err := stdnet.ParseCIDR(conv.ToString(prefix)) - return network, err - } +// ParseAddr - +func (f NetFuncs) ParseAddr(ip interface{}) (netip.Addr, error) { + return netip.ParseAddr(conv.ToString(ip)) } +// ParsePrefix - +func (f NetFuncs) ParsePrefix(ipprefix interface{}) (netip.Prefix, error) { + return netip.ParsePrefix(conv.ToString(ipprefix)) +} + +// ParseRange - +// +// Experimental: this API may change in the future +func (f NetFuncs) ParseRange(iprange interface{}) (netipx.IPRange, error) { + return netipx.ParseIPRange(conv.ToString(iprange)) +} + +// func (f *NetFuncs) parseStdnetIPNet(prefix interface{}) (*stdnet.IPNet, error) { +// switch p := prefix.(type) { +// case *stdnet.IPNet: +// return p, nil +// case netaddr.IPPrefix: +// deprecated.WarnDeprecated(f.ctx, +// "support for netaddr.IPPrefix is deprecated - use net.ParsePrefix to produce a netip.Prefix instead") +// return p.Masked().IPNet(), nil +// case netip.Prefix: +// net := &stdnet.IPNet{ +// IP: p.Masked().Addr().AsSlice(), +// Mask: stdnet.CIDRMask(p.Bits(), p.Addr().BitLen()), +// } +// return net, nil +// default: +// _, network, err := stdnet.ParseCIDR(conv.ToString(prefix)) +// return network, err +// } +// } + // TODO: look at using this instead of parseStdnetIPNet // //nolint:unused -func (f NetFuncs) parseNetipPrefix(prefix interface{}) (netip.Prefix, error) { +func (f *NetFuncs) parseNetipPrefix(prefix interface{}) (netip.Prefix, error) { switch p := prefix.(type) { case *stdnet.IPNet: return f.ipPrefixFromIPNet(p), nil case netaddr.IPPrefix: + deprecated.WarnDeprecated(f.ctx, + "support for netaddr.IPPrefix is deprecated - use net.ParsePrefix to produce a netip.Prefix instead") return f.ipPrefixFromIPNet(p.Masked().IPNet()), nil case netip.Prefix: return p, nil @@ -121,10 +153,10 @@ func (f NetFuncs) parseNetipPrefix(prefix interface{}) (netip.Prefix, error) { } } -func (f NetFuncs) ipFromNetIP(n stdnet.IP) netip.Addr { - ip, _ := netip.AddrFromSlice(n) - return ip -} +// func (f NetFuncs) ipFromNetIP(n stdnet.IP) netip.Addr { +// ip, _ := netip.AddrFromSlice(n) +// return ip +// } func (f NetFuncs) ipPrefixFromIPNet(n *stdnet.IPNet) netip.Prefix { ip, _ := netip.AddrFromSlice(n.IP) @@ -134,52 +166,62 @@ func (f NetFuncs) ipPrefixFromIPNet(n *stdnet.IPNet) netip.Prefix { // CIDRHost - // Experimental! -func (f NetFuncs) CIDRHost(hostnum interface{}, prefix interface{}) (netip.Addr, error) { +func (f *NetFuncs) CIDRHost(hostnum interface{}, prefix interface{}) (netip.Addr, error) { if err := checkExperimental(f.ctx); err != nil { return netip.Addr{}, err } - network, err := f.parseStdnetIPNet(prefix) + network, err := f.parseNetipPrefix(prefix) if err != nil { return netip.Addr{}, err } ip, err := cidr.HostBig(network, big.NewInt(conv.ToInt64(hostnum))) - return f.ipFromNetIP(ip), err + return ip, err } // CIDRNetmask - // Experimental! -func (f NetFuncs) CIDRNetmask(prefix interface{}) (netip.Addr, error) { +func (f *NetFuncs) CIDRNetmask(prefix interface{}) (netip.Addr, error) { if err := checkExperimental(f.ctx); err != nil { return netip.Addr{}, err } - network, err := f.parseStdnetIPNet(prefix) + p, err := f.parseNetipPrefix(prefix) if err != nil { return netip.Addr{}, err } - netmask := stdnet.IP(network.Mask) - return f.ipFromNetIP(netmask), nil + // fill an appropriately sized byte slice with as many 1s as prefix bits + b := make([]byte, p.Addr().BitLen()/8) + for i := 0; i < p.Bits(); i++ { + b[i/8] |= 1 << uint(7-i%8) + } + + m, ok := netip.AddrFromSlice(b) + if !ok { + return netip.Addr{}, fmt.Errorf("invalid netmask") + } + + return m, nil } // CIDRSubnets - // Experimental! -func (f NetFuncs) CIDRSubnets(newbits interface{}, prefix interface{}) ([]netip.Prefix, error) { +func (f *NetFuncs) CIDRSubnets(newbits interface{}, prefix interface{}) ([]netip.Prefix, error) { if err := checkExperimental(f.ctx); err != nil { return nil, err } - network, err := f.parseStdnetIPNet(prefix) + network, err := f.parseNetipPrefix(prefix) if err != nil { return nil, err } nBits := conv.ToInt(newbits) if nBits < 1 { - return nil, errors.Errorf("must extend prefix by at least one bit") + return nil, fmt.Errorf("must extend prefix by at least one bit") } maxNetNum := int64(1 << uint64(nBits)) @@ -189,7 +231,7 @@ func (f NetFuncs) CIDRSubnets(newbits interface{}, prefix interface{}) ([]netip. if err != nil { return nil, err } - retValues[i] = f.ipPrefixFromIPNet(subnet) + retValues[i] = subnet } return retValues, nil @@ -197,22 +239,22 @@ func (f NetFuncs) CIDRSubnets(newbits interface{}, prefix interface{}) ([]netip. // CIDRSubnetSizes - // Experimental! -func (f NetFuncs) CIDRSubnetSizes(args ...interface{}) ([]netip.Prefix, error) { +func (f *NetFuncs) CIDRSubnetSizes(args ...interface{}) ([]netip.Prefix, error) { if err := checkExperimental(f.ctx); err != nil { return nil, err } if len(args) < 2 { - return nil, errors.Errorf("wrong number of args: want 2 or more, got %d", len(args)) + return nil, fmt.Errorf("wrong number of args: want 2 or more, got %d", len(args)) } - network, err := f.parseStdnetIPNet(args[len(args)-1]) + network, err := f.parseNetipPrefix(args[len(args)-1]) if err != nil { return nil, err } newbits := conv.ToInts(args[:len(args)-1]...) - startPrefixLen, _ := network.Mask.Size() + startPrefixLen := network.Bits() firstLength := newbits[0] firstLength += startPrefixLen @@ -222,38 +264,38 @@ func (f NetFuncs) CIDRSubnetSizes(args ...interface{}) ([]netip.Prefix, error) { for i, length := range newbits { if length < 1 { - return nil, errors.Errorf("must extend prefix by at least one bit") + return nil, fmt.Errorf("must extend prefix by at least one bit") } // For portability with 32-bit systems where the subnet number // will be a 32-bit int, we only allow extension of 32 bits in // one call even if we're running on a 64-bit machine. // (Of course, this is significant only for IPv6.) if length > 32 { - return nil, errors.Errorf("may not extend prefix by more than 32 bits") + return nil, fmt.Errorf("may not extend prefix by more than 32 bits") } length += startPrefixLen - if length > (len(network.IP) * 8) { + if length > network.Addr().BitLen() { protocol := "IP" - switch len(network.IP) { - case stdnet.IPv4len: + switch { + case network.Addr().Is4(): protocol = "IPv4" - case stdnet.IPv6len: + case network.Addr().Is6(): protocol = "IPv6" } - return nil, errors.Errorf("would extend prefix to %d bits, which is too long for an %s address", length, protocol) + return nil, fmt.Errorf("would extend prefix to %d bits, which is too long for an %s address", length, protocol) } next, rollover := cidr.NextSubnet(current, length) - if rollover || !network.Contains(next.IP) { + if rollover || !network.Contains(next.Addr()) { // If we run out of suffix bits in the base CIDR prefix then // NextSubnet will start incrementing the prefix bits, which // we don't allow because it would then allocate addresses // outside of the caller's given prefix. - return nil, errors.Errorf("not enough remaining address space for a subnet with a prefix of %d bits after %s", length, current.String()) + return nil, fmt.Errorf("not enough remaining address space for a subnet with a prefix of %d bits after %s", length, current.String()) } current = next - retValues[i] = f.ipPrefixFromIPNet(current) + retValues[i] = current } return retValues, nil diff --git a/funcs/net_test.go b/funcs/net_test.go index e8914fa6b..642a356c5 100644 --- a/funcs/net_test.go +++ b/funcs/net_test.go @@ -9,6 +9,7 @@ import ( "github.com/hairyhenderson/gomplate/v3/internal/config" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "inet.af/netaddr" ) @@ -39,12 +40,12 @@ func TestNetLookupIP(t *testing.T) { func TestParseIP(t *testing.T) { t.Parallel() - n := NetFuncs{} + n := testNetNS() _, err := n.ParseIP("not an IP") assert.Error(t, err) ip, err := n.ParseIP("2001:470:20::2") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, netaddr.IPFrom16([16]byte{ 0x20, 0x01, 0x04, 0x70, 0, 0x20, 0, 0, @@ -56,7 +57,7 @@ func TestParseIP(t *testing.T) { func TestParseIPPrefix(t *testing.T) { t.Parallel() - n := NetFuncs{} + n := testNetNS() _, err := n.ParseIPPrefix("not an IP") assert.Error(t, err) @@ -64,14 +65,14 @@ func TestParseIPPrefix(t *testing.T) { assert.Error(t, err) ipprefix, err := n.ParseIPPrefix("192.168.0.2/28") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "192.168.0.0/28", ipprefix.Masked().String()) } func TestParseIPRange(t *testing.T) { t.Parallel() - n := NetFuncs{} + n := testNetNS() _, err := n.ParseIPRange("not an IP") assert.Error(t, err) @@ -79,10 +80,56 @@ func TestParseIPRange(t *testing.T) { assert.Error(t, err) iprange, err := n.ParseIPRange("192.168.0.2-192.168.23.255") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "192.168.0.2-192.168.23.255", iprange.String()) } +func TestParseAddr(t *testing.T) { + t.Parallel() + + n := testNetNS() + _, err := n.ParseAddr("not an IP") + assert.Error(t, err) + + ip, err := n.ParseAddr("2001:470:20::2") + require.NoError(t, err) + assert.Equal(t, netip.AddrFrom16([16]byte{ + 0x20, 0x01, 0x04, 0x70, + 0, 0x20, 0, 0, + 0, 0, 0, 0, + 0, 0, 0, 0x02, + }), ip) +} + +func TestParsePrefix(t *testing.T) { + t.Parallel() + + n := testNetNS() + _, err := n.ParsePrefix("not an IP") + assert.Error(t, err) + + _, err = n.ParsePrefix("1.1.1.1") + assert.Error(t, err) + + ipprefix, err := n.ParsePrefix("192.168.0.2/28") + require.NoError(t, err) + assert.Equal(t, "192.168.0.0/28", ipprefix.Masked().String()) +} + +func TestParseRange(t *testing.T) { + t.Parallel() + + n := testNetNS() + _, err := n.ParseRange("not an IP") + assert.Error(t, err) + + _, err = n.ParseRange("1.1.1.1") + assert.Error(t, err) + + iprange, err := n.ParseRange("192.168.0.2-192.168.23.255") + require.NoError(t, err) + assert.Equal(t, "192.168.0.2-192.168.23.255", iprange.String()) +} func testNetNS() *NetFuncs { return &NetFuncs{ctx: config.SetExperimental(context.Background())} } @@ -94,48 +141,48 @@ func TestCIDRHost(t *testing.T) { _, netIP, _ := stdnet.ParseCIDR("10.12.127.0/20") ip, err := n.CIDRHost(16, netIP) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "10.12.112.16", ip.String()) ip, err = n.CIDRHost(268, netIP) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "10.12.113.12", ip.String()) _, netIP, _ = stdnet.ParseCIDR("fd00:fd12:3456:7890:00a2::/72") ip, err = n.CIDRHost(34, netIP) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "fd00:fd12:3456:7890::22", ip.String()) // inet.af/netaddr.IPPrefix ipPrefix, _ := n.ParseIPPrefix("10.12.127.0/20") ip, err = n.CIDRHost(16, ipPrefix) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "10.12.112.16", ip.String()) ip, err = n.CIDRHost(268, ipPrefix) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "10.12.113.12", ip.String()) ipPrefix, _ = n.ParseIPPrefix("fd00:fd12:3456:7890:00a2::/72") ip, err = n.CIDRHost(34, ipPrefix) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "fd00:fd12:3456:7890::22", ip.String()) // net/netip.Prefix prefix := netip.MustParsePrefix("10.12.127.0/20") ip, err = n.CIDRHost(16, prefix) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "10.12.112.16", ip.String()) ip, err = n.CIDRHost(268, prefix) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "10.12.113.12", ip.String()) prefix = netip.MustParsePrefix("fd00:fd12:3456:7890:00a2::/72") ip, err = n.CIDRHost(34, prefix) - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "fd00:fd12:3456:7890::22", ip.String()) } @@ -143,11 +190,11 @@ func TestCIDRNetmask(t *testing.T) { n := testNetNS() ip, err := n.CIDRNetmask("10.0.0.0/12") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "255.240.0.0", ip.String()) ip, err = n.CIDRNetmask("fd00:fd12:3456:7890:00a2::/72") - assert.NoError(t, err) + require.NoError(t, err) assert.Equal(t, "ffff:ffff:ffff:ffff:ff00::", ip.String()) } @@ -156,11 +203,11 @@ func TestCIDRSubnets(t *testing.T) { network := netip.MustParsePrefix("10.0.0.0/16") subnets, err := n.CIDRSubnets(-1, network) - assert.Nil(t, subnets) assert.Error(t, err) + assert.Nil(t, subnets) subnets, err = n.CIDRSubnets(2, network) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, subnets, 4) assert.Equal(t, "10.0.0.0/18", subnets[0].String()) assert.Equal(t, "10.0.64.0/18", subnets[1].String()) @@ -170,25 +217,50 @@ func TestCIDRSubnets(t *testing.T) { func TestCIDRSubnetSizes(t *testing.T) { n := testNetNS() - network := netip.MustParsePrefix("10.1.0.0/16") - subnets, err := n.CIDRSubnetSizes(network) - assert.Nil(t, subnets) + subnets, err := n.CIDRSubnetSizes(netip.MustParsePrefix("10.1.0.0/16")) assert.Error(t, err) - - subnets, err = n.CIDRSubnetSizes(32, network) assert.Nil(t, subnets) + + subnets, err = n.CIDRSubnetSizes(32, netip.MustParsePrefix("10.1.0.0/16")) assert.Error(t, err) + assert.Nil(t, subnets) - subnets, err = n.CIDRSubnetSizes(-1, network) + subnets, err = n.CIDRSubnetSizes(127, netip.MustParsePrefix("ffff::/48")) + assert.Error(t, err) assert.Nil(t, subnets) + + subnets, err = n.CIDRSubnetSizes(-1, netip.MustParsePrefix("10.1.0.0/16")) assert.Error(t, err) + assert.Nil(t, subnets) + + network := netip.MustParsePrefix("8000::/1") + subnets, err = n.CIDRSubnetSizes(1, 2, 2, network) + require.NoError(t, err) + assert.Len(t, subnets, 3) + assert.Equal(t, "8000::/2", subnets[0].String()) + assert.Equal(t, "c000::/3", subnets[1].String()) + assert.Equal(t, "e000::/3", subnets[2].String()) + network = netip.MustParsePrefix("10.1.0.0/16") subnets, err = n.CIDRSubnetSizes(4, 4, 8, 4, network) - assert.NoError(t, err) + require.NoError(t, err) assert.Len(t, subnets, 4) assert.Equal(t, "10.1.0.0/20", subnets[0].String()) assert.Equal(t, "10.1.16.0/20", subnets[1].String()) assert.Equal(t, "10.1.32.0/24", subnets[2].String()) assert.Equal(t, "10.1.48.0/20", subnets[3].String()) + + network = netip.MustParsePrefix("2016:1234:5678:9abc:ffff:ffff:ffff:cafe/64") + subnets, err = n.CIDRSubnetSizes(2, 2, 3, 3, 6, 6, 8, 10, network) + require.NoError(t, err) + assert.Len(t, subnets, 8) + assert.Equal(t, "2016:1234:5678:9abc::/66", subnets[0].String()) + assert.Equal(t, "2016:1234:5678:9abc:4000::/66", subnets[1].String()) + assert.Equal(t, "2016:1234:5678:9abc:8000::/67", subnets[2].String()) + assert.Equal(t, "2016:1234:5678:9abc:a000::/67", subnets[3].String()) + assert.Equal(t, "2016:1234:5678:9abc:c000::/70", subnets[4].String()) + assert.Equal(t, "2016:1234:5678:9abc:c400::/70", subnets[5].String()) + assert.Equal(t, "2016:1234:5678:9abc:c800::/72", subnets[6].String()) + assert.Equal(t, "2016:1234:5678:9abc:c900::/74", subnets[7].String()) } diff --git a/go.mod b/go.mod index ef4c2abeb..7252e8ae0 100644 --- a/go.mod +++ b/go.mod @@ -28,6 +28,7 @@ require ( github.com/stretchr/testify v1.8.1 github.com/ugorji/go/codec v1.2.8 github.com/zealic/xignore v0.3.3 + go4.org/netipx v0.0.0-20230125063823-8449b0a6169f gocloud.dev v0.28.0 golang.org/x/crypto v0.5.0 golang.org/x/sys v0.4.0 @@ -132,7 +133,7 @@ require ( go.opencensus.io v0.24.0 // indirect go.uber.org/atomic v1.10.0 // indirect go4.org/intern v0.0.0-20220617035311-6925f38cc365 // indirect - go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 // indirect + go4.org/unsafe/assume-no-moving-gc v0.0.0-20230204201903-c31fa085b70e // indirect golang.org/x/net v0.5.0 // indirect golang.org/x/oauth2 v0.2.0 // indirect golang.org/x/time v0.2.0 // indirect diff --git a/go.sum b/go.sum index 85a682364..360e210a8 100644 --- a/go.sum +++ b/go.sum @@ -1990,9 +1990,12 @@ go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= go4.org/intern v0.0.0-20220617035311-6925f38cc365 h1:t9hFvR102YlOqU0fQn1wgwhNvSbHGBbbJxX9JKfU3l0= go4.org/intern v0.0.0-20220617035311-6925f38cc365/go.mod h1:WXRv3p7T6gzt0CcJm43AAKdKVZmcQbwwC7EwquU5BZU= +go4.org/netipx v0.0.0-20230125063823-8449b0a6169f h1:ketMxHg+vWm3yccyYiq+uK8D3fRmna2Fcj+awpQp84s= +go4.org/netipx v0.0.0-20230125063823-8449b0a6169f/go.mod h1:tgPU4N2u9RByaTN3NC2p9xOzyFpte4jYwsIIRF7XlSc= go4.org/unsafe/assume-no-moving-gc v0.0.0-20211027215541-db492cf91b37/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760 h1:FyBZqvoA/jbNzuAWLQE2kG820zMAkcilx6BMjGbL/E4= go4.org/unsafe/assume-no-moving-gc v0.0.0-20220617031537-928513b29760/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20230204201903-c31fa085b70e h1:AY/D6WBvaYJLmXK9VTIAX0tokDhrkkqdvIUwOU2nxio= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20230204201903-c31fa085b70e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= gocloud.dev v0.28.0 h1:PjL1f9zu8epY1pFCIHdrQnJRZzRcDyAr18hNTkXIKlQ= gocloud.dev v0.28.0/go.mod h1:nzSs01FpRYyIb/OqXLNNa+NMPZG9CdTUY/pGLgSpIN0= golang.org/x/crypto v0.0.0-20171113213409-9f005a07e0d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= diff --git a/internal/cidr/cidr.go b/internal/cidr/cidr.go new file mode 100644 index 000000000..7ac4b053f --- /dev/null +++ b/internal/cidr/cidr.go @@ -0,0 +1,138 @@ +package cidr + +import ( + "fmt" + "math/big" + "net/netip" + + "go4.org/netipx" +) + +// taken from github.com/apparentelymart/go-cidr/ and modified to use the net/netip +// package instead of the stdlib net package - this will hopefully be merged back +// upstream at some point + +// SubnetBig takes a parent CIDR range and creates a subnet within it with the +// given number of additional prefix bits and the given network number. It +// differs from Subnet in that it takes a *big.Int for the num, instead of an int. +// +// For example, 10.3.0.0/16, extended by 8 bits, with a network number of 5, +// becomes 10.3.5.0/24 . +func SubnetBig(base netip.Prefix, newBits int, num *big.Int) (netip.Prefix, error) { + parentLen := base.Bits() + addrLen := base.Addr().BitLen() + + newPrefixLen := parentLen + newBits + + if newPrefixLen > addrLen { + return netip.Prefix{}, fmt.Errorf("insufficient address space to extend prefix of %d by %d", parentLen, newBits) + } + + maxNetNum := uint64(1< maxNetNum { + return netip.Prefix{}, fmt.Errorf("prefix extension of %d does not accommodate a subnet numbered %d", newBits, num) + } + + prefix := netip.PrefixFrom(insertNumIntoIP(base.Masked().Addr(), num, newPrefixLen), newPrefixLen) + + return prefix, nil +} + +// HostBig takes a parent CIDR range and turns it into a host IP address with +// the given host number. It differs from Host in that it takes a *big.Int for +// the num, instead of an int. +// +// For example, 10.3.0.0/16 with a host number of 2 gives 10.3.0.2. +func HostBig(base netip.Prefix, num *big.Int) (netip.Addr, error) { + parentLen := base.Bits() + addrLen := base.Addr().BitLen() + + hostLen := addrLen - parentLen + + maxHostNum := big.NewInt(int64(1)) + maxHostNum.Lsh(maxHostNum, uint(hostLen)) + maxHostNum.Sub(maxHostNum, big.NewInt(1)) + + numUint64 := big.NewInt(int64(num.Uint64())) + if num.Cmp(big.NewInt(0)) == -1 { + numUint64.Neg(num) + numUint64.Sub(numUint64, big.NewInt(int64(1))) + num.Sub(maxHostNum, numUint64) + } + + if numUint64.Cmp(maxHostNum) == 1 { + return netip.Addr{}, fmt.Errorf("prefix of %d does not accommodate a host numbered %d", parentLen, num) + } + + return insertNumIntoIP(base.Masked().Addr(), num, addrLen), nil +} + +func ipToInt(ip netip.Addr) (*big.Int, int) { + val := &big.Int{} + val.SetBytes(ip.AsSlice()) + + return val, ip.BitLen() +} + +func intToIP(ipInt *big.Int, bits int) netip.Addr { + ipBytes := ipInt.Bytes() + ret := make([]byte, bits/8) + // Pack our IP bytes into the end of the return array, + // since big.Int.Bytes() removes front zero padding. + for i := 1; i <= len(ipBytes); i++ { + ret[len(ret)-i] = ipBytes[len(ipBytes)-i] + } + + addr, ok := netip.AddrFromSlice(ret) + if !ok { + panic("invalid IP address") + } + + return addr +} + +func insertNumIntoIP(ip netip.Addr, bigNum *big.Int, prefixLen int) netip.Addr { + ipInt, totalBits := ipToInt(ip) + bigNum.Lsh(bigNum, uint(totalBits-prefixLen)) + ipInt.Or(ipInt, bigNum) + return intToIP(ipInt, totalBits) +} + +// PreviousSubnet returns the subnet of the desired mask in the IP space +// just lower than the start of Prefix provided. If the IP space rolls over +// then the second return value is true +func PreviousSubnet(network netip.Prefix, prefixLen int) (netip.Prefix, bool) { + previousIP := network.Masked().Addr().Prev() + + previous, err := previousIP.Prefix(prefixLen) + if err != nil { + return netip.Prefix{}, false + } + if !previous.IsValid() { + return previous, true + } + + return previous.Masked(), false +} + +// NextSubnet returns the next available subnet of the desired mask size +// starting for the maximum IP of the offset subnet +// If the IP exceeds the maxium IP then the second return value is true +func NextSubnet(network netip.Prefix, prefixLen int) (netip.Prefix, bool) { + currentLast := netipx.PrefixLastIP(network) + + currentSubnet, err := currentLast.Prefix(prefixLen) + if err != nil { + return netip.Prefix{}, false + } + + last := netipx.PrefixLastIP(currentSubnet).Next() + next, err := last.Prefix(prefixLen) + if err != nil { + return netip.Prefix{}, false + } + if !last.IsValid() { + return next, true + } + return next, false +} diff --git a/internal/cidr/cidr_test.go b/internal/cidr/cidr_test.go new file mode 100644 index 000000000..6f0b70ee3 --- /dev/null +++ b/internal/cidr/cidr_test.go @@ -0,0 +1,204 @@ +package cidr + +import ( + "fmt" + "math/big" + "net/netip" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSubnetBig(t *testing.T) { + cases := []struct { + base string + num *big.Int + out string + bits int + err bool + }{ + { + base: "192.168.2.0/20", + bits: 4, + num: big.NewInt(int64(6)), + out: "192.168.6.0/24", + }, + { + base: "192.168.2.0/20", + bits: 4, + num: big.NewInt(int64(0)), + out: "192.168.0.0/24", + }, + { + base: "192.168.0.0/31", + bits: 1, + num: big.NewInt(int64(1)), + out: "192.168.0.1/32", + }, + { + base: "192.168.0.0/21", + bits: 4, + num: big.NewInt(int64(7)), + out: "192.168.3.128/25", + }, + { + base: "fe80::/48", + bits: 16, + num: big.NewInt(int64(6)), + out: "fe80:0:0:6::/64", + }, + { + base: "fe80::/48", + bits: 33, + num: big.NewInt(int64(6)), + out: "fe80::3:0:0:0/81", + }, + { + base: "fe80::/49", + bits: 16, + num: big.NewInt(int64(7)), + out: "fe80:0:0:3:8000::/65", + }, + { + base: "192.168.2.0/31", + bits: 2, + num: big.NewInt(int64(0)), + err: true, // not enough bits to expand into + }, + { + base: "fe80::/126", + bits: 4, + num: big.NewInt(int64(0)), + err: true, // not enough bits to expand into + }, + { + base: "192.168.2.0/24", + bits: 4, + num: big.NewInt(int64(16)), + err: true, // can't fit 16 into 4 bits + }, + } + + for _, testCase := range cases { + t.Run(fmt.Sprintf("SubnetBig(%#v,%#v,%#v)", testCase.base, testCase.bits, testCase.num), func(t *testing.T) { + base := netip.MustParsePrefix(testCase.base) + + subnet, err := SubnetBig(base, testCase.bits, testCase.num) + if testCase.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.out, subnet.String()) + } + }) + } +} + +func TestHostBig(t *testing.T) { + cases := []struct { + prefix string + num *big.Int + out string + err bool + }{ + { + prefix: "192.168.2.0/20", + num: big.NewInt(int64(6)), + out: "192.168.0.6", + }, + { + prefix: "192.168.0.0/20", + num: big.NewInt(int64(257)), + out: "192.168.1.1", + }, + { + prefix: "2001:db8::/32", + num: big.NewInt(int64(1)), + out: "2001:db8::1", + }, + { + prefix: "192.168.1.0/24", + num: big.NewInt(int64(256)), + err: true, // only 0-255 will fit in 8 bits + }, + { + prefix: "192.168.0.0/30", + num: big.NewInt(int64(-3)), + out: "192.168.0.1", // 4 address (0-3) in 2 bits; 3rd from end = 1 + }, + { + prefix: "192.168.0.0/30", + num: big.NewInt(int64(-4)), + out: "192.168.0.0", // 4 address (0-3) in 2 bits; 4th from end = 0 + }, + { + prefix: "192.168.0.0/30", + num: big.NewInt(int64(-5)), + err: true, // 4 address (0-3) in 2 bits; cannot accommodate 5 + }, + { + prefix: "fd9d:bc11:4020::/64", + num: big.NewInt(int64(2)), + out: "fd9d:bc11:4020::2", + }, + { + prefix: "fd9d:bc11:4020::/64", + num: big.NewInt(int64(-2)), + out: "fd9d:bc11:4020:0:ffff:ffff:ffff:fffe", + }, + } + + for _, testCase := range cases { + t.Run(fmt.Sprintf("HostBig(%v,%v)", testCase.prefix, testCase.num), func(t *testing.T) { + network := netip.MustParsePrefix(testCase.prefix) + + gotIP, err := HostBig(network, testCase.num) + if testCase.err { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.Equal(t, testCase.out, gotIP.String()) + } + }) + } +} + +func TestPreviousNextSubnet(t *testing.T) { + testCases := []struct { + next, prev string + overflow bool + }{ + {"10.0.0.0/24", "9.255.255.0/24", false}, + {"100.0.0.0/26", "99.255.255.192/26", false}, + {"100.0.0.0/32", "99.255.255.192/26", false}, + {"0.0.0.0/26", "255.255.255.192/26", true}, + {"2001:db8:e000::/36", "2001:db8:d000::/36", false}, + {"::/64", "ffff:ffff:ffff:ffff::/64", true}, + } + for _, tc := range testCases { + c1 := netip.MustParsePrefix(tc.next) + c2 := netip.MustParsePrefix(tc.prev) + mask := c1.Bits() + + p1, rollback := PreviousSubnet(c1, mask) + if tc.overflow { + assert.True(t, rollback) + continue + } + + assert.Equal(t, c2, p1) + } + + for _, tc := range testCases { + c1 := netip.MustParsePrefix(tc.next) + c2 := netip.MustParsePrefix(tc.prev) + mask := c1.Bits() + + n1, rollover := NextSubnet(c2, mask) + if tc.overflow { + assert.True(t, rollover) + continue + } + assert.Equal(t, c1, n1) + } +} diff --git a/internal/tests/integration/net_test.go b/internal/tests/integration/net_test.go index 35218eaf7..a0ff4e6f4 100644 --- a/internal/tests/integration/net_test.go +++ b/internal/tests/integration/net_test.go @@ -9,7 +9,7 @@ func TestNet_LookupIP(t *testing.T) { } func TestNet_CIDRHost(t *testing.T) { - inOutTestExperimental(t, `{{ net.ParseIPPrefix "10.12.127.0/20" | net.CIDRHost 16 }}`, "10.12.112.16") + inOutTestExperimental(t, `{{ net.ParsePrefix "10.12.127.0/20" | net.CIDRHost 16 }}`, "10.12.112.16") inOutTestExperimental(t, `{{ "10.12.127.0/20" | net.CIDRHost 16 }}`, "10.12.112.16") inOutTestExperimental(t, `{{ net.CIDRHost 268 "10.12.127.0/20" }}`, "10.12.113.12") inOutTestExperimental(t, `{{ net.CIDRHost 34 "fd00:fd12:3456:7890:00a2::/72" }}`, "fd00:fd12:3456:7890::22")