Post

Replies

Boosts

Views

Activity

Reply to Cannot open Chrome UDP flows in Transparent Proxy Provider
I ran more tests and, I think, I can now explain the Chrome QUIC failures. change_fdguard_np is not the problem: a UDP socket guarded with GUARD_CLOSE | GUARD_DUP (applied before connect(), exactly like Chromium) proxies flawlessly through my NETransparentProxyProvider (both datagram directions). The actual cause: on a flow-diverted UDP socket, setsockopt(..., IPPROTO_IP, IP_DONTFRAG, ...) and setsockopt(..., IPPROTO_IP, IP_RECVTOS, ...) fail with ECONNRESET (errno 54). Without the proxy, they succeed. Chromium applies exactly these options immediately after connect() (QuicSessionPool::FinishConnectAndConfigureSocket, net/quic/quic_session_pool.cc) and aborts QUIC session creation on any failure, closing the socket within microseconds of the flow being created. The provider's subsequent flow.open() therefore always fails with NEAppProxyFlowErrorDomain Code=2 "The peer closed the flow". Repro without Chrome: create a UDP socket, connect() to any destination covered by the proxy's rules, then call setsockopt(..., IPPROTO_IP, IP_DONTFRAG, ...): It returns ECONNRESET whenever the transparent proxy is active. I will create a new bug report.
3w
Reply to Cannot open Chrome UDP flows in Transparent Proxy Provider
The problem still occurs on macOS 26 and 27. I asked Claude to find a reason in the Chromium source code. Its findings: Finding 1: All Chrome QUIC UDP sockets are guarded immediately after creation In net/socket/udp_socket_posix.cc, ConfigureOpenedSocket() is called as the very last step of Open(), before any data is sent: // Called from Open() — runs before connect() or any I/O int UDPSocketPosix::ConfigureOpenedSocket() { #if BUILDFLAG(IS_APPLE) && !BUILDFLAG(CRONET_BUILD) && !BUILDFLAG(IS_IOS_TVOS) guardid_t guardid = reinterpret_cast<guardid_t>(this); PCHECK(change_fdguard_np(socket_, nullptr, 0, &guardid, GUARD_CLOSE | GUARD_DUP, nullptr) == 0); #endif // ... } change_fdguard_np is a private macOS API that prevents any external code — including the kernel itself — from calling dup() or close() on this file descriptor. GUARD_DUP specifically blocks duplication of the fd. This is Apple-only code, excluded from Cronet builds. Firefox and Safari have no equivalent guard. Finding 2: The kernel's transparent proxy mechanism likely needs to dup the socket When NETransparentProxyProvider claims a flow via open(), the kernel needs to maintain a reference to Chrome's socket to deliver proxy responses back to it — even if Chrome closes it. The standard mechanism for this at the kernel level is to dup() the fd internally. GUARD_DUP blocks that operation, causing the kernel to immediately give up on the flow and report NEAppProxyFlowErrorDomain Code=2 "The peer closed the flow". Finding 3: QUIC path probing creates additional short-lived sockets In quic_chromium_client_session.cc, QUIC path validation creates extra UDP sockets via QuicChromiumPathValidationContext — all of which also get GUARD_CLOSE | GUARD_DUP applied. When a probe fails, the socket is discarded immediately. These probe sockets add to the volume of flows your proxy sees that all fail to open. So maybe the NETransparentProxyProvider is incompatible with Chromium QUIC flows?
Jul ’26
Reply to Get UDP/TCP Payload for NWConnections?
In the Network framework "header" file I have found the following doc comment for NWConnection.startDataTransferReport: /// Start a new pending data transfer report on a connection. Multiple /// reports may be created for a single NWConnection. The report begins /// capturing data when the connection moves to the .ready state, or when /// the report is created (whichever occurs last). So it seems that not capturing the TLS handshake is the intended behavior. Too bad! I also noticed that TLS alert messages are not included in the data report.
Mar ’26
Reply to Get UDP/TCP Payload for NWConnections?
Yes, I already tested that method. But unfortunately, sentTransportByteCount and receivedTransportByteCount don’t include the Handshake payload even when I call startDataTransferReport before starting the connection. So I get, for example, 35/31 Bytes instead of 1674/3070 Bytes for a simple "send 'Hello' and cancel" TLS connection.
Mar ’26
Reply to Cannot open Chrome UDP flows in Transparent Proxy Provider
Created new bug report: FB23636906
Replies
Boosts
Views
Activity
3w
Reply to Cannot open Chrome UDP flows in Transparent Proxy Provider
I ran more tests and, I think, I can now explain the Chrome QUIC failures. change_fdguard_np is not the problem: a UDP socket guarded with GUARD_CLOSE | GUARD_DUP (applied before connect(), exactly like Chromium) proxies flawlessly through my NETransparentProxyProvider (both datagram directions). The actual cause: on a flow-diverted UDP socket, setsockopt(..., IPPROTO_IP, IP_DONTFRAG, ...) and setsockopt(..., IPPROTO_IP, IP_RECVTOS, ...) fail with ECONNRESET (errno 54). Without the proxy, they succeed. Chromium applies exactly these options immediately after connect() (QuicSessionPool::FinishConnectAndConfigureSocket, net/quic/quic_session_pool.cc) and aborts QUIC session creation on any failure, closing the socket within microseconds of the flow being created. The provider's subsequent flow.open() therefore always fails with NEAppProxyFlowErrorDomain Code=2 "The peer closed the flow". Repro without Chrome: create a UDP socket, connect() to any destination covered by the proxy's rules, then call setsockopt(..., IPPROTO_IP, IP_DONTFRAG, ...): It returns ECONNRESET whenever the transparent proxy is active. I will create a new bug report.
Replies
Boosts
Views
Activity
3w
Reply to Cannot open Chrome UDP flows in Transparent Proxy Provider
Thanks for the reply! I tried to reproduce the issue with a test app sending simple SNTP requests, but unfortunately I couldn't. 😢 Opening the flows through the transparent proxy worked fine both with and without calling change_fdguard_np on the socket before connecting it.
Replies
Boosts
Views
Activity
3w
Reply to Cannot open Chrome UDP flows in Transparent Proxy Provider
The problem still occurs on macOS 26 and 27. I asked Claude to find a reason in the Chromium source code. Its findings: Finding 1: All Chrome QUIC UDP sockets are guarded immediately after creation In net/socket/udp_socket_posix.cc, ConfigureOpenedSocket() is called as the very last step of Open(), before any data is sent: // Called from Open() — runs before connect() or any I/O int UDPSocketPosix::ConfigureOpenedSocket() { #if BUILDFLAG(IS_APPLE) && !BUILDFLAG(CRONET_BUILD) && !BUILDFLAG(IS_IOS_TVOS) guardid_t guardid = reinterpret_cast<guardid_t>(this); PCHECK(change_fdguard_np(socket_, nullptr, 0, &guardid, GUARD_CLOSE | GUARD_DUP, nullptr) == 0); #endif // ... } change_fdguard_np is a private macOS API that prevents any external code — including the kernel itself — from calling dup() or close() on this file descriptor. GUARD_DUP specifically blocks duplication of the fd. This is Apple-only code, excluded from Cronet builds. Firefox and Safari have no equivalent guard. Finding 2: The kernel's transparent proxy mechanism likely needs to dup the socket When NETransparentProxyProvider claims a flow via open(), the kernel needs to maintain a reference to Chrome's socket to deliver proxy responses back to it — even if Chrome closes it. The standard mechanism for this at the kernel level is to dup() the fd internally. GUARD_DUP blocks that operation, causing the kernel to immediately give up on the flow and report NEAppProxyFlowErrorDomain Code=2 "The peer closed the flow". Finding 3: QUIC path probing creates additional short-lived sockets In quic_chromium_client_session.cc, QUIC path validation creates extra UDP sockets via QuicChromiumPathValidationContext — all of which also get GUARD_CLOSE | GUARD_DUP applied. When a probe fails, the socket is discarded immediately. These probe sockets add to the volume of flows your proxy sees that all fail to open. So maybe the NETransparentProxyProvider is incompatible with Chromium QUIC flows?
Replies
Boosts
Views
Activity
Jul ’26
Reply to Get UDP/TCP Payload for NWConnections?
In the Network framework "header" file I have found the following doc comment for NWConnection.startDataTransferReport: /// Start a new pending data transfer report on a connection. Multiple /// reports may be created for a single NWConnection. The report begins /// capturing data when the connection moves to the .ready state, or when /// the report is created (whichever occurs last). So it seems that not capturing the TLS handshake is the intended behavior. Too bad! I also noticed that TLS alert messages are not included in the data report.
Replies
Boosts
Views
Activity
Mar ’26
Reply to Get UDP/TCP Payload for NWConnections?
Thanks for checking with the network team so quickly! I have filed a bug: FB22181144
Replies
Boosts
Views
Activity
Mar ’26
Reply to Get UDP/TCP Payload for NWConnections?
Yes, I already tested that method. But unfortunately, sentTransportByteCount and receivedTransportByteCount don’t include the Handshake payload even when I call startDataTransferReport before starting the connection. So I get, for example, 35/31 Bytes instead of 1674/3070 Bytes for a simple "send 'Hello' and cancel" TLS connection.
Replies
Boosts
Views
Activity
Mar ’26
Reply to Cannot open Chrome UDP flows in Transparent Proxy Provider
[quote='817942022, DTS Engineer, /thread/770707?answerId=817942022#817942022'] And Safari? [/quote] It seems that it is a problem with Chromium-based UDP flows. For Chromium, Chrome, Brave, Opera I get the "The peer closed the flow" errors, for Safari and Firefox not.
Replies
Boosts
Views
Activity
Dec ’24
Reply to Cannot open Chrome UDP flows in Transparent Proxy Provider
With Safari flows it works as well.
Replies
Boosts
Views
Activity
Dec ’24