mirror of
https://github.com/moby/moby.git
synced 2026-01-11 18:51:37 +00:00
218 lines
6.9 KiB
Go
218 lines
6.9 KiB
Go
package network
|
|
|
|
// Collect and decode broadcast ARP and unsolicited ICMP6 Neighbour Advertisement messages...
|
|
|
|
import (
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/moby/moby/v2/daemon/libnetwork/nlwrap"
|
|
"golang.org/x/sys/unix"
|
|
"gotest.tools/v3/assert"
|
|
)
|
|
|
|
// TimestampedPkt has a Data slice representing a packet, ReceivedAt is a timestamp
|
|
// set after the packet was received in user-space.
|
|
type TimestampedPkt struct {
|
|
ReceivedAt time.Time
|
|
Data []byte
|
|
}
|
|
|
|
// CollectBcastARPs collects broadcast ARPs from interface ifname.
|
|
// It returns a stop function, to stop collection and return a slice of collected packets (with
|
|
// timestamps added when they were received in userspace).
|
|
func CollectBcastARPs(t *testing.T, ifname string) (stop func() []TimestampedPkt) {
|
|
sd, err := unix.Socket(unix.AF_PACKET, unix.SOCK_DGRAM|unix.SOCK_CLOEXEC, int(htons(unix.ETH_P_ARP)))
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, sd >= 0)
|
|
|
|
link, err := nlwrap.LinkByName(ifname)
|
|
assert.NilError(t, err)
|
|
|
|
err = unix.Bind(sd, &unix.SockaddrLinklayer{
|
|
Protocol: htons(unix.ETH_P_ARP),
|
|
Pkttype: unix.PACKET_BROADCAST,
|
|
Ifindex: link.Attrs().Index,
|
|
})
|
|
assert.NilError(t, err)
|
|
|
|
return collectPackets(t, sd, "ARP")
|
|
}
|
|
|
|
// From https://github.com/mdlayher/packet/blob/f9999b41d9cfb0586e75467db1c81cfde4f965ba/packet_linux.go#L238-L248
|
|
func htons(i uint16) uint16 {
|
|
var bigEndian [2]byte
|
|
binary.BigEndian.PutUint16(bigEndian[:], i)
|
|
return binary.NativeEndian.Uint16(bigEndian[:])
|
|
}
|
|
|
|
// CollectICMP6 collects ICMP6 packets sent to the all nodes address.
|
|
// It returns a stop function, to stop collection and return a slice of collected packets (with
|
|
// timestamps added when they were received in userspace).
|
|
func CollectICMP6(t *testing.T, ifname string) (stop func() []TimestampedPkt) {
|
|
sd, err := unix.Socket(unix.AF_INET6, unix.SOCK_RAW|unix.SOCK_CLOEXEC, unix.IPPROTO_ICMPV6)
|
|
assert.NilError(t, err)
|
|
assert.Assert(t, sd >= 0)
|
|
|
|
link, err := nlwrap.LinkByName(ifname)
|
|
assert.NilError(t, err)
|
|
|
|
mreq := &unix.IPv6Mreq{
|
|
Interface: uint32(link.Attrs().Index),
|
|
}
|
|
copy(mreq.Multiaddr[:], net.IPv6linklocalallnodes)
|
|
err = unix.SetsockoptIPv6Mreq(sd, unix.IPPROTO_IPV6, unix.IPV6_JOIN_GROUP, mreq)
|
|
assert.NilError(t, err)
|
|
|
|
return collectPackets(t, sd, "ICMP6")
|
|
}
|
|
|
|
func collectPackets(t *testing.T, sd int, what string) (stop func() []TimestampedPkt) {
|
|
err := unix.SetsockoptTimeval(sd, unix.SOL_SOCKET, unix.SO_RCVTIMEO, &unix.Timeval{Sec: 1})
|
|
assert.NilError(t, err)
|
|
|
|
stopC := make(chan struct{})
|
|
stoppedC := make(chan struct{})
|
|
var pkts []TimestampedPkt
|
|
go func() {
|
|
defer close(stoppedC)
|
|
defer unix.Close(sd)
|
|
for {
|
|
buf := make([]byte, 50)
|
|
// Blocking read, if no packet is received it'll return an EWOULDBLOCK after a timeout.
|
|
n, err := unix.Read(sd, buf)
|
|
// If the stop function has been called, return. That closes stoppedC to confirm that
|
|
// nothing else will be added to pkts. (Read() probably hit its timeout but, even
|
|
// if it read a packet, it was racing the stop and the packet can just be dropped.)
|
|
select {
|
|
case <-stopC:
|
|
return
|
|
default:
|
|
}
|
|
if err != nil {
|
|
if errors.Is(err, unix.EINTR) || errors.Is(err, unix.EWOULDBLOCK) {
|
|
continue
|
|
}
|
|
t.Log(what, "read error:", err, "sd", sd)
|
|
return
|
|
}
|
|
pkts = append(pkts, TimestampedPkt{
|
|
ReceivedAt: time.Now(),
|
|
Data: buf[:n],
|
|
})
|
|
}
|
|
}()
|
|
|
|
// Return a stop function - packet collection will continue until this is called.
|
|
return func() []TimestampedPkt {
|
|
select {
|
|
case <-stopC:
|
|
default:
|
|
close(stopC)
|
|
}
|
|
// Wait for confirmation that packet collection has stopped, to be sure the
|
|
// pkts slice won't change after the return.
|
|
<-stoppedC
|
|
return pkts
|
|
}
|
|
}
|
|
|
|
// UnpackUnsolARP checks the packet is a valid Ethernet unsolicited/broadcast ARP
|
|
// request packet. It returns sender hardware and protocol addresses,
|
|
// and true if it is - else, an error.
|
|
func UnpackUnsolARP(pkt TimestampedPkt) (sh net.HardwareAddr, sp netip.Addr, err error) {
|
|
if len(pkt.Data) != 28 {
|
|
return net.HardwareAddr{}, netip.Addr{}, fmt.Errorf("packet size %d", len(pkt.Data))
|
|
}
|
|
// Hardware type (1)
|
|
if pkt.Data[0] != 0 || pkt.Data[1] != 1 {
|
|
return net.HardwareAddr{}, netip.Addr{}, fmt.Errorf("hardware type %v", pkt.Data[0:2])
|
|
}
|
|
// Protocol type (0x800).
|
|
if pkt.Data[2] != 8 || pkt.Data[3] != 0 {
|
|
return net.HardwareAddr{}, netip.Addr{}, fmt.Errorf("protocol type %v", pkt.Data[2:4])
|
|
}
|
|
// Hardware length (6)
|
|
if pkt.Data[4] != 6 {
|
|
return net.HardwareAddr{}, netip.Addr{}, fmt.Errorf("hardware length %v", pkt.Data[4])
|
|
}
|
|
// Protocol length (4)
|
|
if pkt.Data[5] != 4 {
|
|
return net.HardwareAddr{}, netip.Addr{}, fmt.Errorf("protocol length %v", pkt.Data[5])
|
|
}
|
|
// Operation (1=request, 2=reply)
|
|
if pkt.Data[6] != 0 || pkt.Data[7] != 1 {
|
|
return net.HardwareAddr{}, netip.Addr{}, fmt.Errorf("operation %v", pkt.Data[6:8])
|
|
}
|
|
|
|
// Sender hardware address
|
|
sh = make(net.HardwareAddr, 6)
|
|
copy(sh, pkt.Data[8:14])
|
|
// Sender protocol address
|
|
sp, _ = netip.AddrFromSlice(pkt.Data[14:18])
|
|
|
|
// Target hardware address
|
|
if slices.Compare(pkt.Data[18:24], net.HardwareAddr{0, 0, 0, 0, 0, 0}) != 0 {
|
|
return net.HardwareAddr{}, netip.Addr{}, fmt.Errorf("nonzero target mac address %s", hex.EncodeToString(pkt.Data[18:24]))
|
|
}
|
|
|
|
// Target protocol address
|
|
if tp, _ := netip.AddrFromSlice(pkt.Data[24:28]); tp != sp {
|
|
return net.HardwareAddr{}, netip.Addr{}, fmt.Errorf("sender and target IP addresses differ, %s/%s", sp, tp)
|
|
}
|
|
|
|
return sh, sp, nil
|
|
}
|
|
|
|
// UnpackUnsolNA returns the hardware (MAC) and protocol (IP) addresses from the
|
|
// packet, if it is an unsolicited Neighbour Advertisement message with a
|
|
// link address option. Otherwise, it returns an error.
|
|
func UnpackUnsolNA(pkt TimestampedPkt) (th net.HardwareAddr, tp netip.Addr, err error) {
|
|
// Treat the packet as invalid unless it's sized for a NA message
|
|
// with a link address option.
|
|
if len(pkt.Data) != 32 {
|
|
return th, tp, fmt.Errorf("packet size %d", len(pkt.Data))
|
|
}
|
|
// Type (136=NA)
|
|
if pkt.Data[0] != 136 {
|
|
return th, tp, fmt.Errorf("type %d", pkt.Data[0])
|
|
}
|
|
// Code
|
|
if pkt.Data[1] != 0 {
|
|
return th, tp, fmt.Errorf("code %d", pkt.Data[1])
|
|
}
|
|
// Checksum pkt.Data[2:4].
|
|
// - calculated by the kernel on packets sent by dockerd, not checking that calc here.
|
|
// Router flag (not sent by a router)
|
|
if pkt.Data[4]&0x80 != 0 {
|
|
return th, tp, errors.New("flag Router is set")
|
|
}
|
|
// Solicited flag (unsolicited)
|
|
if pkt.Data[4]&0x40 != 0 {
|
|
return th, tp, errors.New("flag Solicited is set")
|
|
}
|
|
// Override flag (SHOULD be set in an unsolicited advertisement)
|
|
if pkt.Data[4]&0x20 == 0 {
|
|
return th, tp, errors.New("flag Override is not set")
|
|
}
|
|
// Reserved pkt.Data[4:8]
|
|
// Target address
|
|
tp, _ = netip.AddrFromSlice(pkt.Data[8:24])
|
|
// Options (02=link address, 01=length 8)
|
|
if pkt.Data[24] != 2 || pkt.Data[25] != 1 {
|
|
return th, tp, fmt.Errorf("option %d length %d", pkt.Data[24], pkt.Data[25])
|
|
}
|
|
// Link address
|
|
th = make(net.HardwareAddr, 6)
|
|
copy(th, pkt.Data[26:32])
|
|
|
|
return th, tp, nil
|
|
}
|