From c7b93702b99866c1beecd9f8b5042ccacaec0018 Mon Sep 17 00:00:00 2001 From: Cory Snider Date: Mon, 14 Jul 2025 16:25:02 -0400 Subject: [PATCH] libn/d/overlay: extract hashable address types The macAddr and ipmac types are generally useful within libnetwork. Move them to a dedicated package and overhaul the API to be more like that of the net/netip package. Update the overlay driver to utilize these types, adapting to the new API. Signed-off-by: Cory Snider --- .../libnetwork/drivers/overlay/joinleave.go | 6 +- .../libnetwork/drivers/overlay/ov_endpoint.go | 21 +++-- .../libnetwork/drivers/overlay/ov_network.go | 7 +- daemon/libnetwork/drivers/overlay/peerdb.go | 36 ++++---- daemon/libnetwork/drivers/overlay/types.go | 52 ------------ .../libnetwork/drivers/overlay/types_test.go | 29 ------- daemon/libnetwork/internal/hashable/net.go | 82 +++++++++++++++++++ .../libnetwork/internal/hashable/net_test.go | 66 +++++++++++++++ 8 files changed, 188 insertions(+), 111 deletions(-) delete mode 100644 daemon/libnetwork/drivers/overlay/types.go delete mode 100644 daemon/libnetwork/drivers/overlay/types_test.go create mode 100644 daemon/libnetwork/internal/hashable/net.go create mode 100644 daemon/libnetwork/internal/hashable/net_test.go diff --git a/daemon/libnetwork/drivers/overlay/joinleave.go b/daemon/libnetwork/drivers/overlay/joinleave.go index eb8a1e3e0c..166d59db5c 100644 --- a/daemon/libnetwork/drivers/overlay/joinleave.go +++ b/daemon/libnetwork/drivers/overlay/joinleave.go @@ -6,12 +6,12 @@ import ( "context" "errors" "fmt" - "net" "net/netip" "syscall" "github.com/containerd/log" "github.com/docker/docker/daemon/libnetwork/driverapi" + "github.com/docker/docker/daemon/libnetwork/internal/hashable" "github.com/docker/docker/daemon/libnetwork/internal/netiputil" "github.com/docker/docker/daemon/libnetwork/netlabel" "github.com/docker/docker/daemon/libnetwork/ns" @@ -104,7 +104,7 @@ func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinf return err } - if err = nlh.LinkSetHardwareAddr(veth, ep.mac); err != nil { + if err = nlh.LinkSetHardwareAddr(veth, ep.mac.AsSlice()); err != nil { return fmt.Errorf("could not set mac address (%v) to the container interface: %v", ep.mac, err) } @@ -187,7 +187,7 @@ func (d *driver) EventNotify(etype driverapi.EventType, nid, tableName, key stri return } - mac, err := net.ParseMAC(peer.EndpointMAC) + mac, err := hashable.ParseMAC(peer.EndpointMAC) if err != nil { log.G(context.TODO()).WithError(err).Errorf("Invalid mac %s received in event notify", peer.EndpointMAC) return diff --git a/daemon/libnetwork/drivers/overlay/ov_endpoint.go b/daemon/libnetwork/drivers/overlay/ov_endpoint.go index cc054a1830..ee75207883 100644 --- a/daemon/libnetwork/drivers/overlay/ov_endpoint.go +++ b/daemon/libnetwork/drivers/overlay/ov_endpoint.go @@ -6,11 +6,11 @@ import ( "context" "errors" "fmt" - "net" "net/netip" "github.com/containerd/log" "github.com/docker/docker/daemon/libnetwork/driverapi" + "github.com/docker/docker/daemon/libnetwork/internal/hashable" "github.com/docker/docker/daemon/libnetwork/internal/netiputil" "github.com/docker/docker/daemon/libnetwork/netutils" "github.com/docker/docker/daemon/libnetwork/ns" @@ -22,7 +22,7 @@ type endpoint struct { id string nid string ifName string - mac net.HardwareAddr + mac hashable.MACAddr addr netip.Prefix } @@ -48,7 +48,6 @@ func (d *driver) CreateEndpoint(_ context.Context, nid, eid string, ifInfo drive ep := &endpoint{ id: eid, nid: n.id, - mac: ifInfo.MacAddress(), } var ok bool ep.addr, ok = netiputil.ToPrefix(ifInfo.Address()) @@ -60,9 +59,19 @@ func (d *driver) CreateEndpoint(_ context.Context, nid, eid string, ifInfo drive return fmt.Errorf("no matching subnet for IP %q in network %q", ep.addr, nid) } - if ep.mac == nil { - ep.mac = netutils.GenerateMACFromIP(ep.addr.Addr().AsSlice()) - if err := ifInfo.SetMacAddress(ep.mac); err != nil { + if ifmac := ifInfo.MacAddress(); ifmac != nil { + var ok bool + ep.mac, ok = hashable.MACAddrFromSlice(ifInfo.MacAddress()) + if !ok { + return fmt.Errorf("invalid MAC address %q assigned to endpoint: unexpected length", ifmac) + } + } else { + var ok bool + ep.mac, ok = hashable.MACAddrFromSlice(netutils.GenerateMACFromIP(ep.addr.Addr().AsSlice())) + if !ok { + panic("GenerateMACFromIP returned a HardwareAddress that is not a MAC-48") + } + if err := ifInfo.SetMacAddress(ep.mac.AsSlice()); err != nil { return err } } diff --git a/daemon/libnetwork/drivers/overlay/ov_network.go b/daemon/libnetwork/drivers/overlay/ov_network.go index 71cb8246b5..8503405b71 100644 --- a/daemon/libnetwork/drivers/overlay/ov_network.go +++ b/daemon/libnetwork/drivers/overlay/ov_network.go @@ -19,6 +19,7 @@ import ( "github.com/docker/docker/daemon/libnetwork/driverapi" "github.com/docker/docker/daemon/libnetwork/drivers/overlay/overlayutils" "github.com/docker/docker/daemon/libnetwork/internal/countmap" + "github.com/docker/docker/daemon/libnetwork/internal/hashable" "github.com/docker/docker/daemon/libnetwork/internal/netiputil" "github.com/docker/docker/daemon/libnetwork/netlabel" "github.com/docker/docker/daemon/libnetwork/ns" @@ -65,7 +66,7 @@ type network struct { endpoints endpointTable joinCnt int // Ref count of VXLAN Forwarding Database entries programmed into the kernel - fdbCnt countmap.Map[ipmac] + fdbCnt countmap.Map[hashable.IPMAC] sboxInit bool initEpoch int initErr error @@ -110,7 +111,7 @@ func (d *driver) CreateNetwork(ctx context.Context, id string, option map[string driver: d, endpoints: endpointTable{}, subnets: []*subnet{}, - fdbCnt: countmap.Map[ipmac]{}, + fdbCnt: countmap.Map[hashable.IPMAC]{}, } vnis := make([]uint32, 0, len(ipV4Data)) @@ -607,7 +608,7 @@ func (n *network) initSandbox() error { // this is needed to let the peerAdd configure the sandbox n.sbox = sbox - n.fdbCnt = countmap.Map[ipmac]{} + n.fdbCnt = countmap.Map[hashable.IPMAC]{} return nil } diff --git a/daemon/libnetwork/drivers/overlay/peerdb.go b/daemon/libnetwork/drivers/overlay/peerdb.go index dc9a382093..eba04bfcc6 100644 --- a/daemon/libnetwork/drivers/overlay/peerdb.go +++ b/daemon/libnetwork/drivers/overlay/peerdb.go @@ -7,11 +7,11 @@ import ( "context" "errors" "fmt" - "net" "net/netip" "syscall" "github.com/containerd/log" + "github.com/docker/docker/daemon/libnetwork/internal/hashable" "github.com/docker/docker/daemon/libnetwork/internal/setmatrix" "github.com/docker/docker/daemon/libnetwork/osl" ) @@ -20,7 +20,7 @@ const ovPeerTable = "overlay_peer_table" type peerEntry struct { eid string - mac macAddr + mac hashable.MACAddr vtep netip.Addr } @@ -49,10 +49,10 @@ func (pm *peerMap) Get(peerIP netip.Prefix) (peerEntry, bool) { return c[0], true } -func (pm *peerMap) Add(eid string, peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) (bool, int) { +func (pm *peerMap) Add(eid string, peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) (bool, int) { pEntry := peerEntry{ eid: eid, - mac: macAddrOf(peerMac), + mac: peerMac, vtep: vtep, } b, i := pm.mp.Insert(peerIP, pEntry) @@ -64,10 +64,10 @@ func (pm *peerMap) Add(eid string, peerIP netip.Prefix, peerMac net.HardwareAddr return b, i } -func (pm *peerMap) Delete(eid string, peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) (bool, int) { +func (pm *peerMap) Delete(eid string, peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) (bool, int) { pEntry := peerEntry{ eid: eid, - mac: macAddrOf(peerMac), + mac: peerMac, vtep: vtep, } @@ -94,7 +94,7 @@ func (n *network) initSandboxPeerDB() error { var errs []error n.peerdb.Walk(func(peerIP netip.Prefix, pEntry peerEntry) { if !pEntry.isLocal() { - if err := n.addNeighbor(peerIP, pEntry.mac.HardwareAddr(), pEntry.vtep); err != nil { + if err := n.addNeighbor(peerIP, pEntry.mac, pEntry.vtep); err != nil { errs = append(errs, fmt.Errorf("failed to add neighbor entries for %s: %w", peerIP, err)) } } @@ -105,7 +105,7 @@ func (n *network) initSandboxPeerDB() error { // peerAdd adds a new entry to the peer database. // // Local peers are signified by an invalid vtep (i.e. netip.Addr{}). -func (n *network) peerAdd(eid string, peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) error { +func (n *network) peerAdd(eid string, peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) error { if eid == "" { return errors.New("invalid endpoint id") } @@ -130,7 +130,7 @@ func (n *network) peerAdd(eid string, peerIP netip.Prefix, peerMac net.HardwareA } // addNeighbor programs the kernel so the given peer is reachable through the VXLAN tunnel. -func (n *network) addNeighbor(peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) error { +func (n *network) addNeighbor(peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) error { if n.sbox == nil { // We are hitting this case for all the events that are arriving before that the sandbox // is being created. The peer got already added into the database and the sandbox init will @@ -154,13 +154,13 @@ func (n *network) addNeighbor(peerIP netip.Prefix, peerMac net.HardwareAddr, vte } // Add neighbor entry for the peer IP - if err := n.sbox.AddNeighbor(peerIP.Addr().AsSlice(), peerMac, osl.WithLinkName(s.vxlanName)); err != nil { + if err := n.sbox.AddNeighbor(peerIP.Addr().AsSlice(), peerMac.AsSlice(), osl.WithLinkName(s.vxlanName)); err != nil { return fmt.Errorf("could not add neighbor entry into the sandbox: %w", err) } // Add fdb entry to the bridge for the peer mac - if n.fdbCnt.Add(ipmacOf(vtep, peerMac), 1) == 1 { - if err := n.sbox.AddNeighbor(vtep.AsSlice(), peerMac, osl.WithLinkName(s.vxlanName), osl.WithFamily(syscall.AF_BRIDGE)); err != nil { + if n.fdbCnt.Add(hashable.IPMACFrom(vtep, peerMac), 1) == 1 { + if err := n.sbox.AddNeighbor(vtep.AsSlice(), peerMac.AsSlice(), osl.WithLinkName(s.vxlanName), osl.WithFamily(syscall.AF_BRIDGE)); err != nil { return fmt.Errorf("could not add fdb entry into the sandbox: %w", err) } } @@ -171,7 +171,7 @@ func (n *network) addNeighbor(peerIP netip.Prefix, peerMac net.HardwareAddr, vte // peerDelete removes an entry from the peer database. // // Local peers are signified by an invalid vtep (i.e. netip.Addr{}). -func (n *network) peerDelete(eid string, peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) error { +func (n *network) peerDelete(eid string, peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) error { if eid == "" { return errors.New("invalid endpoint id") } @@ -207,7 +207,7 @@ func (n *network) peerDelete(eid string, peerIP netip.Prefix, peerMac net.Hardwa if !ok { return fmt.Errorf("peerDelete: unable to restore a configuration: no entry for %v found in the database", peerIP) } - err := n.addNeighbor(peerIP, peerEntry.mac.HardwareAddr(), peerEntry.vtep) + err := n.addNeighbor(peerIP, peerEntry.mac, peerEntry.vtep) if err != nil { return fmt.Errorf("peer delete operation failed: %w", err) } @@ -217,7 +217,7 @@ func (n *network) peerDelete(eid string, peerIP netip.Prefix, peerMac net.Hardwa // deleteNeighbor removes programming from the kernel for the given peer to be // reachable through the VXLAN tunnel. It is the inverse of [driver.addNeighbor]. -func (n *network) deleteNeighbor(peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) error { +func (n *network) deleteNeighbor(peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) error { if n.sbox == nil { return nil } @@ -233,14 +233,14 @@ func (n *network) deleteNeighbor(peerIP netip.Prefix, peerMac net.HardwareAddr, return fmt.Errorf("could not find the subnet %q in network %q", peerIP.String(), n.id) } // Remove fdb entry to the bridge for the peer mac - if n.fdbCnt.Add(ipmacOf(vtep, peerMac), -1) == 0 { - if err := n.sbox.DeleteNeighbor(vtep.AsSlice(), peerMac, osl.WithLinkName(s.vxlanName), osl.WithFamily(syscall.AF_BRIDGE)); err != nil { + if n.fdbCnt.Add(hashable.IPMACFrom(vtep, peerMac), -1) == 0 { + if err := n.sbox.DeleteNeighbor(vtep.AsSlice(), peerMac.AsSlice(), osl.WithLinkName(s.vxlanName), osl.WithFamily(syscall.AF_BRIDGE)); err != nil { return fmt.Errorf("could not delete fdb entry in the sandbox: %w", err) } } // Delete neighbor entry for the peer IP - if err := n.sbox.DeleteNeighbor(peerIP.Addr().AsSlice(), peerMac, osl.WithLinkName(s.vxlanName)); err != nil { + if err := n.sbox.DeleteNeighbor(peerIP.Addr().AsSlice(), peerMac.AsSlice(), osl.WithLinkName(s.vxlanName)); err != nil { return fmt.Errorf("could not delete neighbor entry in the sandbox:%v", err) } diff --git a/daemon/libnetwork/drivers/overlay/types.go b/daemon/libnetwork/drivers/overlay/types.go deleted file mode 100644 index fe87b00046..0000000000 --- a/daemon/libnetwork/drivers/overlay/types.go +++ /dev/null @@ -1,52 +0,0 @@ -package overlay - -// Handy utility types for making unhashable values hashable. - -import ( - "net" - "net/netip" -) - -// macAddr is a hashable encoding of a MAC address. -type macAddr uint64 - -// macAddrOf converts a net.HardwareAddr to a macAddr. -func macAddrOf(mac net.HardwareAddr) macAddr { - if len(mac) != 6 { - return 0 - } - return macAddr(mac[0])<<40 | macAddr(mac[1])<<32 | macAddr(mac[2])<<24 | - macAddr(mac[3])<<16 | macAddr(mac[4])<<8 | macAddr(mac[5]) -} - -// HardwareAddr converts a macAddr back to a net.HardwareAddr. -func (p macAddr) HardwareAddr() net.HardwareAddr { - mac := [6]byte{ - byte(p >> 40), byte(p >> 32), byte(p >> 24), - byte(p >> 16), byte(p >> 8), byte(p), - } - return mac[:] -} - -// String returns p.HardwareAddr().String(). -func (p macAddr) String() string { - return p.HardwareAddr().String() -} - -// ipmac is a hashable tuple of an IP address and a MAC address suitable for use as a map key. -type ipmac struct { - ip netip.Addr - mac macAddr -} - -// ipmacOf is a convenience constructor for creating an ipmac from a [net.HardwareAddr]. -func ipmacOf(ip netip.Addr, mac net.HardwareAddr) ipmac { - return ipmac{ - ip: ip, - mac: macAddrOf(mac), - } -} - -func (i ipmac) String() string { - return i.ip.String() + " " + i.mac.String() -} diff --git a/daemon/libnetwork/drivers/overlay/types_test.go b/daemon/libnetwork/drivers/overlay/types_test.go deleted file mode 100644 index fed39e1b06..0000000000 --- a/daemon/libnetwork/drivers/overlay/types_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package overlay - -import ( - "net" - "net/netip" - "testing" - - "gotest.tools/v3/assert" - is "gotest.tools/v3/assert/cmp" -) - -func TestMACAddrOf(t *testing.T) { - want := net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06} - assert.DeepEqual(t, macAddrOf(want).HardwareAddr(), want) -} - -func TestIPMACOf(t *testing.T) { - assert.Check(t, is.Equal(ipmacOf(netip.Addr{}, nil), ipmac{})) - assert.Check(t, is.Equal( - ipmacOf( - netip.MustParseAddr("11.22.33.44"), - net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, - ), - ipmac{ - ip: netip.MustParseAddr("11.22.33.44"), - mac: macAddrOf(net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}), - }, - )) -} diff --git a/daemon/libnetwork/internal/hashable/net.go b/daemon/libnetwork/internal/hashable/net.go new file mode 100644 index 0000000000..f33a3fbe79 --- /dev/null +++ b/daemon/libnetwork/internal/hashable/net.go @@ -0,0 +1,82 @@ +// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16: +//go:build go1.23 + +// Package hashable provides handy utility types for making unhashable values +// hashable. +package hashable + +import ( + "net" + "net/netip" +) + +// MACAddr is a hashable encoding of a MAC address. +type MACAddr uint64 + +// MACAddrFromSlice parses the 6-byte slice as a MAC-48 address. +// Note that a [net.HardwareAddr] can be passed directly as the []byte argument. +// If slice's length is not 6, MACAddrFromSlice returns 0, false. +func MACAddrFromSlice(slice net.HardwareAddr) (MACAddr, bool) { + if len(slice) != 6 { + return 0, false + } + return MACAddrFrom6([6]byte(slice)), true +} + +// MACAddrFrom6 returns the address of the MAC-48 address +// given by the bytes in addr. +func MACAddrFrom6(addr [6]byte) MACAddr { + return MACAddr(addr[0])<<40 | MACAddr(addr[1])<<32 | MACAddr(addr[2])<<24 | + MACAddr(addr[3])<<16 | MACAddr(addr[4])<<8 | MACAddr(addr[5]) +} + +// ParseMAC parses s as an IEEE 802 MAC-48 address using one of the formats +// accepted by [net.ParseMAC]. +func ParseMAC(s string) (MACAddr, error) { + hw, err := net.ParseMAC(s) + if err != nil { + return 0, err + } + mac, ok := MACAddrFromSlice(hw) + if !ok { + return 0, &net.AddrError{Err: "not a MAC-48 address", Addr: s} + } + return mac, nil +} + +// AsSlice returns a MAC address in its 6-byte representation. +func (p MACAddr) AsSlice() []byte { + mac := [6]byte{ + byte(p >> 40), byte(p >> 32), byte(p >> 24), + byte(p >> 16), byte(p >> 8), byte(p), + } + return mac[:] +} + +// String returns net.HardwareAddr(p.AsSlice()).String(). +func (p MACAddr) String() string { + return net.HardwareAddr(p.AsSlice()).String() +} + +// IPMAC is a hashable tuple of an IP address and a MAC address suitable for use as a map key. +type IPMAC struct { + ip netip.Addr + mac MACAddr +} + +// IPMACFrom returns an [IPMAC] with the provided IP and MAC addresses. +func IPMACFrom(ip netip.Addr, mac MACAddr) IPMAC { + return IPMAC{ip: ip, mac: mac} +} + +func (i IPMAC) String() string { + return i.ip.String() + " " + i.mac.String() +} + +func (i IPMAC) IP() netip.Addr { + return i.ip +} + +func (i IPMAC) MAC() MACAddr { + return i.mac +} diff --git a/daemon/libnetwork/internal/hashable/net_test.go b/daemon/libnetwork/internal/hashable/net_test.go new file mode 100644 index 0000000000..7d281a5bf6 --- /dev/null +++ b/daemon/libnetwork/internal/hashable/net_test.go @@ -0,0 +1,66 @@ +package hashable + +import ( + "net" + "net/netip" + "testing" + + "gotest.tools/v3/assert" + is "gotest.tools/v3/assert/cmp" +) + +// Assert that the types are hashable. +var ( + _ map[MACAddr]bool + _ map[IPMAC]bool +) + +func TestMACAddrFrom6(t *testing.T) { + want := [6]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06} + assert.DeepEqual(t, MACAddrFrom6(want).AsSlice(), want[:]) +} + +func TestMACAddrFromSlice(t *testing.T) { + mac, ok := MACAddrFromSlice(net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}) + assert.Check(t, ok) + assert.Check(t, is.DeepEqual(mac.AsSlice(), []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})) + + // Invalid length + for _, tc := range [][]byte{ + {0x01, 0x02, 0x03, 0x04, 0x05}, + {0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07}, + {}, + nil, + } { + mac, ok = MACAddrFromSlice(net.HardwareAddr(tc)) + assert.Check(t, !ok, "want MACAddrFromSlice(%#v) ok=false, got true", tc) + assert.Check(t, is.DeepEqual(mac.AsSlice(), []byte{0, 0, 0, 0, 0, 0}), "want MACAddrFromSlice(%#v) = %#v, got %#v", tc, []byte{0, 0, 0, 0, 0, 0}, mac.AsSlice()) + } +} + +func TestParseMAC(t *testing.T) { + mac, err := ParseMAC("01:02:03:04:05:06") + assert.Check(t, err) + assert.Check(t, is.DeepEqual(mac.AsSlice(), []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})) + + // Invalid MAC address + _, err = ParseMAC("01:02:03:04:05:06:07:08") + assert.Check(t, is.ErrorContains(err, "not a MAC-48 address")) +} + +func TestMACAddr_String(t *testing.T) { + mac := MACAddrFrom6([6]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}) + assert.Check(t, is.Equal(mac.String(), "01:02:03:04:05:06")) + assert.Check(t, is.Equal(MACAddr(0).String(), "00:00:00:00:00:00")) +} + +func TestIPMACFrom(t *testing.T) { + assert.Check(t, is.Equal(IPMACFrom(netip.Addr{}, 0), IPMAC{})) + + ipm := IPMACFrom( + netip.MustParseAddr("11.22.33.44"), + MACAddrFrom6([6]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}), + ) + assert.Check(t, is.Equal(ipm.IP(), netip.MustParseAddr("11.22.33.44"))) + assert.Check(t, is.Equal(ipm.MAC(), MACAddrFrom6([6]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}))) +}