Browse code

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 <csnider@mirantis.com>

Cory Snider authored on 2025/07/15 05:25:02
Showing 8 changed files
... ...
@@ -6,12 +6,12 @@ import (
6 6
 	"context"
7 7
 	"errors"
8 8
 	"fmt"
9
-	"net"
10 9
 	"net/netip"
11 10
 	"syscall"
12 11
 
13 12
 	"github.com/containerd/log"
14 13
 	"github.com/docker/docker/daemon/libnetwork/driverapi"
14
+	"github.com/docker/docker/daemon/libnetwork/internal/hashable"
15 15
 	"github.com/docker/docker/daemon/libnetwork/internal/netiputil"
16 16
 	"github.com/docker/docker/daemon/libnetwork/netlabel"
17 17
 	"github.com/docker/docker/daemon/libnetwork/ns"
... ...
@@ -104,7 +104,7 @@ func (d *driver) Join(ctx context.Context, nid, eid string, sboxKey string, jinf
104 104
 		return err
105 105
 	}
106 106
 
107
-	if err = nlh.LinkSetHardwareAddr(veth, ep.mac); err != nil {
107
+	if err = nlh.LinkSetHardwareAddr(veth, ep.mac.AsSlice()); err != nil {
108 108
 		return fmt.Errorf("could not set mac address (%v) to the container interface: %v", ep.mac, err)
109 109
 	}
110 110
 
... ...
@@ -187,7 +187,7 @@ func (d *driver) EventNotify(etype driverapi.EventType, nid, tableName, key stri
187 187
 		return
188 188
 	}
189 189
 
190
-	mac, err := net.ParseMAC(peer.EndpointMAC)
190
+	mac, err := hashable.ParseMAC(peer.EndpointMAC)
191 191
 	if err != nil {
192 192
 		log.G(context.TODO()).WithError(err).Errorf("Invalid mac %s received in event notify", peer.EndpointMAC)
193 193
 		return
... ...
@@ -6,11 +6,11 @@ import (
6 6
 	"context"
7 7
 	"errors"
8 8
 	"fmt"
9
-	"net"
10 9
 	"net/netip"
11 10
 
12 11
 	"github.com/containerd/log"
13 12
 	"github.com/docker/docker/daemon/libnetwork/driverapi"
13
+	"github.com/docker/docker/daemon/libnetwork/internal/hashable"
14 14
 	"github.com/docker/docker/daemon/libnetwork/internal/netiputil"
15 15
 	"github.com/docker/docker/daemon/libnetwork/netutils"
16 16
 	"github.com/docker/docker/daemon/libnetwork/ns"
... ...
@@ -22,7 +22,7 @@ type endpoint struct {
22 22
 	id     string
23 23
 	nid    string
24 24
 	ifName string
25
-	mac    net.HardwareAddr
25
+	mac    hashable.MACAddr
26 26
 	addr   netip.Prefix
27 27
 }
28 28
 
... ...
@@ -48,7 +48,6 @@ func (d *driver) CreateEndpoint(_ context.Context, nid, eid string, ifInfo drive
48 48
 	ep := &endpoint{
49 49
 		id:  eid,
50 50
 		nid: n.id,
51
-		mac: ifInfo.MacAddress(),
52 51
 	}
53 52
 	var ok bool
54 53
 	ep.addr, ok = netiputil.ToPrefix(ifInfo.Address())
... ...
@@ -60,9 +59,19 @@ func (d *driver) CreateEndpoint(_ context.Context, nid, eid string, ifInfo drive
60 60
 		return fmt.Errorf("no matching subnet for IP %q in network %q", ep.addr, nid)
61 61
 	}
62 62
 
63
-	if ep.mac == nil {
64
-		ep.mac = netutils.GenerateMACFromIP(ep.addr.Addr().AsSlice())
65
-		if err := ifInfo.SetMacAddress(ep.mac); err != nil {
63
+	if ifmac := ifInfo.MacAddress(); ifmac != nil {
64
+		var ok bool
65
+		ep.mac, ok = hashable.MACAddrFromSlice(ifInfo.MacAddress())
66
+		if !ok {
67
+			return fmt.Errorf("invalid MAC address %q assigned to endpoint: unexpected length", ifmac)
68
+		}
69
+	} else {
70
+		var ok bool
71
+		ep.mac, ok = hashable.MACAddrFromSlice(netutils.GenerateMACFromIP(ep.addr.Addr().AsSlice()))
72
+		if !ok {
73
+			panic("GenerateMACFromIP returned a HardwareAddress that is not a MAC-48")
74
+		}
75
+		if err := ifInfo.SetMacAddress(ep.mac.AsSlice()); err != nil {
66 76
 			return err
67 77
 		}
68 78
 	}
... ...
@@ -19,6 +19,7 @@ import (
19 19
 	"github.com/docker/docker/daemon/libnetwork/driverapi"
20 20
 	"github.com/docker/docker/daemon/libnetwork/drivers/overlay/overlayutils"
21 21
 	"github.com/docker/docker/daemon/libnetwork/internal/countmap"
22
+	"github.com/docker/docker/daemon/libnetwork/internal/hashable"
22 23
 	"github.com/docker/docker/daemon/libnetwork/internal/netiputil"
23 24
 	"github.com/docker/docker/daemon/libnetwork/netlabel"
24 25
 	"github.com/docker/docker/daemon/libnetwork/ns"
... ...
@@ -65,7 +66,7 @@ type network struct {
65 65
 	endpoints endpointTable
66 66
 	joinCnt   int
67 67
 	// Ref count of VXLAN Forwarding Database entries programmed into the kernel
68
-	fdbCnt    countmap.Map[ipmac]
68
+	fdbCnt    countmap.Map[hashable.IPMAC]
69 69
 	sboxInit  bool
70 70
 	initEpoch int
71 71
 	initErr   error
... ...
@@ -110,7 +111,7 @@ func (d *driver) CreateNetwork(ctx context.Context, id string, option map[string
110 110
 		driver:    d,
111 111
 		endpoints: endpointTable{},
112 112
 		subnets:   []*subnet{},
113
-		fdbCnt:    countmap.Map[ipmac]{},
113
+		fdbCnt:    countmap.Map[hashable.IPMAC]{},
114 114
 	}
115 115
 
116 116
 	vnis := make([]uint32, 0, len(ipV4Data))
... ...
@@ -607,7 +608,7 @@ func (n *network) initSandbox() error {
607 607
 
608 608
 	// this is needed to let the peerAdd configure the sandbox
609 609
 	n.sbox = sbox
610
-	n.fdbCnt = countmap.Map[ipmac]{}
610
+	n.fdbCnt = countmap.Map[hashable.IPMAC]{}
611 611
 
612 612
 	return nil
613 613
 }
... ...
@@ -7,11 +7,11 @@ import (
7 7
 	"context"
8 8
 	"errors"
9 9
 	"fmt"
10
-	"net"
11 10
 	"net/netip"
12 11
 	"syscall"
13 12
 
14 13
 	"github.com/containerd/log"
14
+	"github.com/docker/docker/daemon/libnetwork/internal/hashable"
15 15
 	"github.com/docker/docker/daemon/libnetwork/internal/setmatrix"
16 16
 	"github.com/docker/docker/daemon/libnetwork/osl"
17 17
 )
... ...
@@ -20,7 +20,7 @@ const ovPeerTable = "overlay_peer_table"
20 20
 
21 21
 type peerEntry struct {
22 22
 	eid  string
23
-	mac  macAddr
23
+	mac  hashable.MACAddr
24 24
 	vtep netip.Addr
25 25
 }
26 26
 
... ...
@@ -49,10 +49,10 @@ func (pm *peerMap) Get(peerIP netip.Prefix) (peerEntry, bool) {
49 49
 	return c[0], true
50 50
 }
51 51
 
52
-func (pm *peerMap) Add(eid string, peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) (bool, int) {
52
+func (pm *peerMap) Add(eid string, peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) (bool, int) {
53 53
 	pEntry := peerEntry{
54 54
 		eid:  eid,
55
-		mac:  macAddrOf(peerMac),
55
+		mac:  peerMac,
56 56
 		vtep: vtep,
57 57
 	}
58 58
 	b, i := pm.mp.Insert(peerIP, pEntry)
... ...
@@ -64,10 +64,10 @@ func (pm *peerMap) Add(eid string, peerIP netip.Prefix, peerMac net.HardwareAddr
64 64
 	return b, i
65 65
 }
66 66
 
67
-func (pm *peerMap) Delete(eid string, peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) (bool, int) {
67
+func (pm *peerMap) Delete(eid string, peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) (bool, int) {
68 68
 	pEntry := peerEntry{
69 69
 		eid:  eid,
70
-		mac:  macAddrOf(peerMac),
70
+		mac:  peerMac,
71 71
 		vtep: vtep,
72 72
 	}
73 73
 
... ...
@@ -94,7 +94,7 @@ func (n *network) initSandboxPeerDB() error {
94 94
 	var errs []error
95 95
 	n.peerdb.Walk(func(peerIP netip.Prefix, pEntry peerEntry) {
96 96
 		if !pEntry.isLocal() {
97
-			if err := n.addNeighbor(peerIP, pEntry.mac.HardwareAddr(), pEntry.vtep); err != nil {
97
+			if err := n.addNeighbor(peerIP, pEntry.mac, pEntry.vtep); err != nil {
98 98
 				errs = append(errs, fmt.Errorf("failed to add neighbor entries for %s: %w", peerIP, err))
99 99
 			}
100 100
 		}
... ...
@@ -105,7 +105,7 @@ func (n *network) initSandboxPeerDB() error {
105 105
 // peerAdd adds a new entry to the peer database.
106 106
 //
107 107
 // Local peers are signified by an invalid vtep (i.e. netip.Addr{}).
108
-func (n *network) peerAdd(eid string, peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) error {
108
+func (n *network) peerAdd(eid string, peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) error {
109 109
 	if eid == "" {
110 110
 		return errors.New("invalid endpoint id")
111 111
 	}
... ...
@@ -130,7 +130,7 @@ func (n *network) peerAdd(eid string, peerIP netip.Prefix, peerMac net.HardwareA
130 130
 }
131 131
 
132 132
 // addNeighbor programs the kernel so the given peer is reachable through the VXLAN tunnel.
133
-func (n *network) addNeighbor(peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) error {
133
+func (n *network) addNeighbor(peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) error {
134 134
 	if n.sbox == nil {
135 135
 		// We are hitting this case for all the events that are arriving before that the sandbox
136 136
 		// 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
154 154
 	}
155 155
 
156 156
 	// Add neighbor entry for the peer IP
157
-	if err := n.sbox.AddNeighbor(peerIP.Addr().AsSlice(), peerMac, osl.WithLinkName(s.vxlanName)); err != nil {
157
+	if err := n.sbox.AddNeighbor(peerIP.Addr().AsSlice(), peerMac.AsSlice(), osl.WithLinkName(s.vxlanName)); err != nil {
158 158
 		return fmt.Errorf("could not add neighbor entry into the sandbox: %w", err)
159 159
 	}
160 160
 
161 161
 	// Add fdb entry to the bridge for the peer mac
162
-	if n.fdbCnt.Add(ipmacOf(vtep, peerMac), 1) == 1 {
163
-		if err := n.sbox.AddNeighbor(vtep.AsSlice(), peerMac, osl.WithLinkName(s.vxlanName), osl.WithFamily(syscall.AF_BRIDGE)); err != nil {
162
+	if n.fdbCnt.Add(hashable.IPMACFrom(vtep, peerMac), 1) == 1 {
163
+		if err := n.sbox.AddNeighbor(vtep.AsSlice(), peerMac.AsSlice(), osl.WithLinkName(s.vxlanName), osl.WithFamily(syscall.AF_BRIDGE)); err != nil {
164 164
 			return fmt.Errorf("could not add fdb entry into the sandbox: %w", err)
165 165
 		}
166 166
 	}
... ...
@@ -171,7 +171,7 @@ func (n *network) addNeighbor(peerIP netip.Prefix, peerMac net.HardwareAddr, vte
171 171
 // peerDelete removes an entry from the peer database.
172 172
 //
173 173
 // Local peers are signified by an invalid vtep (i.e. netip.Addr{}).
174
-func (n *network) peerDelete(eid string, peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) error {
174
+func (n *network) peerDelete(eid string, peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) error {
175 175
 	if eid == "" {
176 176
 		return errors.New("invalid endpoint id")
177 177
 	}
... ...
@@ -207,7 +207,7 @@ func (n *network) peerDelete(eid string, peerIP netip.Prefix, peerMac net.Hardwa
207 207
 		if !ok {
208 208
 			return fmt.Errorf("peerDelete: unable to restore a configuration: no entry for %v found in the database", peerIP)
209 209
 		}
210
-		err := n.addNeighbor(peerIP, peerEntry.mac.HardwareAddr(), peerEntry.vtep)
210
+		err := n.addNeighbor(peerIP, peerEntry.mac, peerEntry.vtep)
211 211
 		if err != nil {
212 212
 			return fmt.Errorf("peer delete operation failed: %w", err)
213 213
 		}
... ...
@@ -217,7 +217,7 @@ func (n *network) peerDelete(eid string, peerIP netip.Prefix, peerMac net.Hardwa
217 217
 
218 218
 // deleteNeighbor removes programming from the kernel for the given peer to be
219 219
 // reachable through the VXLAN tunnel. It is the inverse of [driver.addNeighbor].
220
-func (n *network) deleteNeighbor(peerIP netip.Prefix, peerMac net.HardwareAddr, vtep netip.Addr) error {
220
+func (n *network) deleteNeighbor(peerIP netip.Prefix, peerMac hashable.MACAddr, vtep netip.Addr) error {
221 221
 	if n.sbox == nil {
222 222
 		return nil
223 223
 	}
... ...
@@ -233,14 +233,14 @@ func (n *network) deleteNeighbor(peerIP netip.Prefix, peerMac net.HardwareAddr,
233 233
 		return fmt.Errorf("could not find the subnet %q in network %q", peerIP.String(), n.id)
234 234
 	}
235 235
 	// Remove fdb entry to the bridge for the peer mac
236
-	if n.fdbCnt.Add(ipmacOf(vtep, peerMac), -1) == 0 {
237
-		if err := n.sbox.DeleteNeighbor(vtep.AsSlice(), peerMac, osl.WithLinkName(s.vxlanName), osl.WithFamily(syscall.AF_BRIDGE)); err != nil {
236
+	if n.fdbCnt.Add(hashable.IPMACFrom(vtep, peerMac), -1) == 0 {
237
+		if err := n.sbox.DeleteNeighbor(vtep.AsSlice(), peerMac.AsSlice(), osl.WithLinkName(s.vxlanName), osl.WithFamily(syscall.AF_BRIDGE)); err != nil {
238 238
 			return fmt.Errorf("could not delete fdb entry in the sandbox: %w", err)
239 239
 		}
240 240
 	}
241 241
 
242 242
 	// Delete neighbor entry for the peer IP
243
-	if err := n.sbox.DeleteNeighbor(peerIP.Addr().AsSlice(), peerMac, osl.WithLinkName(s.vxlanName)); err != nil {
243
+	if err := n.sbox.DeleteNeighbor(peerIP.Addr().AsSlice(), peerMac.AsSlice(), osl.WithLinkName(s.vxlanName)); err != nil {
244 244
 		return fmt.Errorf("could not delete neighbor entry in the sandbox:%v", err)
245 245
 	}
246 246
 
247 247
deleted file mode 100644
... ...
@@ -1,52 +0,0 @@
1
-package overlay
2
-
3
-// Handy utility types for making unhashable values hashable.
4
-
5
-import (
6
-	"net"
7
-	"net/netip"
8
-)
9
-
10
-// macAddr is a hashable encoding of a MAC address.
11
-type macAddr uint64
12
-
13
-// macAddrOf converts a net.HardwareAddr to a macAddr.
14
-func macAddrOf(mac net.HardwareAddr) macAddr {
15
-	if len(mac) != 6 {
16
-		return 0
17
-	}
18
-	return macAddr(mac[0])<<40 | macAddr(mac[1])<<32 | macAddr(mac[2])<<24 |
19
-		macAddr(mac[3])<<16 | macAddr(mac[4])<<8 | macAddr(mac[5])
20
-}
21
-
22
-// HardwareAddr converts a macAddr back to a net.HardwareAddr.
23
-func (p macAddr) HardwareAddr() net.HardwareAddr {
24
-	mac := [6]byte{
25
-		byte(p >> 40), byte(p >> 32), byte(p >> 24),
26
-		byte(p >> 16), byte(p >> 8), byte(p),
27
-	}
28
-	return mac[:]
29
-}
30
-
31
-// String returns p.HardwareAddr().String().
32
-func (p macAddr) String() string {
33
-	return p.HardwareAddr().String()
34
-}
35
-
36
-// ipmac is a hashable tuple of an IP address and a MAC address suitable for use as a map key.
37
-type ipmac struct {
38
-	ip  netip.Addr
39
-	mac macAddr
40
-}
41
-
42
-// ipmacOf is a convenience constructor for creating an ipmac from a [net.HardwareAddr].
43
-func ipmacOf(ip netip.Addr, mac net.HardwareAddr) ipmac {
44
-	return ipmac{
45
-		ip:  ip,
46
-		mac: macAddrOf(mac),
47
-	}
48
-}
49
-
50
-func (i ipmac) String() string {
51
-	return i.ip.String() + " " + i.mac.String()
52
-}
53 1
deleted file mode 100644
... ...
@@ -1,29 +0,0 @@
1
-package overlay
2
-
3
-import (
4
-	"net"
5
-	"net/netip"
6
-	"testing"
7
-
8
-	"gotest.tools/v3/assert"
9
-	is "gotest.tools/v3/assert/cmp"
10
-)
11
-
12
-func TestMACAddrOf(t *testing.T) {
13
-	want := net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
14
-	assert.DeepEqual(t, macAddrOf(want).HardwareAddr(), want)
15
-}
16
-
17
-func TestIPMACOf(t *testing.T) {
18
-	assert.Check(t, is.Equal(ipmacOf(netip.Addr{}, nil), ipmac{}))
19
-	assert.Check(t, is.Equal(
20
-		ipmacOf(
21
-			netip.MustParseAddr("11.22.33.44"),
22
-			net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},
23
-		),
24
-		ipmac{
25
-			ip:  netip.MustParseAddr("11.22.33.44"),
26
-			mac: macAddrOf(net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),
27
-		},
28
-	))
29
-}
30 1
new file mode 100644
... ...
@@ -0,0 +1,82 @@
0
+// FIXME(thaJeztah): remove once we are a module; the go:build directive prevents go from downgrading language version to go1.16:
1
+//go:build go1.23
2
+
3
+// Package hashable provides handy utility types for making unhashable values
4
+// hashable.
5
+package hashable
6
+
7
+import (
8
+	"net"
9
+	"net/netip"
10
+)
11
+
12
+// MACAddr is a hashable encoding of a MAC address.
13
+type MACAddr uint64
14
+
15
+// MACAddrFromSlice parses the 6-byte slice as a MAC-48 address.
16
+// Note that a [net.HardwareAddr] can be passed directly as the []byte argument.
17
+// If slice's length is not 6, MACAddrFromSlice returns 0, false.
18
+func MACAddrFromSlice(slice net.HardwareAddr) (MACAddr, bool) {
19
+	if len(slice) != 6 {
20
+		return 0, false
21
+	}
22
+	return MACAddrFrom6([6]byte(slice)), true
23
+}
24
+
25
+// MACAddrFrom6 returns the address of the MAC-48 address
26
+// given by the bytes in addr.
27
+func MACAddrFrom6(addr [6]byte) MACAddr {
28
+	return MACAddr(addr[0])<<40 | MACAddr(addr[1])<<32 | MACAddr(addr[2])<<24 |
29
+		MACAddr(addr[3])<<16 | MACAddr(addr[4])<<8 | MACAddr(addr[5])
30
+}
31
+
32
+// ParseMAC parses s as an IEEE 802 MAC-48 address using one of the formats
33
+// accepted by [net.ParseMAC].
34
+func ParseMAC(s string) (MACAddr, error) {
35
+	hw, err := net.ParseMAC(s)
36
+	if err != nil {
37
+		return 0, err
38
+	}
39
+	mac, ok := MACAddrFromSlice(hw)
40
+	if !ok {
41
+		return 0, &net.AddrError{Err: "not a MAC-48 address", Addr: s}
42
+	}
43
+	return mac, nil
44
+}
45
+
46
+// AsSlice returns a MAC address in its 6-byte representation.
47
+func (p MACAddr) AsSlice() []byte {
48
+	mac := [6]byte{
49
+		byte(p >> 40), byte(p >> 32), byte(p >> 24),
50
+		byte(p >> 16), byte(p >> 8), byte(p),
51
+	}
52
+	return mac[:]
53
+}
54
+
55
+// String returns net.HardwareAddr(p.AsSlice()).String().
56
+func (p MACAddr) String() string {
57
+	return net.HardwareAddr(p.AsSlice()).String()
58
+}
59
+
60
+// IPMAC is a hashable tuple of an IP address and a MAC address suitable for use as a map key.
61
+type IPMAC struct {
62
+	ip  netip.Addr
63
+	mac MACAddr
64
+}
65
+
66
+// IPMACFrom returns an [IPMAC] with the provided IP and MAC addresses.
67
+func IPMACFrom(ip netip.Addr, mac MACAddr) IPMAC {
68
+	return IPMAC{ip: ip, mac: mac}
69
+}
70
+
71
+func (i IPMAC) String() string {
72
+	return i.ip.String() + " " + i.mac.String()
73
+}
74
+
75
+func (i IPMAC) IP() netip.Addr {
76
+	return i.ip
77
+}
78
+
79
+func (i IPMAC) MAC() MACAddr {
80
+	return i.mac
81
+}
0 82
new file mode 100644
... ...
@@ -0,0 +1,66 @@
0
+package hashable
1
+
2
+import (
3
+	"net"
4
+	"net/netip"
5
+	"testing"
6
+
7
+	"gotest.tools/v3/assert"
8
+	is "gotest.tools/v3/assert/cmp"
9
+)
10
+
11
+// Assert that the types are hashable.
12
+var (
13
+	_ map[MACAddr]bool
14
+	_ map[IPMAC]bool
15
+)
16
+
17
+func TestMACAddrFrom6(t *testing.T) {
18
+	want := [6]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
19
+	assert.DeepEqual(t, MACAddrFrom6(want).AsSlice(), want[:])
20
+}
21
+
22
+func TestMACAddrFromSlice(t *testing.T) {
23
+	mac, ok := MACAddrFromSlice(net.HardwareAddr{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})
24
+	assert.Check(t, ok)
25
+	assert.Check(t, is.DeepEqual(mac.AsSlice(), []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}))
26
+
27
+	// Invalid length
28
+	for _, tc := range [][]byte{
29
+		{0x01, 0x02, 0x03, 0x04, 0x05},
30
+		{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07},
31
+		{},
32
+		nil,
33
+	} {
34
+		mac, ok = MACAddrFromSlice(net.HardwareAddr(tc))
35
+		assert.Check(t, !ok, "want MACAddrFromSlice(%#v) ok=false, got true", tc)
36
+		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())
37
+	}
38
+}
39
+
40
+func TestParseMAC(t *testing.T) {
41
+	mac, err := ParseMAC("01:02:03:04:05:06")
42
+	assert.Check(t, err)
43
+	assert.Check(t, is.DeepEqual(mac.AsSlice(), []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}))
44
+
45
+	// Invalid MAC address
46
+	_, err = ParseMAC("01:02:03:04:05:06:07:08")
47
+	assert.Check(t, is.ErrorContains(err, "not a MAC-48 address"))
48
+}
49
+
50
+func TestMACAddr_String(t *testing.T) {
51
+	mac := MACAddrFrom6([6]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})
52
+	assert.Check(t, is.Equal(mac.String(), "01:02:03:04:05:06"))
53
+	assert.Check(t, is.Equal(MACAddr(0).String(), "00:00:00:00:00:00"))
54
+}
55
+
56
+func TestIPMACFrom(t *testing.T) {
57
+	assert.Check(t, is.Equal(IPMACFrom(netip.Addr{}, 0), IPMAC{}))
58
+
59
+	ipm := IPMACFrom(
60
+		netip.MustParseAddr("11.22.33.44"),
61
+		MACAddrFrom6([6]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}),
62
+	)
63
+	assert.Check(t, is.Equal(ipm.IP(), netip.MustParseAddr("11.22.33.44")))
64
+	assert.Check(t, is.Equal(ipm.MAC(), MACAddrFrom6([6]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06})))
65
+}