Alternatives to using en0 for WiFi IP Address lookup

Currently, we have a Flutter app that uses the network_info_plus plugin to connect to the Wifi network of the current iOS device our app is running on. Unfortunately, we are unable to use Bonjour on the target device that we are attempting to connect to, which acts as an AP, due to legacy device issues. Hence the reason why we use the device's Wifi IP address to interact with this device when it's connected to this device's Access Point.

This plugin's iOS implementation is using a non-guaranteed way of fetching the device's Wifi IP address according to a recent DevForum post, presuming that it's en0 as seen here:

- (void)enumerateWifiAddresses:(NSInteger)family
                    usingBlock:(void (^)(struct ifaddrs *))block {
  struct ifaddrs *interfaces = NULL;
  struct ifaddrs *temp_addr = NULL;
  int success = 0;

  // retrieve the current interfaces - returns 0 on success
  success = getifaddrs(&interfaces);
  if (success == 0) {
    // Loop through linked list of interfaces
    temp_addr = interfaces;
    while (temp_addr != NULL) {
      if (temp_addr->ifa_addr->sa_family == family) {
        // en0 is the wifi connection on iOS
        if ([[NSString stringWithUTF8String:temp_addr->ifa_name]
                isEqualToString:@"en0"]) {
          block(temp_addr);
        }
      }

      temp_addr = temp_addr->ifa_next;
    }
  }

  // Free memory
  freeifaddrs(interfaces);
}

I was just wondering what an alternative Objective-C implementation would look like for fetching the actual IP address using a subnet broadcast, since the aforementioned DevForum post suggested that we get all Ethernet-like interfaces (I'm assuming those devices that are prefixed with en) and create a Socket for them bound to IP_BOUND_IF.

Thank you in advance.

Unfortunately, we are unable to use Bonjour on the target device accessory that we are attempting to connect to, which acts as an AP

First up, I’ve replaced “target device” with “accessory” in the above, because when talking about iOS it’s best to reserve device for the iOS device.

Lemme see if I understand you correctly:

  • You have a Wi-Fi based accessory.

  • Your building an app to talk to that accessory.

  • Your app assumes that the iOS device has joined the accessory’s Wi-Fi network.

Is that right?

Once you’ve determined the interface, how do you discover the accessory? I see two common approaches here:

  • You have a custom service discovery protocol, and so you need to broadcast on that interface.

  • The accessory is always at a fixed location on the network that it publishes. For example, the accessory publishes a network of the form 192.168.17.0/24 and it then uses a fixed IP address of 192.168.17.1.

Share and Enjoy

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

That's one half of the equation.

The other half is that we spin up a short-lived HTTP server on the iOS device, in order to host a firmware file that the accessory will download in order to upgrade itself to a later version that supports BLE. We send an HTTP network call to the accessory's magic IP address when the iOS device is connected to its network, which it uses to fetch the hosted firmware file from the iOS device.

This is why it's important that we get the device's WiFi IP address, so that the accessory knows where to download the file from, since the accessory is not connected to the internet at this point. That and the fact that it only supports BLE in later versions, hence the legacy nature of this problem.

Hope this helps in terms of context.

That's one half of the equation.

OK, but how exactly? I suggested two possibilities: service discovery and fixed location. Which of those applies? Or is it something else?

it's important that we get the device's WiFi IP address, so … the accessory knows where to download the file from

Interesting. Fortunately once you’re in communication with the accessory it’s easy to solve this. Let’s focus on that first, and then I’ll come back to this.

Share and Enjoy

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

We communicate with it via fixed location (a.k.a. magic IP address, from a known SSID containing the accessory's serial number), hence we are able to discover it very trivially if the user's nearby. The iOS device then connects to the accessory's WiFi SSID, and then sends HTTP network calls to a fixed IP address on that network. One of them tells the accessory where to download and install firmware from a hosted file from the iOS device on the network via short-lived HTTP server.

We just need to know what IP address the iOS device is using when connected to that network. If it helps, the accessory's IP address is the first IP address under the 192.168.43.0/24 subnet, in this case it's 192.168.43.1.

If there was a better way to do this from a user standpoint, where it can easily be encapsulated without providing any additional data from the caller, that would be great, since I'll be able to use that to submit a PR for that plugin's iOS platform code.

Presumably you are able to open a TCP connection to this accessory, because that’s what you’re using to send it the command to update the firmware. If so, you don’t need to mess around with any low-level interface stuff at all. The local IP address of your TCP connection is the right IP address to use.

Share and Enjoy

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

Actually, the iOS devices uses HTTP to send a connection to that accessory's IP address. This happens after the iOS device connects to the accessory's network.

As part of the payload, it sends a link to the firmware file hosted on the iOS device via short-lived HTTP server. That means, the accessory has no idea what device is connecting to it and needs to know an absolute location from which to fetch the firmware file from on the accessory's network. That and due to the legacy nature of this ask, we're unable to modify it so that the accessory assumes the request's IP address as the hostname to this file. Hence the reason why we create a URL that we send to the accessory via POST, with the iOS device's IP address as the host. The device listens on a particular port and returns the file in question once the accessory attempts to open that local URL.

Hopefully this adds more clarity.

Correction, the iOS devices uses HTTP to send a POST request to that accessory's IP address, containing a locally hosted URL pointing to the firmware file hosted on a short-lived HTTP service on the iOS device.

Alternatively, if the iOS device creates an HTTP server bound to any IP address on that accessory's network, and if Dart, the high-level language that powers Flutter, exposes the bound server's host and port after starting it up, wouldn't that for all intents and purposes be the IP address from which the accessory should fetch the file from?

Alternatively, if the iOS device creates an HTTP server bound to any IP address on that accessory's network, and if Dart, the high-level language that powers Flutter, exposes the bound server's host and port after starting it up, wouldn't that for all intents and purposes be the IP address from which the accessory should fetch the file from?

I can’t give definitive answers about third-party libraries, but I think you’ve misunderstood how TCP listeners work. If you start a TCP listener on the wildcard address, the listener isn’t bound to any IP address. If you call getsockname on the listener, it returns the wildcard address.

Consider this:

import Foundation
import System

func main() throws {
    let listener = try FileDescriptor.socket(AF_INET, SOCK_STREAM, 0)
    try listener.bind("0.0.0.0", 12345)
    try listener.listen(5)
    let connection = try listener.accept()
    let listenerAddr = try listener.getSockName()
    print(listenerAddr)     
    let connectionAddr = try connection.getSockName()
    print(connectionAddr)   
}

try main()

If, in Terminal, I use nc connect to it at 127.0.0.1:12345, it prints:

(address: "0.0.0.0", port: 12345)
(address: "127.0.0.1", port: 12345)

Note how the connection socket is bound to the interface’s IP address but the listener socket remains bound to the wildcard.


Actually, the iOS devices uses HTTP to send a connection to that accessory's IP address.

That complicates things, but not in a way that fundamentally breaks my approach. I doubt that this is the first HTTP request you’ve sent to the accessory, right? That is, to run the update you have to send a few HTTP requests, with the last one being this update command that contain the IP address of your server.

If that’s the case then you can get the local IP address from the immediately prior HTTP request. Unless the network stack reconfigures between the requests, which seems unlikely, that’ll be fine.

With URLSession you can use the task metrics for this. For example:

import Foundation

class MetricCaptureDelegate: NSObject, URLSessionTaskDelegate {
    var metrics: [URLSessionTaskMetrics] = []
    func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) {
        self.metrics.append(metrics)
    }
}

func main() async throws {
    let url = URL(string: "https://example.com")!
    let request = URLRequest(url: url, cachePolicy: .reloadIgnoringLocalCacheData, timeoutInterval: 60.0)
    let delegate = MetricCaptureDelegate()
    let (data, response) = try await URLSession.shared.data(for: request, delegate: delegate)
    let addresses = delegate.metrics.flatMap { m in
        m.transactionMetrics.compactMap { m in
            m.localAddress
        }
    }
    print((response as? HTTPURLResponse)?.statusCode ?? -1)
    print(data.count)
    print(addresses)
}

try await main()

On my Mac this prints:

200
1256
["192.168.1.71"]

where 192.168.1.71 is the IP address for the interface that leads to example.com.

ps Over the weekend I finished up Extra-ordinary Networking. I think you’ll find it interesting. You might also be amused to know that your thread inspired one of the examples in Don’t Try to Get the Device’s IP Address (-:

Share and Enjoy

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

I doubt that this is the first HTTP request you’ve sent to the accessory, right? That is, to run the update you have to send a few HTTP requests, with the last one being this update command that contain the IP address of your server.

Actually, it only makes a single HTTP POST request to the accessory containing the IP address of the iOS's device's server.

So what you're saying is that the iOS device should make a fake request to a fake website, knowing that it's connected to the accessory's WiFi AP, where it's not even connected to the internet at that point? I'm a bit confused as to what needs to be done in order to get the iOS device's IP address from a network call that will not succeed due to the accessory not being connected to to the internet at that point.

Alternatively, can I make a no-op HTTP POST request to this accessory (one in which the accessory will not accept but will respond with a generic error response), wait for it to respond, and then collect its metrics instead, which should yield the IP address used, correct?

can I make a no-op HTTP POST request to this accessory

I can’t answer that, because I don’t know how your accessory works.

If you really don’t want to send a request to the accessory, you can do this sort of thing using a UDP flow (either a connected UDP socket or an NWConnection with UDP). If you start such a flow but don’t send any traffic, the system will still bind the flow’s local address to the right interface.

However, sending a no-op request seems like a better option, assuming your access can handle that.

Share and Enjoy

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

Alternatives to using en0 for WiFi IP Address lookup
 
 
Q