Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ require (
github.com/spf13/cobra v1.6.1
github.com/thediveo/enumflag/v2 v2.0.1
go.uber.org/mock v0.4.0
golang.org/x/sys v0.45.0
golang.org/x/term v0.43.0
howett.net/plist v1.0.0
howett.net/ranger v0.0.0-20171016084633-e2e137620847
Expand Down Expand Up @@ -69,7 +70,6 @@ require (
golang.org/x/mod v0.35.0 // indirect
golang.org/x/net v0.55.0 // indirect
golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.45.0 // indirect
golang.org/x/text v0.37.0 // indirect
golang.org/x/tools v0.44.0 // indirect
google.golang.org/protobuf v1.36.11 // indirect
Expand Down
66 changes: 66 additions & 0 deletions pkg/util/machine/mac_address.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package machine

import (
"bytes"
"errors"
"fmt"
"net"
)

const (
macPriorityPrimary = iota
macPriorityPhysical
macPriorityFallback
)

type macAddressCandidate struct {
address net.HardwareAddr
priority int
}

func usableMacAddress(address net.HardwareAddr) bool {
return len(address) == 6 && address[0]&1 == 0 &&
!bytes.Equal(address, []byte{0, 0, 0, 0, 0, 0}) &&
!bytes.Equal(address, []byte{0x02, 0, 0, 0, 0, 0})
}

func selectMacAddress(interfaces []net.Interface, lookup func(net.Interface) (macAddressCandidate, error)) (string, error) {
var (
selected macAddressCandidate
lookupErr error
)

for _, networkInterface := range interfaces {
if networkInterface.Flags&(net.FlagLoopback|net.FlagPointToPoint) != 0 {
continue
}

candidate, err := lookup(networkInterface)
if err != nil {
lookupErr = errors.Join(lookupErr, err)

continue
}

if !usableMacAddress(candidate.address) {
continue
}

// Do not use enumeration order, interface index or link state as a
// tie-breaker: those can change without changing the machine.
if selected.address == nil || candidate.priority < selected.priority ||
(candidate.priority == selected.priority && bytes.Compare(candidate.address, selected.address) < 0) {
selected = candidate
}
}

if selected.address != nil {
return selected.address.String(), nil
}

if lookupErr != nil {
return "", fmt.Errorf("could not find network interfaces with a valid mac address: %w", lookupErr)
}

return "", errors.New("could not find network interfaces with a valid mac address")
}
32 changes: 17 additions & 15 deletions pkg/util/machine/mac_address_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
package machine

import (
"bytes"
"errors"
"fmt"
"net"
Expand All @@ -12,29 +11,32 @@ import (
"github.com/ebitengine/purego"
)

func interfaceMacAddress(networkInterface net.Interface) (string, error) {
func interfaceMacAddress(networkInterface net.Interface) (macAddressCandidate, error) {
return macAddressWithHardwareLookup(networkInterface, hardwareMacAddress)
}

func macAddressWithHardwareLookup(networkInterface net.Interface, lookup func(string) (net.HardwareAddr, error)) (string, error) {
// macOS 27 can redact network interface addresses with a shared placeholder.
// Resolve the same interface through IOKit to preserve its existing identity.
if !bytes.Equal(networkInterface.HardwareAddr, []byte{0x02, 0, 0, 0, 0, 0}) {
return networkInterface.HardwareAddr.String(), nil
func macAddressWithHardwareLookup(networkInterface net.Interface, lookup func(string) (net.HardwareAddr, error)) (macAddressCandidate, error) {
// IOKit supplies the hardware address even when the current network address
// is randomized or redacted. Prefer en0, then other hardware interfaces.
address, err := lookup(networkInterface.Name)
if err == nil && usableMacAddress(address) {
priority := macPriorityPhysical
if networkInterface.Name == "en0" {
priority = macPriorityPrimary
}

return macAddressCandidate{address: address, priority: priority}, nil
}

address, err := lookup(networkInterface.Name)
if err != nil {
return "", fmt.Errorf("macOS redacted the mac address for %q; failed to read its hardware address: %w", networkInterface.Name, err)
if usableMacAddress(networkInterface.HardwareAddr) {
return macAddressCandidate{address: networkInterface.HardwareAddr, priority: macPriorityFallback}, nil
}

if len(address) != 6 || address[0]&1 != 0 ||
bytes.Equal(address, []byte{0, 0, 0, 0, 0, 0}) ||
bytes.Equal(address, []byte{0x02, 0, 0, 0, 0, 0}) {
return "", fmt.Errorf("macOS redacted the mac address for %q; IOKit returned no usable hardware address", networkInterface.Name)
if err != nil {
return macAddressCandidate{}, fmt.Errorf("failed to read hardware address for %q: %w", networkInterface.Name, err)
}

return address.String(), nil
return macAddressCandidate{}, fmt.Errorf("IOKit returned no usable hardware address for %q", networkInterface.Name)
}

type macAddressAPI struct {
Expand Down
32 changes: 23 additions & 9 deletions pkg/util/machine/mac_address_darwin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,16 @@ var _ = Describe("macOS hardware address", func() {
}
})

It("preserves an existing unredacted identity without consulting IOKit", func() {
It("falls back to an unredacted address when IOKit is unavailable", func() {
networkInterface.HardwareAddr = net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}

address, err := macAddressWithHardwareLookup(networkInterface, func(string) (net.HardwareAddr, error) {
Fail("unexpected IOKit lookup")

return nil, nil
return nil, errors.New("IOKit unavailable")
})

Expect(err).ToNot(HaveOccurred())
Expect(address).To(Equal("aa:bb:cc:dd:ee:ff"))
Expect(address.address.String()).To(Equal("aa:bb:cc:dd:ee:ff"))
Expect(address.priority).To(Equal(macPriorityFallback))
})

It("recovers the hardware address of the selected interface when redacted", func() {
Expand All @@ -42,7 +41,22 @@ var _ = Describe("macOS hardware address", func() {
})

Expect(err).ToNot(HaveOccurred())
Expect(address).To(Equal("aa:bb:cc:dd:ee:ff"))
Expect(address.address.String()).To(Equal("aa:bb:cc:dd:ee:ff"))
Expect(address.priority).To(Equal(macPriorityPhysical))
})

It("prefers en0's hardware address over its randomized network address", func() {
networkInterface.Name = "en0"
networkInterface.HardwareAddr = net.HardwareAddr{0x02, 1, 2, 3, 4, 5}
address, err := macAddressWithHardwareLookup(networkInterface, func(name string) (net.HardwareAddr, error) {
Expect(name).To(Equal("en0"))

return net.HardwareAddr{0x00, 1, 2, 3, 4, 5}, nil
})

Expect(err).NotTo(HaveOccurred())
Expect(address.address.String()).To(Equal("00:01:02:03:04:05"))
Expect(address.priority).To(Equal(macPriorityPrimary))
})

It("reports unavailable hardware information without returning the placeholder", func() {
Expand All @@ -51,17 +65,17 @@ var _ = Describe("macOS hardware address", func() {
return nil, lookupErr
})

Expect(address).To(BeEmpty())
Expect(address.address).To(BeEmpty())
Expect(errors.Is(err, lookupErr)).To(BeTrue())
Expect(err.Error()).To(ContainSubstring("macOS redacted the mac address for \"en4\""))
Expect(err.Error()).To(ContainSubstring("failed to read hardware address for \"en4\""))
})

DescribeTable("rejects an unusable IOKit address", func(hardware net.HardwareAddr) {
address, err := macAddressWithHardwareLookup(networkInterface, func(string) (net.HardwareAddr, error) {
return hardware, nil
})

Expect(address).To(BeEmpty())
Expect(address.address).To(BeEmpty())
Expect(err).To(MatchError(ContainSubstring("IOKit returned no usable hardware address")))
},
Entry("missing", nil),
Expand Down
6 changes: 3 additions & 3 deletions pkg/util/machine/mac_address_default.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
//go:build !darwin || ios
//go:build (!darwin && !linux && !windows) || ios

package machine

import "net"

func interfaceMacAddress(networkInterface net.Interface) (string, error) {
return networkInterface.HardwareAddr.String(), nil
func interfaceMacAddress(networkInterface net.Interface) (macAddressCandidate, error) {
return macAddressCandidate{address: networkInterface.HardwareAddr, priority: macPriorityFallback}, nil
}
22 changes: 22 additions & 0 deletions pkg/util/machine/mac_address_linux.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package machine

import (
"net"
"os"
"path/filepath"
)

func interfaceMacAddress(networkInterface net.Interface) (macAddressCandidate, error) {
return macAddressWithSysfs(networkInterface, "/sys/class/net"), nil
}

func macAddressWithSysfs(networkInterface net.Interface, root string) macAddressCandidate {
candidate := macAddressCandidate{address: networkInterface.HardwareAddr, priority: macPriorityFallback}
// Physical adapters have a backing device in sysfs. Keep a fallback for
// containers and systems where sysfs is unavailable.
if _, err := os.Stat(filepath.Join(root, networkInterface.Name, "device")); err == nil {
candidate.priority = macPriorityPhysical
}

return candidate
}
24 changes: 24 additions & 0 deletions pkg/util/machine/mac_address_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package machine

import (
"net"
"os"
"path/filepath"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

var _ = Describe("Linux hardware address", func() {
It("prefers a sysfs backing device and preserves virtual adapters as fallbacks", func() {
root := GinkgoT().TempDir()
Expect(os.MkdirAll(filepath.Join(root, "eth0", "device"), 0o755)).To(Succeed())
Expect(os.MkdirAll(filepath.Join(root, "veth0"), 0o755)).To(Succeed())
for name, priority := range map[string]int{"eth0": macPriorityPhysical, "veth0": macPriorityFallback, "missing": macPriorityFallback} {
networkInterface := net.Interface{Name: name, HardwareAddr: net.HardwareAddr{2, 1, 2, 3, 4, 5}}
candidate := macAddressWithSysfs(networkInterface, root)
Expect(candidate.priority).To(Equal(priority))
Expect(candidate.address).To(Equal(networkInterface.HardwareAddr))
}
})
})
Loading
Loading