pax_global_header00006660000000000000000000000064144414452230014515gustar00rootroot0000000000000052 comment=3ecfe7cd1cd167698126940f7c49bf7b3a091bf4 golang-github-mdlayher-netx-0.0~git20230430.7e21880/000077500000000000000000000000001444144522300213175ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/.github/000077500000000000000000000000001444144522300226575ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/.github/workflows/000077500000000000000000000000001444144522300247145ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/.github/workflows/static-analysis.yml000066400000000000000000000012761444144522300305550ustar00rootroot00000000000000name: Static Analysis on: push: branches: - '*' pull_request: branches: - '*' jobs: build: strategy: matrix: go-version: ["1.20"] runs-on: ubuntu-latest steps: - name: Set up Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Install staticcheck run: go install honnef.co/go/tools/cmd/staticcheck@latest - name: Print staticcheck version run: staticcheck -version - name: Run staticcheck run: staticcheck ./... - name: Run go vet run: go vet ./... golang-github-mdlayher-netx-0.0~git20230430.7e21880/.github/workflows/test.yml000066400000000000000000000010071444144522300264140ustar00rootroot00000000000000name: Test on: push: branches: - '*' pull_request: branches: - '*' jobs: build: strategy: fail-fast: false matrix: go-version: ["1.20"] os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Set up Go uses: actions/setup-go@v2 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v2 - name: Run tests run: go test ./... golang-github-mdlayher-netx-0.0~git20230430.7e21880/LICENSE.md000066400000000000000000000020631444144522300227240ustar00rootroot00000000000000# MIT License Copyright (C) 2020-2022 Matt Layher Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. golang-github-mdlayher-netx-0.0~git20230430.7e21880/README.md000066400000000000000000000006671444144522300226070ustar00rootroot00000000000000# netx [![Test Status](https://github.com/mdlayher/netx/workflows/Test/badge.svg)](https://github.com/mdlayher/netx/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/mdlayher/netx.svg)](https://pkg.go.dev/github.com/mdlayher/netx) [![Go Report Card](https://goreportcard.com/badge/github.com/mdlayher/netx)](https://goreportcard.com/report/github.com/mdlayher/netx) A collection of small Go networking packages. MIT Licensed. golang-github-mdlayher-netx-0.0~git20230430.7e21880/cmd/000077500000000000000000000000001444144522300220625ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/cmd/eui64/000077500000000000000000000000001444144522300230165ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/cmd/eui64/main.go000066400000000000000000000021151444144522300242700ustar00rootroot00000000000000// Command eui64 provides a simple utility to convert an IPv6 address to an // IPv6 prefix and MAC address, or to convert an IPv6 prefix and MAC address // to an IPv6 address. package main import ( "flag" "fmt" "log" "net" "github.com/mdlayher/netx/eui64" ) var ( ipFlag = flag.String("ip", "fe80::", "IPv6 address or IPv6 prefix to parse") macFlag = flag.String("mac", "", "EUI-48 or EUI-64 MAC address to parse") ) func main() { flag.Parse() // IP flag required for both operations. ip := net.ParseIP(*ipFlag) if ip == nil { log.Fatalf("invalid IP address: %s", *ipFlag) } // Attempt to parse prefix and MAC address from an IPv6 address. if *ipFlag != "" && *macFlag == "" { prefix, mac, err := eui64.ParseIP(ip) if err != nil { log.Fatal(err) } fmt.Printf("Prefix: %s\n MAC: %s\n", prefix, mac) return } // Attempt to parse IPv6 address from IPv6 prefix and MAC address. mac, err := net.ParseMAC(*macFlag) if err != nil { log.Fatal(err) } outIP, err := eui64.ParseMAC(ip, mac) if err != nil { log.Fatal(err) } fmt.Printf("IP: %s\n", outIP) } golang-github-mdlayher-netx-0.0~git20230430.7e21880/cmd/rfc4193/000077500000000000000000000000001444144522300231555ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/cmd/rfc4193/main.go000066400000000000000000000023641444144522300244350ustar00rootroot00000000000000// Command rfc4193 generates a Unique Local IPv6 Unicast Address prefix, as // described in RFC4193. package main import ( "bytes" "flag" "fmt" "log" "net" "os" "github.com/mdlayher/netx/rfc4193" ) func main() { flag.Parse() ll := log.New(os.Stderr, "", 0) // If an argument is passed, parse it as a RFC4193 prefix. if s := flag.Arg(0); s != "" { p, err := rfc4193.Parse(s) if err != nil { ll.Fatalf("failed to parse: %v", err) } size, _ := p.IPNet().Mask.Size() fmt.Printf("local: %v, global ID: %#0x, subnet ID: %#04x, prefix: /%d\n", p.Local, p.GlobalID, p.SubnetID, size) return } ifis, err := net.Interfaces() if err != nil { ll.Fatalf("failed to get network interfaces: %v", err) } // Try to choose a suitable interface MAC address as a seed, but also fall // back to random data (nil mac input) if a suitable address isn't found. var mac net.HardwareAddr for _, ifi := range ifis { // Must be Ethernet address, must be non-zero (skip loopback). if len(ifi.HardwareAddr) != 6 || bytes.Equal(ifi.HardwareAddr, make([]byte, 6)) { continue } mac = ifi.HardwareAddr break } p, err := rfc4193.Generate(mac) if err != nil { ll.Fatalf("failed to generate RFC4193 prefix: %v", err) } fmt.Println(p) } golang-github-mdlayher-netx-0.0~git20230430.7e21880/eui64/000077500000000000000000000000001444144522300222535ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/eui64/eui64.go000066400000000000000000000064641444144522300235500ustar00rootroot00000000000000// Package eui64 enables creation and parsing of Modified EUI-64 format // interface identifiers, as described in RFC 4291, Section 2.5.1. package eui64 import ( "errors" "net" ) // Possible errors due to bad input. var ( errInvalidIP = errors.New("eui64: IP must be an IPv6 address") errInvalidMAC = errors.New("eui64: MAC address must be in EUI-48 or EUI-64 form") errInvalidPrefix = errors.New("eui64: prefix must be an IPv6 address prefix of /64 or less") ) // ParseIP parses an input IPv6 address to retrieve its IPv6 address prefix and // EUI-48 or EUI-64 MAC address. ip must be an IPv6 address or an error is // returned. func ParseIP(ip net.IP) (net.IP, net.HardwareAddr, error) { if !isIPv6Addr(ip) { return nil, nil, errInvalidIP } // Prefix is first 8 bytes of IPv6 address. prefix := make(net.IP, 16) copy(prefix[0:8], ip[0:8]) // If IP address contains bytes 0xff and 0xfe adjacent in the middle // of the MAC address section, these bytes must be removed to parse // a EUI-48 hardware address. isEUI48 := ip[11] == 0xff && ip[12] == 0xfe // MAC address length is determined by whether address is EUI-48 or EUI-64. macLen := 8 if isEUI48 { macLen = 6 } mac := make(net.HardwareAddr, macLen) if isEUI48 { // Copy bytes preceeding and succeeding 0xff and 0xfe into MAC. copy(mac[0:3], ip[8:11]) copy(mac[3:6], ip[13:16]) } else { // Copy IP directly into MAC. copy(mac, ip[8:16]) } // Flip 7th bit from left on the first byte of the MAC address, the // "universal/local (U/L)" bit. See RFC 4291, Section 2.5.1 for more // information. mac[0] ^= 0x02 return prefix, mac, nil } // ParseMAC parses an input IPv6 address prefix and EUI-48 or EUI-64 MAC // address to retrieve an IPv6 address in EUI-64 modified form, with the // designated prefix. // // An error is returned if prefix is not an IPv6 address with only the first 64 // bits or less set, or mac is not in EUI-48 or EUI-64 form. func ParseMAC(prefix net.IP, mac net.HardwareAddr) (net.IP, error) { if !isIPv6Addr(prefix) { return nil, errInvalidIP } // Prefix must be 64 bits or less in length, meaning the last 8 // bytes must be entirely zero. if !isAllZeroes(prefix[8:16]) { return nil, errInvalidPrefix } // MAC must be in EUI-48 or EUI64 form. if len(mac) != 6 && len(mac) != 8 { return nil, errInvalidMAC } // Copy prefix directly into first 8 bytes of IP address. ip := make(net.IP, 16) copy(ip[0:8], prefix[0:8]) // Flip 7th bit from left on the first byte of the MAC address, the // "universal/local (U/L)" bit. See RFC 4291, Section 2.5.1 for more // information. // If MAC is in EUI-64 form, directly copy it into output IP address. if len(mac) == 8 { copy(ip[8:16], mac) ip[8] ^= 0x02 return ip, nil } // If MAC is in EUI-48 form, split first three bytes and last three bytes, // and inject 0xff and 0xfe between them. copy(ip[8:11], mac[0:3]) ip[8] ^= 0x02 ip[11] = 0xff ip[12] = 0xfe copy(ip[13:16], mac[3:6]) return ip, nil } // isAllZeroes returns if a byte slice is entirely populated with byte 0. func isAllZeroes(b []byte) bool { for i := 0; i < len(b); i++ { if b[i] != 0 { return false } } return true } // isIPv6Addr returns if an IP address is a valid IPv6 address. func isIPv6Addr(ip net.IP) bool { if ip.To16() == nil { return false } return ip.To4() == nil } golang-github-mdlayher-netx-0.0~git20230430.7e21880/eui64/eui64_test.go000066400000000000000000000151201444144522300245740ustar00rootroot00000000000000package eui64 import ( "bytes" "fmt" "log" "net" "testing" ) // TestParseIP verifies that ParseIP generates appropriate output IPv6 prefixes // and MAC addresses for input IP addresses. func TestParseIP(t *testing.T) { tests := []struct { desc string ip net.IP prefix net.IP mac net.HardwareAddr err error }{ { desc: "nil IP address", err: errInvalidIP, }, { desc: "short IP address", ip: bytes.Repeat([]byte{0}, 15), err: errInvalidIP, }, { desc: "long IP address", ip: bytes.Repeat([]byte{0}, 17), err: errInvalidIP, }, { desc: "IPv4 address", ip: net.IPv4(192, 168, 1, 1), err: errInvalidIP, }, { desc: "IPv6 EUI-64 MAC", ip: net.ParseIP("2001:db8::1"), prefix: net.ParseIP("2001:db8::"), mac: net.HardwareAddr{0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01}, }, { desc: "IPv6 EUI-48 MAC", ip: net.ParseIP("fe80::212:7fff:feeb:6b40"), prefix: net.ParseIP("fe80::"), mac: net.HardwareAddr{0x00, 0x12, 0x7f, 0xeb, 0x6b, 0x40}, }, } for _, tt := range tests { t.Run(tt.desc, func(t *testing.T) { // Copy input value to ensure it is not modified later origIP := make(net.IP, len(tt.ip)) copy(origIP, tt.ip) prefix, mac, err := ParseIP(tt.ip) if err != nil { if want, got := tt.err, err; want != got { t.Fatalf("unexpected error:\n- want: %v\n- got: %v", want, got) } return } // Verify input value was not modified if want, got := origIP, tt.ip; !want.Equal(got) { t.Fatalf("IP was modified:\n- want: %v\n- got: %v", want, got) } if want, got := tt.prefix, prefix; !want.Equal(got) { t.Fatalf("unexpected IPv6 prefix:\n- want: %v\n- got: %v", want, got) } if want, got := tt.mac, mac; !bytes.Equal(want, got) { t.Fatalf("unexpected MAC address:\n- want: %v\n- got: %v", want, got) } }) } } // TestParseMAC verifies that ParseMAC generates appropriate output IPv6 // addresses for input IPv6 prefixes and EUI-48 or EUI-64 MAC addresses. func TestParseMAC(t *testing.T) { tests := []struct { desc string prefix net.IP mac net.HardwareAddr ip net.IP err error }{ { desc: "nil IPv6 prefix", err: errInvalidIP, }, { desc: "short IPv6 prefix", prefix: bytes.Repeat([]byte{0}, 15), err: errInvalidIP, }, { desc: "long IPv6 prefix", prefix: bytes.Repeat([]byte{0}, 17), err: errInvalidIP, }, { desc: "IPv4 prefix", prefix: net.IPv4(192, 168, 1, 1), err: errInvalidIP, }, { desc: "IPv6 /128 prefix", prefix: net.ParseIP("fe80::1"), err: errInvalidPrefix, }, { desc: "nil MAC address", prefix: net.ParseIP("fe80::"), err: errInvalidMAC, }, { desc: "length 5 MAC address", prefix: net.ParseIP("fe80::"), mac: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde}, err: errInvalidMAC, }, { desc: "length 9 MAC address", prefix: net.ParseIP("fe80::"), mac: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 0xbe, 0xef, 0xde}, err: errInvalidMAC, }, { desc: "EUI-48 MAC address 02:00:00:00:00:01", prefix: net.ParseIP("2002:db8::"), mac: net.HardwareAddr{0x02, 0x00, 0x00, 0x00, 0x00, 0x01}, ip: net.ParseIP("2002:db8::ff:fe00:1"), }, { desc: "EUI-48 MAC address 00:12:7f:eb:6b:40", prefix: net.ParseIP("fe80::"), mac: net.HardwareAddr{0x00, 0x12, 0x7f, 0xeb, 0x6b, 0x40}, ip: net.ParseIP("fe80::212:7fff:feeb:6b40"), }, { desc: "EUI-48 MAC address 22:ac:9e:18:be:80", prefix: net.ParseIP("fe80::"), mac: net.HardwareAddr{0x22, 0xac, 0x9e, 0x18, 0xbe, 0x80}, ip: net.ParseIP("fe80::20ac:9eff:fe18:be80"), }, { desc: "EUI-64 MAC address 00:00:00:ff:fe:00:00:01", prefix: net.ParseIP("2002:db8::"), mac: net.HardwareAddr{0x00, 0x00, 0x00, 0xff, 0xfe, 0x00, 0x00, 0x01}, ip: net.ParseIP("2002:db8::200:ff:fe00:1"), }, { desc: "EUI-64 MAC address 00:12:7f:ff:fe:eb:6b:40", prefix: net.ParseIP("fe80::"), mac: net.HardwareAddr{0x00, 0x12, 0x7f, 0xff, 0xfe, 0xeb, 0x6b, 0x40}, ip: net.ParseIP("fe80::212:7fff:feeb:6b40"), }, { desc: "EUI-64 MAC address 22:ac:9e:ff:fe:18:be:80", prefix: net.ParseIP("fe80::"), mac: net.HardwareAddr{0x22, 0xac, 0x9e, 0xff, 0xfe, 0x18, 0xbe, 0x80}, ip: net.ParseIP("fe80::20ac:9eff:fe18:be80"), }, } for i, tt := range tests { // Copy input values to ensure they are not modified later origPrefix := make(net.IP, len(tt.prefix)) copy(origPrefix, tt.prefix) origMAC := make(net.HardwareAddr, len(tt.mac)) copy(origMAC, tt.mac) ip, err := ParseMAC(tt.prefix, tt.mac) if err != nil { if want, got := tt.err, err; want != got { t.Fatalf("[%02d] test %q, unexpected error:\n- want: %v\n- got: %v", i, tt.desc, want, got) } continue } // Verify input values were not modified if want, got := origPrefix, tt.prefix; !want.Equal(got) { t.Fatalf("[%02d] test %q, prefix was modified:\n- want: %v\n- got: %v", i, tt.desc, want, got) } if want, got := origMAC, tt.mac; !bytes.Equal(want, got) { t.Fatalf("[%02d] test %q, MAC was modified:\n- want: %v\n- got: %v", i, tt.desc, want, got) } if want, got := tt.ip, ip; !want.Equal(got) { t.Fatalf("[%02d] test %q, unexpected IPv6 address:\n- want: %v\n- got: %v", i, tt.desc, want, got) } } } // ExampleParseIP demonstrates usage of ParseIP. This example parses an // input IPv6 address into a IPv6 prefix and a MAC address. func ExampleParseIP() { // Example data taken from: // http://packetlife.net/blog/2008/aug/4/eui-64-ipv6/ ip := net.ParseIP("fe80::212:7fff:feeb:6b40") // Retrieve IPv6 prefix and MAC address from IPv6 address prefix, mac, err := ParseIP(ip) if err != nil { log.Fatal(err) } fmt.Printf(" ip: %s\nprefix: %s\n mac: %s", ip, prefix, mac) // Output: // ip: fe80::212:7fff:feeb:6b40 // prefix: fe80:: // mac: 00:12:7f:eb:6b:40 } // ExampleParseMAC demonstrates usage of ParseMAC. This example parses an // input IPv6 address into a IPv6 prefix and a MAC address. func ExampleParseMAC() { // Example data taken from: // http://packetlife.net/blog/2008/aug/4/eui-64-ipv6/ prefix := net.ParseIP("fe80::") mac, err := net.ParseMAC("00:12:7f:eb:6b:40") if err != nil { log.Fatal(err) } // Retrieve IPv6 address from IPv6 prefix and MAC address ip, err := ParseMAC(prefix, mac) if err != nil { log.Fatal(err) } fmt.Printf("prefix: %s\n mac: %s\n ip: %s", prefix, mac, ip) // Output: // prefix: fe80:: // mac: 00:12:7f:eb:6b:40 // ip: fe80::212:7fff:feeb:6b40 } golang-github-mdlayher-netx-0.0~git20230430.7e21880/go.mod000066400000000000000000000002121444144522300224200ustar00rootroot00000000000000module github.com/mdlayher/netx go 1.20 require ( github.com/google/go-cmp v0.5.9 golang.org/x/net v0.9.0 golang.org/x/sync v0.1.0 ) golang-github-mdlayher-netx-0.0~git20230430.7e21880/go.sum000066400000000000000000000007271444144522300224600ustar00rootroot00000000000000github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang-github-mdlayher-netx-0.0~git20230430.7e21880/multinet/000077500000000000000000000000001444144522300231605ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/multinet/doc.go000066400000000000000000000001331444144522300242510ustar00rootroot00000000000000// Package multinet allows combining multiple package net types into one. package multinet golang-github-mdlayher-netx-0.0~git20230430.7e21880/multinet/internal/000077500000000000000000000000001444144522300247745ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/multinet/internal/nettestx/000077500000000000000000000000001444144522300266525ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/multinet/internal/nettestx/listenertest.go000066400000000000000000000157371444144522300317430ustar00rootroot00000000000000// Copyright 2019 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. // This code is copied and slightly modified from an open CL: // https://go-review.googlesource.com/c/net/+/123056. package nettestx import ( "net" "sync" "testing" "time" ) var ( aLongTimeAgo = time.Unix(233431200, 0) neverTimeout = time.Time{} ) // MakeOpenerSet creates and returns a set of connection openers for // both passive- and active-open sides. // The stop function closes all resources including ln, cancels the // dial operation, and should not be nil. type MakeOpenerSet func() (ln net.Listener, dial func(addr net.Addr) (net.Conn, error), stop func(), err error) // TestListener tests that a net.Listener implementation properly // satisfies the interface. // The tests should not produce any false positives, but may // experience false negatives. // Thus, some issues may only be detected when the test is run // multiple times. // For maximal effectiveness, run the tests under the race detector. func TestListener(t *testing.T, mos MakeOpenerSet) { t.Run("Accept", func(t *testing.T) { openerSetTimeoutWrapper(t, mos, testOpenerSetAccept) }) t.Run("RacyAccept", func(t *testing.T) { openerSetTimeoutWrapper(t, mos, testOpenerSetRacyAccept) }) t.Run("PastTimeout", func(t *testing.T) { openerSetTimeoutWrapper(t, mos, testOpenerSetPastTimeout) }) t.Run("PresentTimeout", func(t *testing.T) { openerSetTimeoutWrapper(t, mos, testOpenerSetPresentTimeout) }) t.Run("FutureTimeout", func(t *testing.T) { openerSetTimeoutWrapper(t, mos, testOpenerSetFutureTimeout) }) t.Run("CloseTimeout", func(t *testing.T) { openerSetTimeoutWrapper(t, mos, testOpenerSetCloseTimeout) }) t.Run("ConcurrentMethods", func(t *testing.T) { openerSetTimeoutWrapper(t, mos, testOpenerSetConcurrentMethods) }) } type listenerTester func(t *testing.T, ln net.Listener, dial func(addr net.Addr) (net.Conn, error)) func openerSetTimeoutWrapper(t *testing.T, mos MakeOpenerSet, f listenerTester) { t.Parallel() ln, dial, stop, err := mos() if err != nil { t.Fatalf("unable to make opener set: %v", err) } var once sync.Once defer once.Do(func() { stop() }) timer := time.AfterFunc(time.Minute, func() { once.Do(func() { t.Error("test timed out; terminating opener set") stop() }) }) defer timer.Stop() f(t, ln, dial) } type deadlineListener interface { net.Listener SetDeadline(time.Time) error } // testOpenerSetAccept tests that the connection setup request invoked // by dial is properly accepted on ln. func testOpenerSetAccept(t *testing.T, ln net.Listener, dial func(net.Addr) (net.Conn, error)) { var wg sync.WaitGroup defer wg.Wait() wg.Add(1) go func() { defer wg.Done() c, err := dial(ln.Addr()) if err != nil { t.Errorf("unexpected Dial error: %v", err) return } if err := c.Close(); err != nil { t.Errorf("unexpected Close error: %v", err) return } }() c, err := ln.Accept() if err != nil { t.Errorf("unexpected Accept error: %v", err) return } c.Close() } // testOpenerSetRacyAccept tests that it is safe to call Accept // concurrently. func testOpenerSetRacyAccept(t *testing.T, ln net.Listener, dial func(net.Addr) (net.Conn, error)) { dl, ok := ln.(deadlineListener) if !ok { t.Skip("deadline not implemented") } var wg sync.WaitGroup dl.SetDeadline(time.Now().Add(time.Millisecond)) for i := 0; i < 5; i++ { wg.Add(2) go func() { defer wg.Done() for j := 0; j < 10; j++ { c, err := dl.Accept() if err != nil { checkForTimeoutError(t, err) dl.SetDeadline(time.Now().Add(time.Millisecond)) continue } c.Close() } }() go func() { defer wg.Done() c, err := dial(dl.Addr()) if err != nil { return } c.Close() }() } wg.Wait() } // testOpenerSetPastTimeout tests that a deadline set in the past // immediately times out Accept operations. func testOpenerSetPastTimeout(t *testing.T, ln net.Listener, dial func(net.Addr) (net.Conn, error)) { dl, ok := ln.(deadlineListener) if !ok { t.Skip("deadline not implemented") } dl.SetDeadline(aLongTimeAgo) _, err := dl.Accept() checkForTimeoutError(t, err) } // testOpenerSetPresentTimeout tests that a deadline set while there // are pending Accept operations immediately times out those // operations. func testOpenerSetPresentTimeout(t *testing.T, ln net.Listener, dial func(net.Addr) (net.Conn, error)) { dl, ok := ln.(deadlineListener) if !ok { t.Skip("deadline not implemented") } var wg sync.WaitGroup wg.Add(2) deadlineSet := make(chan bool, 1) go func() { defer wg.Done() time.Sleep(10 * time.Millisecond) deadlineSet <- true dl.SetDeadline(aLongTimeAgo) }() go func() { defer wg.Done() _, err := dl.Accept() checkForTimeoutError(t, err) if len(deadlineSet) == 0 { t.Error("Accept timed out before deadline is set") } }() wg.Wait() } // testOpenerSetFutureTimeout tests that a future deadline will // eventually time out Accept operations. func testOpenerSetFutureTimeout(t *testing.T, ln net.Listener, dial func(net.Addr) (net.Conn, error)) { dl, ok := ln.(deadlineListener) if !ok { t.Skip("deadline not implemented") } var wg sync.WaitGroup wg.Add(1) dl.SetDeadline(time.Now().Add(100 * time.Millisecond)) go func() { defer wg.Done() _, err := dl.Accept() checkForTimeoutError(t, err) }() wg.Wait() } // testOpenerSetCloseTimeout tests that calling Close immediately // times out pending Accept operations. func testOpenerSetCloseTimeout(t *testing.T, ln net.Listener, dial func(net.Addr) (net.Conn, error)) { dl, ok := ln.(deadlineListener) if !ok { t.Skip("deadline not implemented") } var wg sync.WaitGroup wg.Add(2) // Test for cancelation upon connection closure. dl.SetDeadline(neverTimeout) go func() { defer wg.Done() time.Sleep(100 * time.Millisecond) dl.Close() }() go func() { defer wg.Done() var err error for err == nil { _, err = dl.Accept() } }() wg.Wait() } // testOpennerSetConcurrentMethods tests that the methods of // net.Listener can safely be called concurrently. func testOpenerSetConcurrentMethods(t *testing.T, ln net.Listener, dial func(net.Addr) (net.Conn, error)) { dl, ok := ln.(deadlineListener) if !ok { t.Skip("deadline not implemented") } // The results of the calls may be nonsensical, but this // should not trigger a race detector warning. var wg sync.WaitGroup for i := 0; i < 100; i++ { wg.Add(3) go func() { defer wg.Done() dl.Accept() }() go func() { defer wg.Done() dl.SetDeadline(time.Now().Add(10 * time.Millisecond)) }() go func() { defer wg.Done() dl.Addr() }() } wg.Wait() // At worst, the deadline is set 10ms into the future } // checkForTimeoutError checks that the error satisfies the Error interface // and that Timeout returns true. func checkForTimeoutError(t *testing.T, err error) { t.Helper() if nerr, ok := err.(net.Error); ok { if !nerr.Timeout() { t.Errorf("err.Timeout() = false, want true") } } else { t.Errorf("got %T, want net.Error", err) } } golang-github-mdlayher-netx-0.0~git20230430.7e21880/multinet/listener.go000066400000000000000000000113131444144522300253330ustar00rootroot00000000000000package multinet import ( "errors" "fmt" "net" "strings" "sync" "time" ) // An Addr is net.Addr which stores network address information for all // net.Listeners being used by a Listener. type Addr []net.Addr var _ net.Addr = Addr{} // Network implements net.Addr, returning a comma-separated list of Network // values for each net.Addr in a. func (a Addr) Network() string { return a.join(func(addr net.Addr) string { return addr.Network() }) } // String implements net.Addr, returning a comma-separated list of String // values for each net.Addr in a. func (a Addr) String() string { return a.join(func(addr net.Addr) string { return addr.String() }) } // join invokes fn for each net.Addr stored in Addr and collects the results // into a comma-separated string. func (a Addr) join(fn func(addr net.Addr) string) string { ss := make([]string, 0, len(a)) for _, addr := range a { ss = append(ss, fn(addr)) } return strings.Join(ss, ",") } // A Listener is a net.Listener which aggregates multiple net.Listeners. The // net.Listeners do not have to be of the same underlying type. Any connection // or error from an individual net.Listener will be forwarded to the Listener. type Listener struct { ls []net.Listener acceptOnce, closeOnce sync.Once wg sync.WaitGroup doneC chan struct{} acceptC chan accept } var _ net.Listener = &Listener{} // Listen creates a Listener which aggregates multiple net.Listeners. Although // it is possible to construct a Listener with no net.Listeners, it will always // return an error on Accept. func Listen(ls ...net.Listener) *Listener { return &Listener{ ls: ls, doneC: make(chan struct{}), acceptC: make(chan accept, len(ls)), } } // Accept accepts a net.Conn from one of the owned net.Listeners. func (l *Listener) Accept() (net.Conn, error) { if len(l.ls) == 0 { // No listeners, nothing to do. return nil, errors.New("multinet: no net.Listeners added to Listener") } l.acceptOnce.Do(func() { // On first Accept, create accept multiplexing goroutines which will // feed accepted connections and errors over l.acceptC. l.wg.Add(len(l.ls)) for _, ln := range l.ls { go func(ln net.Listener) { defer l.wg.Done() l.accept(ln) }(ln) } }) select { case a := <-l.acceptC: return a.c, a.err case <-l.doneC: // TODO: good enough? return nil, errors.New("multinet: use of closed network connection") } } // Addr creates a net.Addr of type Addr with all the aggregated addresses of // the owned net.Listeners. func (l *Listener) Addr() net.Addr { addrs := make(Addr, 0, len(l.ls)) for _, ln := range l.ls { addrs = append(addrs, ln.Addr()) } return addrs } // A deadlineListener is a net.Listener with deadline support. type deadlineListener interface { net.Listener SetDeadline(t time.Time) error } // SetDeadline sets a deadline t on all net.Listeners owned by this Listener. // All net.Listeners must support the method "SetDeadline(t time.Time) error" // or an error will be returned. If more than one net.Listener returns an error, // only the first error is returned. func (l *Listener) SetDeadline(t time.Time) error { dls := make([]deadlineListener, 0, len(l.ls)) for _, ln := range l.ls { dl, ok := ln.(deadlineListener) if !ok { return fmt.Errorf("multinet: net.Listener %T does not have a SetDeadline method", ln) } dls = append(dls, dl) } var err error for _, dl := range dls { // Only propagate the first returned error to the caller. if lerr := dl.SetDeadline(t); lerr != nil && err == nil { err = lerr } } return err } // Close closes all net.Listeners owned by this Listener. If more than one // net.Listener returns an error, only the first error is returned. func (l *Listener) Close() error { var err error l.closeOnce.Do(func() { // On first invocation of Close, halt all accept multiplexing // goroutines and Close the individual listeners. defer l.wg.Wait() close(l.doneC) for _, ln := range l.ls { // Close all listeners to avoid any file descriptor leaks, but only // propagate the first returned error to the caller. if lerr := ln.Close(); lerr != nil && err == nil { err = lerr } } }) return err } // An accept is the result of the Accept method. type accept struct { c net.Conn err error } // accept begins accepting connections on ln, sending the results to l.acceptC. func (l *Listener) accept(ln net.Listener) { for { c, err := ln.Accept() // Prioritize the done signal over accepting a connection, but allow // either to occur later to satisfy nettest. select { case <-l.doneC: return default: } select { case <-l.doneC: return case l.acceptC <- accept{c: c, err: err}: } } } golang-github-mdlayher-netx-0.0~git20230430.7e21880/multinet/listener_nettest_test.go000066400000000000000000000011621444144522300301410ustar00rootroot00000000000000package multinet_test import ( "net" "testing" "github.com/mdlayher/netx/multinet" "github.com/mdlayher/netx/multinet/internal/nettestx" ) func TestIntegrationNettestTestListener(t *testing.T) { mos := func() (ln net.Listener, dial func(net.Addr) (net.Conn, error), stop func(), err error) { l4, err := net.Listen("tcp", ":0") if err != nil { return nil, nil, nil, err } l := multinet.Listen(l4) stop = func() { _ = l.Close() } dial = func(addr net.Addr) (net.Conn, error) { return net.Dial(addr.Network(), addr.String()) } return l, dial, stop, nil } nettestx.TestListener(t, mos) } golang-github-mdlayher-netx-0.0~git20230430.7e21880/multinet/listener_test.go000066400000000000000000000200031444144522300263660ustar00rootroot00000000000000package multinet_test import ( "context" "errors" "fmt" "io" "net" "net/http" "net/url" "path/filepath" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/mdlayher/netx/multinet" "golang.org/x/net/nettest" "golang.org/x/sync/errgroup" ) // TODO: copy over nettest.TestListener from vsock. func TestListenerAddr(t *testing.T) { l := multinet.Listen( localListener("tcp4"), localListener("tcp6"), localListener("unix"), ) defer l.Close() if diff := cmp.Diff("tcp,tcp,unix", l.Addr().Network()); diff != "" { t.Fatalf("unexpected networks (-want +got):\n%s", diff) } // Unpack each individual address from the slice to verify the fields // of the individual addresses. var ( tcp4 *net.TCPAddr tcp6 *net.TCPAddr unix *net.UnixAddr ) for i, a := range l.Addr().(multinet.Addr) { switch i { case 0: tcp4 = a.(*net.TCPAddr) case 1: tcp6 = a.(*net.TCPAddr) case 2: unix = a.(*net.UnixAddr) default: panic("l.Addr() returned too many addresses") } } // Port will be randomized, so just verify the correct localhost IP for // TCP addresses. if !tcp4.IP.Equal(net.IPv4(127, 0, 0, 1)) { t.Fatalf("unexpected IPv4 address: %s", tcp4.IP) } if !tcp6.IP.Equal(net.IPv6loopback) { t.Fatalf("unexpected IPv6 address: %s", tcp6.IP) } // The filename is randomized, so just look for a temporary directory prefix. // TODO: make work on non-UNIX. if filepath.Dir(unix.Name) != "/tmp" { t.Fatalf("unexpected UNIX address: %s", unix.Name) } // Finally, verify the String output in a deterministic way. tcp4.Port = 80 tcp6.Port = 80 unix.Name = "/tmp/foo" got := multinet.Addr{tcp4, tcp6, unix}.String() if diff := cmp.Diff("127.0.0.1:80,[::1]:80,/tmp/foo", got); diff != "" { t.Fatalf("unexpected string output (-want +got):\n%s", diff) } } func TestListenerHTTP(t *testing.T) { // Open several local listeners using different socket types so that we can // verify each works as expected for HTTP requests. var ( tcp4 = localListener("tcp4") tcp6 = localListener("tcp6") unix = localListener("unix") ) l := multinet.Listen(tcp4, tcp6, unix) srv := &http.Server{ Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Echo the client's remote address back to them. _, _ = io.WriteString(w, r.RemoteAddr) }), } // Serve HTTP on Listener until server is closed at the end of the test. var eg errgroup.Group eg.Go(func() error { if err := srv.Serve(l); err != nil && err != http.ErrServerClosed { return fmt.Errorf("failed to serve: %v", err) } return nil }) defer func() { if err := srv.Close(); err != nil { t.Fatalf("failed to close server: %v", err) } if err := eg.Wait(); err != nil { t.Fatalf("failed to wait for server: %v", err) } }() // Given a certain listener address, perform an HTTP GET to the // multi-listener server and verify that the server handled our request // using the appropriate listener family. tests := []struct { name string l net.Listener fn func(t *testing.T, addr string) }{ { name: "TCPv4", l: tcp4, fn: func(t *testing.T, addr string) { ip := parseAddrIP(addr) if ip.To16() == nil || ip.To4() == nil { t.Fatalf("IP %q is not an IPv4 address", ip) } }, }, { name: "TCPv6", l: tcp6, fn: func(t *testing.T, addr string) { ip := parseAddrIP(addr) if ip.To16() == nil || ip.To4() != nil { t.Fatalf("IP %q is not an IPv6 address", ip) } }, }, { name: "UNIX", l: unix, fn: func(t *testing.T, addr string) { // UNIX socket addresses seem to be represented this way. if diff := cmp.Diff("@", addr); diff != "" { t.Fatalf("unexpected UNIX address (-want +got):\n%s", diff) } }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { tt.fn(t, httpGet(t, tt.l.Addr())) }) } } func TestListenerCloseError(t *testing.T) { // Verify that an error from a single listener is propagated back to the // caller on Close, and that further calls return no error. var ( errFoo = errors.New("some error") // The first listener returns the expected error and the second's value // should be ignored. Close should be called on both. el1 = &errListener{err: errFoo} el2 = &errListener{err: errors.New("another error")} ) l := multinet.Listen( localListener("tcp"), el1, el2, ) var errs []error for i := 0; i < 3; i++ { errs = append(errs, l.Close()) } if diff := cmp.Diff([]error{errFoo, nil, nil}, errs, cmp.Comparer(compareErrors)); diff != "" { t.Fatalf("unexpected Close errors (-want +got):\n%s", diff) } if !el1.closed { t.Fatal("first errListener was not closed") } if !el2.closed { t.Fatal("second errListener was not closed") } } func TestListenerNoSetDeadline(t *testing.T) { // TCP listener supports deadlines, but errListener does not. l := multinet.Listen(localListener("tcp"), &errListener{}) defer l.Close() if err := l.SetDeadline(time.Now()); err == nil { t.Fatal("expected an error, but none occurred") } } func TestListenNoListeners(t *testing.T) { // While a Listener constructed with no net.Listeners wouldn't be useful, // we should verify it doesn't panic or similar. l := multinet.Listen() if diff := cmp.Diff(multinet.Addr{}, l.Addr()); diff != "" { t.Fatalf("unexpected Addr (-want +got):\n%s", diff) } // Close before and after accept to verify sanity. doClose := func() { for i := 0; i < 3; i++ { if err := l.Close(); err != nil { t.Fatalf("failed to close listener: %v", err) } } } doClose() if c, err := l.Accept(); err == nil || c != nil { t.Fatalf("expected nil net.Conn (got: %#v) and non-nil error", c) } doClose() } func compareErrors(x, y error) bool { switch { case x == nil && y == nil: // Both nil. return true case x == nil || y == nil: // One or the other nil. return false default: // Verify by string contents. return x.Error() == y.Error() } } func localListener(network string) net.Listener { l, err := nettest.NewLocalListener(network) if err != nil { panicf("failed to create local listener: %v", err) } return l } func httpGet(t *testing.T, addr net.Addr) string { t.Helper() var ( transport = &http.Transport{} reqAddr string ) switch addr.(type) { case *net.TCPAddr: // Send requests to the TCP address of the server. reqAddr = (&url.URL{ Scheme: "http", Host: addr.String(), }).String() case *net.UnixAddr: // Send requests over UNIX socket instead of TCP. transport.DialContext = func(_ context.Context, _, _ string) (net.Conn, error) { return net.Dial("unix", addr.String()) } // It seems this just has to be set to something nonsensical since we // are overriding Dial with a known address anyway. reqAddr = "http://foo" default: panicf("unhandled type: %T", addr) } c := &http.Client{ Timeout: 1 * time.Second, Transport: transport, } req, err := http.NewRequest(http.MethodGet, reqAddr, nil) if err != nil { t.Fatalf("failed to create HTTP request: %v", err) } res, err := c.Do(req) if err != nil { t.Fatalf("failed to perform HTTP request: %v", err) } defer res.Body.Close() if res.StatusCode != http.StatusOK { t.Fatalf("expected HTTP 200, but got HTTP %d", res.StatusCode) } b, err := io.ReadAll(io.LimitReader(res.Body, 1024)) if err != nil { t.Fatalf("failed to read HTTP body: %v", err) } return string(b) } func parseAddrIP(s string) net.IP { // Assume s is a host:port string where host is always an IP. host, _, err := net.SplitHostPort(s) if err != nil { panicf("failed to split host/port: %v", err) } ip := net.ParseIP(host) if ip == nil { panicf("failed to parse IP address: %v", err) } return ip } func panicf(format string, a ...interface{}) { panic(fmt.Sprintf(format, a...)) } type errListener struct { err error closed bool } var _ net.Listener = &errListener{} func (*errListener) Addr() net.Addr { panic("unimplemented") } func (*errListener) Accept() (net.Conn, error) { panic("unimplemented") } func (l *errListener) Close() error { l.closed = true return l.err } golang-github-mdlayher-netx-0.0~git20230430.7e21880/rfc4193/000077500000000000000000000000001444144522300224125ustar00rootroot00000000000000golang-github-mdlayher-netx-0.0~git20230430.7e21880/rfc4193/doc.go000066400000000000000000000002001444144522300234760ustar00rootroot00000000000000// Package rfc4193 implements Unique Local IPv6 Unicast Address prefix // generation, as described in RFC 4193. package rfc4193 golang-github-mdlayher-netx-0.0~git20230430.7e21880/rfc4193/rfc4193.go000066400000000000000000000131151444144522300240350ustar00rootroot00000000000000package rfc4193 import ( "crypto/rand" "crypto/sha1" "encoding/binary" "errors" "fmt" "io" "net" "time" ) // ula is the IPv6 Unique Local Address prefix. var ula = &net.IPNet{ IP: net.ParseIP("fc00::"), Mask: net.CIDRMask(7, 128), } // A Prefix represents a Local IPv6 Unicast Address prefix, as described in // RFC 4193, section 3.1. type Prefix struct { // Local indicates if the prefix is locally assigned. Local bool // GlobalID stores an identifier for a globally unique prefix. GlobalID [5]byte // SubnetID identifies an individual /64 subnet within a Prefix. SubnetID uint16 // mask is the Prefix's length. It is set to either /48 or /64 when IPNet or // Subnet is called, depending on whether the Prefix was created by Generate // or manually by the caller. mask net.IPMask } // IPNet produces a *net.IPNet prefix value from a Prefix. func (p *Prefix) IPNet() *net.IPNet { // Finalize the computation started by Generate: // // "6) Concatenate FC00::/7, the L bit set to 1, and the 40-bit Global // ID to create a Local IPv6 address prefix." ip := net.IP{0: 0xfc, 15: 0x00} if p.Local { ip[0] |= 0x01 } copy(ip[1:6], p.GlobalID[:]) // Also set the subnet ID portion. If this Prefix was produced by Generate // and no subnet ID and mask were previously assigned, we will produce a /48. // // However, if Prefix.Subnet was called, all subsequent calls will produce // a /64 subnet within the parent /48 prefix. binary.BigEndian.PutUint16(ip[6:8], p.SubnetID) if p.mask == nil { if p.SubnetID == 0 { p.mask = net.CIDRMask(48, 128) } else { p.mask = net.CIDRMask(64, 128) } } return &net.IPNet{ IP: ip, Mask: p.mask, } } // Subnet produces a /64 Prefix with the specified subnet ID. // // If p is a /48 Prefix, the new /64 Prefix will be a child of that parent // /48 Prefix. // // If p is a /64 Prefix (typically produced through a previous call to Subnet), // the new /64 Prefix will be a sibling /64 Prefix of the source /64 Prefix. func (p *Prefix) Subnet(id uint16) *Prefix { // Make a copy of p's parameters and produce a /64 prefix. pp := *p pp.SubnetID = id pp.mask = net.CIDRMask(64, 128) return &pp } // String returns the CIDR notation string for a Prefix. func (p *Prefix) String() string { return p.IPNet().String() } // Parse parses a /48 or /64 Prefix from a CIDR notation string. If s is not a // /48 or /64 IPv6 Unique Local Address prefix, it returns an error. func Parse(s string) (*Prefix, error) { ip, cidr, err := net.ParseCIDR(s) if err != nil { return nil, err } // Only accept IPv6 ULA /48 or /64 prefixes. if ip.To16() == nil || ip.To4() != nil { return nil, fmt.Errorf("rfc4193: invalid IPv6 address: %s", s) } ones, _ := cidr.Mask.Size() if !cidr.IP.Equal(ip) || !ula.Contains(ip) || (ones != 48 && ones != 64) { return nil, fmt.Errorf("rfc4193: must specify a Unique Local Address /48 or /64 IPv6 prefix: %s", s) } p := Prefix{ Local: ip[0]&0x01 == 1, SubnetID: binary.BigEndian.Uint16(ip[6:8]), mask: net.CIDRMask(ones, 128), } copy(p.GlobalID[:], ip[1:6]) return &p, nil } // Generate produces a /48 Prefix by using mac (typically the MAC address of a // network interface) as a seed. It uses the algorithm specified in RFC 4193, // section 3.2.2. // // mac must either be nil or a 6-byte EUI-48 format MAC address. If mac is nil, // cryptographically-secure random bytes will be used as a seed. func Generate(mac net.HardwareAddr) (*Prefix, error) { // Generate a Prefix using real timestamps and crypto/rand.Reader. return (&generator{ now: time.Now, cr: rand.Reader, }).generate(mac) } // A generator backs the logic for Generate. Its fields can be modified to // generate deterministic output for tests. type generator struct { now func() time.Time cr io.Reader } // generate generates a Prefix using the configured generator and seed. func (g *generator) generate(seed net.HardwareAddr) (*Prefix, error) { // Store a timestamp and 8-byte value for hash input. in := make([]byte, 16) // "1) Obtain the current time of day in 64-bit NTP format [NTP]." binary.BigEndian.PutUint64(in[:8], uint64(g.now().UnixNano())) // Produce an 8-byte value: // // "2) Obtain an EUI-64 identifier from the system running this // algorithm. If an EUI-64 does not exist, one can be created from // a 48-bit MAC address as specified in [ADDARCH]. If an EUI-64 // cannot be obtained or created, a suitably unique identifier, // local to the node, should be used (e.g., system serial number)." // // And combine it with the timestamp: // // "3) Concatenate the time of day with the system-specific identifier // in order to create a key." switch { case len(seed) == 6: // EUI-48 input; produce an EUI-64 value as input. // Reference: https://packetlife.net/blog/2008/aug/4/eui-64-ipv6/. in[11] = 0xff in[12] = 0xfe copy(in[8:], seed[:3]) copy(in[13:], seed[3:]) in[8] ^= 0x02 case seed == nil: // No seed; so we will use an io.Reader (usually crypto/rand.Reader) to // produce the "suitably unique identifier". if _, err := io.ReadFull(g.cr, in[8:]); err != nil { return nil, err } default: return nil, errors.New("rfc4193: expected an EUI-48 format MAC address or nil MAC address") } // Always produce a /48 with the local flag set, per the p := &Prefix{ // Always set to true, per RFC 4193, section 3.2.2. Local: true, mask: net.CIDRMask(48, 128), } // "4) Compute an SHA-1 digest on the key as specified in [FIPS, SHA1]; // the resulting value is 160 bits."" // // "5) Use the least significant 40 bits as the Global ID." out := sha1.Sum(in) copy(p.GlobalID[:], out[15:]) return p, nil } golang-github-mdlayher-netx-0.0~git20230430.7e21880/rfc4193/rfc4193_test.go000066400000000000000000000154571444144522300251070ustar00rootroot00000000000000package rfc4193 import ( "bytes" "encoding/binary" "net" "testing" "time" "github.com/google/go-cmp/cmp" ) var ( p48 = net.CIDRMask(48, 128) p64 = net.CIDRMask(64, 128) ) func TestGenerateRandom(t *testing.T) { tests := []struct { name string mac net.HardwareAddr ok bool }{ { name: "bad MAC", mac: net.HardwareAddr{0xff}, }, { name: "OK MAC", mac: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}, ok: true, }, { name: "nil MAC", ok: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p, err := Generate(tt.mac) if tt.ok && err != nil { t.Fatalf("failed to generate prefix: %v", err) } if !tt.ok && err == nil { t.Fatal("expected an error, but none occurred") } if err != nil { return } // GlobalID portion is randomized in this test, but the rest of the // address is deterministic and can be tested for comparison after // zeroing it. // // It is possible (but very unlikely) that GlobalID would be randomly // set to all zero, causing this check to fail. if p.GlobalID == [5]byte{} { t.Fatalf("global ID for prefix was not set: %v", p.GlobalID) } p.GlobalID = [5]byte{} // Generate always produces a /48 with the local flag set. want := &Prefix{ Local: true, mask: net.CIDRMask(48, 128), } testPrefixes(t, want, p, want.IPNet()) }) } } func TestGenerateDeterministic(t *testing.T) { tests := []struct { name string seed net.HardwareAddr ok bool p *Prefix ipn *net.IPNet }{ { name: "bad seed", seed: net.HardwareAddr{0xff}, }, { name: "OK seed", seed: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}, ok: true, p: &Prefix{ Local: true, GlobalID: [5]byte{0x5a, 0x5c, 0x39, 0x0f, 0xc1}, mask: p48, }, ipn: &net.IPNet{ IP: net.ParseIP("fd5a:5c39:fc1::"), Mask: p48, }, }, { name: "nil seed", ok: true, p: &Prefix{ Local: true, GlobalID: [5]byte{0xd0, 0x9c, 0x74, 0xd0, 0x17}, mask: p48, }, ipn: &net.IPNet{ IP: net.ParseIP("fdd0:9c74:d017::"), Mask: p48, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { // Set up g for deterministic output with a fixed timestamp and // reader bytes. g := &generator{ now: func() time.Time { return time.Unix(1, 0) }, cr: bytes.NewReader(make([]byte, 8)), } p, err := g.generate(tt.seed) if tt.ok && err != nil { t.Fatalf("failed to generate prefix: %v", err) } if !tt.ok && err == nil { t.Fatal("expected an error, but none occurred") } if err != nil { return } testPrefixes(t, tt.p, p, tt.ipn) }) } } func TestPrefixManual(t *testing.T) { tests := []struct { name string p *Prefix ipn *net.IPNet }{ { name: "local false /48", p: &Prefix{ GlobalID: [5]byte{0: 0x01}, }, ipn: &net.IPNet{ IP: net.ParseIP("fc01::"), Mask: p48, }, }, { name: "local true /48", p: &Prefix{ Local: true, GlobalID: [5]byte{0: 0x02}, }, ipn: &net.IPNet{ IP: net.ParseIP("fd02::"), Mask: p48, }, }, { name: "local false /64", p: &Prefix{ GlobalID: [5]byte{0: 0x03}, SubnetID: 0x1010, }, ipn: &net.IPNet{ IP: net.ParseIP("fc03:0:0:1010::"), Mask: p64, }, }, { name: "local true /64", p: &Prefix{ Local: true, GlobalID: [5]byte{0: 0x04}, SubnetID: 0x2020, }, ipn: &net.IPNet{ IP: net.ParseIP("fd04:0:0:2020::"), Mask: p64, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { if diff := cmp.Diff(tt.ipn, tt.p.IPNet()); diff != "" { t.Fatalf("unexpected Prefix.IPNet (-want +got):\n%s", diff) } // Child subnet with a matching subnet ID should always reside // within (or be equal to for /64) their parent. child := tt.p.Subnet(tt.p.SubnetID).IPNet() if !tt.ipn.Contains(child.IP) { t.Fatalf("parent prefix %q does not contain child prefix %q", tt.ipn, child) } // For /64s exclusively, a different subnet ID produces a // non-overlapping sibling /64 prefix. if ones, _ := tt.ipn.Mask.Size(); ones != 48 { sibling := tt.p.Subnet(tt.p.SubnetID + 1).IPNet() if child.Contains(sibling.IP) { t.Fatalf("child prefix %q contains sibling prefix %q", child, sibling) } } }) } } func TestParse(t *testing.T) { tests := []struct { name string s string ok bool }{ { name: "bad", s: "foo", }, { name: "IPv4", s: "192.0.2.0/24", }, { name: "individual IP", s: "fd00::1/64", }, { name: "global unicast prefix", s: "2001:db8::/32", }, { name: "wrong subnet size", s: "2001:db8::/56", }, { name: "local false /48", s: "fc01::/48", ok: true, }, { name: "local true /48", s: "fd02::/48", ok: true, }, { name: "local false /64", s: "fc03:0:0:1010::/64", ok: true, }, { name: "local true /64", s: "fd04:0:0:2020::/64", ok: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p, err := Parse(tt.s) if tt.ok && err != nil { t.Fatalf("failed to parse: %v", err) } if !tt.ok && err == nil { t.Fatal("expected an error, but none occurred") } if err != nil { t.Logf("err: %v", err) return } if diff := cmp.Diff(tt.s, p.String()); diff != "" { t.Fatalf("unexpected Prefix string (-want +got):\n%s", diff) } }) } } func testPrefixes(t *testing.T, want, got *Prefix, parent *net.IPNet) { t.Helper() // Expect want, got, and parent to all represent the same values in // different forms. if diff := cmp.Diff(want, got, cmp.AllowUnexported(Prefix{})); diff != "" { t.Fatalf("unexpected Prefix (-want +got):\n%s", diff) } if diff := cmp.Diff(want.IPNet(), got.IPNet()); diff != "" { t.Fatalf("unexpected Prefix.IPNet (-want +got):\n%s", diff) } if diff := cmp.Diff(parent, got.IPNet()); diff != "" { t.Fatalf("unexpected parent Prefix (-want +got):\n%s", diff) } if ones, bits := parent.Mask.Size(); ones != 48 || bits != 128 { t.Fatalf("parent prefix must be IPv6 /48: %q", parent) } // Iterate through subnets of the Prefix and verify each is a valid /64 // with its own subnet ID. for i := uint16(0); i < 257; i++ { sub := got.Subnet(i).IPNet() if !parent.Contains(sub.IP) { t.Fatalf("parent prefix %q does not contain child prefix %q", parent, sub) } if ones, bits := sub.Mask.Size(); ones != 64 || bits != 128 { t.Fatalf("child prefix must be IPv6 /64: %q", sub) } // Verify the subnet ID is incremented as appropriate for each subnet. id := make(net.IP, 2) binary.BigEndian.PutUint16(id, i) if diff := cmp.Diff(id, sub.IP[6:8]); diff != "" { t.Fatalf("unexpected child prefix subnet ID (-want +got):\n%s", diff) } } }