Getting NWInterface for NWEthernetChannel without Internet Connectivity

The "advances in networking" talks for WWDC had this example of using NWEthernetChannel to monitor a custom protocol.


import Foundation

Import Network

let path = NWPathMonitor(requiredInterfaceType: .wiredEthernet).currentPath

guard let interface = path.availableInterfaces.first else {

fatalError("not connected to Internet")

}

let channel = NWEthernetChannel(on: interface, etherType: 0xB26E)


For my application I need to use a NWEthernetChannel to monitor a custom protocol* on an Ethernet link which does not have IP Internet connectivity (but it does have physical link to a switch). NWPath appears to only give me a NWInterface struct if it is a valid path to the Internet.


How can I get a list of NWInterface Structs on the Mac without having a valid Internet path?


In my particular use case I'm only interested in .wiredEthernet.


Darrell


* I'm trying to use Network.framework to receive LLDP (link-layer discovery protocol) and CDP (Cisco discovery protocol) traffic when I'm plugged into an ethernet switch, which may or may not have Internet connectivity.

Accepted Reply

This is an interesting edge case. I had a chat with the Network framework team about this and there is no direct way to do what you want. This kinda makes sense given Network framework’s original focus on transport protocols, but it’s clearly no longer sufficient. We would appreciate you filing a bug about this, explaining what you’re doing and where you got stuck. Please post your bug number, just for the record.

In the meantime, they suggested a workaround that seems viable. On Apple platforms all interfaces get a link-local IPv6 address. If you create an UDP connection (

NWConnection
) to that address, the resulting path contain’s the interface object you need.

Pasted in below is some code that shows this in action. I tested it as follows:

  1. I started with a MacBook running 10.15.2.

  2. I connected the Wi-Fi interface to the wider Internet.

  3. I attached a Thunderbolt Ethernet dongle. This became

    en7
    .
  4. I connected the Ethernet cable to the Ethernet port on another Mac. The other Mac isn’t relevant here, I just needed an active port that would bring up the link but not route any traffic.

  5. I ran the program below. As you see, the UDP connection’s path update handler is called with an interface object for

    en7
    . You should be able to use that for your
    NWEthernetChannel
    .

Please try this out and let me know how you get along.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
import Foundation
import Network

func firstV6AddrForInterface(name: String) -> String? {
    var addrList: UnsafeMutablePointer<ifaddrs>? = nil
    guard
        getifaddrs(&addrList) == 0,
        let first = addrList
    else { return nil }
    defer { freeifaddrs(addrList) }
    return sequence(first: first, next: { $0.pointee.ifa_next })
        .first { addr -> Bool in
            guard
                let sa = addr.pointee.ifa_addr,
                sa.pointee.sa_family == AF_INET6,
                String(cString: addr.pointee.ifa_name) == name
            else {
                return false
            }
            return true
        }
        .flatMap { addr -> String? in
            var name = [CChar](repeating: 0, count: Int(NI_MAXHOST))
            let err = getnameinfo(
                addr.pointee.ifa_addr,
                socklen_t(addr.pointee.ifa_addr.pointee.sa_len),
                &name, socklen_t(name.count),
                nil,
                0,
                NI_NUMERICHOST | NI_NUMERICSERV
            )
            guard err == 0 else {
                return nil
            }
            return String(cString: name)
        }
}

func main() {
    guard let en7Addr = firstV6AddrForInterface(name: "en7") else {
        fatalError()
    }
    print("starting with \(en7Addr)…")
    let conn = NWConnection(host: .init(en7Addr), port: 12345, using: .udp)
    conn.pathUpdateHandler = { path in
        print(path.availableInterfaces) // prints: [en7]
    }
    conn.start(queue: .main)
    dispatchMain()
}

main()

Replies

This is an interesting edge case. I had a chat with the Network framework team about this and there is no direct way to do what you want. This kinda makes sense given Network framework’s original focus on transport protocols, but it’s clearly no longer sufficient. We would appreciate you filing a bug about this, explaining what you’re doing and where you got stuck. Please post your bug number, just for the record.

In the meantime, they suggested a workaround that seems viable. On Apple platforms all interfaces get a link-local IPv6 address. If you create an UDP connection (

NWConnection
) to that address, the resulting path contain’s the interface object you need.

Pasted in below is some code that shows this in action. I tested it as follows:

  1. I started with a MacBook running 10.15.2.

  2. I connected the Wi-Fi interface to the wider Internet.

  3. I attached a Thunderbolt Ethernet dongle. This became

    en7
    .
  4. I connected the Ethernet cable to the Ethernet port on another Mac. The other Mac isn’t relevant here, I just needed an active port that would bring up the link but not route any traffic.

  5. I ran the program below. As you see, the UDP connection’s path update handler is called with an interface object for

    en7
    . You should be able to use that for your
    NWEthernetChannel
    .

Please try this out and let me know how you get along.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
import Foundation
import Network

func firstV6AddrForInterface(name: String) -> String? {
    var addrList: UnsafeMutablePointer<ifaddrs>? = nil
    guard
        getifaddrs(&addrList) == 0,
        let first = addrList
    else { return nil }
    defer { freeifaddrs(addrList) }
    return sequence(first: first, next: { $0.pointee.ifa_next })
        .first { addr -> Bool in
            guard
                let sa = addr.pointee.ifa_addr,
                sa.pointee.sa_family == AF_INET6,
                String(cString: addr.pointee.ifa_name) == name
            else {
                return false
            }
            return true
        }
        .flatMap { addr -> String? in
            var name = [CChar](repeating: 0, count: Int(NI_MAXHOST))
            let err = getnameinfo(
                addr.pointee.ifa_addr,
                socklen_t(addr.pointee.ifa_addr.pointee.sa_len),
                &name, socklen_t(name.count),
                nil,
                0,
                NI_NUMERICHOST | NI_NUMERICSERV
            )
            guard err == 0 else {
                return nil
            }
            return String(cString: name)
        }
}

func main() {
    guard let en7Addr = firstV6AddrForInterface(name: "en7") else {
        fatalError()
    }
    print("starting with \(en7Addr)…")
    let conn = NWConnection(host: .init(en7Addr), port: 12345, using: .udp)
    conn.pathUpdateHandler = { path in
        print(path.availableInterfaces) // prints: [en7]
    }
    conn.start(queue: .main)
    dispatchMain()
}

main()

Thank you eskimo! I was able to get the NWInterface with the code you sent (which will be useful for other things too--I was trying to figure out how to open link-local IPv6 sockets with a specific source interface ;-).


Feedback FB7551376 submitted.


FYI I am experiencing other issues with NWEthernetChannel. I may come back and submit another bug report. Per wireshark the destination mac address on the wire does not match the NWEthernetChannel.EthernetAddress("11:11:11:11:11:11")! I configured for sending a frame. But I need to narrow down the code to have a submittable case.

Feedback FB7551376 submitted.

Thank you!

FYI I am experiencing other issues with

NWEthernetChannel
.

Well, you’re doing better than me. After posting the code above, I tried extending it to support sending data and I ended up with an mysterious crash )-: (r. 58835959)

But I need to narrow down the code to have a submittable case.

OK. Please post back here with your results; I’m curious how things panned out, good or bad.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I just found this thread after being stuck quite a while trying to figure out how to actually create an nw_interface_t for an interface. Is there any update on this, maybe some new API that I am overlooking here or do I still need this cumbersome workaround?

(Edit: I accidentally posted this as answer, it was meant to be a comment but there is no way to delete this now apparently. Sorry!)

do I still need this cumbersome workaround?

AFAIK you still need this workaround.

Looking at the bug mentioned above (FB7551376), it was closed without resulting in us adding an API [1]. If you find yourself in a situation where you deploy the above workaround, I encourage you to file a new bug describing your specific circumstances.

Please post your bug number, just for the record.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

[1] There’s nothing sinister going on here, I’m just not able to share details about another developer’s interactions with Apple.

It appears that this workaround is no longer working in 12.1 (perhaps other versions of macOS).

If i run this code, with en7 as an external Apple TB GbE adapter, I get the following output:

starting with fe80::1016:f5f0:a202:9ee3%en7…
[lo0]

So the interface being attached to is the lo0 (loopback) rather than the en7 interface.

So how do we reliably get the NWInterface on current OSes; as with the OP I need this to open a custom ethernet protocol on a local link with no Internet connectivity.

Since kexts are just about gone, we need a mechanism that works reliably in UserSpace to do raw enet packets; this was the path that was recommend (NWEthernetChannel), but there is still not a complete replacement for the KPIs (nor does there appear to be reliable workarounds) after at least 4 years of filing bugs, talking to DTS and the Network engineering team.

FB7665061 (April 2020)

So the interface being attached to is the lo0 (loopback) rather than the en7 interface.

Yep. I’m seeing this same thing on macOS 12.2.

There’s two parts to this:

  • Adding an API so that workarounds like this are unnecessary

  • Finding a new workaround in the meantime

With regards the first part, all the bugs listed above are closed, so it’d definitely be worthwhile filing a new one. Please post your bug number, just for the record.

On the ongoing workaround front, I had a quick look, which is about all I can do here on DevForums, and nothing jumped out. If you want me to allocate more time to this, please open a DTS tech support incident.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

  • So I have filed another bug about this specifically ( FB9974803 ) even though the one I referred to above ( FB7665061 ) is not closed.

    I have a (currently) viable workaround (described in FB9974803 ) but we really do need an official API for this task as the KPIs that nw_ethernet_channel is meant to replace are not replaced without a supported method of binding to a specific ethernet NIC.

Add a Comment

I've filed a duplicate Feedback too as I need the same missing API (currently using private API for this) as FB9753262. I've got the update that they believe this is fixed in macOS 13 Beta 3, however as far as I can tell by looking at the API diffs, there is no new API in Beta 3 in the Network framework that would help me get an nw_interface_t, is anyone aware of any new API I might be missing?

Oh, cool! I knew that this was coming and I’m very glad to see that it’s finally landed.

Anyway, the droid you’re looking for here is nw_path_monitor_create_for_ethernet_channel (C) or NWPathMonitor.ethernetChannel (Swift) (r. 85307591).

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Thanks @eskimo,

I just tried the new API, however I never get it to print any interfaces, maybe I am doing some obvious mistake here that I am missing?

    dispatch_queue_attr_t attributes = dispatch_queue_attr_make_with_qos_class(DISPATCH_QUEUE_SERIAL, QOS_CLASS_USER_INITIATED, -1);
    attributes = dispatch_queue_attr_make_initially_inactive(attributes);
    _nwqueue = dispatch_queue_create("de.epirat.nwqueue", attributes);
    if (@available(macOS 13.0, *)) {
        nw_path_monitor_t monitor = nw_path_monitor_create_for_ethernet_channel();
        nw_path_monitor_set_queue(monitor, _nwqueue);
        nw_path_monitor_set_update_handler(monitor, ^(nw_path_t  _Nonnull path) {
            nw_path_enumerate_interfaces(path, ^bool(nw_interface_t  _Nonnull interface) {
                const char *name = nw_interface_get_name(interface);
                uint32_t index = nw_interface_get_index(interface);
                NSLog(@"Interface %u: %s", index, name);
                return true;
            });
        });
    } else {
        // ...
    }

In addition, when Sandbox is enabled, I get a bunch of log messages warning about not being able to access preferences:

2022-07-22 17:02:34.351448+0200 LEDSender[28683:2062406] [User Defaults] Couldn't read values in CFPrefsPlistSource<0x60000338c100> (Domain: com.apple.powerlogd, User: kCFPreferencesAnyUser, ByHost: Yes, Container: (null), Contents Need Refresh: Yes): accessing preferences outside an application's container requires user-preference-read or file-read-data sandbox access
2022-07-22 17:02:34.466892+0200 LEDSender[28683:2062406] [] networkd_settings_read_from_file Sandbox is preventing this process from reading networkd settings file at "/Library/Preferences/com.apple.networkd.plist", please add an exception.
2022-07-22 17:02:34.467026+0200 LEDSender[28683:2062406] [] networkd_settings_read_from_file Sandbox is preventing this process from reading networkd settings file at "/Library/Preferences/com.apple.networkd.plist", please add an exception.

(I already updated my Feedback with these information as well)

maybe I am doing some obvious mistake here that I am missing?

I see no call to nw_path_monitor_start there. That’s kinda important (-:

In addition, when Sandbox is enabled

I recommend that you test this in a simple non-sandboxed test app first, then worry about App Sandbox.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

@eskimo

Whoops 🤦🏼

I've added it now. Apparently something is wrong with my dispatch queue too, as even when properly starting the path monitor, I never got the updates. But it works fine using the main queue… Not entirely sure why yet. But anyway everything seems to work fine now even when enabling the Sandbox, I still get the warnings in the console but it works regardless. Thanks a lot for your help!

    _nwqueue = _nwqueue = dispatch_get_main_queue();

    __block nw_interface_t intf = NULL;
    if (@available(macOS 13.0, *)) {
        _monitor = nw_path_monitor_create_for_ethernet_channel();
        nw_path_monitor_set_queue(_monitor, _nwqueue);
        nw_path_monitor_set_update_handler(_monitor, ^(nw_path_t  _Nonnull path) {
            nw_path_enumerate_interfaces(path, ^bool(nw_interface_t  _Nonnull interface) {
                const char *name = nw_interface_get_name(interface);
                uint32_t index = nw_interface_get_index(interface);
                NSLog(@"Interface %u: %s", index, name);
                return true;
            });
        });
        nw_path_monitor_start(_monitor);
    } else {
        // ...
    }