Networking

RSS for tag

Explore the networking protocols and technologies used by the device to connect to Wi-Fi networks, Bluetooth devices, and cellular data services.

Networking Documentation

Posts under Networking subtopic

Post

Replies

Boosts

Views

Activity

Sporadic "no route to host" over ssh
When connecting to my M1 mac mini over ssh, certain programs are often unable to reach network destinations in the corporate LAN, although they can usually reach external addresses like www.apple.com. For example, a java program attempting to download from teamcity.dev.corp.com:8111 often fails like: java.net.NoRouteToHostException: No route to host Running the exact same command from the Apple Terminal program works like normal, simply connecting over ethernet on en0 to a TeamCity server inside the same building. Basic diagnostics from the ssh session do not show anything unusual: > traceroute teamcity.dev.corp.com traceroute to teamcity.dev.corp.com (10.21.4.1), 64 hops max, 40 byte packets 1 teamcity.dev.corp.com (10.21.4.1) 1.702 ms 0.409 ms 0.336 ms > route -n get teamcity.dev.corp.com route to: 10.21.4.1 destination: 10.21.4.1 interface: en0 flags: <UP,HOST,DONE,LLINFO,WASCLONED,IFSCOPE,IFREF> recvpipe sendpipe ssthresh rtt,msec rttvar hopcount mtu expire 0 0 0 0 0 0 1500 1194 > uname -a Darwin mac 25.1.0 Darwin Kernel Version 25.1.0: Mon Oct 20 19:32:47 PDT 2025; root:xnu-12377.41.6~2/RELEASE_ARM64_T8103 arm64 Similar problems occur in docker commands to a remote daemon ("no route to host" or "connection refused"): docker -H tcp://<ip>:<port> ... Most other programs are never affected by this problem. Are there other diagnostic steps that might reveal the cause?
3
0
69
3d
A Peek Behind the NECP Curtain
From time to time the subject of NECP grows up, both here on DevForums and in DTS cases. I’ve posted about this before but I wanted to collect those tidbits into single coherent post. If you have questions or comments, start a new thread in the App & System Services > Networking subtopic and tag it with Network Extension. That way I’ll be sure to see it go by. Share and Enjoy — Quinn “The Eskimo!” @ Developer Technical Support @ Apple let myEmail = "eskimo" + "1" + "@" + "apple.com" A Peek Behind the NECP Curtain NECP stands for Network Extension Control Protocol. It’s a subsystem within the Apple networking stack that controls which programs have access to which network interfaces. It’s vitally important to the Network Extension subsystem, hence the name, but it’s used in many different places. Indeed, a very familiar example of its use is the Settings > Mobile Data [1] user interface on iOS. NECP has no explicit API, although there are APIs that are offer some insight into its state. Continuing the Settings > Mobile Data example above, there is a little-known API, CTCellularData in the Core Telephony framework, that returns whether your app has access to WWAN. Despite having no API, NECP is still relevant to developers. The Settings > Mobile Data example is one place where it affects app developers but it’s most important for Network Extension (NE) developers. A key use case for NECP is to prevent VPN loops. When starting an NE provider, the system configures the NECP policy for the NE provider’s process to prevent it from using a VPN interface. This means that you can safely open a network connection inside your VPN provider without having to worry about its traffic being accidentally routed back to you. This is why, for example, an NE packet tunnel provider can use any networking API it wants, including BSD Sockets, to run its connection without fear of creating a VPN loop [1]. One place that NECP shows up regularly is the system log. Next time you see a system log entry like this: type: debug time: 15:02:54.817903+0000 process: Mail subsystem: com.apple.network category: connection message: nw_protocol_socket_set_necp_attributes [C723.1.1:1] setsockopt 39 SO_NECP_ATTRIBUTES … you’ll at least know what the necp means (-: Finally, a lot of NECP infrastructure is in the Darwin open source. As with all things in Darwin, it’s fine to poke around and see how your favourite feature works, but do not incorporate any information you find into your product. Stuff you uncover by looking in Darwin is not considered API. [1] Settings > Cellular Data if you speak American (-: [2] Network Extension providers can call the createTCPConnection(to:enableTLS:tlsParameters:delegate:) method to create an NWTCPConnection [3] that doesn’t run through the tunnel. You can use that if it’s convenient but you don’t need to use it. [3] NWTCPConnection is now deprecated, but there are non-deprecated equivalents. For the full story, see NWEndpoint History and Advice. Revision History 2025-12-12 Replaced “macOS networking stack” with “Apple networking stack” to avoid giving the impression that this is all about macOS. Added a link to NWEndpoint History and Advice. Made other minor editorial changes. 2023-02-27 First posted.
0
0
2.4k
Dec ’25
iOS reports incorrect own IP address to apps when connected to WiFi
I have an iPhone app which relies heavily on TCP/IP communication in the local network. Therefore, the application starts a server socket and accepts incoming connections. This worked flawlessly for a long time and we had no problems with this. Problem In the last days however, we observed that for some iPhones with the server role other devices cannot connect to the server of our app. The server does not accept incoming connections on the devices IP address and the client times out. Environment Both iPhones (the server and the client) are in the same network with 192.168.1.0 address range and 255.255.255.0 subnet mask. The server has the IP 192.168.1.11 and the client has 192.168.1.22. This is a normal home WiFi network with no special firewall rules. Both devices have mobile data disabled and the "access local network" permission is granted. The server socket is bound to all interfaces (0.0.0.0). More technical symptoms When the server iPhone is in this faulty state, it seems like it somehow has two ip addresses: 192.168.2.123 and 192.168.1.11 The WiFi preferences show the (correct) .1.11 ip address. The Apps however see the (wrong) .2.123 ip address. I cannot explain where the other ip address comes from and why the device thinks it has this ip address. I've collected interface diagnosis information on a faulty iPhone and it listed the following interfaces and IPs: en0 -> 192.168.2.123 lo0 -> 127.0.0.1 pdp_ip0 (cellular) -> 192.0.0.2 pdp_ip1 to pdp_ip6 (cellular) -> -/- ipsec0 to ipsec6 (vpn) -> -/- llw0 (vpn) -> -/- awdl0 -> -/- anpi0 -> -/- ap1 -> -/- XHC0 -> -/- en1 and en2 (wired) -> -/- utun0 to utun2 (vpn) -> -/- The correct ip of the device is not listed anywhere in this list. A reboot helped to temporarily fix this problem. One user reported the same issue again a few hours later after a reboot. Switching off WiFi and reconnecting does not solve the problem. This issue occurred on several iPhones with the following specs: iOS Version 18.1.1, 18.3.1 iPhone 13 Pro, iPhone 13 Pro Max, iPhone 15 Pro The problem must be on the server side as the client can successfully connect to any other device in the same network. Question(s) Where does this second IP come from and why does the server not accept connections to either ip even though it is bound to 0.0.0.0? Are there any iOS system settings which could lead to this problem? (privacy setting, vpn, ...) What could be done to permanently fix this issue?
1
0
261
Mar ’25
Crash when removing network extension
Our application uses NEFilterPacketProvider to filter network traffic and we sometimes get a wired crash when removing/updating the network extension. It only happens on MacOS 11-12 . The crashing thread is always this one and it shows up after I call the completionHandler from the stopFilter func Application Specific Information: BUG IN CLIENT OF LIBDISPATCH: Release of a suspended object Thread 6 Crashed:: Dispatch queue: com.apple.network.connections 0 libdispatch.dylib 0x00007fff2039cc35 _dispatch_queue_xref_dispose.cold.1 + 24 1 libdispatch.dylib 0x00007fff20373808 _dispatch_queue_xref_dispose + 50 2 libdispatch.dylib 0x00007fff2036e2eb -[OS_dispatch_source _xref_dispose] + 17 3 libnetwork.dylib 0x00007fff242b5999 __nw_queue_context_create_source_block_invoke + 41 4 libdispatch.dylib 0x00007fff2036d623 _dispatch_call_block_and_release + 12 5 libdispatch.dylib 0x00007fff2036e806 _dispatch_client_callout + 8 6 libdispatch.dylib 0x00007fff203711b0 _dispatch_continuation_pop + 423 7 libdispatch.dylib 0x00007fff203811f4 _dispatch_source_invoke + 1181 8 libdispatch.dylib 0x00007fff20376318 _dispatch_workloop_invoke + 1784 9 libdispatch.dylib 0x00007fff2037ec0d _dispatch_workloop_worker_thread + 811 10 libsystem_pthread.dylib 0x00007fff2051545d _pthread_wqthread + 314 11 libsystem_pthread.dylib 0x00007fff2051442f start_wqthread + 15 I do have a DispatchSourceTimer but I cancel it in the stop func. Any ideas on how to tackle this?
7
0
166
Nov ’25
[iOS 26] Unable to start TLS handshake connection to devices with self-signed certificates
Hi there, We are facing some issues regarding TLS connectivity: Starting with iOS 26, the operating system refuses to open TLS sockets to local devices with self-signed certificates over Wi-Fi. In this situation, connection is no longer possible, even if the device is detected on the network with Bonjour. We have not found a workaround for this problem. We've tryied those solutions without success: Added the 'NSAppTransportSecurity' key to the info.plist file, testing all its items, such as "NSAllowsLocalNetworking", "NSExceptionDomains", etc. Various code changes to use properties such as "sec_protocol_options_set_local_identity" and "sec_protocol_options_set_tls_server_name" to no avail. Brutally import the certificate files into the project and load them via, for example, "Bundle.main.url(forResource: "nice_INTERFACE_server_cert", withExtension: "crt")", using methods such as sec_trust_copy_ref and SecCertificateCopyData. Download the .pem or .crt files to the iPhone, install them (now visible under "VPN & Device Management"), and then flag them as trusted by going to "Settings -> General -> Info -> Trust". certificates" The most critical part seems to be the line sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { $2(true) }, queue) whose purpose is to bypass certificate checks and validate all of them (as apps already do). However, on iOS26, if I set a breakpoint on leg$2(true),` it never gets there, while on iOS 18, it does. I'll leave as example the part of the code that was tested the most below. Currently, on iOS26, the handler systematically falls back to .cancelled: func startConnection(host: String, port: UInt16) { self.queue = DispatchQueue(label: "socketQueue") let tlsOptions = NWProtocolTLS.Options() sec_protocol_options_set_verify_block(tlsOptions.securityProtocolOptions, { $2(true) }, queue) let parameters = NWParameters(tls: tlsOptions) self.nwConnection = NWConnection(host: .init(host), port: .init(rawValue: port)!, using: parameters) self.nwConnection.stateUpdateHandler = { [weak self] state in switch state { case .setup: break case .waiting(let error): self?.connectionDidFail(error: error) case .preparing: break case .ready: self?.didConnectSubject.onNext(Void()) case .failed(let error): self?.connectionDidFail(error: error) case .cancelled: self?.didDisconnectSubject.onNext(nil) @unknown default: break } } self.setupReceive() self.nwConnection.start(queue: queue) } These are the prints made during the procedure. The ones with the dot are from the app, while the ones without are warnings/info from Xcode: 🔵 INFO WifiNetworkManager.connect():52 - Try to connect onto the interface access point with ssid NiceProView4A9151_AP 🔵 INFO WifiNetworkManager.connect():68 - Connected to NiceProView4A9151_AP tcp_output [C13:2] flags=[R.] seq=215593821, ack=430284980, win=4096 state=CLOSED rcv_nxt=430284980, snd_una=215593821 nw_endpoint_flow_failed_with_error [C13 192.168.0.1:443 in_progress channel-flow (satisfied (Path is satisfied), viable, interface: en0[802.11], dns, uses wifi, LQM: unknown)] already failing, returning nw_connection_copy_protocol_metadata_internal_block_invoke [C13] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection nw_connection_copy_protocol_metadata_internal_block_invoke [C13] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection nw_connection_copy_connected_local_endpoint_block_invoke [C13] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection nw_connection_copy_connected_remote_endpoint_block_invoke [C13] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection nw_connection_copy_protocol_metadata_internal_block_invoke [C14] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection nw_connection_copy_protocol_metadata_internal_block_invoke [C14] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection nw_connection_copy_connected_local_endpoint_block_invoke [C14] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection nw_connection_copy_connected_remote_endpoint_block_invoke [C14] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection [C14 192.168.0.1:443 tcp, tls, attribution: developer] is already cancelled, ignoring cancel [C14 192.168.0.1:443 tcp, tls, attribution: developer] is already cancelled, ignoring cancel nw_connection_copy_protocol_metadata_internal_block_invoke [C15] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection nw_connection_copy_protocol_metadata_internal_block_invoke [C15] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection nw_connection_copy_connected_local_endpoint_block_invoke [C15] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection nw_connection_copy_connected_remote_endpoint_block_invoke [C15] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection nw_connection_copy_protocol_metadata_internal_block_invoke [C16] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection nw_connection_copy_protocol_metadata_internal_block_invoke [C16] Client called nw_connection_copy_protocol_metadata_internal on unconnected nw_connection nw_connection_copy_connected_local_endpoint_block_invoke [C16] Client called nw_connection_copy_connected_local_endpoint on unconnected nw_connection nw_connection_copy_connected_remote_endpoint_block_invoke [C16] Client called nw_connection_copy_connected_remote_endpoint on unconnected nw_connection [C16 192.168.0.1:443 tcp, tls, attribution: developer] is already cancelled, ignoring cancel [C16 192.168.0.1:443 tcp, tls, attribution: developer] is already cancelled, ignoring cancel 🔴 ERROR InterfaceDisconnectedViewModel.connect():51 - Sequence timeout.
1
0
212
Oct ’25
“Wi-Fi Aware Sample” on Phone quit unexpectedly.
The app “Wi-Fi Aware Sample” on Bojie的iPhone quit unexpectedly. Domain: IDEDebugSessionErrorDomain Code: 20 Failure Reason: Message from debugger: The LLDB RPC server has crashed. You may need to manually terminate your process. The crash log is located in ~/Library/Logs/DiagnosticReports and has a prefix 'lldb-rpc-server'. Please file a bug and attach the most recent crash log. User Info: { DVTErrorCreationDateKey = "2025-09-17 10:26:56 +0000"; IDEDebugSessionErrorUserInfoUnavailabilityError = "Error Domain=com.apple.dt.deviceprep Code=-10 "Fetching debug symbols for Bojie\U7684iPhone" UserInfo={NSLocalizedRecoverySuggestion=Xcode will continue when the operation completes., NSLocalizedDescription=Fetching debug symbols for Bojie\U7684iPhone}"; IDERunOperationFailingWorker = DBGLLDBLauncher; } Event Metadata: com.apple.dt.IDERunOperationWorkerFinished : { "device_identifier" = "00008101-001E29E01E63003A"; "device_isCoreDevice" = 1; "device_model" = "iPhone13,3"; "device_osBuild" = "26.0 (23A341)"; "device_osBuild_monotonic" = 2300034100; "device_os_variant" = 1; "device_platform" = "com.apple.platform.iphoneos"; "device_platform_family" = 2; "device_reality" = 1; "device_thinningType" = "iPhone13,3"; "device_transport" = 1; "dvt_coredevice_version" = "477.23"; "dvt_coredevice_version_monotonic" = 477023000000000; "dvt_coresimulator_version" = 1043; "dvt_coresimulator_version_monotonic" = 1043000000000000; "dvt_mobiledevice_version" = "1818.0.1"; "dvt_mobiledevice_version_monotonic" = 1818000001000000; "launchSession_schemeCommand" = Run; "launchSession_schemeCommand_enum" = 1; "launchSession_targetArch" = arm64; "launchSession_targetArch_enum" = 6; "operation_duration_ms" = 1922640; "operation_errorCode" = 20; "operation_errorDomain" = IDEDebugSessionErrorDomain; "operation_errorWorker" = DBGLLDBLauncher; "operation_error_reportable" = 1; "operation_name" = IDERunOperationWorkerGroup; "operation_unavailabilityErrorCode" = "-10"; "operation_unavailabilityErrorDomain" = "com.apple.dt.deviceprep"; "param_consoleMode" = 1; "param_debugger_attachToExtensions" = 0; "param_debugger_attachToXPC" = 1; "param_debugger_type" = 3; "param_destination_isProxy" = 0; "param_destination_platform" = "com.apple.platform.iphoneos"; "param_diag_MainThreadChecker_stopOnIssue" = 0; "param_diag_MallocStackLogging_enableDuringAttach" = 0; "param_diag_MallocStackLogging_enableForXPC" = 1; "param_diag_allowLocationSimulation" = 1; "param_diag_checker_mtc_enable" = 1; "param_diag_checker_tpc_enable" = 1; "param_diag_gpu_frameCapture_enable" = 0; "param_diag_gpu_shaderValidation_enable" = 0; "param_diag_gpu_validation_enable" = 0; "param_diag_guardMalloc_enable" = 0; "param_diag_memoryGraphOnResourceException" = 0; "param_diag_queueDebugging_enable" = 1; "param_diag_runtimeProfile_generate" = 0; "param_diag_sanitizer_asan_enable" = 0; "param_diag_sanitizer_tsan_enable" = 0; "param_diag_sanitizer_tsan_stopOnIssue" = 0; "param_diag_sanitizer_ubsan_enable" = 0; "param_diag_sanitizer_ubsan_stopOnIssue" = 0; "param_diag_showNonLocalizedStrings" = 0; "param_diag_viewDebugging_enabled" = 1; "param_diag_viewDebugging_insertDylibOnLaunch" = 1; "param_install_style" = 2; "param_launcher_UID" = 2; "param_launcher_allowDeviceSensorReplayData" = 0; "param_launcher_kind" = 0; "param_launcher_style" = 99; "param_launcher_substyle" = 0; "param_lldbVersion_component_idx_1" = 0; "param_lldbVersion_monotonic" = 170300230950; "param_runnable_appExtensionHostRunMode" = 0; "param_runnable_productType" = "com.apple.product-type.application"; "param_testing_launchedForTesting" = 0; "param_testing_suppressSimulatorApp" = 0; "param_testing_usingCLI" = 0; "sdk_canonicalName" = "iphoneos26.0"; "sdk_osVersion" = "26.0"; "sdk_platformID" = 2; "sdk_variant" = iphoneos; "sdk_version_monotonic" = 2300527605; } System Information macOS Version 15.5 (Build 24F74) Xcode 26.0 (24141.31) (Build 17A5241o) Timestamp: 2025-09-17T18:26:56+08:00
3
0
189
Sep ’25
OSX and ARP spoofing
Here's a simple program that spoofs an ARP reply for a given IP address. If I spin up two terminal sessions on the same machine. Run this code in one window % ./spoof en0 192.168.1.7 Listening on en0 for ARP requests to 192.168.1.7 Spoofing MAC: 00:0c:87:47:50:27 And in the second window cause the OS to issue an ARP_REQ % ping 192.168.1.7 You will see the program respond to the ARP request. (Wireshark will see the ARP_REQ and ARP_REPLY packets) however my arp table isn't updated with the MAC for the IP address. There is no firewall active. % arp -a|grep 192.168.1.7 (192.168.1.7) at (incomplete) on en0 ifscope [ethernet] This is running on a MacBook pro M3 (OSX 15.4). HOWEVER, on a MacBook pro M4 (OSX 15.2) is does Work !!!!! Can anyone explain why its not working? spoof.txt
2
0
513
Feb ’25
UDP Broadcast/Multicast Reception Stops for Intervals
Our app receives real-time GPS and aircraft data from devices via UDP broadcast and/or multicast on a WiFi network created by the device. We have identified that the iPhone or iPad will just stop receiving UDP broadcast/multicast data for an interval of time. In general, it appears after roughly every 128KB of data is received. In the attached screenshot from Xcode instruments, you can see the data reception alternating on/off. We have verified with Wireshark that the data is still flowing during that entire time period. And by tracking bytes received the app ultimately receives about 55% of the bytes, which tracks with the Network graph. We have used different approaches to the network code, including GCDAsyncUdpSocket, BSD Sockets, and the Network framework. We've tried it on background threads and the main thread. Tested it on iPads and iPhones. All produce the same result. The data is just never reaching the app code. Any insight on what may be temporarily disabling data reception?
1
0
261
Mar ’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.4k
Apr ’25
NWPathMonitor Reports Unexpected satisfied→unsatisfied→satisfied Sequence After WiFi Re-enablement
I am developing an iOS application using NWPathMonitor for network connectivity monitoring. We discovered a reproducible issue where disabling and re-enabling WiFi triggers an unexpected network status sequence. ENVIRONMENT: iOS Version: 17.x Device: iPhone (various models tested) Network Framework: NWPathMonitor from iOS Network framework STEPS TO REPRODUCE: Device connected to WiFi normally Disable WiFi via Settings or Control Center Re-enable WiFi via Settings or Control Center EXPECTED BEHAVIOR: WiFi reconnects and NWPathMonitor reports stable satisfied status ACTUAL BEHAVIOR: T+0s: WiFi re-enables, NWPathMonitor reports path.status = .satisfied T+8s: NWPathMonitor unexpectedly reports path.status = .unsatisfied with unsatisfiedReason = .notAvailable T+9-10s: NWPathMonitor reports path.status = .satisfied again Connection becomes stable afterward NETWORK PATH TIMELINE: T+0s: satisfied (IPv4: true, DNS: false) T+140ms: satisfied (IPv4: true, DNS: true) T+8.0s: unsatisfied (reason: notAvailable, no interfaces available) T+10.0s: satisfied (IPv4: true, DNS: true) KEY OBSERVATIONS: Timing consistency: unsatisfied event always occurs ~8 seconds after reconnection resolution: "Reset Network Settings" eliminates this behavior TECHNICAL QUESTIONS: What causes the 8-second delayed unsatisfied status after WiFi re-enablement? Is this expected behavior that applications should handle? Why does reset network setting in iPhone fix this issue?
1
0
100
Jul ’25
Symbol not found: ___res_9_state
When running my app with Xcode16.4, it crashed with the error: dyld[1045]: Symbol not found: ___res_9_state Referenced from: <8B329554-5BEF-38D0-BFCD-1731FA6120CB> /private/var/containers/Bundle/Application/00C941BA-E397-4D0B-B280-E75583FF2890/xxx.app/xxx.debug.dylib Expected in: <7D74C679-8F55-3A01-9AA2-C205A4A19D3E> /usr/lib/libresolv.9.dylib The ___res_9_state related code in my app is: let state = __res_9_state() res_9_ninit(state) var servers = [res_9_sockaddr_union](repeating: res_9_sockaddr_union(), count: Int(MAXNS)) let found = Int(res_9_getservers(state, &servers, Int32(MAXNS))) res_9_nclose(state) if found > 0 { return Array(servers[0..<found]).filter() { $0.sin.sin_len > 0 } } else { return [] } Previously, __res_9_state() could run normally in Xcode 16.1 How to fix this problem?
2
0
175
Jul ’25
Crash when deallocating NEAppProxyFlow
Hello, I'm working on a Transparent Proxy and when the proxy is being stopped, I'm stopping all the flows by calling flow.closeWriteWithError(POSIXError(.ECANCELED)) flow.closeReadWithError(POSIXError(.ECANCELED)) Then all the flows are deallocated. When deallocating the flow the crash occurs: OS Version: macOS 14.1.2 (23B92) Exception Type: EXC_BREAKPOINT (SIGTRAP) Exception Codes: 0x0000000000000001, 0x000000018c2ef704 Termination Reason: Namespace SIGNAL, Code 5 Trace/BPT trap: 5 Terminating Process: exc handler [553] Thread 32 Crashed:: Dispatch queue: <my dispatch queue> 0 CoreFoundation 0x18c2ef704 CF_IS_OBJC + 76 1 CoreFoundation 0x18c23f61c CFErrorGetDomain + 32 2 libnetworkextension.dylib 0x19fe56a00 flow_error_to_errno + 28 3 libnetworkextension.dylib 0x19fe56920 flow_handle_pending_write_requests + 216 4 libnetworkextension.dylib 0x19fe5667c __NEFlowDeallocate + 380 5 CoreFoundation 0x18c2efe28 _CFRelease + 292 6 NetworkExtension 0x19d208390 -[NEAppProxyFlow dealloc] + 36 Is there any way to debug what is happening and if it's related to closing the flow with POSIXError? Thank you
3
0
204
Jul ’25
Need Help with TUN Writeback
Hi everyone, I'm currently experimenting with building a simple DNS filter using Apple's Packet Tunnel framework. Here's the flow I'm trying to implement: Create a TUN interface Set up a UDP socket Read packets via packetFlow.readPackets Parse the raw IP packet Forward the UDP payload through the socket Receive the response from the server Reconstruct the IP packet with the response Write it back to the TUN interface using packetFlow.writePackets Here’s an example of an intercepted IP packet (DNS request): 45 00 00 3c 15 c4 00 00 40 11 93 d1 c0 a8 00 64 08 08 08 08 ed 6e 00 35 00 28 e5 c9 7f da 01 00 00 01 00 00 00 00 00 00 04 74 69 6d 65 05 61 70 70 6c 65 03 63 6f 6d 00 00 01 00 01 And here’s the IP packet I tried writing back into the TUN interface (DNS response): 45 00 00 89 5e 37 40 00 40 11 0b 11 08 08 08 08 c0 a8 00 64 00 35 ed 6e 00 75 91 e8 7f da 81 80 00 01 00 04 00 00 00 00 04 74 69 6d 65 05 61 70 70 6c 65 03 63 6f 6d 00 00 01 00 01 c0 0c 00 05 00 01 00 00 0c fb 00 11 04 74 69 6d 65 01 67 07 61 61 70 6c 69 6d 67 c0 17 c0 2c 00 01 00 01 00 00 03 04 00 04 11 fd 74 fd c0 2c 00 01 00 01 00 00 03 04 00 04 11 fd 74 7d c0 2c 00 01 00 01 00 00 03 04 00 04 11 fd 54 fb Unfortunately, it seems the packet is not being written back correctly to the TUN interface. I'm not seeing any expected DNS response behavior on the device. Also, I noticed that after creating the TUN, the interface address shows up as 0.0.0.0:0 in Xcode. The system log includes this message when connecting the VPN: NWPath does not have valid interface: satisfied (Path is satisfied), interface: utun20[endc_sub6], ipv4, dns, expensive, uses cellular Does anyone know how to properly initialize the TUN so that the system recognizes it with a valid IP configuration? Or why my written-back packet might be getting ignored? Any help would be appreciated!
1
0
84
Jul ’25
Verifying TLS 1.3 early_data behavior on iOS 26
Development environment Xcode 26.0 Beta 6 iOS 26 Simulator macOS 15.6.1 To verify TLS 1.3 session resumption behavior in URLSession, I configured URLSessionConfiguration as follows and sent an HTTP GET request: let config = URLSessionConfiguration.ephemeral config.tlsMinimumSupportedProtocolVersion = .TLSv13 config.tlsMaximumSupportedProtocolVersion = .TLSv13 config.httpMaximumConnectionsPerHost = 1 config.httpAdditionalHeaders = ["Connection": "close"] config.enablesEarlyData = true let session = URLSession(configuration: config, delegate: nil, delegateQueue: nil) let url = URL(string: "https://www.google.com")! var request = URLRequest(url: url) request.assumesHTTP3Capable = true request.httpMethod = "GET" let task = session.dataTask(with: request) { data, response, error in if let error = error { print("Error during URLSession data task: \(error)") return } if let data = data, let responseString = String(data: data, encoding: .utf8) { print("Received data via URLSession: \(responseString)") } else { print("No data received or data is not UTF-8 encoded") } } task.resume() However, after capturing the packets, I found that the ClientHello packet did not include the early_data extension. It seems that enablesEarlyData on URLSessionConfiguration is not being applied. How can I make this work properly?
1
0
138
Aug ’25
NEHotspotNetwork headaches
I'm trying to use NEHotspotNetwork to configure an IoT. I've read all the issues that have plagued other developers when using this framework, and I was under the impression that bugs were filed and fixed. Here are my issues in hopes that someone can catch my bug, or has finally figured this out and it's not a bug in the framework with no immediate fix on the horizon. If I use the following code: let config = NEHotspotConfiguration(ssid: ssid) config.joinOnce = true KiniStatusBanner.shared.show(text: "Connecting to Kini", in: presentingVC.view) NEHotspotConfigurationManager.shared.apply(config) { error in DispatchQueue.main.async { if let nsError = error as NSError?, nsError.domain == NEHotspotConfigurationErrorDomain, nsError.code == NEHotspotConfigurationError.alreadyAssociated.rawValue { print("Already connected to \(self.ssid)") KiniStatusBanner.shared.dismiss() self.presentCaptivePortal(from: presentingVC, activationCode: activationCode) } else if let error = error { // This doesn't happen print("❌ Failed to connect: \(error.localizedDescription)") KiniStatusBanner.shared.update(text: "Failed to Connect to Kini. Try again later.") KiniStatusBanner.shared.dismiss(after: 2.5) } else { // !!!! Most often, this is the path the code takes NEHotspotNetwork.fetchCurrent { current in if let ssid = current?.ssid, ssid == self.ssid { log("✅✅ 1st attempt: connected to \(self.ssid)") KiniStatusBanner.shared.dismiss() self.presentCaptivePortal(from: presentingVC, activationCode: activationCode) } else { // Dev forums talked about giving things a bit of time to settle and then try again DispatchQueue.main.asyncAfter(deadline: .now() + 2) { NEHotspotNetwork.fetchCurrent { current in if let ssid = current?.ssid, ssid == self.ssid { log("✅✅✅ 2nd attempt: connected to \(self.ssid)") KiniStatusBanner.shared.dismiss() self.presentCaptivePortal(from: presentingVC, activationCode: activationCode) } else { log("❌❌❌ 2nd attempt: Failed to connect: \(self.ssid)") KiniStatusBanner.shared.update(text: "Could not join Kini network. Try again.") KiniStatusBanner.shared.dismiss(after: 2.5) self.cleanupHotspot() DispatchQueue.main.asyncAfter(deadline: .now() + 2) { print("cleanup again") self.cleanupHotspot() } } } } log("❌❌ 1st attempt: Failed to connect: \(self.ssid)") KiniStatusBanner.shared.update(text: "Could not join Kini network. Try again.") KiniStatusBanner.shared.dismiss(after: 2.5) self.cleanupHotspot() } As you can see, one can't just use NEHotspotConfigurationManager.shared.apply and has to double-check to make sure that it actually succeeds, by checking to see if the SSID desired, matches the one that the device is using. Ok, but about 50% of the time, the call to NEHotspotNetwork.fetchCurrent gives me this error: NEHotspotNetwork nehelper sent invalid result code [1] for Wi-Fi information request Well, there is a workaround for that randomness too. At some point before calling this code, one can: let locationManager = CLLocationManager() locationManager.requestWhenInUseAuthorization() That eliminates the NEHotspotNetwork nehelper sent invalid result code [1] for Wi-Fi information request BUT... three issues. The user is presented with an authorization alert: Allow "Kini" to use your location? This app needs access to you Wi-Fi name to connect to your Kini device. Along with a map with a location pin on it. This gives my users a completely wrong impression, especially for a device/app where we promise users not to track their location. They actually see a map with their location pinned on it, implying something that would freak out anyone who was expecting no tracking. I understand why an authorization is normally required, but since all we are getting is our own IoT's SSID, there should be no need for an authorization for this, and no map associated with the request. Again, they are accessing my IoT's network, NOT their home/location Wi-Fi SSID. My app already knows and specifies that network, and all I am trying to do is to work around a bug that makes it look like I have a successful return from NEHotspotConfigurationManager.shared.apply() when in fact the network I was looking for wasn't even on. Not only do I get instances where the network doesn't connect, and result codes show no errors, but I also get instances where I get an alert that says that the network is unreachable, yet my IoT shows that the app is connected to its Wi-Fi. On the iOS device, I go to the Wi-Fi settings, and see that I am on the IoT's network. So basically, sometimes I connect, but the frameworks says that there is no connection, and sometimes it reports a connection when there is none. As you can see in the code, I call cleanupHotspot() to make the iOS device get off of my temp Wi-Fi SSID. This is the code: func cleanupHotspot() { NEHotspotConfigurationManager.shared.removeConfiguration(forSSID: ssid) } That code gets called by the above code when things aren't as I expect and need to cleanup. And I also call it when the user dismisses the viewcontroller that is attempting to make the connection. It doesn't always work. I get stuck on the tempo SSID, unless I go through this whole thing again: try to make the connection again, this time it succeeds quickly, and then I can disconnect. Any ideas? I'm on iOS18.5, and have tried this on multiple iPhones including 11, 13 and 16.
3
0
154
Nov ’25
Could not delete cookies on IOS18
Hello, I have encountered an issue with an iPhone 15PM with iOS 18.5. The NSHTTPCookieStorage failed to clear cookies, after clearing them, I was still able to retrieve them. However, on the same system NSHTTPCookie *cookie; NSHTTPCookieStorage *storage = [NSHTTPCookieStorage sharedHTTPCookieStorage]; for (cookie in [storage cookies]) { [storage deleteCookie:cookie]; } NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:[[self url] absoluteURL]]; // still able to get cookies,why???
1
0
97
Jun ’25
Why does an NSURLSessionDataTask sent from PacketTunnelProvider intermittently fail with error code NSURLErrorTimedOut (-1001) ?
Hi, We're hoping someone can help us determine why we're running into some odd behavior where a simple HTTP request is intermittently failing with error code NSURLErrorTimedOut (-1001) Background: HTTP request details: The request is sent from a PacketTunnelProvider and is meant to be a Captive Portal check. The request is insecure (HTTP, instead of HTTPS) but we have configured App Transport Security (ATS) to allow insecure HTTP loads from this hostname. See info.plist excerpt below. The request is sent using NSMutableURLRequest/NSURLSessionDataTask using an Ephemeral session configuration. We only modify 2 properties on NSMutableURLRequest The timeoutInterval property is set to 5 seconds. The allowsCellularAccess property is set to NO. No headers or other configuration are modified. NSURLSessionDataTask completionHandler receives an NSError: We checked the NSError's userInfo dictionary for an underlying error (NSUnderlyingErrorKey). The underlying error shows the same code NSURLErrorTimedOut (-1001). We haven't seen any underlying errors with code NSURLErrorAppTransportSecurityRequiresSecureConnection (-1022) . On a laptop, we confirmed that the Captive portal check site is accessible and loads correctly. Laptop and iOS device are on the same Wi-fi. I've witnessed the error in the debugger, and been able to load the site on my laptop at the same time. So, we don't have any reason to believe this is server related. The PacketTunnelProvider is configured to only handle DNS queries and is not intercepting/routing the HTTP traffic. The DNS query for the Captive portal request is handled correctly. In fact, outside of the PacketTunnelProvider, all sites load in Mobile Safari. So, we're not breaking internet on this device. In other words, we have no reason to believe our DNS handling is interfering with the HTTP request since other HTTP requests are working as expected. We setup CFNetwork Diagnostic Logging (https://developer.apple.com/documentation/network/debugging-https-problems-with-cfnetwork-diagnostic-logging) In console.app, we are able to find some logging on the Timeout See excerpt from Console.app's log below. We confirmed that the nscurl tool did not flag the request (https://developer.apple.com/documentation/security/identifying-the-source-of-blocked-connections) All ATS tests run with nscurl were successful. See nscurl command used below. Questions: What are next steps to debug this intermittent timeout? What should we look for in the CFNetwork Diagnostic Logging to help debug the issue further? Thanks in advance for your help! ATS configuration setup in both the UI and the PacketTunnel's info.plist file: <key>NSAppTransportSecurity</key> <dict> <key>NSExceptionDomains</key> <dict> <key>subdomain.subdomain.example.com</key> <dict> <key>NSExceptionAllowsInsecureHTTPLoads</key> <true/> <key>NSIncludesSubdomains</key> <true/> </dict> </dict> </dict> Excerpt from Console.app's log: CFNetwork Example PacketTunnel 10836 Diagnostics default 11:30:33.029032-0700 CFNetwork Diagnostics [3:834] 11:30:32.946 { Did Timeout: (null) Loader: request GET http://subdomain.subdomain.example.com/content/cpcheck.txt HTTP/1.1 Timeout Interval: 5.000 seconds init to origin load: 0.000592947s total time: 5.00607s total bytes: 0 } [3:834] nscurl command $ /usr/bin/nscurl --ats-diagnostics --verbose http://subdomain.subdomain.example.com/content/cpcheck.txt
2
0
88
Jun ’25