How to redirect TCP flow using Transparent Proxy to original destination

Hi everyone! Currently, I am experimenting with NETransparentProxy Network Extension. As I understand from docs, returning false from handleNewFlow leads to routing the traffic to the original destination. Otherwise, the Network Extension is responsible for routing a flow somewhere. Currently, I simply want to play with NWConnection, read() and write() methods of NEAppProxyTCPFlow and NEAppProxyUDPFlow. My goal is to redirect traffic to the original address, but rather via using NWConnection after returning true. I have managed to do that with UDP flows, but struggling with TCP ones. These threads helped me a lot: https://developer.apple.com/forums/thread/655535?login=true, https://developer.apple.com/forums/thread/678464?page=1#671531022 However, my attempts to handle TCP flow failed. From the first thread, it is a bit unclear, what I have to do with a flow when I decide to handle it. It is stated that: "Decide if the flow is one you want your transparent proxy to handle and return true/YES in handleNewFlow." - should I save the flow object into some field of the class? I have tried, but in that case the proxy network profile changes state to "disconnected". Here is my current implementation.

override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
	os_log(.debug, log: self.log, "Handle new flow")
     if let tcpFlow = flow as? NEAppProxyTCPFlow {
         handleTCPFlow(tcpFlow)
         return true
     } 
     os_log(.debug, log: self.log, "Process id: \(processId)")
     return false
}

Here is handleTCPFlow:

private func handleTCPFlow(_ flow: NEAppProxyTCPFlow) {
    guard let targetEndpoint = flow.remoteEndpoint as? NWHostEndpoint else {
        os_log(.debug, log: self.log, "On outboundCopierTCP. Unable to cast remote    endpoint to NWHostEndpoint")
        return
    }
   let tcpConnectionManager = TCPConnectionManager(flow: flow, endpoint: targetEndpoint)
  tcpConnectionManager.startExchangingData()
}
Answered by yblk in 725401022

Seems like the code snippet from my last reply (with calls of inbound- and outbound- copiers inside open() completion handler) works, I am successful in controlling the traffic. On the other hand, is it supposed to work like that?

Finally, TCPConnectionManager

class TCPConnectionManager {

    public let flow: NEAppProxyTCPFlow
    private let connection: NWConnection
    private let tcpConnection = NWTCPConnection()
    private let log = OSLog(subsystem: "com.ybelikov.transparent.proxy.network.extension", category: "provider")
    private let connectionQueue = DispatchQueue(label: "com.ybelikov.transparent.proxy.network.extension.TCPQueue")
    init(flow: NEAppProxyTCPFlow, endpoint: NWHostEndpoint) {
        self.flow = flow
        let host = Network.NWEndpoint.Host(endpoint.hostname)
        let port = Network.NWEndpoint.Port(endpoint.port) ?? Network.NWEndpoint.Port.any
        os_log(.debug, log: self.log, "On TCP init. host: %@, port: %@", String(describing: host), String(describing: port))
        self.connection = NWConnection(host: host, port: port, using: .tcp)
    }
   
    public func startExchangingData() {
        connection.stateUpdateHandler = self.stateChangedCallback(to:)
        connection.start(queue: connectionQueue)
    }

    private func stateChangedCallback(to state: NWConnection.State) {
            switch state {
            case .ready:
                os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is ready")
                flow.open(withLocalEndpoint: tcpConnection.localAddress as? NWHostEndpoint) { error in
                    if let error = error {
                        os_log(.debug, log: self.log, "TCP flow opening failed %@", error.localizedDescription)
                        return
                    }
                    self.handleOutgoingTCPData()
                }
                break

            case .failed(let error):
                os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is failed %@", error.localizedDescription)
                self.connection.cancel()
                break

            case .cancelled:
                os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is cancelled")
                break

            case .preparing:
                os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is preparing")
                break

            case .setup:
                os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is in setup state")
                break

            case .waiting(let error):
                os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. TCP connection is in waiting state, %@", error.localizedDescription)
                self.connection.cancel()
                break        

            default:
                os_log(.debug, log: self.log, "TCPConnectionManager::stateChangedCallback. State is unknown")
                break
            }

    }

    

    private func handleOutgoingTCPData() {
        os_log(.debug, log: self.log, "handleOutgoingTCPData. handling TCP flow data")
        flow.readData { data, error in
            if let error = error {
                os_log(.debug, log: self.log, "handleOutgoingTCPData. Error on handling TCP flow data: %@", error.localizedDescription)
                return
            }

            if let data = data, !data.isEmpty {
                self.connection.send(content: data, completion: .contentProcessed({ error in
                    if let error = error {
                        os_log(.debug, log: self.log, "TCPConnectionManager::sendDataToEndpoint. TCP error: %@", error.localizedDescription)
                        return
                    }
                    os_log(.debug, log: self.log, "TCP data sent successfully")
                    self.handleOutgoingTCPData()
                }))
            } else {
                self.handleIncomingTCPData()
            }
    	}
    }

    private func handleIncomingTCPData() {
        os_log(.debug, log: self.log, "On handleIncomingTCPData")
        connection.receive(minimumIncompleteLength: 0,

                               maximumLength: 2048) { (data, _, isComplete, error) in

            switch (data, isComplete, error) {

            case (let data?, _, _):

                os_log(.debug, log: self.log, "On handleIncomingTCPData. Received data, writing it to the flow")

                self.flow.write(data) { writeError in

                    if writeError == nil {

                        os_log(.debug, log: self.log, "On handleIncomingTCPData. Write error is nil")
                        self.handleIncomingTCPData()
                    }
                }

            case (_, true, _):
                os_log(.debug, log: self.log, "On handleIncomingTCPData. Connection is completed")
                self.connection.stateUpdateHandler = nil
                self.connection.cancel()
                self.flow.closeReadWithError(error)
                self.flow.closeWriteWithError(error)

            case (_, _, let error?):
                os_log(.debug, log: self.log, "On handleIncomingTCPData. Error: %@", error.localizedDescription)
                self.connection.cancel()
                self.flow.closeReadWithError(error)
                self.flow.closeWriteWithError(error)

            default:
                os_log(.debug, log: self.log, "On handleIncomingTCPData. Unknown case")
            }
        }
    }
}

should I save the flow object into some field of the class?

Yes. Because you’ll need that object to read and write data on the flow.

I have tried, but in that case the proxy network profile changes state to "disconnected".

That suggests that your provider is crashing.

Share and Enjoy

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

Thank you for reply, Eskimo! So, I try to handle TCP flow like this

override func handleNewFlow(_ flow: NEAppProxyFlow) -> Bool {
    	os_log(.debug, log: self.log, "Handle new flow")
        if let tcpFlow = flow as? NEAppProxyTCPFlow {
            guard let targetEndpoint = tcpFlow.remoteEndpoint as? NWHostEndpoint else {
                os_log(.debug, log: self.log, "On outboundCopierTCP. Unable to cast remote endpoint to NWHostEndpoint")
                return false
            }
            let tcpConnectionManager = TCPConnectionManager(flow: tcpFlow, endpoint: targetEndpoint)
            tcpConnectionManager.startExchangingData()
            return true
        } 
        return false
    }

Am I heading to the right direction? You can review a TCPConnectionManagerClass implementation in my reply above. It manages NWConnection with intended target of the flow and read\write operation between connection and flow

I have read Handling flow copying article. So, it doesn't clear to me how I can switch between reading and writing on both sides of the connection. Here is the example of outboundCopier() from the article

 let flow: NEAppProxyTCPFlow
let connection: NWConnection

// Reads from the flow and writes to the remote connection.
func outboundCopier() {

    flow.readData { (data, error) in
        if error == nil, let readData = data, !readData.isEmpty {
            connection.send(content: readData, 
                            completion: .contentProcessed( { connectionError in
                // Handle completion success or error.
                // Set up another read if there is no error.
                if connectionError == nil {
                    self.outboundCopier()
                }
            }))
        } else {
            // Handle error case or the read that contains empty data.
        }
    }
}

I see that the method is called recursively until there is data that can be sent from the flow to the remote endpoint. However, I don't get where we should call inboundCopier() to read the response. Should it be called in the else branch from the outboundCopier(), when we detect that there is no flow data left unread? Or I can call inbound and outbound copiers synchronously inside the flow open() completion handler, when the state of the NWConnection transfers to .ready? For instance:

case .ready:
    flow.open(withLocalEndpoint: nil) { error in
        if let error = error {
            os_log(.debug, log: self.log, "TCP flow opening failed %@", error.localizedDescription)
            return
        }
        self.outboundCopier()
        self.inboundCopier()
   }

Let's assume that inboundCopier() and outboundCopier() have the same implementations as in the article Thank you in advance, for explanation

Accepted Answer

Seems like the code snippet from my last reply (with calls of inbound- and outbound- copiers inside open() completion handler) works, I am successful in controlling the traffic. On the other hand, is it supposed to work like that?

On the other hand, is it supposed to work like that?

Yes. Both the inbound and outbound sides of the flow need a flow copier.

Share and Enjoy

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

How to redirect TCP flow using Transparent Proxy to original destination
 
 
Q