Network connections send and receive data using transport and security protocols.

Posts under Network tag

44 Posts

Post

Replies

Boosts

Views

Activity

When restarting a PC with macOS15, communication on the local network is denied for the target app even though "Local Network" in "Privacy and Security" is allowed.
We are developing a desktop app for macOS, built with Electron + Angular + Node.js. The app has the ability to access devices on the local network. We are having some trouble with the behavior of the "Local Network" feature added in macOS 15. Procedure: MacBook Pro 14inch (M1Pro) 2021, 16GB memory - macOS Sequoia15.4.1 Network Environment: The PC and the access point are connected by wire, and the target device and the access point are also connected by wire. Not connected to an external network Install the target app Start the application and allow the "Local Network" dialog that appears Open the "Local Network" item in "Privacy and Security" and make sure the target app is allowed. Confirm that you can access the target device on the local network Restart the PC Open the "Local Network" item in "Privacy and Security" and make sure the target app is allowed. Start the app and access the target device on the local network Result: Access to the target device may fail (this does not occur 100% of the time) If you turn the target app in the "Local Network" section of "Privacy and Security" OFF and then ON, you will be able to connect However, it seems that the problem may occur again if you restart your PC. According to our research, the probability of this happening increases if the target app is the only app allowed on the "local network". This happens not only with our app, but also when you follow the above steps in "Google Chrome". Based on the above investigation results, I suspect that this is a problem with the OS. What do you think?
2
0
150
May ’25
intermittent multicast socket failures, new to Sequoia, still not fixed
multicast sockets fail to send/receive on macosx, errno 65 "no route to host". Wireshark and Terminal.app (which have root privileges) both show incoming multicast traffic just fine. Normal UDP broadcast sockets have no problems. Toggling the Security&Privacy -> Local Network setting may fix the problem for some Users. There is no pattern for when multicast socket fails. Sometimes, recreating the sockets fix the problem. Restart the app, sometimes multicast fails, sometimes success (intermittent, no pattern). Reboot machine (intermittent fail) Create a fresh new user on machine, install single version of app, give app permission. (intermittent fail, same as above). We have all the normal entitlements / notarized app. Similar posts here see FB16923535, Related to FB16512666 https://forum.xojo.com/t/udp-multicast-receive-on-mac-failing-intermittant/83221 see my post from 2012 "distinguishing between SENDING sockets and RECEIVING sockets" for source code example of how we bind multicast sockets. Our other socket code is standard "Stevens, et al." code. The bind() is the call that fails in this case. https://stackoverflow.com/questions/10692956/what-does-it-mean-to-bind-a-multicast-udp-socket . Note that this post from 2012 is still relevant, and that it is a workaround to a longstanding Apple bug that was never fixed. Namely, "Without this fix, multicast sending will intermittently get sendto() errno 'No route to host'. If anyone can shed light on why unplugging a DHCP gateway causes Mac OS X multicast SENDING sockets to get confused, I would love to hear it." This may be a hint as to the underlying bug that Apple really needs to fix, but if it's not, then please Apple, fix the Sequoia bug first. These are probably different bugs because in one case, sendto() fails when a socket becomes "unbound" after you unplug an unrelated network cable. In this case, bind() fails, so sendto() is never even called. Note, that we have also tried to use other implementations for network discovery, including Bonjour, CFNetwork, etc. Bonjour fails intermittently, and also suffers from both bugs mentioned above, amongst others.
3
0
116
May ’25
My app suddenly getting "A server with the specified hostname could not be found"
I've had no problem running my app in a simulator or on a device, but today my app is failing on a URLRequest to my local machine (in a sim). From the same simulator I can go to Safari and manually enter the URL that the app is using (and that appears in the error message), and it works fine. I think there was a recent Xcode update; did something change in this regard?
6
0
258
May ’25
Determine outgoing flow source IPs without allowing data leakage using NEFilterDataProvider
I'm looking for help with a network extension filtering issue. Specifically, we have a subclass of NEFilterDataProvider that is used to filter flows based upon a set of rules, including source IP and destination IP. We've run into an issue where the source IP is frequently 0.0.0.0 (or the IPv6 equivalent) on outgoing flows. This has made it so rules based upon source IP don't work. This is also an issue as we report these connections, but we're lacking critical data. We were able to work around the issue somewhat by keeping a list of flows that we allow that we periodically check to see if the source IP is available, and then report after it becomes available. We also considered doing a "peekBytes" to allow a bit of data to flow and then recheck the flow, but we don't want to allow data leakage on connections that should be blocked because of the source IP. Is there a way to force the operating system or network extension frameworks to determine the source IP for an outbound flow without allowing any bytes to flow to the network? STEPS TO REPRODUCE Create a network filtering extension for filtering flows using NEFilterDataProvider See that when handleNewFlow: is called, the outgoing flow lacks the source IP (is 0.0.0.0) in most cases There is this post that is discussing a similar question, though for a slightly different reason. I imagine the answer to this and the other post will be related, at least as far as NEFilterDataProvider:handleNewFlow not having source IP is considered. Thanks!
1
0
185
Apr ’25
DNS Resolution fails in 15.4
Hi, DNS resolution using libresolv (res_nquery) fails in 15.4 when connected to VPN. The same is working fine for 15.3 and lower and this happens for all the domains. The method returns -1 and res->res_h_errno is set to 2. In wireshark we can see that the DNS request is sent and server also returns the response successfully. The same works fine if we use TCP instead of UDP by setting the following option res->options |= RES_USEVC;
5
0
226
Apr ’25
Using Cellular Data While Connected to Wifi
Hello, A quick background: I am developing an App that receives a data stream from a device through its Wi-Fi network. The device itself is not connected to the internet, so the app won't be either. Now, I am adding a new feature to the App that would require internet connection during the data stream. Consequently, my users would need to use their cellular data. On later versions of iPhone, the phone would occasionally detect the lack of internet connection and asks the user via a pop-up if they want to use their cellular data. However, this behavior is not consistent. So my question is- can we programmatically invoke this pop-up so the user can connect to the internet? Or even better- can we program the App to use cellular data while still being connected to a Wi-Fi network? Note: I have seen mixed answers on the internet whether this is doable or not, and I know that users are able do it themselves by manually configuring their IP in their WiFi settings page, but I doubt this operation can be done through the App for security reasons. Thanks!
4
0
3k
Apr ’25
Use cellular data on the app while connected to hardware's wifi that doesn't have internet connection
Hello, I am in a very similar situation as described in the thread: https://developer.apple.com/forums/thread/655183 Context: I am working on an app that receives data from a hardware device through its Wifi network, and the hardware is not connected to the internet. Now, I would need to call some API while still connected to hardware so I would need to use the cellular data. As mentioned on the thread, I can achieve this via Network framework, using the requiredInterfaceType property. But Is there any other way I can achieve this? I can also do some suggestion on the hardware if that's helpful. Thank you!
1
0
202
Apr ’25
Understanding Also-Ran Connections
Every now and again folks notice that Network framework seems to create an unexpected number of connections on the wire. This post explains why that happens and what you should do about it. If you have questions or comments, put them in a new thread here on the forums. Use the App & System Services > Networking topic area and the Network tag. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Understanding Also-Ran Connections Network framework implements the Happy Eyeballs algorithm. That might create more on-the-wire connections than you expect. There are two common places where folks notice this: When looking at a packet trace When implementing a listener Imagine that you’ve implemented a TCP server using NWListener and you connect to it from a client using NWConnection. In many situations there are multiple network paths between the client and the server. For example, on a local network there’s always at least two paths: the link-local IPv6 path and either an infrastructure IPv4 path or the link-local IPv4 path. When you start your NWConnection, Network framework’s Happy Eyeballs algorithm might [1] start a TCP connection for each of these paths. It then races those connections. The one that connects first is the ‘winner’, and Network framework uses that connection for your traffic. Once it has a winner, the other connections, the also-ran connections, are redundant, and Network framework just closes them. You can observe this behaviour on the client side by looking in the system log. Many Network framework log entries (subsystem com.apple.network) contain a connection identifier. For example C8 is the eighth connection started by this process. Each connection may have child connections (C8.1, C8.2, …) and grandchild connections (C8.1.1, C8.1.2, …), and so on. You’ll see state transitions for these child connections occurring in parallel. For example, the following log entries show that C8 is racing the connection of two grandchild connections, C8.1.1 and C8.1.2: type: debug time: 12:22:26.825331+0100 process: TestAlsoRanConnections subsystem: com.apple.network category: connection message: nw_socket_connect [C8.1.1:1] Calling connectx(…) type: debug time: 12:22:26.964150+0100 process: TestAlsoRanConnections subsystem: com.apple.network category: connection message: nw_socket_connect [C8.1.2:1] Calling connectx(…) Note For more information about accessing the system log, see Your Friend the System Log. You also see this on the server side, but in this case each connection is visible to your code. When you connect from the client, Network framework calls your listener’s new connection handler with multiple connections. One of those is the winning connection and you’ll receive traffic on it. The others are the also-ran connections, and they close promptly. IMPORTANT Depending on network conditions there may be no also-ran connections. Or there may be lots of them. If you want to test the also-ran connection case, use Network Link Conditioner to add a bunch of delay to your packets. You don’t need to write special code to handle also-ran connections. From the perspective of your listener, these are simply connections that open and then immediately close. There’s no difference between an also-ran connection and, say, a connection from a client that immediately crashes. Or a connection generated by someone doing a port scan. Your server must be resilient to such things. However, the presence of these also-ran connections can be confusing, especially if you’re just getting started with Network framework, and hence this post. [1] This is “might” because the exact behaviour depends on network conditions. More on that below.
0
0
164
Apr ’25
iOS 18.4 key usage requirements fails TLS connections
iOS 18.4 introduced some requirements on the Key Usage of 802.1x server certificates, as described here. https://support.apple.com/en-us/121158 When using TLS_ECDHE_RSA or TLS_DHE_RSA cipher suites, 802.1X server certificates containing a Key Usage extension must have Digital Signature key usage set. When using the TLS_RSA cipher suite, 802.1X server certificates containing a Key Usage extension must have Key Encipherment key usage set. It reads like the change is supposed to affect 802.1x only. However, we have found out that the new restrictions are actually imposed on all TLS connections using the Network framework, including in Safari. Unlike other certificate errors which can be either ignored by users (as in Safari) or by code (via sec_protocol_options_set_verify_block), these new ones can't. Even if passing completion(true) in the TLS verification block, the connection still ends up in waiting state with error -9830: illegal parameter. I understand that these requirements are valid ones but as a generic TLS library I also expect that Network framework could at least allow overriding the behavior. The current treatment is not consistent with those on other certificate errors. Since I can't upload certificates, here is how to reproduce a certificate that fails. Create a OpenSSL config file test.cnf [ req ] default_bits = 2048 distinguished_name = dn x509_extensions = v3_ca prompt = no [ dn ] CN = example.com [ v3_ca ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = CA:TRUE keyUsage = critical, keyCertSign, cRLSign Generate certificate and private key openssl req -x509 -new -nodes -keyout key.pem -out cert.pem -days 365 -config test.cnf And here is the client code to test. // Target server and port let host = NWEndpoint.Host("example.com") let port = NWEndpoint.Port("443")! // Configure insecure TLS options let tlsOptions = NWProtocolTLS.Options() sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { _, _, completion in // Always trust completion(true) }, DispatchQueue.global()) let params = NWParameters(tls: tlsOptions) let connection = NWConnection(host: .init(host), port: .init(rawValue: port)!, using: params) connection.stateUpdateHandler = { newState in switch newState { case .ready: print("TLS connection established") case .failed(let error): print("Connection failed: \(error)") case .cancelled: print("Connection canceled") case .preparing: print("Connection preparing") case .waiting(let error): print("Connection waiting: \(error)") case .setup: print("Connection setup") default: break } } connection.start(queue: .global()) Output Connection preparing Connection waiting: -9830: illegal parameter Previously reported as FB17099740
5
0
302
Apr ’25
use user ip address
Hi, is it legal to use open APIs to get the users's country country code using the Ip address in the app? I mean I want to know the user country for the game leaderboards data, and there are sites say this is free and open. So, I have two questions, first, is this making the user calling open api to get its country code concept legal? second question, what if these sites suddenly decided that it is not legal to use their apis for commercial use, and i miss that announcement; will you remove my app from the store? or what action will you take exactly?
1
0
150
Apr ’25
Level Networking on watchOS for Duplex audio streaming
I did watch WWDC 2019 Session 716 and understand that an active audio session is key to unlocking low‑level networking on watchOS. I’m configuring my audio session and engine as follows: private func configureAudioSession(completion: @escaping (Bool) -> Void) { let audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: []) try audioSession.setActive(true, options: .notifyOthersOnDeactivation) // Retrieve sample rate and configure the audio format. let sampleRate = audioSession.sampleRate print("Active hardware sample rate: \(sampleRate)") audioFormat = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1) // Configure the audio engine. audioInputNode = audioEngine.inputNode audioEngine.attach(audioPlayerNode) audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: audioFormat) try audioEngine.start() completion(true) } catch { print("Error configuring audio session: \(error.localizedDescription)") completion(false) } } private func setupUDPConnection() { let parameters = NWParameters.udp parameters.includePeerToPeer = true connection = NWConnection(host: "***.***.xxxxx.***", port: 0000, using: parameters) setupNWConnectionHandlers() } private func setupTCPConnection() { let parameters = NWParameters.tcp connection = NWConnection(host: "***.***.xxxxx.***", port: 0000, using: parameters) setupNWConnectionHandlers() } private func setupWebSocketConnection() { guard let url = URL(string: "ws://***.***.xxxxx.***:0000") else { print("Invalid WebSocket URL") return } let session = URLSession(configuration: .default) webSocketTask = session.webSocketTask(with: url) webSocketTask?.resume() print("WebSocket connection initiated") sendAudioToServer() receiveDataFromServer() sendWebSocketPing(after: 0.6) } private func setupNWConnectionHandlers() { connection?.stateUpdateHandler = { [weak self] state in DispatchQueue.main.async { switch state { case .ready: print("Connected (NWConnection)") self?.isConnected = true self?.failToConnect = false self?.receiveDataFromServer() self?.sendAudioToServer() case .waiting(let error), .failed(let error): print("Connection error: \(error.localizedDescription)") DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self?.setupNetwork() } case .cancelled: print("NWConnection cancelled") self?.isConnected = false default: break } } } connection?.start(queue: .main) } Duplex in this context refers to two-way audio transmission simultaneously recording and sending audio while also receiving and playing back incoming audio, similar to a VoIP/SIP call. The setup works fine on the simulator, which suggests that the core logic is correct. However, since the simulator doesn’t fully replicate WatchOS hardware behavior especially for audio sessions and networking issues might arise when running on a real device. The problem likely lies in either the Watch’s actual hardware limitations, permission constraints, or specific audio session configurations. I am reaching out to seek further assistance regarding the challenges I've been experiencing with establishing a UDP, TCP & web socket connection on watchOS using NWConnection for duplex audio streaming. Despite implementing the recommendations provided earlier, I am still encountering difficulties From what I can see, your implementation is focused on streaming audio playback with the server. In my case, I'm looking for a slightly different approach: I want to capture audio and send buffers of a specific size to the server while playing audio simultaneously, essentially achieving full duplex streaming similar to a VOIP call. Additionally, I’d like to ensure that if no external audio route is connected, the Apple Watch speaker is used by default. Any thoughts or insights on adapting this setup for those requirements would be very welcome.
1
0
257
Apr ’25
If UDP communication is used to obtain NTP, will it be approved by the Apple Store review?
There may be mistakes in the English translation. Please understand. I am developing a smartphone application using Unity and considering using a public NTP server to obtain the current time. During my research, I found an article stating that communications using UDP are blocked by Apple's review process. The article also mentioned that obtaining the time via HTTPS communication successfully passed Apple's review. If an app uses UDP communication to obtain the current time from NTP, will it fail the review process? When I built the app in Xcode and launched it, the current time obtained from NTP was displayed in the Xcode log, so I believe the communication is working properly. Below is the URL of the article I found. https://deep-verdure.hatenablog.com/entry/2020/05/06/050506
2
0
133
Apr ’25
Can NWBrowser be used in an AppIntent extension? NoAuth
I have an iOS app that connects to a server running on macOS by leveraging NWListener & NWBrowser. It also makes use of the peerToPeer functionality / AWDL offered via the Network framework. This works great in the iOS app. Now I would like to add support for Shortcuts / App Intents in general. The NWConnection on its own is also working great in the App Intent, but only if I provide the host/port manually, which means I can't use the peer to peer functionality. If I try to run my NWBrowser in the AppIntent it immediately changes its state to failed with a NoAuth (-65555) error: nw_browser_cancel [B1517] The browser has already been cancelled, ignoring nw_browser_cancel(). nw_browser_fail_on_dns_error_locked [B1518] DNSServiceBrowse failed: NoAuth(-65555) NWClientManager: Browser failed: -65555: NoAuth I haven't found documentation/information on whether NWBrowser should work in an AppIntent extension or not.
1
0
127
Apr ’25
Level Networking on watchOS for Duplex audio streaming
I did watch WWDC 2019 Session 716 and understand that an active audio session is key to unlocking low‑level networking on watchOS. I’m configuring my audio session and engine as follows: private func configureAudioSession(completion: @escaping (Bool) -> Void) { let audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: []) try audioSession.setActive(true, options: .notifyOthersOnDeactivation) // Retrieve sample rate and configure the audio format. let sampleRate = audioSession.sampleRate print("Active hardware sample rate: \(sampleRate)") audioFormat = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1) // Configure the audio engine. audioInputNode = audioEngine.inputNode audioEngine.attach(audioPlayerNode) audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: audioFormat) try audioEngine.start() completion(true) } catch { print("Error configuring audio session: \(error.localizedDescription)") completion(false) } } private func setupUDPConnection() { let parameters = NWParameters.udp parameters.includePeerToPeer = true connection = NWConnection(host: "***.***.xxxxx.***", port: 0000, using: parameters) setupNWConnectionHandlers() } private func setupTCPConnection() { let parameters = NWParameters.tcp connection = NWConnection(host: "***.***.xxxxx.***", port: 0000, using: parameters) setupNWConnectionHandlers() } private func setupWebSocketConnection() { guard let url = URL(string: "ws://***.***.xxxxx.***:0000") else { print("Invalid WebSocket URL") return } let session = URLSession(configuration: .default) webSocketTask = session.webSocketTask(with: url) webSocketTask?.resume() print("WebSocket connection initiated") sendAudioToServer() receiveDataFromServer() sendWebSocketPing(after: 0.6) } private func setupNWConnectionHandlers() { connection?.stateUpdateHandler = { [weak self] state in DispatchQueue.main.async { switch state { case .ready: print("Connected (NWConnection)") self?.isConnected = true self?.failToConnect = false self?.receiveDataFromServer() self?.sendAudioToServer() case .waiting(let error), .failed(let error): print("Connection error: \(error.localizedDescription)") DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self?.setupNetwork() } case .cancelled: print("NWConnection cancelled") self?.isConnected = false default: break } } } connection?.start(queue: .main) } I am reaching out to seek further assistance regarding the challenges I've been experiencing with establishing a UDP, TCP & web socket connection on watchOS using NWConnection for duplex audio streaming. Despite implementing the recommendations provided earlier, I am still encountering difficulties. Or duplex audio streaming not possible on apple watch?
6
0
203
Apr ’25
Connecting to a service found by Bonjour isn't working.
I'm using NWBrowser to search for a server that I hosted. The browser does find my service but when it tries to connect to it, it gets stuck in the preparing phase in NWConnection.stateUpdateHandler. When I hardcode the local IP address of my computer (where the server is hosted) into NWConnection it works perfectly fine and is able to connect. When it gets stuck in the preparing phase, it gives me the warnings and error messages in the image below. You can also see that the service name is correct and it is found. I have tried _http._tcp and _ssh._tcp types and neither work. This is what my code looks like: func findServerAndConnect(port: UInt16) { print("Searching for server...") let browser = NWBrowser(for: .bonjour(type: "_ssh._tcp", domain: "local."), using: .tcp) browser.browseResultsChangedHandler = { results, _ in print("Found results: \(results)") for result in results { if case let NWEndpoint.service(name, type_, domain, interface) = result.endpoint { if name == "PocketPadServer" { print("Found service: \(name) of type \(type_) in domain \(domain) on interface \(interface)") // Construct the full service name, including type and domain let fullServiceName = "\(name).\(type_).\(domain)" print("Full service name: \(fullServiceName), \(result.endpoint)") self.connect(to: result.endpoint, port: port) browser.cancel() break } } } } browser.start(queue: .main) } func connect(to endpoint: NWEndpoint, port: UInt16) { print("Connecting to \(endpoint) on port \(port)...") // endpoint = NWEndpoint( let tcpParams = NWProtocolTCP.Options() tcpParams.enableFastOpen = true tcpParams.keepaliveIdle = 2 let params = NWParameters(tls: nil, tcp: tcpParams) params.includePeerToPeer = true // connection = NWConnection(host: NWEndpoint.Host("xx.xxx.xxx.xxx"), port: NWEndpoint.Port(3000), using: params) connection = NWConnection(to: endpoint, using: params) connection?.pathUpdateHandler = { path in print("Connection path update: \(path)") if path.status == .satisfied { print("Connection path is satisfied") } else { print("Connection path is not satisfied: \(path.status)") } } connection?.stateUpdateHandler = { newState in DispatchQueue.main.async { switch newState { case .ready: print("Connected to server") self.pairing = true self.receiveMessage() case .failed(let error): print("Connection failed: \(error)") self.isConnected = false case .waiting(let error): print("Waiting for connection... \(error)") self.isConnected = false case .cancelled: print("Connection cancelled") self.isConnected = false case .preparing: print("Preparing connection...") self.isConnected = false default: print("Connection state changed: \(newState)") break } } } connection?.start(queue: .main) }
4
0
166
Apr ’25
Hide OS logs coming out of the Network framework (category: com.apple.network)
I'm establishing a connection with NWListener and NWConnection which is working great. However, if the listener disappears, a lot of logs are appearing: Is there a way to hide these logs? I'm aware of OS_ACTIVITY_MODE=disabled, but that will also hide a lot of other logs. I also know you can hide these using Xcode's filtering. I'm looking for a programmatically way to hide these completely. I'm not interested in seeing these at all, or, at least, I want to be in control. Thanks!
4
0
193
Apr ’25
in-addr.arpa default search domains
Hi, I observed some unexpected behavior and hope that someone can enlighten me as to what this is about: mDNSResponder prepends IP / network based default search domains that are checked before any other search domain. E.g. 0.1.168.192.in-addr.arpa. would be used for an interface with an address in the the 192.168.1.0/24 subnet. This is done for any configured non-link-local IP address. I tried to find any mention of an approach like this in RFCs but couldn't spot anything. Please note that this is indeed a search domain and different from reverse-DNS lookups. Example output of tcpdump for ping devtest: 10:02:13.850802 IP (tos 0x0, ttl 64, id 43461, offset 0, flags [none], proto UDP (17), length 92) 192.168.1.2.52319 > 192.168.1.1.53: 54890+ [1au] A? devtest.0.1.168.192.in-addr.arpa. (64) I was able to identify the code that adds those default IP subnet based search domains but failed to spot any indication as to what this is about: https://github.com/apple-oss-distributions/mDNSResponder/blob/d5029b5/mDNSMacOSX/mDNSMacOSX.c#L4171-L4211 Does anyone here have an ideas as to what this might be about?
1
0
786
Apr ’25
Drop from URLSession to Network framework for SNI
Hi Dev Forums and Quinn "The Eskimo!", Short version Is there sample NWConnection code available that behaves in a similar way to the higher level URLSession and URLRequest APIs? Long version I have not been able to make this question get past the "sensitive language filter" on the dev forums. I figured it might be 'fool' or 'heck', or the X link, but removing each of those still triggers the sensitive language filter. Please see this gist: https://gist.github.com/lzell/8672c26ecb6ee1bb26d3aa3c7d67dd62 Thank you! Lou Zell
2
0
158
Apr ’25
Moving from Multipeer Connectivity to Network Framework
I see a lot of folks spend a lot of time trying to get Multipeer Connectivity to work for them. My experience is that the final result is often unsatisfactory. Instead, my medium-to-long term recommendation is to use Network framework instead. This post explains how you might move from Multipeer Connectivity to Network framework. If you have questions or comments, put them in a new thread. Place it in the App & System Services > Networking topic area and tag it with Multipeer Connectivity and Network framework. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Moving from Multipeer Connectivity to Network Framework Multipeer Connectivity has a number of drawbacks: It has an opinionated networking model, where every participant in a session is a symmetric peer. Many apps work better with the traditional client/server model. It offers good latency but poor throughput. It doesn’t support flow control, aka back pressure, which severely constrains its utility for general-purpose networking. It includes a number of UI components that are effectively obsolete. It hasn’t evolved in recent years. For example, it relies on NSStream, which has been scheduled for deprecation as far as networking is concerned. It always enables peer-to-peer Wi-Fi, something that’s not required for many apps and can impact the performance of the network (see Enable peer-to-peer Wi-Fi, below, for more about this). Its security model requires the use of PKI — public key infrastructure, that is, digital identities and certificates — which are tricky to deploy in a peer-to-peer environment. It has some gnarly bugs. IMPORTANT Many folks use Multipeer Connectivity because they think it’s the only way to use peer-to-peer Wi-Fi. That’s not the case. Network framework has opt-in peer-to-peer Wi-Fi support. See Enable peer-to-peer Wi-Fi, below. If Multipeer Connectivity is not working well for you, consider moving to Network framework. This post explains how to do that in 13 easy steps (-: Plan for security Select a network architecture Create a peer identifier Choose a protocol to match your send mode Discover peers Design for privacy Configure your connections Manage a listener Manage a connection Send and receive reliable messages Send and receive best effort messages Start a stream Send a resource Finally, at the end of the post you’ll find two appendices: Final notes contains some general hints and tips. Symbol cross reference maps symbols in the Multipeer Connectivity framework to sections of this post. Consult it if you’re not sure where to start with a specific Multipeer Connectivity construct. Plan for security The first thing you need to think about is security. Multipeer Connectivity offers three security models, expressed as choices in the MCEncryptionPreference enum: .none for no security .optional for optional security .required for required security For required security each peer must have a digital identity. Optional security is largely pointless. It’s more complex than no security but doesn’t yield any benefits. So, in this post we’ll focus on the no security and required security models. Your security choice affects the network protocols you can use: QUIC is always secure. WebSocket, TCP, and UDP can be used with and without TLS security. QUIC security only supports PKI. TLS security supports both TLS-PKI and pre-shared key (PSK). You might find that TLS-PSK is easier to deploy in a peer-to-peer environment. To configure the security of the QUIC protocol: func quicParameters() -> NWParameters { let quic = NWProtocolQUIC.Options(alpn: ["MyAPLN"]) let sec = quic.securityProtocolOptions … configure `sec` here … return NWParameters(quic: quic) } To enable TLS over TCP: func tlsOverTCPParameters() -> NWParameters { let tcp = NWProtocolTCP.Options() let tls = NWProtocolTLS.Options() let sec = tls.securityProtocolOptions … configure `sec` here … return NWParameters(tls: tls, tcp: tcp) } To enable TLS over UDP, also known as DTLS: func dtlsOverUDPParameters() -> NWParameters { let udp = NWProtocolUDP.Options() let dtls = NWProtocolTLS.Options() let sec = dtls.securityProtocolOptions … configure `sec` here … return NWParameters(dtls: dtls, udp: udp) } To configure TLS with a local digital identity and custom server trust evaluation: func configureTLSPKI(sec: sec_protocol_options_t, identity: SecIdentity) { let secIdentity = sec_identity_create(identity)! sec_protocol_options_set_local_identity(sec, secIdentity) if disableServerTrustEvaluation { sec_protocol_options_set_verify_block(sec, { metadata, secTrust, completionHandler in let trust = sec_trust_copy_ref(secTrust).takeRetainedValue() … evaluate `trust` here … completionHandler(true) }, .main) } } To configure TLS with a pre-shared key: func configureTLSPSK(sec: sec_protocol_options_t, identity: Data, key: Data) { let identityDD = identity.withUnsafeBytes { DispatchData(bytes: $0) } let keyDD = identity.withUnsafeBytes { DispatchData(bytes: $0) } sec_protocol_options_add_pre_shared_key( sec, keyDD as dispatch_data_t, identityDD as dispatch_data_t ) sec_protocol_options_append_tls_ciphersuite( sec, tls_ciphersuite_t(rawValue: TLS_PSK_WITH_AES_128_GCM_SHA256)! ) } Select a network architecture Multipeer Connectivity uses a star network architecture. All peers are equal, and every peer is effectively connected to every peer. Many apps work better with the client/server model, where one peer acts on the server and all the others are clients. Network framework supports both models. To implement a client/server network architecture with Network framework: Designate one peer as the server and all the others as clients. On the server, use NWListener to listen for incoming connections. On each client, use NWConnection to made an outgoing connection to the server. To implement a star network architecture with Network framework: On each peer, start a listener. And also start a connection to each of the other peers. This is likely to generate a lot of redundant connections, as peer A connects to peer B and vice versa. You’ll need to a way to deduplicate those connections, which is the subject of the next section. IMPORTANT While the star network architecture is more likely to create redundant connections, the client/server network architecture can generate redundant connections as well. The advice in the next section applies to both architectures. Create a peer identifier Multipeer Connectivity uses MCPeerID to uniquely identify each peer. There’s nothing particularly magic about MCPeerID; it’s effectively a wrapper around a large random number. To identify each peer in Network framework, generate your own large random number. One good choice for a peer identifier is a locally generated UUID, created using the system UUID type. Some Multipeer Connectivity apps persist their local MCPeerID value, taking advantage of its NSSecureCoding support. You can do the same with a UUID, using either its string representation or its Codable support. IMPORTANT Before you decide to persist a peer identifier, think about the privacy implications. See Design for privacy below. Avoid having multiple connections between peers; that’s both wasteful and potentially confusing. Use your peer identifier to deduplicate connections. Deduplicating connections in a client/server network architecture is easy. Have each client check in with the server with its peer identifier. If the server already has a connection for that identifier, it can either close the old connection and keep the new connection, or vice versa. Deduplicating connections in a star network architecture is a bit trickier. One option is to have each peer send its peer identifier to the other peer and then the peer with the ‘best’ identifier wins. For example, imagine that peer A makes an outgoing connection to peer B while peer B is simultaneously making an outgoing connection to peer A. When a peer receives a peer identifier from a connection, it checks for a duplicate. If it finds one, it compares the peer identifiers and then chooses a connection to drop based on that comparison: if local peer identifier > remote peer identifier then drop outgoing connection else drop incoming connection end if So, peer A drops its incoming connection and peer B drops its outgoing connection. Et voilà! Choose a protocol to match your send mode Multipeer Connectivity offers two send modes, expressed as choices in the MCSessionSendDataMode enum: .reliable for reliable messages .unreliable for best effort messages Best effort is useful when sending latency-sensitive data, that is, data where retransmission is pointless because, by the retransmission arrives, the data will no longer be relevant. This is common in audio and video applications. In Network framework, the send mode is set by the connection’s protocol: A specific QUIC connection is either reliable or best effort. WebSocket and TCP are reliable. UDP is best effort. Start with a reliable connection. In many cases you can stop there, because you never need a best effort connection. If you’re not sure which reliable protocol to use, choose WebSocket. It has key advantages over other protocols: It supports both security models: none and required. Moreover, its required security model supports both TLS-PKI and TLS PSK. In contrast, QUIC only supports the required security model, and within that model it only supports TLS-PKI. It allows you to send messages over the connection. In contrast, TCP works in terms of bytes, meaning that you have to add your own framing. If you need a best effort connection, get started with a reliable connection and use that connection to set up a parallel best effort connection. For example, you might have an exchange like this: Peer A uses its reliable WebSocket connection to peer B to send a request for a parallel best effort UDP connection. Peer B receives that, opens a UDP listener, and sends the UDP listener’s port number back to peer A. Peer A opens its parallel UDP connection to that port on peer B. Note For step 3, get peer B’s IP address from the currentPath property of the reliable WebSocket connection. If you’re not sure which best effort protocol to use, use UDP. While it is possible to use QUIC in datagram mode, it has the same security complexities as QUIC in reliable mode. Discover peers Multipeer Connectivity has a types for advertising a peer’s session (MCAdvertiserAssistant) and a type for browsering for peer (MCNearbyServiceBrowser). In Network framework, configure the listener to advertise its service by setting the service property of NWListener: let listener: NWListener = … listener.service = .init(type: "_example._tcp") listener.serviceRegistrationUpdateHandler = { change in switch change { case .add(let endpoint): … update UI for the added listener endpoint … break case .remove(let endpoint): … update UI for the removed listener endpoint … break @unknown default: break } } listener.stateUpdateHandler = … handle state changes … listener.newConnectionHandler = … handle the new connection … listener.start(queue: .main) This example also shows how to use the serviceRegistrationUpdateHandler to update your UI to reflect changes in the listener. Note This example uses a service type of _example._tcp. See About service types, below, for more details on that. To browse for services, use NWBrowser: let browser = NWBrowser(for: .bonjour(type: "_example._tcp", domain: nil), using: .tcp) browser.browseResultsChangedHandler = { latestResults, _ in … update UI to show the latest results … } browser.stateUpdateHandler = … handle state changes … browser.start(queue: .main) This yields NWEndpoint values for each peer that it discovers. To connect to a given peer, create an NWConnection with that endpoint. About service types The examples in this post use _example._tcp for the service type. The first part, _example, is directly analogous to the serviceType value you supply when creating MCAdvertiserAssistant and MCNearbyServiceBrowser objects. The second part is either _tcp or _udp depending on the underlying transport protocol. For TCP and WebSocket, use _tcp. For UDP and QUIC, use _udp. Service types are described in RFC 6335. If you deploy an app that uses a new service type, register that service type with IANA. Discovery UI Multipeer Connectivity also has UI components for advertising (MCNearbyServiceAdvertiser) and browsing (MCBrowserViewController). There’s no direct equivalent to this in Network framework. Instead, use your preferred UI framework to create a UI that best suits your requirements. Note If you’re targeting Apple TV, check out the DeviceDiscoveryUI framework. Discovery TXT records The Bonjour service discovery protocol used by Network framework supports TXT records. Using these, a listener can associate metadata with its service and a browser can get that metadata for each discovered service. To advertise a TXT record with your listener, include it it the service property value: let listener: NWListener = … let peerID: UUID = … var txtRecord = NWTXTRecord() txtRecord["peerID"] = peerID.uuidString listener.service = .init(type: "_example._tcp", txtRecord: txtRecord.data) To browse for services and their associated TXT records, use the .bonjourWithTXTRecord(…) descriptor: let browser = NWBrowser(for: .bonjourWithTXTRecord(type: "_example._tcp", domain: nil), using: .tcp) browser.browseResultsChangedHandler = { latestResults, _ in for result in latestResults { guard case .bonjour(let txtRecord) = result.metadata, let peerID = txtRecord["peerID"] else { continue } // … examine `result` and `peerID` … _ = peerID } } This example includes the peer identifier in the TXT record with the goal of reducing the number of duplicate connections, but that’s just one potential use for TXT records. Design for privacy This section lists some privacy topics to consider as you implement your app. Obviously this isn’t an exhaustive list. For general advice on this topic, see Protecting the User’s Privacy. There can be no privacy without security. If you didn’t opt in to security with Multipeer Connectivity because you didn’t want to deal with PKI, consider the TLS-PSK options offered by Network framework. For more on this topic, see Plan for security. When you advertise a service, the default behaviour is to use the user-assigned device name as the service name. To override that, create a service with a custom name: let listener: NWListener = … let name: String = … listener.service = .init(name: name, type: "_example._tcp") It’s not uncommon for folks to use the peer identifier as the service name. Whether that’s a good option depends on the user experience of your product: Some products present a list of remote peers and have the user choose from that list. In that case it’s best to stick with the user-assigned device name, because that’s what the user will recognise. Some products automatically connect to services as they discover them. In that case it’s fine to use the peer identifier as the service name, because the user won’t see it anyway. If you stick with the user-assigned device name, consider advertising the peer identifier in your TXT record. See Discovery TXT records. IMPORTANT Using a peer identifier in your service name or TXT record is a heuristic to reduce the number of duplicate connections. Don’t rely on it for correctness. Rather, deduplicate connections using the process described in Create a peer identifier. There are good reasons to persist your peer identifier, but doing so isn’t great for privacy. Persisting the identifier allows for tracking of your service over time and between networks. Consider whether you need a persistent peer identifier at all. If you do, consider whether it makes sense to rotate it over time. A persistent peer identifier is especially worrying if you use it as your service name or put it in your TXT record. Configure your connections Multipeer Connectivity’s symmetric architecture means that it uses a single type, MCSession, to manage the connections to all peers. In Network framework, that role is fulfilled by two types: NWListener to listen for incoming connections. NWConnection to make outgoing connections. Both types require you to supply an NWParameters value that specifies the network protocol and options to use. In addition, when creating an NWConnection you pass in an NWEndpoint to tell it the service to connect to. For example, here’s how to configure a very simple listener for TCP: let parameters = NWParameters.tcp let listener = try NWListener(using: parameters) … continue setting up the listener … And here’s how you might configure an outgoing TCP connection: let parameters = NWParameters.tcp let endpoint = NWEndpoint.hostPort(host: "example.com", port: 80) let connection = NWConnection.init(to: endpoint, using: parameters) … continue setting up the connection … NWParameters has properties to control exactly what protocol to use and what options to use with those protocols. To work with QUIC connections, use code like that shown in the quicParameters() example from the Security section earlier in this post. To work with TCP connections, use the NWParameters.tcp property as shown above. To enable TLS on your TCP connections, use code like that shown in the tlsOverTCPParameters() example from the Security section earlier in this post. To work with WebSocket connections, insert it into the application protocols array: let parameters = NWParameters.tcp let ws = NWProtocolWebSocket.Options(.version13) parameters.defaultProtocolStack.applicationProtocols.insert(ws, at: 0) To enable TLS on your WebSocket connections, use code like that shown in the tlsOverTCPParameters() example to create your base parameters and then add the WebSocket application protocol to that. To work with UDP connections, use the NWParameters.udp property: let parameters = NWParameters.udp To enable TLS on your UDP connections, use code like that shown in the dtlsOverUDPParameters() example from the Security section earlier in this post. Enable peer-to-peer Wi-Fi By default, Network framework doesn’t use peer-to-peer Wi-Fi. To enable that, set the includePeerToPeer property on the parameters used to create your listener and connection objects. parameters.includePeerToPeer = true IMPORTANT Enabling peer-to-peer Wi-Fi can impact the performance of the network. Only opt into it if it’s a significant benefit to your app. If you enable peer-to-peer Wi-Fi, it’s critical to stop network operations as soon as you’re done with them. For example, if you’re browsing for services with peer-to-peer Wi-Fi enabled and the user picks a service, stop the browse operation immediately. Otherwise, the ongoing browse operation might affect the performance of your connection. Manage a listener In Network framework, use NWListener to listen for incoming connections: let parameters: NWParameters = .tcp … configure parameters … let listener = try NWListener(using: parameters) listener.service = … service details … listener.serviceRegistrationUpdateHandler = … handle service registration changes … listener.stateUpdateHandler = { newState in … handle state changes … } listener.newConnectionHandler = { newConnection in … handle the new connection … } listener.start(queue: .main) For details on how to set up parameters, see Configure your connections. For details on how to set up up service and serviceRegistrationUpdateHandler, see Discover peers. Network framework calls your state update handler when the listener changes state: let listener: NWListener = … listener.stateUpdateHandler = { newState in switch newState { case .setup: // The listener has not yet started. … case .waiting(let error): // The listener tried to start and failed. It might recover in the // future. … case .ready: // The listener is running. … case .failed(let error): // The listener tried to start and failed irrecoverably. … case .cancelled: // The listener was cancelled by you. … @unknown default: break } } Network framework calls your new connection handler when a client connects to it: var connections: [NWConnection] = [] let listener: NWListener = listener listener.newConnectionHandler = { newConnection in … configure the new connection … newConnection.start(queue: .main) connections.append(newConnection) } IMPORTANT Don’t forget to call start(queue:) on your connections. In Multipeer Connectivity, the session (MCSession) keeps track of all the peers you’re communicating with. With Network framework, that responsibility falls on you. This example uses a simple connections array for that purpose. In your app you may or may not need a more complex data structure. For example: In the client/server network architecture, the client only needs to manage the connections to a single peer, the server. On the other hand, the server must managed the connections to all client peers. In the star network architecture, every peer must maintain a listener and connections to each of the other peers. Understand UDP flows Network framework handles UDP using the same NWListener and NWConnection types as it uses for TCP. However, the underlying UDP protocol is not implemented in terms of listeners and connections. To resolve this, Network framework works in terms of UDP flows. A UDP flow is defined as a bidirectional sequence of UDP datagrams with the same 4 tuple (local IP address, local port, remote IP address, and remote port). In Network framework: Each NWConnection object manages a single UDP flow. If an NWListener receives a UDP datagram whose 4 tuple doesn’t match any known NWConnection, it creates a new NWConnection. Manage a connection In Network framework, use NWConnection to start an outgoing connection: var connections: [NWConnection] = [] let parameters: NWParameters = … let endpoint: NWEndpoint = … let connection = NWConnection(to: endpoint, using: parameters) connection.stateUpdateHandler = … handle state changes … connection.viabilityUpdateHandler = … handle viability changes … connection.pathUpdateHandler = … handle path changes … connection.betterPathUpdateHandler = … handle better path notifications … connection.start(queue: .main) connections.append(connection) As in the listener case, you’re responsible for keeping track of this connection. Each connection supports four different handlers. Of these, the state and viability update handlers are the most important. For information about the path update and better path handlers, see the NWConnection documentation. Network framework calls your state update handler when the connection changes state: let connection: NWConnection = … connection.stateUpdateHandler = { newState in switch newState { case .setup: // The connection has not yet started. … case .preparing: // The connection is starting. … case .waiting(let error): // The connection tried to start and failed. It might recover in the // future. … case .ready: // The connection is running. … case .failed(let error): // The connection tried to start and failed irrecoverably. … case .cancelled: // The connection was cancelled by you. … @unknown default: break } } If you a connection is in the .waiting(_:) state and you want to force an immediate retry, call the restart() method. Network framework calls your viability update handler when its viability changes: let connection: NWConnection = … connection.viabilityUpdateHandler = { isViable in … react to viability changes … } A connection becomes inviable when a network resource that it depends on is unavailable. A good example of this is the network interface that the connection is running over. If you have a connection running over Wi-Fi, and the user turns off Wi-Fi or moves out of range of their Wi-Fi network, any connection running over Wi-Fi becomes inviable. The inviable state is not necessarily permanent. To continue the above example, the user might re-enable Wi-Fi or move back into range of their Wi-Fi network. If the connection becomes viable again, Network framework calls your viability update handler with a true value. It’s a good idea to debounce the viability handler. If the connection becomes inviable, don’t close it down immediately. Rather, wait for a short while to see if it becomes viable again. If a connection has been inviable for a while, you get to choose as to how to respond. For example, you might close the connection down or inform the user. To close a connection, call the cancel() method. This gracefully disconnects the underlying network connection. To close a connection immediately, call the forceCancel() method. This is not something you should do as a matter of course, but it does make sense in exceptional circumstances. For example, if you’ve determined that the remote peer has gone deaf, it makes sense to cancel it in this way. Send and receive reliable messages In Multipeer Connectivity, a single session supports both reliable and best effort send modes. In Network framework, a connection is either reliable or best effort, depending on the underlying network protocol. The exact mechanism for sending a message depends on the underlying network protocol. A good protocol for reliable messages is WebSocket. To send a message on a WebSocket connection: let connection: NWConnection = … let message: Data = … let metadata = NWProtocolWebSocket.Metadata(opcode: .binary) let context = NWConnection.ContentContext(identifier: "send", metadata: [metadata]) connection.send(content: message, contentContext: context, completion: .contentProcessed({ error in // … check `error` … _ = error })) In WebSocket, the content identifier is ignored. Using an arbitrary fixed value, like the send in this example, is just fine. Multipeer Connectivity allows you to send a message to multiple peers in a single send call. In Network framework each send call targets a specific connection. To send a message to multiple peers, make a send call on the connection associated with each peer. If your app needs to transfer arbitrary amounts of data on a connection, it must implement flow control. See Start a stream, below. To receive messages on a WebSocket connection: func startWebSocketReceive(on connection: NWConnection) { connection.receiveMessage { message, _, _, error in if let error { … handle the error … return } if let message { … handle the incoming message … } startWebSocketReceive(on: connection) } } IMPORTANT WebSocket preserves message boundaries, which is one of the reasons why it’s ideal for your reliable messaging connections. If you use a streaming protocol, like TCP or QUIC streams, you must do your own framing. A good way to do that is with NWProtocolFramer. If you need the metadata associated with the message, get it from the context parameter: connection.receiveMessage { message, context, _, error in … if let message, let metadata = context?.protocolMetadata(definition: NWProtocolWebSocket.definition) as? NWProtocolWebSocket.Metadata { … handle the incoming message and its metadata … } … } Send and receive best effort messages In Multipeer Connectivity, a single session supports both reliable and best effort send modes. In Network framework, a connection is either reliable or best effort, depending on the underlying network protocol. The exact mechanism for sending a message depends on the underlying network protocol. A good protocol for best effort messages is UDP. To send a message on a UDP connection: let connection: NWConnection = … let message: Data = … connection.send(content: message, completion: .idempotent) IMPORTANT UDP datagrams have a theoretical maximum size of just under 64 KiB. However, sending a large datagram results in IP fragmentation, which is very inefficient. For this reason, Network framework prevents you from sending UDP datagrams that will be fragmented. To find the maximum supported datagram size for a connection, gets its maximumDatagramSize property. To receive messages on a UDP connection: func startUDPReceive(on connection: NWConnection) { connection.receiveMessage { message, _, _, error in if let error { … handle the error … return } if let message { … handle the incoming message … } startUDPReceive(on: connection) } } This is exactly the same code as you’d use for WebSocket. Start a stream In Multipeer Connectivity, you can ask the session to start a stream to a specific peer. There are two ways to achieve this in Network framework: If you’re using QUIC for your reliable connection, start a new QUIC stream over that connection. This is one place that QUIC shines. You can run an arbitrary number of QUIC connections over a single QUIC connection group, and QUIC manages flow control (see below) for each connection and for the group as a whole. If you’re using some other protocol for your reliable connection, like WebSocket, you must start a new connection. You might use TCP for this new connection, but it’s not unreasonable to use WebSocket or QUIC. If you need to open a new connection for your stream, you can manage that process over your reliable connection. Choose a protocol to match your send mode explains the general approach for this, although in that case it’s opening a parallel best effort UDP connection rather than a parallel stream connection. The main reason to start a new stream is that you want to send a lot of data to the remote peer. In that case you need to worry about flow control. Flow control applies to both the send and receive side. IMPORTANT Failing to implement flow control can result in unbounded memory growth in your app. This is particularly bad on iOS, where jetsam will terminate your app if it uses too much memory. On the send side, implement flow control by waiting for the connection to call your completion handler before generating and sending more data. For example, on a TCP connection or QUIC stream you might have code like this: func sendNextChunk(on connection: NWConnection) { let chunk: Data = … read next chunk from disk … connection.send(content: chunk, completion: .contentProcessed({ error in if let error { … handle error … return } sendNextChunk(on: connection) })) } This acts like an asynchronous loop. The first send call completes immediately because the connection just copies the data to its send buffer. In response, your app generates more data. This continues until the connection’s send buffer fills up, at which point it defers calling your completion handler. Eventually, the connection moves enough data across the network to free up space in its send buffer, and calls your completion handler. Your app generates another chunk of data For best performance, use a chunk size of at least 64 KiB. If you’re expecting to run on a fast device with a fast network, a chunk size of 1 MiB is reasonable. Receive-side flow control is a natural extension of the standard receive pattern. For example, on a TCP connection or QUIC stream you might have code like this: func receiveNextChunk(on connection: NWConnection) { let chunkSize = 64 * 1024 connection.receive(minimumIncompleteLength: chunkSize, maximumLength: chunkSize) { chunk, _, isComplete, error in if let chunk { … write chunk to disk … } if isComplete { … close the file … return } if let error { … handle the error … return } receiveNextChunk(on: connection) } } IMPORTANT The above is cast in terms of writing the chunk to disk. That’s important, because it prevents unbounded memory growth. If, for example, you accumulated the chunks into an in-memory buffer, that buffer could grow without bound, which risks jetsam terminating your app. The above assumes that you can read and write chunks of data synchronously and promptly, for example, reading and writing a file on a local disk. That’s not always the case. For example, you might be writing data to an accessory over a slow interface, like Bluetooth LE. In such cases you need to read and write each chunk asynchronously. This results in a structure where you read from an asynchronous input and write to an asynchronous output. For an example of how you might approach this, albeit in a very different context, see Handling Flow Copying. Send a resource In Multipeer Connectivity, you can ask the session to send a complete resource, identified by either a file or HTTP URL, to a specific peer. Network framework has no equivalent support for this, but you can implement it on top of a stream: To send, open a stream and then read chunks of data using URLSession and send them over that stream. To receive, open a stream and then receive chunks of data from that stream and write those chunks to disk. In this situation it’s critical to implement flow control, as described in the previous section. Final notes This section collects together some general hints and tips. Concurrency In Multipeer Connectivity, each MCSession has its own internal queue and calls delegate callbacks on that queue. In Network framework, you get to control the queue used by each object for its callbacks. A good pattern is to have a single serial queue for all networking, including your listener and all connections. In a simple app it’s reasonable to use the main queue for networking. If you do this, be careful not to do CPU intensive work in your networking callbacks. For example, if you receive a message that holds JPEG data, don’t decode that data on the main queue. Overriding protocol defaults Many network protocols, most notably TCP and QUIC, are intended to be deployed at vast scale across the wider Internet. For that reason they use default options that aren’t optimised for local networking. Consider changing these defaults in your app. TCP has the concept of a send timeout. If you send data on a TCP connection and TCP is unable to successfully transfer it to the remote peer within the send timeout, TCP will fail the connection. The default send timeout is infinite. TCP just keeps trying. To change this, set the connectionDropTime property. TCP has the concept of keepalives. If a connection is idle, TCP will send traffic on the connection for two reasons: If the connection is running through a NAT, the keepalives prevent the NAT mapping from timing out. If the remote peer is inaccessible, the keepalives fail, which in turn causes the connection to fail. This prevents idle but dead connections from lingering indefinitely. TCP keepalives default to disabled. To enable and configure them, set the enableKeepalive property. To configure their behaviour, set the keepaliveIdle, keepaliveCount, and keepaliveInterval properties. Symbol cross reference If you’re not sure where to start with a specific Multipeer Connectivity construct, find it in the tables below and follow the link to the relevant section. [Sorry for the poor formatting here. DevForums doesn’t support tables properly, so I’ve included the tables as preformatted text.] | For symbol | See | | ----------------------------------- | --------------------------- | | `MCAdvertiserAssistant` | *Discover peers* | | `MCAdvertiserAssistantDelegate` | *Discover peers* | | `MCBrowserViewController` | *Discover peers* | | `MCBrowserViewControllerDelegate` | *Discover peers* | | `MCNearbyServiceAdvertiser` | *Discover peers* | | `MCNearbyServiceAdvertiserDelegate` | *Discover peers* | | `MCNearbyServiceBrowser` | *Discover peers* | | `MCNearbyServiceBrowserDelegate` | *Discover peers* | | `MCPeerID` | *Create a peer identifier* | | `MCSession` | See below. | | `MCSessionDelegate` | See below. | Within MCSession: | For symbol | See | | --------------------------------------------------------- | ------------------------------------ | | `cancelConnectPeer(_:)` | *Manage a connection* | | `connectedPeers` | *Manage a listener* | | `connectPeer(_:withNearbyConnectionData:)` | *Manage a connection* | | `disconnect()` | *Manage a connection* | | `encryptionPreference` | *Plan for security* | | `myPeerID` | *Create a peer identifier* | | `nearbyConnectionData(forPeer:withCompletionHandler:)` | *Discover peers* | | `securityIdentity` | *Plan for security* | | `send(_:toPeers:with:)` | *Send and receive reliable messages* | | `sendResource(at:withName:toPeer:withCompletionHandler:)` | *Send a resource* | | `startStream(withName:toPeer:)` | *Start a stream* | Within MCSessionDelegate: | For symbol | See | | ---------------------------------------------------------------------- | ------------------------------------ | | `session(_:didFinishReceivingResourceWithName:fromPeer:at:withError:)` | *Send a resource* | | `session(_:didReceive:fromPeer:)` | *Send and receive reliable messages* | | `session(_:didReceive:withName:fromPeer:)` | *Start a stream* | | `session(_:didReceiveCertificate:fromPeer:certificateHandler:)` | *Plan for security* | | `session(_:didStartReceivingResourceWithName:fromPeer:with:)` | *Send a resource* | | `session(_:peer:didChange:)` | *Manage a connection* | Revision History 2025-04-11 Added some advice as to whether to use the peer identifier in your service name. Expanded the discussion of how to deduplicate connections in a star network architecture. 2025-03-20 Added a link to the DeviceDiscoveryUI framework to the Discovery UI section. Made other minor editorial changes. 2025-03-11 Expanded the Enable peer-to-peer Wi-Fi section to stress the importance of stopping network operations once you’re done with them. Added a link to that section from the list of Multipeer Connectivity drawbacks. 2025-03-07 First posted.
0
0
1.8k
Apr ’25
Question about listen on the utun interface to serve as system dns server
Hello, Recently I am trying to add stub dns server to my Network Extension (a VPN app), after some research on this forum, and since my language is C, I have the following plan: create a udp socket which use setsockopt(IP_BOUND_IF) to bound the socket to the utun if index obtained, and also bind to the address of the utun address I set(let's say 192.168.99.2), then listen on the udp port 53 which is ready to handle dns request. configure the dns server to 192.168.99.2 in the provider's Network Settings, thus iOS system will send udp query to the udp socket created in step 1, and it can then do some split dns function such as resolve using local interface (cellular or wifi), or some nameserve which will be routed to the VPN tunnel (will create new UDP socket and do IP_BOUND_IF to ensure the traffic will enter the VPN tunnel), and the result should be return to the system and then the non VPP apps. But I observer weird issue, indeed I can get the system send the dns request to the listening udp socket and I can get the result write to the system(address like 192.168.99.2:56144, the port should be allocated by the iOS system's DNS component) without any failure(I did get some error before due to I using the wrong utun if index, but fixed it later), but it seems non VPN app like browser can't get the resolved ip for domains. I want to ask is this limited by the sandbox? or any special sock opt I need to do. Thanks. PS: in the provider's network settings, all the system's traffic will be point to the utun, which means the VPN process will process all the traffic. the reason I do not set the dns server to utun peers side which is my userspace networking stack's ip (192.168.99.1) is the stack is not be able to leverage some dns libraries due to architecture issue. (it's fd.io vpp which we ported to apple platform).
7
0
191
Apr ’25
When restarting a PC with macOS15, communication on the local network is denied for the target app even though "Local Network" in "Privacy and Security" is allowed.
We are developing a desktop app for macOS, built with Electron + Angular + Node.js. The app has the ability to access devices on the local network. We are having some trouble with the behavior of the "Local Network" feature added in macOS 15. Procedure: MacBook Pro 14inch (M1Pro) 2021, 16GB memory - macOS Sequoia15.4.1 Network Environment: The PC and the access point are connected by wire, and the target device and the access point are also connected by wire. Not connected to an external network Install the target app Start the application and allow the "Local Network" dialog that appears Open the "Local Network" item in "Privacy and Security" and make sure the target app is allowed. Confirm that you can access the target device on the local network Restart the PC Open the "Local Network" item in "Privacy and Security" and make sure the target app is allowed. Start the app and access the target device on the local network Result: Access to the target device may fail (this does not occur 100% of the time) If you turn the target app in the "Local Network" section of "Privacy and Security" OFF and then ON, you will be able to connect However, it seems that the problem may occur again if you restart your PC. According to our research, the probability of this happening increases if the target app is the only app allowed on the "local network". This happens not only with our app, but also when you follow the above steps in "Google Chrome". Based on the above investigation results, I suspect that this is a problem with the OS. What do you think?
Replies
2
Boosts
0
Views
150
Activity
May ’25
intermittent multicast socket failures, new to Sequoia, still not fixed
multicast sockets fail to send/receive on macosx, errno 65 "no route to host". Wireshark and Terminal.app (which have root privileges) both show incoming multicast traffic just fine. Normal UDP broadcast sockets have no problems. Toggling the Security&Privacy -> Local Network setting may fix the problem for some Users. There is no pattern for when multicast socket fails. Sometimes, recreating the sockets fix the problem. Restart the app, sometimes multicast fails, sometimes success (intermittent, no pattern). Reboot machine (intermittent fail) Create a fresh new user on machine, install single version of app, give app permission. (intermittent fail, same as above). We have all the normal entitlements / notarized app. Similar posts here see FB16923535, Related to FB16512666 https://forum.xojo.com/t/udp-multicast-receive-on-mac-failing-intermittant/83221 see my post from 2012 "distinguishing between SENDING sockets and RECEIVING sockets" for source code example of how we bind multicast sockets. Our other socket code is standard "Stevens, et al." code. The bind() is the call that fails in this case. https://stackoverflow.com/questions/10692956/what-does-it-mean-to-bind-a-multicast-udp-socket . Note that this post from 2012 is still relevant, and that it is a workaround to a longstanding Apple bug that was never fixed. Namely, "Without this fix, multicast sending will intermittently get sendto() errno 'No route to host'. If anyone can shed light on why unplugging a DHCP gateway causes Mac OS X multicast SENDING sockets to get confused, I would love to hear it." This may be a hint as to the underlying bug that Apple really needs to fix, but if it's not, then please Apple, fix the Sequoia bug first. These are probably different bugs because in one case, sendto() fails when a socket becomes "unbound" after you unplug an unrelated network cable. In this case, bind() fails, so sendto() is never even called. Note, that we have also tried to use other implementations for network discovery, including Bonjour, CFNetwork, etc. Bonjour fails intermittently, and also suffers from both bugs mentioned above, amongst others.
Replies
3
Boosts
0
Views
116
Activity
May ’25
My app suddenly getting "A server with the specified hostname could not be found"
I've had no problem running my app in a simulator or on a device, but today my app is failing on a URLRequest to my local machine (in a sim). From the same simulator I can go to Safari and manually enter the URL that the app is using (and that appears in the error message), and it works fine. I think there was a recent Xcode update; did something change in this regard?
Replies
6
Boosts
0
Views
258
Activity
May ’25
Determine outgoing flow source IPs without allowing data leakage using NEFilterDataProvider
I'm looking for help with a network extension filtering issue. Specifically, we have a subclass of NEFilterDataProvider that is used to filter flows based upon a set of rules, including source IP and destination IP. We've run into an issue where the source IP is frequently 0.0.0.0 (or the IPv6 equivalent) on outgoing flows. This has made it so rules based upon source IP don't work. This is also an issue as we report these connections, but we're lacking critical data. We were able to work around the issue somewhat by keeping a list of flows that we allow that we periodically check to see if the source IP is available, and then report after it becomes available. We also considered doing a "peekBytes" to allow a bit of data to flow and then recheck the flow, but we don't want to allow data leakage on connections that should be blocked because of the source IP. Is there a way to force the operating system or network extension frameworks to determine the source IP for an outbound flow without allowing any bytes to flow to the network? STEPS TO REPRODUCE Create a network filtering extension for filtering flows using NEFilterDataProvider See that when handleNewFlow: is called, the outgoing flow lacks the source IP (is 0.0.0.0) in most cases There is this post that is discussing a similar question, though for a slightly different reason. I imagine the answer to this and the other post will be related, at least as far as NEFilterDataProvider:handleNewFlow not having source IP is considered. Thanks!
Replies
1
Boosts
0
Views
185
Activity
Apr ’25
DNS Resolution fails in 15.4
Hi, DNS resolution using libresolv (res_nquery) fails in 15.4 when connected to VPN. The same is working fine for 15.3 and lower and this happens for all the domains. The method returns -1 and res->res_h_errno is set to 2. In wireshark we can see that the DNS request is sent and server also returns the response successfully. The same works fine if we use TCP instead of UDP by setting the following option res->options |= RES_USEVC;
Replies
5
Boosts
0
Views
226
Activity
Apr ’25
Using Cellular Data While Connected to Wifi
Hello, A quick background: I am developing an App that receives a data stream from a device through its Wi-Fi network. The device itself is not connected to the internet, so the app won't be either. Now, I am adding a new feature to the App that would require internet connection during the data stream. Consequently, my users would need to use their cellular data. On later versions of iPhone, the phone would occasionally detect the lack of internet connection and asks the user via a pop-up if they want to use their cellular data. However, this behavior is not consistent. So my question is- can we programmatically invoke this pop-up so the user can connect to the internet? Or even better- can we program the App to use cellular data while still being connected to a Wi-Fi network? Note: I have seen mixed answers on the internet whether this is doable or not, and I know that users are able do it themselves by manually configuring their IP in their WiFi settings page, but I doubt this operation can be done through the App for security reasons. Thanks!
Replies
4
Boosts
0
Views
3k
Activity
Apr ’25
Use cellular data on the app while connected to hardware's wifi that doesn't have internet connection
Hello, I am in a very similar situation as described in the thread: https://developer.apple.com/forums/thread/655183 Context: I am working on an app that receives data from a hardware device through its Wifi network, and the hardware is not connected to the internet. Now, I would need to call some API while still connected to hardware so I would need to use the cellular data. As mentioned on the thread, I can achieve this via Network framework, using the requiredInterfaceType property. But Is there any other way I can achieve this? I can also do some suggestion on the hardware if that's helpful. Thank you!
Replies
1
Boosts
0
Views
202
Activity
Apr ’25
Understanding Also-Ran Connections
Every now and again folks notice that Network framework seems to create an unexpected number of connections on the wire. This post explains why that happens and what you should do about it. If you have questions or comments, put them in a new thread here on the forums. Use the App & System Services > Networking topic area and the Network tag. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Understanding Also-Ran Connections Network framework implements the Happy Eyeballs algorithm. That might create more on-the-wire connections than you expect. There are two common places where folks notice this: When looking at a packet trace When implementing a listener Imagine that you’ve implemented a TCP server using NWListener and you connect to it from a client using NWConnection. In many situations there are multiple network paths between the client and the server. For example, on a local network there’s always at least two paths: the link-local IPv6 path and either an infrastructure IPv4 path or the link-local IPv4 path. When you start your NWConnection, Network framework’s Happy Eyeballs algorithm might [1] start a TCP connection for each of these paths. It then races those connections. The one that connects first is the ‘winner’, and Network framework uses that connection for your traffic. Once it has a winner, the other connections, the also-ran connections, are redundant, and Network framework just closes them. You can observe this behaviour on the client side by looking in the system log. Many Network framework log entries (subsystem com.apple.network) contain a connection identifier. For example C8 is the eighth connection started by this process. Each connection may have child connections (C8.1, C8.2, …) and grandchild connections (C8.1.1, C8.1.2, …), and so on. You’ll see state transitions for these child connections occurring in parallel. For example, the following log entries show that C8 is racing the connection of two grandchild connections, C8.1.1 and C8.1.2: type: debug time: 12:22:26.825331+0100 process: TestAlsoRanConnections subsystem: com.apple.network category: connection message: nw_socket_connect [C8.1.1:1] Calling connectx(…) type: debug time: 12:22:26.964150+0100 process: TestAlsoRanConnections subsystem: com.apple.network category: connection message: nw_socket_connect [C8.1.2:1] Calling connectx(…) Note For more information about accessing the system log, see Your Friend the System Log. You also see this on the server side, but in this case each connection is visible to your code. When you connect from the client, Network framework calls your listener’s new connection handler with multiple connections. One of those is the winning connection and you’ll receive traffic on it. The others are the also-ran connections, and they close promptly. IMPORTANT Depending on network conditions there may be no also-ran connections. Or there may be lots of them. If you want to test the also-ran connection case, use Network Link Conditioner to add a bunch of delay to your packets. You don’t need to write special code to handle also-ran connections. From the perspective of your listener, these are simply connections that open and then immediately close. There’s no difference between an also-ran connection and, say, a connection from a client that immediately crashes. Or a connection generated by someone doing a port scan. Your server must be resilient to such things. However, the presence of these also-ran connections can be confusing, especially if you’re just getting started with Network framework, and hence this post. [1] This is “might” because the exact behaviour depends on network conditions. More on that below.
Replies
0
Boosts
0
Views
164
Activity
Apr ’25
iOS 18.4 key usage requirements fails TLS connections
iOS 18.4 introduced some requirements on the Key Usage of 802.1x server certificates, as described here. https://support.apple.com/en-us/121158 When using TLS_ECDHE_RSA or TLS_DHE_RSA cipher suites, 802.1X server certificates containing a Key Usage extension must have Digital Signature key usage set. When using the TLS_RSA cipher suite, 802.1X server certificates containing a Key Usage extension must have Key Encipherment key usage set. It reads like the change is supposed to affect 802.1x only. However, we have found out that the new restrictions are actually imposed on all TLS connections using the Network framework, including in Safari. Unlike other certificate errors which can be either ignored by users (as in Safari) or by code (via sec_protocol_options_set_verify_block), these new ones can't. Even if passing completion(true) in the TLS verification block, the connection still ends up in waiting state with error -9830: illegal parameter. I understand that these requirements are valid ones but as a generic TLS library I also expect that Network framework could at least allow overriding the behavior. The current treatment is not consistent with those on other certificate errors. Since I can't upload certificates, here is how to reproduce a certificate that fails. Create a OpenSSL config file test.cnf [ req ] default_bits = 2048 distinguished_name = dn x509_extensions = v3_ca prompt = no [ dn ] CN = example.com [ v3_ca ] subjectKeyIdentifier = hash authorityKeyIdentifier = keyid:always,issuer basicConstraints = CA:TRUE keyUsage = critical, keyCertSign, cRLSign Generate certificate and private key openssl req -x509 -new -nodes -keyout key.pem -out cert.pem -days 365 -config test.cnf And here is the client code to test. // Target server and port let host = NWEndpoint.Host("example.com") let port = NWEndpoint.Port("443")! // Configure insecure TLS options let tlsOptions = NWProtocolTLS.Options() sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { _, _, completion in // Always trust completion(true) }, DispatchQueue.global()) let params = NWParameters(tls: tlsOptions) let connection = NWConnection(host: .init(host), port: .init(rawValue: port)!, using: params) connection.stateUpdateHandler = { newState in switch newState { case .ready: print("TLS connection established") case .failed(let error): print("Connection failed: \(error)") case .cancelled: print("Connection canceled") case .preparing: print("Connection preparing") case .waiting(let error): print("Connection waiting: \(error)") case .setup: print("Connection setup") default: break } } connection.start(queue: .global()) Output Connection preparing Connection waiting: -9830: illegal parameter Previously reported as FB17099740
Replies
5
Boosts
0
Views
302
Activity
Apr ’25
use user ip address
Hi, is it legal to use open APIs to get the users's country country code using the Ip address in the app? I mean I want to know the user country for the game leaderboards data, and there are sites say this is free and open. So, I have two questions, first, is this making the user calling open api to get its country code concept legal? second question, what if these sites suddenly decided that it is not legal to use their apis for commercial use, and i miss that announcement; will you remove my app from the store? or what action will you take exactly?
Replies
1
Boosts
0
Views
150
Activity
Apr ’25
Level Networking on watchOS for Duplex audio streaming
I did watch WWDC 2019 Session 716 and understand that an active audio session is key to unlocking low‑level networking on watchOS. I’m configuring my audio session and engine as follows: private func configureAudioSession(completion: @escaping (Bool) -> Void) { let audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: []) try audioSession.setActive(true, options: .notifyOthersOnDeactivation) // Retrieve sample rate and configure the audio format. let sampleRate = audioSession.sampleRate print("Active hardware sample rate: \(sampleRate)") audioFormat = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1) // Configure the audio engine. audioInputNode = audioEngine.inputNode audioEngine.attach(audioPlayerNode) audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: audioFormat) try audioEngine.start() completion(true) } catch { print("Error configuring audio session: \(error.localizedDescription)") completion(false) } } private func setupUDPConnection() { let parameters = NWParameters.udp parameters.includePeerToPeer = true connection = NWConnection(host: "***.***.xxxxx.***", port: 0000, using: parameters) setupNWConnectionHandlers() } private func setupTCPConnection() { let parameters = NWParameters.tcp connection = NWConnection(host: "***.***.xxxxx.***", port: 0000, using: parameters) setupNWConnectionHandlers() } private func setupWebSocketConnection() { guard let url = URL(string: "ws://***.***.xxxxx.***:0000") else { print("Invalid WebSocket URL") return } let session = URLSession(configuration: .default) webSocketTask = session.webSocketTask(with: url) webSocketTask?.resume() print("WebSocket connection initiated") sendAudioToServer() receiveDataFromServer() sendWebSocketPing(after: 0.6) } private func setupNWConnectionHandlers() { connection?.stateUpdateHandler = { [weak self] state in DispatchQueue.main.async { switch state { case .ready: print("Connected (NWConnection)") self?.isConnected = true self?.failToConnect = false self?.receiveDataFromServer() self?.sendAudioToServer() case .waiting(let error), .failed(let error): print("Connection error: \(error.localizedDescription)") DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self?.setupNetwork() } case .cancelled: print("NWConnection cancelled") self?.isConnected = false default: break } } } connection?.start(queue: .main) } Duplex in this context refers to two-way audio transmission simultaneously recording and sending audio while also receiving and playing back incoming audio, similar to a VoIP/SIP call. The setup works fine on the simulator, which suggests that the core logic is correct. However, since the simulator doesn’t fully replicate WatchOS hardware behavior especially for audio sessions and networking issues might arise when running on a real device. The problem likely lies in either the Watch’s actual hardware limitations, permission constraints, or specific audio session configurations. I am reaching out to seek further assistance regarding the challenges I've been experiencing with establishing a UDP, TCP & web socket connection on watchOS using NWConnection for duplex audio streaming. Despite implementing the recommendations provided earlier, I am still encountering difficulties From what I can see, your implementation is focused on streaming audio playback with the server. In my case, I'm looking for a slightly different approach: I want to capture audio and send buffers of a specific size to the server while playing audio simultaneously, essentially achieving full duplex streaming similar to a VOIP call. Additionally, I’d like to ensure that if no external audio route is connected, the Apple Watch speaker is used by default. Any thoughts or insights on adapting this setup for those requirements would be very welcome.
Replies
1
Boosts
0
Views
257
Activity
Apr ’25
If UDP communication is used to obtain NTP, will it be approved by the Apple Store review?
There may be mistakes in the English translation. Please understand. I am developing a smartphone application using Unity and considering using a public NTP server to obtain the current time. During my research, I found an article stating that communications using UDP are blocked by Apple's review process. The article also mentioned that obtaining the time via HTTPS communication successfully passed Apple's review. If an app uses UDP communication to obtain the current time from NTP, will it fail the review process? When I built the app in Xcode and launched it, the current time obtained from NTP was displayed in the Xcode log, so I believe the communication is working properly. Below is the URL of the article I found. https://deep-verdure.hatenablog.com/entry/2020/05/06/050506
Replies
2
Boosts
0
Views
133
Activity
Apr ’25
Can NWBrowser be used in an AppIntent extension? NoAuth
I have an iOS app that connects to a server running on macOS by leveraging NWListener & NWBrowser. It also makes use of the peerToPeer functionality / AWDL offered via the Network framework. This works great in the iOS app. Now I would like to add support for Shortcuts / App Intents in general. The NWConnection on its own is also working great in the App Intent, but only if I provide the host/port manually, which means I can't use the peer to peer functionality. If I try to run my NWBrowser in the AppIntent it immediately changes its state to failed with a NoAuth (-65555) error: nw_browser_cancel [B1517] The browser has already been cancelled, ignoring nw_browser_cancel(). nw_browser_fail_on_dns_error_locked [B1518] DNSServiceBrowse failed: NoAuth(-65555) NWClientManager: Browser failed: -65555: NoAuth I haven't found documentation/information on whether NWBrowser should work in an AppIntent extension or not.
Replies
1
Boosts
0
Views
127
Activity
Apr ’25
Level Networking on watchOS for Duplex audio streaming
I did watch WWDC 2019 Session 716 and understand that an active audio session is key to unlocking low‑level networking on watchOS. I’m configuring my audio session and engine as follows: private func configureAudioSession(completion: @escaping (Bool) -> Void) { let audioSession = AVAudioSession.sharedInstance() do { try audioSession.setCategory(.playAndRecord, mode: .voiceChat, options: []) try audioSession.setActive(true, options: .notifyOthersOnDeactivation) // Retrieve sample rate and configure the audio format. let sampleRate = audioSession.sampleRate print("Active hardware sample rate: \(sampleRate)") audioFormat = AVAudioFormat(standardFormatWithSampleRate: sampleRate, channels: 1) // Configure the audio engine. audioInputNode = audioEngine.inputNode audioEngine.attach(audioPlayerNode) audioEngine.connect(audioPlayerNode, to: audioEngine.mainMixerNode, format: audioFormat) try audioEngine.start() completion(true) } catch { print("Error configuring audio session: \(error.localizedDescription)") completion(false) } } private func setupUDPConnection() { let parameters = NWParameters.udp parameters.includePeerToPeer = true connection = NWConnection(host: "***.***.xxxxx.***", port: 0000, using: parameters) setupNWConnectionHandlers() } private func setupTCPConnection() { let parameters = NWParameters.tcp connection = NWConnection(host: "***.***.xxxxx.***", port: 0000, using: parameters) setupNWConnectionHandlers() } private func setupWebSocketConnection() { guard let url = URL(string: "ws://***.***.xxxxx.***:0000") else { print("Invalid WebSocket URL") return } let session = URLSession(configuration: .default) webSocketTask = session.webSocketTask(with: url) webSocketTask?.resume() print("WebSocket connection initiated") sendAudioToServer() receiveDataFromServer() sendWebSocketPing(after: 0.6) } private func setupNWConnectionHandlers() { connection?.stateUpdateHandler = { [weak self] state in DispatchQueue.main.async { switch state { case .ready: print("Connected (NWConnection)") self?.isConnected = true self?.failToConnect = false self?.receiveDataFromServer() self?.sendAudioToServer() case .waiting(let error), .failed(let error): print("Connection error: \(error.localizedDescription)") DispatchQueue.main.asyncAfter(deadline: .now() + 2) { self?.setupNetwork() } case .cancelled: print("NWConnection cancelled") self?.isConnected = false default: break } } } connection?.start(queue: .main) } I am reaching out to seek further assistance regarding the challenges I've been experiencing with establishing a UDP, TCP & web socket connection on watchOS using NWConnection for duplex audio streaming. Despite implementing the recommendations provided earlier, I am still encountering difficulties. Or duplex audio streaming not possible on apple watch?
Replies
6
Boosts
0
Views
203
Activity
Apr ’25
Connecting to a service found by Bonjour isn't working.
I'm using NWBrowser to search for a server that I hosted. The browser does find my service but when it tries to connect to it, it gets stuck in the preparing phase in NWConnection.stateUpdateHandler. When I hardcode the local IP address of my computer (where the server is hosted) into NWConnection it works perfectly fine and is able to connect. When it gets stuck in the preparing phase, it gives me the warnings and error messages in the image below. You can also see that the service name is correct and it is found. I have tried _http._tcp and _ssh._tcp types and neither work. This is what my code looks like: func findServerAndConnect(port: UInt16) { print("Searching for server...") let browser = NWBrowser(for: .bonjour(type: "_ssh._tcp", domain: "local."), using: .tcp) browser.browseResultsChangedHandler = { results, _ in print("Found results: \(results)") for result in results { if case let NWEndpoint.service(name, type_, domain, interface) = result.endpoint { if name == "PocketPadServer" { print("Found service: \(name) of type \(type_) in domain \(domain) on interface \(interface)") // Construct the full service name, including type and domain let fullServiceName = "\(name).\(type_).\(domain)" print("Full service name: \(fullServiceName), \(result.endpoint)") self.connect(to: result.endpoint, port: port) browser.cancel() break } } } } browser.start(queue: .main) } func connect(to endpoint: NWEndpoint, port: UInt16) { print("Connecting to \(endpoint) on port \(port)...") // endpoint = NWEndpoint( let tcpParams = NWProtocolTCP.Options() tcpParams.enableFastOpen = true tcpParams.keepaliveIdle = 2 let params = NWParameters(tls: nil, tcp: tcpParams) params.includePeerToPeer = true // connection = NWConnection(host: NWEndpoint.Host("xx.xxx.xxx.xxx"), port: NWEndpoint.Port(3000), using: params) connection = NWConnection(to: endpoint, using: params) connection?.pathUpdateHandler = { path in print("Connection path update: \(path)") if path.status == .satisfied { print("Connection path is satisfied") } else { print("Connection path is not satisfied: \(path.status)") } } connection?.stateUpdateHandler = { newState in DispatchQueue.main.async { switch newState { case .ready: print("Connected to server") self.pairing = true self.receiveMessage() case .failed(let error): print("Connection failed: \(error)") self.isConnected = false case .waiting(let error): print("Waiting for connection... \(error)") self.isConnected = false case .cancelled: print("Connection cancelled") self.isConnected = false case .preparing: print("Preparing connection...") self.isConnected = false default: print("Connection state changed: \(newState)") break } } } connection?.start(queue: .main) }
Replies
4
Boosts
0
Views
166
Activity
Apr ’25
Hide OS logs coming out of the Network framework (category: com.apple.network)
I'm establishing a connection with NWListener and NWConnection which is working great. However, if the listener disappears, a lot of logs are appearing: Is there a way to hide these logs? I'm aware of OS_ACTIVITY_MODE=disabled, but that will also hide a lot of other logs. I also know you can hide these using Xcode's filtering. I'm looking for a programmatically way to hide these completely. I'm not interested in seeing these at all, or, at least, I want to be in control. Thanks!
Replies
4
Boosts
0
Views
193
Activity
Apr ’25
in-addr.arpa default search domains
Hi, I observed some unexpected behavior and hope that someone can enlighten me as to what this is about: mDNSResponder prepends IP / network based default search domains that are checked before any other search domain. E.g. 0.1.168.192.in-addr.arpa. would be used for an interface with an address in the the 192.168.1.0/24 subnet. This is done for any configured non-link-local IP address. I tried to find any mention of an approach like this in RFCs but couldn't spot anything. Please note that this is indeed a search domain and different from reverse-DNS lookups. Example output of tcpdump for ping devtest: 10:02:13.850802 IP (tos 0x0, ttl 64, id 43461, offset 0, flags [none], proto UDP (17), length 92) 192.168.1.2.52319 > 192.168.1.1.53: 54890+ [1au] A? devtest.0.1.168.192.in-addr.arpa. (64) I was able to identify the code that adds those default IP subnet based search domains but failed to spot any indication as to what this is about: https://github.com/apple-oss-distributions/mDNSResponder/blob/d5029b5/mDNSMacOSX/mDNSMacOSX.c#L4171-L4211 Does anyone here have an ideas as to what this might be about?
Replies
1
Boosts
0
Views
786
Activity
Apr ’25
Drop from URLSession to Network framework for SNI
Hi Dev Forums and Quinn "The Eskimo!", Short version Is there sample NWConnection code available that behaves in a similar way to the higher level URLSession and URLRequest APIs? Long version I have not been able to make this question get past the "sensitive language filter" on the dev forums. I figured it might be 'fool' or 'heck', or the X link, but removing each of those still triggers the sensitive language filter. Please see this gist: https://gist.github.com/lzell/8672c26ecb6ee1bb26d3aa3c7d67dd62 Thank you! Lou Zell
Replies
2
Boosts
0
Views
158
Activity
Apr ’25
Moving from Multipeer Connectivity to Network Framework
I see a lot of folks spend a lot of time trying to get Multipeer Connectivity to work for them. My experience is that the final result is often unsatisfactory. Instead, my medium-to-long term recommendation is to use Network framework instead. This post explains how you might move from Multipeer Connectivity to Network framework. If you have questions or comments, put them in a new thread. Place it in the App & System Services > Networking topic area and tag it with Multipeer Connectivity and Network framework. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" Moving from Multipeer Connectivity to Network Framework Multipeer Connectivity has a number of drawbacks: It has an opinionated networking model, where every participant in a session is a symmetric peer. Many apps work better with the traditional client/server model. It offers good latency but poor throughput. It doesn’t support flow control, aka back pressure, which severely constrains its utility for general-purpose networking. It includes a number of UI components that are effectively obsolete. It hasn’t evolved in recent years. For example, it relies on NSStream, which has been scheduled for deprecation as far as networking is concerned. It always enables peer-to-peer Wi-Fi, something that’s not required for many apps and can impact the performance of the network (see Enable peer-to-peer Wi-Fi, below, for more about this). Its security model requires the use of PKI — public key infrastructure, that is, digital identities and certificates — which are tricky to deploy in a peer-to-peer environment. It has some gnarly bugs. IMPORTANT Many folks use Multipeer Connectivity because they think it’s the only way to use peer-to-peer Wi-Fi. That’s not the case. Network framework has opt-in peer-to-peer Wi-Fi support. See Enable peer-to-peer Wi-Fi, below. If Multipeer Connectivity is not working well for you, consider moving to Network framework. This post explains how to do that in 13 easy steps (-: Plan for security Select a network architecture Create a peer identifier Choose a protocol to match your send mode Discover peers Design for privacy Configure your connections Manage a listener Manage a connection Send and receive reliable messages Send and receive best effort messages Start a stream Send a resource Finally, at the end of the post you’ll find two appendices: Final notes contains some general hints and tips. Symbol cross reference maps symbols in the Multipeer Connectivity framework to sections of this post. Consult it if you’re not sure where to start with a specific Multipeer Connectivity construct. Plan for security The first thing you need to think about is security. Multipeer Connectivity offers three security models, expressed as choices in the MCEncryptionPreference enum: .none for no security .optional for optional security .required for required security For required security each peer must have a digital identity. Optional security is largely pointless. It’s more complex than no security but doesn’t yield any benefits. So, in this post we’ll focus on the no security and required security models. Your security choice affects the network protocols you can use: QUIC is always secure. WebSocket, TCP, and UDP can be used with and without TLS security. QUIC security only supports PKI. TLS security supports both TLS-PKI and pre-shared key (PSK). You might find that TLS-PSK is easier to deploy in a peer-to-peer environment. To configure the security of the QUIC protocol: func quicParameters() -> NWParameters { let quic = NWProtocolQUIC.Options(alpn: ["MyAPLN"]) let sec = quic.securityProtocolOptions … configure `sec` here … return NWParameters(quic: quic) } To enable TLS over TCP: func tlsOverTCPParameters() -> NWParameters { let tcp = NWProtocolTCP.Options() let tls = NWProtocolTLS.Options() let sec = tls.securityProtocolOptions … configure `sec` here … return NWParameters(tls: tls, tcp: tcp) } To enable TLS over UDP, also known as DTLS: func dtlsOverUDPParameters() -> NWParameters { let udp = NWProtocolUDP.Options() let dtls = NWProtocolTLS.Options() let sec = dtls.securityProtocolOptions … configure `sec` here … return NWParameters(dtls: dtls, udp: udp) } To configure TLS with a local digital identity and custom server trust evaluation: func configureTLSPKI(sec: sec_protocol_options_t, identity: SecIdentity) { let secIdentity = sec_identity_create(identity)! sec_protocol_options_set_local_identity(sec, secIdentity) if disableServerTrustEvaluation { sec_protocol_options_set_verify_block(sec, { metadata, secTrust, completionHandler in let trust = sec_trust_copy_ref(secTrust).takeRetainedValue() … evaluate `trust` here … completionHandler(true) }, .main) } } To configure TLS with a pre-shared key: func configureTLSPSK(sec: sec_protocol_options_t, identity: Data, key: Data) { let identityDD = identity.withUnsafeBytes { DispatchData(bytes: $0) } let keyDD = identity.withUnsafeBytes { DispatchData(bytes: $0) } sec_protocol_options_add_pre_shared_key( sec, keyDD as dispatch_data_t, identityDD as dispatch_data_t ) sec_protocol_options_append_tls_ciphersuite( sec, tls_ciphersuite_t(rawValue: TLS_PSK_WITH_AES_128_GCM_SHA256)! ) } Select a network architecture Multipeer Connectivity uses a star network architecture. All peers are equal, and every peer is effectively connected to every peer. Many apps work better with the client/server model, where one peer acts on the server and all the others are clients. Network framework supports both models. To implement a client/server network architecture with Network framework: Designate one peer as the server and all the others as clients. On the server, use NWListener to listen for incoming connections. On each client, use NWConnection to made an outgoing connection to the server. To implement a star network architecture with Network framework: On each peer, start a listener. And also start a connection to each of the other peers. This is likely to generate a lot of redundant connections, as peer A connects to peer B and vice versa. You’ll need to a way to deduplicate those connections, which is the subject of the next section. IMPORTANT While the star network architecture is more likely to create redundant connections, the client/server network architecture can generate redundant connections as well. The advice in the next section applies to both architectures. Create a peer identifier Multipeer Connectivity uses MCPeerID to uniquely identify each peer. There’s nothing particularly magic about MCPeerID; it’s effectively a wrapper around a large random number. To identify each peer in Network framework, generate your own large random number. One good choice for a peer identifier is a locally generated UUID, created using the system UUID type. Some Multipeer Connectivity apps persist their local MCPeerID value, taking advantage of its NSSecureCoding support. You can do the same with a UUID, using either its string representation or its Codable support. IMPORTANT Before you decide to persist a peer identifier, think about the privacy implications. See Design for privacy below. Avoid having multiple connections between peers; that’s both wasteful and potentially confusing. Use your peer identifier to deduplicate connections. Deduplicating connections in a client/server network architecture is easy. Have each client check in with the server with its peer identifier. If the server already has a connection for that identifier, it can either close the old connection and keep the new connection, or vice versa. Deduplicating connections in a star network architecture is a bit trickier. One option is to have each peer send its peer identifier to the other peer and then the peer with the ‘best’ identifier wins. For example, imagine that peer A makes an outgoing connection to peer B while peer B is simultaneously making an outgoing connection to peer A. When a peer receives a peer identifier from a connection, it checks for a duplicate. If it finds one, it compares the peer identifiers and then chooses a connection to drop based on that comparison: if local peer identifier > remote peer identifier then drop outgoing connection else drop incoming connection end if So, peer A drops its incoming connection and peer B drops its outgoing connection. Et voilà! Choose a protocol to match your send mode Multipeer Connectivity offers two send modes, expressed as choices in the MCSessionSendDataMode enum: .reliable for reliable messages .unreliable for best effort messages Best effort is useful when sending latency-sensitive data, that is, data where retransmission is pointless because, by the retransmission arrives, the data will no longer be relevant. This is common in audio and video applications. In Network framework, the send mode is set by the connection’s protocol: A specific QUIC connection is either reliable or best effort. WebSocket and TCP are reliable. UDP is best effort. Start with a reliable connection. In many cases you can stop there, because you never need a best effort connection. If you’re not sure which reliable protocol to use, choose WebSocket. It has key advantages over other protocols: It supports both security models: none and required. Moreover, its required security model supports both TLS-PKI and TLS PSK. In contrast, QUIC only supports the required security model, and within that model it only supports TLS-PKI. It allows you to send messages over the connection. In contrast, TCP works in terms of bytes, meaning that you have to add your own framing. If you need a best effort connection, get started with a reliable connection and use that connection to set up a parallel best effort connection. For example, you might have an exchange like this: Peer A uses its reliable WebSocket connection to peer B to send a request for a parallel best effort UDP connection. Peer B receives that, opens a UDP listener, and sends the UDP listener’s port number back to peer A. Peer A opens its parallel UDP connection to that port on peer B. Note For step 3, get peer B’s IP address from the currentPath property of the reliable WebSocket connection. If you’re not sure which best effort protocol to use, use UDP. While it is possible to use QUIC in datagram mode, it has the same security complexities as QUIC in reliable mode. Discover peers Multipeer Connectivity has a types for advertising a peer’s session (MCAdvertiserAssistant) and a type for browsering for peer (MCNearbyServiceBrowser). In Network framework, configure the listener to advertise its service by setting the service property of NWListener: let listener: NWListener = … listener.service = .init(type: "_example._tcp") listener.serviceRegistrationUpdateHandler = { change in switch change { case .add(let endpoint): … update UI for the added listener endpoint … break case .remove(let endpoint): … update UI for the removed listener endpoint … break @unknown default: break } } listener.stateUpdateHandler = … handle state changes … listener.newConnectionHandler = … handle the new connection … listener.start(queue: .main) This example also shows how to use the serviceRegistrationUpdateHandler to update your UI to reflect changes in the listener. Note This example uses a service type of _example._tcp. See About service types, below, for more details on that. To browse for services, use NWBrowser: let browser = NWBrowser(for: .bonjour(type: "_example._tcp", domain: nil), using: .tcp) browser.browseResultsChangedHandler = { latestResults, _ in … update UI to show the latest results … } browser.stateUpdateHandler = … handle state changes … browser.start(queue: .main) This yields NWEndpoint values for each peer that it discovers. To connect to a given peer, create an NWConnection with that endpoint. About service types The examples in this post use _example._tcp for the service type. The first part, _example, is directly analogous to the serviceType value you supply when creating MCAdvertiserAssistant and MCNearbyServiceBrowser objects. The second part is either _tcp or _udp depending on the underlying transport protocol. For TCP and WebSocket, use _tcp. For UDP and QUIC, use _udp. Service types are described in RFC 6335. If you deploy an app that uses a new service type, register that service type with IANA. Discovery UI Multipeer Connectivity also has UI components for advertising (MCNearbyServiceAdvertiser) and browsing (MCBrowserViewController). There’s no direct equivalent to this in Network framework. Instead, use your preferred UI framework to create a UI that best suits your requirements. Note If you’re targeting Apple TV, check out the DeviceDiscoveryUI framework. Discovery TXT records The Bonjour service discovery protocol used by Network framework supports TXT records. Using these, a listener can associate metadata with its service and a browser can get that metadata for each discovered service. To advertise a TXT record with your listener, include it it the service property value: let listener: NWListener = … let peerID: UUID = … var txtRecord = NWTXTRecord() txtRecord["peerID"] = peerID.uuidString listener.service = .init(type: "_example._tcp", txtRecord: txtRecord.data) To browse for services and their associated TXT records, use the .bonjourWithTXTRecord(…) descriptor: let browser = NWBrowser(for: .bonjourWithTXTRecord(type: "_example._tcp", domain: nil), using: .tcp) browser.browseResultsChangedHandler = { latestResults, _ in for result in latestResults { guard case .bonjour(let txtRecord) = result.metadata, let peerID = txtRecord["peerID"] else { continue } // … examine `result` and `peerID` … _ = peerID } } This example includes the peer identifier in the TXT record with the goal of reducing the number of duplicate connections, but that’s just one potential use for TXT records. Design for privacy This section lists some privacy topics to consider as you implement your app. Obviously this isn’t an exhaustive list. For general advice on this topic, see Protecting the User’s Privacy. There can be no privacy without security. If you didn’t opt in to security with Multipeer Connectivity because you didn’t want to deal with PKI, consider the TLS-PSK options offered by Network framework. For more on this topic, see Plan for security. When you advertise a service, the default behaviour is to use the user-assigned device name as the service name. To override that, create a service with a custom name: let listener: NWListener = … let name: String = … listener.service = .init(name: name, type: "_example._tcp") It’s not uncommon for folks to use the peer identifier as the service name. Whether that’s a good option depends on the user experience of your product: Some products present a list of remote peers and have the user choose from that list. In that case it’s best to stick with the user-assigned device name, because that’s what the user will recognise. Some products automatically connect to services as they discover them. In that case it’s fine to use the peer identifier as the service name, because the user won’t see it anyway. If you stick with the user-assigned device name, consider advertising the peer identifier in your TXT record. See Discovery TXT records. IMPORTANT Using a peer identifier in your service name or TXT record is a heuristic to reduce the number of duplicate connections. Don’t rely on it for correctness. Rather, deduplicate connections using the process described in Create a peer identifier. There are good reasons to persist your peer identifier, but doing so isn’t great for privacy. Persisting the identifier allows for tracking of your service over time and between networks. Consider whether you need a persistent peer identifier at all. If you do, consider whether it makes sense to rotate it over time. A persistent peer identifier is especially worrying if you use it as your service name or put it in your TXT record. Configure your connections Multipeer Connectivity’s symmetric architecture means that it uses a single type, MCSession, to manage the connections to all peers. In Network framework, that role is fulfilled by two types: NWListener to listen for incoming connections. NWConnection to make outgoing connections. Both types require you to supply an NWParameters value that specifies the network protocol and options to use. In addition, when creating an NWConnection you pass in an NWEndpoint to tell it the service to connect to. For example, here’s how to configure a very simple listener for TCP: let parameters = NWParameters.tcp let listener = try NWListener(using: parameters) … continue setting up the listener … And here’s how you might configure an outgoing TCP connection: let parameters = NWParameters.tcp let endpoint = NWEndpoint.hostPort(host: "example.com", port: 80) let connection = NWConnection.init(to: endpoint, using: parameters) … continue setting up the connection … NWParameters has properties to control exactly what protocol to use and what options to use with those protocols. To work with QUIC connections, use code like that shown in the quicParameters() example from the Security section earlier in this post. To work with TCP connections, use the NWParameters.tcp property as shown above. To enable TLS on your TCP connections, use code like that shown in the tlsOverTCPParameters() example from the Security section earlier in this post. To work with WebSocket connections, insert it into the application protocols array: let parameters = NWParameters.tcp let ws = NWProtocolWebSocket.Options(.version13) parameters.defaultProtocolStack.applicationProtocols.insert(ws, at: 0) To enable TLS on your WebSocket connections, use code like that shown in the tlsOverTCPParameters() example to create your base parameters and then add the WebSocket application protocol to that. To work with UDP connections, use the NWParameters.udp property: let parameters = NWParameters.udp To enable TLS on your UDP connections, use code like that shown in the dtlsOverUDPParameters() example from the Security section earlier in this post. Enable peer-to-peer Wi-Fi By default, Network framework doesn’t use peer-to-peer Wi-Fi. To enable that, set the includePeerToPeer property on the parameters used to create your listener and connection objects. parameters.includePeerToPeer = true IMPORTANT Enabling peer-to-peer Wi-Fi can impact the performance of the network. Only opt into it if it’s a significant benefit to your app. If you enable peer-to-peer Wi-Fi, it’s critical to stop network operations as soon as you’re done with them. For example, if you’re browsing for services with peer-to-peer Wi-Fi enabled and the user picks a service, stop the browse operation immediately. Otherwise, the ongoing browse operation might affect the performance of your connection. Manage a listener In Network framework, use NWListener to listen for incoming connections: let parameters: NWParameters = .tcp … configure parameters … let listener = try NWListener(using: parameters) listener.service = … service details … listener.serviceRegistrationUpdateHandler = … handle service registration changes … listener.stateUpdateHandler = { newState in … handle state changes … } listener.newConnectionHandler = { newConnection in … handle the new connection … } listener.start(queue: .main) For details on how to set up parameters, see Configure your connections. For details on how to set up up service and serviceRegistrationUpdateHandler, see Discover peers. Network framework calls your state update handler when the listener changes state: let listener: NWListener = … listener.stateUpdateHandler = { newState in switch newState { case .setup: // The listener has not yet started. … case .waiting(let error): // The listener tried to start and failed. It might recover in the // future. … case .ready: // The listener is running. … case .failed(let error): // The listener tried to start and failed irrecoverably. … case .cancelled: // The listener was cancelled by you. … @unknown default: break } } Network framework calls your new connection handler when a client connects to it: var connections: [NWConnection] = [] let listener: NWListener = listener listener.newConnectionHandler = { newConnection in … configure the new connection … newConnection.start(queue: .main) connections.append(newConnection) } IMPORTANT Don’t forget to call start(queue:) on your connections. In Multipeer Connectivity, the session (MCSession) keeps track of all the peers you’re communicating with. With Network framework, that responsibility falls on you. This example uses a simple connections array for that purpose. In your app you may or may not need a more complex data structure. For example: In the client/server network architecture, the client only needs to manage the connections to a single peer, the server. On the other hand, the server must managed the connections to all client peers. In the star network architecture, every peer must maintain a listener and connections to each of the other peers. Understand UDP flows Network framework handles UDP using the same NWListener and NWConnection types as it uses for TCP. However, the underlying UDP protocol is not implemented in terms of listeners and connections. To resolve this, Network framework works in terms of UDP flows. A UDP flow is defined as a bidirectional sequence of UDP datagrams with the same 4 tuple (local IP address, local port, remote IP address, and remote port). In Network framework: Each NWConnection object manages a single UDP flow. If an NWListener receives a UDP datagram whose 4 tuple doesn’t match any known NWConnection, it creates a new NWConnection. Manage a connection In Network framework, use NWConnection to start an outgoing connection: var connections: [NWConnection] = [] let parameters: NWParameters = … let endpoint: NWEndpoint = … let connection = NWConnection(to: endpoint, using: parameters) connection.stateUpdateHandler = … handle state changes … connection.viabilityUpdateHandler = … handle viability changes … connection.pathUpdateHandler = … handle path changes … connection.betterPathUpdateHandler = … handle better path notifications … connection.start(queue: .main) connections.append(connection) As in the listener case, you’re responsible for keeping track of this connection. Each connection supports four different handlers. Of these, the state and viability update handlers are the most important. For information about the path update and better path handlers, see the NWConnection documentation. Network framework calls your state update handler when the connection changes state: let connection: NWConnection = … connection.stateUpdateHandler = { newState in switch newState { case .setup: // The connection has not yet started. … case .preparing: // The connection is starting. … case .waiting(let error): // The connection tried to start and failed. It might recover in the // future. … case .ready: // The connection is running. … case .failed(let error): // The connection tried to start and failed irrecoverably. … case .cancelled: // The connection was cancelled by you. … @unknown default: break } } If you a connection is in the .waiting(_:) state and you want to force an immediate retry, call the restart() method. Network framework calls your viability update handler when its viability changes: let connection: NWConnection = … connection.viabilityUpdateHandler = { isViable in … react to viability changes … } A connection becomes inviable when a network resource that it depends on is unavailable. A good example of this is the network interface that the connection is running over. If you have a connection running over Wi-Fi, and the user turns off Wi-Fi or moves out of range of their Wi-Fi network, any connection running over Wi-Fi becomes inviable. The inviable state is not necessarily permanent. To continue the above example, the user might re-enable Wi-Fi or move back into range of their Wi-Fi network. If the connection becomes viable again, Network framework calls your viability update handler with a true value. It’s a good idea to debounce the viability handler. If the connection becomes inviable, don’t close it down immediately. Rather, wait for a short while to see if it becomes viable again. If a connection has been inviable for a while, you get to choose as to how to respond. For example, you might close the connection down or inform the user. To close a connection, call the cancel() method. This gracefully disconnects the underlying network connection. To close a connection immediately, call the forceCancel() method. This is not something you should do as a matter of course, but it does make sense in exceptional circumstances. For example, if you’ve determined that the remote peer has gone deaf, it makes sense to cancel it in this way. Send and receive reliable messages In Multipeer Connectivity, a single session supports both reliable and best effort send modes. In Network framework, a connection is either reliable or best effort, depending on the underlying network protocol. The exact mechanism for sending a message depends on the underlying network protocol. A good protocol for reliable messages is WebSocket. To send a message on a WebSocket connection: let connection: NWConnection = … let message: Data = … let metadata = NWProtocolWebSocket.Metadata(opcode: .binary) let context = NWConnection.ContentContext(identifier: "send", metadata: [metadata]) connection.send(content: message, contentContext: context, completion: .contentProcessed({ error in // … check `error` … _ = error })) In WebSocket, the content identifier is ignored. Using an arbitrary fixed value, like the send in this example, is just fine. Multipeer Connectivity allows you to send a message to multiple peers in a single send call. In Network framework each send call targets a specific connection. To send a message to multiple peers, make a send call on the connection associated with each peer. If your app needs to transfer arbitrary amounts of data on a connection, it must implement flow control. See Start a stream, below. To receive messages on a WebSocket connection: func startWebSocketReceive(on connection: NWConnection) { connection.receiveMessage { message, _, _, error in if let error { … handle the error … return } if let message { … handle the incoming message … } startWebSocketReceive(on: connection) } } IMPORTANT WebSocket preserves message boundaries, which is one of the reasons why it’s ideal for your reliable messaging connections. If you use a streaming protocol, like TCP or QUIC streams, you must do your own framing. A good way to do that is with NWProtocolFramer. If you need the metadata associated with the message, get it from the context parameter: connection.receiveMessage { message, context, _, error in … if let message, let metadata = context?.protocolMetadata(definition: NWProtocolWebSocket.definition) as? NWProtocolWebSocket.Metadata { … handle the incoming message and its metadata … } … } Send and receive best effort messages In Multipeer Connectivity, a single session supports both reliable and best effort send modes. In Network framework, a connection is either reliable or best effort, depending on the underlying network protocol. The exact mechanism for sending a message depends on the underlying network protocol. A good protocol for best effort messages is UDP. To send a message on a UDP connection: let connection: NWConnection = … let message: Data = … connection.send(content: message, completion: .idempotent) IMPORTANT UDP datagrams have a theoretical maximum size of just under 64 KiB. However, sending a large datagram results in IP fragmentation, which is very inefficient. For this reason, Network framework prevents you from sending UDP datagrams that will be fragmented. To find the maximum supported datagram size for a connection, gets its maximumDatagramSize property. To receive messages on a UDP connection: func startUDPReceive(on connection: NWConnection) { connection.receiveMessage { message, _, _, error in if let error { … handle the error … return } if let message { … handle the incoming message … } startUDPReceive(on: connection) } } This is exactly the same code as you’d use for WebSocket. Start a stream In Multipeer Connectivity, you can ask the session to start a stream to a specific peer. There are two ways to achieve this in Network framework: If you’re using QUIC for your reliable connection, start a new QUIC stream over that connection. This is one place that QUIC shines. You can run an arbitrary number of QUIC connections over a single QUIC connection group, and QUIC manages flow control (see below) for each connection and for the group as a whole. If you’re using some other protocol for your reliable connection, like WebSocket, you must start a new connection. You might use TCP for this new connection, but it’s not unreasonable to use WebSocket or QUIC. If you need to open a new connection for your stream, you can manage that process over your reliable connection. Choose a protocol to match your send mode explains the general approach for this, although in that case it’s opening a parallel best effort UDP connection rather than a parallel stream connection. The main reason to start a new stream is that you want to send a lot of data to the remote peer. In that case you need to worry about flow control. Flow control applies to both the send and receive side. IMPORTANT Failing to implement flow control can result in unbounded memory growth in your app. This is particularly bad on iOS, where jetsam will terminate your app if it uses too much memory. On the send side, implement flow control by waiting for the connection to call your completion handler before generating and sending more data. For example, on a TCP connection or QUIC stream you might have code like this: func sendNextChunk(on connection: NWConnection) { let chunk: Data = … read next chunk from disk … connection.send(content: chunk, completion: .contentProcessed({ error in if let error { … handle error … return } sendNextChunk(on: connection) })) } This acts like an asynchronous loop. The first send call completes immediately because the connection just copies the data to its send buffer. In response, your app generates more data. This continues until the connection’s send buffer fills up, at which point it defers calling your completion handler. Eventually, the connection moves enough data across the network to free up space in its send buffer, and calls your completion handler. Your app generates another chunk of data For best performance, use a chunk size of at least 64 KiB. If you’re expecting to run on a fast device with a fast network, a chunk size of 1 MiB is reasonable. Receive-side flow control is a natural extension of the standard receive pattern. For example, on a TCP connection or QUIC stream you might have code like this: func receiveNextChunk(on connection: NWConnection) { let chunkSize = 64 * 1024 connection.receive(minimumIncompleteLength: chunkSize, maximumLength: chunkSize) { chunk, _, isComplete, error in if let chunk { … write chunk to disk … } if isComplete { … close the file … return } if let error { … handle the error … return } receiveNextChunk(on: connection) } } IMPORTANT The above is cast in terms of writing the chunk to disk. That’s important, because it prevents unbounded memory growth. If, for example, you accumulated the chunks into an in-memory buffer, that buffer could grow without bound, which risks jetsam terminating your app. The above assumes that you can read and write chunks of data synchronously and promptly, for example, reading and writing a file on a local disk. That’s not always the case. For example, you might be writing data to an accessory over a slow interface, like Bluetooth LE. In such cases you need to read and write each chunk asynchronously. This results in a structure where you read from an asynchronous input and write to an asynchronous output. For an example of how you might approach this, albeit in a very different context, see Handling Flow Copying. Send a resource In Multipeer Connectivity, you can ask the session to send a complete resource, identified by either a file or HTTP URL, to a specific peer. Network framework has no equivalent support for this, but you can implement it on top of a stream: To send, open a stream and then read chunks of data using URLSession and send them over that stream. To receive, open a stream and then receive chunks of data from that stream and write those chunks to disk. In this situation it’s critical to implement flow control, as described in the previous section. Final notes This section collects together some general hints and tips. Concurrency In Multipeer Connectivity, each MCSession has its own internal queue and calls delegate callbacks on that queue. In Network framework, you get to control the queue used by each object for its callbacks. A good pattern is to have a single serial queue for all networking, including your listener and all connections. In a simple app it’s reasonable to use the main queue for networking. If you do this, be careful not to do CPU intensive work in your networking callbacks. For example, if you receive a message that holds JPEG data, don’t decode that data on the main queue. Overriding protocol defaults Many network protocols, most notably TCP and QUIC, are intended to be deployed at vast scale across the wider Internet. For that reason they use default options that aren’t optimised for local networking. Consider changing these defaults in your app. TCP has the concept of a send timeout. If you send data on a TCP connection and TCP is unable to successfully transfer it to the remote peer within the send timeout, TCP will fail the connection. The default send timeout is infinite. TCP just keeps trying. To change this, set the connectionDropTime property. TCP has the concept of keepalives. If a connection is idle, TCP will send traffic on the connection for two reasons: If the connection is running through a NAT, the keepalives prevent the NAT mapping from timing out. If the remote peer is inaccessible, the keepalives fail, which in turn causes the connection to fail. This prevents idle but dead connections from lingering indefinitely. TCP keepalives default to disabled. To enable and configure them, set the enableKeepalive property. To configure their behaviour, set the keepaliveIdle, keepaliveCount, and keepaliveInterval properties. Symbol cross reference If you’re not sure where to start with a specific Multipeer Connectivity construct, find it in the tables below and follow the link to the relevant section. [Sorry for the poor formatting here. DevForums doesn’t support tables properly, so I’ve included the tables as preformatted text.] | For symbol | See | | ----------------------------------- | --------------------------- | | `MCAdvertiserAssistant` | *Discover peers* | | `MCAdvertiserAssistantDelegate` | *Discover peers* | | `MCBrowserViewController` | *Discover peers* | | `MCBrowserViewControllerDelegate` | *Discover peers* | | `MCNearbyServiceAdvertiser` | *Discover peers* | | `MCNearbyServiceAdvertiserDelegate` | *Discover peers* | | `MCNearbyServiceBrowser` | *Discover peers* | | `MCNearbyServiceBrowserDelegate` | *Discover peers* | | `MCPeerID` | *Create a peer identifier* | | `MCSession` | See below. | | `MCSessionDelegate` | See below. | Within MCSession: | For symbol | See | | --------------------------------------------------------- | ------------------------------------ | | `cancelConnectPeer(_:)` | *Manage a connection* | | `connectedPeers` | *Manage a listener* | | `connectPeer(_:withNearbyConnectionData:)` | *Manage a connection* | | `disconnect()` | *Manage a connection* | | `encryptionPreference` | *Plan for security* | | `myPeerID` | *Create a peer identifier* | | `nearbyConnectionData(forPeer:withCompletionHandler:)` | *Discover peers* | | `securityIdentity` | *Plan for security* | | `send(_:toPeers:with:)` | *Send and receive reliable messages* | | `sendResource(at:withName:toPeer:withCompletionHandler:)` | *Send a resource* | | `startStream(withName:toPeer:)` | *Start a stream* | Within MCSessionDelegate: | For symbol | See | | ---------------------------------------------------------------------- | ------------------------------------ | | `session(_:didFinishReceivingResourceWithName:fromPeer:at:withError:)` | *Send a resource* | | `session(_:didReceive:fromPeer:)` | *Send and receive reliable messages* | | `session(_:didReceive:withName:fromPeer:)` | *Start a stream* | | `session(_:didReceiveCertificate:fromPeer:certificateHandler:)` | *Plan for security* | | `session(_:didStartReceivingResourceWithName:fromPeer:with:)` | *Send a resource* | | `session(_:peer:didChange:)` | *Manage a connection* | Revision History 2025-04-11 Added some advice as to whether to use the peer identifier in your service name. Expanded the discussion of how to deduplicate connections in a star network architecture. 2025-03-20 Added a link to the DeviceDiscoveryUI framework to the Discovery UI section. Made other minor editorial changes. 2025-03-11 Expanded the Enable peer-to-peer Wi-Fi section to stress the importance of stopping network operations once you’re done with them. Added a link to that section from the list of Multipeer Connectivity drawbacks. 2025-03-07 First posted.
Replies
0
Boosts
0
Views
1.8k
Activity
Apr ’25
Question about listen on the utun interface to serve as system dns server
Hello, Recently I am trying to add stub dns server to my Network Extension (a VPN app), after some research on this forum, and since my language is C, I have the following plan: create a udp socket which use setsockopt(IP_BOUND_IF) to bound the socket to the utun if index obtained, and also bind to the address of the utun address I set(let's say 192.168.99.2), then listen on the udp port 53 which is ready to handle dns request. configure the dns server to 192.168.99.2 in the provider's Network Settings, thus iOS system will send udp query to the udp socket created in step 1, and it can then do some split dns function such as resolve using local interface (cellular or wifi), or some nameserve which will be routed to the VPN tunnel (will create new UDP socket and do IP_BOUND_IF to ensure the traffic will enter the VPN tunnel), and the result should be return to the system and then the non VPP apps. But I observer weird issue, indeed I can get the system send the dns request to the listening udp socket and I can get the result write to the system(address like 192.168.99.2:56144, the port should be allocated by the iOS system's DNS component) without any failure(I did get some error before due to I using the wrong utun if index, but fixed it later), but it seems non VPN app like browser can't get the resolved ip for domains. I want to ask is this limited by the sandbox? or any special sock opt I need to do. Thanks. PS: in the provider's network settings, all the system's traffic will be point to the utun, which means the VPN process will process all the traffic. the reason I do not set the dns server to utun peers side which is my userspace networking stack's ip (192.168.99.1) is the stack is not be able to leverage some dns libraries due to architecture issue. (it's fd.io vpp which we ported to apple platform).
Replies
7
Boosts
0
Views
191
Activity
Apr ’25