We are currently working on a zero-configuration networking compliant device thru avahi-daemon.
Our Device want to have multiple Instance name for different services.
Example
InstanceA._ipps._tcp.local.
InstanceA._ipp._tcp.local.
InstanceB._ipps._tcp.local.
InstanceB._ipp._tcp.local.
Will BCT confuse this as multiple device connected in the network and cause it to fail? Does Bonjour only allows only a Single Instance name with multiple services?
Networking
RSS for tagExplore the networking protocols and technologies used by the device to connect to Wi-Fi networks, Bluetooth devices, and cellular data services.
Selecting any option will automatically load the page
Post
Replies
Boosts
Views
Activity
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.***.***.***"), 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)
}
Hello,
I recently replaced NSURLConnection with NSURLSession in my application, and I have noticed a significant decrease in network speed. I am seeking advice on why this might be happening and how to resolve the issue.
Here is the code before the change:
Old Code:
- (void)execute:(id<STHttpEntity>)entity
{
[entity addHeader:API_HDR_CLASS_NAME value:self.apiClass];
[entity addHeader:API_HDR_METHOD_NAME value:self.apiMethod];
NSMutableURLRequest *req = [self createRequest:entity];
self.connection = [[NSURLConnection alloc]
initWithRequest:req
delegate:self];
[self.connection start];
}
- (void)connectionDidFinishLoading:(NSURLConnection *)connection
{
[self clearConnectionTimeout];
self.requestData = nil;
if (self.httpStatus != HTTPSTATUS_OK) {
[self callFailedWithStatus:self.httpStatus];
return;
}
[self callSucceeded];
}
And here is the code after the change:
New Code:
- (void)execute:(id<STHttpEntity>)entity
{
[entity addHeader:API_HDR_CLASS_NAME value:self.apiClass];
[entity addHeader:API_HDR_METHOD_NAME value:self.apiMethod];
NSMutableURLRequest *req = [self createRequest:entity];
NSURLSessionConfiguration *sessionConfig = [NSURLSessionConfiguration defaultSessionConfiguration];
NSURLSession *session = [NSURLSession sessionWithConfiguration:sessionConfig delegate:self delegateQueue:nil];
self.dataTask = [session dataTaskWithRequest:req];
[self.dataTask resume];
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task didCompleteWithError:(nullable NSError *)error
{
[self clearConnectionTimeout];
self.requestData = nil;
if (error) {
[self callFailed:error];
} else {
[self callSucceeded];
}
}
Issue: After replacing NSURLConnection with NSURLSession, the network speed has significantly decreased. The new implementation seems to be much slower than the old one.
Questions:
1.What could be the reasons for the significant decrease in network speed after switching to NSURLSession?
2.Are there any specific configurations or best practices for NSURLSession that I should be aware of to improve performance?
3.Is there any known issue with NSURLSession that could cause such a performance drop?
Any insights or suggestions would be greatly appreciated. Thank you in advance for your help!
We put the apple-app-site-association file at https://ourdomain.com.tr/.well-known/apple-app-site-association.
When we send a request to url, we get 200 response code every time and we can see the file. But sometimes when we try to access https://app-site-association.cdn-apple.com/a/v1/ourdomain.com.tr url with browser or CMD tool, we are facing with 404 response code.
There isn't any ip adress filter in our systems and we tried using vpn for sending same request from different locations(america and europe) but nothing changed.
In addition, can anyone provide the ip list of apple cdn servers to check the F5 Load balancer WAF logs?
CMD output:
C:\Users\Name>curl -Lv https://app-site-association.cdn-apple.com/a/v1/ourdomain.com.tr
Host app-site-association.cdn-apple.com:443 was resolved.
IPv6: (none)
IPv4: 17.253.122.197, 17.253.15.210, 17.253.122.196, 17.253.107.201, 17.253.57.203, 17.253.15.198, 17.253.57.200
Trying 17.253.122.197:443...
Connected to app-site-association.cdn-apple.com (17.253.122.197) port 443
schannel: disabled automatic use of client certificate
ALPN: curl offers http/1.1
ALPN: server accepted http/1.1
using HTTP/1.x
GET /a/v1/ourdomain.com HTTP/1.1
Host: app-site-association.cdn-apple.com
User-Agent: curl/8.9.1
Accept: /
Request completely sent off
schannel: remote party requests renegotiation
schannel: renegotiating SSL/TLS connection
schannel: SSL/TLS connection renegotiated
< HTTP/1.1 404 Not Found
< Apple-Failure-Details: {"cause":"context deadline exceeded (Client.Timeout exceeded while awaiting headers)"}
< Apple-Failure-Reason: SWCERR00301 Timeout
< Apple-From: https://ourdomain.com.tr/.well-known/apple-app-site-association
< Apple-Try-Direct: true
< Cache-Control: max-age=3600,public
< Content-Length: 10
< Content-Type: text/plain; charset=utf-8
< Date: Mon, 14 Apr 2025 12:52:04 GMT
< Expires: Mon, 14 Apr 2025 12:52:14 GMT
< Age: 1770
< Via: http/1.1 uklon5-vp-vst-004.ts.apple.com (acdn/268.14469), https/1.1 uklon5-vp-vfe-002.ts.apple.com (acdn/268.14469), http/1.1 frmrs1-edge-mx-008.ts.apple.com (acdn/268.14469), http/1.1 frmrs1-edge-fx-005.ts.apple.com (acdn/268.14469)
< X-Cache: hit-fresh, hit-stale, hit-fresh, hit-fresh
< CDNUUID: 9e72cf99-1503-4644-9ea3-173328a25c94-31496306226
< Connection: keep-alive
<
Not Found
Connection #0 to host app-site-association.cdn-apple.com left intact
I’m developing an app designed for hospital environments, where public internet access may not be available. The app includes two components: the main app and a Local Connectivity Extension. Both rely on persistent TCP socket connections to communicate with a local server.
We’re observing a recurring issue where the extension’s socket becomes unresponsive every 1–3 hours, but only when the device is on the lock screen, even if the main app remains in the foreground.
When the screen is not locked, the connection is stable and no disconnections occur.
❗ Issue Details:
• What’s going on: The extension sends a keep-alive ping packet every second, and the server replies with a pong and a system time packet.
• The bug: The server stops receiving keep alive packets from the extension.
• On the server, we detect about 30 second gap on the server, a gap that shows no packets were received by the extension. This was confirmed via server logs and Wireshark).
• On the extension, from our logs there was no gap in sending packets. From it’s perspective, all packets were sent with no error.
• Because no packet are being received by the server, no packets will be sent to the extension. Eventually the server closes the connection due to keep-alive timeout.
• FYI we log when the NEAppPushProvider subclass sleeps and it did NOT go to sleep while we were debugging.
🧾 Example Logs:
Extension log:
2025-03-24 18:34:48.808 sendKeepAliveRequest()
2025-03-24 18:34:49.717 sendKeepAliveRequest()
2025-03-24 18:34:50.692 sendKeepAliveRequest()
... // continuous sending of the ping packet to the server, no problems here
2025-03-24 18:35:55.063 sendKeepAliveRequest()
2025-03-24 18:35:55.063 keepAliveTimer IS TIME OUT... in CoreService. // this is triggered because we did not receive any packets from the server
2025-03-24 18:34:16.298 No keep-alive received for 16 seconds... connection ID=95b3... // this shows that there has been no packets being received by the extension
...
2025-03-24 18:34:30.298 Connection timed out on keep-alive. connection ID=95b3... // eventually closes due to no packets being received
2025-03-24 18:34:30.298 Remote Subsystem Disconnected {name=iPhone|Replica-Ext|...}
✅ Observations:
• The extension process continues running and logging keep-alive attempts.
• However, network traffic stops reaching the server, and no inbound packets are received by the extension.
• It looks like the socket becomes silently suspended or frozen, without being properly closed or throwing an error.
❓Questions:
• Do you know why this might happen within a Local Connectivity Extension, especially under foreground conditions and locked ?
• Is there any known system behavior that might cause the socket to be suspended or blocked in this way after running for a few hours?
Any insights or recommendations would be greatly appreciated.
Thank you!
For important background information, read Extra-ordinary Networking before reading this.
Share and Enjoy
—
Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"
On Host Names
I commonly see questions like How do I get the device’s host name? This question doesn’t make sense without more context. Apple systems have a variety of things that you might consider to be the host name:
The user-assigned device name — This is a user-visible value, for example, Guy Smiley. People set this in Settings > General > About > Name.
The local host name — This is a DNS name used by Bonjour, for example, guy-smiley.local. By default this is algorithmically derived from the user-assigned device name. On macOS, people can override this in Settings > General > Sharing > Local hostname.
The reverse DNS name associated with the various IP addresses assigned to the device’s various network interfaces
That last one is pretty much useless. You can’t get a single host name because there isn’t a single IP address. For more on that, see Don’t Try to Get the Device’s IP Address.
The other two have well-defined answers, although those answers vary by platform. I’ll talk more about that below.
Before getting to that, however, let’s look at the big picture.
Big Picture
The use cases for the user-assigned device name are pretty clear. I rarely see folks confused about that.
Another use case for this stuff is that you’ve started a server and you want to tell the user how to connect to it. I discuss this in detail in Showing Connection Information in an iOS Server.
However, most folks who run into problems like this do so because they’re suffering from one of the following misconceptions:
The device has a DNS name.
Its DNS name is unique.
Its DNS name doesn’t change.
Its DNS name is in some way useful for networking.
Some of these may be true in some specific circumstances, but none of them are true in all circumstances.
These issues are not unique to Apple platforms — if you look at the Posix spec for gethostname, it says nothing about DNS! — but folks tend to notice these problems more on Apple platforms because Apple devices are often deployed to highly dynamic network environments.
So, before you start using the APIs discussed in this post, think carefully about your assumptions.
And if you actually do want to work with DNS, there are two cases to consider:
If you’re looking for the local host name, use the APIs discussed above.
In other cases, it’s likely that the APIs in this post will not be helpful and you’d be better off focusing on DNS APIs [1].
[1] The API I recommend for this is DNS-SD. See the DNS section in TN3151 Choosing the right networking API.
macOS
To get the user-assigned device name, call the SCDynamicStoreCopyComputerName(_:_:) function. For example:
let userAssignedDeviceName = SCDynamicStoreCopyComputerName(nil, nil) as String?
To get the local host name, call the SCDynamicStoreCopyLocalHostName(_:) function. For example:
let localHostName = SCDynamicStoreCopyLocalHostName(nil) as String?
IMPORTANT This returns just the name label. To form a local host name, append .local..
Both routines return an optional result; code defensively!
If you’re displaying these values to the user, use the System Configuration framework dynamic store notification mechanism to keep your UI up to date.
iOS and Friends
On iOS, iPadOS, tvOS, and visionOS, get the user-assigned device name from the name property on UIDevice.
IMPORTANT Access to this is now restricted. For more on that, see the documentation for the com.apple.developer.device-information.user-assigned-device-name entitlement.
There is no direct mechanism to get the local host name.
Other APIs
There are a wide variety of other APIs that purport to return the host name. These include:
gethostname
The name property on NSHost [1]
The hostName property on NSProcessInfo (ProcessInfo in Swift)
These are problematic for a number of reasons:
They have a complex implementation that makes it hard to predict what value you’ll get back.
They might end up trying to infer the host name from the network environment.
The existing behaviour is hard to change due to compatibility concerns.
Some of them are marked as to-be-deprecated.
IMPORTANT The second issue is particularly problematic, because it involves synchronous DNS requests [2]. That’s slow in general. Worse yet, if the network environment is restricted in some way, these calls can be very slow, taking about 30 seconds to time out.
Given these problems, it’s generally best to avoid calling these routines at all.
[1] It also has a names property, which is a little closer to reality but still not particularly useful.
[2] Actually, that’s not true for gethostname. Rather, that call just returns whatever was last set by sethostname. This is always fast. The System Configuration framework infrastructure calls sethostname to update the host name as the system state changes.
Hello Apple Developer Community,
I am currently working on a macOS project where my primary goal is to intercept IP packets, modify them (specifically the TCP payload), and then forward them. My intended use case involves selectively intercepting outgoing packets based on their destination IP, altering their content, and sending them on their way to the original destination.
What I’ve Tried:
NEAppProxyProvider:
• I explored using App Proxy Provider to handle new TCP and UDP flows.
• While it allowed me to read the data, handling direct packet modification and forwarding without creating a new connection or proxy setup proved challenging, especially for maintaining TCP state and handling TLS traffic.
System Extension with NEFilterPacketProvider:
• I considered NEFilterPacketProvider for intercepting and modifying network packets.
• However, the documentation implies that packet filtering only supports allow/block actions, not modification and reinjection of packets back into the system.
I am planning to try NEPacketTunnelProvider: But the documentation states that this is not the right use case. Packets are expected to go into the tunnel. Since I don't have any requirement to create and maintain a tunnel, this doesn't look like an option for me.
Transparent proxy setups like NETransparentProxyProvider do not appear to offer direct packet modification capabilities without involving a user-space proxy approach.
Implementing packet-level interception outside of the Network Extension framework (e.g., Network Kernel Extension) seems unsupported in newer macOS versions (Sequoia and later).
My Questions:
Is there a recommended approach or combination of Network Extension capabilities that would allow intercepting and modifying IP packets directly?
Can NEFilterPacketProvider or any other extension be utilized in a way to modify and reinject packets back into the system?
Are there any examples or sample projects that achieve similar functionality, possibly using a blend of Network Extension and lower-level networking frameworks?
I appreciate any insights or pointers to documentation or examples that could help achieve this.
Thanks and Regards.
Prasanna.
For our outdoor power supply company that builds public WiFi networks at camping sites, we want to implement the following features in our app:
Scan surrounding WiFi networks
When detecting specific public WiFi SSIDs, provide users with corresponding passwords
Automatically connect to those WiFi networks
Regarding the NEHotspotHelper API permission application, when I clicked on https://developer.apple.com/contact/request/network-extension, it redirected me to https://developer.apple.com/unauthorized/. I'm not sure where to properly apply for this permission now.
Topic:
App & System Services
SubTopic:
Networking
We have a NEFilterDataProvider extension that intercepts all TCP and UDP IPv4/6 traffic. At times just after wakeup from sleep, it causes internet access issues, such as showing "This site can't be reached" when opening websites.
The traffic is not being dropped by the extension.
According to the logs, the connection is being closed after approximately 4 minutes.
During the issue, the flow logs are as follows:
Flow 515129771 is connecting
New flow: NEFlow type = stream, app = com.google.Chrome.helper...
Detaching, ref count = 2 (logged after ~4 minutes)
Sending close, how = 2
Removing from group 2, ref count = 2
Destroying, app tx 0, tunnel tx 0, tunnel rx 0
Closing reads, not closed by plugin
Closing writes, not sending close
Any suggestions on the possible cause and how to further debug it?
Please consider this trivial C code which deals with BSD sockets. This will illustrate an issue with sendto() which seems to be impacted by the recent "Local Network" restrictions on 15.3.1 macos.
#include <stdio.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "sys/socket.h"
#include <string.h>
#include <unistd.h>
#include <ifaddrs.h>
#include <net/if.h>
// prints out the sockaddr_in6
void print_addr(const char *msg_prefix, struct sockaddr_in6 sa6) {
char addr_text[INET6_ADDRSTRLEN] = {0};
printf("%s%s:%d, addr family=%u\n",
msg_prefix,
inet_ntop(AF_INET6, &sa6.sin6_addr, (char *) &addr_text, INET6_ADDRSTRLEN),
sa6.sin6_port,
sa6.sin6_family);
}
// creates a datagram socket
int create_dgram_socket() {
const int fd = socket(AF_INET6, SOCK_DGRAM, 0);
if (fd < 0) {
perror("Socket creation failed");
return -1;
}
return fd;
}
// returns a string representing the current local time
char *current_time() {
time_t seconds_since_epoch;
time(&seconds_since_epoch);
char *res = ctime(&seconds_since_epoch);
const size_t len = strlen(res);
// strip off the newline character that's at the end of the ctime() output
res[len - 1] = '\0';
return res;
}
// Creates a datagram socket and then sends a messages (through sendto()) to a valid
// multicast address. This it does two times, to the exact same destination address from
// the exact same socket.
//
// Between the first and the second attempt to sendto(), there is
// a sleep of 1 second.
//
// The first time, the sendto() succeeds and claims to have sent the expected number of bytes.
// However system logs (generated through "log collect") seem to indicate that the message isn't
// actually sent (there's a "cfil_service_inject_queue:4466 CFIL: sosend() failed 65" in the logs).
//
// The second time the sendto() returns a EHOSTUNREACH ("No route to host") error.
//
// If the sleep between these two sendto() attempts is removed then both the attempts "succeed".
// However, the system logs still suggest that the message isn't actually sent.
int main() {
printf("current process id:%ld parent process id: %ld\n", (long) getpid(), (long) getppid());
// valid multicast address as specified in
// https://www.iana.org/assignments/ipv6-multicast-addresses/ipv6-multicast-addresses.xhtml
const char *ip6_addr_str = "ff01::1";
struct in6_addr ip6_addr;
int rv = inet_pton(AF_INET6, ip6_addr_str, &ip6_addr);
if (rv != 1) {
fprintf(stderr, "failed to parse ipv6 addr %s\n", ip6_addr_str);
exit(EXIT_FAILURE);
}
// create a AF_INET6 SOCK_DGRAM socket
const int sock_fd = create_dgram_socket();
if (sock_fd < 0) {
exit(EXIT_FAILURE);
}
printf("created a socket, descriptor=%d\n", sock_fd);
const int dest_port = 12345; // arbitrary port
struct sockaddr_in6 dest_sock_addr;
memset((char *) &dest_sock_addr, 0, sizeof(struct sockaddr_in6));
dest_sock_addr.sin6_addr = ip6_addr; // the target multicast address
dest_sock_addr.sin6_port = htons(dest_port);
dest_sock_addr.sin6_family = AF_INET6;
print_addr("test will attempt to sendto() to destination host:port -> ", dest_sock_addr);
const char *msg = "hello";
const size_t msg_len = strlen(msg) + 1;
for (int i = 1; i <= 2; i++) {
if (i != 1) {
// if not the first attempt, then sleep a while before attempting to sendto() again
int num_sleep_seconds = 1;
printf("sleeping for %d second(s) before calling sendto()\n", num_sleep_seconds);
sleep(num_sleep_seconds);
}
printf("%s attempt %d to sendto() %lu bytes\n", current_time(), i, msg_len);
const size_t num_sent = sendto(sock_fd, msg, msg_len, 0, (struct sockaddr *) &dest_sock_addr,
sizeof(dest_sock_addr));
if (num_sent == -1) {
fprintf(stderr, "%s ", current_time());
perror("sendto() failed");
close(sock_fd);
exit(EXIT_FAILURE);
}
printf("%s attempt %d of sendto() succeeded, sent %lu bytes\n", current_time(), i, num_sent);
}
return 0;
}
What this program does is, it uses the sendto() system call to send a message over a datagram socket to a (valid) multicast address. It does this twice, from the same socket to the same target address. There is a sleep() of 1 second between these two sendto() attempts.
Copy that code into noroutetohost.c and compile:
clang noroutetohost.c
Then run:
./a.out
This generates the following output:
current process id:58597 parent process id: 21614
created a socket, descriptor=3
test will attempt to sendto() to destination host:port ->ff01::1:14640, addr family=30
Fri Mar 14 20:34:09 2025 attempt 1 to sendto() 6 bytes
Fri Mar 14 20:34:09 2025 attempt 1 of sendto() succeeded, sent 6 bytes
sleeping for 1 second(s) before calling sendto()
Fri Mar 14 20:34:10 2025 attempt 2 to sendto() 6 bytes
Fri Mar 14 20:34:10 2025 sendto() failed: No route to host
Notice how the first call to sendto() "succeeds", even the return value (that represents the number of bytes sent) matches the number of bytes that were supposed to be sent. Then notice how the second attempt fails with a EHOSTUNREACH ("No route to host") error. Looking through the system logs, it appears that the first attempt itself has failed:
2025-03-14 20:34:09.474797 default kernel cfil_hash_entry_log:6082 <CFIL: Error: sosend_reinject() failed>: [58597 a.out] <UDP(17) out so 891be95f3a70c605 22558774573152560 22558774573152560 age 0> lport 0 fport 12345 laddr :: faddr ff01::1 hash 1003930
2025-03-14 20:34:09.474806 default kernel cfil_service_inject_queue:4466 CFIL: sosend() failed 65
(notice the time on that log messages, they match the first attempt from the program's output log)
So even though the first attempt failed, it never got reported back to the application. Then after sleeping for (an arbitrary amount of) 1 second, the second call fails with the EHOSTUNREACH. The system logs don't show any error (at least not the one similar to that previous one) for the second call.
If I remove that sleep() between those two attempts, then both the sendto() calls "succeed" (and return the expected value for the number of bytes sent). However, the system logs show that the first call (and very likely even the second) has failed with the exact same log message from the kernel like before.
If I'm not wrong then this appears to be some kind of a bug in the "local network" restrictions. Should this be reported? I can share the captured logs but I would prefer to do it privately for this one.
Another interesting thing in all this is that there's absolutely no notification to the end user (I ran this program from the Terminal) about any of the "Local Network" restrictions.
I'm working on a network extension that provides a VPN tunnel. The logic behind the tunnel provider requires me to connect the backend to pull recent configuration and then configure the routing. It works in general but fails with some circumstances. I have 100% reproducible fails if I run OpenVPN tunnel in parallel. When it happens it looks like the network extension cannot connect the backend (any internet resource actually). Requests fail by timeout.
To troubleshoot this situations I've added NWPathMonitor at my NEPacketTunnelProvider subclass:
pathMonitor?.pathUpdateHandler = { path in
logger.info("Path update: \(path)")
logger.info(" : \(path.availableInterfaces)")
}
On successful scenarios I observed logs:
14:53:19:829 Starting VPN tunnel...
14:53:19:895 Path update: satisfied (Path is satisfied), interface: en0[802.11], scoped, ipv4, ipv6, dns, uses wifi
14:53:19:899 : [en0]
14:53:22:237 Path update: satisfied (Path is satisfied), interface: en0[802.11], scoped, ipv4, ipv6, dns, uses wifi
14:53:22:253 : [en0, utun12]
14:53:22:325 VPN tunnel is started.
But if I start another tunnel first using OpenVPN (it's our corporate VPN) I observe failures with such log messages:
14:54:26:113 Starting VPN tunnel...
14:54:26:140 Path update: satisfied (Path is satisfied), interface: en0[802.11], scoped, ipv4, ipv6, dns, uses wifi
14:54:26:141 : [en0]
14:55:28:259 Failed to start VPN tunnel.
utun12 that was used by the extension in case of success is now occupied by the OpenVPN tunnel. The system creates utun13 for me but it feels like its misconfigured:
> ifconfig
(omitted most of the output)
utun12: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1500
inet 172.28.11.236 --> 172.28.10.1 netmask 0xfffffe00
nd6 options=201<PERFORMNUD,DAD>
utun13: flags=8051<UP,POINTOPOINT,RUNNING,MULTICAST> mtu 1500
options=6460<TSO4,TSO6,CHANNEL_IO,PARTIAL_CSUM,ZEROINVERT_CSUM>
Is it a system bug and what else can I do to diagnose the root cause of these failures?
Just bought a macbook pro m4, im trying to run an api on port 5000, disabled airplay receiver, checked processes, ghost ones, hidden ones, and stuck ones. I didn't find a thing using the port, but i still get port in use.
Topic:
App & System Services
SubTopic:
Networking
PLATFORM AND VERSION
iOS
Development environment: Xcode Version 16.0 (16A242d), macOS 15.0 (24A335)
Run-time configuration: macOS 15.0 (24A335)
DESCRIPTION OF PROBLEM
macOS Sequoia has new Privacy & Security requirements for local area network access. This causes a call to POSIX socket function 'sendto' to fail immediately with EHOSTUNREACH on the first execution of any app that calls it. That failure occurs even if the socket is set to block for well over the time that it would typically take for the user to click "Allow" when presented with a system dialogue box that requests new permissions for the app.
A test XCode project has been provided that is capable of reproducing the issue (see macOSsocketfail.zip at https://drive.google.com/file/d/14VxkT03ddm48RCXikLHf-aWgdqxwnpAB/view?usp=sharing). It will generate a log file that contains time-stamped messages. They report each step in the creation of a blocking UDP socket – and which system call has failed as a result of which error. The message time stamps demonstrate that macOS has terminated the sendto function call after tens of microseconds, well before the 5 minute timeout on the socket, and that it terminates with errno EHOSTUNREACH. That error is misleading, because the destination is pingable on my setup at the time of execution. The second execution of the app functions without error if "Allow" has been selected during the first run.
This specific macOS behaviour does not appear to be documented anywhere that I have yet encountered; e.g. the sendto man page, the Privacy & Security LAN FAQ, etc. It is, however, highly disruptive to the use of our product, which hinges on LAN access. We have a situation in which a relatively large collection of apps are using the same shared library to manage network access. All of them must now receive manual permission from an administrator to work; but all of them will fail on their first execution. The problem is amplified because our customers use our framework to build their own apps, and not every user is an administrator. In contrast, apps that use our framework would simply work without issue on their first execution when run on macOS versions that precede Sequoia.
We must support our software across multiple platforms, hence the reason that we are using POSIX function calls to implement networking. Unfortunately, the use of an Apple-specific networking API is not a viable solution for us.
How should we mitigate this problem? Is there some way to configure an Xcode project so that the build product will already have Sequoia LAN permissions? I have read about the com.apple.developer.networking.multicast entitlement, but it is unclear whether it will help us, from the material that is available.
STEPS TO REPRODUCE
POSIX function call sequence
For the following, addr has type struct sockaddr_in, and it is set appropriately for binding or broadcasting using standard library macros and functions. &addr is cast to a const struct sockaddr pointer and assigned to saddr.
sock = socket( PF_INET, SOCK_DGRAM , 0 ) ;
bind( sock, saddr, sizeof( addr ) ) ;
r = 1 ; setsockopt( sock, SOL_SOCKET, SO_BROADCAST, &r, sizeof( r ) );
struct timeval timeout = { 300 , 0 }; setsockopt( sock , SOL_SOCKET , SO_RCVTIMEO , &timeout , sizeof( timeout ) );
sendto( sock, msg, strlen( msg ) + 1, 0, saddr , sizeof( addr ) ) ;
Test program, presuming that a device with IP 100.1.1.1 exists on the LAN that the mac is also connected to.
Open Xcode project macOSsocketfail.
Build project.
Execute first run of macOSsocketfail.app.
Click "Allow" when asked for LAN permissions.
Search for macOSsocketfail_log.txt, and make a copy with a distinct name e.g. macOSsocketfail_log_firstrun.txt.
Execute second run of macOSsocketfail.app.
Search for macOSsocketfail_log.txt, and make a copy with a distinct name e.g. macOSsocketfail_log_secondrun.txt.
Examine log files.
The first run log file will contain the following messages (time stamping will naturally differ):
1727710614.064009: Running udptest
1727710614.064015: Creating socket
1727710614.064030: Bind socket to port: 4000
1727710614.064061: Enable socket broadcast
1727710614.064064: Set socket timeout to 300.000000sec
1727710614.064067: Attempt to send blocking UDP connection packet to 100.1.1.1:589
1727710614.064124: sendto: No route to host
Error during call to sendto: errno is EHOSTUNREACH: No route to host
The second run log file will record a different outcome:
1727713660.733431: Running udptest
1727713660.733436: Creating socket
1727713660.733451: Bind socket to port: 4000
1727713660.733476: Enable socket broadcast
1727713660.733479: Set socket timeout to 300.000000sec
1727713660.733482: Attempt to send blocking UDP connection packet to 100.1.1.1:589
1727713660.733540: Ran to completion with no error detected
Note that each line of the log files begins with a timestamp. The unit is seconds, and the resolution is to the nearest microsecond. Time values are obtained using gettimeofday().
RELEVANT LINKS
https://developer.apple.com/forums/thread/663858
https://forums.developer.apple.com/forums/thread/757824
https://developer.apple.com/forums/thread/760964
https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_developer_networking_multicast/
https://developer.apple.com/forums/thread/657887
https://developer.apple.com/forums/thread/655920
Dear Apple:
We found that after mirroring an iPhone and a Mac, calling the NEHotspotConfigurationManager applyConfiguration interface on the iPhone fails to connect to Wi-Fi. Are there any restrictions on using this interface in mirror mode?
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.
Apology for repost. I needed to fix the tags for original thread.
https://developer.apple.com/forums/thread/777159
On iOS 18.3, I noted that partition "HTTPCookiePropertyKey: StoragePartition" is not observed to be set for cookies returned from the wkwebview cookie store.
Now on 18.4 beta 4 we are now seeing those same cookies are populated with a partition property. Is there documentation for this change? Is it intended to be suddenly populated in 18.4?
Now that partition property is set, HTTPCookieStorage.shared.cookies(for: serverUri) doesn't seem to return the expected cookies correctly. For context, we are using the cookies extracted from wkwebview, setting them in HTTPCookieStorage.shared and using URLSession to make network calls outside the webivew. Works fine once I forcefully set partition on the cookie to nil.
More details on what the cookie looks like here: https://feedbackassistant.apple.com/feedback/16906526
Hopefully this is on your radar?
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?
I'm trying to use ThreadNetwork API to manage TheradNetworks on device (following this documentation: https://developer.apple.com/documentation/threadnetwork/), but while some functions on THClient work (such as getPreferedNetwork), most don't (storeCredentials, retrieveAllCredentials). When calling these functions I get the following warning/error:
Client: -[THClient getConnectionEntitlementValidity]_block_invoke - Error:
-[THClient storeCredentialsForBorderAgent:activeOperationalDataSet:completion:]_block_invoke:701: - Error: Error Domain=NSCocoaErrorDomain Code=4099 "The connection to service with pid 414 named com.apple.ThreadNetwork.xpc was invalidated from this process." UserInfo={NSDebugDescription=The connection to service with pid 414 named com.apple.ThreadNetwork.xpc was invalidated from this process.}
Error Domain=NSCocoaErrorDomain Code=4099 "The connection to service with pid 414 named com.apple.ThreadNetwork.xpc was invalidated from this process." UserInfo={NSDebugDescription=The connection to service with pid 414 named com.apple.ThreadNetwork.xpc was invalidated from this process.}
Failed to store Thread credentials: Couldn’t communicate with a helper application.
STEPS TO REPRODUCE
Create new project
Add Thread Network capability via Xcode UI (com.apple.developer.networking.manage-thread-network-credentials)
Trigger storeCredentials
let extendedMacData = "9483C451DC3E".hexadecimal
let tlvHex = "0e080000000000010000000300001035060004001fffe002083c66f0dc9ef53f1c0708fdb360c72874da9905104094dce45388fd3d3426e992cbf0697b030d474c2d5332302d6e65773030310102250b04106c9f919a4da9b213764fc83f849381080c0402a0f7f8".hexadecimal
// Initialize the THClient
let thClient = THClient()
// Store the credentials
await thClient.storeCredentials(forBorderAgent: extendedMacData!, activeOperationalDataSet: tlvHex!) { error in
if let error = error {
print(error)
print("Failed to store Thread credentials: \(error.localizedDescription)")
} else {
print("Successfully stored Thread credentials")
}
}
NOTES:
I tried with first calling getPreferedNetwork to initiate network permission dialog
Tried adding meshcop to bojur services
Tried with different release and debug build configurations
We have developed a DNS filter extension that works for most applications, but it does not receive all DNS queries.
In particular, if we have our extension installed and enabled, we see Safari browsing cause local DNS servers to be used instead of going through our extension.
What is the logic for how DNS servers vs. extensions are chosen to resolve DNS queries?
Apple multi-peer with 12 devices is unstable. Dear All,
Has anyone tried Apple multi-peer with 12 devices connected? We are building an application relying on multi-peer where 12 Ipads will be updating data and each device needs to share data between. Can anyone tell me if we can use multi-peer framework for connecting 12 devices in the multi-peer network? We are facing stability problems in the connection when we connect 12 devices in the network.