Best practices for handling nw_connection_state_waiting in Transparent Proxy

I am working on a Network Extension (Transparent Proxy) which is used by a DLP to prevent data leaks over the network. For each incoming NEAppProxyTCPFlow, the extension instantiates a custom wrapper object that creates an outbound nw_connection_t to the target host and port using Network framework.

When a client application connects to unreachable or blocked ports (e.g., 5222), our extension proxies this connection. As the destination is unreachable, the newly created connection transitions into nw_connection_state_waiting in Network Extension. Since macOS keeps nw_connection_t in the nw_connection_state_waiting state indefinitely, such connections cause system resource leaks. Over time, this leads to:

  • exhaustion of system file descriptors and sockets.
  • system-wide network unavailability until the extension process is killed.

Could you provide best-practice recommendations for handling nw_connection_state_waiting in a Network Extension to prevent such resource leaks?

Answered by DTS Engineer in 901814022

I don’t think there’s a single best practice here. Rather, there are various approaches you can take and the best one is likely going to vary by the circumstances (something that’s pretty common problems when you’re ‘patching’ the system).

I see three basic options:

  • Immediately fail the flow’s open request when the connection enters the .waiting(_:) state.
  • Do that, but after some arbitrary timeout.
  • Let the flow’s open request stall waiting for things to change.

I suspect that the first option is likely to be the best. If the client is itself using Network framework, this error is likely to cause it to move on to the next IP address and, if there are no more, enter its own .waiting(_:) state. However, things might work out less well for BSD Socket clients, which are likely to just fail immediately in response to such an error. OTOH, in the case of a bad port number, failing fast with ECONNREFUSED is what BSD Sockets clients are likely to expect.

Share and Enjoy

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

Accepted Answer

I don’t think there’s a single best practice here. Rather, there are various approaches you can take and the best one is likely going to vary by the circumstances (something that’s pretty common problems when you’re ‘patching’ the system).

I see three basic options:

  • Immediately fail the flow’s open request when the connection enters the .waiting(_:) state.
  • Do that, but after some arbitrary timeout.
  • Let the flow’s open request stall waiting for things to change.

I suspect that the first option is likely to be the best. If the client is itself using Network framework, this error is likely to cause it to move on to the next IP address and, if there are no more, enter its own .waiting(_:) state. However, things might work out less well for BSD Socket clients, which are likely to just fail immediately in response to such an error. OTOH, in the case of a bad port number, failing fast with ECONNREFUSED is what BSD Sockets clients are likely to expect.

Share and Enjoy

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

Thank you very much for the help!

It looks like the easiest way is to immediately fail the flow’s open request when the connection enters the nw_connection_state_waiting state. According to the documentation, closeWriteWithError: and closeReadWithError: must take either nil or an NSError with the NEAppProxyErrorDomain. Therefore, it seems that the error codes used for flow closure are restricted to NEAppProxyFlowError values. If so, does the client application receive ECONNREFUSED when NEAppProxyFlowErrorRefused is passed? Is my understanding correct?

The way I usually wrangle this is:

  1. In handleNewFlow(_:), once I decide to accept the flow, I start the underlying connection and return true.
  2. Once the connection has connected successfully, I call open(withLocalFlowEndpoint:) on the flow.
  3. OTOH, if the connection fails, I call closeReadWithError(_:) and closeWriteWithError(_:) on the flow.
does the client application receive ECONNREFUSED when NEAppProxyFlowErrorRefused is passed?

It’s been a long time since I’ve tested this but, yeah, that’s my recollection. I don’t think it’s a coincidence that so many of the error codes use BSD Sockets terminology.

Share and Enjoy

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

I am afraid, due to our DLP requirements, we cannot allow applications to establish connections independently, bypassing our security controls. Therefore, unless an app explicitly matches an exclusion filter, handleNewFlow(_:) always returns true.

Am I correct in understanding that an application only establishes the actual network connection after handleNewFlow(_:) returns? Furthermore, I want to clarify the exact behavior if my Network Extension returns false. Could traffic potentially bypass the DLP in that scenario? Is there any guarantee that the app will be completely blocked from connecting to the remote host if the extension does not handle the flow (i.e., returns false) in case of ETIMEDOUT issue?

Thank you for your help!

Am I correct in understanding that an application only establishes the actual network connection after handleNewFlow(_:) returns?

If your handle-new-flow method returns true, thereby claiming the flow, then you’re in complete control over the traffic on the wire. The only way a connection is created is if you create it.

I want to clarify the exact behavior if my Network Extension returns false.

That behaviour depends on whether you’re an app proxy or a transparent proxy:

  • If you subclass NEAppProxyProvider directly then, if you return false, the system fails the connection.
  • If you subclass NETransparentProxyProvider then, if you return false, the system processes the connection as if your app proxy wasn’t installed [1].

Not that I’m recommending that you switch to being an app proxy. Transparent proxies have other nice features. See the doc comments in <NetworkExtension/NETransparentProxyProvider.h>.

If your transparent proxy claims a flow and you don’t want it to hit the wire, simply skip the “start the underlying connection” bit and go straight to step 3 (referencing my previous reply).

Share and Enjoy

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

[1] Well, it should do that. There’s been a long history of this not quite working correctly in various corner cases.

Thank you a lot for your help!

Best practices for handling nw_connection_state_waiting in Transparent Proxy
 
 
Q