SOCKS5 proxies (from PAC or ProxyConfiguration) are never applied to QUIC, and NWConnection silently connects directly

On macOS 27.0.1 (26A434), a SOCKS5 proxy is applied to TCP connections but never to QUIC. This happens whether the proxy comes from a PAC file in the system proxy settings or from an explicit ProxyConfiguration. The system SOCKS5 client only ever sends CONNECT; it never sends UDP ASSOCIATE (RFC 1928, CMD 0x03), so it has no way to carry UDP. URLSession handles this by staying on TCP through the proxy. A QUIC NWConnection instead connects directly to the origin, as if no proxy were configured.

Setup. The system proxy settings use a PAC URL (ProxyAutoConfigURLString in scutil --proxy; in my test it was installed through NEProxySettings.proxyAutoConfigurationURL). For the test host, a public site that serves HTTP/3, the PAC returns SOCKS5 127.0.0.1:<port>. For the explicit cases I used a local SOCKS5 server that logs every command it receives.

With the system PAC, default parameters, no explicit proxy:

  1. URLSession, assumesHTTP3Capable = true: goes through the proxy over TCP (transaction metrics: h2, isProxyConnection == true, remote address 127.0.0.1:<port>). HTTP/3 isn't used, which is consistent: the proxy can't carry it.
  2. NWConnection, TLS: currentPath.remoteEndpoint is 127.0.0.1:<port>. The PAC is honored.
  3. NWConnection, QUIC (ALPN h3): .ready in about 170 ms, and currentPath.remoteEndpoint is the origin's own address. The PAC is ignored.

With an explicit proxy, set via NWParameters.PrivacyContext.proxyConfigurations:

  1. QUIC with a working SOCKS5 server: .ready, and the server receives nothing — no CONNECT, no UDP ASSOCIATE.
  2. QUIC with the proxy pointing at a port nothing listens on: still .ready, still connected to the origin's own address.
  3. TLS with that same dead proxy: stays in .waiting with ECONNREFUSED. The proxy is honored for TCP.

At no point did the SOCKS5 server receive a UDP ASSOCIATE request.

Cases 3–5 are the concerning part. An app that configures a proxy, or a user or administrator who installs a PAC, reasonably expects traffic to go through the proxy or fail. Instead, QUIC traffic goes straight to the origin, which then sees the device's own address, and nothing in the API signals that the proxy was skipped.

Minimal snippet:

import Network

let host: NWEndpoint.Host = "YOUR_HTTP3_HOST"   // any public host that serves HTTP/3
let parameters = NWParameters(quic: NWProtocolQUIC.Options(alpn: ["h3"]))

// Remove these four lines to test with the system PAC instead.
let privacy = NWParameters.PrivacyContext(description: "socks5-quic")
privacy.proxyConfigurations = [
    ProxyConfiguration(socksv5Proxy: .hostPort(host: "127.0.0.1", port: 1099)) // nothing listens on 1099
]
parameters.setPrivacyContext(privacy)

let connection = NWConnection(host: host, port: 443, using: parameters)
connection.stateUpdateHandler = { state in
    if case .ready = state {
        // Prints the origin's own address. With NWParameters.tls instead of QUIC,
        // the same proxy configuration leaves the connection in .waiting (ECONNREFUSED).
        print(connection.currentPath?.remoteEndpoint as Any)
    }
}
connection.start(queue: .main)

I filed this as FB24760235 on 09/13/2026 and haven't had a response, so I'd like to ask here:

  1. Is it intended that a QUIC NWConnection ignores both the system PAC and proxyConfigurations? If so, how can an app find out that a connection went direct? And shouldn't a connection that can't honor the configured proxy fail rather than bypass it?
  2. Is UDP ASSOCIATE support planned for the system SOCKS5 client, so that QUIC and other UDP traffic can follow a SOCKS5 proxy the way TCP does?

Thanks!

Answered by DTS Engineer in 908525022
I filed this as FB24760235

Thanks. That’s the right path for this issue.

Is UDP ASSOCIATE support planned for the system SOCKS5 client … ?

I can’t talk about The Future™ — see tip 3 in Quinn’s Top Ten DevForums Tips — but the fact that your bug hasn’t been bounced back is a promising sign (-:

how can an app find out that a connection went direct?

This should be reflected in the usedProxy property of the connection establishment report.

shouldn't a connection that can't honor the configured proxy fail rather than bypass it?

Not in general. It’s traditional for proxies to be deployed in environments where the network itself blocks the connection, and thus this isn’t a concern. But…

Did you try setting the allowFailover property to false?

I think it defaults to false, in which case this will have no effect. And if that’s the case then, yeah, I’d consider this behaviour to be a bug.

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

I filed this as FB24760235

Thanks. That’s the right path for this issue.

Is UDP ASSOCIATE support planned for the system SOCKS5 client … ?

I can’t talk about The Future™ — see tip 3 in Quinn’s Top Ten DevForums Tips — but the fact that your bug hasn’t been bounced back is a promising sign (-:

how can an app find out that a connection went direct?

This should be reflected in the usedProxy property of the connection establishment report.

shouldn't a connection that can't honor the configured proxy fail rather than bypass it?

Not in general. It’s traditional for proxies to be deployed in environments where the network itself blocks the connection, and thus this isn’t a concern. But…

Did you try setting the allowFailover property to false?

I think it defaults to false, in which case this will have no effect. And if that’s the case then, yeah, I’d consider this behaviour to be a bug.

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

Thanks, Quinn!

Did you try setting the allowFailover property to false?

I have now. It reads false by default (matching the header comment, "Failover is not allowed by default"), and setting it to false explicitly makes no difference: the QUIC connection still goes direct. Results on macOS 27.0.1 (26A434), with the proxy set via NWParameters.PrivacyContext.proxyConfigurations:

  • QUIC, working SOCKS5 server, allowFailover = false: .ready, connected to the origin directly; the server receives nothing.
  • QUIC, proxy pointing at a port nothing listens on, allowFailover = false: .ready, connected to the origin directly.
  • TLS, same dead proxy, allowFailover = false: stays in .waiting with ECONNREFUSED.
  • TLS, same dead proxy, allowFailover = true: .ready, connected to the origin directly, as documented.

So by your reasoning this is a bug. I'll add these results to FB24760235.

This should be reflected in the usedProxy property of the connection establishment report.

It is, thanks. For the QUIC connections usedProxy is false and proxyEndpoint is nil, so an app can at least detect it after the fact.

One more detail that may help whoever picks up the bug: proxyConfigured is false as well. The header says that property is true when "a proxy configuration applied to the connection, even if the connection did not end up using a proxy", and the TLS failover case above does report proxyConfigured == true with usedProxy == false. For QUIC both are false, so it looks like the SOCKS5 configuration is treated as not applicable to QUIC and dropped before establishment, rather than tried and failed over. That would also explain why allowFailover has no effect.

So by your reasoning this is a bug.

Yep.

I'll add these results to FB24760235.

Ta.

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