// Copyright 2020 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. //lint:file-ignore U1000 allow unused code in tests for experiments. package netipx import ( "bytes" "encoding" "flag" "net" "net/netip" "reflect" "testing" ) type ( IPPrefix = netip.Prefix IP = netip.Addr IPPort = netip.AddrPort ) // IPv4 returns the IP of the IPv4 address a.b.c.d. func IPv4(a, b, c, d uint8) IP { return netip.AddrFrom4([4]byte{a, b, c, d}) } var long = flag.Bool("long", false, "run long tests") func TestFromStdIP(t *testing.T) { mustFromStdIPMustPanic := func(std net.IP) (IP, bool) { defer func() { if r := recover(); r == nil { t.Errorf("expected MustFromStdIP(%#v) to panic", std) } }() return MustFromStdIP(std), true } tests := []struct { name string fn func(net.IP) (IP, bool) std net.IP want IP }{ { name: "v4", fn: FromStdIP, std: []byte{1, 2, 3, 4}, want: IPv4(1, 2, 3, 4), }, { name: "v6", fn: FromStdIP, std: net.ParseIP("::1"), want: netip.AddrFrom16([...]byte{15: 1}), }, { name: "4in6-unmap", fn: FromStdIP, std: net.ParseIP("1.2.3.4"), want: IPv4(1, 2, 3, 4), }, { name: "v4-raw", fn: FromStdIPRaw, std: net.ParseIP("1.2.3.4").To4(), want: IPv4(1, 2, 3, 4), }, { name: "4in6-raw", fn: FromStdIPRaw, std: net.ParseIP("1.2.3.4"), want: netip.AddrFrom16([...]byte{10: 0xff, 11: 0xff, 12: 1, 13: 2, 14: 3, 15: 4}), }, { name: "bad-raw", fn: FromStdIPRaw, std: net.IP{0xff}, }, { name: "bad-must-panic", fn: mustFromStdIPMustPanic, std: net.IP{0x00, 0x01, 0x02}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := tt.fn(tt.std) if got != tt.want { t.Errorf("got (%#v, %v); want %#v", got, ok, tt.want) } }) } } func TestFromStdAddr(t *testing.T) { tests := []struct { name string std net.IP port int zone string want netip.AddrPort wantOK bool }{ { name: "invalid IP", std: net.IP{0xff}, }, { name: "invalid port", std: net.IP{1, 2, 3, 4}, port: -1, }, { name: "v4", std: net.IP{1, 2, 3, 4}, port: 8080, want: netip.AddrPortFrom(netip.AddrFrom4([...]byte{1, 2, 3, 4}), 8080), wantOK: true, }, { name: "v4 with zone", std: net.IP{1, 2, 3, 4}, port: 8080, zone: "foobar", }, { name: "v6", std: net.ParseIP("fc::"), port: 8080, want: netip.AddrPortFrom(netip.MustParseAddr("fc::"), 8080), wantOK: true, }, { name: "v6 with zone", std: net.ParseIP("fc::"), port: 8080, zone: "foobar", want: netip.AddrPortFrom(netip.MustParseAddr("fc::").WithZone("foobar"), 8080), wantOK: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := FromStdAddr(tt.std, tt.port, tt.zone) if got != tt.want || ok != tt.wantOK { t.Errorf("FromStdAddr(%#v, %d, %q): got (%#v, %v); want (%#v, %t)", tt.std, tt.port, tt.zone, got, ok, tt.want, tt.wantOK) } }) } } func TestFromStdIPNet(t *testing.T) { tests := []struct { name string std *net.IPNet want IPPrefix }{ { name: "invalid IP", std: &net.IPNet{ IP: net.IP{0xff}, }, }, { name: "invalid mask", std: &net.IPNet{ IP: net.IPv6loopback, Mask: nil, }, }, { name: "non-contiguous mask", std: &net.IPNet{ IP: net.IPv4(192, 0, 2, 0).To4(), Mask: net.IPv4Mask(255, 0, 255, 0), }, }, { name: "IPv4", std: &net.IPNet{ IP: net.IPv4(192, 0, 2, 0).To4(), Mask: net.CIDRMask(24, 32), }, want: mustIPPrefix("192.0.2.0/24"), }, { name: "IPv6", std: &net.IPNet{ IP: net.ParseIP("2001:db8::"), Mask: net.CIDRMask(64, 128), }, want: mustIPPrefix("2001:db8::/64"), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, ok := FromStdIPNet(tt.std) if !ok && got != (IPPrefix{}) { t.Fatalf("!ok but non-zero result") } if got != tt.want { t.Errorf("FromStdIPNet(%q) = %+v; want %+v", tt.std, got, tt.want) } }) } } func TestAddrIPNet(t *testing.T) { tests := []struct { name string addr netip.Addr want *net.IPNet }{ { name: "invalid IP", addr: netip.Addr{}, want: &net.IPNet{}, }, { name: "IPv4", addr: mustIP("127.0.0.1"), want: &net.IPNet{ IP: net.IPv4(127, 0, 0, 1).To4(), Mask: net.IPv4Mask(255, 255, 255, 255), }, }, { name: "IPv6", addr: mustIP("2001:db8::"), want: &net.IPNet{ IP: net.ParseIP("2001:db8::"), Mask: net.CIDRMask(128, 128), }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := AddrIPNet(tt.addr) if got == nil { t.Fatalf("nil result") } else if !reflect.DeepEqual(got, tt.want) { t.Errorf("AddrIPNet(%q) = %+v; want %+v", tt.addr, got, tt.want) } }) } } 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) } } var ( mustIP = netip.MustParseAddr mustIPPrefix = netip.MustParsePrefix ) func mustIPs(strs ...string) []IP { var res []IP for _, s := range strs { res = append(res, mustIP(s)) } 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{} for i := 0; i < b.N; i++ { ip := net.IPv4(8, 8, 8, 8) ips = ips[:0] for i := 0; i < 100; i++ { ips = append(ips, ip) } } } func BenchmarkIPv4(b *testing.B) { b.ReportAllocs() ips := []IP{} for i := 0; i < b.N; i++ { ip := IPv4(8, 8, 8, 8) ips = ips[:0] for i := 0; i < 100; i++ { ips = append(ips, ip) } } } // ip4i was one of the possible representations of IP that came up in // discussions, inlining IPv4 addresses, but having an "overflow" // interface for IPv6 or IPv6 + zone. This is here for benchmarking. type ip4i struct { ip4 [4]byte flags1 byte flags2 byte flags3 byte flags4 byte ipv6 interface{} } func newip4i_v4(a, b, c, d byte) ip4i { return ip4i{ip4: [4]byte{a, b, c, d}} } // BenchmarkIPv4_inline benchmarks the candidate representation, ip4i. func BenchmarkIPv4_inline(b *testing.B) { b.ReportAllocs() ips := []ip4i{} for i := 0; i < b.N; i++ { ip := newip4i_v4(8, 8, 8, 8) ips = ips[:0] for i := 0; i < 100; i++ { ips = append(ips, ip) } } } func BenchmarkStdIPv6(b *testing.B) { b.ReportAllocs() ips := []net.IP{} for i := 0; i < b.N; i++ { ip := net.ParseIP("2001:db8::1") ips = ips[:0] for i := 0; i < 100; i++ { ips = append(ips, ip) } } } func BenchmarkIPv6(b *testing.B) { b.ReportAllocs() ips := []IP{} for i := 0; i < b.N; i++ { ip := mustIP("2001:db8::1") ips = ips[:0] for i := 0; i < 100; i++ { ips = append(ips, ip) } } } var parseBenchInputs = []struct { name string ip string }{ {"v4", "192.168.1.1"}, {"v6", "fd7a:115c:a1e0:ab12:4843:cd96:626b:430b"}, {"v6_ellipsis", "fd7a:115c::626b:430b"}, {"v6_v4", "::ffff:192.168.140.255"}, {"v6_zone", "1:2::ffff:192.168.140.255%eth1"}, } func pxv(cidrStrs ...string) (out []IPPrefix) { for _, s := range cidrStrs { out = append(out, mustIPPrefix(s)) } return } func TestRangePrefixes(t *testing.T) { tests := []struct { from string to string want []IPPrefix }{ {"0.0.0.0", "255.255.255.255", pxv("0.0.0.0/0")}, {"::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff", pxv("::/0")}, {"10.0.0.0", "10.255.255.255", pxv("10.0.0.0/8")}, {"10.0.0.0", "10.127.255.255", pxv("10.0.0.0/9")}, {"0.0.0.4", "0.0.0.11", pxv( // 4 0100 // 5 0101 // 6 0110 // 7 0111 // 8 1000 // 9 1001 //10 1010 //11 1011 "0.0.0.4/30", "0.0.0.8/30", )}, {"10.0.0.0", "11.10.255.255", pxv( "10.0.0.0/8", "11.0.0.0/13", "11.8.0.0/15", "11.10.0.0/16", )}, {"1.2.3.5", "5.6.7.8", pxv( "1.2.3.5/32", "1.2.3.6/31", "1.2.3.8/29", "1.2.3.16/28", "1.2.3.32/27", "1.2.3.64/26", "1.2.3.128/25", "1.2.4.0/22", "1.2.8.0/21", "1.2.16.0/20", "1.2.32.0/19", "1.2.64.0/18", "1.2.128.0/17", "1.3.0.0/16", "1.4.0.0/14", "1.8.0.0/13", "1.16.0.0/12", "1.32.0.0/11", "1.64.0.0/10", "1.128.0.0/9", "2.0.0.0/7", "4.0.0.0/8", "5.0.0.0/14", "5.4.0.0/15", "5.6.0.0/22", "5.6.4.0/23", "5.6.6.0/24", "5.6.7.0/29", "5.6.7.8/32", )}, } for _, tt := range tests { 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) for _, v := range got { t.Errorf(" %v", v) } t.Error("want:\n") for _, v := range tt.want { t.Errorf(" %v", v) } } } } func BenchmarkIPRangePrefixes(b *testing.B) { b.ReportAllocs() buf := make([]IPPrefix, 0, 50) r := IPRange{mustIP("1.2.3.5"), mustIP("5.6.7.8")} for i := 0; i < b.N; i++ { _ = r.AppendPrefixes(buf[:0]) } } func TestParseIPRange(t *testing.T) { tests := []struct { in string 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")}}, } for _, tt := range tests { r, err := ParseIPRange(tt.in) var got interface{} if err != nil { got = err.Error() } else { got = r } if got != tt.want { t.Errorf("ParseIPRange(%q) = %v; want %v", tt.in, got, tt.want) } if err == nil { back := r.String() if back != tt.in { 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") } } func TestIPRangeContains(t *testing.T) { type rtest struct { ip IP want bool } tests := []struct { r IPRange rtests []rtest }{ { IPRangeFrom(mustIP("10.0.0.2"), mustIP("10.0.0.4")), []rtest{ {mustIP("10.0.0.1"), false}, {mustIP("10.0.0.2"), true}, {mustIP("10.0.0.3"), true}, {mustIP("10.0.0.4"), true}, {mustIP("10.0.0.5"), false}, {IP{}, false}, {mustIP("::"), false}, }, }, { 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}, {IP{}, false}, }, }, { IPRangeFrom(mustIP("10.0.0.2"), mustIP("::")), // invalid []rtest{ {mustIP("10.0.0.2"), false}, }, }, { IPRange{}, []rtest{ {IP{}, false}, }, }, } for _, tt := range tests { for _, rt := range tt.rtests { got := tt.r.Contains(rt.ip) if got != rt.want { t.Errorf("Range(%v).Contains(%v) = %v; want %v", tt.r, rt.ip, got, rt.want) } } } } func TestIPRangeOverlaps(t *testing.T) { tests := []struct { r, o IPRange want bool }{ { IPRange{}, IPRange{}, false, }, { IPRange{mustIP("10.0.0.1"), mustIP("10.0.0.3")}, IPRange{mustIP("10.0.0.3"), mustIP("10.0.0.4")}, true, // overlaps on edge }, { IPRange{mustIP("10.0.0.1"), mustIP("10.0.0.3")}, IPRange{mustIP("10.0.0.2"), mustIP("10.0.0.4")}, true, // overlaps in middle }, { IPRange{mustIP("10.0.0.1"), mustIP("10.0.0.3")}, IPRange{mustIP("10.0.0.4"), mustIP("10.0.0.4")}, false, // doesn't overlap }, { IPRange{mustIP("10.0.0.1"), mustIP("10.0.0.3")}, IPRange{mustIP("10.0.0.0"), mustIP("10.0.0.5")}, true, // one fully inside the other }, { IPRange{mustIP("10.0.0.1"), mustIP("10.0.0.3")}, IPRange{mustIP("::1"), mustIP("::2")}, false, }, { IPRange{mustIP("::"), mustIP("ff::")}, IPRange{mustIP("cc::1"), mustIP("cc::2")}, true, }, } for _, tt := range tests { got := tt.r.Overlaps(tt.o) if got != tt.want { t.Errorf("Overlaps(%v, %v) = %v; want %v", tt.r, tt.o, got, tt.want) } got = tt.o.Overlaps(tt.r) if got != tt.want { t.Errorf("Overlaps(%v, %v) (reversed) = %v; want %v", tt.o, tt.r, got, tt.want) } } } func TestIPRangeValid(t *testing.T) { tests := []struct { r IPRange want bool }{ {IPRange{mustIP("10.0.0.0"), mustIP("10.0.0.255")}, true}, {IPRange{mustIP("::1"), mustIP("::2")}, true}, {IPRange{mustIP("::1%foo"), mustIP("::2%foo")}, true}, {IPRange{mustIP("::1%foo"), mustIP("::2%bar")}, false}, // zones differ {IPRange{IP{}, IP{}}, false}, // zero values {IPRange{mustIP("::2"), mustIP("::1")}, false}, // bad order {IPRange{mustIP("1.2.3.4"), mustIP("::1")}, false}, // family mismatch } for _, tt := range tests { 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) } } } func TestIPRangePrefix(t *testing.T) { tests := []struct { r IPRange want IPPrefix }{ {IPRange{mustIP("10.0.0.0"), mustIP("10.0.0.255")}, mustIPPrefix("10.0.0.0/24")}, {IPRange{mustIP("10.0.0.0"), mustIP("10.0.0.254")}, IPPrefix{}}, {IPRange{mustIP("fc00::"), AddrPrior(mustIP("fe00::"))}, mustIPPrefix("fc00::/7")}, } for _, tt := range tests { got, ok := tt.r.Prefix() if ok != (got != IPPrefix{}) { t.Errorf("for %v, Prefix() results inconsistent: %v, %v", tt.r, got, ok) } if got != tt.want { t.Errorf("for %v, Prefix = %v; want %v", tt.r, got, tt.want) } } allocs := int(testing.AllocsPerRun(1000, func() { tt := tests[0] if _, ok := tt.r.Prefix(); !ok { t.Fatal("expected okay") } })) if allocs != 0 { t.Errorf("allocs = %v", allocs) } } func BenchmarkIPRangePrefix(b *testing.B) { b.ReportAllocs() r := IPRange{mustIP("10.0.0.0"), mustIP("10.0.0.255")} for i := 0; i < b.N; i++ { if _, ok := r.Prefix(); !ok { b.Fatal("expected a prefix") } } } var nextPriorTests = []struct { ip IP next IP prior IP }{ {mustIP("10.0.0.1"), mustIP("10.0.0.2"), mustIP("10.0.0.0")}, {mustIP("10.0.0.255"), mustIP("10.0.1.0"), mustIP("10.0.0.254")}, {mustIP("127.0.0.1"), mustIP("127.0.0.2"), mustIP("127.0.0.0")}, {mustIP("254.255.255.255"), mustIP("255.0.0.0"), mustIP("254.255.255.254")}, {mustIP("255.255.255.255"), IP{}, mustIP("255.255.255.254")}, {mustIP("0.0.0.0"), mustIP("0.0.0.1"), IP{}}, {mustIP("::"), mustIP("::1"), IP{}}, {mustIP("::%x"), mustIP("::1%x"), IP{}}, {mustIP("::1"), mustIP("::2"), mustIP("::")}, {mustIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff"), IP{}, mustIP("ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe")}, } func TestAddrNextPrior(t *testing.T) { doNextPrior(t) for _, ip := range []IP{ mustIP("0.0.0.0"), mustIP("::"), } { got := AddrPrior(ip) if got.IsValid() { t.Errorf("IP(%v).Prior = %v; want zero", ip, got) } } var allFF [16]byte for i := range allFF { allFF[i] = 0xff } for _, ip := range []IP{ mustIP("255.255.255.255"), netip.AddrFrom16(allFF), } { got := ip.Next() if got.IsValid() { t.Errorf("IP(%v).Next = %v; want zero", ip, got) } } } func BenchmarkIPNextPrior(b *testing.B) { for i := 0; i < b.N; i++ { doNextPrior(b) } } func doNextPrior(t testing.TB) { for _, tt := range nextPriorTests { gnext, gprior := AddrNext(tt.ip), AddrPrior(tt.ip) if gnext != tt.next { t.Errorf("IP(%v).Next = %v; want %v", tt.ip, gnext, tt.next) } if gprior != tt.prior { t.Errorf("IP(%v).Prior = %v; want %v", tt.ip, gprior, tt.prior) } if AddrNext(tt.ip).IsValid() && AddrPrior(AddrNext(tt.ip)) != tt.ip { t.Errorf("IP(%v).Next.Prior = %v; want %v", tt.ip, AddrPrior(AddrNext(tt.ip)), tt.ip) } if AddrPrior(tt.ip).IsValid() && AddrNext(AddrPrior(tt.ip)) != tt.ip { t.Errorf("IP(%v).Prior.Next = %v; want %v", tt.ip, AddrNext(AddrPrior(tt.ip)), tt.ip) } } } // Sink variables are here to force the compiler to not elide // seemingly useless work in benchmarks and allocation tests. If you // were to just `_ = foo()` within a test function, the compiler could // correctly deduce that foo() does nothing and doesn't need to be // called. By writing results to a global variable, we hide that fact // from the compiler and force it to keep the code under test. var ( sinkIP IP sinkStdIP net.IP sinkIPPort IPPort sinkIPPrefix IPPrefix sinkIPPrefixSlice []IPPrefix sinkIPRange IPRange sinkIP16 [16]byte sinkIP4 [4]byte sinkBool bool sinkString string sinkBytes []byte sinkUDPAddr = &net.UDPAddr{IP: make(net.IP, 0, 16)} ) func TestNoAllocs(t *testing.T) { // Wrappers that panic on error, to prove that our alloc-free // methods are returning successfully. panicIPOK := func(ip IP, ok bool) IP { if !ok { panic("not ok") } return ip } panicPfxOK := func(pfx IPPrefix, ok bool) IPPrefix { if !ok { panic("not ok") } return pfx } panicIPPOK := func(ipp IPPort, ok bool) IPPort { if !ok { panic("not ok") } return ipp } test := func(name string, f func()) { t.Run(name, func(t *testing.T) { n := testing.AllocsPerRun(1000, f) if n != 0 { t.Fatalf("allocs = %d; want 0", int(n)) } }) } // IP constructors test("FromStdIP", func() { sinkIP = panicIPOK(FromStdIP(net.IP([]byte{1, 2, 3, 4}))) }) test("FromStdIPRaw", func() { sinkIP = panicIPOK(FromStdIPRaw(net.IP([]byte{1, 2, 3, 4}))) }) // IPPort constructors test("FromStdAddr", func() { std := net.IP{1, 2, 3, 4} sinkIPPort = panicIPPOK(FromStdAddr(std, 5678, "")) }) // IPPrefix constructors test("FromStdIPNet", func() { std := &net.IPNet{ IP: net.IP{1, 2, 3, 4}, Mask: net.IPMask{255, 255, 0, 0}, } sinkIPPrefix = panicPfxOK(FromStdIPNet(std)) }) // IPRange constructors 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.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 := 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 := MustParseIPRange("1.2.3.0-1.2.3.255") sinkIPPrefix = panicPfxOK(a.Prefix()) }) }