From f6eb4511ee3fc3f4ea20c3a72035eba6bc904939 Mon Sep 17 00:00:00 2001 From: Majd Alfhaily Date: Thu, 1 Oct 2026 20:26:19 +0200 Subject: [PATCH] fix: select a stable machine identity from network adapters --- go.mod | 2 +- pkg/util/machine/mac_address.go | 66 +++++++++ pkg/util/machine/mac_address_darwin.go | 32 +++-- pkg/util/machine/mac_address_darwin_test.go | 32 +++-- pkg/util/machine/mac_address_default.go | 6 +- pkg/util/machine/mac_address_linux.go | 22 +++ pkg/util/machine/mac_address_linux_test.go | 24 ++++ pkg/util/machine/mac_address_test.go | 142 +++++++++++++++++++ pkg/util/machine/mac_address_windows.go | 55 +++++++ pkg/util/machine/mac_address_windows_test.go | 57 ++++++++ pkg/util/machine/machine.go | 37 ++--- pkg/util/machine/machine_test.go | 2 +- 12 files changed, 431 insertions(+), 46 deletions(-) create mode 100644 pkg/util/machine/mac_address.go create mode 100644 pkg/util/machine/mac_address_linux.go create mode 100644 pkg/util/machine/mac_address_linux_test.go create mode 100644 pkg/util/machine/mac_address_test.go create mode 100644 pkg/util/machine/mac_address_windows.go create mode 100644 pkg/util/machine/mac_address_windows_test.go diff --git a/go.mod b/go.mod index 68d82d20..29862f44 100644 --- a/go.mod +++ b/go.mod @@ -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 @@ -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 diff --git a/pkg/util/machine/mac_address.go b/pkg/util/machine/mac_address.go new file mode 100644 index 00000000..b0dd2f41 --- /dev/null +++ b/pkg/util/machine/mac_address.go @@ -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") +} diff --git a/pkg/util/machine/mac_address_darwin.go b/pkg/util/machine/mac_address_darwin.go index e802addd..06588e81 100644 --- a/pkg/util/machine/mac_address_darwin.go +++ b/pkg/util/machine/mac_address_darwin.go @@ -3,7 +3,6 @@ package machine import ( - "bytes" "errors" "fmt" "net" @@ -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 { diff --git a/pkg/util/machine/mac_address_darwin_test.go b/pkg/util/machine/mac_address_darwin_test.go index 054f04ce..f2418651 100644 --- a/pkg/util/machine/mac_address_darwin_test.go +++ b/pkg/util/machine/mac_address_darwin_test.go @@ -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() { @@ -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() { @@ -51,9 +65,9 @@ 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) { @@ -61,7 +75,7 @@ var _ = Describe("macOS hardware address", func() { 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), diff --git a/pkg/util/machine/mac_address_default.go b/pkg/util/machine/mac_address_default.go index 1a5b1bf8..39276ba4 100644 --- a/pkg/util/machine/mac_address_default.go +++ b/pkg/util/machine/mac_address_default.go @@ -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 } diff --git a/pkg/util/machine/mac_address_linux.go b/pkg/util/machine/mac_address_linux.go new file mode 100644 index 00000000..fcce755a --- /dev/null +++ b/pkg/util/machine/mac_address_linux.go @@ -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 +} diff --git a/pkg/util/machine/mac_address_linux_test.go b/pkg/util/machine/mac_address_linux_test.go new file mode 100644 index 00000000..ea3f9eea --- /dev/null +++ b/pkg/util/machine/mac_address_linux_test.go @@ -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)) + } + }) +}) diff --git a/pkg/util/machine/mac_address_test.go b/pkg/util/machine/mac_address_test.go new file mode 100644 index 00000000..24213759 --- /dev/null +++ b/pkg/util/machine/mac_address_test.go @@ -0,0 +1,142 @@ +package machine + +import ( + "errors" + "net" + "slices" + "sync" + "sync/atomic" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Machine address selection", func() { + DescribeTable("validates hardware addresses", func(address net.HardwareAddr, valid bool) { + Expect(usableMacAddress(address)).To(Equal(valid)) + }, + Entry("missing", nil, false), + Entry("short", net.HardwareAddr{0, 1}, false), + Entry("EUI-64", net.HardwareAddr{0, 1, 2, 3, 4, 5, 6, 7}, false), + Entry("zero", net.HardwareAddr{0, 0, 0, 0, 0, 0}, false), + Entry("redacted", net.HardwareAddr{2, 0, 0, 0, 0, 0}, false), + Entry("broadcast", net.HardwareAddr{255, 255, 255, 255, 255, 255}, false), + Entry("multicast", net.HardwareAddr{1, 0, 0x5e, 0, 0, 1}, false), + Entry("unicast", net.HardwareAddr{0, 1, 2, 3, 4, 5}, true), + Entry("locally administered", net.HardwareAddr{2, 1, 2, 3, 4, 5}, true), + ) + + It("prefers physical adapters regardless of enumeration order, index or link state", func() { + interfaces := []net.Interface{ + {Name: "virtual", Index: 1, Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 1}}, + {Name: "wired", Index: 20, HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 3}}, + {Name: "wireless", Index: 10, Flags: net.FlagUp, HardwareAddr: net.HardwareAddr{0, 0, 0, 0, 0, 2}}, + } + lookup := func(networkInterface net.Interface) (macAddressCandidate, error) { + priority := macPriorityPhysical + if networkInterface.Name == "virtual" { + priority = macPriorityFallback + } + + return macAddressCandidate{address: networkInterface.HardwareAddr, priority: priority}, nil + } + for range 3 { + for _, reversed := range []bool{false, true} { + input := slices.Clone(interfaces) + if reversed { + slices.Reverse(input) + } + address, err := selectMacAddress(input, lookup) + Expect(err).NotTo(HaveOccurred()) + Expect(address).To(Equal("00:00:00:00:00:02")) + } + interfaces = append(interfaces[1:], interfaces[0]) + for i := range interfaces { + interfaces[i].Index += 100 + interfaces[i].Flags ^= net.FlagUp + } + } + }) + + It("prefers a primary hardware interface over other physical adapters", func() { + interfaces := []net.Interface{{Name: "other"}, {Name: "primary"}} + address, err := selectMacAddress(interfaces, func(networkInterface net.Interface) (macAddressCandidate, error) { + if networkInterface.Name == "primary" { + return macAddressCandidate{address: net.HardwareAddr{0, 0, 0, 0, 0, 2}, priority: macPriorityPrimary}, nil + } + + return macAddressCandidate{address: net.HardwareAddr{0, 0, 0, 0, 0, 1}, priority: macPriorityPhysical}, nil + }) + Expect(err).NotTo(HaveOccurred()) + Expect(address).To(Equal("00:00:00:00:00:02")) + }) + + It("skips loopback, tunnels, failed lookups and unusable addresses before using a fallback", func() { + interfaces := []net.Interface{ + {Name: "loopback", Flags: net.FlagLoopback}, + {Name: "tunnel", Flags: net.FlagPointToPoint}, + {Name: "unavailable"}, + {Name: "redacted", HardwareAddr: net.HardwareAddr{2, 0, 0, 0, 0, 0}}, + {Name: "virtual", HardwareAddr: net.HardwareAddr{2, 1, 2, 3, 4, 5}}, + } + address, err := selectMacAddress(interfaces, func(networkInterface net.Interface) (macAddressCandidate, error) { + Expect(networkInterface.Flags & (net.FlagLoopback | net.FlagPointToPoint)).To(BeZero()) + if networkInterface.Name == "unavailable" { + return macAddressCandidate{}, errors.New("hardware lookup failed") + } + + return macAddressCandidate{address: networkInterface.HardwareAddr, priority: macPriorityFallback}, nil + }) + Expect(err).NotTo(HaveOccurred()) + Expect(address).To(Equal("02:01:02:03:04:05")) + }) + + It("reports missing usable addresses without wrapping a nil error", func() { + address, err := selectMacAddress(nil, nil) + Expect(address).To(BeEmpty()) + Expect(err).To(MatchError("could not find network interfaces with a valid mac address")) + }) + + It("retains lookup failures if no usable adapter remains", func() { + lookupErr := errors.New("hardware unavailable") + address, err := selectMacAddress([]net.Interface{{Name: "en0"}}, func(net.Interface) (macAddressCandidate, error) { + return macAddressCandidate{}, lookupErr + }) + Expect(address).To(BeEmpty()) + Expect(errors.Is(err, lookupErr)).To(BeTrue()) + }) + + It("keeps one identity across concurrent calls and subsequent interface changes", func() { + var calls atomic.Int32 + sut := &machine{interfaces: func() ([]net.Interface, error) { + last := byte(calls.Add(1)) + + return []net.Interface{{Index: 2147483647, Name: "ipatool-test", HardwareAddr: net.HardwareAddr{2, 1, 2, 3, 4, last}}}, nil + }} + var wg sync.WaitGroup + for range 8 { + wg.Add(1) + go func() { + defer wg.Done() + defer GinkgoRecover() + address, err := sut.MacAddress() + Expect(err).NotTo(HaveOccurred()) + Expect(address).To(Equal("02:01:02:03:04:01")) + }() + } + wg.Wait() + address, err := sut.MacAddress() + Expect(err).NotTo(HaveOccurred()) + Expect(address).To(Equal("02:01:02:03:04:01")) + Expect(calls.Load()).To(Equal(int32(1))) + }) + + It("preserves interface enumeration errors", func() { + lookupErr := errors.New("interfaces unavailable") + sut := &machine{interfaces: func() ([]net.Interface, error) { return nil, lookupErr }} + address, err := sut.MacAddress() + Expect(address).To(BeEmpty()) + Expect(errors.Is(err, lookupErr)).To(BeTrue()) + Expect(err.Error()).To(HavePrefix("failed to get network interfaces:")) + }) +}) diff --git a/pkg/util/machine/mac_address_windows.go b/pkg/util/machine/mac_address_windows.go new file mode 100644 index 00000000..20c7e425 --- /dev/null +++ b/pkg/util/machine/mac_address_windows.go @@ -0,0 +1,55 @@ +package machine + +import ( + "net" + + "golang.org/x/sys/windows" +) + +var getIfEntry2Ex = windows.NewLazySystemDLL("iphlpapi.dll").NewProc("GetIfEntry2Ex") + +func interfaceMacAddress(networkInterface net.Interface) (macAddressCandidate, error) { + // Older Windows 10 builds lack this entry point. Metadata is optional; + // preserve the validated network address as a fallback on those systems. + fallback := macAddressCandidate{address: networkInterface.HardwareAddr, priority: macPriorityFallback} + if err := getIfEntry2Ex.Find(); err != nil { + return fallback, nil + } + + row := windows.MibIfRow2{InterfaceIndex: uint32(networkInterface.Index)} + if err := windows.GetIfEntry2Ex(windows.MibIfTableNormalWithoutStatistics, &row); err != nil { + return fallback, nil + } + + return macAddressFromWindowsRow(row), nil +} + +func macAddressFromWindowsRow(row windows.MibIfRow2) macAddressCandidate { + candidate := macAddressCandidate{priority: macPriorityFallback} + if row.PhysicalAddressLength != 6 { + return candidate + } + + candidate.address = append(net.HardwareAddr(nil), row.PhysicalAddress[:6]...) + // Use the permanent address so Wi-Fi randomization does not change the + // identity. Virtual adapters remain available as a lower-priority fallback. + if usableMacAddress(row.PermanentPhysicalAddress[:6]) { + candidate.address = append(net.HardwareAddr(nil), row.PermanentPhysicalAddress[:6]...) + } + + // MIB_IF_ROW2's first two status bits are HardwareInterface and + // FilterInterface. Bluetooth PAN can otherwise look like Ethernet. + const ( + hardwareInterface = 1 + filterInterface = 2 + bluetoothMedium = 10 + ) + + if row.InterfaceAndOperStatusFlags&(hardwareInterface|filterInterface) == hardwareInterface && + row.PhysicalMediumType != bluetoothMedium && + (row.Type == windows.IF_TYPE_ETHERNET_CSMACD || row.Type == windows.IF_TYPE_IEEE80211) { + candidate.priority = macPriorityPhysical + } + + return candidate +} diff --git a/pkg/util/machine/mac_address_windows_test.go b/pkg/util/machine/mac_address_windows_test.go new file mode 100644 index 00000000..eac6d3f5 --- /dev/null +++ b/pkg/util/machine/mac_address_windows_test.go @@ -0,0 +1,57 @@ +package machine + +import ( + "net" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "golang.org/x/sys/windows" +) + +var _ = Describe("Windows hardware address", func() { + var row windows.MibIfRow2 + + BeforeEach(func() { + row = windows.MibIfRow2{ + PhysicalAddressLength: 6, + Type: windows.IF_TYPE_ETHERNET_CSMACD, + InterfaceAndOperStatusFlags: 1, + } + copy(row.PhysicalAddress[:], []byte{2, 1, 2, 3, 4, 5}) + copy(row.PermanentPhysicalAddress[:], []byte{0, 1, 2, 3, 4, 5}) + }) + + It("uses a physical adapter's permanent address regardless of connection state", func() { + for _, state := range []uint32{windows.IfOperStatusUp, windows.IfOperStatusDown} { + row.OperStatus = state + candidate := macAddressFromWindowsRow(row) + Expect(candidate.address.String()).To(Equal("00:01:02:03:04:05")) + Expect(candidate.priority).To(Equal(macPriorityPhysical)) + } + }) + + It("uses the current address when the permanent address is unavailable", func() { + clear(row.PermanentPhysicalAddress[:]) + candidate := macAddressFromWindowsRow(row) + Expect(candidate.address).To(Equal(net.HardwareAddr{2, 1, 2, 3, 4, 5})) + Expect(candidate.priority).To(Equal(macPriorityPhysical)) + }) + + DescribeTable("classifies adapters", func(flags uint8, interfaceType, medium uint32, priority int) { + row.InterfaceAndOperStatusFlags = flags + row.Type = interfaceType + row.PhysicalMediumType = medium + Expect(macAddressFromWindowsRow(row).priority).To(Equal(priority)) + }, + Entry("Wi-Fi hardware", uint8(1), uint32(windows.IF_TYPE_IEEE80211), uint32(9), macPriorityPhysical), + Entry("virtual Ethernet", uint8(0), uint32(windows.IF_TYPE_ETHERNET_CSMACD), uint32(0), macPriorityFallback), + Entry("filter", uint8(3), uint32(windows.IF_TYPE_ETHERNET_CSMACD), uint32(0), macPriorityFallback), + Entry("Bluetooth PAN", uint8(1), uint32(windows.IF_TYPE_ETHERNET_CSMACD), uint32(10), macPriorityFallback), + Entry("tunnel", uint8(1), uint32(windows.IF_TYPE_TUNNEL), uint32(0), macPriorityFallback), + ) + + DescribeTable("rejects non-Ethernet address lengths without slicing past the buffer", func(length uint32) { + row.PhysicalAddressLength = length + Expect(macAddressFromWindowsRow(row).address).To(BeEmpty()) + }, Entry("empty", uint32(0)), Entry("EUI-64", uint32(8)), Entry("oversized", uint32(100))) +}) diff --git a/pkg/util/machine/machine.go b/pkg/util/machine/machine.go index 14796c46..92ef9046 100644 --- a/pkg/util/machine/machine.go +++ b/pkg/util/machine/machine.go @@ -5,6 +5,7 @@ import ( "net" "path/filepath" "runtime" + "sync" "github.com/majd/ipatool/v2/pkg/util/operatingsystem" "golang.org/x/term" @@ -18,7 +19,11 @@ type Machine interface { } type machine struct { - os operatingsystem.OperatingSystem + os operatingsystem.OperatingSystem + interfaces func() ([]net.Interface, error) + macAddressOnce sync.Once + macAddress string + macAddressErr error } type Args struct { @@ -27,28 +32,26 @@ type Args struct { func New(args Args) Machine { return &machine{ - os: args.OS, + os: args.OS, + interfaces: net.Interfaces, } } -func (*machine) MacAddress() (string, error) { - interfaces, err := net.Interfaces() - if err != nil { - return "", fmt.Errorf("failed to get network interfaces: %w", err) - } - - if len(interfaces) == 0 { - return "", fmt.Errorf("could not find network interfaces: %w", err) - } +// Resolve once so bag requests, authentication and downloads use the same +// identity even if an adapter is added or removed during this invocation. +func (m *machine) MacAddress() (string, error) { + m.macAddressOnce.Do(func() { + interfaces, err := m.interfaces() + if err != nil { + m.macAddressErr = fmt.Errorf("failed to get network interfaces: %w", err) - for _, netInterface := range interfaces { - addr := netInterface.HardwareAddr.String() - if addr != "" { - return interfaceMacAddress(netInterface) + return } - } - return "", fmt.Errorf("could not find network interfaces with a valid mac address: %w", err) + m.macAddress, m.macAddressErr = selectMacAddress(interfaces, interfaceMacAddress) + }) + + return m.macAddress, m.macAddressErr } func (m *machine) HomeDirectory() string { diff --git a/pkg/util/machine/machine_test.go b/pkg/util/machine/machine_test.go index 3d1a11d0..e8fe4424 100644 --- a/pkg/util/machine/machine_test.go +++ b/pkg/util/machine/machine_test.go @@ -53,7 +53,7 @@ var _ = Describe("Machine", func() { }) When("machine has network interfaces", func() { - It("returns MAC address of the first interface", func() { + It("returns a usable MAC address", func() { res, err := machine.MacAddress() Expect(err).ToNot(HaveOccurred()) Expect(res).To(ContainSubstring(":"))