pax_global_header00006660000000000000000000000064145756131000014514gustar00rootroot0000000000000052 comment=8f23d783cef36757ad4906f11c7a0f6f273b3773 golang-github-mdlayher-ndp-1.1.0/000077500000000000000000000000001457561310000166245ustar00rootroot00000000000000golang-github-mdlayher-ndp-1.1.0/.github/000077500000000000000000000000001457561310000201645ustar00rootroot00000000000000golang-github-mdlayher-ndp-1.1.0/.github/workflows/000077500000000000000000000000001457561310000222215ustar00rootroot00000000000000golang-github-mdlayher-ndp-1.1.0/.github/workflows/static-analysis.yml000066400000000000000000000013341457561310000260550ustar00rootroot00000000000000name: Static Analysis on: push: branches: - "*" pull_request: branches: - "*" jobs: build: strategy: matrix: go-version: ["1.22"] runs-on: ubuntu-latest steps: - name: Set up Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v3 - 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-ndp-1.1.0/.github/workflows/test.yml000066400000000000000000000014711457561310000237260ustar00rootroot00000000000000name: Test on: push: branches: - "*" pull_request: branches: - "*" jobs: build: strategy: matrix: go-version: ["1.21", "1.22"] # TODO(mdlayher): tests are failing on macOS but almost all consumers of # this package are Linux. Investigate. os: [ubuntu-latest] runs-on: ${{ matrix.os }} steps: - name: Set up Go uses: actions/setup-go@v3 with: go-version: ${{ matrix.go-version }} id: go - name: Check out code into the Go module directory uses: actions/checkout@v3 - name: Run tests run: go test -race -tags gofuzz ./... - name: Build test binary run: go test -c -race - name: Run integration tests run: sudo ./ndp.test -test.v -test.run TestConn golang-github-mdlayher-ndp-1.1.0/.gitignore000066400000000000000000000000231457561310000206070ustar00rootroot00000000000000cmd/ndp/ndp *.test golang-github-mdlayher-ndp-1.1.0/CHANGELOG.md000066400000000000000000000037211457561310000204400ustar00rootroot00000000000000# CHANGELOG # v1.1.0 - [Improvement]: updated dependencies, test with Go 1.22. - [New API] [PR](https://github.com/mdlayher/ndp/pull/31): `ndp.PREF64` implements the PREF64 option as defined in RFC 8781. Thanks @jmbaur for the contribution. # v1.0.1 - [Improvement]: updated dependencies, test with Go 1.20. - [Improvement]: switch from `math/rand` to `crypto/rand` for Nonce generation. ## v1.0.0 First stable release, no API changes since v0.10.0. ## v0.10.0 - [API Change] [commit](https://github.com/mdlayher/ndp/commit/0e153112a3ae254e05f4e55afdb684da0712d5c9): `ndp.CaptivePortal` and `ndp.MTU` are now structs to allow for better extensibility. `ndp.NewCaptivePortal` now does argument validation and returns an error for various cases. `ndp.Unrestricted` is available to specify "no captive portal". - [New API] [commit](https://github.com/mdlayher/ndp/commit/7d558c930180892ed63e3213bb45bc62c71b6fa5): `ndp.Nonce` implements the NDP Nonce option as described in RFC 3971. Though this library does not implement Secure Neighbor Discovery (SEND) as of today, this option can also be used for Enhanced Duplicate Address Detection (DAD). ## v0.9.0 **This is the first release of package `ndp` that only supports Go 1.18+ due to the use of `net/netip`. Users on older versions of Go must use v0.8.0.** - [Improvement]: cut over from `net.IP` to `netip.Addr` throughout - [API Change]: drop `ndp.TestConns`; this API was awkward and didn't test actual ICMPv6 functionality. Users are encouraged to either run privileged ICMPv6 tests or to swap out `*ndp.Conn` via an interface. - [Improvement]: drop a lot of awkward test functionality related to unprivileged UDP connections to mock out ICMPv6 connections ## v0.8.0 First release of package `ndp` based on the APIs that have been stable for years with `net.IP`. **This is the first and last release of package `ndp` which supports Go 1.17 or older. Future versions will require Go 1.18 and `net/netip`.** golang-github-mdlayher-ndp-1.1.0/LICENSE.md000066400000000000000000000020631457561310000202310ustar00rootroot00000000000000# MIT License Copyright (C) 2017-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-ndp-1.1.0/README.md000066400000000000000000000040071457561310000201040ustar00rootroot00000000000000# ndp [![Test Status](https://github.com/mdlayher/ndp/workflows/Test/badge.svg)](https://github.com/mdlayher/ndp/actions) [![Go Reference](https://pkg.go.dev/badge/github.com/mdlayher/ndp.svg)](https://pkg.go.dev/github.com/mdlayher/ndp) [![Go Report Card](https://goreportcard.com/badge/github.com/mdlayher/ndp)](https://goreportcard.com/report/github.com/mdlayher/ndp) Package `ndp` implements the Neighbor Discovery Protocol, as described in [RFC 4861](https://tools.ietf.org/html/rfc4861). MIT Licensed. The command `ndp` is a utility for working with the Neighbor Discovery Protocol. To learn more about NDP, and how to use this package, check out my blog: [Network Protocol Breakdown: NDP and Go](https://mdlayher.com/blog/network-protocol-breakdown-ndp-and-go/). ## Examples Listen for incoming NDP messages on interface eth0 to one of the interface's global unicast addresses. ```none $ sudo ndp -i eth0 -a global listen $ sudo ndp -i eth0 -a 2001:db8::1 listen ```` Send router solicitations on interface eth0 from the interface's link-local address until a router advertisement is received. ```none $ sudo ndp -i eth0 -a linklocal rs ``` Send neighbor solicitations on interface eth0 to a neighbor's link-local address until a neighbor advertisement is received. ```none $ sudo ndp -i eth0 -a linklocal -t fe80::1 ns ``` An example of the tool sending a router solicitation and receiving a router advertisement on the WAN interface of a Ubiquiti router: ```none $ sudo ndp -i eth1 -a linklocal rs ndp> interface: eth1, link-layer address: 04:18:d6:a1:ce:b8, IPv6 address: fe80::618:d6ff:fea1:ceb8 ndp rs> router solicitation: - source link-layer address: 04:18:d6:a1:ce:b8 ndp rs> router advertisement from: fe80::201:5cff:fe69:f246: - hop limit: 0 - flags: [MO] - preference: 0 - router lifetime: 2h30m0s - reachable time: 1h0m0s - retransmit timer: 0s - options: - prefix information: 2600:6c4a:7002:100::/64, flags: [], valid: 720h0m0s, preferred: 168h0m0s ``` golang-github-mdlayher-ndp-1.1.0/addr.go000066400000000000000000000042051457561310000200660ustar00rootroot00000000000000package ndp import ( "fmt" "net" "net/netip" ) // An Addr is an IPv6 unicast address. type Addr string // Possible Addr types for an IPv6 unicast address. const ( Unspecified Addr = "unspecified" LinkLocal Addr = "linklocal" UniqueLocal Addr = "uniquelocal" Global Addr = "global" ) // chooseAddr selects an Addr from the interface based on the specified Addr type. func chooseAddr(addrs []net.Addr, zone string, addr Addr) (netip.Addr, error) { // Does the caller want an unspecified address? if addr == Unspecified { return netip.IPv6Unspecified().WithZone(zone), nil } // Select an IPv6 address from the interface's addresses. var match func(ip netip.Addr) bool switch addr { case LinkLocal: match = (netip.Addr).IsLinkLocalUnicast case UniqueLocal: match = (netip.Addr).IsPrivate case Global: match = func(ip netip.Addr) bool { // Specifically exclude the ULA range. return ip.IsGlobalUnicast() && !ip.IsPrivate() } default: // Special case: try to match Addr as a literal IPv6 address. ip, err := netip.ParseAddr(string(addr)) if err != nil { return netip.Addr{}, fmt.Errorf("ndp: invalid IPv6 address: %q", addr) } if err := checkIPv6(ip); err != nil { return netip.Addr{}, err } match = func(check netip.Addr) bool { return ip == check } } return findAddr(addrs, addr, zone, match) } // findAddr searches for a valid IPv6 address in the slice of net.Addr that // matches the input function. If none is found, the IPv6 unspecified address // "::" is returned. func findAddr(addrs []net.Addr, addr Addr, zone string, match func(ip netip.Addr) bool) (netip.Addr, error) { for _, a := range addrs { ipn, ok := a.(*net.IPNet) if !ok { continue } ip, ok := netip.AddrFromSlice(ipn.IP) if !ok { panicf("ndp: failed to convert net.IPNet: %v", ipn.IP) } if err := checkIPv6(ip); err != nil { continue } // From here on, we can assume that only IPv6 addresses are // being checked. if match(ip) { return ip.WithZone(zone), nil } } // No matching address on this interface. return netip.Addr{}, fmt.Errorf("ndp: address %q not found on interface %q", addr, zone) } golang-github-mdlayher-ndp-1.1.0/addr_test.go000066400000000000000000000044471457561310000211350ustar00rootroot00000000000000package ndp import ( "net" "net/netip" "testing" "github.com/google/go-cmp/cmp" ) func Test_chooseAddr(t *testing.T) { // Assumed zone for all tests. const zone = "eth0" var ( ip4 = net.IPv4(192, 168, 1, 1).To4() ip6 = mustIPv6("2001:db8::1000") gua = mustIPv6("2001:db8::1") ula = mustIPv6("fc00::1") lla = mustIPv6("fe80::1") ) addrs := []net.Addr{ // Ignore non-IP addresses. &net.TCPAddr{IP: gua}, &net.IPNet{IP: ip4}, &net.IPNet{IP: ula}, &net.IPNet{IP: lla}, // The second GUA IPv6 address should only be found when // Addr specifies it explicitly. &net.IPNet{IP: gua}, &net.IPNet{IP: ip6}, } tests := []struct { name string addrs []net.Addr addr Addr ip netip.Addr ok bool }{ { name: "empty", }, { name: "IPv4 Addr", addr: Addr(ip4.String()), }, { name: "no IPv6 addresses", addrs: []net.Addr{ &net.IPNet{ IP: ip4, }, }, addr: LinkLocal, }, { name: "ok, unspecified", ip: netip.IPv6Unspecified(), addr: Unspecified, ok: true, }, { name: "ok, GUA", addrs: addrs, ip: netip.MustParseAddr("2001:db8::1"), addr: Global, ok: true, }, { name: "ok, ULA", addrs: addrs, ip: netip.MustParseAddr("fc00::1"), addr: UniqueLocal, ok: true, }, { name: "ok, LLA", addrs: addrs, ip: netip.MustParseAddr("fe80::1"), addr: LinkLocal, ok: true, }, { name: "ok, arbitrary", addrs: addrs, ip: netip.MustParseAddr("2001:db8::1000"), addr: Addr(ip6.String()), ok: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { ipa, err := chooseAddr(tt.addrs, zone, tt.addr) if err != nil && tt.ok { t.Fatalf("unexpected error: %v", err) } if err == nil && !tt.ok { t.Fatal("expected an error, but none occurred") } if err != nil { t.Logf("OK error: %v", err) return } ttipa := tt.ip.WithZone(zone) if diff := cmp.Diff(ttipa, ipa, cmp.Comparer(addrEqual)); diff != "" { t.Fatalf("unexpected IPv6 address (-want +got):\n%s", diff) } }) } } // MustIPv6 parses s as a valid IPv6 address, or it panics. func mustIPv6(s string) net.IP { ip := net.ParseIP(s) if ip == nil || ip.To4() != nil { panicf("invalid IPv6 address: %q", s) } return ip } golang-github-mdlayher-ndp-1.1.0/cmd/000077500000000000000000000000001457561310000173675ustar00rootroot00000000000000golang-github-mdlayher-ndp-1.1.0/cmd/ndp/000077500000000000000000000000001457561310000201505ustar00rootroot00000000000000golang-github-mdlayher-ndp-1.1.0/cmd/ndp/main.go000066400000000000000000000074611457561310000214330ustar00rootroot00000000000000// Command ndp is a utility for working with the Neighbor Discovery Protocol. package main import ( "context" "errors" "flag" "fmt" "log" "net" "net/netip" "os" "os/signal" "github.com/mdlayher/ndp" "github.com/mdlayher/ndp/internal/ndpcmd" ) func main() { var ( ifiFlag = flag.String("i", "", "network interface to use for NDP communication (default: automatic)") addrFlag = flag.String("a", string(ndp.LinkLocal), "address to use for NDP communication (unspecified, linklocal, uniquelocal, global, or a literal IPv6 address)") targetFlag = flag.String("t", "", "IPv6 target address for neighbor solicitation NDP messages") ) flag.Usage = func() { fmt.Println(usage) fmt.Println("Flags:") flag.PrintDefaults() } flag.Parse() ll := log.New(os.Stderr, "ndp> ", 0) if flag.NArg() > 1 { ll.Fatalf("too many args on command line: %v", flag.Args()[1:]) } ifi, err := findInterface(*ifiFlag) if err != nil { ll.Fatalf("failed to get interface: %v", err) } c, ip, err := ndp.Listen(ifi, ndp.Addr(*addrFlag)) if err != nil { ll.Fatalf("failed to open NDP connection: %v", err) } defer c.Close() var target netip.Addr if t := *targetFlag; t != "" { target, err = netip.ParseAddr(t) if err != nil { ll.Fatalf("failed to parse IPv6 target address: %v", err) } } sigC := make(chan os.Signal, 1) signal.Notify(sigC, os.Interrupt) ctx, cancel := context.WithCancel(context.Background()) defer cancel() go func() { <-sigC cancel() }() // Non-Ethernet interfaces (such as PPPoE) may not have a MAC address. var mac string if ifi.HardwareAddr != nil { mac = ifi.HardwareAddr.String() } else { mac = "none" } ll.Printf("interface: %s, link-layer address: %s, IPv6 address: %s", ifi.Name, mac, ip) if err := ndpcmd.Run(ctx, c, ifi, flag.Arg(0), target); err != nil { // Context cancel means a signal was sent, so no need to log an error. if err == context.Canceled { os.Exit(1) } ll.Fatal(err) } } // findInterface attempts to find the specified interface. If name is empty, // it attempts to find a usable, up and ready, network interface. func findInterface(name string) (*net.Interface, error) { if name != "" { ifi, err := net.InterfaceByName(name) if err != nil { return nil, fmt.Errorf("could not find interface %q: %v", name, err) } return ifi, nil } ifis, err := net.Interfaces() if err != nil { return nil, err } for _, ifi := range ifis { // Is the interface up, multicast, and not a loopback? if ifi.Flags&net.FlagUp == 0 || ifi.Flags&net.FlagMulticast == 0 || ifi.Flags&net.FlagLoopback != 0 { continue } // Does the interface have an IPv6 address assigned? addrs, err := ifi.Addrs() if err != nil { return nil, err } for _, a := range addrs { ipNet, ok := a.(*net.IPNet) if !ok { continue } ip, ok := netip.AddrFromSlice(ipNet.IP) if !ok { panicf("failed to parse IPv6 address: %q", ipNet.IP) } // Is this address an IPv6 address? if ip.Is6() && !ip.Is4In6() { return &ifi, nil } } } return nil, errors.New("could not find a usable IPv6-enabled interface") } const usage = `ndp: utility for working with the Neighbor Discovery Protocol. By default, this tool will automatically bind to IPv6 link-local address of the first interface which is capable of using NDP. To enable more convenient use without sudo on Linux, apply the CAP_NET_RAW capability: $ sudo setcap cap_net_raw+ep ./ndp Examples: Listen for incoming NDP messages on the default interface. $ ndp Send router solicitations on the default interface until a router advertisement is received. $ ndp rs Send neighbor solicitations on the default interface until a neighbor advertisement is received. $ ndp -t fe80::1 ns` func panicf(format string, a ...any) { panic(fmt.Sprintf(format, a...)) } golang-github-mdlayher-ndp-1.1.0/conn.go000066400000000000000000000153741457561310000201220ustar00rootroot00000000000000package ndp import ( "errors" "fmt" "net" "net/netip" "runtime" "time" "golang.org/x/net/icmp" "golang.org/x/net/ipv6" ) // HopLimit is the expected IPv6 hop limit for all NDP messages. const HopLimit = 255 // A Conn is a Neighbor Discovery Protocol connection. type Conn struct { pc *ipv6.PacketConn cm *ipv6.ControlMessage ifi *net.Interface addr netip.Addr // icmpTest disables the self-filtering mechanism in ReadFrom. icmpTest bool } // Listen creates a NDP connection using the specified interface and address // type. // // As a special case, literal IPv6 addresses may be specified to bind to a // specific address for an interface. If the IPv6 address does not exist on the // interface, an error will be returned. // // Listen returns a Conn and the chosen IPv6 address of the interface. func Listen(ifi *net.Interface, addr Addr) (*Conn, netip.Addr, error) { addrs, err := ifi.Addrs() if err != nil { return nil, netip.Addr{}, err } ip, err := chooseAddr(addrs, ifi.Name, addr) if err != nil { return nil, netip.Addr{}, err } ic, err := icmp.ListenPacket("ip6:ipv6-icmp", ip.String()) if err != nil { return nil, netip.Addr{}, err } pc := ic.IPv6PacketConn() // Hop limit is always 255, per RFC 4861. if err := pc.SetHopLimit(HopLimit); err != nil { return nil, netip.Addr{}, err } if err := pc.SetMulticastHopLimit(HopLimit); err != nil { return nil, netip.Addr{}, err } if runtime.GOOS != "windows" { // Calculate and place ICMPv6 checksum at correct offset in all // messages (not implemented by golang.org/x/net/ipv6 on Windows). const chkOff = 2 if err := pc.SetChecksum(true, chkOff); err != nil { return nil, netip.Addr{}, err } } return newConn(pc, ip, ifi) } // newConn is an internal test constructor used for creating a Conn from an // arbitrary ipv6.PacketConn. func newConn(pc *ipv6.PacketConn, src netip.Addr, ifi *net.Interface) (*Conn, netip.Addr, error) { c := &Conn{ pc: pc, // The default control message used when none is specified. cm: &ipv6.ControlMessage{ HopLimit: HopLimit, Src: src.AsSlice(), IfIndex: ifi.Index, }, ifi: ifi, addr: src, } return c, src, nil } // Close closes the Conn's underlying connection. func (c *Conn) Close() error { return c.pc.Close() } // SetDeadline sets the read and write deadlines for Conn. It is // equivalent to calling both SetReadDeadline and SetWriteDeadline. func (c *Conn) SetDeadline(t time.Time) error { return c.pc.SetDeadline(t) } // SetReadDeadline sets a deadline for the next NDP message to arrive. func (c *Conn) SetReadDeadline(t time.Time) error { return c.pc.SetReadDeadline(t) } // SetWriteDeadline sets a deadline for the next NDP message to be written. func (c *Conn) SetWriteDeadline(t time.Time) error { return c.pc.SetWriteDeadline(t) } // JoinGroup joins the specified multicast group. If group contains an IPv6 // zone, it is overwritten by the zone of the network interface which backs // Conn. func (c *Conn) JoinGroup(group netip.Addr) error { return c.pc.JoinGroup(c.ifi, &net.IPAddr{ IP: group.AsSlice(), Zone: c.ifi.Name, }) } // LeaveGroup leaves the specified multicast group. If group contains an IPv6 // zone, it is overwritten by the zone of the network interface which backs // Conn. func (c *Conn) LeaveGroup(group netip.Addr) error { return c.pc.LeaveGroup(c.ifi, &net.IPAddr{ IP: group.AsSlice(), Zone: c.ifi.Name, }) } // SetICMPFilter applies the specified ICMP filter. This option can be used // to ensure a Conn only accepts certain kinds of NDP messages. func (c *Conn) SetICMPFilter(f *ipv6.ICMPFilter) error { return c.pc.SetICMPFilter(f) } // SetControlMessage enables the reception of *ipv6.ControlMessages based on // the specified flags. func (c *Conn) SetControlMessage(cf ipv6.ControlFlags, on bool) error { return c.pc.SetControlMessage(cf, on) } // ReadFrom reads a Message from the Conn and returns its control message and // source network address. Messages sourced from this machine and malformed or // unrecognized ICMPv6 messages are filtered. // // If more control and/or a more efficient low-level API are required, see // ReadRaw. func (c *Conn) ReadFrom() (Message, *ipv6.ControlMessage, netip.Addr, error) { b := make([]byte, c.ifi.MTU) for { n, cm, ip, err := c.ReadRaw(b) if err != nil { return nil, nil, netip.Addr{}, err } // Filter if this address sent this message, but allow toggling that // behavior in tests. if !c.icmpTest && ip == c.addr { continue } m, err := ParseMessage(b[:n]) if err != nil { // Filter parsing errors on the caller's behalf. if errors.Is(err, errParseMessage) { continue } return nil, nil, netip.Addr{}, err } return m, cm, ip, nil } } // ReadRaw reads ICMPv6 message bytes into b from the Conn and returns the // number of bytes read, the control message, and the source network address. // // Most callers should use ReadFrom instead, which parses bytes into Messages // and also handles malformed and unrecognized ICMPv6 messages. func (c *Conn) ReadRaw(b []byte) (int, *ipv6.ControlMessage, netip.Addr, error) { n, cm, src, err := c.pc.ReadFrom(b) if err != nil { return n, nil, netip.Addr{}, err } // We fully control the underlying ipv6.PacketConn, so panic if the // conversions fail. ip, ok := netip.AddrFromSlice(src.(*net.IPAddr).IP) if !ok { panicf("ndp: invalid source IP address: %s", src) } // Always apply the IPv6 zone of this interface. return n, cm, ip.WithZone(c.ifi.Name), nil } // WriteTo writes a Message to the Conn, with an optional control message and // destination network address. If dst contains an IPv6 zone, it is overwritten // by the zone of the network interface which backs Conn. // // If cm is nil, a default control message will be sent. func (c *Conn) WriteTo(m Message, cm *ipv6.ControlMessage, dst netip.Addr) error { b, err := MarshalMessage(m) if err != nil { return err } return c.writeRaw(b, cm, dst) } // writeRaw allows writing raw bytes with a Conn. func (c *Conn) writeRaw(b []byte, cm *ipv6.ControlMessage, dst netip.Addr) error { // Set reasonable defaults if control message is nil. if cm == nil { cm = c.cm } _, err := c.pc.WriteTo(b, cm, &net.IPAddr{ IP: dst.AsSlice(), Zone: c.ifi.Name, }) return err } // SolicitedNodeMulticast returns the solicited-node multicast address for // an IPv6 address. func SolicitedNodeMulticast(ip netip.Addr) (netip.Addr, error) { if err := checkIPv6(ip); err != nil { return netip.Addr{}, err } // Fixed prefix, and low 24 bits taken from input address. var ( // ff02::1:ff00:0/104 snm = [16]byte{0: 0xff, 1: 0x02, 11: 0x01, 12: 0xff} ips = ip.As16() ) for i := 13; i < 16; i++ { snm[i] = ips[i] } return netip.AddrFrom16(snm), nil } func panicf(format string, a ...any) { panic(fmt.Sprintf(format, a...)) } golang-github-mdlayher-ndp-1.1.0/conn_setup_test.go000066400000000000000000000032031457561310000223650ustar00rootroot00000000000000package ndp import ( "errors" "net" "net/netip" "os" "testing" ) func testICMPConn(t *testing.T) (*Conn, *Conn, netip.Addr) { t.Helper() ifi := testInterface(t) // Create two ICMPv6 connections that will communicate with each other. c1, addr := icmpConn(t, ifi) c2, _ := icmpConn(t, ifi) t.Cleanup(func() { _ = c1.Close() _ = c2.Close() }) return c1, c2, addr } func icmpConn(t *testing.T, ifi *net.Interface) (*Conn, netip.Addr) { t.Helper() // Wire up a standard ICMPv6 NDP connection. c, addr, err := Listen(ifi, LinkLocal) if err != nil { if !errors.Is(err, os.ErrPermission) { t.Fatalf("failed to dial NDP: %v", err) } t.Skipf("skipping, permission denied, cannot test ICMPv6 NDP: %v", err) } c.icmpTest = true return c, addr } func testInterface(t *testing.T) *net.Interface { t.Helper() ifis, err := net.Interfaces() if err != nil { t.Fatalf("failed to get interfaces: %v", err) } for _, ifi := range ifis { // Is the interface up and not a loopback? if ifi.Flags&net.FlagUp != 1 || ifi.Flags&net.FlagLoopback != 0 { continue } // Does the interface have an IPv6 address assigned? addrs, err := ifi.Addrs() if err != nil { t.Fatalf("failed to get interface %q addresses: %v", ifi.Name, err) } for _, a := range addrs { ipNet, ok := a.(*net.IPNet) if !ok { continue } ip, ok := netip.AddrFromSlice(ipNet.IP) if !ok { t.Fatalf("failed to parse IPv6 address: %v", ipNet.IP) } // Is this address an IPv6 address? if ip.Is6() && !ip.Is4In6() { return &ifi } } } t.Skip("skipping, could not find a usable IPv6-enabled interface") return nil } golang-github-mdlayher-ndp-1.1.0/conn_test.go000066400000000000000000000101131457561310000211430ustar00rootroot00000000000000package ndp import ( "bytes" "errors" "net" "net/netip" "sync" "testing" "time" "github.com/google/go-cmp/cmp" ) func TestConn(t *testing.T) { tests := []struct { name string fn func(t *testing.T, c1, c2 *Conn, addr netip.Addr) }{ { name: "echo", fn: testConnEcho, }, { name: "filter invalid", fn: testConnFilterInvalid, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { c1, c2, addr := testICMPConn(t) tt.fn(t, c1, c2, addr) }) } } func testConnEcho(t *testing.T, c1, c2 *Conn, addr netip.Addr) { // Echo this message between two connections. rs := &RouterSolicitation{} var wg sync.WaitGroup wg.Add(1) go func() { defer wg.Done() // Read and bounce the message back to the second Conn. m, _, _, err := c2.ReadFrom() if err != nil { panicf("failed to read from c2: %v", err) } if err := c2.WriteTo(m, nil, addr); err != nil { panicf("failed to write from c2: %v", err) } }() if err := c1.WriteTo(rs, nil, addr); err != nil { t.Fatalf("failed to write from c1: %v", err) } m, _, _, err := c1.ReadFrom() if err != nil { t.Fatalf("failed to read from c1: %v", err) } wg.Wait() if diff := cmp.Diff(rs, m); diff != "" { t.Fatalf("unexpected message (-want +got):\n%s", diff) } } func testConnFilterInvalid(t *testing.T, c1, c2 *Conn, addr netip.Addr) { // Echo this message between two connections. rs := &RouterSolicitation{} var wg sync.WaitGroup wg.Add(1) sigC := make(chan struct{}) go func() { defer wg.Done() // Wait for the caller to send us a message, then send: // - invalid message (filtered) // - valid message // And finally force a timeout to verify the ReadFrom error check. m, _, _, err := c2.ReadFrom() if err != nil { panicf("failed to read from c2: %v", err) } if err := c2.writeRaw(bytes.Repeat([]byte{0xff}, 255), nil, addr); err != nil { panicf("failed to write invalid from c2: %v", err) } // Write in lockstep and wait for the consumer to acknowledge the write. if err := c2.WriteTo(m, nil, addr); err != nil { panicf("failed to write valid from c2: %v", err) } <-sigC if err := c1.SetReadDeadline(time.Unix(1, 0)); err != nil { panicf("failed to interrupt c1: %v", err) } <-sigC }() if err := c1.WriteTo(rs, nil, addr); err != nil { t.Fatalf("failed to write from c1: %v", err) } var m Message for i := 0; i < 2; i++ { // Acknowledge each write from the other Conn. msg, _, _, err := c1.ReadFrom() sigC <- struct{}{} if err == nil { m = msg continue } switch i { case 0: t.Fatalf("failed to read from c1: %v", err) case 1: var nerr net.Error if !errors.As(err, &nerr) { t.Fatalf("error is not net.Error: %v", err) } if !nerr.Timeout() { t.Fatal("error did not indicate a timeout") } default: panic("too many loop iterations") } } wg.Wait() if diff := cmp.Diff(rs, m); diff != "" { t.Fatalf("unexpected message (-want +got):\n%s", diff) } } func TestSolicitedNodeMulticast(t *testing.T) { tests := []struct { name string ip netip.Addr snm netip.Addr ok bool }{ { name: "bad, IPv4", ip: netip.MustParseAddr("192.168.1.1"), }, { name: "ok, link-local", ip: netip.MustParseAddr("fe80::1234:5678"), snm: netip.MustParseAddr("ff02::1:ff34:5678"), ok: true, }, { name: "ok, global", ip: netip.MustParseAddr("2001:db8::dead:beef"), snm: netip.MustParseAddr("ff02::1:ffad:beef"), ok: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { snm, err := SolicitedNodeMulticast(tt.ip) if err != nil && tt.ok { t.Fatalf("unexpected error: %v", err) } if err == nil && !tt.ok { t.Fatal("expected an error, but none occurred") } if err != nil { t.Logf("OK error: %v", err) return } if diff := cmp.Diff(tt.snm, snm, cmp.Comparer(addrEqual)); diff != "" { t.Fatalf("unexpected solicited-node multicast address (-want +got):\n%s", diff) } }) } } func addrEqual(x, y netip.Addr) bool { return x == y } func prefixEqual(x, y netip.Prefix) bool { return x == y } golang-github-mdlayher-ndp-1.1.0/doc.go000066400000000000000000000002371457561310000177220ustar00rootroot00000000000000// Package ndp implements the Neighbor Discovery Protocol, as described in // RFC 4861. package ndp //go:generate stringer -type=Preference -output=string.go golang-github-mdlayher-ndp-1.1.0/fuzz.go000066400000000000000000000006511457561310000201530ustar00rootroot00000000000000package ndp import ( "fmt" ) // fuzz is a shared function for go-fuzz and tests that verify go-fuzz bugs // are fixed. func fuzz(data []byte) int { m, err := ParseMessage(data) if err != nil { return 0 } b2, err := MarshalMessage(m) if err != nil { panic(fmt.Sprintf("failed to marshal: %v", err)) } if _, err := ParseMessage(b2); err != nil { panic(fmt.Sprintf("failed to parse: %v", err)) } return 1 } golang-github-mdlayher-ndp-1.1.0/fuzz_test.go000066400000000000000000000042521457561310000212130ustar00rootroot00000000000000package ndp import "testing" func Test_fuzz(t *testing.T) { tests := []struct { name string s string }{ { name: "parse option length", s: "\x86000000000000000\x01\xc0", }, { name: "prefix information length", s: "\x86000000000000000\x03\x0100" + "0000", }, { name: "raw option marshal symmetry", s: "\x860000000000000000!00" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000", }, { name: "rdnss no servers", s: "\x850000000\x19\x01000000", }, { name: "dnssl bad domain", s: "\x850000000\x1f\x02000000\x02.0\x01" + "0\x00\x000", }, { name: "dnssl length padding", s: "\x86000000000000000\x1f\b00" + "0000\x010\x010\x010\x1d000000000" + "00000000000000000000" + "\x010\x010\x00\x0000000000000000", }, { name: "dnssl early termination no padding", s: "\x850000000\x1f\f000000\x010\x010" + "\x0200\x00\x00000000000000000" + "00000000000000000000" + "00000000000000000000" + "00000000000000000000" + "0000", }, { name: "dnssl early termination one pad null", s: "\x850000000\x1f\a000000\x0200\x00" + "\t000000000\x00\x0000000000" + "00000000000000000000" + "0000", }, { name: "dnssl punycode empty string", s: "\x850000000\x1f\x02000000\x04xn-" + "-\x00\x000", }, { name: "dnssl with spaces", s: "\x850000000\x1f\x03000000\x05." + "00\x010\x0500000\x00\x00", }, { name: "dnssl not ASCII", s: "\x850000000\x1f\x02000000\x06." + "000\x00", }, { name: "dnssl decodes to empty string", s: "\x850000000\x1f\x02000000\x04xn-" + "-\x010\x00", }, { name: "dnssl unicode replacement character", s: "\x850000000\x1f\x04000000\x010\x020" + "0\x0exn---00000H00F\x01@\x00\x00", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _ = fuzz([]byte(tt.s)) }) } } golang-github-mdlayher-ndp-1.1.0/go.mod000066400000000000000000000003121457561310000177260ustar00rootroot00000000000000module github.com/mdlayher/ndp go 1.20 require ( github.com/google/go-cmp v0.6.0 golang.org/x/net v0.22.0 ) require ( golang.org/x/sys v0.18.0 // indirect golang.org/x/text v0.14.0 // indirect ) golang-github-mdlayher-ndp-1.1.0/go.sum000066400000000000000000000011641457561310000177610ustar00rootroot00000000000000github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= golang.org/x/net v0.22.0 h1:9sGLhx7iRIHEiX0oAJ3MRZMUCElJgy7Br1nO+AMN3Tc= golang.org/x/net v0.22.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang-github-mdlayher-ndp-1.1.0/gofuzz.go000066400000000000000000000001431457561310000204750ustar00rootroot00000000000000//go:build gofuzz // +build gofuzz package ndp func Fuzz(data []byte) int { return fuzz(data) } golang-github-mdlayher-ndp-1.1.0/internal/000077500000000000000000000000001457561310000204405ustar00rootroot00000000000000golang-github-mdlayher-ndp-1.1.0/internal/ndpcmd/000077500000000000000000000000001457561310000217055ustar00rootroot00000000000000golang-github-mdlayher-ndp-1.1.0/internal/ndpcmd/print.go000066400000000000000000000106411457561310000233720ustar00rootroot00000000000000package ndpcmd import ( "fmt" "io" "log" "net/netip" "strings" "github.com/mdlayher/ndp" ) func printMessage(ll *log.Logger, m ndp.Message, from netip.Addr) { switch m := m.(type) { case *ndp.NeighborAdvertisement: printNA(ll, m, from) case *ndp.NeighborSolicitation: printNS(ll, m, from) case *ndp.RouterAdvertisement: printRA(ll, m, from) case *ndp.RouterSolicitation: printRS(ll, m, from) default: ll.Printf("%s %#v", from, m) } } func printRA(ll *log.Logger, ra *ndp.RouterAdvertisement, from netip.Addr) { var flags []string if ra.ManagedConfiguration { flags = append(flags, "managed") } if ra.OtherConfiguration { flags = append(flags, "other") } if ra.MobileIPv6HomeAgent { flags = append(flags, "mobile") } if ra.NeighborDiscoveryProxy { flags = append(flags, "proxy") } var s strings.Builder writef(&s, "router advertisement from: %s:\n", from) if ra.CurrentHopLimit > 0 { writef(&s, " - hop limit: %d\n", ra.CurrentHopLimit) } if len(flags) > 0 { writef(&s, " - flags: [%s]\n", strings.Join(flags, ", ")) } writef(&s, " - preference: %s\n", ra.RouterSelectionPreference) if ra.RouterLifetime > 0 { writef(&s, " - router lifetime: %s\n", ra.RouterLifetime) } if ra.ReachableTime != 0 { writef(&s, " - reachable time: %s\n", ra.ReachableTime) } if ra.RetransmitTimer != 0 { writef(&s, " - retransmit timer: %s\n", ra.RetransmitTimer) } _, _ = s.WriteString(optionsString(ra.Options)) ll.Print(s.String()) } func printRS(ll *log.Logger, rs *ndp.RouterSolicitation, from netip.Addr) { s := fmt.Sprintf( rsFormat, from.String(), ) ll.Print(s + optionsString(rs.Options)) } const rsFormat = "router solicitation from %s:\n" func printNA(ll *log.Logger, na *ndp.NeighborAdvertisement, from netip.Addr) { s := fmt.Sprintf( naFormat, from.String(), na.Router, na.Solicited, na.Override, na.TargetAddress.String(), ) ll.Print(s + optionsString(na.Options)) } const naFormat = `neighbor advertisement from %s: - router: %t - solicited: %t - override: %t - target address: %s ` func printNS(ll *log.Logger, ns *ndp.NeighborSolicitation, from netip.Addr) { s := fmt.Sprintf( nsFormat, from.String(), ns.TargetAddress.String(), ) ll.Print(s + optionsString(ns.Options)) } const nsFormat = `neighbor solicitation from %s: - target address: %s ` func optionsString(options []ndp.Option) string { if len(options) == 0 { return "" } var s strings.Builder s.WriteString(" - options:\n") for _, o := range options { writef(&s, " - %s\n", optStr(o)) } return s.String() } func optStr(o ndp.Option) string { switch o := o.(type) { case *ndp.LinkLayerAddress: dir := "source" if o.Direction == ndp.Target { dir = "target" } return fmt.Sprintf("%s link-layer address: %s", dir, o.Addr.String()) case *ndp.MTU: return fmt.Sprintf("MTU: %d", o.MTU) case *ndp.PrefixInformation: var flags []string if o.OnLink { flags = append(flags, "on-link") } if o.AutonomousAddressConfiguration { flags = append(flags, "autonomous") } return fmt.Sprintf("prefix information: %s/%d, flags: [%s], valid: %s, preferred: %s", o.Prefix.String(), o.PrefixLength, strings.Join(flags, ", "), o.ValidLifetime, o.PreferredLifetime, ) case *ndp.RawOption: return fmt.Sprintf("type: %03d, value: %v", o.Type, o.Value) case *ndp.RouteInformation: return fmt.Sprintf("route information: %s/%d, preference: %s, lifetime: %s", o.Prefix.String(), o.PrefixLength, o.Preference.String(), o.RouteLifetime, ) case *ndp.RecursiveDNSServer: var ss []string for _, s := range o.Servers { ss = append(ss, s.String()) } servers := strings.Join(ss, ", ") return fmt.Sprintf("recursive DNS servers: lifetime: %s, servers: %s", o.Lifetime, servers) case *ndp.RAFlagsExtension: return fmt.Sprintf("RA flags extension: [%# 02x]", o.Flags) case *ndp.DNSSearchList: return fmt.Sprintf("DNS search list: lifetime: %s, domain names: %s", o.Lifetime, strings.Join(o.DomainNames, ", ")) case *ndp.CaptivePortal: return fmt.Sprintf("captive portal: %s", o.URI) case *ndp.PREF64: return fmt.Sprintf("pref64: %s, lifetime: %s", o.Prefix, o.Lifetime) case *ndp.Nonce: return fmt.Sprintf("nonce: %s", o) default: panic(fmt.Sprintf("unrecognized option: %v", o)) } } func writef(sw io.StringWriter, format string, a ...any) { _, _ = sw.WriteString(fmt.Sprintf(format, a...)) } golang-github-mdlayher-ndp-1.1.0/internal/ndpcmd/run.go000066400000000000000000000063271457561310000230500ustar00rootroot00000000000000// Package ndpcmd provides the commands for the ndp utility. package ndpcmd import ( "context" "errors" "fmt" "log" "net" "net/netip" "os" "github.com/mdlayher/ndp" ) var errTargetOp = errors.New("flag '-t' is only valid for neighbor solicitation operation") // Run runs the ndp utility. func Run( ctx context.Context, c *ndp.Conn, ifi *net.Interface, op string, target netip.Addr, ) error { if op != "ns" && target.IsValid() { return errTargetOp } switch op { // listen is the default when no op is specified. case "listen", "": return listen(ctx, c) case "ns": return sendNS(ctx, c, ifi.HardwareAddr, target) case "rs": return sendRS(ctx, c, ifi.HardwareAddr) default: return fmt.Errorf("unrecognized operation: %q", op) } } func listen(ctx context.Context, c *ndp.Conn) error { ll := log.New(os.Stderr, "ndp listen> ", 0) ll.Println("listening for messages") // Also listen for router solicitations from other hosts, even though we // will never reply to them. if err := c.JoinGroup(netip.MustParseAddr("ff02::2")); err != nil { return err } // No filtering, print all messages. if err := receiveLoop(ctx, c, ll, nil, nil); err != nil { return fmt.Errorf("failed to read message: %v", err) } return nil } func sendNS(ctx context.Context, c *ndp.Conn, addr net.HardwareAddr, target netip.Addr) error { ll := log.New(os.Stderr, "ndp ns> ", 0) ll.Printf("neighbor solicitation:\n - source link-layer address: %s", addr.String()) // Always multicast the message to the target's solicited-node multicast // group as if we have no knowledge of its MAC address. snm, err := ndp.SolicitedNodeMulticast(target) if err != nil { return fmt.Errorf("failed to determine solicited-node multicast address: %v", err) } m := &ndp.NeighborSolicitation{ TargetAddress: target, Options: []ndp.Option{ &ndp.LinkLayerAddress{ Direction: ndp.Source, Addr: addr, }, }, } // Expect neighbor advertisement messages with the correct target address. check := func(m ndp.Message) bool { na, ok := m.(*ndp.NeighborAdvertisement) if !ok { return false } return na.TargetAddress == target } if err := sendReceiveLoop(ctx, c, ll, m, snm, check); err != nil { if err == context.Canceled { return err } return fmt.Errorf("failed to send neighbor solicitation: %v", err) } return nil } func sendRS(ctx context.Context, c *ndp.Conn, addr net.HardwareAddr) error { ll := log.New(os.Stderr, "ndp rs> ", 0) // Non-Ethernet interfaces (such as PPPoE) may not have a MAC address, so // optionally set the source LLA option if addr is set. m := &ndp.RouterSolicitation{} msg := "router solicitation:" if addr != nil { msg += fmt.Sprintf("\n - source link-layer address: %s", addr.String()) m.Options = append(m.Options, &ndp.LinkLayerAddress{ Direction: ndp.Source, Addr: addr, }) } ll.Println(msg) // Expect any router advertisement message. check := func(m ndp.Message) bool { _, ok := m.(*ndp.RouterAdvertisement) return ok } if err := sendReceiveLoop(ctx, c, ll, m, netip.MustParseAddr("ff02::2"), check); err != nil { if err == context.Canceled { return err } return fmt.Errorf("failed to send router solicitation: %v", err) } return nil } golang-github-mdlayher-ndp-1.1.0/internal/ndpcmd/transfer.go000066400000000000000000000045401457561310000240630ustar00rootroot00000000000000package ndpcmd import ( "context" "errors" "fmt" "log" "net" "net/netip" "time" "github.com/mdlayher/ndp" ) func sendReceiveLoop( ctx context.Context, c *ndp.Conn, ll *log.Logger, m ndp.Message, dst netip.Addr, check func(m ndp.Message) bool, ) error { for i := 0; ; i++ { msg, from, err := sendReceive(ctx, c, m, dst, check) switch err { case context.Canceled: fmt.Println() ll.Printf("canceled, sent %d message(s)", i+1) return err case errRetry: fmt.Print(".") continue case nil: fmt.Println() printMessage(ll, msg, from) return nil default: return err } } } func receiveLoop( ctx context.Context, c *ndp.Conn, ll *log.Logger, check func(m ndp.Message) bool, recv func(ll *log.Logger, msg ndp.Message, from netip.Addr), ) error { if recv == nil { recv = printMessage } var count int for { msg, from, err := receive(ctx, c, check) switch err { case context.Canceled: ll.Printf("received %d message(s)", count) return nil case errRetry: continue case nil: count++ recv(ll, msg, from) default: return err } } } var errRetry = errors.New("retry") func sendReceive( ctx context.Context, c *ndp.Conn, m ndp.Message, dst netip.Addr, check func(m ndp.Message) bool, ) (ndp.Message, netip.Addr, error) { if err := c.WriteTo(m, nil, dst); err != nil { return nil, netip.Addr{}, fmt.Errorf("failed to write message: %v", err) } return receive(ctx, c, check) } func receive( ctx context.Context, c *ndp.Conn, check func(m ndp.Message) bool, ) (ndp.Message, netip.Addr, error) { if err := c.SetReadDeadline(time.Now().Add(1 * time.Second)); err != nil { return nil, netip.Addr{}, fmt.Errorf("failed to set deadline: %v", err) } msg, _, from, err := c.ReadFrom() if err == nil { if check != nil && !check(msg) { // Read a message, but it isn't the one we want. Keep trying. return nil, netip.Addr{}, errRetry } // Got a message that passed the check, if check was not nil. return msg, from, nil } // Was the context canceled already? select { case <-ctx.Done(): return nil, netip.Addr{}, ctx.Err() default: } // Was the error caused by a read timeout, and should the loop continue? if nerr, ok := err.(net.Error); ok && nerr.Timeout() { return nil, netip.Addr{}, errRetry } return nil, netip.Addr{}, fmt.Errorf("failed to read message: %v", err) } golang-github-mdlayher-ndp-1.1.0/internal/ndptest/000077500000000000000000000000001457561310000221215ustar00rootroot00000000000000golang-github-mdlayher-ndp-1.1.0/internal/ndptest/ndptest.go000066400000000000000000000012111457561310000241240ustar00rootroot00000000000000// Package ndptest provides test functions and types for package ndp. package ndptest import ( "bytes" "net" "net/netip" ) // Shared test data for commonly needed data types. var ( Prefix = netip.MustParseAddr("2001:db8::") IP = netip.MustParseAddr("2001:db8::1") MAC = net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad} ) // Merge merges a slice of byte slices into a single, contiguous slice. func Merge(bs [][]byte) []byte { var b []byte for _, bb := range bs { b = append(b, bb...) } return b } // Zero returns a byte slice of size n filled with zeros. func Zero(n int) []byte { return bytes.Repeat([]byte{0x00}, n) } golang-github-mdlayher-ndp-1.1.0/message.go000066400000000000000000000231651457561310000206060ustar00rootroot00000000000000package ndp import ( "encoding/binary" "errors" "fmt" "io" "net/netip" "time" "golang.org/x/net/icmp" "golang.org/x/net/ipv6" ) const ( // Length of an ICMPv6 header. icmpLen = 4 // Minimum byte length values for each type of valid Message. naLen = 20 nsLen = 20 raLen = 12 rsLen = 4 ) // A Message is a Neighbor Discovery Protocol message. type Message interface { // Type specifies the ICMPv6 type for a Message. Type() ipv6.ICMPType // Called via MarshalMessage and ParseMessage. marshal() ([]byte, error) unmarshal(b []byte) error } func marshalMessage(m Message, psh []byte) ([]byte, error) { mb, err := m.marshal() if err != nil { return nil, err } im := icmp.Message{ Type: m.Type(), // Always zero. Code: 0, // Calculated by caller or OS. Checksum: 0, Body: &icmp.RawBody{ Data: mb, }, } return im.Marshal(psh) } // MarshalMessage marshals a Message into its binary form and prepends an // ICMPv6 message with the correct type. // // It is assumed that the operating system or caller will calculate and place // the ICMPv6 checksum in the result. func MarshalMessage(m Message) ([]byte, error) { // Pseudo-header always nil so checksum is calculated by caller or OS. return marshalMessage(m, nil) } // MarshalMessageChecksum marshals a Message into its binary form and prepends // an ICMPv6 message with the correct type. // // The source and destination IP addresses are used to compute an IPv6 pseudo // header for checksum calculation. func MarshalMessageChecksum(m Message, source, destination netip.Addr) ([]byte, error) { return marshalMessage( m, icmp.IPv6PseudoHeader(source.AsSlice(), destination.AsSlice()), ) } // errParseMessage is a sentinel which indicates an error from ParseMessage. var errParseMessage = errors.New("failed to parse message") // ParseMessage parses a Message from its binary form after determining its // type from a leading ICMPv6 message. func ParseMessage(b []byte) (Message, error) { if len(b) < icmpLen { return nil, fmt.Errorf("ndp: ICMPv6 message too short: %w", errParseMessage) } // TODO(mdlayher): verify checksum? var m Message t := ipv6.ICMPType(b[0]) switch t { case ipv6.ICMPTypeNeighborAdvertisement: m = new(NeighborAdvertisement) case ipv6.ICMPTypeNeighborSolicitation: m = new(NeighborSolicitation) case ipv6.ICMPTypeRouterAdvertisement: m = new(RouterAdvertisement) case ipv6.ICMPTypeRouterSolicitation: m = new(RouterSolicitation) default: return nil, fmt.Errorf("ndp: unrecognized ICMPv6 type %d: %w", t, errParseMessage) } if err := m.unmarshal(b[icmpLen:]); err != nil { return nil, fmt.Errorf("ndp: failed to unmarshal %s: %w", t, errParseMessage) } return m, nil } var _ Message = &NeighborAdvertisement{} // A NeighborAdvertisement is a Neighbor Advertisement message as // described in RFC 4861, Section 4.4. type NeighborAdvertisement struct { Router bool Solicited bool Override bool TargetAddress netip.Addr Options []Option } // Type implements Message. func (na *NeighborAdvertisement) Type() ipv6.ICMPType { return ipv6.ICMPTypeNeighborAdvertisement } func (na *NeighborAdvertisement) marshal() ([]byte, error) { if err := checkIPv6(na.TargetAddress); err != nil { return nil, err } b := make([]byte, naLen) if na.Router { b[0] |= (1 << 7) } if na.Solicited { b[0] |= (1 << 6) } if na.Override { b[0] |= (1 << 5) } copy(b[4:], na.TargetAddress.AsSlice()) ob, err := marshalOptions(na.Options) if err != nil { return nil, err } b = append(b, ob...) return b, nil } func (na *NeighborAdvertisement) unmarshal(b []byte) error { if len(b) < naLen { return io.ErrUnexpectedEOF } // Skip flags and reserved area. addr := b[4:naLen] target, ok := netip.AddrFromSlice(addr) if !ok { panicf("ndp: invalid IPv6 address slice: %v", addr) } if err := checkIPv6(target); err != nil { return err } options, err := parseOptions(b[naLen:]) if err != nil { return err } *na = NeighborAdvertisement{ Router: (b[0] & 0x80) != 0, Solicited: (b[0] & 0x40) != 0, Override: (b[0] & 0x20) != 0, TargetAddress: target, Options: options, } return nil } var _ Message = &NeighborSolicitation{} // A NeighborSolicitation is a Neighbor Solicitation message as // described in RFC 4861, Section 4.3. type NeighborSolicitation struct { TargetAddress netip.Addr Options []Option } // Type implements Message. func (ns *NeighborSolicitation) Type() ipv6.ICMPType { return ipv6.ICMPTypeNeighborSolicitation } func (ns *NeighborSolicitation) marshal() ([]byte, error) { if err := checkIPv6(ns.TargetAddress); err != nil { return nil, err } b := make([]byte, nsLen) copy(b[4:], ns.TargetAddress.AsSlice()) ob, err := marshalOptions(ns.Options) if err != nil { return nil, err } b = append(b, ob...) return b, nil } func (ns *NeighborSolicitation) unmarshal(b []byte) error { if len(b) < nsLen { return io.ErrUnexpectedEOF } // Skip reserved area. addr := b[4:nsLen] target, ok := netip.AddrFromSlice(addr) if !ok { panicf("ndp: invalid IPv6 address slice: %v", addr) } if err := checkIPv6(target); err != nil { return err } options, err := parseOptions(b[nsLen:]) if err != nil { return err } *ns = NeighborSolicitation{ TargetAddress: target, Options: options, } return nil } var _ Message = &RouterAdvertisement{} // A RouterAdvertisement is a Router Advertisement message as // described in RFC 4861, Section 4.1. type RouterAdvertisement struct { CurrentHopLimit uint8 ManagedConfiguration bool OtherConfiguration bool MobileIPv6HomeAgent bool RouterSelectionPreference Preference NeighborDiscoveryProxy bool RouterLifetime time.Duration ReachableTime time.Duration RetransmitTimer time.Duration Options []Option } // A Preference is a NDP router selection or route preference value as // described in RFC 4191, Section 2.1. type Preference int // Possible Preference values. const ( Medium Preference = 0 High Preference = 1 prfReserved Preference = 2 Low Preference = 3 ) // Type implements Message. func (ra *RouterAdvertisement) Type() ipv6.ICMPType { return ipv6.ICMPTypeRouterAdvertisement } func (ra *RouterAdvertisement) marshal() ([]byte, error) { if err := checkPreference(ra.RouterSelectionPreference); err != nil { return nil, err } b := make([]byte, raLen) b[0] = ra.CurrentHopLimit if ra.ManagedConfiguration { b[1] |= (1 << 7) } if ra.OtherConfiguration { b[1] |= (1 << 6) } if ra.MobileIPv6HomeAgent { b[1] |= (1 << 5) } if prf := uint8(ra.RouterSelectionPreference); prf != 0 { b[1] |= (prf << 3) } if ra.NeighborDiscoveryProxy { b[1] |= (1 << 2) } lifetime := ra.RouterLifetime.Seconds() binary.BigEndian.PutUint16(b[2:4], uint16(lifetime)) reach := ra.ReachableTime / time.Millisecond binary.BigEndian.PutUint32(b[4:8], uint32(reach)) retrans := ra.RetransmitTimer / time.Millisecond binary.BigEndian.PutUint32(b[8:12], uint32(retrans)) ob, err := marshalOptions(ra.Options) if err != nil { return nil, err } b = append(b, ob...) return b, nil } func (ra *RouterAdvertisement) unmarshal(b []byte) error { if len(b) < raLen { return io.ErrUnexpectedEOF } // Skip message body for options. options, err := parseOptions(b[raLen:]) if err != nil { return err } var ( mFlag = (b[1] & 0x80) != 0 oFlag = (b[1] & 0x40) != 0 hFlag = (b[1] & 0x20) != 0 prf = Preference((b[1] & 0x18) >> 3) pFlag = (b[1] & 0x04) != 0 lifetime = time.Duration(binary.BigEndian.Uint16(b[2:4])) * time.Second reach = time.Duration(binary.BigEndian.Uint32(b[4:8])) * time.Millisecond retrans = time.Duration(binary.BigEndian.Uint32(b[8:12])) * time.Millisecond ) // Per RFC 4191, Section 2.2: // "If the Reserved (10) value is received, the receiver MUST treat the // value as if it were (00)." if prf == prfReserved { prf = Medium } *ra = RouterAdvertisement{ CurrentHopLimit: b[0], ManagedConfiguration: mFlag, OtherConfiguration: oFlag, MobileIPv6HomeAgent: hFlag, RouterSelectionPreference: prf, NeighborDiscoveryProxy: pFlag, RouterLifetime: lifetime, ReachableTime: reach, RetransmitTimer: retrans, Options: options, } return nil } var _ Message = &RouterSolicitation{} // A RouterSolicitation is a Router Solicitation message as // described in RFC 4861, Section 4.1. type RouterSolicitation struct { Options []Option } // Type implements Message. func (rs *RouterSolicitation) Type() ipv6.ICMPType { return ipv6.ICMPTypeRouterSolicitation } func (rs *RouterSolicitation) marshal() ([]byte, error) { // b contains reserved area. b := make([]byte, rsLen) ob, err := marshalOptions(rs.Options) if err != nil { return nil, err } b = append(b, ob...) return b, nil } func (rs *RouterSolicitation) unmarshal(b []byte) error { if len(b) < rsLen { return io.ErrUnexpectedEOF } // Skip reserved area. options, err := parseOptions(b[rsLen:]) if err != nil { return err } *rs = RouterSolicitation{ Options: options, } return nil } // checkIPv6 verifies that ip is an IPv6 address. func checkIPv6(ip netip.Addr) error { if !ip.Is6() || ip.Is4In6() { return fmt.Errorf("ndp: invalid IPv6 address: %q", ip) } return nil } // checkPreference checks the validity of a Preference value. func checkPreference(prf Preference) error { switch prf { case Low, Medium, High: return nil case prfReserved: return errors.New("ndp: cannot use reserved router selection preference value") default: return fmt.Errorf("ndp: unknown router selection preference value: %d", prf) } } golang-github-mdlayher-ndp-1.1.0/message_internal_test.go000066400000000000000000000012311457561310000235270ustar00rootroot00000000000000package ndp import ( "testing" "github.com/google/go-cmp/cmp" ) func TestRouterAdvertisementUnmarshalReservedPrf(t *testing.T) { // Assume that unmarshaling sets Prf to medium if reserved value received. const want = Medium b := []byte{0x0, byte(prfReserved) << 3, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0} ra := new(RouterAdvertisement) if err := ra.unmarshal(b); err != nil { t.Fatalf("failed to unmarshal: %v", err) } // Assume that unmarshaling ignores any prefix bits longer than the // specified length. if diff := cmp.Diff(want, ra.RouterSelectionPreference); diff != "" { t.Fatalf("unexpected prf (-want +got):\n%s", diff) } } golang-github-mdlayher-ndp-1.1.0/message_test.go000066400000000000000000000233071457561310000216430ustar00rootroot00000000000000package ndp_test import ( "errors" "net/netip" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/mdlayher/ndp" "github.com/mdlayher/ndp/internal/ndptest" ) // A messageSub is a sub-test structure for Message marshal/unmarshal tests. type messageSub struct { name string m ndp.Message bs [][]byte ok bool } func TestMarshalParseMessage(t *testing.T) { tests := []struct { name string header []byte subs []messageSub }{ { name: "NA", header: []byte{136, 0x00, 0x00, 0x00}, subs: naTests(), }, { name: "NS", header: []byte{135, 0x00, 0x00, 0x00}, subs: nsTests(), }, { name: "RA", header: []byte{134, 0x00, 0x00, 0x00}, subs: raTests(), }, { name: "RS", header: []byte{133, 0x00, 0x00, 0x00}, subs: rsTests(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for _, st := range tt.subs { t.Run(st.name, func(t *testing.T) { b, err := ndp.MarshalMessage(st.m) if err != nil && st.ok { t.Fatalf("unexpected error: %v", err) } if err == nil && !st.ok { t.Fatal("expected an error, but none occurred") } if err != nil { t.Logf("OK error: %v", err) return } // ICMPv6 header precedes the message bytes. ttb := append(tt.header, ndptest.Merge(st.bs)...) if diff := cmp.Diff(ttb, b); diff != "" { t.Fatalf("unexpected message bytes (-want +got):\n%s", diff) } m, err := ndp.ParseMessage(b) if err != nil { t.Fatalf("failed to unmarshal message: %v", err) } if diff := cmp.Diff(st.m, m, cmp.Comparer(addrEqual)); diff != "" { t.Fatalf("unexpected message (-want +got):\n%s", diff) } }) } }) } } func TestParseMessageError(t *testing.T) { type sub struct { name string bs [][]byte } tests := []struct { name string header []byte subs []sub }{ { name: "invalid", // No common header; these messages are only ICMPv6 headers. subs: []sub{ { name: "short", bs: [][]byte{{ 255, }}, }, { name: "unknown type", bs: [][]byte{{ 255, 0x00, 0x00, 0x00, }}, }, }, }, { name: "NA", header: []byte{136, 0x00, 0x00, 0x00}, subs: []sub{ { name: "short", bs: [][]byte{ndptest.Zero(16)}, }, { name: "IPv4", bs: [][]byte{ {0xe0, 0x00, 0x00, 0x00}, netip.IPv4Unspecified().AsSlice(), }, }, }, }, { name: "NS", header: []byte{135, 0x00, 0x00, 0x00}, subs: []sub{ { name: "bad, short", bs: [][]byte{ndptest.Zero(16)}, }, { name: "bad, IPv4", bs: [][]byte{ {0xe0, 0x00, 0x00, 0x00}, netip.IPv4Unspecified().AsSlice(), }, }, }, }, { name: "RA", header: []byte{134, 0x00, 0x00, 0x00}, subs: []sub{ { name: "short", bs: [][]byte{{0x00}}, }, }, }, { name: "RS", header: []byte{133, 0x00, 0x00, 0x00}, subs: []sub{ { name: "short", bs: [][]byte{{0x00}}, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for _, st := range tt.subs { t.Run(st.name, func(t *testing.T) { ttb := append(tt.header, ndptest.Merge(st.bs)...) _, err := ndp.ParseMessage(ttb) if err == nil { t.Fatal("expected an error, but none occurred") } // Rather then exporting errParseMessage, we will just check // for a wrapped substring here for now. perr := errors.Unwrap(err) if perr.Error() != "failed to parse message" { t.Fatalf("unexpected error: %v", err) } }) } }) } } func TestMarshalMessageChecksum(t *testing.T) { var ( source = netip.MustParseAddr("2001:db8::10") destination = netip.MustParseAddr("2001:db8::1") ) message := &ndp.NeighborAdvertisement{ Solicited: true, Override: true, TargetAddress: source, Options: []ndp.Option{&ndp.LinkLayerAddress{ Direction: ndp.Target, Addr: ndptest.MAC, }}, } buf, err := ndp.MarshalMessageChecksum(message, source, destination) if err != nil { t.Fatalf("failed to marshal message with checksum: %v", err) } // Checksum is in bytes 3 and 4. if diff := cmp.Diff(buf[2:4], []uint8{0x10, 0x0c}); diff != "" { t.Fatalf("unexpected set checksum (-want +got):\n%s", diff) } // Check that MarshalMessage has a 0 checksum. buf, err = ndp.MarshalMessage(message) if err != nil { t.Fatalf("failed to marshal message: %v", err) } if diff := cmp.Diff(buf[2:4], []uint8{0, 0}); diff != "" { t.Fatalf("unexpected unset checksum (-want +got):\n%s", diff) } } func naTests() []messageSub { return []messageSub{ { name: "bad, IPv4 address", m: &ndp.NeighborAdvertisement{ TargetAddress: netip.IPv4Unspecified(), }, }, { name: "ok, no flags", m: &ndp.NeighborAdvertisement{ TargetAddress: ndptest.IP, }, bs: [][]byte{ {0x00, 0x00, 0x00, 0x00}, ndptest.IP.AsSlice(), }, ok: true, }, { name: "ok, router", m: &ndp.NeighborAdvertisement{ Router: true, TargetAddress: ndptest.IP, }, bs: [][]byte{ {0x80, 0x00, 0x00, 0x00}, ndptest.IP.AsSlice(), }, ok: true, }, { name: "ok, solicited", m: &ndp.NeighborAdvertisement{ Solicited: true, TargetAddress: ndptest.IP, }, bs: [][]byte{ {0x40, 0x00, 0x00, 0x00}, ndptest.IP.AsSlice(), }, ok: true, }, { name: "ok, override", m: &ndp.NeighborAdvertisement{ Override: true, TargetAddress: ndptest.IP, }, bs: [][]byte{ {0x20, 0x00, 0x00, 0x00}, ndptest.IP.AsSlice(), }, ok: true, }, { name: "ok, all flags", m: &ndp.NeighborAdvertisement{ Router: true, Solicited: true, Override: true, TargetAddress: ndptest.IP, }, bs: [][]byte{ {0xe0, 0x00, 0x00, 0x00}, ndptest.IP.AsSlice(), }, ok: true, }, { name: "ok, with target LLA", m: &ndp.NeighborAdvertisement{ Router: true, Solicited: true, Override: true, TargetAddress: ndptest.IP, Options: []ndp.Option{ &ndp.LinkLayerAddress{ Direction: ndp.Target, Addr: ndptest.MAC, }, }, }, bs: [][]byte{ // NA message. {0xe0, 0x00, 0x00, 0x00}, ndptest.IP.AsSlice(), // Target LLA option. {0x02, 0x01}, ndptest.MAC, }, ok: true, }, } } func nsTests() []messageSub { return []messageSub{ { name: "bad, IPv4 address", m: &ndp.NeighborSolicitation{ TargetAddress: netip.IPv4Unspecified(), }, }, { name: "ok, no options", m: &ndp.NeighborSolicitation{ TargetAddress: ndptest.IP, }, bs: [][]byte{ {0x00, 0x00, 0x00, 0x00}, ndptest.IP.AsSlice(), }, ok: true, }, { name: "ok, with source LLA", m: &ndp.NeighborSolicitation{ TargetAddress: ndptest.IP, Options: []ndp.Option{ &ndp.LinkLayerAddress{ Direction: ndp.Source, Addr: ndptest.MAC, }, }, }, bs: [][]byte{ // NS message. {0x00, 0x00, 0x00, 0x00}, ndptest.IP.AsSlice(), // Source LLA option. {0x01, 0x01}, ndptest.MAC, }, ok: true, }, } } func raTests() []messageSub { return []messageSub{ { name: "bad, reserved prf", m: &ndp.RouterAdvertisement{ RouterSelectionPreference: 2, }, }, { name: "bad, unknown prf", m: &ndp.RouterAdvertisement{ RouterSelectionPreference: 4, }, }, { name: "ok, no options", m: &ndp.RouterAdvertisement{ CurrentHopLimit: 10, ManagedConfiguration: true, OtherConfiguration: true, RouterLifetime: 30 * time.Second, ReachableTime: 12345 * time.Millisecond, RetransmitTimer: 23456 * time.Millisecond, }, bs: [][]byte{ {0x0a, 0xc0, 0x00, 0x1e, 0x00, 0x00, 0x30, 0x39, 0x00, 0x00, 0x5b, 0xa0}, }, ok: true, }, { name: "ok, with options", m: &ndp.RouterAdvertisement{ CurrentHopLimit: 10, ManagedConfiguration: true, OtherConfiguration: true, RouterSelectionPreference: ndp.Medium, RouterLifetime: 30 * time.Second, ReachableTime: 12345 * time.Millisecond, RetransmitTimer: 23456 * time.Millisecond, Options: []ndp.Option{ &ndp.LinkLayerAddress{ Direction: ndp.Source, Addr: ndptest.MAC, }, ndp.NewMTU(1280), }, }, bs: [][]byte{ // RA message. {0x0a, 0xc0, 0x00, 0x1e, 0x00, 0x00, 0x30, 0x39, 0x00, 0x00, 0x5b, 0xa0}, // Source LLA option. {0x01, 0x01}, ndptest.MAC, // MTU option. {0x05, 0x01, 0x00, 0x00}, {0x00, 0x00, 0x05, 0x00}, }, ok: true, }, { name: "ok, new flags", m: &ndp.RouterAdvertisement{ MobileIPv6HomeAgent: true, RouterSelectionPreference: ndp.Low, NeighborDiscoveryProxy: true, }, bs: [][]byte{ {0x0, 0x3c, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, }, ok: true, }, { name: "ok, prf high", m: &ndp.RouterAdvertisement{ RouterSelectionPreference: ndp.High, }, bs: [][]byte{ {0x0, 0x08, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}, }, ok: true, }, } } func rsTests() []messageSub { return []messageSub{ { name: "ok, no options", m: &ndp.RouterSolicitation{}, bs: [][]byte{ {0x00, 0x00, 0x00, 0x00}, }, ok: true, }, { name: "ok, with source LLA", m: &ndp.RouterSolicitation{ Options: []ndp.Option{ &ndp.LinkLayerAddress{ Direction: ndp.Source, Addr: ndptest.MAC, }, }, }, bs: [][]byte{ // RS message. {0x00, 0x00, 0x00, 0x00}, // Source LLA option. {0x01, 0x01}, ndptest.MAC, }, ok: true, }, } } func addrEqual(x, y netip.Addr) bool { return x == y } golang-github-mdlayher-ndp-1.1.0/option.go000066400000000000000000000702131457561310000204660ustar00rootroot00000000000000package ndp import ( "bytes" "crypto/rand" "crypto/subtle" "encoding/binary" "encoding/hex" "errors" "fmt" "io" "math" "net" "net/netip" "net/url" "strings" "time" "unicode" "golang.org/x/net/idna" ) // Infinity indicates that a prefix is valid for an infinite amount of time, // unless a new, finite, value is received in a subsequent router advertisement. const Infinity = time.Duration(0xffffffff) * time.Second const ( // Length of a link-layer address for Ethernet networks. ethAddrLen = 6 // The assumed NDP option length (in units of 8 bytes) for fixed length options. llaOptLen = 1 piOptLen = 4 mtuOptLen = 1 // Type values for each type of valid Option. optSourceLLA = 1 optTargetLLA = 2 optPrefixInformation = 3 optMTU = 5 optNonce = 14 optRouteInformation = 24 optRDNSS = 25 optRAFlagsExtension = 26 optDNSSL = 31 optCaptivePortal = 37 optPREF64 = 38 ) // A Direction specifies the direction of a LinkLayerAddress Option as a source // or target. type Direction int // Possible Direction values. const ( Source Direction = optSourceLLA Target Direction = optTargetLLA ) // An Option is a Neighbor Discovery Protocol option. type Option interface { // Code specifies the NDP option code for an Option. Code() uint8 // "Code" as a method name isn't actually accurate because NDP options // also refer to that field as "Type", but we want to avoid confusion // with Message implementations which already use Type. // Called when dealing with a Message's Options. marshal() ([]byte, error) unmarshal(b []byte) error } var _ Option = &LinkLayerAddress{} // A LinkLayerAddress is a Source or Target Link-Layer Address option, as // described in RFC 4861, Section 4.6.1. type LinkLayerAddress struct { Direction Direction Addr net.HardwareAddr } // TODO(mdlayher): deal with non-ethernet links and variable option length? // Code implements Option. func (lla *LinkLayerAddress) Code() byte { return byte(lla.Direction) } func (lla *LinkLayerAddress) marshal() ([]byte, error) { if d := lla.Direction; d != Source && d != Target { return nil, fmt.Errorf("ndp: invalid link-layer address direction: %d", d) } if len(lla.Addr) != ethAddrLen { return nil, fmt.Errorf("ndp: invalid link-layer address: %q", lla.Addr) } raw := &RawOption{ Type: lla.Code(), Length: llaOptLen, Value: lla.Addr, } return raw.marshal() } func (lla *LinkLayerAddress) unmarshal(b []byte) error { raw := new(RawOption) if err := raw.unmarshal(b); err != nil { return err } d := Direction(raw.Type) if d != Source && d != Target { return fmt.Errorf("ndp: invalid link-layer address direction: %d", d) } if l := raw.Length; l != llaOptLen { return fmt.Errorf("ndp: unexpected link-layer address option length: %d", l) } *lla = LinkLayerAddress{ Direction: d, Addr: net.HardwareAddr(raw.Value), } return nil } var _ Option = new(MTU) // An MTU is an MTU option, as described in RFC 4861, Section 4.6.1. type MTU struct { MTU uint32 } // NewMTU creates an MTU Option from an MTU value. func NewMTU(mtu uint32) *MTU { return &MTU{MTU: mtu} } // Code implements Option. func (*MTU) Code() byte { return optMTU } func (m *MTU) marshal() ([]byte, error) { raw := &RawOption{ Type: m.Code(), Length: mtuOptLen, // 2 reserved bytes, 4 for MTU. Value: make([]byte, 6), } binary.BigEndian.PutUint32(raw.Value[2:6], uint32(m.MTU)) return raw.marshal() } func (m *MTU) unmarshal(b []byte) error { raw := new(RawOption) if err := raw.unmarshal(b); err != nil { return err } *m = MTU{MTU: binary.BigEndian.Uint32(raw.Value[2:6])} return nil } var _ Option = &PrefixInformation{} // A PrefixInformation is a a Prefix Information option, as described in RFC 4861, Section 4.6.1. type PrefixInformation struct { PrefixLength uint8 OnLink bool AutonomousAddressConfiguration bool ValidLifetime time.Duration PreferredLifetime time.Duration Prefix netip.Addr } // Code implements Option. func (*PrefixInformation) Code() byte { return optPrefixInformation } func (pi *PrefixInformation) marshal() ([]byte, error) { // Per the RFC: // "The bits in the prefix after the prefix length are reserved and MUST // be initialized to zero by the sender and ignored by the receiver." // // Therefore, any prefix, when masked with its specified length, should be // identical to the prefix itself for it to be valid. p := netip.PrefixFrom(pi.Prefix, int(pi.PrefixLength)) if masked := p.Masked(); pi.Prefix != masked.Addr() { return nil, fmt.Errorf("ndp: invalid prefix information: %s/%d", pi.Prefix, pi.PrefixLength) } raw := &RawOption{ Type: pi.Code(), Length: piOptLen, // 30 bytes for PrefixInformation body. Value: make([]byte, 30), } raw.Value[0] = pi.PrefixLength if pi.OnLink { raw.Value[1] |= (1 << 7) } if pi.AutonomousAddressConfiguration { raw.Value[1] |= (1 << 6) } valid := pi.ValidLifetime.Seconds() binary.BigEndian.PutUint32(raw.Value[2:6], uint32(valid)) pref := pi.PreferredLifetime.Seconds() binary.BigEndian.PutUint32(raw.Value[6:10], uint32(pref)) // 4 bytes reserved. copy(raw.Value[14:30], pi.Prefix.AsSlice()) return raw.marshal() } func (pi *PrefixInformation) unmarshal(b []byte) error { raw := new(RawOption) if err := raw.unmarshal(b); err != nil { return err } // Guard against incorrect option length. if raw.Length != piOptLen { return io.ErrUnexpectedEOF } var ( oFlag = (raw.Value[1] & 0x80) != 0 aFlag = (raw.Value[1] & 0x40) != 0 valid = time.Duration(binary.BigEndian.Uint32(raw.Value[2:6])) * time.Second preferred = time.Duration(binary.BigEndian.Uint32(raw.Value[6:10])) * time.Second ) // Skip to address. addr := raw.Value[14:30] ip, ok := netip.AddrFromSlice(addr) if !ok { panicf("ndp: invalid IPv6 address slice: %v", addr) } if err := checkIPv6(ip); err != nil { return err } // Per the RFC, bits in prefix past prefix length are ignored by the // receiver. pl := raw.Value[0] p := netip.PrefixFrom(ip, int(pl)).Masked() *pi = PrefixInformation{ PrefixLength: pl, OnLink: oFlag, AutonomousAddressConfiguration: aFlag, ValidLifetime: valid, PreferredLifetime: preferred, Prefix: p.Addr(), } return nil } var _ Option = &RouteInformation{} // A RouteInformation is a Route Information option, as described in RFC 4191, // Section 2.3. type RouteInformation struct { PrefixLength uint8 Preference Preference RouteLifetime time.Duration Prefix netip.Addr } // Code implements Option. func (*RouteInformation) Code() byte { return optRouteInformation } func (ri *RouteInformation) marshal() ([]byte, error) { // Per the RFC: // "The bits in the prefix after the prefix length are reserved and MUST // be initialized to zero by the sender and ignored by the receiver." // // Therefore, any prefix, when masked with its specified length, should be // identical to the prefix itself for it to be valid. err := fmt.Errorf("ndp: invalid route information: %s/%d", ri.Prefix, ri.PrefixLength) p := netip.PrefixFrom(ri.Prefix, int(ri.PrefixLength)) if masked := p.Masked(); ri.Prefix != masked.Addr() { return nil, err } // Depending on the length of the prefix, we can add fewer bytes to the // option. var iplen int switch { case ri.PrefixLength == 0: iplen = 0 case ri.PrefixLength > 0 && ri.PrefixLength < 65: iplen = 1 case ri.PrefixLength > 64 && ri.PrefixLength < 129: iplen = 2 default: // Invalid IPv6 prefix. return nil, err } raw := &RawOption{ Type: ri.Code(), Length: uint8(iplen) + 1, // Prefix length, preference, lifetime, and prefix body as computed by // using iplen. Value: make([]byte, 1+1+4+(iplen*8)), } raw.Value[0] = ri.PrefixLength // Adjacent bits are reserved. if prf := uint8(ri.Preference); prf != 0 { raw.Value[1] |= (prf << 3) } lt := ri.RouteLifetime.Seconds() binary.BigEndian.PutUint32(raw.Value[2:6], uint32(lt)) copy(raw.Value[6:], ri.Prefix.AsSlice()) return raw.marshal() } func (ri *RouteInformation) unmarshal(b []byte) error { raw := new(RawOption) if err := raw.unmarshal(b); err != nil { return err } // Verify the option's length against prefix length using the rules defined // in the RFC. l := raw.Value[0] rerr := fmt.Errorf("ndp: invalid route information for /%d prefix", l) switch { case l == 0: if raw.Length < 1 || raw.Length > 3 { return rerr } case l > 0 && l < 65: // Some devices will use length 3 anyway for a route that fits in /64. if raw.Length != 2 && raw.Length != 3 { return rerr } case l > 64 && l < 129: if raw.Length != 3 { return rerr } default: // Invalid IPv6 prefix. return rerr } // Unpack preference (with adjacent reserved bits) and lifetime values. var ( pref = Preference((raw.Value[1] & 0x18) >> 3) lt = time.Duration(binary.BigEndian.Uint32(raw.Value[2:6])) * time.Second ) if err := checkPreference(pref); err != nil { return err } // Take up to the specified number of IP bytes into the prefix. var ( addr [16]byte buf = raw.Value[6 : 6+(l/8)] ) copy(addr[:], buf) *ri = RouteInformation{ PrefixLength: l, Preference: pref, RouteLifetime: lt, Prefix: netip.AddrFrom16(addr), } return nil } // A RecursiveDNSServer is a Recursive DNS Server option, as described in // RFC 8106, Section 5.1. type RecursiveDNSServer struct { Lifetime time.Duration Servers []netip.Addr } // Code implements Option. func (*RecursiveDNSServer) Code() byte { return optRDNSS } // Offsets for the RDNSS option. const ( rdnssLifetimeOff = 2 rdnssServersOff = 6 ) var ( errRDNSSNoServers = errors.New("ndp: recursive DNS server option requires at least one server") errRDNSSBadServer = errors.New("ndp: recursive DNS server option has malformed IPv6 address") ) func (r *RecursiveDNSServer) marshal() ([]byte, error) { slen := len(r.Servers) if slen == 0 { return nil, errRDNSSNoServers } raw := &RawOption{ Type: r.Code(), // Always have one length unit to start, and then each IPv6 address // occupies two length units. Length: 1 + uint8((slen * 2)), // Allocate enough space for all data. Value: make([]byte, rdnssServersOff+(slen*net.IPv6len)), } binary.BigEndian.PutUint32( raw.Value[rdnssLifetimeOff:rdnssServersOff], uint32(r.Lifetime.Seconds()), ) for i := 0; i < len(r.Servers); i++ { // Determine the start and end byte offsets for each address, // effectively iterating 16 bytes at a time to insert an address. var ( start = rdnssServersOff + (i * net.IPv6len) end = rdnssServersOff + net.IPv6len + (i * net.IPv6len) ) copy(raw.Value[start:end], r.Servers[i].AsSlice()) } return raw.marshal() } func (r *RecursiveDNSServer) unmarshal(b []byte) error { raw := new(RawOption) if err := raw.unmarshal(b); err != nil { return err } // Skip 2 reserved bytes to get lifetime. lt := time.Duration(binary.BigEndian.Uint32( raw.Value[rdnssLifetimeOff:rdnssServersOff])) * time.Second // Determine the number of DNS servers specified using the method described // in the RFC. Remember, length is specified in units of 8 octets. // // "That is, the number of addresses is equal to (Length - 1) / 2." // // Make sure at least one server is present, and that the IPv6 addresses are // the expected 16 byte length. dividend := (int(raw.Length) - 1) if dividend%2 != 0 { return errRDNSSBadServer } count := dividend / 2 if count == 0 { return errRDNSSNoServers } servers := make([]netip.Addr, 0, count) for i := 0; i < count; i++ { // Determine the start and end byte offsets for each address, // effectively iterating 16 bytes at a time to fetch an address. var ( start = rdnssServersOff + (i * net.IPv6len) end = rdnssServersOff + net.IPv6len + (i * net.IPv6len) ) s, ok := netip.AddrFromSlice(raw.Value[start:end]) if !ok { return errRDNSSBadServer } servers = append(servers, s) } *r = RecursiveDNSServer{ Lifetime: lt, Servers: servers, } return nil } // A DNSSearchList is a DNS search list option, as described in // RFC 8106, Section 5.2. type DNSSearchList struct { Lifetime time.Duration DomainNames []string } // Code implements Option. func (*DNSSearchList) Code() byte { return optDNSSL } // Offsets for the RDNSS option. const ( dnsslLifetimeOff = 2 dnsslDomainsOff = 6 ) var ( errDNSSLBadDomains = errors.New("ndp: DNS search list option has malformed domain names") errDNSSLNoDomains = errors.New("ndp: DNS search list option requires at least one domain name") ) func (d *DNSSearchList) marshal() ([]byte, error) { if len(d.DomainNames) == 0 { return nil, errDNSSLNoDomains } // Make enough room for reserved bytes and lifetime. value := make([]byte, dnsslDomainsOff) binary.BigEndian.PutUint32( value[dnsslLifetimeOff:dnsslDomainsOff], uint32(d.Lifetime.Seconds()), ) // Attach each label component of a domain name with a one byte length prefix // and a null terminator between full domain names, using the algorithm from: // https://tools.ietf.org/html/rfc1035#section-3.1. for _, dn := range d.DomainNames { // All unicode names must be converted to punycode. dn, err := idna.ToASCII(dn) if err != nil { return nil, errDNSSLBadDomains } for _, label := range strings.Split(dn, ".") { // Label must be convertable to valid Punycode. if !isASCII(label) { return nil, errDNSSLBadDomains } value = append(value, byte(len(label))) value = append(value, label...) } value = append(value, 0) } // Pad null bytes into value, so that when combined with type and length, // the entire buffer length is divisible by 8 bytes for proper NDP option // length. if r := (len(value) + 2) % 8; r != 0 { value = append(value, bytes.Repeat([]byte{0x00}, 8-r)...) } raw := &RawOption{ Type: d.Code(), // Always have one length unit to start, and then calculate the length // needed for value. Length: uint8((len(value) + 2) / 8), Value: value, } return raw.marshal() } func (d *DNSSearchList) unmarshal(b []byte) error { raw := new(RawOption) if err := raw.unmarshal(b); err != nil { return err } // Skip 2 reserved bytes to get lifetime. lt := time.Duration(binary.BigEndian.Uint32( raw.Value[dnsslLifetimeOff:dnsslDomainsOff])) * time.Second // This block implements the domain name space parsing algorithm from: // https://tools.ietf.org/html/rfc1035#section-3.1. // // A domain is comprised of a sequence of labels, which are accumulated and // then separated by periods later on. var domains []string var labels []string for i := dnsslDomainsOff; ; { if len(raw.Value[i:]) < 2 { return errDNSSLBadDomains } // Parse the length of the upcoming label. length := int(raw.Value[i]) if length >= len(raw.Value[i:])-1 { // Length out of range. return errDNSSLBadDomains } if length == 0 { // No more labels. break } i++ // Parse the label string and ensure it is ASCII, and that it doesn't // contain invalid characters. label := string(raw.Value[i : i+length]) if !isASCII(label) { return errDNSSLBadDomains } // TODO(mdlayher): much smarter validation. if label == "" || strings.Contains(label, ".") || strings.Contains(label, " ") { return errDNSSLBadDomains } // Verify that the Punycode label decodes to something sane. label, err := idna.ToUnicode(label) if err != nil { return errDNSSLBadDomains } // TODO(mdlayher): much smarter validation. if label == "" || hasUnicodeReplacement(label) || strings.Contains(label, ".") || strings.Contains(label, " ") { return errDNSSLBadDomains } labels = append(labels, label) i += length // If we've reached a null byte, join labels into a domain name and // empty the label stack for reuse. if raw.Value[i] == 0 { i++ domain, err := idna.ToUnicode(strings.Join(labels, ".")) if err != nil { return errDNSSLBadDomains } domains = append(domains, domain) labels = []string{} // Have we reached the end of the value slice? if len(raw.Value[i:]) == 0 || (len(raw.Value[i:]) == 1 && raw.Value[i] == 0) { // No more non-padding bytes, no more labels. break } } } // Must have found at least one domain. if len(domains) == 0 { return errDNSSLNoDomains } *d = DNSSearchList{ Lifetime: lt, DomainNames: domains, } return nil } // Unrestricted is the IANA-assigned URI for a network with no captive portal // restrictions, as specified in RFC 8910, Section 2. const Unrestricted = "urn:ietf:params:capport:unrestricted" // A CaptivePortal is a Captive-Portal option, as described in RFC 8910, Section // 2.3. type CaptivePortal struct { URI string } // NewCaptivePortal produces a CaptivePortal Option for the input URI string. As // a special case, if uri is empty, Unrestricted is used as the CaptivePortal // OptionURI. // // If uri is an IP address literal, an error is returned. Per RFC 8910, uri // "SHOULD NOT" be an IP address, but there are circumstances where this // behavior may be useful. In that case, the caller can bypass NewCaptivePortal // and construct a CaptivePortal Option directly. func NewCaptivePortal(uri string) (*CaptivePortal, error) { if uri == "" { return &CaptivePortal{URI: Unrestricted}, nil } // Try to comply with the max limit for DHCPv4. if len(uri) > 255 { return nil, errors.New("ndp: captive portal option URI is too long") } // TODO(mdlayher): a URN is almost a URL, but investigate compliance with // https://datatracker.ietf.org/doc/html/rfc8141. In particular there are // some tricky rules around case-sensitivity. urn, err := url.Parse(uri) if err != nil { return nil, err } // "The URI SHOULD NOT contain an IP address literal." // // Since this is a constructor and there's nothing stopping the user from // manually creating this string if they so choose, we'll return an error // IP addresses. This includes bare IP addresses or IP addresses with some // kind of path appended. for _, s := range strings.Split(urn.Path, "/") { if ip, err := netip.ParseAddr(s); err == nil { return nil, fmt.Errorf("ndp: captive portal option URIs should not contain IP addresses: %s", ip) } } return &CaptivePortal{URI: urn.String()}, nil } // Code implements Option. func (*CaptivePortal) Code() byte { return optCaptivePortal } func (cp *CaptivePortal) marshal() ([]byte, error) { if len(cp.URI) == 0 { return nil, errors.New("ndp: captive portal option requires a non-empty URI") } // Pad up to next unit of 8 bytes including 2 bytes for code, length, and // bytes for the URI string. Extra bytes will be null. l := len(cp.URI) if r := (l + 2) % 8; r != 0 { l += 8 - r } value := make([]byte, l) copy(value, []byte(cp.URI)) raw := &RawOption{ Type: cp.Code(), Length: (uint8(l) + 2) / 8, Value: value, } return raw.marshal() } func (cp *CaptivePortal) unmarshal(b []byte) error { raw := new(RawOption) if err := raw.unmarshal(b); err != nil { return err } // Don't allow a null URI. if len(raw.Value) == 0 || raw.Value[0] == 0x00 { return errors.New("ndp: captive portal URI is null") } // Find any trailing null bytes and trim them away before setting the URI. i := bytes.Index(raw.Value, []byte{0x00}) if i == -1 { i = len(raw.Value) } // Our constructor does validation of URIs, but we treat the URI as opaque // for parsing, since we likely have to interop with other implementations. *cp = CaptivePortal{URI: string(raw.Value[:i])} return nil } // PREF64 is a PREF64 option, as described in RFC 8781, Section 4. The prefix // must have a prefix length of 96, 64, 56, 40, or 32. The lifetime is used to // indicate to clients how long the PREF64 prefix is valid for. A lifetime of 0 // indicates the prefix is no longer valid. If unsure, refer to RFC 8781 // Section 4.1 for how to calculate an appropriate lifetime. type PREF64 struct { Lifetime time.Duration Prefix netip.Prefix } func (p *PREF64) Code() byte { return optPREF64 } func (p *PREF64) marshal() ([]byte, error) { var plc uint8 switch p.Prefix.Bits() { case 96: plc = 0 case 64: plc = 1 case 56: plc = 2 case 48: plc = 3 case 40: plc = 4 case 32: plc = 5 default: return nil, errors.New("ndp: invalid pref64 prefix size") } scaledLifetime := uint16(math.Round(p.Lifetime.Seconds() / 8)) // The scaled lifetime must be less than the maximum of 8191. if scaledLifetime > 8191 { return nil, errors.New("ndp: pref64 scaled lifetime is too large") } value := []byte{} // The scaled lifetime and PLC values live within the same 16-bit field. // Here we move the scaled lifetime to the left-most 13 bits and place the // PLC at the last 3 bits of the 16-bit field. value = binary.BigEndian.AppendUint16( value, (scaledLifetime<<3&(0xffff^0b111))|uint16(plc&0b111), ) allPrefixBits := p.Prefix.Masked().Addr().As16() optionPrefixBits := allPrefixBits[:96/8] value = append(value, optionPrefixBits...) raw := &RawOption{ Type: p.Code(), Length: (uint8(len(value)) + 2) / 8, Value: value, } return raw.marshal() } func (p *PREF64) unmarshal(b []byte) error { raw := new(RawOption) if err := raw.unmarshal(b); err != nil { return err } if raw.Type != optPREF64 { return errors.New("ndp: invalid pref64 type") } if len(raw.Value) != (96/8)+2 { return errors.New("ndp: invalid pref64 message length") } lifetimeAndPlc := binary.BigEndian.Uint16(raw.Value[:2]) plc := uint8(lifetimeAndPlc & 0b111) var prefixSize int switch plc { case 0: prefixSize = 96 case 1: prefixSize = 64 case 2: prefixSize = 56 case 3: prefixSize = 48 case 4: prefixSize = 40 case 5: prefixSize = 32 default: return errors.New("ndp: invalid pref64 prefix length code") } addr := [16]byte{} copy(addr[:], raw.Value[2:]) prefix, err := netip.AddrFrom16(addr).Prefix(int(prefixSize)) if err != nil { return err } scaledLifetime := (lifetimeAndPlc & (0xffff ^ 0b111)) >> 3 lifetime := time.Duration(scaledLifetime) * 8 * time.Second *p = PREF64{ Lifetime: lifetime, Prefix: prefix, } return nil } // A RAFlagsExtension is a Router Advertisement Flags Extension (or Expansion) // option, as described in RFC 5175, Section 4. type RAFlagsExtension struct { Flags RAFlags } // RAFlags is a bitmask of Router Advertisement flags contained within an // RAFlagsExtension. type RAFlags []byte // Code implements Option. func (*RAFlagsExtension) Code() byte { return optRAFlagsExtension } func (ra *RAFlagsExtension) marshal() ([]byte, error) { // "MUST NOT be added to a Router Advertisement message if no flags in the // option are set." // // TODO(mdlayher): replace with slices.IndexFunc when we raise the minimum // Go version. var found bool for _, b := range ra.Flags { if b != 0x00 { found = true break } } if !found { return nil, errors.New("ndp: RA flags extension requires one or more flags to be set") } // Enforce the option size matches the next unit of 8 bytes including 2 // bytes for code and length. l := len(ra.Flags) if r := (l + 2) % 8; r != 0 { return nil, errors.New("ndp: RA flags extension length is invalid") } value := make([]byte, l) copy(value, ra.Flags) raw := &RawOption{ Type: ra.Code(), Length: (uint8(l) + 2) / 8, Value: value, } return raw.marshal() } func (ra *RAFlagsExtension) unmarshal(b []byte) error { raw := new(RawOption) if err := raw.unmarshal(b); err != nil { return err } // Don't allow short bytes. if len(raw.Value) < 6 { return errors.New("ndp: RA Flags Extension too short") } // raw already made a copy. ra.Flags = raw.Value return nil } // A Nonce is a Nonce option, as described in RFC 3971, Section 5.3.2. type Nonce struct { b []byte } // NewNonce creates a Nonce option with an opaque random value. func NewNonce() *Nonce { // Minimum is 6 bytes, and this is also the only value that the Linux kernel // recognizes as of kernel 5.17. const n = 6 b := make([]byte, n) if _, err := rand.Read(b); err != nil { panicf("ndp: failed to generate nonce bytes: %v", err) } return &Nonce{b: b} } // Equal reports whether n and x are the same nonce. func (n *Nonce) Equal(x *Nonce) bool { return subtle.ConstantTimeCompare(n.b, x.b) == 1 } // Code implements Option. func (*Nonce) Code() byte { return optNonce } // String returns the string representation of a Nonce. func (n *Nonce) String() string { return hex.EncodeToString(n.b) } func (n *Nonce) marshal() ([]byte, error) { if len(n.b) == 0 { return nil, errors.New("ndp: nonce option requires a non-empty nonce value") } // Enforce the nonce size matches the next unit of 8 bytes including 2 bytes // for code and length. l := len(n.b) if r := (l + 2) % 8; r != 0 { return nil, errors.New("ndp: nonce size is invalid") } value := make([]byte, l) copy(value, n.b) raw := &RawOption{ Type: n.Code(), Length: (uint8(l) + 2) / 8, Value: value, } return raw.marshal() } func (n *Nonce) unmarshal(b []byte) error { raw := new(RawOption) if err := raw.unmarshal(b); err != nil { return err } // raw already made a copy. n.b = raw.Value return nil } var _ Option = &RawOption{} // A RawOption is an Option in its raw and unprocessed format. Options which // are not recognized by this package can be represented using a RawOption. type RawOption struct { Type uint8 Length uint8 Value []byte } // Code implements Option. func (r *RawOption) Code() byte { return r.Type } func (r *RawOption) marshal() ([]byte, error) { // Length specified in units of 8 bytes, and the caller must provide // an accurate length. l := int(r.Length * 8) if 1+1+len(r.Value) != l { return nil, io.ErrUnexpectedEOF } b := make([]byte, r.Length*8) b[0] = r.Type b[1] = r.Length copy(b[2:], r.Value) return b, nil } func (r *RawOption) unmarshal(b []byte) error { if len(b) < 2 { return io.ErrUnexpectedEOF } r.Type = b[0] r.Length = b[1] // Exclude type and length fields from value's length. l := int(r.Length*8) - 2 // Enforce a valid length value that matches the expected one. if lb := len(b[2:]); l != lb { return fmt.Errorf("ndp: option value byte length should be %d, but length is %d", l, lb) } r.Value = make([]byte, l) copy(r.Value, b[2:]) return nil } // marshalOptions marshals a slice of Options into a single byte slice. func marshalOptions(options []Option) ([]byte, error) { var b []byte for _, o := range options { ob, err := o.marshal() if err != nil { return nil, err } b = append(b, ob...) } return b, nil } // parseOptions parses a slice of Options from a byte slice. func parseOptions(b []byte) ([]Option, error) { var options []Option for i := 0; len(b[i:]) != 0; { // Two bytes: option type and option length. if len(b[i:]) < 2 { return nil, io.ErrUnexpectedEOF } // Type processed as-is, but length is stored in units of 8 bytes, // so expand it to the actual byte length. t := b[i] l := int(b[i+1]) * 8 // Verify that we won't advance beyond the end of the byte slice. if l > len(b[i:]) { return nil, io.ErrUnexpectedEOF } // Infer the option from its type value and use it for unmarshaling. var o Option switch t { case optSourceLLA, optTargetLLA: o = new(LinkLayerAddress) case optMTU: o = new(MTU) case optPrefixInformation: o = new(PrefixInformation) case optRouteInformation: o = new(RouteInformation) case optRDNSS: o = new(RecursiveDNSServer) case optRAFlagsExtension: o = new(RAFlagsExtension) case optDNSSL: o = new(DNSSearchList) case optCaptivePortal: o = new(CaptivePortal) case optPREF64: o = new(PREF64) case optNonce: o = new(Nonce) default: o = new(RawOption) } // Unmarshal at the current offset, up to the expected length. if err := o.unmarshal(b[i : i+l]); err != nil { return nil, err } // Advance to the next option's type field. i += l options = append(options, o) } return options, nil } // isASCII verifies that the contents of s are all ASCII characters. func isASCII(s string) bool { for _, c := range s { if c > unicode.MaxASCII { return false } } return true } // hasUnicodeReplacement checks for the Unicode replacment character in s. func hasUnicodeReplacement(s string) bool { for _, c := range s { if c == unicode.ReplacementChar { return true } } return false } golang-github-mdlayher-ndp-1.1.0/option_test.go000066400000000000000000000517751457561310000215410ustar00rootroot00000000000000package ndp // Package ndp_test not used because we need access to direct option marshaling // and unmarshaling functions. import ( "net" "net/netip" "strings" "testing" "time" "github.com/google/go-cmp/cmp" "github.com/mdlayher/ndp/internal/ndptest" ) // An optionSub is a sub-test structure for Option marshal/unmarshal tests. type optionSub struct { name string os []Option bs [][]byte ok bool } func TestOptionMarshalUnmarshal(t *testing.T) { tests := []struct { name string subs []optionSub }{ { name: "raw option", subs: roTests(), }, { name: "link layer address", subs: llaTests(), }, { name: "MTU", subs: []optionSub{{ name: "ok", os: []Option{NewMTU(1500)}, bs: [][]byte{ {0x05, 0x01, 0x00, 0x00}, {0x00, 0x00, 0x05, 0xdc}, }, ok: true, }}, }, { name: "prefix information", subs: piTests(), }, { name: "route information", subs: riTests(), }, { name: "recursive DNS servers", subs: rdnssTests(), }, { name: "RA flags extension", subs: raFlagsExtensionTests(), }, { name: "DNS search list", subs: dnsslTests(), }, { name: "captive portal", subs: cpTests(), }, { name: "pref64", subs: pref64Tests(), }, { name: "nonce", subs: nonceTests(), }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for _, st := range tt.subs { t.Run(st.name, func(t *testing.T) { b, err := marshalOptions(st.os) if err != nil && st.ok { t.Fatalf("unexpected error: %v", err) } if err == nil && !st.ok { t.Fatal("expected an error, but none occurred") } if err != nil { t.Logf("OK error: %v", err) return } ttb := ndptest.Merge(st.bs) if diff := cmp.Diff(ttb, b); diff != "" { t.Fatalf("unexpected options bytes (-want +got):\n%s", diff) } got, err := parseOptions(b) if err != nil { t.Fatalf("failed to unmarshal options: %v", err) } if diff := cmp.Diff(st.os, got, cmp.Comparer(addrEqual), cmp.Comparer(prefixEqual)); diff != "" { t.Fatalf("unexpected options (-want +got):\n%s", diff) } }) } }) } } func TestOptionUnmarshalError(t *testing.T) { type sub struct { name string bs [][]byte } tests := []struct { name string o Option subs []sub }{ { name: "raw option", o: &RawOption{}, subs: []sub{ { name: "short", bs: [][]byte{{0x01}}, }, { name: "misleading length", bs: [][]byte{{0x10, 0x10}}, }, }, }, { name: "link layer address", o: &LinkLayerAddress{}, subs: []sub{ { name: "short", bs: [][]byte{{0x01, 0x01, 0xff}}, }, { name: "invalid direction", bs: [][]byte{ {0x10, 0x01}, ndptest.MAC, }, }, { name: "long", bs: [][]byte{ {0x01, 0x02}, ndptest.Zero(16), }, }, }, }, { name: "mtu", o: new(MTU), subs: []sub{ { name: "short", bs: [][]byte{{0x01}}, }, }, }, { name: "prefix information", o: &PrefixInformation{}, subs: []sub{ { name: "short", bs: [][]byte{{0x01}}, }, }, }, { name: "route information", o: &RouteInformation{}, subs: []sub{ { name: "short", bs: [][]byte{{0x01}}, }, { name: "bad /0", bs: [][]byte{ // Length must be 1-3. {24, 0x04}, ndptest.Zero(30), }, }, { name: "bad /64", bs: [][]byte{ // Length must be 2-3. {24, 0x01}, {64, 0x04}, {0x00, 0x00, 0x00, 0xff}, }, }, { name: "bad /96", bs: [][]byte{ // Length must be 3. {24, 0x04}, {96, 0x04}, ndptest.Zero(28), }, }, { name: "bad /255", bs: [][]byte{ {24, 0x01}, // Invalid IPv6 prefix. {0xff, 0x00}, ndptest.Zero(4), }, }, { name: "bad preference", bs: [][]byte{ {24, 0x01}, // Reserved preference. {0, 0x10}, ndptest.Zero(4), }, }, }, }, { name: "rdnss", o: &RecursiveDNSServer{}, subs: []sub{ { name: "no servers", bs: [][]byte{ {25, 1}, // Reserved. {0x00, 0x00}, // Lifetime. ndptest.Zero(4), // No servers. }, }, { name: "bad first server", bs: [][]byte{ {25, 2}, // Reserved. {0x00, 0x00}, // Lifetime. ndptest.Zero(4), // First server, half an IPv6 address. ndptest.Zero(8), }, }, { name: "bad second server", bs: [][]byte{ {25, 4}, // Reserved. {0x00, 0x00}, // Lifetime. ndptest.Zero(4), // First server. ndptest.Zero(16), // Second server, half an IPv6 address. ndptest.Zero(8), }, }, }, }, { name: "ra flags extension", o: &RAFlagsExtension{}, subs: []sub{ { name: "short flags", bs: [][]byte{ {26, 1}, // Short flags. ndptest.Zero(5), }, }, }, }, { name: "dnssl", o: &DNSSearchList{}, subs: []sub{ { name: "no domains", bs: [][]byte{ {31, 1}, // Reserved. {0x00, 0x00}, // Lifetime. ndptest.Zero(4), // No domains. }, }, { name: "misleading length", bs: [][]byte{ {31, 2}, // Reserved. {0x00, 0x00}, // Lifetime. ndptest.Zero(4), // Length misleading. {0xff}, ndptest.Zero(7), }, }, { name: "no room for null terminator", bs: [][]byte{ {31, 2}, // Reserved. {0x00, 0x00}, // Lifetime. ndptest.Zero(4), // Length leaves no room for null terminator. {7}, ndptest.Zero(7), }, }, { name: "no domains, padded", bs: [][]byte{ {31, 2}, // Reserved. {0x00, 0x00}, // Lifetime. ndptest.Zero(4), // No domains. ndptest.Zero(8), }, }, }, }, { name: "captive portal", o: new(CaptivePortal), subs: []sub{ { name: "null URI", bs: [][]byte{ {37, 1}, // URI. ndptest.Zero(6), }, }, }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { for _, st := range tt.subs { t.Run(st.name, func(t *testing.T) { err := tt.o.unmarshal(ndptest.Merge(st.bs)) if err == nil { t.Fatal("expected an error, but none occurred") } else { t.Logf("OK error: %v", err) } }) } }) } } func TestPrefixInformationUnmarshalPrefixLength(t *testing.T) { // Assume that unmarshaling ignores any prefix bits longer than the // specified length. var ( prefix = netip.MustParseAddr("2001:db8::") l = uint8(16) want = netip.MustParseAddr("2001::") ) bs := [][]byte{ // Option type and length. {0x03, 0x04}, // Prefix Length, shorter than the prefix itself, so the prefix // should be cut off. {l}, // Flags, O and A set. {0xc0}, // Valid lifetime. {0x00, 0x00, 0x02, 0x58}, // Preferred lifetime. {0x00, 0x00, 0x04, 0xb0}, // Reserved. {0x00, 0x00, 0x00, 0x00}, // Prefix. prefix.AsSlice(), } pi := new(PrefixInformation) if err := pi.unmarshal(ndptest.Merge(bs)); err != nil { t.Fatalf("failed to unmarshal: %v", err) } // Assume that unmarshaling ignores any prefix bits longer than the // specified length. if diff := cmp.Diff(want, pi.Prefix, cmp.Comparer(addrEqual)); diff != "" { t.Fatalf("unexpected prefix (-want +got):\n%s", diff) } } func TestRouteInformationUnmarshalPrefixLength(t *testing.T) { // This route prefix easily fits in 2 bytes, but this test will also verify // it can be decoded from 3 bytes due to device behaviors seen in the wild. var ( prefix = netip.MustParseAddr("2001:db8::") mask uint8 = 64 ) tests := []struct { name string length uint8 idx int }{ { name: "length 2", length: 2, idx: net.IPv6len / 2, }, { name: "length 3", length: 3, idx: net.IPv6len, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { bs := [][]byte{ // Option type and length. Note that a /64 would normally // fit in length 2, but this option was received with padding // resulting in length 3. {24, tt.length}, // Prefix length. {mask}, // Preference. {0x00}, // Route lifetime. ndptest.Zero(4), // Prefix, possibly in a shortened form. prefix.AsSlice()[:tt.idx], } ri := new(RouteInformation) if err := ri.unmarshal(ndptest.Merge(bs)); err != nil { t.Fatalf("failed to unmarshal: %v", err) } want := &RouteInformation{ PrefixLength: mask, Prefix: prefix, } if diff := cmp.Diff(want, ri, cmp.Comparer(addrEqual)); diff != "" { t.Fatalf("unexpected route information (-want +got):\n%s", diff) } }) } } func TestNewCaptivePortalErrors(t *testing.T) { tests := []struct { name, uri string }{ { name: "bad URI", uri: "%#x", }, { name: "long URI", uri: strings.Repeat("x", 256), }, { name: "IPv4", uri: "192.0.2.0", }, { name: "IPv4 path", uri: "192.0.2.0/portal", }, { name: "IPv6", uri: "2001:db8::1", }, { name: "IPv6 path", uri: "2001:db8::1/portal", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { _, err := NewCaptivePortal(tt.uri) if err == nil { t.Fatalf("expected an error for URI %q, but got none", tt.uri) } t.Logf("err: %v", err) }) } } func llaTests() []optionSub { return []optionSub{ { name: "bad, invalid direction", os: []Option{ &LinkLayerAddress{ Direction: 10, }, }, }, { name: "bad, invalid address", os: []Option{ &LinkLayerAddress{ Direction: Source, Addr: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef}, }, }, }, { name: "ok, source", os: []Option{ &LinkLayerAddress{ Direction: Source, Addr: ndptest.MAC, }, }, bs: [][]byte{ {0x01, 0x01}, ndptest.MAC, }, ok: true, }, { name: "ok, target", os: []Option{ &LinkLayerAddress{ Direction: Target, Addr: ndptest.MAC, }, }, bs: [][]byte{ {0x02, 0x01}, ndptest.MAC, }, ok: true, }, } } func piTests() []optionSub { return []optionSub{ { name: "bad, prefix length", os: []Option{ &PrefixInformation{ // Host IP specified. PrefixLength: 64, Prefix: ndptest.IP, }, }, }, { name: "ok", os: []Option{ &PrefixInformation{ // Prefix IP specified. PrefixLength: 32, OnLink: true, AutonomousAddressConfiguration: true, ValidLifetime: Infinity, PreferredLifetime: 20 * time.Minute, Prefix: ndptest.Prefix, }, }, bs: [][]byte{ // Option type and length. {0x03, 0x04}, // Prefix Length. {32}, // Flags, O and A set. {0xc0}, // Valid lifetime. {0xff, 0xff, 0xff, 0xff}, // Preferred lifetime. {0x00, 0x00, 0x04, 0xb0}, // Reserved. {0x00, 0x00, 0x00, 0x00}, // Prefix. ndptest.Prefix.AsSlice(), }, ok: true, }, } } func riTests() []optionSub { return []optionSub{ { name: "bad, prefix length", os: []Option{ &RouteInformation{ // Host IP specified. PrefixLength: 64, Prefix: ndptest.IP, }, }, }, { name: "bad, prefix invalid", os: []Option{ &RouteInformation{ // Host IP specified. PrefixLength: 255, }, }, }, { name: "ok /0", os: []Option{ &RouteInformation{ PrefixLength: 0, Preference: High, RouteLifetime: Infinity, Prefix: netip.IPv6Unspecified(), }, }, bs: [][]byte{ // Option type and length. {24, 0x01}, // Prefix length. {0}, // Preference. {0x08}, // Route lifetime. {0xff, 0xff, 0xff, 0xff}, }, ok: true, }, { name: "ok /64", os: []Option{ &RouteInformation{ PrefixLength: 64, Preference: Low, RouteLifetime: 1 * time.Second, Prefix: ndptest.Prefix, }, }, bs: [][]byte{ // Option type and length. {24, 0x02}, // Prefix length. {64}, // Preference. {0x18}, // Route lifetime. {0x00, 0x00, 0x00, 0x01}, // Prefix, second half omitted due to /64 length. {0x20, 0x1, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00}, }, ok: true, }, { name: "ok /96", os: []Option{ &RouteInformation{ PrefixLength: 96, Preference: Medium, RouteLifetime: 255 * time.Second, Prefix: ndptest.Prefix, }, }, bs: [][]byte{ // Option type and length. {24, 0x03}, // Prefix length. {96}, // Preference. {0x00}, // Route lifetime. {0x00, 0x00, 0x00, 0xff}, // Prefix, full size due to /96 length. {0x20, 0x1, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00}, ndptest.Zero(8), }, ok: true, }, } } func roTests() []optionSub { return []optionSub{ { name: "bad, length", os: []Option{ &RawOption{ Type: 1, Length: 1, Value: ndptest.Zero(7), }, }, }, { name: "ok", os: []Option{ &RawOption{ Type: 10, Length: 2, Value: ndptest.Zero(14), }, }, bs: [][]byte{ {0x0a, 0x02}, ndptest.Zero(14), }, ok: true, }, } } func rdnssTests() []optionSub { var ( first = netip.MustParseAddr("2001:db8::1") second = netip.MustParseAddr("2001:db8::2") ) return []optionSub{ { name: "bad, no servers", os: []Option{ &RecursiveDNSServer{ Lifetime: 1 * time.Second, }, }, }, { name: "ok, one server", os: []Option{ &RecursiveDNSServer{ Lifetime: 1 * time.Hour, Servers: []netip.Addr{first}, }, }, bs: [][]byte{ {25, 3}, {0x00, 0x00}, {0x00, 0x00, 0x0e, 0x10}, first.AsSlice(), }, ok: true, }, { name: "ok, two servers", os: []Option{ &RecursiveDNSServer{ Lifetime: 24 * time.Hour, Servers: []netip.Addr{first, second}, }, }, bs: [][]byte{ {25, 5}, {0x00, 0x00}, {0x00, 0x01, 0x51, 0x80}, first.AsSlice(), second.AsSlice(), }, ok: true, }, } } func raFlagsExtensionTests() []optionSub { return []optionSub{ { name: "bad, no flags", os: []Option{ &RAFlagsExtension{}, }, }, { name: "bad, zero flags", os: []Option{ &RAFlagsExtension{ Flags: RAFlags(ndptest.Zero(6)), }, }, }, { name: "bad, short padding", os: []Option{ &RAFlagsExtension{ Flags: RAFlags{ 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, }, }, }, { name: "ok, length 1", os: []Option{ &RAFlagsExtension{ Flags: RAFlags{0x80, 0x00, 0x00, 0x00, 0x00, 0x00}, }, }, bs: [][]byte{ {26, 1}, // Short values. {128, 0, 0, 0, 0, 0}, }, ok: true, }, { name: "ok, length 2", os: []Option{ &RAFlagsExtension{ Flags: RAFlags{ 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, }, }, bs: [][]byte{ {26, 2}, // Short values. { 128, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, }, ok: true, }, } } func dnsslTests() []optionSub { return []optionSub{ { name: "bad, no domains", os: []Option{ &DNSSearchList{ Lifetime: 1 * time.Second, }, }, }, { name: "ok, one domain", os: []Option{ &DNSSearchList{ Lifetime: 1 * time.Hour, DomainNames: []string{"example.com"}, }, }, bs: [][]byte{ {31, 3}, // Reserved. {0x00, 0x00}, // Lifetime. {0x00, 0x00, 0x0e, 0x10}, // Labels. {7}, []byte("example"), {3}, []byte("com"), {0x00}, // Padding. ndptest.Zero(3), }, ok: true, }, { name: "ok, multiple servers", os: []Option{ &DNSSearchList{ Lifetime: 1 * time.Hour, DomainNames: []string{ "example.com", "foo.example.com", "bar.foo.example.com", }, }, }, bs: [][]byte{ {31, 8}, // Reserved. {0x00, 0x00}, // Lifetime. {0x00, 0x00, 0x0e, 0x10}, // Labels. {7}, []byte("example"), {3}, []byte("com"), {0x00}, {3}, []byte("foo"), {7}, []byte("example"), {3}, []byte("com"), {0x00}, {3}, []byte("bar"), {3}, []byte("foo"), {7}, []byte("example"), {3}, []byte("com"), {0x00}, // Padding. ndptest.Zero(5), }, ok: true, }, { name: "ok, punycode domain", os: []Option{ &DNSSearchList{ Lifetime: 1 * time.Hour, DomainNames: []string{"😃.example.com"}, }, }, bs: [][]byte{ {31, 4}, // Reserved. {0x00, 0x00}, // Lifetime. {0x00, 0x00, 0x0e, 0x10}, // Labels. {8}, []byte("xn--h28h"), {7}, []byte("example"), {3}, []byte("com"), {0x00}, // Padding. ndptest.Zero(2), }, ok: true, }, } } func cpTests() []optionSub { urnBytes := [][]byte{ {37, 5}, // URI. []byte(Unrestricted), // Padding. ndptest.Zero(2), } return []optionSub{ // Some of these cases are not permitted by the constructor; create them // manually. The RFC says "SHOULD NOT" but not "MUST NOT". { name: "bad, empty", os: []Option{&CaptivePortal{URI: ""}}, }, { name: "ok, IP", os: []Option{&CaptivePortal{URI: "2001:db8::1"}}, bs: [][]byte{ {37, 2}, // URI. []byte("2001:db8::1"), // Padding. ndptest.Zero(3), }, ok: true, }, { name: "ok, no padding", os: []Option{mustCaptivePortal("urn:xx")}, bs: [][]byte{ {37, 1}, // URI. {'u', 'r', 'n', ':', 'x', 'x'}, }, ok: true, }, { name: "ok, padding", os: []Option{mustCaptivePortal(Unrestricted)}, bs: urnBytes, ok: true, }, { name: "ok, default URN", os: []Option{mustCaptivePortal("")}, bs: urnBytes, ok: true, }, } } func pref64Tests() []optionSub { return []optionSub{ { name: "bad, invalid prefix size", os: []Option{ &PREF64{Prefix: netip.MustParsePrefix("2001:db8::/33"), Lifetime: time.Duration(0)}, }, }, { name: "bad, invalid lifetime", os: []Option{ &PREF64{Prefix: netip.MustParsePrefix("2001:db8::/32"), Lifetime: time.Hour * 24}, }, }, { name: "ok, smallest prefix, max lifetime", os: []Option{ &PREF64{Prefix: netip.MustParsePrefix("2001:db8::/96"), Lifetime: time.Second * 8 * 8191}, }, bs: [][]byte{ {0x26, 0x02}, { 0xff, 0xf8, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, }, ok: true, }, { name: "ok, /64 prefix", os: []Option{ &PREF64{Prefix: netip.MustParsePrefix("2001:db8::/64"), Lifetime: time.Second * 8 * 8191}, }, bs: [][]byte{ {0x26, 0x02}, { 0xff, 0xf9, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, }, ok: true, }, { name: "ok, /56 prefix", os: []Option{ &PREF64{Prefix: netip.MustParsePrefix("2001:db8::/56"), Lifetime: time.Second * 8 * 8191}, }, bs: [][]byte{ {0x26, 0x02}, { 0xff, 0xfa, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, }, ok: true, }, { name: "ok, /48 prefix", os: []Option{ &PREF64{Prefix: netip.MustParsePrefix("2001:db8::/48"), Lifetime: time.Second * 8 * 8191}, }, bs: [][]byte{ {0x26, 0x02}, { 0xff, 0xfb, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, }, ok: true, }, { name: "ok, /40 prefix", os: []Option{ &PREF64{Prefix: netip.MustParsePrefix("2001:db8::/40"), Lifetime: time.Second * 8 * 8191}, }, bs: [][]byte{ {0x26, 0x02}, { 0xff, 0xfc, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, }, ok: true, }, { name: "ok, maximum prefix, small lifetime", os: []Option{ &PREF64{Prefix: netip.MustParsePrefix("2001:db8::/32"), Lifetime: time.Minute * 10}, }, bs: [][]byte{ {0x26, 0x02}, { 0x02, 0x5d, 0x20, 0x01, 0x0d, 0xb8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, }, }, ok: true, }, } } func nonceTests() []optionSub { nonce := NewNonce() return []optionSub{ { name: "bad, empty", os: []Option{&Nonce{}}, }, { name: "bad, unaligned", os: []Option{&Nonce{b: []byte{0xff}}}, }, { name: "ok, minimum length", os: []Option{&Nonce{b: make([]byte, 6)}}, bs: [][]byte{ {14, 1}, // Nonce. ndptest.Zero(6), }, ok: true, }, { name: "ok, larger length", os: []Option{&Nonce{b: make([]byte, 14)}}, bs: [][]byte{ {14, 2}, // Nonce. ndptest.Zero(14), }, ok: true, }, { name: "ok, random", os: []Option{nonce}, bs: [][]byte{ {14, 1}, // Nonce. nonce.b, }, ok: true, }, } } func mustCaptivePortal(uri string) *CaptivePortal { cp, err := NewCaptivePortal(uri) if err != nil { panicf("failed to parse captive portal URI: %v", err) } return cp } golang-github-mdlayher-ndp-1.1.0/string.go000066400000000000000000000012771457561310000204700ustar00rootroot00000000000000// Code generated by "stringer -type=Preference -output=string.go"; DO NOT EDIT. package ndp import "strconv" func _() { // An "invalid array index" compiler error signifies that the constant values have changed. // Re-run the stringer command to generate them again. var x [1]struct{} _ = x[Medium-0] _ = x[High-1] _ = x[prfReserved-2] _ = x[Low-3] } const _Preference_name = "MediumHighprfReservedLow" var _Preference_index = [...]uint8{0, 6, 10, 21, 24} func (i Preference) String() string { if i < 0 || i >= Preference(len(_Preference_index)-1) { return "Preference(" + strconv.FormatInt(int64(i), 10) + ")" } return _Preference_name[_Preference_index[i]:_Preference_index[i+1]] }