Thank you @roee84 for the timely response! This is very encouraging. I will look into changing the code to leverage the recommended approach.
Post
Replies
Boosts
Views
Activity
Hi @roee84,
To report back, createTCPConnectionThroughTunnel(to: endpoint, enableTLS:false, tlsParameters:nil, delegate:nil) seems to work! At least I can see in wireShark this call triggered DNS queries are now going through the VPN tunnel, instead of my home router IP. I believe HTTPS calls with this connection will also go through the tunnel.
I use a trick found on this forum by @spensaurus - https://developer.apple.com/forums/thread/13503?answerId=328010022#328010022 to convert packetFlow to an int so C code can use it as a socket (I'm aware Apple recommends against it.) In the future I might have to port all such C code to Swift. But it's a lot of work! For now, I'm just glad that this trick still works and so I can reuse the same kind of C code that works fine on android.
let tunFd = self.packetFlow.value(forKeyPath: "socket.fileDescriptor") as! Int32;
Is there a similar trick to get hold of the socket of the connection returned from this Swift call createTCPConnectionThroughTunnel() for C to use?
Thanks
Update: createTCPConnectionThroughTunnel() does allow app extension to send Https Json call through the tunnel. This is good news.
My remote server did receive the json request sent by my app extension, and it shows the request was from the IP of the VPN server.
Having a hard time getting the response though. My code crashes the app extension. I don't know how to properly handle Http request/response on a NWTCPConnection.
I created a class:
class HttpClientConnection: NSObject
in which there is a method:
open func startConnection(_ provider: NETunnelProvider, _ serverNamePort: String, _ httpRequestData: String, _ size: Int ) -> Bool {
...
connection = (provider as! NEPacketTunnelProvider).createTCPConnectionThroughTunnel(to: endpoint, enableTLS:true, tlsParameters:nil, delegate:nil)
connection!.addObserver(self, forKeyPath: "state", options: .initial, context: &connection)
The observer:
open override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey: Any]?, context: UnsafeMutableRawPointer?) {
guard keyPath == "state" && context?.assumingMemoryBound(to: Optional<NWTCPConnection>.self).pointee == connection else {
super.observeValue(forKeyPath: keyPath, of: object, change: change, context: context)
return
}
simpleTunnelLog("Tunnel connection state changed to \(connection!.state)")
switch connection!.state {
case .connected:
if let remoteAddress = self.connection!.remoteAddress as? NWHostEndpoint {
remoteHost = remoteAddress.hostname
}
// connect? We send the http request:
sendHttpRequest(httpRequestData, httpRequestDataSize) {_ in
simpleTunnelLog("Sent Http Request")
let response = self.receiveHttpResponse()
}
case .disconnected:
closeHttpConnectionWithError(connection!.error as NSError?)
case .cancelled:
connection!.removeObserver(self, forKeyPath:"state", context:&connection)
connection = nil
// delegate?.tunnelDidClose(self)
default:
break
}
}
This is the code I use to send out the JSON Http request in function:
open func sendHttpRequest(_ httpRequestData: String, _ size: Int, completionHandler: @escaping(Error?) -> Void) {
if let rawData = httpRequestData.data(using: .utf8) {
simpleTunnelLog("sendHttpRequest - to call write")
connection?.write(rawData, completionHandler: completionHandler) // this call crashes the app extension
}
Then, I tried to get the response. The key part is below method. It's not working, and causing memory issue. readMinimumLength() seems to come back w/o reading any data. I searched but couldn't find a good example on how to handle receiving of http response on a NWTCPConnection in Swift. I wish URLSession can be subclassed to use this connection returned by createTCPConnectionThroughTunnel().
func HttpRecvALine() -> String {
guard let targetConnection = connection else {
closeHttpConnectionWithError(SimpleTunnelError.badConnection as NSError)
return ""
}
var charThisRead = ""
var textRead = ""
while true {
targetConnection.readMinimumLength(1, maximumLength: 1) { data, error in
if let readError = error {
simpleTunnelLog("Got an error on the tunnel connection: \(readError)")
self.closeHttpConnectionWithError(readError as NSError?)
return
}
if let data = data {
charThisRead = String(decoding: data, as: UTF8.self)
textRead += charThisRead
simpleTunnelLog("textRead: \(textRead) - data\(data)") // nothing was read here.
} else {
simpleTunnelLog("No more data to read")
return
}
}
if charThisRead == "\n" {
// found end of line
simpleTunnelLog("found end of line - so far:\(textRead)")
break
}
}
return textRead
}
Any insight and pointers would be greatly appreciated.
Found the root cause of the issue. I was assuming that the readMinimumLength() is a blocking call but in reality it's an async call. After I moved the code of reading/parsing the HTTP response into the closure of this call, it started working.
I'm running into the extract same issue. Every time loadAllFromPreferences() is called, later there is one more notification on NEVPNStatusDidChange. For example, after 12 calls, the view controller will receive 12 Connecting status notifications followed by 12 Connected notifications, or 12 Disconnecting followed by 12 Disconnected notifications.
To answer @Matt's question, in my case, I've made sure I'm only calling addListener once: put them in viewDidLoad, and with logs showing when the function is called, plus I removeObserver() right before I addObserver(). I can see clearly addObserver is only called once. But the number of notifications keeps increasing for the same VPN status as more calls to loadAllFromPreferences are made.
My experiments seem to indicate that this is a side effect of calling loadAllFromPreferences. Each call to loadAllFromPreferences seems to keep its own copy/reference of the vpnManagers array that keeps sending notifications to the observers.
In my case, in order to get a vpnManager properly, I follow recommendations from this forum in this weird way and it works! Not sure if it has anything to do with this issue.
call loadAllFromPreferences()
call saveToPreferences()
call loadAllFromPreferences() for the 2nd time; vpnManagers[0] is what I need