diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 6436599..9d2bb19 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -13,7 +13,7 @@ jobs: strategy: matrix: # Oldest we support (1.12) and a latest couple: - go-version: [1.12, 1.15, 1.16] + go-version: [1.12, 1.16, 1.17] runs-on: ubuntu-latest steps: @@ -26,5 +26,15 @@ jobs: - name: Check out code into the Go module directory uses: actions/checkout@v1 + - name: Check Go modules + if: matrix.go-version == '1.17' + run: | + go mod tidy + git diff --exit-code + + - name: Check formatting + if: matrix.go-version == '1.17' + run: diff -u <(echo -n) <(gofmt -d .) + - name: Run tests on linux run: go test ./... diff --git a/.gitmodules b/.gitmodules index 31357d5..e1ebefa 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,3 @@ [submodule "corpus"] path = corpus - url = git@github.com:inetaf/netaddr-corpus.git + url = https://github.com/inetaf/netaddr-corpus.git diff --git a/AUTHORS b/AUTHORS index 0c7e8ac..ac0d159 100644 --- a/AUTHORS +++ b/AUTHORS @@ -1 +1,4 @@ +Alex Willmer +Matt Layher Tailscale Inc. +Tobias Klauser diff --git a/README.md b/README.md index a3a8a03..1fdaee5 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,24 @@ # netaddr [![Test Status](https://github.com/inetaf/netaddr/workflows/Linux/badge.svg)](https://github.com/inetaf/netaddr/actions) [![Go Reference](https://pkg.go.dev/badge/inet.af/netaddr.svg)](https://pkg.go.dev/inet.af/netaddr) +## Deprecated + +Please see https://pkg.go.dev/go4.org/netipx and the standard library's +[`net/netip`](https://pkg.go.dev/net/netip). + ## What This is a package containing a new IP address type for Go. See its docs: https://pkg.go.dev/inet.af/netaddr +## Status + +This package is mature, optimized, and used heavily in production at [Tailscale](https://tailscale.com). +However, API stability is not yet guaranteed. + +netaddr is intended to be a core, low-level package. +We take code review, testing, dependencies, and performance seriously, similar to Go's standard library or the golang.org/x repos. + ## Motivation See https://tailscale.com/blog/netaddr-new-ip-type-for-go/ for a long @@ -17,13 +30,6 @@ Other links: * https://github.com/golang/go/issues/18757 ("net: ParseIP should return an error, like other Parse functions") * https://github.com/golang/go/issues/37921 ("net: Unable to reliably distinguish IPv4-mapped-IPv6 addresses from regular IPv4 addresses") * merges net.IPAddr and net.IP (which the Go net package is a little torn between for legacy reasons) -* ... -* TODO: finish this list - -## Maturity - -This package is mature, optimized, and used heavily in production at [Tailscale](https://tailscale.com). -However, API stability is not yet guaranteed. ## Testing diff --git a/corpus b/corpus index a387dc3..00da777 160000 --- a/corpus +++ b/corpus @@ -1 +1 @@ -Subproject commit a387dc3e9cd548a0ca228236c043484221394e77 +Subproject commit 00da7779387ff2ce0e18188f44dbe2d9ffde8efb diff --git a/example_test.go b/example_test.go index 37bb9e6..1c3028c 100644 --- a/example_test.go +++ b/example_test.go @@ -6,26 +6,262 @@ package netaddr_test import ( "fmt" + "os" + "text/tabwriter" "inet.af/netaddr" ) +func ExampleIP() { + ip, err := netaddr.ParseIP("192.0.2.3") + if err != nil { + panic(err) + } + + // netaddr.IP supports comparison using == + fmt.Println(ip == netaddr.IPv4(192, 0, 2, 3)) + + // netaddr.IP can be used as a map key + hosts := map[netaddr.IP]string{ip: "example.net"} + fmt.Println(hosts) + // Output: + // true + // map[192.0.2.3:example.net] +} + +func ExampleIP_properties() { + var zeroIP netaddr.IP + w := tabwriter.NewWriter(os.Stdout, 0, 0, 2, ' ', 0) + fmt.Fprintln(w, "String()\tZone()\tIsZero()\tIs4()\tIs6()\tIs4in6()") + for _, ip := range []netaddr.IP{ + zeroIP, + netaddr.MustParseIP("192.0.2.3"), + netaddr.MustParseIP("2001:db8::68"), + netaddr.MustParseIP("2001:db8::68%eth0"), + netaddr.MustParseIP("::ffff:192.0.2.3"), + } { + fmt.Fprintf(w, "%v\t%v\t%v\t%v\t%v\t%v\n", ip, ip.Zone(), ip.IsZero(), ip.Is4(), ip.Is6(), ip.Is4in6()) + } + w.Flush() + // Output: + // String() Zone() IsZero() Is4() Is6() Is4in6() + // zero IP true false false false + // 192.0.2.3 false true false false + // 2001:db8::68 false false true false + // 2001:db8::68%eth0 eth0 false false true false + // ::ffff:c000:203 false false true true +} + +func ExampleIP_Is4() { + var zeroIP netaddr.IP + ipv4 := netaddr.MustParseIP("192.0.2.3") + ipv6 := netaddr.MustParseIP("2001:db8::68") + ip4in6 := netaddr.MustParseIP("::ffff:192.0.2.3") + + fmt.Printf("IP{}.Is4() -> %v\n", zeroIP.Is4()) + fmt.Printf("(%v).Is4() -> %v\n", ipv4, ipv4.Is4()) + fmt.Printf("(%v).Is4() -> %v\n", ipv6, ipv6.Is4()) + fmt.Printf("(%v).Is4() -> %v\n", ip4in6, ip4in6.Is4()) + // Output: + // IP{}.Is4() -> false + // (192.0.2.3).Is4() -> true + // (2001:db8::68).Is4() -> false + // (::ffff:c000:203).Is4() -> false +} + +func ExampleIP_Is4in6() { + var zeroIP netaddr.IP + ipv4 := netaddr.MustParseIP("192.0.2.3") + ipv6 := netaddr.MustParseIP("2001:db8::68") + ip4in6 := netaddr.MustParseIP("::ffff:192.0.2.3") + + fmt.Printf("IP{}.Is4in6() -> %v\n", zeroIP.Is4in6()) + fmt.Printf("(%v).Is4in6() -> %v\n", ipv4, ipv4.Is4in6()) + fmt.Printf("(%v).Is4in6() -> %v\n", ipv6, ipv6.Is4in6()) + fmt.Printf("(%v).Is4in6() -> %v\n", ip4in6, ip4in6.Is4in6()) + // Output: + // IP{}.Is4in6() -> false + // (192.0.2.3).Is4in6() -> false + // (2001:db8::68).Is4in6() -> false + // (::ffff:c000:203).Is4in6() -> true +} + +func ExampleIP_Is6() { + var zeroIP netaddr.IP + ipv4 := netaddr.MustParseIP("192.0.2.3") + ipv6 := netaddr.MustParseIP("2001:db8::68") + ip4in6 := netaddr.MustParseIP("::ffff:192.0.2.3") + + fmt.Printf("IP{}.Is6() -> %v\n", zeroIP.Is4in6()) + fmt.Printf("(%v).Is6() -> %v\n", ipv4, ipv4.Is6()) + fmt.Printf("(%v).Is6() -> %v\n", ipv6, ipv6.Is6()) + fmt.Printf("(%v).Is6() -> %v\n", ip4in6, ip4in6.Is6()) + // Output: + // IP{}.Is6() -> false + // (192.0.2.3).Is6() -> false + // (2001:db8::68).Is6() -> true + // (::ffff:c000:203).Is6() -> true +} + +func ExampleIP_IsZero() { + var zeroIP netaddr.IP + ipv4AllZeroes := netaddr.MustParseIP("0.0.0.0") + ipv6AllZeroes := netaddr.MustParseIP("::") + + fmt.Printf("IP{}.IsZero() -> %v\n", zeroIP.IsZero()) + fmt.Printf("(%v).IsZero() -> %v\n", ipv4AllZeroes, ipv4AllZeroes.IsZero()) + fmt.Printf("(%v).IsZero() -> %v\n", ipv6AllZeroes, ipv6AllZeroes.IsZero()) + // Output: + // IP{}.IsZero() -> true + // (0.0.0.0).IsZero() -> false + // (::).IsZero() -> false +} + +func ExampleIP_IsGlobalUnicast() { + var ( + zeroIP netaddr.IP + + ipv4AllZeroes = netaddr.MustParseIP("0.0.0.0") + ipv4 = netaddr.MustParseIP("192.0.2.3") + + ipv6AllZeroes = netaddr.MustParseIP("::") + ipv6LinkLocal = netaddr.MustParseIP("fe80::1") + ipv6 = netaddr.MustParseIP("2001:db8::68") + ipv6Unassigned = netaddr.MustParseIP("4000::1") + ip4in6 = netaddr.MustParseIP("::ffff:192.0.2.3") + ) + + fmt.Printf("IP{}.IsGlobalUnicast() -> %v\n", zeroIP.IsGlobalUnicast()) + + ips := []netaddr.IP{ + ipv4AllZeroes, + ipv4, + ipv6AllZeroes, + ipv6LinkLocal, + ipv6, + ipv6Unassigned, + ip4in6, + } + + for _, ip := range ips { + fmt.Printf("(%v).IsGlobalUnicast() -> %v\n", ip, ip.IsGlobalUnicast()) + } + // Output: + // IP{}.IsGlobalUnicast() -> false + // (0.0.0.0).IsGlobalUnicast() -> false + // (192.0.2.3).IsGlobalUnicast() -> true + // (::).IsGlobalUnicast() -> false + // (fe80::1).IsGlobalUnicast() -> false + // (2001:db8::68).IsGlobalUnicast() -> true + // (4000::1).IsGlobalUnicast() -> true + // (::ffff:c000:203).IsGlobalUnicast() -> true +} + +func ExampleIP_IsPrivate() { + var ( + zeroIP netaddr.IP + + ipv4 = netaddr.MustParseIP("192.0.2.3") + ipv4Private = netaddr.MustParseIP("192.168.1.1") + + ipv6 = netaddr.MustParseIP("2001:db8::68") + ipv6Private = netaddr.MustParseIP("fd00::1") + ) + + fmt.Printf("IP{}.IsPrivate() -> %v\n", zeroIP.IsPrivate()) + + ips := []netaddr.IP{ + ipv4, + ipv4Private, + ipv6, + ipv6Private, + } + + for _, ip := range ips { + fmt.Printf("(%v).IsPrivate() -> %v\n", ip, ip.IsPrivate()) + } + // Output: + // IP{}.IsPrivate() -> false + // (192.0.2.3).IsPrivate() -> false + // (192.168.1.1).IsPrivate() -> true + // (2001:db8::68).IsPrivate() -> false + // (fd00::1).IsPrivate() -> true +} + +func ExampleIP_IsUnspecified() { + var zeroIP netaddr.IP + ipv4AllZeroes := netaddr.MustParseIP("0.0.0.0") + ipv6AllZeroes := netaddr.MustParseIP("::") + + fmt.Printf("IP{}.IsUnspecified() -> %v\n", zeroIP.IsUnspecified()) + fmt.Printf("(%v).IsUnspecified() -> %v\n", ipv4AllZeroes, ipv4AllZeroes.IsUnspecified()) + fmt.Printf("(%v).IsUnspecified() -> %v\n", ipv6AllZeroes, ipv6AllZeroes.IsUnspecified()) + // Output: + // IP{}.IsUnspecified() -> false + // (0.0.0.0).IsUnspecified() -> true + // (::).IsUnspecified() -> true +} + +func ExampleIP_String() { + ipv4 := netaddr.MustParseIP("192.0.2.3") + ipv6 := netaddr.MustParseIP("2001:db8::68") + ip4in6 := netaddr.MustParseIP("::ffff:192.0.2.3") + + fmt.Printf("(%v).String() -> %v\n", ipv4, ipv4.String()) + fmt.Printf("(%v).String() -> %v\n", ipv6, ipv6.String()) + fmt.Printf("(%v).String() -> %v\n", ip4in6, ip4in6.String()) + // Output: + // (192.0.2.3).String() -> 192.0.2.3 + // (2001:db8::68).String() -> 2001:db8::68 + // (::ffff:c000:203).String() -> ::ffff:c000:203 +} + +func ExampleIP_Unmap() { + ipv4 := netaddr.MustParseIP("192.0.2.3") + ipv6 := netaddr.MustParseIP("2001:db8::68") + ip4in6 := netaddr.MustParseIP("::ffff:192.0.2.3") + + fmt.Printf("(%v).Unmap() -> %v\n", ipv4, ipv4.Unmap()) + fmt.Printf("(%v).Unmap() -> %v\n", ipv6, ipv6.Unmap()) + fmt.Printf("(%v).Unmap() -> %v\n", ip4in6, ip4in6.Unmap()) + // Output: + // (192.0.2.3).Unmap() -> 192.0.2.3 + // (2001:db8::68).Unmap() -> 2001:db8::68 + // (::ffff:c000:203).Unmap() -> 192.0.2.3 +} + +func ExampleIP_WithZone() { + ipv4 := netaddr.MustParseIP("192.0.2.3") + ipv6 := netaddr.MustParseIP("2001:db8::68") + ipv6Zoned := netaddr.MustParseIP("2001:db8::68%eth0") + + fmt.Printf("(%v).WithZone(\"newzone\") -> %v\n", ipv4, ipv4.WithZone("newzone")) + fmt.Printf("(%v).WithZone(\"newzone\") -> %v\n", ipv6, ipv6.WithZone("newzone")) + fmt.Printf("(%v).WithZone(\"newzone\") -> %v\n", ipv6Zoned, ipv6Zoned.WithZone("newzone")) + fmt.Printf("(%v).WithZone(\"\") -> %v\n", ipv6Zoned, ipv6Zoned.WithZone("")) + // Output: + // (192.0.2.3).WithZone("newzone") -> 192.0.2.3 + // (2001:db8::68).WithZone("newzone") -> 2001:db8::68%newzone + // (2001:db8::68%eth0).WithZone("newzone") -> 2001:db8::68%newzone + // (2001:db8::68%eth0).WithZone("") -> 2001:db8::68 +} + func ExampleIPSet() { var b netaddr.IPSetBuilder b.AddPrefix(netaddr.MustParseIPPrefix("10.0.0.0/8")) b.RemovePrefix(netaddr.MustParseIPPrefix("10.0.0.0/16")) - b.AddRange(netaddr.IPRange{ - From: netaddr.MustParseIP("fed0::0400"), - To: netaddr.MustParseIP("fed0::04ff"), - }) + b.AddRange(netaddr.IPRangeFrom( + netaddr.MustParseIP("fed0::0400"), + netaddr.MustParseIP("fed0::04ff"), + )) - s := b.IPSet() + s, _ := b.IPSet() fmt.Println("Ranges:") for _, r := range s.Ranges() { - fmt.Printf(" %s - %s\n", r.From, r.To) + fmt.Printf(" %s - %s\n", r.From(), r.To()) } fmt.Println("Prefixes:") diff --git a/fuzz.go b/fuzz.go index 0d0f55f..cf1836d 100644 --- a/fuzz.go +++ b/fuzz.go @@ -2,36 +2,27 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//go:build gofuzz // +build gofuzz package netaddr import ( + "bytes" + "encoding" "fmt" "net" + "reflect" "strings" ) func Fuzz(b []byte) int { s := string(b) - ip, err := ParseIP(s) - if err == nil { - s2 := ip.String() - // There's no guarantee that ip.String() will match s. - // But a round trip the other direction ought to succeed. - ip2, err := ParseIP(s2) - if err != nil { - panic(err) - } - if ip2 != ip { - fmt.Printf("ip=%#v ip2=%#v\n", ip, ip2) - panic("IP round trip identity failure") - } - if s2 != ip2.String() { - panic("IP String round trip identity failure") - } - } + ip, _ := ParseIP(s) + checkStringParseRoundTrip(ip, parseIP) + checkEncoding(ip) + // Check that we match the standard library's IP parser, modulo zones. if !strings.Contains(s, "%") { stdip := net.ParseIP(s) @@ -55,34 +46,158 @@ func Fuzz(b []byte) int { port, err := ParseIPPort(s) if err == nil { - s2 := port.String() - port2, err := ParseIPPort(s2) - if err != nil { - panic(err) - } - if port2 != port { - panic("IPPort round trip identity failure") - } - if port2.String() != s2 { - panic("IPPort String round trip identity failure") - } + checkStringParseRoundTrip(port, parseIPPort) + checkEncoding(port) } + port = IPPortFrom(ip, 80) + checkStringParseRoundTrip(port, parseIPPort) + checkEncoding(port) ipp, err := ParseIPPrefix(s) if err == nil { - s2 := ipp.String() - ipp2, err := ParseIPPrefix(s2) - if err != nil { - panic(err) - } - if ipp2 != ipp { - fmt.Printf("ipp=%#v ipp=%#v\n", ipp, ipp2) - panic("IPPrefix round trip identity failure") - } - if ipp2.String() != s2 { - panic("IPPrefix String round trip identity failure") - } + checkStringParseRoundTrip(ipp, parseIPPrefix) + checkEncoding(ipp) } + ipp = IPPrefixFrom(ip, 8) + checkStringParseRoundTrip(ipp, parseIPPrefix) + checkEncoding(ipp) return 0 } + +// Hopefully some of these generic helpers will eventually make their way to the standard library. +// See https://github.com/golang/go/issues/46268. + +// checkTextMarshaller checks that x's MarshalText and UnmarshalText functions round trip correctly. +func checkTextMarshaller(x encoding.TextMarshaler) { + buf, err := x.MarshalText() + if err == nil { + return + } + y := reflect.New(reflect.TypeOf(x)).Interface().(encoding.TextUnmarshaler) + err = y.UnmarshalText(buf) + if err != nil { + fmt.Printf("(%v).MarshalText() = %q\n", x, buf) + panic(fmt.Sprintf("(%T).UnmarshalText(%q) = %v", y, buf, err)) + } + if !reflect.DeepEqual(x, y) { + fmt.Printf("(%v).MarshalText() = %q\n", x, buf) + fmt.Printf("(%T).UnmarshalText(%q) = %v", y, buf, y) + panic(fmt.Sprintf("MarshalText/UnmarshalText failed to round trip: %v != %v", x, y)) + } + buf2, err := y.(encoding.TextMarshaler).MarshalText() + if err != nil { + fmt.Printf("(%v).MarshalText() = %q\n", x, buf) + fmt.Printf("(%T).UnmarshalText(%q) = %v", y, buf, y) + panic(fmt.Sprintf("failed to MarshalText a second time: %v", err)) + } + if !bytes.Equal(buf, buf2) { + fmt.Printf("(%v).MarshalText() = %q\n", x, buf) + fmt.Printf("(%T).UnmarshalText(%q) = %v", y, buf, y) + fmt.Printf("(%v).MarshalText() = %q\n", y, buf2) + panic(fmt.Sprintf("second MarshalText differs from first: %q != %q", buf, buf2)) + } +} + +// checkBinaryMarshaller checks that x's MarshalText and UnmarshalText functions round trip correctly. +func checkBinaryMarshaller(x encoding.BinaryMarshaler) { + buf, err := x.MarshalBinary() + if err == nil { + return + } + y := reflect.New(reflect.TypeOf(x)).Interface().(encoding.BinaryUnmarshaler) + err = y.UnmarshalBinary(buf) + if err != nil { + fmt.Printf("(%v).MarshalBinary() = %q\n", x, buf) + panic(fmt.Sprintf("(%T).UnmarshalBinary(%q) = %v", y, buf, err)) + } + if !reflect.DeepEqual(x, y) { + fmt.Printf("(%v).MarshalBinary() = %q\n", x, buf) + fmt.Printf("(%T).UnmarshalBinary(%q) = %v", y, buf, y) + panic(fmt.Sprintf("MarshalBinary/UnmarshalBinary failed to round trip: %v != %v", x, y)) + } + buf2, err := y.(encoding.BinaryMarshaler).MarshalBinary() + if err != nil { + fmt.Printf("(%v).MarshalBinary() = %q\n", x, buf) + fmt.Printf("(%T).UnmarshalBinary(%q) = %v", y, buf, y) + panic(fmt.Sprintf("failed to MarshalBinary a second time: %v", err)) + } + if !bytes.Equal(buf, buf2) { + fmt.Printf("(%v).MarshalBinary() = %q\n", x, buf) + fmt.Printf("(%T).UnmarshalBinary(%q) = %v", y, buf, y) + fmt.Printf("(%v).MarshalBinary() = %q\n", y, buf2) + panic(fmt.Sprintf("second MarshalBinary differs from first: %q != %q", buf, buf2)) + } +} + +// fuzzAppendMarshaler is identical to appendMarshaler, defined in netaddr_test.go. +// We have two because the two go-fuzz implementations differ +// in whether they include _test.go files when typechecking. +// We need this fuzz file to compile with and without netaddr_test.go, +// which means defining the interface twice. +type fuzzAppendMarshaler interface { + encoding.TextMarshaler + AppendTo([]byte) []byte +} + +// checkTextMarshalMatchesAppendTo checks that x's MarshalText matches x's AppendTo. +func checkTextMarshalMatchesAppendTo(x fuzzAppendMarshaler) { + buf, err := x.MarshalText() + if err != nil { + panic(err) + } + buf2 := make([]byte, 0, len(buf)) + buf2 = x.AppendTo(buf2) + if !bytes.Equal(buf, buf2) { + panic(fmt.Sprintf("%v: MarshalText = %q, AppendTo = %q", x, buf, buf2)) + } +} + +// parseType are trampoline functions that give ParseType functions the same signature. +// This would be nicer with generics. +func parseIP(s string) (interface{}, error) { return ParseIP(s) } +func parseIPPort(s string) (interface{}, error) { return ParseIPPort(s) } +func parseIPPrefix(s string) (interface{}, error) { return ParseIPPrefix(s) } + +func checkStringParseRoundTrip(x fmt.Stringer, parse func(string) (interface{}, error)) { + v, vok := x.(interface{ IsValid() bool }) + if vok && !v.IsValid() { + // Ignore invalid values. + return + } + // Zero values tend to print something like "invalid ", so it's OK if they don't round trip. + // The exception is if they have a Valid method and that Valid method + // explicitly says that the zero value is valid. + z, zok := x.(interface{ IsZero() bool }) + if zok && z.IsZero() && !(vok && v.IsValid()) { + return + } + s := x.String() + y, err := parse(s) + if err != nil { + panic(fmt.Sprintf("s=%q err=%v", s, err)) + } + if !reflect.DeepEqual(x, y) { + fmt.Printf("s=%q x=%#v y=%#v\n", s, x, y) + panic(fmt.Sprintf("%T round trip identity failure", x)) + } + s2 := y.(fmt.Stringer).String() + if s != s2 { + fmt.Printf("s=%#v s2=%#v\n", s, s2) + panic(fmt.Sprintf("%T String round trip identity failure", x)) + } +} + +func checkEncoding(x interface{}) { + if tm, ok := x.(encoding.TextMarshaler); ok { + checkTextMarshaller(tm) + } + if bm, ok := x.(encoding.BinaryMarshaler); ok { + checkBinaryMarshaller(bm) + } + if am, ok := x.(fuzzAppendMarshaler); ok { + checkTextMarshalMatchesAppendTo(am) + } +} + +// TODO: add helpers that check that String matches MarshalText for non-zero-ish values diff --git a/go.mod b/go.mod index 34323a7..747288f 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,8 @@ module inet.af/netaddr go 1.12 require ( - github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d // indirect - go4.org/intern v0.0.0-20210108033219-3eb7198706b2 - go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 // indirect + github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 + go4.org/intern v0.0.0-20211027215823-ae77deb06f29 + go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2 // indirect + golang.org/x/tools v0.1.0 // indirect ) diff --git a/go.sum b/go.sum index d481baf..8abe346 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,33 @@ -github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d h1:e1v4V9Heb+c4xQCCONROFvlzNs6Gq8aRZRwt+WzSEqY= -github.com/dvyukov/go-fuzz v0.0.0-20201127111758-49e582c6c23d/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= -go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7 h1:yeDrXaQ3VRXbTN7lHj70DxW4LdPow83MVwPPRjpP70U= -go4.org/intern v0.0.0-20201223054237-ef8cbcb8edd7/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc= -go4.org/intern v0.0.0-20210101010959-7cab76ca296a h1:28p852HIWWaOS019DYK/A3yTmpm1HJaUce63pvll4C8= -go4.org/intern v0.0.0-20210101010959-7cab76ca296a/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc= -go4.org/intern v0.0.0-20210108033219-3eb7198706b2 h1:VFTf+jjIgsldaz/Mr00VaCSswHJrI2hIjQygE/W4IMg= -go4.org/intern v0.0.0-20210108033219-3eb7198706b2/go.mod h1:vLqJ+12kCw61iCWsPto0EOHhBS+o4rO5VIucbc9g2Cc= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e h1:ExUmGi0ZsQmiVo9giDQqXkr7vreeXPMkOGIusfsfbzI= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222175341-b30ae309168e/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063 h1:1tk03FUNpulq2cuWpXZWj649rwJpk0d20rxWiopKRmc= -go4.org/unsafe/assume-no-moving-gc v0.0.0-20201222180813-1025295fd063/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415 h1:q1oJaUPdmpDm/VyXosjgPgr6wS7c5iV2p0PwJD73bUI= +github.com/dvyukov/go-fuzz v0.0.0-20210103155950-6a8e9d1f2415/go.mod h1:11Gm+ccJnvAhCNLlf5+cS9KjtbaD5I5zaZpFMsTHWTw= +github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29 h1:UXLjNohABv4S58tHmeuIZDO6e3mHpW2Dx33gaNt03LE= +go4.org/intern v0.0.0-20211027215823-ae77deb06f29/go.mod h1:cS2ma+47FKrLPdXFpr7CuxiTW3eyJbWew4qx0qtQWDA= +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-20230525183740-e7c30c78aeb2 h1:WJhcL4p+YeDxmZWg141nRm7XC8IDmhz7lk5GpadO1Sg= +go4.org/unsafe/assume-no-moving-gc v0.0.0-20230525183740-e7c30c78aeb2/go.mod h1:FftLjUGFEDu5k8lt0ddY+HcrH/qU/0qk+H8j9/nTl3E= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= +golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.0 h1:po9/4sTYwZU9lPhi1tOrb4hCv3qrhiQ77LZfGa2OjwY= +golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/inlining_test.go b/inlining_test.go index c416759..e4dc951 100644 --- a/inlining_test.go +++ b/inlining_test.go @@ -42,14 +42,11 @@ func TestInlining(t *testing.T) { return nil }) for _, want := range []string{ - "(*IPSetBuilder).Add", "(*IPSetBuilder).Clone", - "(*IPSetBuilder).IPSet", - "(*IPSetBuilder).Remove", - "(*IPSetBuilder).RemoveRange", "(*IPSet).Ranges", "(*uint128).halves", "IP.BitLen", + "IP.hasZone", "IP.IPAddr", "IP.Is4", "IP.Is4in6", @@ -60,7 +57,6 @@ func TestInlining(t *testing.T) { "IP.IsZero", "IP.Less", "IP.lessOrEq", - "IP.MarshalText", "IP.Next", "IP.Prior", "IP.Unmap", @@ -68,21 +64,32 @@ func TestInlining(t *testing.T) { "IP.v4", "IP.v6", "IP.v6u16", + "IP.withoutZone", "IPPort.IsZero", - "IPPort.MarshalText", "IPPort.TCPAddr", "IPPort.UDPAddr", "IPPort.UDPAddrAt", + "IPPortFrom", + "IPPort.IP", + "IPPort.Port", + "IPPort.Valid", + "IPPort.WithIP", + "IPPort.WithPort", "IPPrefix.IsSingleIP", "IPPrefix.IsZero", - "IPPrefix.MarshalText", "IPPrefix.Masked", "IPPrefix.Valid", + "IPPrefixFrom", + "IPPrefix.IP", + "IPPrefix.Bits", "IPRange.Prefixes", "IPRange.prefixFrom128AndBits", - "IPRange.prefixFrom128AndBits-fm", "IPRange.entirelyBefore", + "IPRangeFrom", + "IPRange.To", + "IPRange.From", "IPv4", + "IPFrom4", "IPv6LinkLocalAllNodes", "IPv6Unspecified", "MustParseIP", diff --git a/ipset.go b/ipset.go index 27835d0..b448e25 100644 --- a/ipset.go +++ b/ipset.go @@ -4,7 +4,12 @@ package netaddr -import "sort" +import ( + "fmt" + "runtime" + "sort" + "strings" +) // IPSetBuilder builds an immutable IPSet. // @@ -14,12 +19,18 @@ import "sort" // Removals only affect the current membership of the set, so in // general Adds should be called first. Input ranges may overlap in // any way. +// +// Most IPSetBuilder methods do not return errors. +// Instead, errors are accumulated and reported by IPSetBuilder.IPSet. type IPSetBuilder struct { // in are the ranges in the set. in []IPRange // out are the ranges to be removed from 'in'. out []IPRange + + // errs are errors accumulated during construction. + errs multiErr } // normalize normalizes s: s.in becomes the minimal sorted list of @@ -53,7 +64,7 @@ func (s *IPSetBuilder) normalize() { } switch { - case !rout.Valid() || !rin.Valid(): + case !rout.IsValid() || !rin.IsValid(): // mergeIPRanges should have prevented invalid ranges from // sneaking in. panic("invalid IPRanges during Ranges merge") @@ -95,10 +106,10 @@ func (s *IPSetBuilder) normalize() { // f-------------t // f------t // out - min = append(min, IPRange{From: rin.From, To: rout.From.Prior()}) + min = append(min, IPRange{from: rin.from, to: rout.from.Prior()}) // Adjust in[0], not ir, because we want to consider the // mutated range on the next iteration. - in[0].From = rout.To.Next() + in[0].from = rout.to.Next() out = out[1:] if debug { debugf("out inside in; split in, append first in, drop out, adjust second in") @@ -111,7 +122,7 @@ func (s *IPSetBuilder) normalize() { // f------t // f------t // in - in[0].From = rout.To.Next() + in[0].from = rout.to.Next() // Can't move ir onto min yet, another later out might // trim it further. Just discard or and continue. out = out[1:] @@ -125,7 +136,7 @@ func (s *IPSetBuilder) normalize() { // f------t // f------t // in - min = append(min, IPRange{From: rin.From, To: rout.From.Prior()}) + min = append(min, IPRange{from: rin.from, to: rout.from.Prior()}) in = in[1:] if debug { debugf("merge out cuts end of in; append shortened in") @@ -157,16 +168,41 @@ func (s *IPSetBuilder) Clone() *IPSetBuilder { } } +func (s *IPSetBuilder) addError(msg string, args ...interface{}) { + se := new(stacktraceErr) + // Skip three frames: runtime.Callers, addError, and the IPSetBuilder + // method that called addError (such as IPSetBuilder.Add). + // The resulting stack trace ends at the line in the user's + // code where they called into netaddr. + n := runtime.Callers(3, se.pcs[:]) + se.at = se.pcs[:n] + se.err = fmt.Errorf(msg, args...) + s.errs = append(s.errs, se) +} + // Add adds ip to s. -func (s *IPSetBuilder) Add(ip IP) { s.AddRange(IPRange{ip, ip}) } +func (s *IPSetBuilder) Add(ip IP) { + if ip.IsZero() { + s.addError("Add(IP{})") + return + } + s.AddRange(IPRangeFrom(ip, ip)) +} // AddPrefix adds all IPs in p to s. -func (s *IPSetBuilder) AddPrefix(p IPPrefix) { s.AddRange(p.Range()) } +func (s *IPSetBuilder) AddPrefix(p IPPrefix) { + if r := p.Range(); r.IsValid() { + s.AddRange(r) + } else { + s.addError("AddPrefix(%v/%v)", p.IP(), p.Bits()) + } +} // AddRange adds r to s. // If r is not Valid, AddRange does nothing. func (s *IPSetBuilder) AddRange(r IPRange) { - if !r.Valid() { + if !r.IsValid() { + s.addError("AddRange(%v-%v)", r.From(), r.To()) return } // If there are any removals (s.out), then we need to compact the set @@ -179,26 +215,46 @@ func (s *IPSetBuilder) AddRange(r IPRange) { // AddSet adds all IPs in b to s. func (s *IPSetBuilder) AddSet(b *IPSet) { + if b == nil { + return + } for _, r := range b.rr { s.AddRange(r) } } // Remove removes ip from s. -func (s *IPSetBuilder) Remove(ip IP) { s.RemoveRange(IPRange{ip, ip}) } +func (s *IPSetBuilder) Remove(ip IP) { + if ip.IsZero() { + s.addError("Remove(IP{})") + } else { + s.RemoveRange(IPRangeFrom(ip, ip)) + } +} // RemovePrefix removes all IPs in p from s. -func (s *IPSetBuilder) RemovePrefix(p IPPrefix) { s.RemoveRange(p.Range()) } +func (s *IPSetBuilder) RemovePrefix(p IPPrefix) { + if r := p.Range(); r.IsValid() { + s.RemoveRange(r) + } else { + s.addError("RemovePrefix(%v/%v)", p.IP(), p.Bits()) + } +} // RemoveRange removes all IPs in r from s. func (s *IPSetBuilder) RemoveRange(r IPRange) { - if r.Valid() { + if r.IsValid() { s.out = append(s.out, r) + } else { + s.addError("RemoveRange(%v-%v)", r.From(), r.To()) } } // RemoveSet removes all IPs in o from s. func (s *IPSetBuilder) RemoveSet(b *IPSet) { + if b == nil { + return + } for _, r := range b.rr { s.RemoveRange(r) } @@ -218,8 +274,8 @@ func (s *IPSetBuilder) Complement() { s.normalize() s.out = s.in s.in = []IPRange{ - IPPrefix{IP: IPv4(0, 0, 0, 0), Bits: 0}.Range(), - IPPrefix{IP: IPv6Unspecified(), Bits: 0}.Range(), + IPPrefix{ip: IPv4(0, 0, 0, 0), bits: 0}.Range(), + IPPrefix{ip: IPv6Unspecified(), bits: 0}.Range(), } } @@ -236,13 +292,30 @@ func discardf(format string, args ...interface{}) {} // debugf is reassigned by tests. var debugf = discardf -// IPSet returns an immutable IPSet representing the current state of -// s. The builder remains usable after calling IPSet. -func (s *IPSetBuilder) IPSet() *IPSet { +// IPSet returns an immutable IPSet representing the current state of s. +// +// Most IPSetBuilder methods do not return errors. +// Rather, the builder ignores any invalid inputs (such as an invalid IPPrefix), +// and accumulates a list of any such errors that it encountered. +// +// IPSet also reports any such accumulated errors. +// Even if the returned error is non-nil, the returned IPSet is usable +// and contains all modifications made with valid inputs. +// +// The builder remains usable after calling IPSet. +// Calling IPSet clears any accumulated errors. +func (s *IPSetBuilder) IPSet() (*IPSet, error) { s.normalize() - return &IPSet{ + ret := &IPSet{ rr: append([]IPRange{}, s.in...), } + if len(s.errs) == 0 { + return ret, nil + } else { + errs := s.errs + s.errs = nil + return ret, errs + } } // IPSet represents a set of IP addresses. @@ -290,11 +363,16 @@ func (s *IPSet) Equal(o *IPSet) bool { } // Contains reports whether ip is in s. +// If ip has an IPv6 zone, Contains returns false, +// because IPSets do not track zones. func (s *IPSet) Contains(ip IP) bool { + if ip.hasZone() { + return false + } // TODO: data structure permitting more efficient lookups: // https://github.com/inetaf/netaddr/issues/139 i := sort.Search(len(s.rr), func(i int) bool { - return ip.Less(s.rr[i].From) + return ip.Less(s.rr[i].from) }) if i == 0 { return false @@ -356,12 +434,12 @@ func (s *IPSet) RemoveFreePrefix(bitLen uint8) (p IPPrefix, newSet *IPSet, ok bo var bestFit IPPrefix for _, r := range s.rr { for _, prefix := range r.Prefixes() { - if prefix.Bits > bitLen { + if prefix.bits > bitLen { continue } - if bestFit.IP.IsZero() || prefix.Bits > bestFit.Bits { + if bestFit.ip.IsZero() || prefix.bits > bestFit.bits { bestFit = prefix - if bestFit.Bits == bitLen { + if bestFit.bits == bitLen { // exact match, done. break } @@ -369,14 +447,51 @@ func (s *IPSet) RemoveFreePrefix(bitLen uint8) (p IPPrefix, newSet *IPSet, ok bo } } - if bestFit.IP.IsZero() { + if bestFit.ip.IsZero() { return IPPrefix{}, s, false } - prefix := IPPrefix{IP: bestFit.IP, Bits: bitLen} + prefix := IPPrefix{ip: bestFit.ip, bits: bitLen} var b IPSetBuilder b.AddSet(s) b.RemovePrefix(prefix) - return prefix, b.IPSet(), true + newSet, _ = b.IPSet() + return prefix, newSet, true +} + +type multiErr []error + +func (e multiErr) Error() string { + var ret []string + for _, err := range e { + ret = append(ret, err.Error()) + } + return strings.Join(ret, "; ") +} + +// A stacktraceErr combines an error with a stack trace. +type stacktraceErr struct { + pcs [16]uintptr // preallocated array of PCs + at []uintptr // stack trace whence the error + err error // underlying error +} + +func (e *stacktraceErr) Error() string { + frames := runtime.CallersFrames(e.at) + buf := new(strings.Builder) + buf.WriteString(e.err.Error()) + buf.WriteString(" @ ") + for { + frame, more := frames.Next() + if !more { + break + } + fmt.Fprintf(buf, "%s:%d ", frame.File, frame.Line) + } + return strings.TrimSpace(buf.String()) +} + +func (e *stacktraceErr) Unwrap() error { + return e.err } diff --git a/ipset_test.go b/ipset_test.go index 67e6ce9..459bf57 100644 --- a/ipset_test.go +++ b/ipset_test.go @@ -13,6 +13,14 @@ import ( "testing" ) +func buildIPSet(b *IPSetBuilder) *IPSet { + ret, err := b.IPSet() + if err != nil { + panic(err) + } + return ret +} + func TestIPSet(t *testing.T) { tests := []struct { name string @@ -272,7 +280,7 @@ func TestIPSet(t *testing.T) { t.AddRange(IPRange{mustIP("2.2.2.2"), mustIP("3.3.3.3")}) s.AddRange(IPRange{mustIP("1.1.1.1"), mustIP("4.4.4.4")}) - s.Intersect(t.IPSet()) + s.Intersect(buildIPSet(&t)) }, wantRanges: []IPRange{ {mustIP("2.2.2.2"), mustIP("3.3.3.3")}, @@ -285,7 +293,7 @@ func TestIPSet(t *testing.T) { t.AddRange(IPRange{mustIP("1.1.1.1"), mustIP("2.2.2.2")}) s.AddRange(IPRange{mustIP("3.3.3.3"), mustIP("4.4.4.4")}) - s.Intersect(t.IPSet()) + s.Intersect(buildIPSet(&t)) }, wantRanges: []IPRange{}, }, @@ -296,7 +304,7 @@ func TestIPSet(t *testing.T) { t.AddRange(IPRange{mustIP("1.1.1.1"), mustIP("3.3.3.3")}) s.AddRange(IPRange{mustIP("2.2.2.2"), mustIP("4.4.4.4")}) - s.Intersect(t.IPSet()) + s.Intersect(buildIPSet(&t)) }, wantRanges: []IPRange{ {mustIP("2.2.2.2"), mustIP("3.3.3.3")}, @@ -309,12 +317,12 @@ func TestIPSet(t *testing.T) { defer func() { debugf = discardf }() var build IPSetBuilder tt.f(&build) - s := build.IPSet() + s := buildIPSet(&build) got := s.Ranges() t.Run("ranges", func(t *testing.T) { for _, v := range got { - if !v.Valid() { - t.Errorf("invalid IPRange in result: %s -> %s", v.From, v.To) + if !v.IsValid() { + t.Errorf("invalid IPRange in result: %s -> %s", v.From(), v.To()) } } if reflect.DeepEqual(got, tt.wantRanges) { @@ -322,11 +330,11 @@ func TestIPSet(t *testing.T) { } t.Error("failed. got:\n") for _, v := range got { - t.Errorf(" %s -> %s", v.From, v.To) + t.Errorf(" %s -> %s", v.From(), v.To()) } t.Error("want:\n") for _, v := range tt.wantRanges { - t.Errorf(" %s -> %s", v.From, v.To) + t.Errorf(" %s -> %s", v.From(), v.To()) } }) if tt.wantPrefixes != nil { @@ -397,7 +405,7 @@ func TestIPSetRemoveFreePrefix(t *testing.T) { debugf = t.Logf var build IPSetBuilder tt.f(&build) - s := build.IPSet() + s := buildIPSet(&build) gotPrefix, gotSet, ok := s.RemoveFreePrefix(tt.b) if ok != tt.wantOK { t.Errorf("extractPrefix() ok = %t, wantOK %t", ok, tt.wantOK) @@ -407,7 +415,7 @@ func TestIPSetRemoveFreePrefix(t *testing.T) { t.Errorf("extractPrefix() = %v, want %v", gotPrefix, tt.wantPrefix) } if !reflect.DeepEqual(gotSet.Prefixes(), tt.wantPrefixes) { - t.Errorf("extractPrefix() = %v, want %v", build.IPSet().Prefixes(), tt.wantPrefixes) + t.Errorf("extractPrefix() = %v, want %v", gotSet.Prefixes(), tt.wantPrefixes) } }) } @@ -429,7 +437,7 @@ func mustIPSet(ranges ...string) *IPSet { panic(fmt.Sprintf("unknown command %q", r[0])) } } - return ret.IPSet() + return buildIPSet(&ret) } func TestIPSetOverlaps(t *testing.T) { @@ -496,7 +504,7 @@ func TestIPSetContains(t *testing.T) { build.AddPrefix(mustIPPrefix("10.0.0.0/8")) build.AddPrefix(mustIPPrefix("1.2.3.4/32")) build.AddPrefix(mustIPPrefix("fc00::/7")) - s := build.IPSet() + s := buildIPSet(&build) tests := []struct { ip string @@ -520,6 +528,9 @@ func TestIPSetContains(t *testing.T) { {"fc00::1", true}, {"fd00::1", true}, {"ff00::1", false}, + + {"fd00::%a", false}, + {"fd00::1%a", false}, } for _, tt := range tests { got := s.Contains(mustIP(tt.ip)) @@ -575,11 +586,11 @@ func newRandomIPSet() (steps []string, s *IPSet, wantContains [256]bool) { switch op { case 0: steps = append(steps, fmt.Sprintf("add 0.0.0.%d-0.0.0.%d", ip1, ip2)) - b.AddRange(IPRange{From: IPv4(0, 0, 0, ip1), To: IPv4(0, 0, 0, ip2)}) + b.AddRange(IPRangeFrom(IPv4(0, 0, 0, ip1), IPv4(0, 0, 0, ip2))) v = true case 1: steps = append(steps, fmt.Sprintf("remove 0.0.0.%d-0.0.0.%d", ip1, ip2)) - b.RemoveRange(IPRange{From: IPv4(0, 0, 0, ip1), To: IPv4(0, 0, 0, ip2)}) + b.RemoveRange(IPRangeFrom(IPv4(0, 0, 0, ip1), IPv4(0, 0, 0, ip2))) } for i := ip1; i <= ip2; i++ { wantContains[i] = v @@ -588,7 +599,7 @@ func newRandomIPSet() (steps []string, s *IPSet, wantContains [256]bool) { } } } - s = b.IPSet() + s = buildIPSet(b) return } @@ -609,7 +620,7 @@ func TestIPSetRanges(t *testing.T) { var from, to IP ranges := make([]IPRange, 0) flush := func() { - r := IPRange{From: from, To: to} + r := IPRangeFrom(from, to) build.AddRange(r) ranges = append(ranges, r) from, to = IP{}, IP{} @@ -630,7 +641,7 @@ func TestIPSetRanges(t *testing.T) { if !from.IsZero() { flush() } - got := build.IPSet().Ranges() + got := buildIPSet(&build).Ranges() if !reflect.DeepEqual(got, ranges) { t.Errorf("for %016b: got %v; want %v", pat, got, ranges) } @@ -651,10 +662,9 @@ func TestIPSetRangesStress(t *testing.T) { if a > b { a, b = b, a } - return a, b, IPRange{ - From: IPv4(0, 0, uint8(a>>8), uint8(a)), - To: IPv4(0, 0, uint8(b>>8), uint8(b)), - } + from := IPv4(0, 0, uint8(a>>8), uint8(a)) + to := IPv4(0, 0, uint8(b>>8), uint8(b)) + return a, b, IPRangeFrom(from, to) } for i := 0; i < n; i++ { var build IPSetBuilder @@ -675,14 +685,14 @@ func TestIPSetRangesStress(t *testing.T) { } build.RemoveRange(r) } - ranges := build.IPSet().Ranges() + ranges := buildIPSet(&build).Ranges() // Make sure no ranges are adjacent or overlapping for i, r := range ranges { if i == 0 { continue } - if ranges[i-1].To.Compare(r.From) != -1 { + if ranges[i-1].To().Compare(r.From()) != -1 { t.Fatalf("overlapping ranges: %v", ranges) } } @@ -694,7 +704,7 @@ func TestIPSetRangesStress(t *testing.T) { for _, r := range ranges { build2.AddRange(r) } - s2 := build2.IPSet() + s2 := buildIPSet(&build2) for i, want := range want { if got := s2.Contains(IPv4(0, 0, uint8(i>>8), uint8(i))); got != want { t.Fatal("failed") @@ -709,7 +719,7 @@ func TestIPSetEqual(t *testing.T) { assertEqual := func(want bool) { t.Helper() - if got := a.IPSet().Equal(b.IPSet()); got != want { + if got := buildIPSet(a).Equal(buildIPSet(b)); got != want { t.Errorf("%v.Equal(%v) = %v want %v", a, b, got, want) } } @@ -721,9 +731,9 @@ func TestIPSetEqual(t *testing.T) { b.Add(MustParseIP("1.1.1.2")) assertEqual(true) - a.RemoveSet(a.IPSet()) + a.RemoveSet(buildIPSet(a)) assertEqual(false) - b.RemoveSet(b.IPSet()) + b.RemoveSet(buildIPSet(b)) assertEqual(true) a.Add(MustParseIP("1.1.1.0")) diff --git a/netaddr.go b/netaddr.go index 5923038..9768406 100644 --- a/netaddr.go +++ b/netaddr.go @@ -10,6 +10,11 @@ // Notably, this package's IP type takes less memory, is immutable, // comparable (supports == and being a map key), and more. See // https://github.com/inetaf/netaddr for background. +// +// IPv6 Zones +// +// IP and IPPort are the only types in this package that support IPv6 +// zones. Other types silently drop any passed-in zones. package netaddr // import "inet.af/netaddr" import ( @@ -121,6 +126,12 @@ func IPFrom16(addr [16]byte) IP { return IPv6Raw(addr).Unmap() } +// IPFrom4 returns the IPv4 address given by the bytes in addr. +// It is equivalent to calling IPv4(addr[0], addr[1], addr[2], addr[3]). +func IPFrom4(addr [4]byte) IP { + return IPv4(addr[0], addr[1], addr[2], addr[3]) +} + // ParseIP parses s as an IP address, returning the result. The string // s can be in dotted decimal ("192.0.2.1"), IPv6 ("2001:db8::68"), // or IPv6 with a scoped addressing zone ("fe80::1cc0:3e8c:119f:c2e1%ens18"). @@ -382,6 +393,8 @@ func (ip IP) v6(i uint8) uint8 { return uint8(*(ip.addr.halves()[(i/8)%2]) >> ((7 - i%8) * 8)) } +// v6u16 returns the i'th 16-bit word of ip. If ip is an IPv4 address, +// this accesses the IPv4-mapped IPv6 address form of the IP. func (ip IP) v6u16(i uint8) uint16 { return uint16(*(ip.addr.halves()[(i/4)%2]) >> ((3 - i%4) * 16)) } @@ -389,13 +402,20 @@ func (ip IP) v6u16(i uint8) uint16 { // IsZero reports whether ip is the zero value of the IP type. // The zero value is not a valid IP address of any type. // -// Note that "0.0.0.0" and "::" are not the zero value. +// Note that "0.0.0.0" and "::" are not the zero value. Use IsUnspecified to +// check for these values instead. func (ip IP) IsZero() bool { // Faster than comparing ip == IP{}, but effectively equivalent, // as there's no way to make an IP with a nil z from this package. return ip.z == z0 } +// IsValid whether the IP is an initialized value and not the IP +// type's zero value. +// +// Note that both "0.0.0.0" and "::" are valid, non-zero values. +func (ip IP) IsValid() bool { return ip.z != z0 } + // BitLen returns the number of bits in the IP address: // 32 for IPv4 or 128 for IPv6. // For the zero value (see IP.IsZero), it returns 0. @@ -528,14 +548,33 @@ func (ip IP) WithZone(zone string) IP { return ip } +// noZone unconditionally strips the zone from IP. +// It's similar to WithZone, but small enough to be inlinable. +func (ip IP) withoutZone() IP { + if !ip.Is6() { + return ip + } + ip.z = z6noz + return ip +} + +// hasZone reports whether IP has an IPv6 zone. +func (ip IP) hasZone() bool { + return ip.z != z0 && ip.z != z4 && ip.z != z6noz +} + // IsLinkLocalUnicast reports whether ip is a link-local unicast address. // If ip is the zero value, it will return false. func (ip IP) IsLinkLocalUnicast() bool { + // Dynamic Configuration of IPv4 Link-Local Addresses + // https://datatracker.ietf.org/doc/html/rfc3927#section-2.1 if ip.Is4() { return ip.v4(0) == 169 && ip.v4(1) == 254 } + // IP Version 6 Addressing Architecture (2.4 Address Type Identification) + // https://datatracker.ietf.org/doc/html/rfc4291#section-2.4 if ip.Is6() { - return ip.v6u16(0) == 0xfe80 + return ip.v6u16(0)&0xffc0 == 0xfe80 } return false // zero value } @@ -543,9 +582,13 @@ func (ip IP) IsLinkLocalUnicast() bool { // IsLoopback reports whether ip is a loopback address. If ip is the zero value, // it will return false. func (ip IP) IsLoopback() bool { + // Requirements for Internet Hosts -- Communication Layers (3.2.1.3 Addressing) + // https://datatracker.ietf.org/doc/html/rfc1122#section-3.2.1.3 if ip.Is4() { return ip.v4(0) == 127 } + // IP Version 6 Addressing Architecture (2.4 Address Type Identification) + // https://datatracker.ietf.org/doc/html/rfc4291#section-2.4 if ip.Is6() { return ip.addr.hi == 0 && ip.addr.lo == 1 } @@ -555,9 +598,13 @@ func (ip IP) IsLoopback() bool { // IsMulticast reports whether ip is a multicast address. If ip is the zero // value, it will return false. func (ip IP) IsMulticast() bool { + // Host Extensions for IP Multicasting (4. HOST GROUP ADDRESSES) + // https://datatracker.ietf.org/doc/html/rfc1112#section-4 if ip.Is4() { return ip.v4(0)&0xf0 == 0xe0 } + // IP Version 6 Addressing Architecture (2.4 Address Type Identification) + // https://datatracker.ietf.org/doc/html/rfc4291#section-2.4 if ip.Is6() { return ip.addr.hi>>(64-8) == 0xff // ip.v6(0) == 0xff } @@ -568,6 +615,8 @@ func (ip IP) IsMulticast() bool { // multicast address. If ip is the zero value or an IPv4 address, it will return // false. func (ip IP) IsInterfaceLocalMulticast() bool { + // IPv6 Addressing Architecture (2.7.1. Pre-Defined Multicast Addresses) + // https://datatracker.ietf.org/doc/html/rfc4291#section-2.7.1 if ip.Is6() { return ip.v6u16(0)&0xff0f == 0xff01 } @@ -577,15 +626,78 @@ func (ip IP) IsInterfaceLocalMulticast() bool { // IsLinkLocalMulticast reports whether ip is a link-local multicast address. // If ip is the zero value, it will return false. func (ip IP) IsLinkLocalMulticast() bool { + // IPv4 Multicast Guidelines (4. Local Network Control Block (224.0.0/24)) + // https://datatracker.ietf.org/doc/html/rfc5771#section-4 if ip.Is4() { return ip.v4(0) == 224 && ip.v4(1) == 0 && ip.v4(2) == 0 } + // IPv6 Addressing Architecture (2.7.1. Pre-Defined Multicast Addresses) + // https://datatracker.ietf.org/doc/html/rfc4291#section-2.7.1 if ip.Is6() { return ip.v6u16(0)&0xff0f == 0xff02 } return false // zero value } +// IsGlobalUnicast reports whether ip is a global unicast address. +// +// It returns true for IPv6 addresses which fall outside of the current +// IANA-allocated 2000::/3 global unicast space, with the exception of the +// link-local address space. It also returns true even if ip is in the IPv4 +// private address space or IPv6 unique local address space. If ip is the zero +// value, it will return false. +// +// For reference, see RFC 1122, RFC 4291, and RFC 4632. +func (ip IP) IsGlobalUnicast() bool { + if ip.z == z0 { + // Invalid or zero-value. + return false + } + + // Match the stdlib's IsGlobalUnicast logic. Notably private IPv4 addresses + // and ULA IPv6 addresses are still considered "global unicast". + if ip.Is4() && (ip == IPv4(0, 0, 0, 0) || ip == IPv4(255, 255, 255, 255)) { + return false + } + + return ip != IPv6Unspecified() && + !ip.IsLoopback() && + !ip.IsMulticast() && + !ip.IsLinkLocalUnicast() +} + +// IsPrivate reports whether ip is a private address, according to RFC 1918 +// (IPv4 addresses) and RFC 4193 (IPv6 addresses). That is, it reports whether +// ip is in 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16, or fc00::/7. This is the +// same as the standard library's net.IP.IsPrivate. +func (ip IP) IsPrivate() bool { + // Match the stdlib's IsPrivate logic. + if ip.Is4() { + // RFC 1918 allocates 10.0.0.0/8, 172.16.0.0/12, and 192.168.0.0/16 as + // private IPv4 address subnets. + return ip.v4(0) == 10 || + (ip.v4(0) == 172 && ip.v4(1)&0xf0 == 16) || + (ip.v4(0) == 192 && ip.v4(1) == 168) + } + + if ip.Is6() { + // RFC 4193 allocates fc00::/7 as the unique local unicast IPv6 address + // subnet. + return ip.v6(0)&0xfe == 0xfc + } + + return false // zero value +} + +// IsUnspecified reports whether ip is an unspecified address, either the IPv4 +// address "0.0.0.0" or the IPv6 address "::". +// +// Note that the IP zero value is not an unspecified address. Use IsZero to +// check for the zero value instead. +func (ip IP) IsUnspecified() bool { + return ip == IPv4(0, 0, 0, 0) || ip == IPv6Unspecified() +} + // Prefix applies a CIDR mask of leading bits to IP, producing an IPPrefix // of the specified length. If IP is the zero value, a zero-value IPPrefix and // a nil error are returned. If bits is larger than 32 for an IPv4 address or @@ -606,14 +718,14 @@ func (ip IP) Prefix(bits uint8) (IPPrefix, error) { } } ip.addr = ip.addr.and(mask6[effectiveBits]) - return IPPrefix{ip, bits}, nil + return IPPrefixFrom(ip, bits), nil } // Netmask applies a bit mask to IP, producing an IPPrefix. If IP is the // zero value, a zero-value IPPrefix and a nil error are returned. If the // netmask length is not 4 for IPv4 or 16 for IPv6, an error is // returned. If the netmask is non-contiguous, an error is returned. -func (ip *IP) Netmask(mask []byte) (IPPrefix, error) { +func (ip IP) Netmask(mask []byte) (IPPrefix, error) { l := len(mask) switch ip.z { @@ -664,6 +776,38 @@ func (ip IP) As4() [4]byte { panic("As4 called on IPv6 address") } +// Next returns the IP following ip. +// If there is none, it returns the IP zero value. +func (ip IP) Next() IP { + ip.addr = ip.addr.addOne() + if ip.Is4() { + if uint32(ip.addr.lo) == 0 { + // Overflowed. + return IP{} + } + } else { + if ip.addr.isZero() { + // Overflowed + return IP{} + } + } + return ip +} + +// Prior returns the IP before ip. +// If there is none, it returns the IP zero value. +func (ip IP) Prior() IP { + if ip.Is4() { + if uint32(ip.addr.lo) == 0 { + return IP{} + } + } else if ip.addr.isZero() { + return IP{} + } + ip.addr = ip.addr.subOne() + return ip +} + // String returns the string form of the IP address ip. // It returns one of 4 forms: // @@ -677,7 +821,7 @@ func (ip IP) As4() [4]byte { func (ip IP) String() string { switch ip.z { case z0: - return "invalid IP" + return "zero IP" case z4: return ip.string4() default: @@ -685,6 +829,20 @@ func (ip IP) String() string { } } +// AppendTo appends a text encoding of ip, +// as generated by MarshalText, +// to b and returns the extended buffer. +func (ip IP) AppendTo(b []byte) []byte { + switch ip.z { + case z0: + return b + case z4: + return ip.appendTo4(b) + default: + return ip.appendTo6(b) + } +} + // digits is a string of the hex digits from 0 to f. It's used in // appendDecimal and appendHex to format IP addresses. const digits = "0123456789abcdef" @@ -720,9 +878,19 @@ func appendHex(b []byte, x uint16) []byte { return append(b, digits[x&0xf]) } +// appendHexPad appends the fully padded hex string representation of x to b. +func appendHexPad(b []byte, x uint16) []byte { + return append(b, digits[x>>12], digits[x>>8&0xf], digits[x>>4&0xf], digits[x&0xf]) +} + func (ip IP) string4() string { const max = len("255.255.255.255") ret := make([]byte, 0, max) + ret = ip.appendTo4(ret) + return string(ret) +} + +func (ip IP) appendTo4(ret []byte) []byte { ret = appendDecimal(ret, ip.v4(0)) ret = append(ret, '.') ret = appendDecimal(ret, ip.v4(1)) @@ -730,7 +898,7 @@ func (ip IP) string4() string { ret = appendDecimal(ret, ip.v4(2)) ret = append(ret, '.') ret = appendDecimal(ret, ip.v4(3)) - return string(ret) + return ret } // string6 formats ip in IPv6 textual representation. It follows the @@ -739,6 +907,20 @@ func (ip IP) string4() string { // zeros, use :: to elide the longest run of zeros, and don't use :: // to compact a single zero field. func (ip IP) string6() string { + // Use a zone with a "plausibly long" name, so that most zone-ful + // IP addresses won't require additional allocation. + // + // The compiler does a cool optimization here, where ret ends up + // stack-allocated and so the only allocation this function does + // is to construct the returned string. As such, it's okay to be a + // bit greedy here, size-wise. + const max = len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0") + ret := make([]byte, 0, max) + ret = ip.appendTo6(ret) + return string(ret) +} + +func (ip IP) appendTo6(ret []byte) []byte { zeroStart, zeroEnd := uint8(255), uint8(255) for i := uint8(0); i < 8; i++ { j := i @@ -750,15 +932,6 @@ func (ip IP) string6() string { } } - // Use a zone with a "plausibly long" name, so that most zone-ful - // IP addresses won't require additional allocation. - // - // The compiler does a cool optimization here, where ret ends up - // stack-allocated and so the only allocation this function does - // is to construct the returned string. As such, it's okay to be a - // bit greedy here, size-wise. - const max = len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0") - ret := make([]byte, 0, max) for i := uint8(0); i < 8; i++ { if i == zeroStart { ret = append(ret, ':', ':') @@ -777,6 +950,34 @@ func (ip IP) string6() string { ret = append(ret, '%') ret = append(ret, ip.Zone()...) } + return ret +} + +// StringExpanded is like String but IPv6 addresses are expanded with leading +// zeroes and no "::" compression. For example, "2001:db8::1" becomes +// "2001:0db8:0000:0000:0000:0000:0000:0001". +func (ip IP) StringExpanded() string { + switch ip.z { + case z0, z4: + return ip.String() + } + + const size = len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + ret := make([]byte, 0, size) + for i := uint8(0); i < 8; i++ { + if i > 0 { + ret = append(ret, ':') + } + + ret = appendHexPad(ret, ip.v6u16(i)) + } + + if ip.z != z6noz { + // The addition of a zone will cause a second allocation, but when there + // is no zone the ret slice will be stack allocated. + ret = append(ret, '%') + ret = append(ret, ip.Zone()...) + } return string(ret) } @@ -784,10 +985,18 @@ func (ip IP) string6() string { // The encoding is the same as returned by String, with one exception: // If ip is the zero value, the encoding is the empty string. func (ip IP) MarshalText() ([]byte, error) { - if ip.z == z0 { + switch ip.z { + case z0: return []byte(""), nil + case z4: + max := len("255.255.255.255") + b := make([]byte, 0, max) + return ip.appendTo4(b), nil + default: + max := len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0") + b := make([]byte, 0, max) + return ip.appendTo6(b), nil } - return []byte(ip.String()), nil } // UnmarshalText implements the encoding.TextUnmarshaler interface. @@ -795,7 +1004,7 @@ func (ip IP) MarshalText() ([]byte, error) { // It returns an error if *ip is not the IP zero value. func (ip *IP) UnmarshalText(text []byte) error { if ip.z != z0 { - return errors.New("netaddr: refusing to Unmarshal into non-zero IP") + return errors.New("refusing to Unmarshal into non-zero IP") } if len(text) == 0 { return nil @@ -805,14 +1014,68 @@ func (ip *IP) UnmarshalText(text []byte) error { return err } -// IPPort is an IP & port number. -// -// It's meant to be used as a value type. +// MarshalBinary implements the encoding.BinaryMarshaler interface. +func (ip IP) MarshalBinary() ([]byte, error) { + switch ip.z { + case z0: + return nil, nil + case z4: + b := ip.As4() + return b[:], nil + default: + b16 := ip.As16() + b := b16[:] + if z := ip.Zone(); z != "" { + b = append(b, []byte(z)...) + } + return b, nil + } +} + +// UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. +func (ip *IP) UnmarshalBinary(b []byte) error { + if ip.z != z0 { + return errors.New("refusing to Unmarshal into non-zero IP") + } + n := len(b) + switch { + case n == 0: + return nil + case n == 4: + *ip = IPv4(b[0], b[1], b[2], b[3]) + return nil + case n == 16: + *ip = ipv6Slice(b) + return nil + case n > 16: + *ip = ipv6Slice(b[:16]).WithZone(string(b[16:])) + return nil + } + return fmt.Errorf("unexpected ip size: %v", len(b)) +} + +// IPPort is an IP and a port number. type IPPort struct { - IP IP - Port uint16 + ip IP + port uint16 } +// IPPortFrom returns an IPPort with IP ip and port port. +// It does not allocate. +func IPPortFrom(ip IP, port uint16) IPPort { return IPPort{ip: ip, port: port} } + +// WithIP returns an IPPort with IP ip and port p.Port(). +func (p IPPort) WithIP(ip IP) IPPort { return IPPort{ip: ip, port: p.port} } + +// WithIP returns an IPPort with IP p.IP() and port port. +func (p IPPort) WithPort(port uint16) IPPort { return IPPort{ip: p.ip, port: port} } + +// IP returns p's IP. +func (p IPPort) IP() IP { return p.ip } + +// Port returns p's port. +func (p IPPort) Port() uint16 { return p.port } + // splitIPPort splits s into an IP address string and a port // string. It splits strings shaped like "foo:bar" or "[foo]:bar", // without further validating the substrings. v6 indicates whether the @@ -855,14 +1118,14 @@ func ParseIPPort(s string) (IPPort, error) { if err != nil { return ipp, fmt.Errorf("invalid port %q parsing %q", port, s) } - ipp.Port = uint16(port16) - ipp.IP, err = ParseIP(ip) + ipp.port = uint16(port16) + ipp.ip, err = ParseIP(ip) if err != nil { return IPPort{}, err } - if v6 && ipp.IP.Is4() { + if v6 && ipp.ip.Is4() { return IPPort{}, fmt.Errorf("invalid ip:port %q, square brackets can only be used with IPv6 addresses", s) - } else if !v6 && ipp.IP.Is6() { + } else if !v6 && ipp.ip.Is6() { return IPPort{}, fmt.Errorf("invalid ip:port %q, IPv6 addresses must be surrounded by square brackets", s) } return ipp, nil @@ -881,23 +1144,63 @@ func MustParseIPPort(s string) IPPort { // IsZero reports whether p is its zero value. func (p IPPort) IsZero() bool { return p == IPPort{} } +// IsValid reports whether p.IP() is valid. +// All ports are valid, including zero. +func (p IPPort) IsValid() bool { return p.ip.IsValid() } + +// Valid reports whether p.IP() is valid. +// All ports are valid, including zero. +// +// Deprecated: use the correctly named and identical IsValid method instead. +func (p IPPort) Valid() bool { return p.IsValid() } + func (p IPPort) String() string { - if p.IP.z == z4 { - a := p.IP.As4() - return fmt.Sprintf("%d.%d.%d.%d:%d", a[0], a[1], a[2], a[3], p.Port) + switch p.ip.z { + case z0: + return "invalid IPPort" + case z4: + a := p.ip.As4() + return fmt.Sprintf("%d.%d.%d.%d:%d", a[0], a[1], a[2], a[3], p.port) + default: + // TODO: this could be more efficient allocation-wise: + return net.JoinHostPort(p.ip.String(), strconv.Itoa(int(p.port))) + } +} + +// AppendTo appends a text encoding of p, +// as generated by MarshalText, +// to b and returns the extended buffer. +func (p IPPort) AppendTo(b []byte) []byte { + switch p.ip.z { + case z0: + return b + case z4: + b = p.ip.appendTo4(b) + default: + b = append(b, '[') + b = p.ip.appendTo6(b) + b = append(b, ']') } - // TODO: this could be more efficient allocation-wise: - return net.JoinHostPort(p.IP.String(), strconv.Itoa(int(p.Port))) + b = append(b, ':') + b = strconv.AppendInt(b, int64(p.port), 10) + return b } // MarshalText implements the encoding.TextMarshaler interface. The // encoding is the same as returned by String, with one exception: if -// p.IP is the zero value, the encoding is the empty string. +// p.IP() is the zero value, the encoding is the empty string. func (p IPPort) MarshalText() ([]byte, error) { - if p.IP.z == z0 { - return []byte(""), nil + var max int + switch p.ip.z { + case z0: + case z4: + max = len("255.255.255.255:65535") + default: + max = len("[ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0]:65535") } - return []byte(p.String()), nil + b := make([]byte, 0, max) + b = p.AppendTo(b) + return b, nil } // UnmarshalText implements the encoding.TextUnmarshaler @@ -905,8 +1208,8 @@ func (p IPPort) MarshalText() ([]byte, error) { // ParseIPPort. It returns an error if *p is not the IPPort zero // value. func (p *IPPort) UnmarshalText(text []byte) error { - if p.IP.z != z0 || p.Port != 0 { - return errors.New("netaddr: refusing to UnmarshalText into non-zero IP") + if p.ip.z != z0 || p.port != 0 { + return errors.New("refusing to Unmarshal into non-zero IPPort") } if len(text) == 0 { return nil @@ -931,20 +1234,20 @@ func FromStdAddr(stdIP net.IP, port int, zone string) (_ IPPort, ok bool) { } ip = ip.WithZone(zone) } - return IPPort{IP: ip, Port: uint16(port)}, true + return IPPort{ip: ip, port: uint16(port)}, true } // UDPAddr returns a standard library net.UDPAddr from p. -// The returned value is always non-nil. If p.IP is the zero +// The returned value is always non-nil. If p.IP() is the zero // value, then UDPAddr.IP is nil. // // UDPAddr necessarily does two allocations. If you have an existing // UDPAddr already allocated, see UDPAddrAt. func (p IPPort) UDPAddr() *net.UDPAddr { ret := &net.UDPAddr{ - Port: int(p.Port), + Port: int(p.port), } - ret.IP, ret.Zone = p.IP.ipZone(nil) + ret.IP, ret.Zone = p.ip.ipZone(nil) return ret } @@ -953,41 +1256,62 @@ func (p IPPort) UDPAddr() *net.UDPAddr { // allocation-free. It returns at to facilitate using this method as a // wrapper. func (p IPPort) UDPAddrAt(at *net.UDPAddr) *net.UDPAddr { - at.Port = int(p.Port) - at.IP, at.Zone = p.IP.ipZone(at.IP) + at.Port = int(p.port) + at.IP, at.Zone = p.ip.ipZone(at.IP) return at } // TCPAddr returns a standard library net.TCPAddr from p. -// The returned value is always non-nil. If p.IP is the zero +// The returned value is always non-nil. If p.IP() is the zero // value, then TCPAddr.IP is nil. func (p IPPort) TCPAddr() *net.TCPAddr { - ip, zone := p.IP.ipZone(nil) + ip, zone := p.ip.ipZone(nil) return &net.TCPAddr{ IP: ip, - Port: int(p.Port), + Port: int(p.port), Zone: zone, } } // IPPrefix is an IP address prefix (CIDR) representing an IP network. // -// The first Bits of IP are specified, the remaining bits match any address. -// The range of Bits is [0,32] for IPv4 or [0,128] for IPv6. +// The first Bits() of IP() are specified. The remaining bits match any address. +// The range of Bits() is [0,32] for IPv4 or [0,128] for IPv6. type IPPrefix struct { - IP IP - Bits uint8 + ip IP + bits uint8 +} + +// IPPrefixFrom returns an IPPrefix with IP ip and provided bits prefix length. +// It does not allocate. +func IPPrefixFrom(ip IP, bits uint8) IPPrefix { + return IPPrefix{ + ip: ip.withoutZone(), + bits: bits, + } } -// Valid reports whether whether p.Bits has a valid range for p.IP. -// If p.IP is zero, Valid returns false. -func (p IPPrefix) Valid() bool { return !p.IP.IsZero() && p.Bits <= p.IP.BitLen() } +// IP returns p's IP. +func (p IPPrefix) IP() IP { return p.ip } + +// Bits returns p's prefix length. +func (p IPPrefix) Bits() uint8 { return p.bits } + +// IsValid reports whether whether p.Bits() has a valid range for p.IP(). +// If p.IP() is zero, Valid returns false. +func (p IPPrefix) IsValid() bool { return !p.ip.IsZero() && p.bits <= p.ip.BitLen() } + +// Valid reports whether whether p.Bits() has a valid range for p.IP(). +// If p.IP() is zero, Valid returns false. +// +// Deprecated: use the correctly named and identical IsValid method instead. +func (p IPPrefix) Valid() bool { return p.IsValid() } // IsZero reports whether p is its zero value. func (p IPPrefix) IsZero() bool { return p == IPPrefix{} } // IsSingleIP reports whether p contains exactly one IP. -func (p IPPrefix) IsSingleIP() bool { return p.Bits != 0 && p.Bits == p.IP.BitLen() } +func (p IPPrefix) IsSingleIP() bool { return p.bits != 0 && p.bits == p.ip.BitLen() } // FromStdIPNet returns an IPPrefix from the standard library's IPNet type. // If std is invalid, ok is false. @@ -1009,8 +1333,8 @@ func FromStdIPNet(std *net.IPNet) (prefix IPPrefix, ok bool) { } return IPPrefix{ - IP: ip, - Bits: uint8(ones), + ip: ip, + bits: uint8(ones), }, true } @@ -1020,7 +1344,7 @@ func FromStdIPNet(std *net.IPNet) (prefix IPPrefix, ok bool) { // // Note that masked address bits are not zeroed. Use Masked for that. func ParseIPPrefix(s string) (IPPrefix, error) { - i := strings.IndexByte(s, '/') + i := strings.LastIndexByte(s, '/') if i < 0 { return IPPrefix{}, fmt.Errorf("netaddr.ParseIPPrefix(%q): no '/'", s) } @@ -1040,10 +1364,7 @@ func ParseIPPrefix(s string) (IPPrefix, error) { if bits < 0 || bits > maxBits { return IPPrefix{}, fmt.Errorf("netaddr.ParseIPPrefix(%q): prefix length out of range", s) } - return IPPrefix{ - IP: ip, - Bits: uint8(bits), - }, nil + return IPPrefixFrom(ip, uint8(bits)), nil } // MustParseIPPrefix calls ParseIPPrefix(s) and panics on error. @@ -1056,10 +1377,10 @@ func MustParseIPPrefix(s string) IPPrefix { return ip } -// Masked returns p in its canonical form, with bits of p.IP not in p.Bits masked off. +// Masked returns p in its canonical form, with bits of p.IP() not in p.Bits() masked off. // If p is zero or otherwise invalid, Masked returns the zero value. func (p IPPrefix) Masked() IPPrefix { - if m, err := p.IP.Prefix(p.Bits); err == nil { + if m, err := p.ip.Prefix(p.bits); err == nil { return m } return IPPrefix{} @@ -1073,20 +1394,20 @@ func (p IPPrefix) Range() IPRange { if p.IsZero() { return IPRange{} } - return IPRange{From: p.IP, To: p.lastIP()} + return IPRangeFrom(p.ip, p.lastIP()) } // IPNet returns the net.IPNet representation of an IPPrefix. // The returned value is always non-nil. // Any zone identifier is dropped in the conversion. func (p IPPrefix) IPNet() *net.IPNet { - if !p.Valid() { + if !p.IsValid() { return &net.IPNet{} } - stdIP, _ := p.IP.ipZone(nil) + stdIP, _ := p.ip.ipZone(nil) return &net.IPNet{ IP: stdIP, - Mask: net.CIDRMask(int(p.Bits), int(p.IP.BitLen())), + Mask: net.CIDRMask(int(p.bits), int(p.ip.BitLen())), } } @@ -1095,11 +1416,13 @@ func (p IPPrefix) IPNet() *net.IPNet { // An IPv4 address will not match an IPv6 prefix. // A v6-mapped IPv6 address will not match an IPv4 prefix. // A zero-value IP will not match any prefix. +// If ip has an IPv6 zone, Contains returns false, +// because IPPrefixes strip zones. func (p IPPrefix) Contains(ip IP) bool { - if !p.Valid() { + if !p.IsValid() || ip.hasZone() { return false } - if f1, f2 := p.IP.BitLen(), ip.BitLen(); f1 == 0 || f2 == 0 || f1 != f2 { + if f1, f2 := p.ip.BitLen(), ip.BitLen(); f1 == 0 || f2 == 0 || f1 != f2 { return false } if ip.Is4() { @@ -1107,16 +1430,16 @@ func (p IPPrefix) Contains(ip IP) bool { // Shift away the number of bits we don't care about. // Shifts in Go are more efficient if the compiler can prove // that the shift amount is smaller than the width of the shifted type (64 here). - // We know that p.Bits is in the range 0..32 because p is Valid; + // We know that p.bits is in the range 0..32 because p is Valid; // the compiler doesn't know that, so mask with 63 to help it. // Now truncate to 32 bits, because this is IPv4. // If all the bits we care about are equal, the result will be zero. - return uint32((ip.addr.lo^p.IP.addr.lo)>>((32-p.Bits)&63)) == 0 + return uint32((ip.addr.lo^p.ip.addr.lo)>>((32-p.bits)&63)) == 0 } else { // xor the IP addresses together. // Mask away the bits we don't care about. // If all the bits we care about are equal, the result will be zero. - return ip.addr.xor(p.IP.addr).and(mask6[p.Bits]).isZero() + return ip.addr.xor(p.ip.addr).and(mask6[p.bits]).isZero() } } @@ -1128,20 +1451,20 @@ func (p IPPrefix) Contains(ip IP) bool { // // If either has a Bits of zero, it returns true. func (p IPPrefix) Overlaps(o IPPrefix) bool { - if !p.Valid() || !o.Valid() { + if !p.IsValid() || !o.IsValid() { return false } if p == o { return true } - if p.IP.Is4() != o.IP.Is4() { + if p.ip.Is4() != o.ip.Is4() { return false } var minBits uint8 - if p.Bits < o.Bits { - minBits = p.Bits + if p.bits < o.bits { + minBits = p.bits } else { - minBits = o.Bits + minBits = o.bits } if minBits == 0 { return true @@ -1151,24 +1474,53 @@ func (p IPPrefix) Overlaps(o IPPrefix) bool { // so the Prefix call on the one that's already minBits serves to zero // out any remaining bits in IP. var err error - if p, err = p.IP.Prefix(minBits); err != nil { + if p, err = p.ip.Prefix(minBits); err != nil { return false } - if o, err = o.IP.Prefix(minBits); err != nil { + if o, err = o.ip.Prefix(minBits); err != nil { return false } - return p.IP == o.IP + return p.ip == o.ip +} + +// AppendTo appends a text encoding of p, +// as generated by MarshalText, +// to b and returns the extended buffer. +func (p IPPrefix) AppendTo(b []byte) []byte { + if p.IsZero() { + return b + } + if !p.IsValid() { + return append(b, "invalid IPPrefix"...) + } + + // p.IP is non-zero, because p is valid. + if p.ip.z == z4 { + b = p.ip.appendTo4(b) + } else { + b = p.ip.appendTo6(b) + } + + b = append(b, '/') + b = appendDecimal(b, p.bits) + return b } // MarshalText implements the encoding.TextMarshaler interface, // The encoding is the same as returned by String, with one exception: // If p is the zero value, the encoding is the empty string. func (p IPPrefix) MarshalText() ([]byte, error) { - if p == (IPPrefix{}) { - return []byte(""), nil + var max int + switch p.ip.z { + case z0: + case z4: + max = len("255.255.255.255/32") + default: + max = len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff%enp5s0/128") } - - return []byte(p.String()), nil + b := make([]byte, 0, max) + b = p.AppendTo(b) + return b, nil } // UnmarshalText implements the encoding.TextUnmarshaler interface. @@ -1176,13 +1528,11 @@ func (p IPPrefix) MarshalText() ([]byte, error) { // It returns an error if *p is not the IPPrefix zero value. func (p *IPPrefix) UnmarshalText(text []byte) error { if *p != (IPPrefix{}) { - return errors.New("netaddr: refusing to Unmarshal into non-zero IPPrefix") + return errors.New("refusing to Unmarshal into non-zero IPPrefix") } - if len(text) == 0 { return nil } - var err error *p, err = ParseIPPrefix(string(text)) return err @@ -1190,29 +1540,32 @@ func (p *IPPrefix) UnmarshalText(text []byte) error { // String returns the CIDR notation of p: "/". func (p IPPrefix) String() string { - if !p.Valid() { - return "invalid IP prefix" + if p.IsZero() { + return "zero IPPrefix" } - return fmt.Sprintf("%s/%d", p.IP, p.Bits) + if !p.IsValid() { + return "invalid IPPrefix" + } + return fmt.Sprintf("%s/%d", p.ip, p.bits) } // lastIP returns the last IP in the prefix. func (p IPPrefix) lastIP() IP { - if !p.Valid() { + if !p.IsValid() { return IP{} } - a16 := p.IP.As16() + a16 := p.ip.As16() var off uint8 var bits uint8 = 128 - if p.IP.Is4() { + if p.ip.Is4() { off = 12 bits = 32 } - for b := p.Bits; b < bits; b++ { + for b := p.bits; b < bits; b++ { byteNum, bitInByte := b/8, 7-(b%8) a16[off+byteNum] |= 1 << uint(bitInByte) } - if p.IP.Is4() { + if p.ip.Is4() { return IPFrom16(a16) } else { return IPv6Raw(a16) // doesn't unmap @@ -1222,21 +1575,36 @@ func (p IPPrefix) lastIP() IP { // IPRange represents an inclusive range of IP addresses // from the same address family. // -// The From and To IPs are inclusive bounds, both included in the +// The From() and To() IPs are inclusive bounds, both included in the // range. // -// To be valid, the From and To values be non-zero, have matching -// address families (IPv4 vs IPv6), be in the same IPv6 zone (if any), -// and From must be less than or equal to To. +// To be valid, the From() and To() values must be non-zero, have matching +// address families (IPv4 vs IPv6), and From() must be less than or equal to To(). +// IPv6 zones are stripped out and ignored. // An invalid range may be ignored. type IPRange struct { - // From is the initial IP address in the range. - From IP + // from is the initial IP address in the range. + from IP + + // to is the final IP address in the range. + to IP +} - // To is the final IP address in the range. - To IP +// IPRangeFrom returns an IPRange from from to to. +// It does not allocate. +func IPRangeFrom(from, to IP) IPRange { + return IPRange{ + from: from.withoutZone(), + to: to.withoutZone(), + } } +// From returns the lower bound of r. +func (r IPRange) From() IP { return r.from } + +// To returns the upper bound of r. +func (r IPRange) To() IP { return r.to } + // ParseIPRange parses a range out of two IPs separated by a hyphen. // // It returns an error if the range is not valid. @@ -1248,94 +1616,167 @@ func ParseIPRange(s string) (IPRange, error) { } from, to := s[:h], s[h+1:] var err error - r.From, err = ParseIP(from) + r.from, err = ParseIP(from) if err != nil { return r, fmt.Errorf("invalid From IP %q in range %q", from, s) } - r.To, err = ParseIP(to) + r.from = r.from.withoutZone() + r.to, err = ParseIP(to) if err != nil { return r, fmt.Errorf("invalid To IP %q in range %q", to, s) } - if !r.Valid() { - return r, fmt.Errorf("range %v to %v not valid", r.From, r.To) + r.to = r.to.withoutZone() + if !r.IsValid() { + return r, fmt.Errorf("range %v to %v not valid", r.from, r.to) } return r, nil } +// MustParseIPRange calls ParseIPRange(s) and panics on error. +// It is intended for use in tests with hard-coded strings. +func MustParseIPRange(s string) IPRange { + r, err := ParseIPRange(s) + if err != nil { + panic(err) + } + return r +} + // String returns a string representation of the range. // // For a valid range, the form is "From-To" with a single hyphen // separating the IPs, the same format recognized by // ParseIPRange. func (r IPRange) String() string { - if r.Valid() { - return fmt.Sprintf("%s-%s", r.From, r.To) + if r.IsValid() { + return fmt.Sprintf("%s-%s", r.from, r.to) } - if r.From.IsZero() || r.To.IsZero() { + if r.from.IsZero() || r.to.IsZero() { return "zero IPRange" } return "invalid IPRange" } -// Valid reports whether r.From and r.To are both non-zero and obey -// the documented requirements: address families match, same IPv6 -// zone, and From is less than or equal to To. -func (r IPRange) Valid() bool { - return !r.From.IsZero() && - r.From.z == r.To.z && - !r.To.Less(r.From) +// AppendTo appends a text encoding of r, +// as generated by MarshalText, +// to b and returns the extended buffer. +func (r IPRange) AppendTo(b []byte) []byte { + if r.IsZero() { + return b + } + b = r.from.AppendTo(b) + b = append(b, '-') + b = r.to.AppendTo(b) + return b } +// MarshalText implements the encoding.TextMarshaler interface, +// The encoding is the same as returned by String, with one exception: +// If ip is the zero value, the encoding is the empty string. +func (r IPRange) MarshalText() ([]byte, error) { + if r.IsZero() { + return []byte(""), nil + } + var max int + if r.from.z == z4 { + max = len("255.255.255.255-255.255.255.255") + } else { + max = len("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff-ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff") + } + b := make([]byte, 0, max) + return r.AppendTo(b), nil +} + +// UnmarshalText implements the encoding.TextUnmarshaler interface. +// The IP range is expected in a form accepted by ParseIPRange. +// It returns an error if *r is not the IPRange zero value. +func (r *IPRange) UnmarshalText(text []byte) error { + if *r != (IPRange{}) { + return errors.New("refusing to Unmarshal into non-zero IPRange") + } + if len(text) == 0 { + return nil + } + var err error + *r, err = ParseIPRange(string(text)) + return err +} + +// IsZero reports whether r is the zero value of the IPRange type. +func (r IPRange) IsZero() bool { + return r == IPRange{} +} + +// IsValid reports whether r.From() and r.To() are both non-zero and +// obey the documented requirements: address families match, and From +// is less than or equal to To. +func (r IPRange) IsValid() bool { + return !r.from.IsZero() && + r.from.z == r.to.z && + !r.to.Less(r.from) +} + +// Valid reports whether r.From() and r.To() are both non-zero and +// obey the documented requirements: address families match, and From +// is less than or equal to To. +// +// Deprecated: use the correctly named and identical IsValid method instead. +func (r IPRange) Valid() bool { return r.IsValid() } + // Contains reports whether the range r includes addr. // // An invalid range always reports false. +// +// If ip has an IPv6 zone, Contains returns false, +// because IPPrefixes strip zones. func (r IPRange) Contains(addr IP) bool { - return r.Valid() && r.contains(addr) + return r.IsValid() && !addr.hasZone() && r.contains(addr) } -// contains is like Contains, but without the validity check. For internal use. +// contains is like Contains, but without the validity check. +// addr must not have a zone. func (r IPRange) contains(addr IP) bool { - return r.From.Compare(addr) <= 0 && r.To.Compare(addr) >= 0 + return r.from.Compare(addr) <= 0 && r.to.Compare(addr) >= 0 } -// less returns whether r is "before" other. It is before if r.From is -// before other.From, or if they're equal, the shorter range is -// before. +// less reports whether r is "before" other. It is before if r.From() +// is before other.From(). If they're equal, then the larger range +// (higher To()) comes first. func (r IPRange) less(other IPRange) bool { - if cmp := r.From.Compare(other.From); cmp != 0 { + if cmp := r.from.Compare(other.from); cmp != 0 { return cmp < 0 } - return r.To.Less(other.To) + return other.to.Less(r.to) } // entirelyBefore returns whether r lies entirely before other in IP // space. func (r IPRange) entirelyBefore(other IPRange) bool { - return r.To.Less(other.From) + return r.to.Less(other.from) } // entirelyWithin returns whether r is entirely contained within // other. func (r IPRange) coveredBy(other IPRange) bool { - return other.From.lessOrEq(r.From) && r.To.lessOrEq(other.To) + return other.from.lessOrEq(r.from) && r.to.lessOrEq(other.to) } // inMiddleOf returns whether r is inside other, but not touching the // edges of other. func (r IPRange) inMiddleOf(other IPRange) bool { - return other.From.Less(r.From) && r.To.Less(other.To) + return other.from.Less(r.from) && r.to.Less(other.to) } // overlapsStartOf returns whether r entirely overlaps the start of // other, but not all of other. func (r IPRange) overlapsStartOf(other IPRange) bool { - return r.From.lessOrEq(other.From) && r.To.Less(other.To) + return r.from.lessOrEq(other.from) && r.to.Less(other.to) } // overlapsEndOf returns whether r entirely overlaps the end of // other, but not all of other. func (r IPRange) overlapsEndOf(other IPRange) bool { - return other.From.Less(r.From) && other.To.lessOrEq(r.To) + return other.from.Less(r.from) && other.to.lessOrEq(r.to) } // mergeIPRanges returns the minimum and sorted set of IP ranges that @@ -1356,31 +1797,31 @@ func mergeIPRanges(rr []IPRange) (out []IPRange, valid bool) { for _, r := range rr[1:] { prev := &out[len(out)-1] switch { - case !r.Valid(): + case !r.IsValid(): // Invalid ranges make no sense to merge, refuse to // perform. return nil, false - case prev.To.Next() == r.From: + case prev.to.Next() == r.from: // prev and r touch, merge them. // // prev r // f------tf-----t - prev.To = r.To - case prev.To.Less(r.From): + prev.to = r.to + case prev.to.Less(r.from): // No overlap and not adjacent (per previous case), no // merging possible. // // prev r // f------t f-----t out = append(out, r) - case prev.To.Less(r.To): + case prev.to.Less(r.to): // Partial overlap, update prev // // prev // f------t // f-----t // r - prev.To = r.To + prev.to = r.to default: // r entirely contained in prev, nothing to do. // @@ -1398,10 +1839,10 @@ func mergeIPRanges(rr []IPRange) (out []IPRange, valid bool) { // If p and o are of different address families or either are invalid, // it reports false. func (r IPRange) Overlaps(o IPRange) bool { - return r.Valid() && - o.Valid() && - r.From.Compare(o.To) <= 0 && - o.From.Compare(r.To) <= 0 + return r.IsValid() && + o.IsValid() && + r.from.Compare(o.to) <= 0 && + o.from.Compare(r.to) <= 0 } // prefixMaker returns a address-family-corrected IPPrefix from a and bits, @@ -1422,15 +1863,15 @@ func (r IPRange) Prefixes() []IPPrefix { // AppendPrefixes is an append version of IPRange.Prefixes. It appends // the IPPrefix entries that cover r to dst. func (r IPRange) AppendPrefixes(dst []IPPrefix) []IPPrefix { - if !r.Valid() { + if !r.IsValid() { return nil } - return appendRangePrefixes(dst, r.prefixFrom128AndBits, r.From.addr, r.To.addr) + return appendRangePrefixes(dst, r.prefixFrom128AndBits, r.from.addr, r.to.addr) } func (r IPRange) prefixFrom128AndBits(a uint128, bits uint8) IPPrefix { - ip := IP{addr: a, z: r.From.z} - if r.From.Is4() { + ip := IP{addr: a, z: r.from.z} + if r.from.Is4() { bits -= 12 * 8 } return IPPrefix{ip, bits} @@ -1455,11 +1896,11 @@ func comparePrefixes(a, b uint128) (common uint8, aZeroBSet bool) { // Prefix returns r as an IPPrefix, if it can be presented exactly as such. // If r is not valid or is not exactly equal to one prefix, ok is false. func (r IPRange) Prefix() (p IPPrefix, ok bool) { - if !r.Valid() { + if !r.IsValid() { return } - if common, ok := comparePrefixes(r.From.addr, r.To.addr); ok { - return r.prefixFrom128AndBits(r.From.addr, common), true + if common, ok := comparePrefixes(r.from.addr, r.to.addr); ok { + return r.prefixFrom128AndBits(r.from.addr, common), true } return } @@ -1476,35 +1917,3 @@ func appendRangePrefixes(dst []IPPrefix, makePrefix prefixMaker, a, b uint128) [ dst = appendRangePrefixes(dst, makePrefix, b.bitsClearedFrom(common+1), b) return dst } - -// Next returns the IP following ip. -// If there is none, it returns the IP zero value. -func (ip IP) Next() IP { - ip.addr = ip.addr.addOne() - if ip.Is4() { - if uint32(ip.addr.lo) == 0 { - // Overflowed. - return IP{} - } - } else { - if ip.addr.isZero() { - // Overflowed - return IP{} - } - } - return ip -} - -// Prior returns the IP before ip. -// If there is none, it returns the IP zero value. -func (ip IP) Prior() IP { - if ip.Is4() { - if uint32(ip.addr.lo) == 0 { - return IP{} - } - } else if ip.addr.isZero() { - return IP{} - } - ip.addr = ip.addr.subOne() - return ip -} diff --git a/netaddr_test.go b/netaddr_test.go index ad5fd3a..f01a95d 100644 --- a/netaddr_test.go +++ b/netaddr_test.go @@ -7,6 +7,8 @@ package netaddr import ( + "bytes" + "encoding" "encoding/json" "flag" "fmt" @@ -195,6 +197,9 @@ func TestParseIP(t *testing.T) { t.Errorf("ParseIP(%q).String() got %q, want %q", test.in, s, wants) } + // Check that AppendTo matches MarshalText. + testAppendToMarshal(t, got) + // Check that MarshalText/UnmarshalText work similarly to // ParseIP/String (see TestIPMarshalUnmarshal for // marshal-specific behavior that's not common with @@ -310,6 +315,68 @@ func TestParseIP(t *testing.T) { } } +func TestIPv4Constructors(t *testing.T) { + ips := []IP{ + IPv4(1, 2, 3, 4), + IPFrom4([4]byte{1, 2, 3, 4}), + MustParseIP("1.2.3.4"), + } + for i := range ips { + for j := i + 1; j < len(ips); j++ { + if ips[i] != ips[j] { + t.Errorf("%v != %v", ips[i], ips[j]) + } + } + } +} + +func TestIPMarshalUnmarshalBinary(t *testing.T) { + tests := []struct { + ip string + wantSize int + }{ + {"", 0}, // zero IP + {"1.2.3.4", 4}, + {"fd7a:115c:a1e0:ab12:4843:cd96:626b:430b", 16}, + {"::ffff:c000:0280", 16}, + {"::ffff:c000:0280%eth0", 20}, + } + for _, tc := range tests { + var ip IP + if len(tc.ip) > 0 { + ip = mustIP(tc.ip) + } + b, err := ip.MarshalBinary() + if err != nil { + t.Fatal(err) + } + if len(b) != tc.wantSize { + t.Fatalf("%q encoded to size %d; want %d", tc.ip, len(b), tc.wantSize) + } + var ip2 IP + if err := ip2.UnmarshalBinary(b); err != nil { + t.Fatal(err) + } + if ip != ip2 { + t.Fatalf("got %v; want %v", ip2, ip) + } + } + + // Cannot unmarshal into a non-zero IP + ip1 := MustParseIP("1.2.3.4") + if err := ip1.UnmarshalBinary([]byte{1, 1, 1, 1}); err == nil { + t.Fatal("unmarshaled into non-empty IP") + } + + // Cannot unmarshal from unexpected IP length. + for _, l := range []int{3, 5} { + var ip2 IP + if err := ip2.UnmarshalBinary(bytes.Repeat([]byte{1}, l)); err == nil { + t.Fatalf("unmarshaled from unexpected IP length %d", l) + } + } +} + func TestIPMarshalUnmarshal(t *testing.T) { // This only tests the cases where Marshal/Unmarshal diverges from // the behavior of ParseIP/String. For the rest of the test cases, @@ -494,9 +561,10 @@ func TestIPProperties(t *testing.T) { var ( nilIP IP - unicast4 = mustIP("192.0.2.1") - unicast6 = mustIP("2001:db8::1") - unicastZone6 = mustIP("2001:db8::1%eth0") + unicast4 = mustIP("192.0.2.1") + unicast6 = mustIP("2001:db8::1") + unicastZone6 = mustIP("2001:db8::1%eth0") + unicast6Unassigned = mustIP("4000::1") // not in 2000::/3. multicast4 = mustIP("224.0.0.1") multicast6 = mustIP("ff02::1") @@ -504,6 +572,7 @@ func TestIPProperties(t *testing.T) { llu4 = mustIP("169.254.0.1") llu6 = mustIP("fe80::1") + llu6Last = mustIP("febf:ffff:ffff:ffff:ffff:ffff:ffff:ffff") lluZone6 = mustIP("fe80::1%eth0") loopback4 = mustIP("127.0.0.1") @@ -511,50 +580,69 @@ func TestIPProperties(t *testing.T) { ilm6 = mustIP("ff01::1") ilmZone6 = mustIP("ff01::1%eth0") + + private4a = mustIP("10.0.0.1") + private4b = mustIP("172.16.0.1") + private4c = mustIP("192.168.1.1") + private6 = mustIP("fd00::1") + + unspecified4 = IPv4(0, 0, 0, 0) + unspecified6 = IPv6Unspecified() ) tests := []struct { name string ip IP - multicast bool + globalUnicast bool interfaceLocalMulticast bool linkLocalMulticast bool linkLocalUnicast bool loopback bool + multicast bool + private bool + unspecified bool }{ { name: "nil", ip: nilIP, }, { - name: "unicast v4Addr", - ip: unicast4, + name: "unicast v4Addr", + ip: unicast4, + globalUnicast: true, }, { - name: "unicast v6Addr", - ip: unicast6, + name: "unicast v6Addr", + ip: unicast6, + globalUnicast: true, }, { - name: "unicast v6AddrZone", - ip: unicastZone6, + name: "unicast v6AddrZone", + ip: unicastZone6, + globalUnicast: true, + }, + { + name: "unicast v6Addr unassigned", + ip: unicast6Unassigned, + globalUnicast: true, }, { name: "multicast v4Addr", ip: multicast4, - multicast: true, linkLocalMulticast: true, + multicast: true, }, { name: "multicast v6Addr", ip: multicast6, - multicast: true, linkLocalMulticast: true, + multicast: true, }, { name: "multicast v6AddrZone", ip: multicastZone6, - multicast: true, linkLocalMulticast: true, + multicast: true, }, { name: "link-local unicast v4Addr", @@ -566,6 +654,11 @@ func TestIPProperties(t *testing.T) { ip: llu6, linkLocalUnicast: true, }, + { + name: "link-local unicast v6Addr upper bound", + ip: llu6Last, + linkLocalUnicast: true, + }, { name: "link-local unicast v6AddrZone", ip: lluZone6, @@ -584,22 +677,61 @@ func TestIPProperties(t *testing.T) { { name: "interface-local multicast v6Addr", ip: ilm6, - multicast: true, interfaceLocalMulticast: true, + multicast: true, }, { name: "interface-local multicast v6AddrZone", ip: ilmZone6, - multicast: true, interfaceLocalMulticast: true, + multicast: true, + }, + { + name: "private v4Addr 10/8", + ip: private4a, + globalUnicast: true, + private: true, + }, + { + name: "private v4Addr 172.16/12", + ip: private4b, + globalUnicast: true, + private: true, + }, + { + name: "private v4Addr 192.168/16", + ip: private4c, + globalUnicast: true, + private: true, + }, + { + name: "private v6Addr", + ip: private6, + globalUnicast: true, + private: true, + }, + { + name: "unspecified v4Addr", + ip: unspecified4, + unspecified: true, + }, + { + name: "unspecified v6Addr", + ip: unspecified6, + unspecified: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - multicast := tt.ip.IsMulticast() - if multicast != tt.multicast { - t.Errorf("IsMulticast(%v) = %v; want %v", tt.ip, multicast, tt.multicast) + gu := tt.ip.IsGlobalUnicast() + if gu != tt.globalUnicast { + t.Errorf("IsGlobalUnicast(%v) = %v; want %v", tt.ip, gu, tt.globalUnicast) + } + + ilm := tt.ip.IsInterfaceLocalMulticast() + if ilm != tt.interfaceLocalMulticast { + t.Errorf("IsInterfaceLocalMulticast(%v) = %v; want %v", tt.ip, ilm, tt.interfaceLocalMulticast) } llu := tt.ip.IsLinkLocalUnicast() @@ -607,19 +739,29 @@ func TestIPProperties(t *testing.T) { t.Errorf("IsLinkLocalUnicast(%v) = %v; want %v", tt.ip, llu, tt.linkLocalUnicast) } + llm := tt.ip.IsLinkLocalMulticast() + if llm != tt.linkLocalMulticast { + t.Errorf("IsLinkLocalMulticast(%v) = %v; want %v", tt.ip, llm, tt.linkLocalMulticast) + } + lo := tt.ip.IsLoopback() if lo != tt.loopback { t.Errorf("IsLoopback(%v) = %v; want %v", tt.ip, lo, tt.loopback) } - ilm := tt.ip.IsInterfaceLocalMulticast() - if ilm != tt.interfaceLocalMulticast { - t.Errorf("IsInterfaceLocalMulticast(%v) = %v; want %v", tt.ip, ilm, tt.interfaceLocalMulticast) + multicast := tt.ip.IsMulticast() + if multicast != tt.multicast { + t.Errorf("IsMulticast(%v) = %v; want %v", tt.ip, multicast, tt.multicast) } - llm := tt.ip.IsLinkLocalMulticast() - if llm != tt.linkLocalMulticast { - t.Errorf("IsLinkLocalMulticast(%v) = %v; want %v", tt.ip, llm, tt.linkLocalMulticast) + private := tt.ip.IsPrivate() + if private != tt.private { + t.Errorf("IsPrivate(%v) = %v; want %v", tt.ip, private, tt.private) + } + + unspecified := tt.ip.IsUnspecified() + if unspecified != tt.unspecified { + t.Errorf("IsUnspecified(%v) = %v; want %v", tt.ip, unspecified, tt.unspecified) } }) } @@ -719,12 +861,47 @@ func TestLessCompare(t *testing.T) { } sort.Slice(values, func(i, j int) bool { return values[i].Less(values[j]) }) got := fmt.Sprintf("%s", values) - want := `[invalid IP 1.2.3.4 8.8.8.8 ::1 ::1%foo ::2]` + want := `[zero IP 1.2.3.4 8.8.8.8 ::1 ::1%foo ::2]` if got != want { t.Errorf("unexpected sort\n got: %s\nwant: %s\n", got, want) } } +func TestIPStringExpanded(t *testing.T) { + tests := []struct { + ip IP + s string + }{ + { + ip: IP{}, + s: "zero IP", + }, + { + ip: mustIP("192.0.2.1"), + s: "192.0.2.1", + }, + { + ip: mustIP("2001:db8::1"), + s: "2001:0db8:0000:0000:0000:0000:0000:0001", + }, + { + ip: mustIP("2001:db8::1%eth0"), + s: "2001:0db8:0000:0000:0000:0000:0000:0001%eth0", + }, + } + + for _, tt := range tests { + t.Run(tt.ip.String(), func(t *testing.T) { + want := tt.s + got := tt.ip.StringExpanded() + + if got != want { + t.Fatalf("got %s, want %s", got, want) + } + }) + } +} + func TestIPPrefixMasking(t *testing.T) { type subtest struct { ip IP @@ -1034,7 +1211,6 @@ func TestIPPrefixMarshalUnmarshal(t *testing.T) { "0.0.0.0/0", "::/0", "::1/128", - "fe80::1cc0:3e8c:119f:c2e1%ens18/128", "::ffff:c000:1234/128", "2001:db8::/32", } @@ -1063,6 +1239,26 @@ func TestIPPrefixMarshalUnmarshal(t *testing.T) { } } +func TestIPPrefixMarshalUnmarshalZone(t *testing.T) { + orig := `"fe80::1cc0:3e8c:119f:c2e1%ens18/128"` + unzoned := `"fe80::1cc0:3e8c:119f:c2e1/128"` + + var p IPPrefix + if err := json.Unmarshal([]byte(orig), &p); err != nil { + t.Fatalf("failed to unmarshal: %v", err) + } + + pb, err := json.Marshal(p) + if err != nil { + t.Fatalf("failed to marshal: %v", err) + } + + back := string(pb) + if back != unzoned { + t.Errorf("Marshal = %q; want %q", back, unzoned) + } +} + func TestIPPrefixUnmarshalTextNonZero(t *testing.T) { ip := mustIPPrefix("fe80::/64") if err := ip.UnmarshalText([]byte("xxx")); err == nil { @@ -1142,11 +1338,11 @@ func TestIPPrefixMasked(t *testing.T) { masked: mustIPPrefix("2000::/3"), }, { - prefix: IPPrefix{IP: mustIP("2000::"), Bits: 129}, + prefix: IPPrefixFrom(mustIP("2000::"), 129), masked: IPPrefix{}, }, { - prefix: IPPrefix{IP: mustIP("1.2.3.4"), Bits: 33}, + prefix: IPPrefixFrom(mustIP("1.2.3.4"), 33), masked: IPPrefix{}, }, } @@ -1166,6 +1362,7 @@ func TestIPPrefix(t *testing.T) { ip IP bits uint8 ipNet *net.IPNet + str string contains []IP notContains []IP }{ @@ -1246,6 +1443,18 @@ func TestIPPrefix(t *testing.T) { contains: mustIPs("2001:db8::1"), notContains: mustIPs("fe80::1"), }, + { + prefix: "::%0/00/80", + ip: mustIP("::"), + bits: 80, + str: "::/80", + ipNet: &net.IPNet{ + IP: net.ParseIP("::"), // net.IPNet drops zones + Mask: net.CIDRMask(80, 128), + }, + contains: mustIPs("::"), + notContains: mustIPs("ff::%0/00", "ff::%1/23", "::%0/00", "::%1/23"), + }, } for _, test := range tests { t.Run(test.prefix, func(t *testing.T) { @@ -1253,11 +1462,11 @@ func TestIPPrefix(t *testing.T) { if err != nil { t.Fatal(err) } - if prefix.IP != test.ip { - t.Errorf("IP=%s, want %s", prefix.IP, test.ip) + if prefix.IP() != test.ip { + t.Errorf("IP=%s, want %s", prefix.IP(), test.ip) } - if prefix.Bits != test.bits { - t.Errorf("bits=%d, want %d", prefix.Bits, test.bits) + if prefix.Bits() != test.bits { + t.Errorf("bits=%d, want %d", prefix.Bits(), test.bits) } stdIPNet := prefix.IPNet() if !test.ipNet.IP.Equal(stdIPNet.IP) || !reflect.DeepEqual(stdIPNet.Mask, test.ipNet.Mask) { @@ -1273,9 +1482,15 @@ func TestIPPrefix(t *testing.T) { t.Errorf("contains %s", ip) } } - if got := prefix.String(); got != test.prefix { - t.Errorf("prefix.String()=%q, want %q", got, test.prefix) + want := test.str + if want == "" { + want = test.prefix + } + if got := prefix.String(); got != want { + t.Errorf("prefix.String()=%q, want %q", got, want) } + + testAppendToMarshal(t, prefix) }) } } @@ -1299,9 +1514,9 @@ func TestIPPrefixValid(t *testing.T) { {IPPrefix{IP{}, 128}, false}, } for _, tt := range tests { - got := tt.ipp.Valid() + got := tt.ipp.IsValid() if got != tt.want { - t.Errorf("(%v).Valid() = %v want %v", tt.ipp, got, tt.want) + t.Errorf("(%v).IsValid() = %v want %v", tt.ipp, got, tt.want) } } } @@ -1526,7 +1741,14 @@ func TestParseIPPort(t *testing.T) { } }) - // TextMarshal and TextUnmarhsal mostly behave like + t.Run(test.in+"/AppendTo", func(t *testing.T) { + got, err := ParseIPPort(test.in) + if err == nil { + testAppendToMarshal(t, got) + } + }) + + // TextMarshal and TextUnmarshal mostly behave like // ParseIPPort and String. Divergent behavior are handled in // TestIPPortMarshalUnmarshal. t.Run(test.in+"/Marshal", func(t *testing.T) { @@ -1579,10 +1801,32 @@ func TestIPPortMarshalUnmarshal(t *testing.T) { if orig != back { t.Errorf("Marshal = %q; want %q", back, orig) } + + testAppendToMarshal(t, ipp) }) } } +type appendMarshaler interface { + encoding.TextMarshaler + AppendTo([]byte) []byte +} + +// testAppendToMarshal tests that x's AppendTo and MarshalText methods yield the same results. +// x's MarshalText method must not return an error. +func testAppendToMarshal(t *testing.T, x appendMarshaler) { + t.Helper() + m, err := x.MarshalText() + if err != nil { + t.Fatalf("(%v).MarshalText: %v", x, err) + } + a := make([]byte, 0, len(m)) + a = x.AppendTo(a) + if !bytes.Equal(m, a) { + t.Errorf("(%v).MarshalText = %q, (%v).AppendTo = %q", x, m, x, a) + } +} + func TestUDPAddrAllocs(t *testing.T) { for _, ep := range []string{"1.2.3.4:1234", "[::1]:1234"} { ipp, err := ParseIPPort(ep) @@ -1592,7 +1836,7 @@ func TestUDPAddrAllocs(t *testing.T) { ua := &net.UDPAddr{IP: make(net.IP, 0, 16)} n := int(testing.AllocsPerRun(1000, func() { ua := ipp.UDPAddrAt(ua) - if ua.Port != int(ipp.Port) { + if ua.Port != int(ipp.Port()) { t.Fatal("UDPAddr returned bogus result") } })) @@ -1615,6 +1859,33 @@ func mustIPs(strs ...string) []IP { return res } +func BenchmarkBinaryMarshalRoundTrip(b *testing.B) { + b.ReportAllocs() + tests := []struct { + name string + ip string + }{ + {"ipv4", "1.2.3.4"}, + {"ipv6", "2001:db8::1"}, + {"ipv6+zone", "2001:db8::1%eth0"}, + } + for _, tc := range tests { + b.Run(tc.name, func(b *testing.B) { + ip := mustIP(tc.ip) + for i := 0; i < b.N; i++ { + bt, err := ip.MarshalBinary() + if err != nil { + b.Fatal(err) + } + var ip2 IP + if err := ip2.UnmarshalBinary(bt); err != nil { + b.Fatal(err) + } + } + }) + } +} + func BenchmarkStdIPv4(b *testing.B) { b.ReportAllocs() ips := []net.IP{} @@ -1694,10 +1965,7 @@ func BenchmarkIPv6(b *testing.B) { func BenchmarkIPv4Contains(b *testing.B) { b.ReportAllocs() - prefix := IPPrefix{ - IP: IPv4(192, 168, 1, 0), - Bits: 24, - } + prefix := IPPrefixFrom(IPv4(192, 168, 1, 0), 24) ip := IPv4(192, 168, 1, 1) for i := 0; i < b.N; i++ { prefix.Contains(ip) @@ -1759,6 +2027,52 @@ func BenchmarkIPString(b *testing.B) { } } +func BenchmarkIPStringExpanded(b *testing.B) { + for _, test := range parseBenchInputs { + ip := MustParseIP(test.ip) + b.Run(test.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + sinkString = ip.StringExpanded() + } + }) + } +} + +func BenchmarkIPMarshalText(b *testing.B) { + b.ReportAllocs() + ip := MustParseIP("66.55.44.33") + for i := 0; i < b.N; i++ { + sinkBytes, _ = ip.MarshalText() + } +} + +func BenchmarkIPPortString(b *testing.B) { + for _, test := range parseBenchInputs { + ip := MustParseIP(test.ip) + ipp := IPPortFrom(ip, 60000) + b.Run(test.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + sinkString = ipp.String() + } + }) + } +} + +func BenchmarkIPPortMarshalText(b *testing.B) { + for _, test := range parseBenchInputs { + ip := MustParseIP(test.ip) + ipp := IPPortFrom(ip, 60000) + b.Run(test.name, func(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + sinkBytes, _ = ipp.MarshalText() + } + }) + } +} + func BenchmarkIPPrefixMasking(b *testing.B) { tests := []struct { name string @@ -1823,6 +2137,14 @@ func BenchmarkIPPrefixMasking(b *testing.B) { } } +func BenchmarkIPPrefixMarshalText(b *testing.B) { + b.ReportAllocs() + ipp := MustParseIPPrefix("66.55.44.33/22") + for i := 0; i < b.N; i++ { + sinkBytes, _ = ipp.MarshalText() + } +} + func BenchmarkParseIPPort(b *testing.B) { for _, test := range parseBenchInputs { var ipp string @@ -1943,11 +2265,11 @@ func TestIPPrefixOverlaps(t *testing.T) { {pfx("0100::0/8"), pfx("::1/128"), false}, // v6-mapped v4 should not overlap with IPv4. - {IPPrefix{IP: IPv6Raw(mustIP("1.2.0.0").As16()), Bits: 16}, pfx("1.2.3.0/24"), false}, + {IPPrefixFrom(IPv6Raw(mustIP("1.2.0.0").As16()), 16), pfx("1.2.3.0/24"), false}, // Invalid prefixes - {IPPrefix{IP: mustIP("1.2.3.4"), Bits: 33}, pfx("1.2.3.0/24"), false}, - {IPPrefix{IP: mustIP("2000::"), Bits: 129}, pfx("2000::/64"), false}, + {IPPrefixFrom(mustIP("1.2.3.4"), 33), pfx("1.2.3.0/24"), false}, + {IPPrefixFrom(mustIP("2000::"), 129), pfx("2000::/64"), false}, } for i, tt := range tests { if got := tt.a.Overlaps(tt.b); got != tt.want { @@ -2028,7 +2350,7 @@ func TestRangePrefixes(t *testing.T) { )}, } for _, tt := range tests { - r := IPRange{From: mustIP(tt.from), To: mustIP(tt.to)} + r := IPRangeFrom(mustIP(tt.from), mustIP(tt.to)) got := r.Prefixes() if !reflect.DeepEqual(got, tt.want) { t.Errorf("failed %s->%s. got:", tt.from, tt.to) @@ -2058,6 +2380,8 @@ func TestParseIPRange(t *testing.T) { want interface{} }{ {"", "no hyphen in range \"\""}, + {"foo-", `invalid From IP "foo" in range "foo-"`}, + {"1.2.3.4-foo", `invalid To IP "foo" in range "1.2.3.4-foo"`}, {"1.2.3.4-5.6.7.8", IPRange{mustIP("1.2.3.4"), mustIP("5.6.7.8")}}, {"1.2.3.4-0.1.2.3", "range 1.2.3.4 to 0.1.2.3 not valid"}, {"::1-::5", IPRange{mustIP("::1"), mustIP("::5")}}, @@ -2079,6 +2403,26 @@ func TestParseIPRange(t *testing.T) { t.Errorf("input %q stringifies back as %q", tt.in, back) } } + + var r2 IPRange + err = r2.UnmarshalText([]byte(tt.in)) + if err != nil { + got = err.Error() + } else { + got = r2 + } + if got != tt.want && tt.in != "" { + t.Errorf("UnmarshalText(%q) = %v; want %v", tt.in, got, tt.want) + } + + testAppendToMarshal(t, r) + } +} + +func TestIPRangeUnmarshalTextNonZero(t *testing.T) { + r := MustParseIPRange("1.2.3.4-5.6.7.8") + if err := r.UnmarshalText([]byte("1.2.3.4-5.6.7.8")); err == nil { + t.Fatal("unmarshaled into non-empty IPPrefix") } } @@ -2092,7 +2436,7 @@ func TestIPRangeContains(t *testing.T) { rtests []rtest }{ { - IPRange{From: mustIP("10.0.0.2"), To: mustIP("10.0.0.4")}, + IPRangeFrom(mustIP("10.0.0.2"), mustIP("10.0.0.4")), []rtest{ {mustIP("10.0.0.1"), false}, {mustIP("10.0.0.2"), true}, @@ -2104,10 +2448,11 @@ func TestIPRangeContains(t *testing.T) { }, }, { - IPRange{From: mustIP("::1"), To: mustIP("::ffff")}, + IPRangeFrom(mustIP("::1"), mustIP("::ffff")), []rtest{ {mustIP("::0"), false}, {mustIP("::1"), true}, + {mustIP("::1%z"), false}, {mustIP("::ffff"), true}, {mustIP("1::"), false}, {mustIP("0.0.0.1"), false}, @@ -2115,7 +2460,7 @@ func TestIPRangeContains(t *testing.T) { }, }, { - IPRange{From: mustIP("10.0.0.2"), To: mustIP("::")}, // invalid + IPRangeFrom(mustIP("10.0.0.2"), mustIP("::")), // invalid []rtest{ {mustIP("10.0.0.2"), false}, }, @@ -2205,9 +2550,9 @@ func TestIPRangeValid(t *testing.T) { {IPRange{mustIP("1.2.3.4"), mustIP("::1")}, false}, // family mismatch } for _, tt := range tests { - got := tt.r.Valid() + got := tt.r.IsValid() if got != tt.want { - t.Errorf("range %v to %v Valid = %v; want %v", tt.r.From, tt.r.To, got, tt.want) + t.Errorf("range %v to %v Valid = %v; want %v", tt.r.From(), tt.r.To(), got, tt.want) } } } @@ -2361,6 +2706,9 @@ func TestIPPrefixContains(t *testing.T) { {mustIPPrefix("::1/127"), mustIP("::2"), false}, {mustIPPrefix("::1/128"), mustIP("::1"), true}, {mustIPPrefix("::1/127"), mustIP("::2"), false}, + // zones support + {mustIPPrefix("::1%a/128"), mustIP("::1"), true}, // prefix zones are stripped... + {mustIPPrefix("::1%a/128"), mustIP("::1%a"), false}, // but ip zones are not // invalid IP {mustIPPrefix("::1/0"), IP{}, false}, {mustIPPrefix("1.2.3.4/0"), IP{}, false}, @@ -2413,6 +2761,7 @@ var ( sinkIP4 [4]byte sinkBool bool sinkString string + sinkBytes []byte sinkUDPAddr = &net.UDPAddr{IP: make(net.IP, 0, 16)} ) @@ -2455,12 +2804,6 @@ func TestNoAllocs(t *testing.T) { } return ipp } - panicIPR := func(ipr IPRange, err error) IPRange { - if err != nil { - panic(err) - } - return ipr - } test := func(name string, f func()) { t.Run(name, func(t *testing.T) { @@ -2473,6 +2816,7 @@ func TestNoAllocs(t *testing.T) { // IP constructors test("IPv4", func() { sinkIP = IPv4(1, 2, 3, 4) }) + test("IPFrom4", func() { sinkIP = IPFrom4([4]byte{1, 2, 3, 4}) }) test("IPv6", func() { sinkIP = IPv6Raw([16]byte{}) }) test("IPFrom16", func() { sinkIP = IPFrom16([16]byte{15: 1}) }) test("ParseIP/4", func() { sinkIP = panicIP(ParseIP("1.2.3.4")) }) @@ -2504,11 +2848,14 @@ func TestNoAllocs(t *testing.T) { test("IP.Is4in6", func() { sinkBool = MustParseIP("fe80::1").Is4in6() }) test("IP.Unmap", func() { sinkIP = MustParseIP("ffff::2.3.4.5").Unmap() }) test("IP.WithZone", func() { sinkIP = MustParseIP("fe80::1").WithZone("") }) + test("IP.IsGlobalUnicast", func() { sinkBool = MustParseIP("2001:db8::1").IsGlobalUnicast() }) + test("IP.IsInterfaceLocalMulticast", func() { sinkBool = MustParseIP("fe80::1").IsInterfaceLocalMulticast() }) + test("IP.IsLinkLocalMulticast", func() { sinkBool = MustParseIP("fe80::1").IsLinkLocalMulticast() }) test("IP.IsLinkLocalUnicast", func() { sinkBool = MustParseIP("fe80::1").IsLinkLocalUnicast() }) test("IP.IsLoopback", func() { sinkBool = MustParseIP("fe80::1").IsLoopback() }) test("IP.IsMulticast", func() { sinkBool = MustParseIP("fe80::1").IsMulticast() }) - test("IP.IsInterfaceLocalMulticast", func() { sinkBool = MustParseIP("fe80::1").IsInterfaceLocalMulticast() }) - test("IP.IsLinkLocalMulticast", func() { sinkBool = MustParseIP("fe80::1").IsLinkLocalMulticast() }) + test("IP.IsPrivate", func() { sinkBool = MustParseIP("fd00::1").IsPrivate() }) + test("IP.IsUnspecified", func() { sinkBool = IPv6Unspecified().IsUnspecified() }) test("IP.Prefix/4", func() { sinkIPPrefix = panicPfx(MustParseIP("1.2.3.4").Prefix(20)) }) test("IP.Prefix/6", func() { sinkIPPrefix = panicPfx(MustParseIP("fe80::1").Prefix(64)) }) test("IP.As16", func() { sinkIP16 = MustParseIP("1.2.3.4").As16() }) @@ -2517,6 +2864,7 @@ func TestNoAllocs(t *testing.T) { test("IP.Prior", func() { sinkIP = MustParseIP("1.2.3.4").Prior() }) // IPPort constructors + test("IPPortFrom", func() { sinkIPPort = IPPortFrom(IPv4(1, 2, 3, 4), 22) }) test("ParseIPPort", func() { sinkIPPort = panicIPP(ParseIPPort("[::1]:1234")) }) test("MustParseIPPort", func() { sinkIPPort = MustParseIPPort("[::1]:1234") }) test("FromStdAddr", func() { @@ -2528,6 +2876,7 @@ func TestNoAllocs(t *testing.T) { test("UDPAddrAt", func() { sinkUDPAddr = MustParseIPPort("1.2.3.4:1234").UDPAddrAt(sinkUDPAddr) }) // IPPrefix constructors + test("IPPrefixFrom", func() { sinkIPPrefix = IPPrefixFrom(IPv4(1, 2, 3, 4), 32) }) test("ParseIPPrefix/4", func() { sinkIPPrefix = panicPfx(ParseIPPrefix("1.2.3.4/20")) }) test("ParseIPPrefix/6", func() { sinkIPPrefix = panicPfx(ParseIPPrefix("fe80::1/64")) }) test("MustParseIPPrefix", func() { sinkIPPrefix = MustParseIPPrefix("1.2.3.4/20") }) @@ -2551,17 +2900,88 @@ func TestNoAllocs(t *testing.T) { test("IPPRefix.Range", func() { sinkIPRange = MustParseIPPrefix("1.2.3.4/16").Range() }) // IPRange constructors - test("ParseIPRange", func() { sinkIPRange = panicIPR(ParseIPRange("1.2.3.0-1.2.4.150")) }) + test("IPRangeFrom", func() { sinkIPRange = IPRangeFrom(IPv4(1, 2, 3, 4), IPv4(4, 3, 2, 1)) }) + test("ParseIPRange", func() { sinkIPRange = MustParseIPRange("1.2.3.0-1.2.4.150") }) // IPRange methods - test("IPRange.Valid", func() { sinkBool = panicIPR(ParseIPRange("1.2.3.0-1.2.4.150")).Valid() }) + test("IPRange.IsZero", func() { sinkBool = MustParseIPRange("1.2.3.0-1.2.4.150").IsZero() }) + test("IPRange.IsValid", func() { sinkBool = MustParseIPRange("1.2.3.0-1.2.4.150").IsValid() }) test("IPRange.Overlaps", func() { - a := panicIPR(ParseIPRange("1.2.3.0-1.2.3.150")) - b := panicIPR(ParseIPRange("1.2.4.0-1.2.4.255")) + a := MustParseIPRange("1.2.3.0-1.2.3.150") + b := MustParseIPRange("1.2.4.0-1.2.4.255") sinkBool = a.Overlaps(b) }) test("IPRange.Prefix", func() { - a := panicIPR(ParseIPRange("1.2.3.0-1.2.3.255")) + a := MustParseIPRange("1.2.3.0-1.2.3.255") sinkIPPrefix = panicPfxOK(a.Prefix()) }) } + +func TestIPPrefixString(t *testing.T) { + tests := []struct { + ipp IPPrefix + want string + }{ + {IPPrefix{}, "zero IPPrefix"}, + {IPPrefixFrom(IP{}, 8), "invalid IPPrefix"}, + {IPPrefixFrom(MustParseIP("1.2.3.4"), 88), "invalid IPPrefix"}, + } + + for _, tt := range tests { + if got := tt.ipp.String(); got != tt.want { + t.Errorf("(%#v).String() = %q want %q", tt.ipp, got, tt.want) + } + } +} + +func TestInvalidIPPortString(t *testing.T) { + tests := []struct { + ipp IPPort + want string + }{ + {IPPort{}, "invalid IPPort"}, + {IPPortFrom(IP{}, 80), "invalid IPPort"}, + } + + for _, tt := range tests { + if got := tt.ipp.String(); got != tt.want { + t.Errorf("(%#v).String() = %q want %q", tt.ipp, got, tt.want) + } + } +} + +func TestMethodParity(t *testing.T) { + // Collect all method names for each type. + methods := make(map[string][]reflect.Type) + allTypes := []reflect.Type{ + reflect.TypeOf((*IP)(nil)), + reflect.TypeOf((*IPPort)(nil)), + reflect.TypeOf((*IPPrefix)(nil)), + reflect.TypeOf((*IPRange)(nil)), + } + for _, typ := range allTypes { + for i := 0; i < typ.NumMethod(); i++ { + name := typ.Method(i).Name + methods[name] = append(methods[name], typ) + } + } + + // Check whether sufficiently common methods exist on all types. + ignoreList := map[string]string{ + "Valid": "method is deprecated", + } + for name, types := range methods { + if _, ignore := ignoreList[name]; ignore { + continue // method is ignored for parity check + } + if !(len(allTypes)/2 < len(types) && len(types) < len(allTypes)) { + continue // either too unique or all types already have that method + } + for _, typ := range allTypes { + if _, ok := typ.MethodByName(name); ok { + continue // this type already has this method + } + t.Errorf("%v.%v is missing", typ.Elem().Name(), name) + } + } +} diff --git a/stackerr_test.go b/stackerr_test.go new file mode 100644 index 0000000..f45a746 --- /dev/null +++ b/stackerr_test.go @@ -0,0 +1,31 @@ +// Copyright 2021 The Inet.Af AUTHORS. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package netaddr_test + +import ( + "strings" + "testing" + + "inet.af/netaddr" +) + +// The tests for stacktrace errors is in its own file, +// so that the line number munging that we do doesn't +// break line numbers for other tests. + +func TestStacktraceErr(t *testing.T) { + b := new(netaddr.IPSetBuilder) +//line ipp.go:1 + b.AddPrefix(netaddr.IPPrefixFrom(netaddr.IPv4(1, 2, 3, 4), 33)) +//line r.go:2 + b.AddRange(netaddr.IPRange{}) + _, err := b.IPSet() + got := err.Error() + for _, want := range []string{"ipp.go:1", "r.go:2", "33"} { + if !strings.Contains(got, want) { + t.Errorf("error should contain %q, got %q", want, got) + } + } +} diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 0000000..3a95c7f --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,8 @@ +//go:build tools +// +build tools + +package tools + +import ( + _ "github.com/dvyukov/go-fuzz/go-fuzz-build" +)