├── doc.go ├── go.mod ├── fuzz.go ├── .travis.yml ├── string.go ├── cmd ├── arpc │ ├── README.md │ └── main.go └── proxyarpd │ └── main.go ├── README.md ├── LICENSE.md ├── go.sum ├── client_test.go ├── client.go ├── packet.go ├── client_request_test.go └── packet_test.go /doc.go: -------------------------------------------------------------------------------- 1 | // Package arp implements the ARP protocol, as described in RFC 826. 2 | package arp 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/mdlayher/arp 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 7 | github.com/mdlayher/packet v1.0.0 8 | ) 9 | -------------------------------------------------------------------------------- /fuzz.go: -------------------------------------------------------------------------------- 1 | //go:build gofuzz 2 | // +build gofuzz 3 | 4 | package arp 5 | 6 | func Fuzz(data []byte) int { 7 | p := new(Packet) 8 | if err := p.UnmarshalBinary(data); err != nil { 9 | return 0 10 | } 11 | 12 | if _, err := p.MarshalBinary(); err != nil { 13 | panic(err) 14 | } 15 | 16 | return 1 17 | } 18 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | go: 3 | - 1.x 4 | os: 5 | - linux 6 | before_install: 7 | - go get golang.org/x/lint/golint 8 | - go get honnef.co/go/tools/cmd/staticcheck 9 | - go get -d ./... 10 | script: 11 | - go build -tags=gofuzz ./... 12 | - go vet ./... 13 | - staticcheck ./... 14 | - golint -set_exit_status ./... 15 | - go test -v -race ./... -------------------------------------------------------------------------------- /string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -output=string.go -type=Operation"; DO NOT EDIT. 2 | 3 | package arp 4 | 5 | import "strconv" 6 | 7 | const _Operation_name = "OperationRequestOperationReply" 8 | 9 | var _Operation_index = [...]uint8{0, 16, 30} 10 | 11 | func (i Operation) String() string { 12 | i -= 1 13 | if i >= Operation(len(_Operation_index)-1) { 14 | return "Operation(" + strconv.FormatInt(int64(i+1), 10) + ")" 15 | } 16 | return _Operation_name[_Operation_index[i]:_Operation_index[i+1]] 17 | } 18 | -------------------------------------------------------------------------------- /cmd/arpc/README.md: -------------------------------------------------------------------------------- 1 | arpc 2 | ===== 3 | 4 | Command `arpc` provides a simple ARP client which can be used to retrieve 5 | hardware addresses of other machines in a LAN using their IPv4 addresses. 6 | 7 | Usage 8 | ----- 9 | 10 | ``` 11 | $ ./arpc -h 12 | Usage of ./arpc: 13 | -d=1s: timeout for ARP request 14 | -i="eth0": network interface to use for ARP request 15 | -ip="": IPv4 address destination for ARP request 16 | ``` 17 | 18 | Request hardware address for IPv4 address: 19 | 20 | ``` 21 | $ ./arpc -i eth0 -ip 192.168.1.1 22 | 192.168.1.1 -> 00:12:7f:eb:6b:40 23 | ``` 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | arp [![Build Status](https://travis-ci.org/mdlayher/arp.svg?branch=master)](https://travis-ci.org/mdlayher/arp) [![GoDoc](https://godoc.org/github.com/mdlayher/arp?status.svg)](https://godoc.org/github.com/mdlayher/arp) [![Go Report Card](https://goreportcard.com/badge/github.com/mdlayher/arp)](https://goreportcard.com/report/github.com/mdlayher/arp) 2 | === 3 | 4 | Package `arp` implements the ARP protocol, as described in RFC 826. 5 | MIT Licensed. 6 | 7 | Portions of this code are taken from the Go standard library. The Go 8 | standard library is Copyright (c) 2012 The Go Authors. All rights reserved. 9 | The Go license can be found at https://golang.org/LICENSE. 10 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | =========== 3 | 4 | Copyright (C) 2015 Matt Layher 5 | 6 | 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: 7 | 8 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 9 | 10 | 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. 11 | -------------------------------------------------------------------------------- /cmd/arpc/main.go: -------------------------------------------------------------------------------- 1 | // Command arpc provides a simple ARP client which can be used to retrieve 2 | // hardware addresses of other machines in a LAN using their IPv4 address. 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "log" 9 | "net" 10 | "net/netip" 11 | "time" 12 | 13 | "github.com/mdlayher/arp" 14 | ) 15 | 16 | var ( 17 | // durFlag is used to set a timeout for an ARP request 18 | durFlag = flag.Duration("d", 1*time.Second, "timeout for ARP request") 19 | 20 | // ifaceFlag is used to set a network interface for ARP requests 21 | ifaceFlag = flag.String("i", "eth0", "network interface to use for ARP request") 22 | 23 | // ipFlag is used to set an IPv4 address destination for an ARP request 24 | ipFlag = flag.String("ip", "", "IPv4 address destination for ARP request") 25 | ) 26 | 27 | func main() { 28 | flag.Parse() 29 | 30 | // Ensure valid network interface 31 | ifi, err := net.InterfaceByName(*ifaceFlag) 32 | if err != nil { 33 | log.Fatal(err) 34 | } 35 | 36 | // Set up ARP client with socket 37 | c, err := arp.Dial(ifi) 38 | if err != nil { 39 | log.Fatal(err) 40 | } 41 | defer c.Close() 42 | 43 | // Set request deadline from flag 44 | if err := c.SetDeadline(time.Now().Add(*durFlag)); err != nil { 45 | log.Fatal(err) 46 | } 47 | 48 | // Request hardware address for IP address 49 | ip, err := netip.ParseAddr(*ipFlag) 50 | if err != nil { 51 | log.Fatal(err) 52 | } 53 | mac, err := c.Resolve(ip) 54 | if err != nil { 55 | log.Fatal(err) 56 | } 57 | 58 | fmt.Printf("%s -> %s", ip, mac) 59 | } 60 | -------------------------------------------------------------------------------- /cmd/proxyarpd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "io" 7 | "log" 8 | "net" 9 | "net/netip" 10 | 11 | "github.com/mdlayher/arp" 12 | "github.com/mdlayher/ethernet" 13 | ) 14 | 15 | var ( 16 | // ifaceFlag is used to set a network interface for ARP traffic 17 | ifaceFlag = flag.String("i", "eth0", "network interface to use for ARP traffic") 18 | 19 | // ipFlag is used to set an IPv4 address to proxy ARP on behalf of 20 | ipFlag = flag.String("ip", "", "IP address for device to proxy ARP on behalf of") 21 | ) 22 | 23 | func main() { 24 | flag.Parse() 25 | 26 | // Ensure valid interface and IPv4 address 27 | ifi, err := net.InterfaceByName(*ifaceFlag) 28 | if err != nil { 29 | log.Fatal(err) 30 | } 31 | ip, err := netip.ParseAddr(*ipFlag) 32 | if err != nil || !ip.Is4() { 33 | log.Fatalf("invalid IPv4 address: %q", *ipFlag) 34 | } 35 | 36 | client, err := arp.Dial(ifi) 37 | if err != nil { 38 | log.Fatalf("couldn't create ARP client: %s", err) 39 | } 40 | 41 | // Handle ARP requests bound for designated IPv4 address, using proxy ARP 42 | // to indicate that the address belongs to this machine 43 | for { 44 | pkt, eth, err := client.Read() 45 | if err != nil { 46 | if err == io.EOF { 47 | log.Println("EOF") 48 | break 49 | } 50 | log.Fatalf("error processing ARP requests: %s", err) 51 | } 52 | 53 | // Ignore ARP replies 54 | if pkt.Operation != arp.OperationRequest { 55 | continue 56 | } 57 | 58 | // Ignore ARP requests which are not broadcast or bound directly for 59 | // this machine 60 | if !bytes.Equal(eth.Destination, ethernet.Broadcast) && !bytes.Equal(eth.Destination, ifi.HardwareAddr) { 61 | continue 62 | } 63 | 64 | log.Printf("request: who-has %s? tell %s (%s)", pkt.TargetIP, pkt.SenderIP, pkt.SenderHardwareAddr) 65 | 66 | // Ignore ARP requests which do not indicate the target IP 67 | if pkt.TargetIP != ip { 68 | continue 69 | } 70 | 71 | log.Printf(" reply: %s is-at %s", ip, ifi.HardwareAddr) 72 | if err := client.Reply(pkt, ifi.HardwareAddr, ip); err != nil { 73 | log.Fatal(err) 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 2 | github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= 3 | github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= 4 | github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk= 5 | github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= 6 | github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 h1:2oDp6OOhLxQ9JBoUuysVz9UZ9uI6oLUbvAZu0x8o+vE= 7 | github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118/go.mod h1:ZFUnHIVchZ9lJoWoEGUg8Q3M4U8aNNWA3CVSUTkW4og= 8 | github.com/mdlayher/packet v1.0.0 h1:InhZJbdShQYt6XV2GPj5XHxChzOfhJJOMbvnGAmOfQ8= 9 | github.com/mdlayher/packet v1.0.0/go.mod h1:eE7/ctqDhoiRhQ44ko5JZU2zxB88g+JH/6jmnjzPjOU= 10 | github.com/mdlayher/socket v0.2.1 h1:F2aaOwb53VsBE+ebRS9bLd7yPOfYUMC8lOODdCBDY6w= 11 | github.com/mdlayher/socket v0.2.1/go.mod h1:QLlNPkFR88mRUNQIzRBMfXxwKal8H7u1h3bL1CV+f0E= 12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 13 | golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 14 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65 h1:+rhAzEzT3f4JtomfC371qB+0Ola2caSKcY69NUBZrRQ= 15 | golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= 16 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 17 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 18 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 19 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 20 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158 h1:rm+CHSpPEEW2IsXUib1ThaHIjuBVZjxNgSKmBLFfD4c= 21 | golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 22 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 23 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= 24 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 25 | -------------------------------------------------------------------------------- /client_test.go: -------------------------------------------------------------------------------- 1 | package arp 2 | 3 | import ( 4 | "net" 5 | "net/netip" 6 | "reflect" 7 | "testing" 8 | "time" 9 | ) 10 | 11 | func TestClientClose(t *testing.T) { 12 | p := &closeCapturePacketConn{} 13 | c := &Client{p: p} 14 | 15 | if err := c.Close(); err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | if !p.closed { 20 | t.Fatal("client was not closed") 21 | } 22 | } 23 | 24 | func TestClientSetDeadline(t *testing.T) { 25 | p := &deadlineCapturePacketConn{} 26 | c := &Client{p: p} 27 | 28 | d := time.Now() 29 | if err := c.SetDeadline(d); err != nil { 30 | t.Fatal(err) 31 | } 32 | 33 | if want, got := d, p.r; want != got { 34 | t.Fatalf("unexpected read deadline: %v != %v", want, got) 35 | } 36 | if want, got := d, p.w; want != got { 37 | t.Fatalf("unexpected write deadline: %v != %v", want, got) 38 | } 39 | } 40 | 41 | func TestClientSetReadDeadline(t *testing.T) { 42 | p := &deadlineCapturePacketConn{} 43 | c := &Client{p: p} 44 | 45 | d := time.Now() 46 | if err := c.SetReadDeadline(d); err != nil { 47 | t.Fatal(err) 48 | } 49 | 50 | if want, got := d, p.r; want != got { 51 | t.Fatalf("unexpected read deadline: %v != %v", want, got) 52 | } 53 | if want, got := (time.Time{}), p.w; want != got { 54 | t.Fatalf("non-zero write deadline: %v", got) 55 | } 56 | } 57 | 58 | func TestClientSetWriteDeadline(t *testing.T) { 59 | p := &deadlineCapturePacketConn{} 60 | c := &Client{p: p} 61 | 62 | d := time.Now() 63 | if err := c.SetWriteDeadline(d); err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | if want, got := (time.Time{}), p.r; want != got { 68 | t.Fatalf("non-zero read deadline: %v", got) 69 | } 70 | if want, got := d, p.w; want != got { 71 | t.Fatalf("unexpected write deadline: %v != %v", want, got) 72 | } 73 | } 74 | 75 | func TestClientHardwareAddr(t *testing.T) { 76 | c := &Client{ 77 | ifi: &net.Interface{ 78 | HardwareAddr: net.HardwareAddr{0, 1, 2, 3, 4, 5}, 79 | }, 80 | } 81 | 82 | if want, got := c.ifi.HardwareAddr.String(), c.HardwareAddr().String(); want != got { 83 | t.Fatalf("unexpected hardware address: %v != %v", want, got) 84 | } 85 | } 86 | 87 | func Test_newClient(t *testing.T) { 88 | tests := []struct { 89 | desc string 90 | addrs []netip.Addr 91 | c *Client 92 | err error 93 | }{ 94 | { 95 | desc: "no network addresses", 96 | c: &Client{}, 97 | err: errNoIPv4Addr, 98 | }, 99 | { 100 | desc: "OK", 101 | addrs: []netip.Addr{ 102 | netip.MustParseAddr("192.168.1.1"), 103 | }, 104 | c: &Client{ 105 | ip: netip.MustParseAddr("192.168.1.1"), 106 | }, 107 | }, 108 | } 109 | 110 | for i, tt := range tests { 111 | c, err := newClient(nil, nil, tt.addrs) 112 | if err != nil { 113 | if want, got := tt.err.Error(), err.Error(); want != got { 114 | t.Fatalf("[%02d] test %q, unexpected error: %v != %v", 115 | i, tt.desc, want, got) 116 | } 117 | 118 | continue 119 | } 120 | 121 | if want, got := tt.c, c; !reflect.DeepEqual(want, got) { 122 | t.Fatalf("[%02d] test %q, unexpected Client: %v != %v", 123 | i, tt.desc, want, got) 124 | } 125 | } 126 | } 127 | 128 | type closeCapturePacketConn struct { 129 | closed bool 130 | 131 | noopPacketConn 132 | } 133 | 134 | func (p *closeCapturePacketConn) Close() error { 135 | p.closed = true 136 | return nil 137 | } 138 | 139 | // deadlineCapturePacketConn is a net.PacketConn which captures read and 140 | // write deadlines. 141 | type deadlineCapturePacketConn struct { 142 | r time.Time 143 | w time.Time 144 | 145 | noopPacketConn 146 | } 147 | 148 | func (p *deadlineCapturePacketConn) SetDeadline(t time.Time) error { 149 | p.r = t 150 | p.w = t 151 | return nil 152 | } 153 | 154 | func (p *deadlineCapturePacketConn) SetReadDeadline(t time.Time) error { 155 | p.r = t 156 | return nil 157 | } 158 | 159 | func (p *deadlineCapturePacketConn) SetWriteDeadline(t time.Time) error { 160 | p.w = t 161 | return nil 162 | } 163 | 164 | // noopPacketConn is a net.PacketConn which simply no-ops any input. It is 165 | // embedded in other implementations so they do not have to implement every 166 | // single method. 167 | type noopPacketConn struct{} 168 | 169 | func (noopPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, nil } 170 | func (noopPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { return 0, nil } 171 | 172 | func (noopPacketConn) Close() error { return nil } 173 | func (noopPacketConn) LocalAddr() net.Addr { return nil } 174 | func (noopPacketConn) SetDeadline(t time.Time) error { return nil } 175 | func (noopPacketConn) SetReadDeadline(t time.Time) error { return nil } 176 | func (noopPacketConn) SetWriteDeadline(t time.Time) error { return nil } 177 | func (noopPacketConn) HardwareAddr() net.HardwareAddr { return nil } 178 | -------------------------------------------------------------------------------- /client.go: -------------------------------------------------------------------------------- 1 | package arp 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/netip" 7 | "time" 8 | 9 | "github.com/mdlayher/ethernet" 10 | "github.com/mdlayher/packet" 11 | ) 12 | 13 | // errNoIPv4Addr is returned when an interface does not have an IPv4 14 | // address. 15 | var errNoIPv4Addr = errors.New("no IPv4 address available for interface") 16 | 17 | // protocolARP is the uint16 EtherType representation of ARP (Address 18 | // Resolution Protocol, RFC 826). 19 | const protocolARP = 0x0806 20 | 21 | // A Client is an ARP client, which can be used to send and receive 22 | // ARP packets. 23 | type Client struct { 24 | ifi *net.Interface 25 | ip netip.Addr 26 | p net.PacketConn 27 | } 28 | 29 | // Dial creates a new Client using the specified network interface. 30 | // Dial retrieves the IPv4 address of the interface and binds a raw socket 31 | // to send and receive ARP packets. 32 | func Dial(ifi *net.Interface) (*Client, error) { 33 | // Open raw socket to send and receive ARP packets using ethernet frames 34 | // we build ourselves. 35 | p, err := packet.Listen(ifi, packet.Raw, protocolARP, nil) 36 | if err != nil { 37 | return nil, err 38 | } 39 | return New(ifi, p) 40 | } 41 | 42 | // New creates a new Client using the specified network interface 43 | // and net.PacketConn. This allows the caller to define exactly how they bind to the 44 | // net.PacketConn. This is most useful to define what protocol to pass to socket(7). 45 | // 46 | // In most cases, callers would be better off calling Dial. 47 | func New(ifi *net.Interface, p net.PacketConn) (*Client, error) { 48 | // Check for usable IPv4 addresses for the Client 49 | addrs, err := ifi.Addrs() 50 | if err != nil { 51 | return nil, err 52 | } 53 | 54 | ipaddrs := make([]netip.Addr, len(addrs)) 55 | for i, a := range addrs { 56 | ipPrefix, err := netip.ParsePrefix(a.String()) 57 | if err != nil { 58 | return nil, err 59 | } 60 | ipaddrs[i] = ipPrefix.Addr() 61 | } 62 | 63 | return newClient(ifi, p, ipaddrs) 64 | } 65 | 66 | // newClient is the internal, generic implementation of newClient. It is used 67 | // to allow an arbitrary net.PacketConn to be used in a Client, so testing 68 | // is easier to accomplish. 69 | func newClient(ifi *net.Interface, p net.PacketConn, addrs []netip.Addr) (*Client, error) { 70 | ip, err := firstIPv4Addr(addrs) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | return &Client{ 76 | ifi: ifi, 77 | ip: ip, 78 | p: p, 79 | }, nil 80 | } 81 | 82 | // Close closes the Client's raw socket and stops sending and receiving 83 | // ARP packets. 84 | func (c *Client) Close() error { 85 | return c.p.Close() 86 | } 87 | 88 | // Request sends an ARP request, asking for the hardware address 89 | // associated with an IPv4 address. The response, if any, can be read 90 | // with the Read method. 91 | // 92 | // Unlike Resolve, which provides an easier interface for getting the 93 | // hardware address, Request allows sending many requests in a row, 94 | // retrieving the responses afterwards. 95 | func (c *Client) Request(ip netip.Addr) error { 96 | if !c.ip.IsValid() { 97 | return errNoIPv4Addr 98 | } 99 | 100 | // Create ARP packet for broadcast address to attempt to find the 101 | // hardware address of the input IP address 102 | arp, err := NewPacket(OperationRequest, c.ifi.HardwareAddr, c.ip, ethernet.Broadcast, ip) 103 | if err != nil { 104 | return err 105 | } 106 | return c.WriteTo(arp, ethernet.Broadcast) 107 | } 108 | 109 | // Resolve performs an ARP request, attempting to retrieve the 110 | // hardware address of a machine using its IPv4 address. Resolve must not 111 | // be used concurrently with Read. If you're using Read (usually in a 112 | // loop), you need to use Request instead. Resolve may read more than 113 | // one message if it receives messages unrelated to the request. 114 | func (c *Client) Resolve(ip netip.Addr) (net.HardwareAddr, error) { 115 | err := c.Request(ip) 116 | if err != nil { 117 | return nil, err 118 | } 119 | 120 | // Loop and wait for replies 121 | for { 122 | arp, _, err := c.Read() 123 | if err != nil { 124 | return nil, err 125 | } 126 | 127 | if arp.Operation != OperationReply || arp.SenderIP != ip { 128 | continue 129 | } 130 | 131 | return arp.SenderHardwareAddr, nil 132 | } 133 | } 134 | 135 | // Read reads a single ARP packet and returns it, together with its 136 | // ethernet frame. 137 | func (c *Client) Read() (*Packet, *ethernet.Frame, error) { 138 | buf := make([]byte, 128) 139 | for { 140 | n, _, err := c.p.ReadFrom(buf) 141 | if err != nil { 142 | return nil, nil, err 143 | } 144 | 145 | p, eth, err := parsePacket(buf[:n]) 146 | if err != nil { 147 | if err == errInvalidARPPacket { 148 | continue 149 | } 150 | return nil, nil, err 151 | } 152 | return p, eth, nil 153 | } 154 | } 155 | 156 | // WriteTo writes a single ARP packet to addr. Note that addr should, 157 | // but doesn't have to, match the target hardware address of the ARP 158 | // packet. 159 | func (c *Client) WriteTo(p *Packet, addr net.HardwareAddr) error { 160 | pb, err := p.MarshalBinary() 161 | if err != nil { 162 | return err 163 | } 164 | 165 | f := ðernet.Frame{ 166 | Destination: addr, 167 | Source: p.SenderHardwareAddr, 168 | EtherType: ethernet.EtherTypeARP, 169 | Payload: pb, 170 | } 171 | 172 | fb, err := f.MarshalBinary() 173 | if err != nil { 174 | return err 175 | } 176 | 177 | _, err = c.p.WriteTo(fb, &packet.Addr{HardwareAddr: addr}) 178 | return err 179 | } 180 | 181 | // Reply constructs and sends a reply to an ARP request. On the ARP 182 | // layer, it will be addressed to the sender address of the packet. On 183 | // the ethernet layer, it will be sent to the actual remote address 184 | // from which the request was received. 185 | // 186 | // For more fine-grained control, use WriteTo to write a custom 187 | // response. 188 | func (c *Client) Reply(req *Packet, hwAddr net.HardwareAddr, ip netip.Addr) error { 189 | p, err := NewPacket(OperationReply, hwAddr, ip, req.SenderHardwareAddr, req.SenderIP) 190 | if err != nil { 191 | return err 192 | } 193 | return c.WriteTo(p, req.SenderHardwareAddr) 194 | } 195 | 196 | // Copyright (c) 2012 The Go Authors. All rights reserved. 197 | // Source code in this file is based on src/net/interface_linux.go, 198 | // from the Go standard library. The Go license can be found here: 199 | // https://golang.org/LICENSE. 200 | 201 | // Documentation taken from net.PacketConn interface. Thanks: 202 | // http://golang.org/pkg/net/#PacketConn. 203 | 204 | // SetDeadline sets the read and write deadlines associated with the 205 | // connection. 206 | func (c *Client) SetDeadline(t time.Time) error { 207 | return c.p.SetDeadline(t) 208 | } 209 | 210 | // SetReadDeadline sets the deadline for future raw socket read calls. 211 | // If the deadline is reached, a raw socket read will fail with a timeout 212 | // (see type net.Error) instead of blocking. 213 | // A zero value for t means a raw socket read will not time out. 214 | func (c *Client) SetReadDeadline(t time.Time) error { 215 | return c.p.SetReadDeadline(t) 216 | } 217 | 218 | // SetWriteDeadline sets the deadline for future raw socket write calls. 219 | // If the deadline is reached, a raw socket write will fail with a timeout 220 | // (see type net.Error) instead of blocking. 221 | // A zero value for t means a raw socket write will not time out. 222 | // Even if a write times out, it may return n > 0, indicating that 223 | // some of the data was successfully written. 224 | func (c *Client) SetWriteDeadline(t time.Time) error { 225 | return c.p.SetWriteDeadline(t) 226 | } 227 | 228 | // HardwareAddr fetches the hardware address for the interface associated 229 | // with the connection. 230 | func (c Client) HardwareAddr() net.HardwareAddr { 231 | return c.ifi.HardwareAddr 232 | } 233 | 234 | // firstIPv4Addr attempts to retrieve the first detected IPv4 address from an 235 | // input slice of network addresses. 236 | func firstIPv4Addr(addrs []netip.Addr) (netip.Addr, error) { 237 | for _, a := range addrs { 238 | if a.Is4() { 239 | return a, nil 240 | } 241 | } 242 | return netip.Addr{}, errNoIPv4Addr 243 | } 244 | -------------------------------------------------------------------------------- /packet.go: -------------------------------------------------------------------------------- 1 | package arp 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "errors" 7 | "io" 8 | "net" 9 | "net/netip" 10 | 11 | "github.com/mdlayher/ethernet" 12 | ) 13 | 14 | var ( 15 | // ErrInvalidHardwareAddr is returned when one or more invalid hardware 16 | // addresses are passed to NewPacket. 17 | ErrInvalidHardwareAddr = errors.New("invalid hardware address") 18 | 19 | // ErrInvalidIP is returned when one or more invalid IPv4 addresses are 20 | // passed to NewPacket. 21 | ErrInvalidIP = errors.New("invalid IPv4 address") 22 | 23 | // errInvalidARPPacket is returned when an ethernet frame does not 24 | // indicate that an ARP packet is contained in its payload. 25 | errInvalidARPPacket = errors.New("invalid ARP packet") 26 | ) 27 | 28 | //go:generate stringer -output=string.go -type=Operation 29 | 30 | // An Operation is an ARP operation, such as request or reply. 31 | type Operation uint16 32 | 33 | // Operation constants which indicate an ARP request or reply. 34 | const ( 35 | OperationRequest Operation = 1 36 | OperationReply Operation = 2 37 | ) 38 | 39 | // A Packet is a raw ARP packet, as described in RFC 826. 40 | type Packet struct { 41 | // HardwareType specifies an IANA-assigned hardware type, as described 42 | // in RFC 826. 43 | HardwareType uint16 44 | 45 | // ProtocolType specifies the internetwork protocol for which the ARP 46 | // request is intended. Typically, this is the IPv4 EtherType. 47 | ProtocolType uint16 48 | 49 | // HardwareAddrLength specifies the length of the sender and target 50 | // hardware addresses included in a Packet. 51 | HardwareAddrLength uint8 52 | 53 | // IPLength specifies the length of the sender and target IPv4 addresses 54 | // included in a Packet. 55 | IPLength uint8 56 | 57 | // Operation specifies the ARP operation being performed, such as request 58 | // or reply. 59 | Operation Operation 60 | 61 | // SenderHardwareAddr specifies the hardware address of the sender of this 62 | // Packet. 63 | SenderHardwareAddr net.HardwareAddr 64 | 65 | // SenderIP specifies the IPv4 address of the sender of this Packet. 66 | SenderIP netip.Addr 67 | 68 | // TargetHardwareAddr specifies the hardware address of the target of this 69 | // Packet. 70 | TargetHardwareAddr net.HardwareAddr 71 | 72 | // TargetIP specifies the IPv4 address of the target of this Packet. 73 | TargetIP netip.Addr 74 | } 75 | 76 | // NewPacket creates a new Packet from an input Operation and hardware/IPv4 77 | // address values for both a sender and target. 78 | // 79 | // If either hardware address is less than 6 bytes in length, or there is a 80 | // length mismatch between the two, ErrInvalidHardwareAddr is returned. 81 | // 82 | // If either IP address is not an IPv4 address, or there is a length mismatch 83 | // between the two, ErrInvalidIP is returned. 84 | func NewPacket(op Operation, srcHW net.HardwareAddr, srcIP netip.Addr, dstHW net.HardwareAddr, dstIP netip.Addr) (*Packet, error) { 85 | // Validate hardware addresses for minimum length, and matching length 86 | if len(srcHW) < 6 { 87 | return nil, ErrInvalidHardwareAddr 88 | } 89 | if len(dstHW) < 6 { 90 | return nil, ErrInvalidHardwareAddr 91 | } 92 | if !bytes.Equal(ethernet.Broadcast, dstHW) && len(srcHW) != len(dstHW) { 93 | return nil, ErrInvalidHardwareAddr 94 | } 95 | 96 | // Validate IP addresses to ensure they are IPv4 addresses, and 97 | // correct length 98 | var invalidIP netip.Addr 99 | if !srcIP.IsValid() || !srcIP.Is4() { 100 | return nil, ErrInvalidIP 101 | } 102 | if !dstIP.Is4() || dstIP == invalidIP { 103 | return nil, ErrInvalidIP 104 | } 105 | 106 | return &Packet{ 107 | // There is no Go-native way to detect hardware type of a network 108 | // interface, so default to 1 (ethernet 10Mb) for now 109 | HardwareType: 1, 110 | 111 | // Default to EtherType for IPv4 112 | ProtocolType: uint16(ethernet.EtherTypeIPv4), 113 | 114 | // Populate other fields using input data 115 | HardwareAddrLength: uint8(len(srcHW)), 116 | IPLength: uint8(4), 117 | Operation: op, 118 | SenderHardwareAddr: srcHW, 119 | SenderIP: srcIP, 120 | TargetHardwareAddr: dstHW, 121 | TargetIP: dstIP, 122 | }, nil 123 | } 124 | 125 | // MarshalBinary allocates a byte slice containing the data from a Packet. 126 | // 127 | // MarshalBinary never returns an error. 128 | func (p *Packet) MarshalBinary() ([]byte, error) { 129 | // 2 bytes: hardware type 130 | // 2 bytes: protocol type 131 | // 1 byte : hardware address length 132 | // 1 byte : protocol length 133 | // 2 bytes: operation 134 | // N bytes: source hardware address 135 | // N bytes: source protocol address 136 | // N bytes: target hardware address 137 | // N bytes: target protocol address 138 | 139 | // Though an IPv4 address should always 4 bytes, go-fuzz 140 | // very quickly created several crasher scenarios which 141 | // indicated that these values can lie. 142 | b := make([]byte, 2+2+1+1+2+(p.IPLength*2)+(p.HardwareAddrLength*2)) 143 | 144 | // Marshal fixed length data 145 | 146 | binary.BigEndian.PutUint16(b[0:2], p.HardwareType) 147 | binary.BigEndian.PutUint16(b[2:4], p.ProtocolType) 148 | 149 | b[4] = p.HardwareAddrLength 150 | b[5] = p.IPLength 151 | 152 | binary.BigEndian.PutUint16(b[6:8], uint16(p.Operation)) 153 | 154 | // Marshal variable length data at correct offset using lengths 155 | // defined in p 156 | 157 | n := 8 158 | hal := int(p.HardwareAddrLength) 159 | pl := int(p.IPLength) 160 | 161 | copy(b[n:n+hal], p.SenderHardwareAddr) 162 | n += hal 163 | 164 | sender4 := p.SenderIP.As4() 165 | copy(b[n:n+pl], sender4[:]) 166 | n += pl 167 | 168 | copy(b[n:n+hal], p.TargetHardwareAddr) 169 | n += hal 170 | 171 | target4 := p.TargetIP.As4() 172 | copy(b[n:n+pl], target4[:]) 173 | 174 | return b, nil 175 | } 176 | 177 | // UnmarshalBinary unmarshals a raw byte slice into a Packet. 178 | func (p *Packet) UnmarshalBinary(b []byte) error { 179 | // Must have enough room to retrieve hardware address and IP lengths 180 | if len(b) < 8 { 181 | return io.ErrUnexpectedEOF 182 | } 183 | 184 | // Retrieve fixed length data 185 | 186 | p.HardwareType = binary.BigEndian.Uint16(b[0:2]) 187 | p.ProtocolType = binary.BigEndian.Uint16(b[2:4]) 188 | 189 | p.HardwareAddrLength = b[4] 190 | p.IPLength = b[5] 191 | 192 | p.Operation = Operation(binary.BigEndian.Uint16(b[6:8])) 193 | 194 | // Unmarshal variable length data at correct offset using lengths 195 | // defined by ml and il 196 | // 197 | // These variables are meant to improve readability of offset calculations 198 | // for the code below 199 | n := 8 200 | ml := int(p.HardwareAddrLength) 201 | ml2 := ml * 2 202 | il := int(p.IPLength) 203 | il2 := il * 2 204 | 205 | // Must have enough room to retrieve both hardware address and IP addresses 206 | addrl := n + ml2 + il2 207 | if len(b) < addrl { 208 | return io.ErrUnexpectedEOF 209 | } 210 | 211 | // Allocate single byte slice to store address information, which 212 | // is resliced into fields 213 | bb := make([]byte, addrl-n) 214 | 215 | // Sender hardware address 216 | copy(bb[0:ml], b[n:n+ml]) 217 | p.SenderHardwareAddr = bb[0:ml] 218 | n += ml 219 | 220 | // Sender IP address 221 | copy(bb[ml:ml+il], b[n:n+il]) 222 | senderIP, ok := netip.AddrFromSlice(bb[ml : ml+il]) 223 | if !ok { 224 | return errors.New("Invalid Sender IP address") 225 | } 226 | p.SenderIP = senderIP 227 | n += il 228 | 229 | // Target hardware address 230 | copy(bb[ml+il:ml2+il], b[n:n+ml]) 231 | p.TargetHardwareAddr = bb[ml+il : ml2+il] 232 | n += ml 233 | 234 | // Target IP address 235 | copy(bb[ml2+il:ml2+il2], b[n:n+il]) 236 | targetIP, ok := netip.AddrFromSlice(bb[ml2+il : ml2+il2]) 237 | if !ok { 238 | return errors.New("Invalid Target IP address") 239 | } 240 | p.TargetIP = targetIP 241 | 242 | return nil 243 | } 244 | 245 | func parsePacket(buf []byte) (*Packet, *ethernet.Frame, error) { 246 | f := new(ethernet.Frame) 247 | if err := f.UnmarshalBinary(buf); err != nil { 248 | return nil, nil, err 249 | } 250 | 251 | // Ignore frames which do not have ARP EtherType 252 | if f.EtherType != ethernet.EtherTypeARP { 253 | return nil, nil, errInvalidARPPacket 254 | } 255 | 256 | p := new(Packet) 257 | if err := p.UnmarshalBinary(f.Payload); err != nil { 258 | return nil, nil, err 259 | } 260 | return p, f, nil 261 | } 262 | -------------------------------------------------------------------------------- /client_request_test.go: -------------------------------------------------------------------------------- 1 | package arp 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net" 8 | "net/netip" 9 | "testing" 10 | ) 11 | 12 | func ipv6loopback() netip.Addr { 13 | l := net.IPv6loopback 14 | a, ok := netip.AddrFromSlice(l) 15 | if !ok { 16 | panic("invalid loopback address") 17 | } 18 | return a 19 | } 20 | 21 | func TestClientRequestNoIPv4Address(t *testing.T) { 22 | c := &Client{} 23 | 24 | _, got := c.Resolve(netip.Addr{}) 25 | if want := errNoIPv4Addr; want != got { 26 | t.Fatalf("unexpected error for no IPv4 address:\n- want: %v\n- got: %v", 27 | want, got) 28 | } 29 | } 30 | 31 | func TestClientRequestInvalidSourceHardwareAddr(t *testing.T) { 32 | c := &Client{ 33 | ifi: &net.Interface{}, 34 | ip: netip.IPv4Unspecified(), 35 | } 36 | 37 | _, got := c.Resolve(netip.IPv4Unspecified()) 38 | if want := ErrInvalidHardwareAddr; want != got { 39 | t.Fatalf("unexpected error for invalid source hardware address:\n- want: %v\n- got: %v", 40 | want, got) 41 | } 42 | } 43 | 44 | func TestClientRequestIPv6Address(t *testing.T) { 45 | c := &Client{ 46 | ifi: &net.Interface{ 47 | HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 0}, 48 | }, 49 | ip: netip.IPv4Unspecified(), 50 | } 51 | 52 | _, got := c.Resolve(ipv6loopback()) 53 | if want := ErrInvalidIP; want != got { 54 | t.Fatalf("unexpected error for IPv6 address:\n- want: %v\n- got: %v", 55 | want, got) 56 | } 57 | } 58 | 59 | func TestClientRequestErrWriteTo(t *testing.T) { 60 | errWriteTo := errors.New("test error") 61 | 62 | c := &Client{ 63 | ifi: &net.Interface{ 64 | HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 0}, 65 | }, 66 | ip: netip.IPv4Unspecified(), 67 | p: &errWriteToPacketConn{ 68 | err: errWriteTo, 69 | }, 70 | } 71 | 72 | _, got := c.Resolve(netip.IPv4Unspecified()) 73 | if want := errWriteTo; want != got { 74 | t.Fatalf("unexpected error during WriteTo:\n- want: %v\n- got: %v", 75 | want, got) 76 | } 77 | } 78 | 79 | func TestClientRequestErrReadFrom(t *testing.T) { 80 | errReadFrom := errors.New("test error") 81 | 82 | c := &Client{ 83 | ifi: &net.Interface{ 84 | HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 0}, 85 | }, 86 | ip: netip.IPv4Unspecified(), 87 | p: &errReadFromPacketConn{ 88 | err: errReadFrom, 89 | }, 90 | } 91 | 92 | _, got := c.Resolve(netip.IPv4Unspecified()) 93 | if want := errReadFrom; want != got { 94 | t.Fatalf("unexpected error during ReadFrom:\n- want: %v\n- got: %v", 95 | want, got) 96 | } 97 | } 98 | 99 | func TestClientRequestEthernetFrameUnexpectedEOF(t *testing.T) { 100 | c := &Client{ 101 | ifi: &net.Interface{ 102 | HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 0}, 103 | }, 104 | ip: netip.IPv4Unspecified(), 105 | p: &bufferReadFromPacketConn{ 106 | b: bytes.NewBuffer([]byte{0}), 107 | }, 108 | } 109 | 110 | _, got := c.Resolve(netip.IPv4Unspecified()) 111 | if want := io.ErrUnexpectedEOF; want != got { 112 | t.Fatalf("unexpected error while reading ethernet frame:\n- want: %v\n- got: %v", 113 | want, got) 114 | } 115 | } 116 | 117 | func TestClientRequestEthernetFrameWrongDestinationHardwareAddr(t *testing.T) { 118 | c := &Client{ 119 | ifi: &net.Interface{ 120 | HardwareAddr: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}, 121 | }, 122 | ip: netip.IPv4Unspecified(), 123 | p: &bufferReadFromPacketConn{ 124 | b: bytes.NewBuffer(append([]byte{ 125 | // Ethernet frame with wrong destination hardware address 126 | 0, 0, 0, 0, 0, 0, // Wrong destination 127 | 0, 0, 0, 0, 0, 0, 128 | 0x00, 0x00, 129 | }, make([]byte, 46)...)), 130 | }, 131 | } 132 | 133 | _, got := c.Resolve(netip.IPv4Unspecified()) 134 | if want := io.EOF; want != got { 135 | t.Fatalf("unexpected error while reading ethernet frame with wrong destination hardware address:\n- want: %v\n- got: %v", 136 | want, got) 137 | } 138 | } 139 | 140 | func TestClientRequestEthernetFrameWrongEtherType(t *testing.T) { 141 | c := &Client{ 142 | ifi: &net.Interface{ 143 | HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 0}, 144 | }, 145 | ip: netip.IPv4Unspecified(), 146 | p: &bufferReadFromPacketConn{ 147 | b: bytes.NewBuffer(append([]byte{ 148 | // Ethernet frame with non-ARP EtherType 149 | 0, 0, 0, 0, 0, 0, 150 | 0, 0, 0, 0, 0, 0, 151 | 0x00, 0x00, // Wrong EtherType 152 | }, make([]byte, 46)...)), 153 | }, 154 | } 155 | 156 | _, got := c.Resolve(netip.IPv4Unspecified()) 157 | if want := io.EOF; want != got { 158 | t.Fatalf("unexpected error while reading ethernet frame with wrong EtherType:\n- want: %v\n- got: %v", 159 | want, got) 160 | } 161 | } 162 | 163 | func TestClientRequestARPPacketUnexpectedEOF(t *testing.T) { 164 | c := &Client{ 165 | ifi: &net.Interface{ 166 | HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 0}, 167 | }, 168 | ip: netip.IPv4Unspecified(), 169 | p: &bufferReadFromPacketConn{ 170 | b: bytes.NewBuffer(append([]byte{ 171 | // Ethernet frame 172 | 0, 0, 0, 0, 0, 0, 173 | 0, 0, 0, 0, 0, 0, 174 | 0x08, 0x06, 175 | // ARP packet with misleading hardware address length 176 | 0, 0, 177 | 0, 0, 178 | 255, 255, // Misleading hardware address length 179 | }, make([]byte, 40)...)), 180 | }, 181 | } 182 | 183 | _, got := c.Resolve(netip.IPv4Unspecified()) 184 | if want := io.ErrUnexpectedEOF; want != got { 185 | t.Fatalf("unexpected error while reading ARP packet:\n- want: %v\n- got: %v", 186 | want, got) 187 | } 188 | } 189 | 190 | func TestClientRequestARPRequestInsteadOfResponse(t *testing.T) { 191 | c := &Client{ 192 | ifi: &net.Interface{ 193 | HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 0}, 194 | }, 195 | ip: netip.IPv4Unspecified(), 196 | p: &bufferReadFromPacketConn{ 197 | b: bytes.NewBuffer(append([]byte{ 198 | // Ethernet frame 199 | 0, 0, 0, 0, 0, 0, 200 | 0, 0, 0, 0, 0, 0, 201 | 0x08, 0x06, 202 | // ARP request, not response 203 | 0, 1, 204 | 0x08, 0x06, 205 | 6, 206 | 4, 207 | 0, 1, // Request, not Response 208 | 0, 0, 0, 0, 0, 0, 209 | 0, 0, 0, 0, 210 | 0, 0, 0, 0, 0, 0, 211 | 0, 0, 0, 0, 212 | }, make([]byte, 46)...)), 213 | }, 214 | } 215 | 216 | _, got := c.Resolve(netip.IPv4Unspecified()) 217 | if want := io.EOF; want != got { 218 | t.Fatalf("unexpected error while reading ARP response with wrong operation type:\n- want: %v\n- got: %v", 219 | want, got) 220 | } 221 | } 222 | 223 | func TestClientRequestARPResponseWrongSenderIP(t *testing.T) { 224 | c := &Client{ 225 | ifi: &net.Interface{ 226 | HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 0}, 227 | }, 228 | ip: netip.AddrFrom4([4]byte{192, 168, 1, 1}), 229 | p: &bufferReadFromPacketConn{ 230 | b: bytes.NewBuffer(append([]byte{ 231 | // Ethernet frame 232 | 0, 0, 0, 0, 0, 0, 233 | 0, 0, 0, 0, 0, 0, 234 | 0x08, 0x06, 235 | // ARP Packet not bound for this IP address 236 | 0, 1, 237 | 0x08, 0x06, 238 | 6, 239 | 4, 240 | 0, 2, 241 | 0, 0, 0, 0, 0, 0, 242 | 192, 168, 1, 10, // Wrong IP address 243 | 0, 0, 0, 0, 0, 0, 244 | 192, 168, 1, 1, 245 | }, make([]byte, 46)...)), 246 | }, 247 | } 248 | 249 | _, got := c.Resolve(netip.IPv4Unspecified()) 250 | if want := io.EOF; want != got { 251 | t.Fatalf("unexpected error while reading ARP response with wrong sender IP:\n- want: %v\n- got: %v", 252 | want, got) 253 | } 254 | } 255 | 256 | func TestClientRequestOK(t *testing.T) { 257 | c := &Client{ 258 | ifi: &net.Interface{ 259 | HardwareAddr: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}, 260 | }, 261 | ip: netip.AddrFrom4([4]byte{192, 168, 1, 1}), 262 | p: &bufferReadFromPacketConn{ 263 | b: bytes.NewBuffer(append([]byte{ 264 | // Ethernet frame 265 | 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 266 | 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 267 | 0x08, 0x06, 268 | // ARP Packet 269 | 0, 1, 270 | 0x08, 0x06, 271 | 6, 272 | 4, 273 | 0, 2, 274 | 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 275 | 192, 168, 1, 10, 276 | 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, // mac needn't match ours 277 | 192, 168, 1, 2, // ip needn't match ours 278 | }, make([]byte, 40)...)), 279 | }, 280 | } 281 | 282 | wantHW := net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff} 283 | gotHW, err := c.Resolve(netip.AddrFrom4([4]byte{192, 168, 1, 10})) 284 | if err != nil { 285 | t.Fatal(err) 286 | } 287 | 288 | if want, got := wantHW, gotHW; !bytes.Equal(want, got) { 289 | t.Fatalf("unexpected hardware address for request:\n- want: %v\n- got: %v", 290 | want, got) 291 | } 292 | } 293 | 294 | // bufferReadFromPacketConn is a net.PacketConn which copies bytes from its 295 | // embedded buffer into b when when its ReadFrom method is called. 296 | type bufferReadFromPacketConn struct { 297 | b *bytes.Buffer 298 | 299 | noopPacketConn 300 | } 301 | 302 | func (p *bufferReadFromPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { 303 | n, err := p.b.Read(b) 304 | return n, nil, err 305 | } 306 | 307 | // errWriteToPacketConn is a net.PacketConn which always returns its embedded 308 | // error when its WriteTo method is called. 309 | type errWriteToPacketConn struct { 310 | err error 311 | 312 | noopPacketConn 313 | } 314 | 315 | func (p *errWriteToPacketConn) WriteTo(b []byte, addr net.Addr) (int, error) { return 0, p.err } 316 | 317 | // errReadFromPacketConn is a net.PacketConn which always returns its embedded 318 | // error when its ReadFrom method is called. 319 | type errReadFromPacketConn struct { 320 | err error 321 | 322 | noopPacketConn 323 | } 324 | 325 | func (p *errReadFromPacketConn) ReadFrom(b []byte) (int, net.Addr, error) { return 0, nil, p.err } 326 | -------------------------------------------------------------------------------- /packet_test.go: -------------------------------------------------------------------------------- 1 | package arp 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net" 7 | "net/netip" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/mdlayher/ethernet" 12 | ) 13 | 14 | func TestNewPacket(t *testing.T) { 15 | zeroHW := net.HardwareAddr{0, 0, 0, 0, 0, 0} 16 | 17 | iboip1 := net.HardwareAddr(bytes.Repeat([]byte{0}, 20)) 18 | 19 | tests := []struct { 20 | desc string 21 | op Operation 22 | srcHW net.HardwareAddr 23 | srcIP netip.Addr 24 | dstHW net.HardwareAddr 25 | dstIP netip.Addr 26 | p *Packet 27 | err error 28 | }{ 29 | { 30 | desc: "short source hardware address", 31 | srcHW: net.HardwareAddr{0, 0, 0, 0, 0}, 32 | err: ErrInvalidHardwareAddr, 33 | }, 34 | { 35 | desc: "short destination hardware address", 36 | srcHW: zeroHW, 37 | dstHW: net.HardwareAddr{0, 0, 0, 0, 0}, 38 | err: ErrInvalidHardwareAddr, 39 | }, 40 | { 41 | desc: "hardware address length mismatch", 42 | srcHW: zeroHW, 43 | dstHW: net.HardwareAddr{0, 0, 0, 0, 0, 0, 0, 0}, 44 | err: ErrInvalidHardwareAddr, 45 | }, 46 | { 47 | desc: "IPv6 source IP address", 48 | srcHW: zeroHW, 49 | dstHW: zeroHW, 50 | srcIP: netip.IPv6Unspecified(), 51 | err: ErrInvalidIP, 52 | }, 53 | { 54 | desc: "IPv6 destination IP address", 55 | srcHW: zeroHW, 56 | dstHW: zeroHW, 57 | srcIP: netip.IPv4Unspecified(), 58 | dstIP: netip.IPv6Unspecified(), 59 | err: ErrInvalidIP, 60 | }, 61 | { 62 | desc: "Gratuitous ARP request, IPoIB hardware addresses", 63 | op: OperationRequest, 64 | srcHW: iboip1, 65 | dstHW: ethernet.Broadcast, 66 | srcIP: netip.IPv4Unspecified(), 67 | dstIP: netip.IPv4Unspecified(), 68 | p: &Packet{ 69 | HardwareType: 1, 70 | ProtocolType: uint16(ethernet.EtherTypeIPv4), 71 | HardwareAddrLength: 20, 72 | IPLength: 4, 73 | Operation: OperationRequest, 74 | SenderHardwareAddr: iboip1, 75 | SenderIP: netip.IPv4Unspecified(), 76 | TargetHardwareAddr: ethernet.Broadcast, 77 | TargetIP: netip.IPv4Unspecified(), 78 | }, 79 | }, 80 | { 81 | desc: "OK", 82 | op: OperationRequest, 83 | srcHW: zeroHW, 84 | dstHW: zeroHW, 85 | srcIP: netip.IPv4Unspecified(), 86 | dstIP: netip.IPv4Unspecified(), 87 | p: &Packet{ 88 | HardwareType: 1, 89 | ProtocolType: uint16(ethernet.EtherTypeIPv4), 90 | HardwareAddrLength: 6, 91 | IPLength: 4, 92 | Operation: OperationRequest, 93 | SenderHardwareAddr: zeroHW, 94 | SenderIP: netip.IPv4Unspecified(), 95 | TargetHardwareAddr: zeroHW, 96 | TargetIP: netip.IPv4Unspecified(), 97 | }, 98 | }, 99 | } 100 | 101 | for i, tt := range tests { 102 | p, err := NewPacket(tt.op, tt.srcHW, tt.srcIP, tt.dstHW, tt.dstIP) 103 | if err != nil { 104 | if want, got := tt.err, err; want != got { 105 | t.Fatalf("[%02d] test %q, unexpected error: %v != %v", 106 | i, tt.desc, want, got) 107 | } 108 | 109 | continue 110 | } 111 | 112 | if want, got := tt.p, p; !reflect.DeepEqual(want, got) { 113 | t.Fatalf("[%02d] test %q, unexpected Packet:\n- want: %v\n- got: %v", 114 | i, tt.desc, want, got) 115 | } 116 | } 117 | } 118 | 119 | func TestPacketMarshalBinary(t *testing.T) { 120 | zeroHW := net.HardwareAddr{0, 0, 0, 0, 0, 0} 121 | ip1 := netip.MustParseAddr("192.168.1.10") 122 | ip2 := netip.MustParseAddr("192.168.1.1") 123 | 124 | iboip1 := net.HardwareAddr(bytes.Repeat([]byte{0}, 20)) 125 | iboip2 := net.HardwareAddr(bytes.Repeat([]byte{1}, 20)) 126 | 127 | tests := []struct { 128 | desc string 129 | p *Packet 130 | b []byte 131 | }{ 132 | { 133 | desc: "ARP request to ethernet broadcast, 6 byte hardware addresses", 134 | p: &Packet{ 135 | HardwareType: 1, 136 | ProtocolType: uint16(ethernet.EtherTypeIPv4), 137 | HardwareAddrLength: 6, 138 | IPLength: 4, 139 | Operation: OperationRequest, 140 | SenderHardwareAddr: zeroHW, 141 | SenderIP: ip1, 142 | TargetHardwareAddr: ethernet.Broadcast, 143 | TargetIP: ip2, 144 | }, 145 | b: []byte{ 146 | 0, 1, 147 | 8, 0, 148 | 6, 149 | 4, 150 | 0, 1, 151 | 0, 0, 0, 0, 0, 0, 152 | 192, 168, 1, 10, 153 | 255, 255, 255, 255, 255, 255, 154 | 192, 168, 1, 1, 155 | }, 156 | }, 157 | { 158 | desc: "ARP reply over infiniband, 20 byte hardware addresses", 159 | p: &Packet{ 160 | HardwareType: 32, 161 | ProtocolType: uint16(ethernet.EtherTypeIPv4), 162 | HardwareAddrLength: 20, 163 | IPLength: 4, 164 | Operation: OperationReply, 165 | SenderHardwareAddr: iboip1, 166 | SenderIP: ip1, 167 | TargetHardwareAddr: iboip2, 168 | TargetIP: ip2, 169 | }, 170 | b: []byte{ 171 | 0, 32, 172 | 8, 0, 173 | 20, 174 | 4, 175 | 0, 2, 176 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 177 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 178 | 192, 168, 1, 10, 179 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 180 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 181 | 192, 168, 1, 1, 182 | }, 183 | }, 184 | } 185 | 186 | for i, tt := range tests { 187 | b, err := tt.p.MarshalBinary() 188 | if err != nil { 189 | t.Fatal(err) 190 | } 191 | 192 | if want, got := tt.b, b; !bytes.Equal(want, got) { 193 | t.Fatalf("[%02d] test %q, unexpected Packet bytes:\n- want: %v\n- got: %v", 194 | i, tt.desc, want, got) 195 | } 196 | } 197 | } 198 | 199 | func TestPacketUnmarshalBinary(t *testing.T) { 200 | zeroHW := net.HardwareAddr{0, 0, 0, 0, 0, 0} 201 | ip1 := netip.MustParseAddr("192.168.1.10") 202 | ip2 := netip.MustParseAddr("192.168.1.1") 203 | 204 | iboip1 := net.HardwareAddr(bytes.Repeat([]byte{0}, 20)) 205 | iboip2 := net.HardwareAddr(bytes.Repeat([]byte{1}, 20)) 206 | 207 | tests := []struct { 208 | desc string 209 | p *Packet 210 | b []byte 211 | err error 212 | }{ 213 | { 214 | desc: "short buffer", 215 | b: bytes.Repeat([]byte{0}, 7), 216 | err: io.ErrUnexpectedEOF, 217 | }, 218 | { 219 | desc: "short buffer, too short for hardware addresses", 220 | b: []byte{ 221 | 0, 1, 222 | 8, 0, 223 | 255, 224 | 4, 225 | 0, 1, 226 | }, 227 | err: io.ErrUnexpectedEOF, 228 | }, 229 | { 230 | desc: "short buffer, too short for IP addresses", 231 | b: []byte{ 232 | 0, 1, 233 | 8, 0, 234 | 6, 235 | 255, 236 | 0, 1, 237 | }, 238 | err: io.ErrUnexpectedEOF, 239 | }, 240 | { 241 | desc: "ARP request to ethernet broadcast, 6 byte hardware addresses", 242 | b: []byte{ 243 | 0, 1, 244 | 8, 0, 245 | 6, 246 | 4, 247 | 0, 1, 248 | 0, 0, 0, 0, 0, 0, 249 | 192, 168, 1, 10, 250 | 255, 255, 255, 255, 255, 255, 251 | 192, 168, 1, 1, 252 | }, 253 | p: &Packet{ 254 | HardwareType: 1, 255 | ProtocolType: uint16(ethernet.EtherTypeIPv4), 256 | HardwareAddrLength: 6, 257 | IPLength: 4, 258 | Operation: OperationRequest, 259 | SenderHardwareAddr: zeroHW, 260 | SenderIP: ip1, 261 | TargetHardwareAddr: ethernet.Broadcast, 262 | TargetIP: ip2, 263 | }, 264 | }, 265 | { 266 | desc: "ARP reply over infiniband, 20 byte hardware addresses", 267 | b: []byte{ 268 | 0, 32, 269 | 8, 0, 270 | 20, 271 | 4, 272 | 0, 2, 273 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 274 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 275 | 192, 168, 1, 10, 276 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 277 | 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 278 | 192, 168, 1, 1, 279 | }, 280 | p: &Packet{ 281 | HardwareType: 32, 282 | ProtocolType: uint16(ethernet.EtherTypeIPv4), 283 | HardwareAddrLength: 20, 284 | IPLength: 4, 285 | Operation: OperationReply, 286 | SenderHardwareAddr: iboip1, 287 | SenderIP: ip1, 288 | TargetHardwareAddr: iboip2, 289 | TargetIP: ip2, 290 | }, 291 | }, 292 | } 293 | 294 | for i, tt := range tests { 295 | p := new(Packet) 296 | if err := p.UnmarshalBinary(tt.b); err != nil { 297 | if want, got := tt.err, err; want != got { 298 | t.Fatalf("[%02d] test %q, unexpected error: %v != %v", 299 | i, tt.desc, want, got) 300 | } 301 | 302 | continue 303 | } 304 | 305 | if want, got := tt.p, p; !reflect.DeepEqual(want, got) { 306 | t.Fatalf("[%02d] test %q, unexpected Packet bytes:\n- want: %v\n- got: %v", 307 | i, tt.desc, want, got) 308 | } 309 | } 310 | } 311 | 312 | func Test_parsePacket(t *testing.T) { 313 | tests := []struct { 314 | desc string 315 | buf []byte 316 | p *Packet 317 | err error 318 | }{ 319 | { 320 | desc: "invalid ethernet frame", 321 | err: io.ErrUnexpectedEOF, 322 | }, 323 | { 324 | desc: "non-ARP EtherType", 325 | // Approximation of 14 byte ethernet frame header and 326 | // 42 byte blank payload (EtherType 0x0000) 327 | buf: make([]byte, 56), 328 | err: errInvalidARPPacket, 329 | }, 330 | { 331 | desc: "invalid ARP packet", 332 | buf: append([]byte{ 333 | // Ethernet frame 334 | 0, 0, 0, 0, 0, 0, 335 | 0, 0, 0, 0, 0, 0, 336 | 0x08, 0x06, 337 | // ARP packet with misleading hardware address length 338 | 0, 0, 339 | 0, 0, 340 | 255, 255, // Misleading hardware address length 341 | }, make([]byte, 40)...), 342 | err: io.ErrUnexpectedEOF, 343 | }, 344 | { 345 | desc: "OK", 346 | buf: append([]byte{ 347 | // Ethernet frame 348 | 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 349 | 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 350 | 0x08, 0x06, 351 | // ARP Packet 352 | 0, 1, 353 | 0x08, 0x06, 354 | 6, 355 | 4, 356 | 0, 2, 357 | 0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff, 358 | 192, 168, 1, 10, 359 | 0xde, 0xad, 0xbe, 0xef, 0xde, 0xad, 360 | 192, 168, 1, 1, 361 | }, make([]byte, 40)...), 362 | p: &Packet{ 363 | HardwareType: 1, 364 | ProtocolType: 2054, 365 | HardwareAddrLength: 6, 366 | IPLength: 4, 367 | Operation: OperationReply, 368 | SenderHardwareAddr: net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, 369 | SenderIP: netip.MustParseAddr("192.168.1.10"), 370 | TargetHardwareAddr: net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}, 371 | TargetIP: netip.MustParseAddr("192.168.1.1"), 372 | }, 373 | }, 374 | } 375 | 376 | for i, tt := range tests { 377 | p, _, err := parsePacket(tt.buf) 378 | if err != nil { 379 | if want, got := tt.err, err; want != got { 380 | t.Fatalf("[%02d] test %q, unexpected error: %v != %v", 381 | i, tt.desc, want, got) 382 | } 383 | 384 | continue 385 | } 386 | 387 | if want, got := tt.p, p; !reflect.DeepEqual(want, got) { 388 | t.Fatalf("[%02d] test %q, unexpected Packet:\n- want: %v\n- got: %v", 389 | i, tt.desc, want, got) 390 | } 391 | } 392 | } 393 | 394 | // Benchmarks for Packet.MarshalBinary 395 | 396 | func BenchmarkPacketMarshalBinary(b *testing.B) { 397 | p, err := NewPacket( 398 | OperationRequest, 399 | net.HardwareAddr{0xad, 0xbe, 0xef, 0xde, 0xad, 0xde}, 400 | netip.MustParseAddr("192.168.1.10"), 401 | net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}, 402 | netip.MustParseAddr("192.168.1.1"), 403 | ) 404 | if err != nil { 405 | b.Fatal(err) 406 | } 407 | 408 | benchmarkPacketMarshalBinary(b, p) 409 | } 410 | 411 | func benchmarkPacketMarshalBinary(b *testing.B, p *Packet) { 412 | b.ResetTimer() 413 | b.ReportAllocs() 414 | for i := 0; i < b.N; i++ { 415 | if _, err := p.MarshalBinary(); err != nil { 416 | b.Fatal(err) 417 | } 418 | } 419 | } 420 | 421 | // Benchmarks for Packet.UnmarshalBinary 422 | 423 | func BenchmarkPacketUnmarshalBinary(b *testing.B) { 424 | p, err := NewPacket( 425 | OperationRequest, 426 | net.HardwareAddr{0xad, 0xbe, 0xef, 0xde, 0xad, 0xde}, 427 | netip.MustParseAddr("192.168.1.10"), 428 | net.HardwareAddr{0xde, 0xad, 0xbe, 0xef, 0xde, 0xad}, 429 | netip.MustParseAddr("192.168.1.1"), 430 | ) 431 | if err != nil { 432 | b.Fatal(err) 433 | } 434 | 435 | benchmarkPacketUnmarshalBinary(b, p) 436 | } 437 | 438 | func benchmarkPacketUnmarshalBinary(b *testing.B, p *Packet) { 439 | pb, err := p.MarshalBinary() 440 | if err != nil { 441 | b.Fatal(err) 442 | } 443 | 444 | b.ResetTimer() 445 | b.ReportAllocs() 446 | for i := 0; i < b.N; i++ { 447 | if err := p.UnmarshalBinary(pb); err != nil { 448 | b.Fatal(err) 449 | } 450 | } 451 | } 452 | --------------------------------------------------------------------------------