Using SwiftNIO with NEAppProxyFlow

I'm implementing an NETransparentProxyProvider for TCP connections and would appreciate some guidance as to whether adopting SwiftNIO is appropriate for my use case.

The NetworkExtension framework abstracts connections and provides an NEAppProxyFlow for each which can be opened, read from, written to and closed.

Initially approaching this with a limited understanding of SwiftNIO, I expected to only need to implement two Channels: one to 'listen' for new flows (similar to ServerSocketChannel) and one to handle the flows themselves (similar to SocketChannel).

I've read through the documentation and added breakpoints to various parts of NIOPosix to understand the code paths that are taken when running a TCP server using ServerBootstrap, but I'm finding it difficult to understand if what I'm trying to implement is possible without having to write my own EventLoop and EventLoopGroup. For example, some types such as Selector, which I understand would be required to post events to SelectableEventLoops, are not public.

If this is feasible, any direction regarding how I could make a start with this would be greatly appreciated.

I'm not an expert in those APIs and you may want to ask on the developer forums to get clarification on the correct way to use NEAppProxyFlow, but it looks to me like you don't need new channels at all.

It should be sufficient to bind a ServerBootstrap and then construct an NWHostEndpoint from the server channel's localAddress. Then open would be sufficient to get the traffic flowing.

1 Like

Thanks for your reply.

An NEAppProxyFlow is handled within the process, rather than a connection being forwarded to a TCP server. Open, read, write and close methods are provided by the object to interact with the underlying TCP flow.

In the example below, all TCP flows are opened, sent a fixed response and then closed.

private static let response = Data("Hello, world!\n".utf8)

override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
    guard let tcpFlow = flow as? NEAppProxyTCPFlow else { return false }
    
    tcpFlow.open(withLocalEndpoint: nil) { error in
        if let error {
            os_log("Failed to open flow: %@", error.localizedDescription)
            return
        }
        
        tcpFlow.write(Self.response) { error in
            if let error {
                os_log("Failed to write to flow: %@", error.localizedDescription)
            }
            
            tcpFlow.closeReadWithError(nil)
            tcpFlow.closeWriteWithError(nil)
        }
    }
    
    return true
}

My understanding is that the NWHostEndpoint provided when opening a flow is used to control which local address and port number are made visible to the application that opened the socket.

Given how this example works, I still think that I'd need to implement two Channels in order to integrate with SwiftNIO. One would receive events from the handleNewFlow implementation whilst another would wrap individual NEAppProxyTCPFlows.

If you're going to implement channels, I recommend using the pattern in swift-nio-transport-services, not in swift-nio itself. swift-nio-transport-services uses Dispatch as its core eventing mechanism, and so it is vastly simpler to understand.

1 Like