12

Is it possible to get the MAC address using Swift?

The MAC address being the primary address for the Wi-Fi or Airport.

I'm trying to make a OS X application.

pkamb
  • 33,281
  • 23
  • 160
  • 191
jimbob
  • 461
  • 2
  • 8
  • 17
  • Well I tried to look inside the api for Swift to see if there was any mention for getting the mac address. But I've only seen NSHost.currentHost().address and that returns something that isn't the mac address. I've also seen a way to do it in IOS but that is using UIDevice which is not related to OS X applications. – jimbob Aug 05 '15 at 14:32
  • There is C code for it, but I don't know if swift can integrate C. – jimbob Aug 05 '15 at 14:34
  • You mean Objective-C, C and Objective-C is different languages – Huang Chen Aug 05 '15 at 14:36
  • @HuangChen, that's not really accurate to say they are different. [Objective-C is a superset of the C language](http://stackoverflow.com/questions/19366134/what-does-objective-c-is-a-superset-of-c-more-strictly-than-c-mean-exactly) – TheDarkKnight Aug 05 '15 at 14:44
  • Is there no way to get the MAC address using swift? – jimbob Aug 05 '15 at 14:52
  • @jimbob There is. Apple provides a C example to do it and Swift is perfectly C interoperable so you can reproduce the code in Swift. – Fantattitude Aug 05 '15 at 15:19
  • How would I go about converting C to Swift. https://developer.apple.com/library/mac/samplecode/GetPrimaryMACAddress/Introduction/Intro.html – jimbob Aug 05 '15 at 15:40
  • I thought Objective C was convertible not C. – jimbob Aug 05 '15 at 15:41
  • I've got a snippet of Swift that gets the MAC address, but it uses NSTask so it *may* not be acceptable for the App Store, and it parses a string to get the result so it's hacky and subject to future errors in case of changes anyway. Knowing these limitations, if you're still interested, I'll post it as an answer. – Eric Aya Aug 05 '15 at 16:00
  • @Eric D. That would be great! I'm not going to be posting this to the App store. – jimbob Aug 05 '15 at 16:08

6 Answers6

16

Apple's sample code from https://developer.apple.com/library/mac/samplecode/GetPrimaryMACAddress/Introduction/Intro.html to retrieve the Ethernet MAC address can be translated to Swift. I have preserved only the most important comments, more explanations can be found in the original code.

// Returns an iterator containing the primary (built-in) Ethernet interface. The caller is responsible for
// releasing the iterator after the caller is done with it.
func FindEthernetInterfaces() -> io_iterator_t? {

    let matchingDictUM = IOServiceMatching("IOEthernetInterface");
    // Note that another option here would be:
    // matchingDict = IOBSDMatching("en0");
    // but en0: isn't necessarily the primary interface, especially on systems with multiple Ethernet ports.

    if matchingDictUM == nil {
        return nil
    }
    let matchingDict = matchingDictUM.takeUnretainedValue() as NSMutableDictionary
    matchingDict["IOPropertyMatch"] = [ "IOPrimaryInterface" : true]

    var matchingServices : io_iterator_t = 0
    if IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &matchingServices) != KERN_SUCCESS {
        return nil
    }

    return matchingServices
}

// Given an iterator across a set of Ethernet interfaces, return the MAC address of the last one.
// If no interfaces are found the MAC address is set to an empty string.
// In this sample the iterator should contain just the primary interface.
func GetMACAddress(intfIterator : io_iterator_t) -> [UInt8]? {

    var macAddress : [UInt8]?

    var intfService = IOIteratorNext(intfIterator)
    while intfService != 0 {

        var controllerService : io_object_t = 0
        if IORegistryEntryGetParentEntry(intfService, "IOService", &controllerService) == KERN_SUCCESS {

            let dataUM = IORegistryEntryCreateCFProperty(controllerService, "IOMACAddress", kCFAllocatorDefault, 0)
            if dataUM != nil {
                let data = dataUM.takeRetainedValue() as! NSData
                macAddress = [0, 0, 0, 0, 0, 0]
                data.getBytes(&macAddress!, length: macAddress!.count)
            }
            IOObjectRelease(controllerService)
        }

        IOObjectRelease(intfService)
        intfService = IOIteratorNext(intfIterator)
    }

    return macAddress
}


if let intfIterator = FindEthernetInterfaces() {
    if let macAddress = GetMACAddress(intfIterator) {
        let macAddressAsString = ":".join(macAddress.map( { String(format:"%02x", $0) } ))
        println(macAddressAsString)
    }

    IOObjectRelease(intfIterator)
}

The only "tricky" part is how to work with Unmanaged objects, those have the suffix UM in my code.

Instead of returning an error code, the functions return an optional value which is nil if the function failed.


Update for Swift 3:

func FindEthernetInterfaces() -> io_iterator_t? {

    let matchingDict = IOServiceMatching("IOEthernetInterface") as NSMutableDictionary
    matchingDict["IOPropertyMatch"] = [ "IOPrimaryInterface" : true]

    var matchingServices : io_iterator_t = 0
    if IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &matchingServices) != KERN_SUCCESS {
        return nil
    }

    return matchingServices
}

func GetMACAddress(_ intfIterator : io_iterator_t) -> [UInt8]? {

    var macAddress : [UInt8]?

    var intfService = IOIteratorNext(intfIterator)
    while intfService != 0 {

        var controllerService : io_object_t = 0
        if IORegistryEntryGetParentEntry(intfService, "IOService", &controllerService) == KERN_SUCCESS {

            let dataUM = IORegistryEntryCreateCFProperty(controllerService, "IOMACAddress" as CFString, kCFAllocatorDefault, 0)
            if let data = dataUM?.takeRetainedValue() as? NSData {
                macAddress = [0, 0, 0, 0, 0, 0]
                data.getBytes(&macAddress!, length: macAddress!.count)
            }
            IOObjectRelease(controllerService)
        }

        IOObjectRelease(intfService)
        intfService = IOIteratorNext(intfIterator)
    }

    return macAddress
}

if let intfIterator = FindEthernetInterfaces() {
    if let macAddress = GetMACAddress(intfIterator) {
        let macAddressAsString = macAddress.map( { String(format:"%02x", $0) } )
            .joined(separator: ":")
        print(macAddressAsString)
    }

    IOObjectRelease(intfIterator)
}
Martin R
  • 529,903
  • 94
  • 1,240
  • 1,382
5

Different approach via if_msghdr

func MACAddressForBSD(bsd : String) -> String?
{
    let MAC_ADDRESS_LENGTH = 6
    let separator = ":"

    var length : size_t = 0
    var buffer : [CChar]

    let bsdIndex = Int32(if_nametoindex(bsd))
    if bsdIndex == 0 {
        print("Error: could not find index for bsd name \(bsd)")
        return nil
    }
    let bsdData = Data(bsd.utf8)
    var managementInfoBase = [CTL_NET, AF_ROUTE, 0, AF_LINK, NET_RT_IFLIST, bsdIndex]

    if sysctl(&managementInfoBase, 6, nil, &length, nil, 0) < 0 {
        print("Error: could not determine length of info data structure");
        return nil;
    }

    buffer = [CChar](unsafeUninitializedCapacity: length, initializingWith: {buffer, initializedCount in
        for x in 0..<length { buffer[x] = 0 }
        initializedCount = length
    })

    if sysctl(&managementInfoBase, 6, &buffer, &length, nil, 0) < 0 {
        print("Error: could not read info data structure");
        return nil;
    }

    let infoData = Data(bytes: buffer, count: length)
    let indexAfterMsghdr = MemoryLayout<if_msghdr>.stride + 1
    let rangeOfToken = infoData[indexAfterMsghdr...].range(of: bsdData)!
    let lower = rangeOfToken.upperBound
    let upper = lower + MAC_ADDRESS_LENGTH
    let macAddressData = infoData[lower..<upper]
    let addressBytes = macAddressData.map{ String(format:"%02x", $0) }
    return addressBytes.joined(separator: separator)
}

MACAddressForBSD(bsd: "en0")
vadian
  • 274,689
  • 30
  • 353
  • 361
5

Update for Swift 4.2

func FindEthernetInterfaces() -> io_iterator_t? {

    let matchingDictUM = IOServiceMatching("IOEthernetInterface");
    // Note that another option here would be:
    // matchingDict = IOBSDMatching("en0");
    // but en0: isn't necessarily the primary interface, especially on systems with multiple Ethernet ports.

    if matchingDictUM == nil {
        return nil
    }

    let matchingDict = matchingDictUM! as NSMutableDictionary
    matchingDict["IOPropertyMatch"] = [ "IOPrimaryInterface" : true]

    var matchingServices : io_iterator_t = 0
    if IOServiceGetMatchingServices(kIOMasterPortDefault, matchingDict, &matchingServices) != KERN_SUCCESS {
        return nil
    }

    return matchingServices
}

// Given an iterator across a set of Ethernet interfaces, return the MAC address of the last one.
// If no interfaces are found the MAC address is set to an empty string.
// In this sample the iterator should contain just the primary interface.
func GetMACAddress(_ intfIterator : io_iterator_t) -> [UInt8]? {

    var macAddress : [UInt8]?

    var intfService = IOIteratorNext(intfIterator)
    while intfService != 0 {

        var controllerService : io_object_t = 0
        if IORegistryEntryGetParentEntry(intfService, kIOServicePlane, &controllerService) == KERN_SUCCESS {

            let dataUM = IORegistryEntryCreateCFProperty(controllerService, "IOMACAddress" as CFString, kCFAllocatorDefault, 0)
            if dataUM != nil {
                let data = (dataUM!.takeRetainedValue() as! CFData) as Data
                macAddress = [0, 0, 0, 0, 0, 0]
                data.copyBytes(to: &macAddress!, count: macAddress!.count)
            }
            IOObjectRelease(controllerService)
        }

        IOObjectRelease(intfService)
        intfService = IOIteratorNext(intfIterator)
    }

    return macAddress
}


func getMacAddress() -> String? {
    var macAddressAsString : String?
    if let intfIterator = FindEthernetInterfaces() {
        if let macAddress = GetMACAddress(intfIterator) {
            macAddressAsString = macAddress.map( { String(format:"%02x", $0) } ).joined(separator: ":")
            print(macAddressAsString!)
        }

        IOObjectRelease(intfIterator)
    }
    return macAddressAsString
}
Marek H
  • 5,173
  • 3
  • 31
  • 42
4

Update to Martin R's entry. There are a couple of lines that won't compile with Swift 2.1.

Change:

let matchingDict = matchingDictUM.takeUnretainedValue() as NSMutableDictionary

To:

let matchingDict = matchingDictUM as NSMutableDictionary

Change:

let macAddressAsString = ":".join(macAddress.map( { String(format:"%02x", $0) } ))

To:

let macAddressAsString = macAddress.map( { String(format:"%02x", $0) } ).joinWithSeparator(":")
Alex M
  • 527
  • 3
  • 13
3

You can also use 'SystemConfiguration' framework

import SystemConfiguration

func collectMACAddresses() -> [String] {
    guard let interfaces = SCNetworkInterfaceCopyAll() as? [SCNetworkInterface] else {
        return []
    }
    
    return interfaces
        .map(SCNetworkInterfaceGetHardwareAddressString)
        .compactMap { $0 as String? }
}
0

DISCLAIMER: this is not production-ready. It would probably be rejected by the App Store. It's also subject to errors if the output of ifconfig changes in the future. I've made this because I lacked the skills to translate the C code given in the links. It does not replace a full Swift solution. That being said, it works...

Get ifconfig's output and parse it to get the MAC address associated with an interface (en0 in this example):

let theTask = NSTask()
let taskOutput = NSPipe()
theTask.launchPath = "/sbin/ifconfig"
theTask.standardOutput = taskOutput
theTask.standardError = taskOutput
theTask.arguments = ["en0"]

theTask.launch()
theTask.waitUntilExit()

let taskData = taskOutput.fileHandleForReading.readDataToEndOfFile()

if let stringResult = NSString(data: taskData, encoding: NSUTF8StringEncoding) {
    if stringResult != "ifconfig: interface en0 does not exist" {
        let f = stringResult.rangeOfString("ether")
        if f.location != NSNotFound {
            let sub = stringResult.substringFromIndex(f.location + f.length)
            let range = Range(start: advance(sub.startIndex, 1), end: advance(sub.startIndex, 18))
            let result = sub.substringWithRange(range)
            println(result)
        }
    }
}
Eric Aya
  • 69,473
  • 35
  • 181
  • 253
  • 1
    Thanks so much! This is much better than the work around I did which was running this bash command: networksetup -listallhardwareports | grep -A 2 \"Hardware Port: Air\" | grep \"Ethernet Address\" | cut -d \":\" -f2- – jimbob Aug 05 '15 at 16:23