ss-abramchuk / OpenVPNAdapter

Objective-C wrapper for OpenVPN library. Compatible with iOS and macOS.
GNU Affero General Public License v3.0
482 stars 215 forks source link

KEEPALIVE_TIMEOUT log message from provider leaves the tunnel dead #141

Open javierdemartin opened 5 years ago

javierdemartin commented 5 years ago

I am tunnelling my traffic through a server. In some cases it has to block determinate websites, e.g. gambling sites. The tunnel works without a problem if I start the tunnel with WiFi or Cellular. When I change the network to the opposite the VPN reconnects automatically as I am using on demand VPN but the VPN doesn't block the websites it did before. My main supposition is that after changing the interface, WiFi to Cellular or vice versa, the outgoing traffic from the device it is not routed through the tunnel interface (utun) but the VPN is still up.

How could I check or "force" the outgoing traffic through my tunnel interface?

I can detect when the interface changes and I could force restart the tunnel but I shouldn't do it. That should be the tunnel's task and I prefer not to force the tunnel's state.

This also happens after a while, between 2 to 5 minutes, the VPN restarts and in the network extension's provider openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) method I get the following messages

Session invalidated: KEEPALIVE_TIMEOUT Client terminated, restarting in 2000 ms...

I don't know if this has to be related with my Swift code or with the OpenVPN server setup.

ms-tii commented 5 years ago

Facing exactly same issue while using VPN ON Demand. Did you find any solution ?

Also in my case- when I switched mobile network from Wifi to LTE or LTE to Wifi, device looses internet due to connection from client to server. And when switching network VPN gets stuck & in iphone settings I can see it stays in loop of connecting , disconnecting & so on., due to this device loose out internet connection.

Don't know what to do ? Can we generate log file similar to one in OpenVPN app ?

javierdemartin commented 5 years ago

I am monitoring my interface changes using defaultPath in the provider. There depending on the interface changes I can reconnect/cancel the tunnel. The problem is that in some cases it enters a loop draining the battery. And I don't know in which cases do I have to resume/cancel/reconnect my tunnel.

I don't think it's the lack of documentation on how to do this with this library. I don't see anything in the Apple's documentation that details how to handle these interface changes.

ms-tii commented 5 years ago

@javierdemartin Thanks for reply, If it is possible for you, can you share the code snippet for monitoring interface changes? Actually i don't know how to use defaultPath in the provider. And I m using .ovpn file to connect with vpn.

javierdemartin commented 5 years ago

You need to add your this observer in the NEPacketTunnelProvider (the network extension target)

// Determine the change in physical network interfaces observing the property using KVO
let options = NSKeyValueObservingOptions([.new, .old])
self.addObserver(self, forKeyPath: "defaultPath", options: options, context: nil)

And observe the changes in the interfaces,

override func observeValue(forKeyPath keyPath: String?, of object: Any?, change: [NSKeyValueChangeKey : Any]?, context: UnsafeMutableRawPointer?) {

        guard let keyPath = keyPath else {
            return
        }

        // defaultPath is the current default path used for connections created by the provider (tunnel-extension)
        if keyPath == "defaultPath" {
            // NWPath objects contain information about which physical network interface will be used by connections
            // opened by the Network Extension provider.
            let oldPath = change?[.oldKey] as! NWPath // Old interface
            let newPath = change?[.newKey] as! NWPath // New interface

            if !reasserting {

                // For example, WiFi (unsatisfied) --> Cellular (satisfied) disconnects from WiFi and switches to Cellular, cancel the tunnel
                if (!oldPath.isExpensive && oldPath.status == .unsatisfied && newPath.isExpensive && newPath.status == .satisfied) {
                    self.cancelTunnelWithError(nil)

                }
            }
}

Here is a reference from StackOverflow talking about it.

ms-tii commented 5 years ago

@javierdemartin Hi,

Can you help me ? I want to print logs same like OpenVPN client app. They shows all network tests and vpn details(handshakes, sleep/wake, uptime) in logs. Can we do the same with this adapter? If yes HOW and in which class (tunnelProvider or can in viewController).

I tried to use this method but it doesn't transfer live continuous value to print on run time in viewController class. I was using userDefaults share data between app and tunnelExtension like:- In PacketTunnel - let kAppGroupName = "group.com.abc.efgh" var sharedContainer : UserDefaults? self.sharedContainer = UserDefaults(suiteName: kAppGroupName) func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) { // Handle log messages if let sharedContainer = self.sharedContainer { sharedContainer.set(logMessage as String, forKey: "networkLogsMessages") sharedContainer.synchronize() } }

In viewController where networkmonitor - let kAppGroupName = "group.com.abc.efgh" var sharedContainer : UserDefaults? self.sharedContainer = UserDefaults(suiteName: kAppGroupName) func networkStatusMonitor(){ NetStatus.shared.netStatusChangeHandler = { DispatchQueue.main.async { [unowned self] in if let sharedContainer = self.sharedContainer { let logMessages = sharedContainer.string(forKey: "networkLogsMessages") print(logMessages) } } } } But it only shows once and old saved logs because when any logMessage occured it saves in userDefaults(in extension) and when in viewController on button click I retrieve these message(In viewDidLoad) from userdefaults it show once only. Within this time period there are new logs occured and it only shows the previous fetched.

javierdemartin commented 5 years ago

I am solving this at the moment by cancelling the tunnel if that log message is received from the provider. It's a workaround as I am checking if the String contains the KEEPALIVE_TIMEOUT message. The implementation is as follows,

func openVPNAdapter(_ openVPNAdapter: OpenVPNAdapter, handleLogMessage logMessage: String) {

        if logMessage.contains("KEEPALIVE_TIMEOUT") {
            cancelTunnelWithError(CancelTunnelErrors.keepAliveTimeout)
        }

    }

I've seen that there is an error of the type OpenVPNAdapterErrorKeepaliveTimeout, how could I be able to throw an error that goes to openVPNAdapter(_, handleError) so I can manage the error there or subscribe to a notification of that event happening. Pinging @ss-abramchuk

ss-abramchuk commented 5 years ago

Hi @javierdemartin,

openvpn3 library responsible for throwing errors. I think it doesn't treat KEEPALIVE_TIMEOUT as error because you have ping-restart in your config. Probably using of ping-exit might help but I'm not sure, I couldn't find any reference of this option in openvpn3 source code.

As for switching interface, normally, these lines of code should force resetting of the tunnel and initialize reconnection using active interface:

vpnReachability.startTracking { [weak self] status in
    guard status != .notReachable else { return }
    self?.vpnAdapter.reconnect(interval: 5)
}

I'm going to update dependencies this weekend and will take a look at this issue as well. It would be very handy if you share your OpenVPN config with me so that I could find root of the problem.

javierdemartin commented 5 years ago

I've solved my issue by cancelling the tunnel when a KEEPALIVE_TIMEOUT message is received and also when the vpnReachability status is .notReachable. By doing so I guarantee my traffic is always going out of my tunnel interface.

My .ovpn configuration is as follow

client
remote VPN_SERVER_ADDRESS 1194 udp
nobind
dev tun
persist-tun
persist-key
verify-x509-name VPN_SERVER_ADDRESS name
remote-cert-tls server
cipher AES-128-CBC

<ca>
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----

-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
</ca>
<cert>
-----BEGIN CERTIFICATE-----
-----END CERTIFICATE-----
</cert>
<key>
-----BEGIN PRIVATE KEY-----
-----END PRIVATE KEY-----
</key>