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
127 changes: 127 additions & 0 deletions pkg/util/machine/mac_address_darwin.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
//go:build darwin && !ios

package machine

import (
"bytes"
"errors"
"fmt"
"net"
"unsafe"

"github.com/ebitengine/purego"
)

func interfaceMacAddress(networkInterface net.Interface) (string, 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
}

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 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)
}

return address.String(), nil
}

type macAddressAPI struct {
matching func(uint32, uint32, string) uintptr
service func(uint32, uintptr) uint32
searchProperty func(uint32, string, uintptr, uintptr, uint32) uintptr
releaseObject func(uint32) int32
createString func(uintptr, string, uint32) uintptr
release func(uintptr)
typeID func(uintptr) uintptr
dataTypeID func() uintptr
dataLength func(uintptr) int64
dataBytes func(uintptr) unsafe.Pointer
}

func hardwareMacAddress(name string) (net.HardwareAddr, error) {
iokit, err := purego.Dlopen("/System/Library/Frameworks/IOKit.framework/IOKit", purego.RTLD_NOW|purego.RTLD_LOCAL)
if err != nil {
return nil, fmt.Errorf("load IOKit: %w", err)
}

defer func() { _ = purego.Dlclose(iokit) }()

coreFoundation, err := purego.Dlopen("/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", purego.RTLD_NOW|purego.RTLD_LOCAL)
if err != nil {
return nil, fmt.Errorf("load CoreFoundation: %w", err)
}

defer func() { _ = purego.Dlclose(coreFoundation) }()

var api macAddressAPI

purego.RegisterLibFunc(&api.matching, iokit, "IOBSDNameMatching")
purego.RegisterLibFunc(&api.service, iokit, "IOServiceGetMatchingService")
purego.RegisterLibFunc(&api.searchProperty, iokit, "IORegistryEntrySearchCFProperty")
purego.RegisterLibFunc(&api.releaseObject, iokit, "IOObjectRelease")
purego.RegisterLibFunc(&api.createString, coreFoundation, "CFStringCreateWithCString")
purego.RegisterLibFunc(&api.release, coreFoundation, "CFRelease")
purego.RegisterLibFunc(&api.typeID, coreFoundation, "CFGetTypeID")
purego.RegisterLibFunc(&api.dataTypeID, coreFoundation, "CFDataGetTypeID")
purego.RegisterLibFunc(&api.dataLength, coreFoundation, "CFDataGetLength")
purego.RegisterLibFunc(&api.dataBytes, coreFoundation, "CFDataGetBytePtr")

return readHardwareMacAddress(name, api)
}

func readHardwareMacAddress(name string, api macAddressAPI) (net.HardwareAddr, error) {
matching := api.matching(0, 0, name)
if matching == 0 {
return nil, errors.New("could not create interface matching dictionary")
}

// IOServiceGetMatchingService consumes the matching dictionary.
service := api.service(0, matching)
if service == 0 {
return nil, errors.New("network interface was not found in IOKit")
}
defer api.releaseObject(service)

const utf8Encoding = 0x08000100

key := api.createString(0, "IOMACAddress", utf8Encoding)
if key == 0 {
return nil, errors.New("could not create hardware address property key")
}
defer api.release(key)

// Apple's receipt-validation guidance reads IOMACAddress from the interface
// or its parents in the IOService plane.
// https://developer.apple.com/documentation/appstorereceipts/validating-receipts-on-the-device
const iterateRecursivelyAndParents = 0x01 | 0x02

property := api.searchProperty(service, "IOService", key, 0, iterateRecursivelyAndParents)
if property == 0 {
return nil, errors.New("hardware address property was not found in IOKit")
}
defer api.release(property)

if api.typeID(property) != api.dataTypeID() || api.dataLength(property) != 6 {
return nil, errors.New("IOKit hardware address must contain six bytes")
}

data := api.dataBytes(property)
if data == nil {
return nil, errors.New("IOKit hardware address data is missing")
}

// Copy the bytes before releasing the CoreFoundation property.
return append(net.HardwareAddr(nil), unsafe.Slice((*byte)(data), 6)...), nil
}
237 changes: 237 additions & 0 deletions pkg/util/machine/mac_address_darwin_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,237 @@
//go:build darwin && !ios

package machine

import (
"errors"
"net"
"unsafe"

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

var _ = Describe("macOS hardware address", func() {
var networkInterface net.Interface

BeforeEach(func() {
networkInterface = net.Interface{
Name: "en4",
HardwareAddr: net.HardwareAddr{0x02, 0, 0, 0, 0, 0},
}
})

It("preserves an existing unredacted identity without consulting IOKit", 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
})

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

It("recovers the hardware address of the selected interface when redacted", func() {
address, err := macAddressWithHardwareLookup(networkInterface, func(name string) (net.HardwareAddr, error) {
Expect(name).To(Equal("en4"))

return net.HardwareAddr{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}, nil
})

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

It("reports unavailable hardware information without returning the placeholder", func() {
lookupErr := errors.New("interface unavailable")
address, err := macAddressWithHardwareLookup(networkInterface, func(string) (net.HardwareAddr, error) {
return nil, lookupErr
})

Expect(address).To(BeEmpty())
Expect(errors.Is(err, lookupErr)).To(BeTrue())
Expect(err.Error()).To(ContainSubstring("macOS redacted the mac 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(err).To(MatchError(ContainSubstring("IOKit returned no usable hardware address")))
},
Entry("missing", nil),
Entry("wrong length", net.HardwareAddr{0xaa, 0xbb}),
Entry("zero", net.HardwareAddr{0, 0, 0, 0, 0, 0}),
Entry("redacted", net.HardwareAddr{0x02, 0, 0, 0, 0, 0}),
Entry("broadcast", net.HardwareAddr{0xff, 0xff, 0xff, 0xff, 0xff, 0xff}),
Entry("multicast", net.HardwareAddr{0x01, 0, 0x5e, 0, 0, 1}),
)
})

var _ = Describe("IOKit hardware address lookup", func() {
const (
dictionary = uintptr(1)
service = uint32(2)
key = uintptr(3)
property = uintptr(4)
dataType = uintptr(5)
)

var (
api macAddressAPI
data []byte
released []uintptr
releasedObjects []uint32
)

BeforeEach(func() {
data = []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff}
released = nil
releasedObjects = nil
api = macAddressAPI{
matching: func(port, options uint32, name string) uintptr {
Expect(port).To(BeZero())
Expect(options).To(BeZero())
Expect(name).To(Equal("en4"))

return dictionary
},
service: func(port uint32, matching uintptr) uint32 {
Expect(port).To(BeZero())
Expect(matching).To(Equal(dictionary))

return service
},
searchProperty: func(object uint32, plane string, propertyKey, allocator uintptr, options uint32) uintptr {
Expect(object).To(Equal(service))
Expect(plane).To(Equal("IOService"))
Expect(propertyKey).To(Equal(key))
Expect(allocator).To(BeZero())
Expect(options).To(Equal(uint32(3)))

return property
},
releaseObject: func(object uint32) int32 {
releasedObjects = append(releasedObjects, object)

return 0
},
createString: func(allocator uintptr, value string, encoding uint32) uintptr {
Expect(allocator).To(BeZero())
Expect(value).To(Equal("IOMACAddress"))
Expect(encoding).To(Equal(uint32(0x08000100)))

return key
},
release: func(object uintptr) {
released = append(released, object)
if object == property {
clear(data)
}
},
typeID: func(object uintptr) uintptr {
Expect(object).To(Equal(property))

return dataType
},
dataTypeID: func() uintptr { return dataType },
dataLength: func(object uintptr) int64 {
Expect(object).To(Equal(property))

return int64(len(data))
},
dataBytes: func(object uintptr) unsafe.Pointer {
Expect(object).To(Equal(property))

return unsafe.Pointer(&data[0])
},
}
})

It("copies the address before releasing the property, key, and interface", func() {
address, err := readHardwareMacAddress("en4", api)

Expect(err).ToNot(HaveOccurred())
Expect(address.String()).To(Equal("aa:bb:cc:dd:ee:ff"))
Expect(released).To(Equal([]uintptr{property, key}))
Expect(releasedObjects).To(Equal([]uint32{service}))
})

It("handles a missing matching dictionary", func() {
api.matching = func(uint32, uint32, string) uintptr { return 0 }

_, err := readHardwareMacAddress("en4", api)

Expect(err).To(MatchError("could not create interface matching dictionary"))
Expect(released).To(BeEmpty())
Expect(releasedObjects).To(BeEmpty())
})

It("handles a missing interface without releasing the consumed dictionary twice", func() {
api.service = func(uint32, uintptr) uint32 { return 0 }

_, err := readHardwareMacAddress("en4", api)

Expect(err).To(MatchError("network interface was not found in IOKit"))
Expect(released).To(BeEmpty())
Expect(releasedObjects).To(BeEmpty())
})

It("releases the interface when the property key cannot be created", func() {
api.createString = func(uintptr, string, uint32) uintptr { return 0 }

_, err := readHardwareMacAddress("en4", api)

Expect(err).To(MatchError("could not create hardware address property key"))
Expect(released).To(BeEmpty())
Expect(releasedObjects).To(Equal([]uint32{service}))
})

It("releases the interface and key when the property is missing", func() {
api.searchProperty = func(uint32, string, uintptr, uintptr, uint32) uintptr { return 0 }

_, err := readHardwareMacAddress("en4", api)

Expect(err).To(MatchError("hardware address property was not found in IOKit"))
Expect(released).To(Equal([]uintptr{key}))
Expect(releasedObjects).To(Equal([]uint32{service}))
})

It("rejects a non-data property without accessing its bytes", func() {
api.typeID = func(uintptr) uintptr { return dataType + 1 }
api.dataLength = nil
api.dataBytes = nil

_, err := readHardwareMacAddress("en4", api)

Expect(err).To(MatchError("IOKit hardware address must contain six bytes"))
Expect(released).To(Equal([]uintptr{property, key}))
Expect(releasedObjects).To(Equal([]uint32{service}))
})

It("rejects an unexpected length without accessing the bytes", func() {
api.dataLength = func(uintptr) int64 { return 8 }
api.dataBytes = nil

_, err := readHardwareMacAddress("en4", api)

Expect(err).To(MatchError("IOKit hardware address must contain six bytes"))
Expect(released).To(Equal([]uintptr{property, key}))
Expect(releasedObjects).To(Equal([]uint32{service}))
})

It("handles missing data without dereferencing a null pointer", func() {
api.dataBytes = func(uintptr) unsafe.Pointer { return nil }

_, err := readHardwareMacAddress("en4", api)

Expect(err).To(MatchError("IOKit hardware address data is missing"))
Expect(released).To(Equal([]uintptr{property, key}))
Expect(releasedObjects).To(Equal([]uint32{service}))
})
})
9 changes: 9 additions & 0 deletions pkg/util/machine/mac_address_default.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
//go:build !darwin || ios

package machine

import "net"

func interfaceMacAddress(networkInterface net.Interface) (string, error) {
return networkInterface.HardwareAddr.String(), nil
}
2 changes: 1 addition & 1 deletion pkg/util/machine/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ func (*machine) MacAddress() (string, error) {
for _, netInterface := range interfaces {
addr := netInterface.HardwareAddr.String()
if addr != "" {
return addr, nil
return interfaceMacAddress(netInterface)
}
}

Expand Down
4 changes: 4 additions & 0 deletions pkg/util/machine/machine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ var _ = Describe("Machine", func() {
res, err := machine.MacAddress()
Expect(err).ToNot(HaveOccurred())
Expect(res).To(ContainSubstring(":"))

if runtime.GOOS == "darwin" {
Expect(res).ToNot(Equal("02:00:00:00:00:00"))
}
})
})

Expand Down
Loading