api/t/network: represent MAC addrs as byte slices

Make invalid states unrepresentable by moving away from stringly-typed
MAC address values in API structs. As go.dev/issue/29678 has not yet
been implemented, provide our own HardwareAddr byte-slice type which
implements TextMarshaler and TextUnmarshaler to retain compatibility
with the API wire format.

When stdlib's net.HardwareAddr type implements TextMarshaler and
TextUnmarshaler and GODEBUG=netmarshal becomes the default, we should be
able to make the type a straight alias for stdlib net.HardwareAddr as a
non-breaking change.

Signed-off-by: Cory Snider <csnider@mirantis.com>
This commit is contained in:
Cory Snider
2025-10-30 15:57:47 -04:00
parent 72f6cec125
commit 19f4c27d81
21 changed files with 235 additions and 75 deletions

View File

@@ -2628,6 +2628,8 @@ definitions:
type: "string" type: "string"
x-omitempty: false x-omitempty: false
example: "02:42:ac:13:00:02" example: "02:42:ac:13:00:02"
x-go-type:
type: HardwareAddr
IPv4Address: IPv4Address:
type: "string" type: "string"
x-omitempty: false x-omitempty: false
@@ -2914,6 +2916,8 @@ definitions:
MAC address for the endpoint on this network. The network driver might ignore this parameter. MAC address for the endpoint on this network. The network driver might ignore this parameter.
type: "string" type: "string"
example: "02:42:ac:11:00:04" example: "02:42:ac:11:00:04"
x-go-type:
type: HardwareAddr
Aliases: Aliases:
type: "array" type: "array"
items: items:

View File

@@ -30,7 +30,7 @@ type EndpointSettings struct {
// MacAddress may be used to specify a MAC address when the container is created. // MacAddress may be used to specify a MAC address when the container is created.
// Once the container is running, it becomes operational data (it may contain a // Once the container is running, it becomes operational data (it may contain a
// generated address). // generated address).
MacAddress string MacAddress HardwareAddr
IPPrefixLen int IPPrefixLen int
IPv6Gateway netip.Addr IPv6Gateway netip.Addr
GlobalIPv6Address netip.Addr GlobalIPv6Address netip.Addr

View File

@@ -24,7 +24,7 @@ type EndpointResource struct {
// mac address // mac address
// Example: 02:42:ac:13:00:02 // Example: 02:42:ac:13:00:02
MacAddress string `json:"MacAddress"` MacAddress HardwareAddr `json:"MacAddress"`
// IPv4 address // IPv4 address
// Example: 172.19.0.2/16 // Example: 172.19.0.2/16

View File

@@ -0,0 +1,37 @@
package network
import (
"encoding"
"fmt"
"net"
)
// A HardwareAddr represents a physical hardware address.
// It implements [encoding.TextMarshaler] and [encoding.TextUnmarshaler]
// in the absence of go.dev/issue/29678.
type HardwareAddr net.HardwareAddr
var _ encoding.TextMarshaler = (HardwareAddr)(nil)
var _ encoding.TextUnmarshaler = (*HardwareAddr)(nil)
var _ fmt.Stringer = (HardwareAddr)(nil)
func (m *HardwareAddr) UnmarshalText(text []byte) error {
if len(text) == 0 {
*m = nil
return nil
}
hw, err := net.ParseMAC(string(text))
if err != nil {
return err
}
*m = HardwareAddr(hw)
return nil
}
func (m HardwareAddr) MarshalText() ([]byte, error) {
return []byte(net.HardwareAddr(m).String()), nil
}
func (m HardwareAddr) String() string {
return net.HardwareAddr(m).String()
}

View File

@@ -0,0 +1,87 @@
package network_test
import (
"encoding/json"
"testing"
"github.com/moby/moby/api/types/network"
"gotest.tools/v3/assert"
is "gotest.tools/v3/assert/cmp"
)
func TestHardwareAddr_UnmarshalText(t *testing.T) {
cases := []struct {
in string
out network.HardwareAddr
err string
}{
{"", nil, ""},
{"00:11:22:33:44:55", network.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, ""},
{"xx:xx:xx:xx:xx:xx", network.HardwareAddr{0xde, 0xad, 0xbe, 0xef}, "invalid MAC address"},
}
for _, c := range cases {
a := network.HardwareAddr{0xde, 0xad, 0xbe, 0xef}
err := a.UnmarshalText([]byte(c.in))
if (c.err == "") != (err == nil) {
t.Errorf("UnmarshalText(%q) error = %v, want %v", c.in, err, c.err)
}
assert.Check(t, is.DeepEqual(a, c.out), "UnmarshalText(%q)", c.in)
}
}
func TestHardwareAddr_MarshalText(t *testing.T) {
cases := []struct {
in network.HardwareAddr
out string
}{
{nil, ""},
{network.HardwareAddr{}, ""},
{network.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, "00:11:22:33:44:55"},
}
for _, c := range cases {
out, err := c.in.MarshalText()
assert.Check(t, err, "MarshalText(%v)", c.in)
assert.Check(t, is.Equal(string(out), c.out), "MarshalText(%v)", c.in)
}
}
func TestHardwareAddr_MarshalJSON(t *testing.T) {
cases := []struct {
in network.HardwareAddr
out string
}{
{nil, `{"mac":""}`},
{network.HardwareAddr{}, `{"mac":""}`},
{network.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, `{"mac":"00:11:22:33:44:55"}`},
}
for _, c := range cases {
s := struct {
Mac network.HardwareAddr `json:"mac"`
}{c.in}
got, err := json.Marshal(s)
assert.Check(t, err, "json.Marshal(network.HardwareAddr(%v))", c.in)
assert.Check(t, is.Equal(string(got), c.out), "json.Marshal(network.HardwareAddr(%v))", c.in)
}
}
func TestHardwareAddr_UnmarshalJSON(t *testing.T) {
cases := []struct {
in string
out network.HardwareAddr
err string
}{
{`{"mac":""}`, nil, ""},
{`{"mac":"00:11:22:33:44:55"}`, network.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}, ""},
{`{"mac":"xx:xx:xx:xx:xx:xx"}`, network.HardwareAddr{0xde, 0xad, 0xbe, 0xef}, "invalid MAC address"},
}
for _, c := range cases {
s := struct {
Mac network.HardwareAddr `json:"mac"`
}{network.HardwareAddr{0xde, 0xad, 0xbe, 0xef}}
err := json.Unmarshal([]byte(c.in), &s)
if (c.err == "") != (err == nil) {
t.Errorf("json.Unmarshal(%q) error = %v, want %v", c.in, err, c.err)
}
assert.Check(t, is.DeepEqual(s.Mac, c.out), "json.Unmarshal(%q)", c.in)
}
}

View File

@@ -43,7 +43,7 @@ type Backend interface {
ActivateContainerServiceBinding(containerName string) error ActivateContainerServiceBinding(containerName string) error
DeactivateContainerServiceBinding(containerName string) error DeactivateContainerServiceBinding(containerName string) error
UpdateContainerServiceConfig(containerName string, serviceConfig *clustertypes.ServiceConfig) error UpdateContainerServiceConfig(containerName string, serviceConfig *clustertypes.ServiceConfig) error
ContainerInspect(ctx context.Context, name string, options backend.ContainerInspectOptions) (_ *container.InspectResponse, desiredMACAddress string, _ error) ContainerInspect(ctx context.Context, name string, options backend.ContainerInspectOptions) (_ *container.InspectResponse, desiredMACAddress network.HardwareAddr, _ error)
ContainerWait(ctx context.Context, name string, condition container.WaitCondition) (<-chan containerpkg.StateStatus, error) ContainerWait(ctx context.Context, name string, condition container.WaitCondition) (<-chan containerpkg.StateStatus, error)
ContainerRm(name string, config *backend.ContainerRmConfig) error ContainerRm(name string, config *backend.ContainerRmConfig) error
ContainerKill(name string, sig string) error ContainerKill(name string, sig string) error

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"net"
"net/netip" "net/netip"
"os" "os"
"runtime" "runtime"
@@ -545,13 +544,6 @@ func validateEndpointSettings(nw *libnetwork.Network, nwName string, epConfig *n
errs = validateIPAMConfigIsInRange(errs, ipamConfig, v4Configs, v6Configs) errs = validateIPAMConfigIsInRange(errs, ipamConfig, v4Configs, v6Configs)
} }
if epConfig.MacAddress != "" {
_, err := net.ParseMAC(epConfig.MacAddress)
if err != nil {
return fmt.Errorf("invalid MAC address %s", epConfig.MacAddress)
}
}
if sysctls, ok := epConfig.DriverOpts[netlabel.EndpointSysctls]; ok { if sysctls, ok := epConfig.DriverOpts[netlabel.EndpointSysctls]; ok {
for _, sysctl := range strings.Split(sysctls, ",") { for _, sysctl := range strings.Split(sysctls, ",") {
scname := strings.SplitN(sysctl, ".", 5) scname := strings.SplitN(sysctl, ".", 5)
@@ -647,7 +639,7 @@ func cleanOperationalData(es *network.EndpointSettings) {
es.IPv6Gateway = netip.Addr{} es.IPv6Gateway = netip.Addr{}
es.GlobalIPv6Address = netip.Addr{} es.GlobalIPv6Address = netip.Addr{}
es.GlobalIPv6PrefixLen = 0 es.GlobalIPv6PrefixLen = 0
es.MacAddress = "" es.MacAddress = nil
if es.IPAMOperational { if es.IPAMOperational {
es.IPAMConfig = nil es.IPAMConfig = nil
} }

View File

@@ -19,10 +19,10 @@ import (
// ContainerInspect returns low-level information about a // ContainerInspect returns low-level information about a
// container. Returns an error if the container cannot be found, or if // container. Returns an error if the container cannot be found, or if
// there is an error getting the data. // there is an error getting the data.
func (daemon *Daemon) ContainerInspect(ctx context.Context, name string, options backend.ContainerInspectOptions) (_ *containertypes.InspectResponse, desiredMACAddress string, _ error) { func (daemon *Daemon) ContainerInspect(ctx context.Context, name string, options backend.ContainerInspectOptions) (_ *containertypes.InspectResponse, desiredMACAddress networktypes.HardwareAddr, _ error) {
ctr, err := daemon.GetContainer(name) ctr, err := daemon.GetContainer(name)
if err != nil { if err != nil {
return nil, "", err return nil, nil, err
} }
ctr.Lock() ctr.Lock()
@@ -30,7 +30,7 @@ func (daemon *Daemon) ContainerInspect(ctx context.Context, name string, options
base, desiredMACAddress, err := daemon.getInspectData(&daemon.config().Config, ctr) base, desiredMACAddress, err := daemon.getInspectData(&daemon.config().Config, ctr)
if err != nil { if err != nil {
ctr.Unlock() ctr.Unlock()
return nil, "", err return nil, nil, err
} }
// TODO(thaJeztah): do we need a deep copy here? Otherwise we could use maps.Clone (see https://github.com/moby/moby/commit/7917a36cc787ada58987320e67cc6d96858f3b55) // TODO(thaJeztah): do we need a deep copy here? Otherwise we could use maps.Clone (see https://github.com/moby/moby/commit/7917a36cc787ada58987320e67cc6d96858f3b55)
@@ -61,7 +61,7 @@ func (daemon *Daemon) ContainerInspect(ctx context.Context, name string, options
if options.Size { if options.Size {
sizeRw, sizeRootFs, err := daemon.imageService.GetContainerLayerSize(ctx, base.ID) sizeRw, sizeRootFs, err := daemon.imageService.GetContainerLayerSize(ctx, base.ID)
if err != nil { if err != nil {
return nil, "", err return nil, nil, err
} }
base.SizeRw = &sizeRw base.SizeRw = &sizeRw
base.SizeRootFs = &sizeRootFs base.SizeRootFs = &sizeRootFs
@@ -83,7 +83,7 @@ func (daemon *Daemon) ContainerInspect(ctx context.Context, name string, options
return base, desiredMACAddress, nil return base, desiredMACAddress, nil
} }
func (daemon *Daemon) getInspectData(daemonCfg *config.Config, ctr *container.Container) (_ *containertypes.InspectResponse, desiredMACAddress string, _ error) { func (daemon *Daemon) getInspectData(daemonCfg *config.Config, ctr *container.Container) (_ *containertypes.InspectResponse, desiredMACAddress networktypes.HardwareAddr, _ error) {
// make a copy to play with // make a copy to play with
hostConfig := *ctr.HostConfig hostConfig := *ctr.HostConfig
@@ -101,7 +101,7 @@ func (daemon *Daemon) getInspectData(daemonCfg *config.Config, ctr *container.Co
// Config.MacAddress field for older API versions (< 1.44). We set it here // Config.MacAddress field for older API versions (< 1.44). We set it here
// unconditionally, to keep backward compatibility with clients that use // unconditionally, to keep backward compatibility with clients that use
// unversioned API endpoints. // unversioned API endpoints.
var macAddress string var macAddress networktypes.HardwareAddr
if ctr.Config != nil { if ctr.Config != nil {
if nwm := hostConfig.NetworkMode; nwm.IsBridge() || nwm.IsUserDefined() { if nwm := hostConfig.NetworkMode; nwm.IsBridge() || nwm.IsUserDefined() {
if epConf, ok := ctr.NetworkSettings.Networks[nwm.NetworkName()]; ok { if epConf, ok := ctr.NetworkSettings.Networks[nwm.NetworkName()]; ok {
@@ -174,7 +174,7 @@ func (daemon *Daemon) getInspectData(daemonCfg *config.Config, ctr *container.Co
if ctr.State.Dead { if ctr.State.Dead {
return inspectResponse, macAddress, nil return inspectResponse, macAddress, nil
} }
return nil, "", errdefs.System(errors.New("RWLayer of container " + ctr.ID + " is unexpectedly nil")) return nil, nil, errdefs.System(errors.New("RWLayer of container " + ctr.ID + " is unexpectedly nil"))
} }
graphDriverData, err := ctr.RWLayer.Metadata() graphDriverData, err := ctr.RWLayer.Metadata()
@@ -184,7 +184,7 @@ func (daemon *Daemon) getInspectData(daemonCfg *config.Config, ctr *container.Co
// have been removed; we can ignore errors. // have been removed; we can ignore errors.
return inspectResponse, macAddress, nil return inspectResponse, macAddress, nil
} }
return nil, "", errdefs.System(err) return nil, nil, errdefs.System(err)
} }
inspectResponse.GraphDriver.Data = graphDriverData inspectResponse.GraphDriver.Data = graphDriverData

View File

@@ -886,7 +886,7 @@ func buildEndpointResource(ep *libnetwork.Endpoint, info libnetwork.EndpointInfo
Name: ep.Name(), Name: ep.Name(),
} }
if iface := info.Iface(); iface != nil { if iface := info.Iface(); iface != nil {
er.MacAddress = iface.MacAddress().String() er.MacAddress = networktypes.HardwareAddr(iface.MacAddress())
er.IPv4Address = netiputil.Unmap(iface.Addr()) er.IPv4Address = netiputil.Unmap(iface.Addr())
er.IPv6Address = iface.AddrIPv6() er.IPv6Address = iface.AddrIPv6()
} }
@@ -955,12 +955,8 @@ func buildCreateEndpointOptions(c *container.Container, n *libnetwork.Network, e
createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(options.Generic{k: v})) createOptions = append(createOptions, libnetwork.EndpointOptionGeneric(options.Generic{k: v}))
} }
if epConfig.DesiredMacAddress != "" { if len(epConfig.DesiredMacAddress) != 0 {
mac, err := net.ParseMAC(epConfig.DesiredMacAddress) genericOptions[netlabel.MacAddress] = net.HardwareAddr(epConfig.DesiredMacAddress)
if err != nil {
return nil, err
}
genericOptions[netlabel.MacAddress] = mac
} }
} }
@@ -1174,8 +1170,8 @@ func buildEndpointInfo(networkSettings *network.Settings, n *libnetwork.Network,
return nil return nil
} }
if iface.MacAddress() != nil { if mac := iface.MacAddress(); mac != nil {
networkSettings.Networks[nwName].MacAddress = iface.MacAddress().String() networkSettings.Networks[nwName].MacAddress = networktypes.HardwareAddr(mac)
} }
if iface.Address() != nil { if iface.Address() != nil {

View File

@@ -28,7 +28,7 @@ type EndpointSettings struct {
IPAMOperational bool IPAMOperational bool
// DesiredMacAddress is the configured value, it's copied from MacAddress (the // DesiredMacAddress is the configured value, it's copied from MacAddress (the
// API param field) when the container is created. // API param field) when the container is created.
DesiredMacAddress string DesiredMacAddress networktypes.HardwareAddr
} }
// AttachmentStore stores the load balancer IP address for a network id. // AttachmentStore stores the load balancer IP address for a network id.

View File

@@ -6,6 +6,7 @@ import (
"github.com/moby/go-archive" "github.com/moby/go-archive"
"github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network"
containerpkg "github.com/moby/moby/v2/daemon/container" containerpkg "github.com/moby/moby/v2/daemon/container"
"github.com/moby/moby/v2/daemon/internal/filters" "github.com/moby/moby/v2/daemon/internal/filters"
"github.com/moby/moby/v2/daemon/server/backend" "github.com/moby/moby/v2/daemon/server/backend"
@@ -48,7 +49,7 @@ type stateBackend interface {
// monitorBackend includes functions to implement to provide containers monitoring functionality. // monitorBackend includes functions to implement to provide containers monitoring functionality.
type monitorBackend interface { type monitorBackend interface {
ContainerChanges(ctx context.Context, name string) ([]archive.Change, error) ContainerChanges(ctx context.Context, name string) ([]archive.Change, error)
ContainerInspect(ctx context.Context, name string, options backend.ContainerInspectOptions) (_ *container.InspectResponse, desiredMACAddress string, _ error) ContainerInspect(ctx context.Context, name string, options backend.ContainerInspectOptions) (_ *container.InspectResponse, desiredMACAddress network.HardwareAddr, _ error)
ContainerLogs(ctx context.Context, name string, config *backend.ContainerLogsOptions) (msgs <-chan *backend.LogMessage, tty bool, err error) ContainerLogs(ctx context.Context, name string, config *backend.ContainerLogsOptions) (msgs <-chan *backend.LogMessage, tty bool, err error)
ContainerStats(ctx context.Context, name string, config *backend.ContainerStatsConfig) error ContainerStats(ctx context.Context, name string, config *backend.ContainerStatsConfig) error
ContainerTop(name string, psArgs string) (*container.TopResponse, error) ContainerTop(name string, psArgs string) (*container.TopResponse, error)

View File

@@ -8,6 +8,7 @@ import (
"io" "io"
"net/http" "net/http"
"runtime" "runtime"
"slices"
"strconv" "strconv"
"strings" "strings"
@@ -672,7 +673,7 @@ func (c *containerRouter) postContainersCreate(ctx context.Context, w http.Respo
// Mac Address of the container. // Mac Address of the container.
// //
// MacAddress field is deprecated since API v1.44. Use EndpointSettings.MacAddress instead. // MacAddress field is deprecated since API v1.44. Use EndpointSettings.MacAddress instead.
MacAddress string `json:",omitempty"` MacAddress network.HardwareAddr `json:",omitempty"`
} }
_ = json.Unmarshal(requestBody.Bytes(), &legacyConfig) _ = json.Unmarshal(requestBody.Bytes(), &legacyConfig)
if warn, err := handleMACAddressBC(hostConfig, networkingConfig, version, legacyConfig.MacAddress); err != nil { if warn, err := handleMACAddressBC(hostConfig, networkingConfig, version, legacyConfig.MacAddress); err != nil {
@@ -745,14 +746,14 @@ func handleVolumeDriverBC(version string, hostConfig *container.HostConfig) (war
// handleMACAddressBC takes care of backward-compatibility for the container-wide MAC address by mutating the // handleMACAddressBC takes care of backward-compatibility for the container-wide MAC address by mutating the
// networkingConfig to set the endpoint-specific MACAddress field introduced in API v1.44. It returns a warning message // networkingConfig to set the endpoint-specific MACAddress field introduced in API v1.44. It returns a warning message
// or an error if the container-wide field was specified for API >= v1.44. // or an error if the container-wide field was specified for API >= v1.44.
func handleMACAddressBC(hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, version string, deprecatedMacAddress string) (string, error) { func handleMACAddressBC(hostConfig *container.HostConfig, networkingConfig *network.NetworkingConfig, version string, deprecatedMacAddress network.HardwareAddr) (string, error) {
// For older versions of the API, migrate the container-wide MAC address to EndpointsConfig. // For older versions of the API, migrate the container-wide MAC address to EndpointsConfig.
if versions.LessThan(version, "1.44") { if versions.LessThan(version, "1.44") {
if deprecatedMacAddress == "" { if len(deprecatedMacAddress) == 0 {
// If a MAC address is supplied in EndpointsConfig, discard it because the old API // If a MAC address is supplied in EndpointsConfig, discard it because the old API
// would have ignored it. // would have ignored it.
for _, ep := range networkingConfig.EndpointsConfig { for _, ep := range networkingConfig.EndpointsConfig {
ep.MacAddress = "" ep.MacAddress = nil
} }
return "", nil return "", nil
} }
@@ -769,7 +770,7 @@ func handleMACAddressBC(hostConfig *container.HostConfig, networkingConfig *netw
} }
// The container-wide MacAddress parameter is deprecated and should now be specified in EndpointsConfig. // The container-wide MacAddress parameter is deprecated and should now be specified in EndpointsConfig.
if deprecatedMacAddress == "" { if len(deprecatedMacAddress) == 0 {
return "", nil return "", nil
} }
@@ -785,9 +786,9 @@ func handleMACAddressBC(hostConfig *container.HostConfig, networkingConfig *netw
} }
// ep is the endpoint that needs the container-wide MAC address; migrate the address // ep is the endpoint that needs the container-wide MAC address; migrate the address
// to it, or bail out if there's a mismatch. // to it, or bail out if there's a mismatch.
if ep.MacAddress == "" { if len(ep.MacAddress) == 0 {
ep.MacAddress = deprecatedMacAddress ep.MacAddress = deprecatedMacAddress
} else if ep.MacAddress != deprecatedMacAddress { } else if !slices.Equal(ep.MacAddress, deprecatedMacAddress) {
return "", errdefs.InvalidParameter(errors.New("the container-wide MAC address must match the endpoint-specific MAC address for the main network, or be left empty")) return "", errdefs.InvalidParameter(errors.New("the container-wide MAC address must match the endpoint-specific MAC address for the main network, or be left empty"))
} }
} }

View File

@@ -4,6 +4,7 @@ import (
"strings" "strings"
"testing" "testing"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/moby/moby/api/types/container" "github.com/moby/moby/api/types/container"
"github.com/moby/moby/api/types/network" "github.com/moby/moby/api/types/network"
"github.com/moby/moby/v2/daemon/libnetwork/netlabel" "github.com/moby/moby/v2/daemon/libnetwork/netlabel"
@@ -15,56 +16,56 @@ func TestHandleMACAddressBC(t *testing.T) {
testcases := []struct { testcases := []struct {
name string name string
apiVersion string apiVersion string
ctrWideMAC string ctrWideMAC network.HardwareAddr
networkMode container.NetworkMode networkMode container.NetworkMode
epConfig map[string]*network.EndpointSettings epConfig map[string]*network.EndpointSettings
expEpWithCtrWideMAC string expEpWithCtrWideMAC string
expEpWithNoMAC string expEpWithNoMAC string
expCtrWideMAC string expCtrWideMAC network.HardwareAddr
expWarning string expWarning string
expError string expError string
}{ }{
{ {
name: "old api ctr-wide mac mix id and name", name: "old api ctr-wide mac mix id and name",
apiVersion: "1.43", apiVersion: "1.43",
ctrWideMAC: "11:22:33:44:55:66", ctrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
networkMode: "aNetId", networkMode: "aNetId",
epConfig: map[string]*network.EndpointSettings{"aNetName": {}}, epConfig: map[string]*network.EndpointSettings{"aNetName": {}},
expEpWithCtrWideMAC: "aNetName", expEpWithCtrWideMAC: "aNetName",
expCtrWideMAC: "11:22:33:44:55:66", expCtrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
}, },
{ {
name: "old api clear ep mac", name: "old api clear ep mac",
apiVersion: "1.43", apiVersion: "1.43",
networkMode: "aNetId", networkMode: "aNetId",
epConfig: map[string]*network.EndpointSettings{"aNetName": {MacAddress: "11:22:33:44:55:66"}}, epConfig: map[string]*network.EndpointSettings{"aNetName": {MacAddress: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}}},
expEpWithNoMAC: "aNetName", expEpWithNoMAC: "aNetName",
}, },
{ {
name: "old api no-network ctr-wide mac", name: "old api no-network ctr-wide mac",
apiVersion: "1.43", apiVersion: "1.43",
networkMode: "none", networkMode: "none",
ctrWideMAC: "11:22:33:44:55:66", ctrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
expError: "conflicting options: mac-address and the network mode", expError: "conflicting options: mac-address and the network mode",
expCtrWideMAC: "11:22:33:44:55:66", expCtrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
}, },
{ {
name: "old api create ep", name: "old api create ep",
apiVersion: "1.43", apiVersion: "1.43",
networkMode: "aNetId", networkMode: "aNetId",
ctrWideMAC: "11:22:33:44:55:66", ctrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
epConfig: map[string]*network.EndpointSettings{}, epConfig: map[string]*network.EndpointSettings{},
expEpWithCtrWideMAC: "aNetId", expEpWithCtrWideMAC: "aNetId",
expCtrWideMAC: "11:22:33:44:55:66", expCtrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
}, },
{ {
name: "old api migrate ctr-wide mac", name: "old api migrate ctr-wide mac",
apiVersion: "1.43", apiVersion: "1.43",
ctrWideMAC: "11:22:33:44:55:66", ctrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
networkMode: "aNetName", networkMode: "aNetName",
epConfig: map[string]*network.EndpointSettings{"aNetName": {}}, epConfig: map[string]*network.EndpointSettings{"aNetName": {}},
expEpWithCtrWideMAC: "aNetName", expEpWithCtrWideMAC: "aNetName",
expCtrWideMAC: "11:22:33:44:55:66", expCtrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
}, },
{ {
name: "new api no macs", name: "new api no macs",
@@ -76,45 +77,45 @@ func TestHandleMACAddressBC(t *testing.T) {
name: "new api ep specific mac", name: "new api ep specific mac",
apiVersion: "1.44", apiVersion: "1.44",
networkMode: "aNetName", networkMode: "aNetName",
epConfig: map[string]*network.EndpointSettings{"aNetName": {MacAddress: "11:22:33:44:55:66"}}, epConfig: map[string]*network.EndpointSettings{"aNetName": {MacAddress: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66}}},
}, },
{ {
name: "new api migrate ctr-wide mac to new ep", name: "new api migrate ctr-wide mac to new ep",
apiVersion: "1.44", apiVersion: "1.44",
ctrWideMAC: "11:22:33:44:55:66", ctrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
networkMode: "aNetName", networkMode: "aNetName",
epConfig: map[string]*network.EndpointSettings{}, epConfig: map[string]*network.EndpointSettings{},
expEpWithCtrWideMAC: "aNetName", expEpWithCtrWideMAC: "aNetName",
expWarning: "The container-wide MacAddress field is now deprecated", expWarning: "The container-wide MacAddress field is now deprecated",
expCtrWideMAC: "", expCtrWideMAC: nil,
}, },
{ {
name: "new api migrate ctr-wide mac to existing ep", name: "new api migrate ctr-wide mac to existing ep",
apiVersion: "1.44", apiVersion: "1.44",
ctrWideMAC: "11:22:33:44:55:66", ctrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
networkMode: "aNetName", networkMode: "aNetName",
epConfig: map[string]*network.EndpointSettings{"aNetName": {}}, epConfig: map[string]*network.EndpointSettings{"aNetName": {}},
expEpWithCtrWideMAC: "aNetName", expEpWithCtrWideMAC: "aNetName",
expWarning: "The container-wide MacAddress field is now deprecated", expWarning: "The container-wide MacAddress field is now deprecated",
expCtrWideMAC: "", expCtrWideMAC: nil,
}, },
{ {
name: "new api mode vs name mismatch", name: "new api mode vs name mismatch",
apiVersion: "1.44", apiVersion: "1.44",
ctrWideMAC: "11:22:33:44:55:66", ctrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
networkMode: "aNetId", networkMode: "aNetId",
epConfig: map[string]*network.EndpointSettings{"aNetName": {}}, epConfig: map[string]*network.EndpointSettings{"aNetName": {}},
expError: "unable to migrate container-wide MAC address to a specific network: HostConfig.NetworkMode must match the identity of a network in NetworkSettings.Networks", expError: "unable to migrate container-wide MAC address to a specific network: HostConfig.NetworkMode must match the identity of a network in NetworkSettings.Networks",
expCtrWideMAC: "11:22:33:44:55:66", expCtrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
}, },
{ {
name: "new api mac mismatch", name: "new api mac mismatch",
apiVersion: "1.44", apiVersion: "1.44",
ctrWideMAC: "11:22:33:44:55:66", ctrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
networkMode: "aNetName", networkMode: "aNetName",
epConfig: map[string]*network.EndpointSettings{"aNetName": {MacAddress: "00:11:22:33:44:55"}}, epConfig: map[string]*network.EndpointSettings{"aNetName": {MacAddress: network.HardwareAddr{0x00, 0x11, 0x22, 0x33, 0x44, 0x55}}},
expError: "the container-wide MAC address must match the endpoint-specific MAC address", expError: "the container-wide MAC address must match the endpoint-specific MAC address",
expCtrWideMAC: "11:22:33:44:55:66", expCtrWideMAC: network.HardwareAddr{0x11, 0x22, 0x33, 0x44, 0x55, 0x66},
}, },
} }
@@ -146,11 +147,11 @@ func TestHandleMACAddressBC(t *testing.T) {
} }
if tc.expEpWithCtrWideMAC != "" { if tc.expEpWithCtrWideMAC != "" {
got := netCfg.EndpointsConfig[tc.expEpWithCtrWideMAC].MacAddress got := netCfg.EndpointsConfig[tc.expEpWithCtrWideMAC].MacAddress
assert.Check(t, is.Equal(got, tc.ctrWideMAC)) assert.Check(t, is.DeepEqual(got, tc.ctrWideMAC, cmpopts.EquateEmpty()))
} }
if tc.expEpWithNoMAC != "" { if tc.expEpWithNoMAC != "" {
got := netCfg.EndpointsConfig[tc.expEpWithNoMAC].MacAddress got := netCfg.EndpointsConfig[tc.expEpWithNoMAC].MacAddress
assert.Check(t, is.Equal(got, "")) assert.Check(t, is.DeepEqual(got, network.HardwareAddr{}, cmpopts.EquateEmpty()))
} }
}) })
} }

View File

@@ -63,7 +63,7 @@ func (c *containerRouter) getContainersByName(ctx context.Context, w http.Respon
// //
// This was deprecated in API v1.44, but kept in place until // This was deprecated in API v1.44, but kept in place until
// API v1.52, which removed this entirely. // API v1.52, which removed this entirely.
if desiredMACAddress != "" { if len(desiredMACAddress) != 0 {
wrapOpts = append(wrapOpts, compat.WithExtraFields(map[string]any{ wrapOpts = append(wrapOpts, compat.WithExtraFields(map[string]any{
"Config": map[string]any{ "Config": map[string]any{
"MacAddress": desiredMACAddress, "MacAddress": desiredMACAddress,

View File

@@ -16,6 +16,7 @@ import (
"github.com/docker/go-units" "github.com/docker/go-units"
"github.com/moby/moby/api/pkg/stdcopy" "github.com/moby/moby/api/pkg/stdcopy"
containertypes "github.com/moby/moby/api/types/container" containertypes "github.com/moby/moby/api/types/container"
networktypes "github.com/moby/moby/api/types/network"
"github.com/moby/moby/client" "github.com/moby/moby/client"
"github.com/moby/moby/v2/integration/internal/container" "github.com/moby/moby/v2/integration/internal/container"
net "github.com/moby/moby/v2/integration/internal/network" net "github.com/moby/moby/v2/integration/internal/network"
@@ -295,7 +296,7 @@ func TestMacAddressIsAppliedToMainNetworkWithShortID(t *testing.T) {
c := container.Inspect(ctx, t, apiClient, cid) c := container.Inspect(ctx, t, apiClient, cid)
assert.Assert(t, c.NetworkSettings.Networks["testnet"] != nil) assert.Assert(t, c.NetworkSettings.Networks["testnet"] != nil)
assert.Equal(t, c.NetworkSettings.Networks["testnet"].MacAddress, "02:42:08:26:a9:55") assert.DeepEqual(t, c.NetworkSettings.Networks["testnet"].MacAddress, networktypes.HardwareAddr{0x02, 0x42, 0x08, 0x26, 0xa9, 0x55})
} }
func TestStaticIPOutsideSubpool(t *testing.T) { func TestStaticIPOutsideSubpool(t *testing.T) {

View File

@@ -2,6 +2,7 @@ package container
import ( import (
"maps" "maps"
"net"
"net/netip" "net/netip"
"slices" "slices"
"strings" "strings"
@@ -151,6 +152,10 @@ func WithTmpfs(targetAndOpts string) func(config *TestContainerConfig) {
} }
func WithMacAddress(networkName, mac string) func(config *TestContainerConfig) { func WithMacAddress(networkName, mac string) func(config *TestContainerConfig) {
maddr, err := net.ParseMAC(mac)
if err != nil {
panic(err)
}
return func(c *TestContainerConfig) { return func(c *TestContainerConfig) {
if c.NetworkingConfig.EndpointsConfig == nil { if c.NetworkingConfig.EndpointsConfig == nil {
c.NetworkingConfig.EndpointsConfig = map[string]*network.EndpointSettings{} c.NetworkingConfig.EndpointsConfig = map[string]*network.EndpointSettings{}
@@ -158,7 +163,7 @@ func WithMacAddress(networkName, mac string) func(config *TestContainerConfig) {
if v, ok := c.NetworkingConfig.EndpointsConfig[networkName]; !ok || v == nil { if v, ok := c.NetworkingConfig.EndpointsConfig[networkName]; !ok || v == nil {
c.NetworkingConfig.EndpointsConfig[networkName] = &network.EndpointSettings{} c.NetworkingConfig.EndpointsConfig[networkName] = &network.EndpointSettings{}
} }
c.NetworkingConfig.EndpointsConfig[networkName].MacAddress = mac c.NetworkingConfig.EndpointsConfig[networkName].MacAddress = network.HardwareAddr(maddr)
} }
} }

View File

@@ -1816,7 +1816,7 @@ func TestAdvertiseAddresses(t *testing.T) {
// The original defer will stop ctr2Id. // The original defer will stop ctr2Id.
ctr2NewMAC := container.Inspect(ctx, t, c, ctr2Id).NetworkSettings.Networks[netName].MacAddress ctr2NewMAC := container.Inspect(ctx, t, c, ctr2Id).NetworkSettings.Networks[netName].MacAddress
assert.Check(t, ctr2OrigMAC != ctr2NewMAC, "expected restarted ctr2 to have a different MAC address") assert.Check(t, !slices.Equal(ctr2OrigMAC, ctr2NewMAC), "expected restarted ctr2 to have a different MAC address")
ctr1Neighs = container.ExecT(ctx, t, c, ctr1Id, []string{"ip", "neigh", "show"}) ctr1Neighs = container.ExecT(ctx, t, c, ctr1Id, []string{"ip", "neigh", "show"})
assert.Assert(t, is.Equal(ctr1Neighs.ExitCode, 0)) assert.Assert(t, is.Equal(ctr1Neighs.ExitCode, 0))
@@ -1843,9 +1843,6 @@ func TestAdvertiseAddresses(t *testing.T) {
// Check ARP/NA messages received for ctr2's new address (all unsolicited). // Check ARP/NA messages received for ctr2's new address (all unsolicited).
ctr2NewHwAddr, err := net.ParseMAC(ctr2NewMAC)
assert.NilError(t, err)
checkPkts := func(pktDesc string, pkts []network.TimestampedPkt, matchIP netip.Addr, unpack func(pkt network.TimestampedPkt) (sh net.HardwareAddr, sp netip.Addr, err error)) { checkPkts := func(pktDesc string, pkts []network.TimestampedPkt, matchIP netip.Addr, unpack func(pkt network.TimestampedPkt) (sh net.HardwareAddr, sp netip.Addr, err error)) {
t.Helper() t.Helper()
var count int var count int
@@ -1860,7 +1857,7 @@ func TestAdvertiseAddresses(t *testing.T) {
continue continue
} }
t.Logf("%s %d: %s '%s' is at '%s'", pktDesc, i+1, p.ReceivedAt.Format("15:04:05.000"), pa, ha) t.Logf("%s %d: %s '%s' is at '%s'", pktDesc, i+1, p.ReceivedAt.Format("15:04:05.000"), pa, ha)
if pa != matchIP || slices.Compare(ha, ctr2NewHwAddr) != 0 { if pa != matchIP || slices.Compare(ha, net.HardwareAddr(ctr2NewMAC)) != 0 {
continue continue
} }
count++ count++

View File

@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/netip" "net/netip"
"slices"
"testing" "testing"
containertypes "github.com/moby/moby/api/types/container" containertypes "github.com/moby/moby/api/types/container"
@@ -82,7 +83,7 @@ func TestMACAddrOnRestart(t *testing.T) {
ctr2Inspect := container.Inspect(ctx, t, c, ctr2Name) ctr2Inspect := container.Inspect(ctx, t, c, ctr2Name)
ctr2MAC := ctr2Inspect.NetworkSettings.Networks[netName].MacAddress ctr2MAC := ctr2Inspect.NetworkSettings.Networks[netName].MacAddress
assert.Check(t, ctr1MAC != ctr2MAC, assert.Check(t, !slices.Equal(ctr1MAC, ctr2MAC),
"expected containers to have different MAC addresses; got %q for both", ctr1MAC) "expected containers to have different MAC addresses; got %q for both", ctr1MAC)
} }
@@ -120,7 +121,7 @@ func TestCfgdMACAddrOnRestart(t *testing.T) {
inspect := container.Inspect(ctx, t, c, ctr1Name) inspect := container.Inspect(ctx, t, c, ctr1Name)
gotMAC := inspect.NetworkSettings.Networks[netName].MacAddress gotMAC := inspect.NetworkSettings.Networks[netName].MacAddress
assert.Check(t, is.Equal(wantMAC, gotMAC)) assert.Check(t, is.Equal(wantMAC, gotMAC.String()))
startAndCheck := func() { startAndCheck := func() {
t.Helper() t.Helper()
@@ -128,7 +129,7 @@ func TestCfgdMACAddrOnRestart(t *testing.T) {
assert.Assert(t, is.Nil(err)) assert.Assert(t, is.Nil(err))
inspect = container.Inspect(ctx, t, c, ctr1Name) inspect = container.Inspect(ctx, t, c, ctr1Name)
gotMAC = inspect.NetworkSettings.Networks[netName].MacAddress gotMAC = inspect.NetworkSettings.Networks[netName].MacAddress
assert.Check(t, is.Equal(wantMAC, gotMAC)) assert.Check(t, is.Equal(wantMAC, gotMAC.String()))
} }
// Restart the container, check that the MAC address is restored. // Restart the container, check that the MAC address is restored.
@@ -301,7 +302,7 @@ func TestWatchtowerCreate(t *testing.T) {
inspect := container.Inspect(ctx, t, c, ctrName) inspect := container.Inspect(ctx, t, c, ctrName)
netSettings := inspect.NetworkSettings.Networks[netName] netSettings := inspect.NetworkSettings.Networks[netName]
assert.Check(t, is.Equal(netSettings.IPAddress, netip.MustParseAddr(ctrIP))) assert.Check(t, is.Equal(netSettings.IPAddress, netip.MustParseAddr(ctrIP)))
assert.Check(t, is.Equal(netSettings.MacAddress, ctrMAC)) assert.Check(t, is.Equal(netSettings.MacAddress.String(), ctrMAC))
} }
type legacyCreateRequest struct { type legacyCreateRequest struct {

View File

@@ -30,7 +30,7 @@ type EndpointSettings struct {
// MacAddress may be used to specify a MAC address when the container is created. // MacAddress may be used to specify a MAC address when the container is created.
// Once the container is running, it becomes operational data (it may contain a // Once the container is running, it becomes operational data (it may contain a
// generated address). // generated address).
MacAddress string MacAddress HardwareAddr
IPPrefixLen int IPPrefixLen int
IPv6Gateway netip.Addr IPv6Gateway netip.Addr
GlobalIPv6Address netip.Addr GlobalIPv6Address netip.Addr

View File

@@ -24,7 +24,7 @@ type EndpointResource struct {
// mac address // mac address
// Example: 02:42:ac:13:00:02 // Example: 02:42:ac:13:00:02
MacAddress string `json:"MacAddress"` MacAddress HardwareAddr `json:"MacAddress"`
// IPv4 address // IPv4 address
// Example: 172.19.0.2/16 // Example: 172.19.0.2/16

View File

@@ -0,0 +1,37 @@
package network
import (
"encoding"
"fmt"
"net"
)
// A HardwareAddr represents a physical hardware address.
// It implements [encoding.TextMarshaler] and [encoding.TextUnmarshaler]
// in the absence of go.dev/issue/29678.
type HardwareAddr net.HardwareAddr
var _ encoding.TextMarshaler = (HardwareAddr)(nil)
var _ encoding.TextUnmarshaler = (*HardwareAddr)(nil)
var _ fmt.Stringer = (HardwareAddr)(nil)
func (m *HardwareAddr) UnmarshalText(text []byte) error {
if len(text) == 0 {
*m = nil
return nil
}
hw, err := net.ParseMAC(string(text))
if err != nil {
return err
}
*m = HardwareAddr(hw)
return nil
}
func (m HardwareAddr) MarshalText() ([]byte, error) {
return []byte(net.HardwareAddr(m).String()), nil
}
func (m HardwareAddr) String() string {
return net.HardwareAddr(m).String()
}