NWListener, P2P and awdl interfaces

I'm attempting to create a service that:

  1. Listens on iOS device A using NWListener
  2. Broadcasts the NWService ( using NWListener(service:using:)) ) on Bonjour
  3. Allows a separate device, iOS device B, to receive information about that service via an NWBrowser
  4. Connect to that service using the information contained in NWBrowser.Result 's NWEndpoint

I've been able to successfully do this using a SwiftNIO service, in the following environments:

  1. iOS device A and iOS device B are physical iOS devices on the same WiFi network. This works.
  2. iOS device A and iOS device B are iOS simulators on the same machine. This works.
  3. iOS device A is a physical device, and iOS device B is a simulator. iOS device A is not connected to a WiFi network, iOS device B is connected to a WiFi network. This works.

However, when iOS device A and iOS device B are physical devices that are not connected to a WiFi network, I encounter the following behavior:

  1. The Bonjour service is correctly advertised, and iOS device A and iOS device B are able to observe the advertisement of the service.
  2. In both cases, iOS device A and iOS device B, while able to resolve an NWEndpoint for the Bonjour service, are not able to connect to each other, and the connection attempt hangs.

My setup for the listener side of things looks roughly like:

let opts: NWParameters = .tcp
opts.includePeerToPeer = true
opts.allowLocalEndpointReuse = true
let service = NWListener.Service(name: "aux", type: BONJOUR_SERVICE_TYPE, domain: "")
try bootstrap.withNWListener(NWListener(service: service, using: opts)).wait() // bootstrap is an artifact of using SwiftNIO

Similarly, my setup on the discovery side of things looks like:

let params: NWParameters = .tcp
params.includePeerToPeer = true
let browser = NWBrowser(for: .bonjour(type: BONJOUR_SERVICE_TYPE, domain: BONJOUR_SERVICE_DOMAIN), using: params)
browser.browseResultsChangedHandler =  { (searchResults, changed) in
  // save the result to pass on its NWEndpoint later
}

and finally, where I have an NWEndpoint, I use SwiftNIO's NIOTSConnectionBootstrap.connect(endpoint:) to initialize a connection to my TCP service ( a web socket server ).

The fact that I am able to get P2P networking (presumably over an awdl interface?) between the simulator and the iOS device suggests to me that I haven't done anything obviously wrong in my setup. Similarly, the fact that it works over the same WiFi network and that, in P2P, I am able to at least observe the Bonjour advertisement, strikes me that I'm somewhere in the right neighborhood of getting this to work. I've also ensured that my Info.plist for the app has a NSLocalNetworkUsageDescription and NSBonjourServices for the Bonjour service type I'm browsing for.

I've even attempted to exercise the "Local Network Permission" dialog by using a hacky attempt that sends data to a local IP in order to trigger a permissions dialog, though the hack does not appear to actually force the dialog to appear.

Is there some trick or other piece of knowledge regarding allowing the use of P2P w/ Network.framework and TCP connections to services?

Answered by DTS Engineer in 733781022

I use SwiftNIO's NIOTSConnectionBootstrap.connect(endpoint:) to initialize a connection to my TCP service ( a web socket server ).

Is that code setting includePeerToPeer on the parameters use to create the underlying NWConnection?

Share and Enjoy

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

Accepted Answer

I use SwiftNIO's NIOTSConnectionBootstrap.connect(endpoint:) to initialize a connection to my TCP service ( a web socket server ).

Is that code setting includePeerToPeer on the parameters use to create the underlying NWConnection?

Share and Enjoy

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

Did setting includePeerToPeer solve your issues? I have the same issues you describe above, but I am setting includePeerToPeer=true.

The only real difference is that I'm using TLS with a key. This works with simulated or real devices on the same broadcast domain, e.g. same VLAN, same SSID, or same multicast domain. I can see the advertisement on a different SSID or VLAN, but I cannot connect. I also have a function where you can specify an IP Address. Since I force advertising on a specific port and can start a connection with Bonjour locating the service, I am at a loss why I can't connect across the broadcast domain. Here is how I'm setting my NWParameters:

extension NWParameters {
    
    convenience init( authenticatingWithKey key: Data ) {
        
        let tlsOptions = NWProtocolTLS.Options()
        let symmetricKey = SymmetricKey( data: key )
        let code = HMAC<SHA256>.authenticationCode( for: Data( "AppleConnect".utf8 ), using: symmetricKey ).withUnsafeBytes( DispatchData.init )
        sec_protocol_options_add_pre_shared_key( tlsOptions.securityProtocolOptions, code as dispatch_data_t, Data( "WindowProjectionTest".utf8 ).withUnsafeBytes( DispatchData.init ) as dispatch_data_t )
        sec_protocol_options_append_tls_ciphersuite( tlsOptions.securityProtocolOptions, tls_ciphersuite_t( rawValue: numericCast( TLS_PSK_WITH_AES_128_GCM_SHA256 ) )! )
        
        // TLS PSK requires TLSv1.2 on Apple platforms - See https://developer.apple.com/forums/thread/688508.
        sec_protocol_options_set_max_tls_protocol_version( tlsOptions.securityProtocolOptions, .TLSv12 )
        sec_protocol_options_set_min_tls_protocol_version( tlsOptions.securityProtocolOptions, .TLSv12 )    // Requires Second Attempt
        
        self.init( tls: tlsOptions )
        defaultProtocolStack.applicationProtocols.insert( NWProtocolFramer.Options( definition: .init( implementation: ConnectProtocol.self ) ), at: 0 )
        
        // Set Connection Configuration
        acceptLocalOnly = false
        allowLocalEndpointReuse = true
        includePeerToPeer = true
        prohibitedInterfaceTypes = [ NWInterface.InterfaceType.loopback ]
        
        // Force IPv4
        if let isOption = defaultProtocolStack.internetProtocol as? NWProtocolIP.Options {
            isOption.version = .v4
        }
    }
}

After posting this, I did some more testing and found that simply removing where I forced IPv4 at the bottom of the code in the previous post solved this issue..... partially. I can now connect across a network broadcast domain boundary, but I still cannot manually input an IP Address. I have to use Bonjour to cross the boundary. I can manually input the IP Address and connect when within the broadcast domain.

I would ideally like to limit everything to IPv4 addressing in my app, but given that that doesn't seem to work and connecting across SSID / VLAN / broadcast / multicast boundaries is more important, how can I either still limit to IPv4 or how can I connect directly to a given IP Address across SSID / VLAN boundaries with the Network Framework?

Here is my code. First up are the Constructors to my Connection object, including the one that takes an IP Address as a String:

// Bonjour Endpoint Constructor
public init( endpoint: NWEndpoint, key: Data ) async throws {
        
        let parameters = NWParameters( authenticatingWithKey: key )

        self.connection = NWConnection( to: endpoint, using: parameters )
        ( data, dataContinuation ) = AsyncThrowingStream.makeStream()
        try await connect()
}
// Explicit IP Address Constructor
public init( ipAddress: String, key: Data ) async throws {
        
        let parameters = NWParameters( authenticatingWithKey: key )
        let host: NWEndpoint.Host = NWEndpoint.Host( ipAddress )
        let port: NWEndpoint.Port = advertisedPort             // Fixed              
        
        self.connection = NWConnection( host: host, port: port, using: parameters )
        ( data, dataContinuation ) = AsyncThrowingStream.makeStream()
        try await connect()
}
// Server Constructor
public init( connection: NWConnection ) async throws {
        
        self.connection = connection
        ( data, dataContinuation ) = AsyncThrowingStream.makeStream()
        try await connect()
}

And here is my Connection.connect():

public func connect() async throws {
        
        try await withCheckedThrowingContinuation { continuation in
            connection.stateUpdateHandler = { [weak connection] state in
                switch state {
                    case .ready:
                        connection?.stateUpdateHandler = { state in
                            switch state {
                                case .failed( let error ):
                                    dataContinuation.finish( throwing: error )
                                    break
                                case .cancelled:
                                    dataContinuation.finish()
                                    break
                                default:
                                    break
                            }
                        }
                        continuation.resume()
                        break
                    case .preparing:
                        break
                    case .waiting( let waitingError ):                    
                        break
                    case .failed( let error ):
                    
                        let out = "Failed with Error: \( error.localizedDescription )"
                        Task {
                            await AppDelegate.log( out )
                        }
                    
                        continuation.resume( throwing: error )
                        break
                    default:
                    
                        let out = "Other Connection Status: \( state )"
                        Task {
                            await AppDelegate.log( out )
                        }
                        print( out )
                        break
                }
            }
            connection.start( queue: .main )
        }
        
        Self.receiveNextMessage( connection: connection, continuation: dataContinuation )
}

I can get as far as ".preparing" the connection with the manually-entered IP Address. Then, it hangs.

I can confirm that you do need to use Bonjour for peer-to-peer Wi-Fi to work [1].

[quote='813720022, pennstump, /thread/718461?answerId=813720022#813720022, /profile/pennstump'] I would ideally like to limit everything to IPv4 addressing in my app [/quote]

Why?

There are numerous situations where IPv4 won’t work and IPv6 will. You’ve found one of them, but there are others. Why limit yourself?

Share and Enjoy

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

[1] Due to the way that its implemented, something I’m really not able to discuss here.

Thank you for your response, Quinn. The short answer is that it's a user requirement for my app. The longer answer is that very few enterprises use IPv6 addressing, some explicitly block it from their routing tables, and if someone elects to manually enter an IP address of a known system in their environment to connect to and it's in IPv4 format, when my app connects to it and reports success - it should be able to report that it connected to the same address that was entered. Instead, I am forced to use the remoteEndPoint which is only reported as an IPv6 address. It's confusing for the user. A better question is why is there an option to force IPv4 and it doesn't work but rather breaks things? I can live with this quirk, but it somewhat defeats the purpose of the flexibility advertised by the Network Framework.

I'm more interested in solving why I can connect across SSID / VLAN using Bonjour, but not using a direct IP Address and Port. I would be grateful if you please help me solve this part of my issue. I'm using wired Ethernet and not Wi-Fi - does that change your answer at all or does P2P only work for Bonjour? Your footnote says this is something you are able to discuss here. Should I assume that means you are not?

If not, is there another framework you would recommend to connect P2P to a direct IP Address? Thanks!

Or, can you manually add an Endpoint to the Bonjour service that will make this work?

Your footnote says this is something you able to discuss here. Should I assume that means you ?

D’oh! You are correct. I’ve fixed that in my post.

if someone elects to manually enter an IP address of a known system in their environment to connect to and it's in IPv4 format, when my app connects to it and reports success - it should be able to report that it connected to the same address that was entered.

Hmmmm, I’m confused by that. If the user enters an IPv4 address then Bonjour isn’t involved. Rather, you’d create NWConnection from a .hostPort(…) endpoint containing the IPv4 address. Such a connection can only work over IPv4 because it only has an IPv4 address to work with. Thus, the remoteEndPoint property of its path will always be IPv4.

A better question is why is there an option to force IPv4 and it doesn't work but rather breaks things?

… because not all networks support IPv4. If you force IPv4, then it’s obvious that it’ll fail in those scenarios.

I'm using wired Ethernet and not Wi-Fi

OK. I think I know what’s going on here but lets confirm some basics:

  • Is this on a Mac? Or iOS?

  • Does your VLAN setup forward any {broad,multi}cast traffic between the VLANs?

Share and Enjoy

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

NWListener, P2P and awdl interfaces
 
 
Q