OpenConnect not able to reach VPN-only destinations

To connect to my client’s corporate network, I have been using the Cisco AnyConnect Secure Mobility Client, and this works fine but does not seem to allow me to use split tunneling (despite the fact that the VPN itself, according to the app’s Statistics tab, has Tunnel Mode (IPv4): Split Include, which to my understanding should mean I can). (Yes, I understand why split tunneling can be dangerous, but the VPN’s lockdown is interfering with my work and my client’s IT department is uninterested in making exceptions for a contractor—and like I said, the configuration seems to allow it.)

Trying to find a way to get split tunneling to work, I’ve installed OpenConnect instead, and that connects just fine—it accepts my credentials, I see the company’s “banner” message in the log, it works. But none of the destinations that I can access only through the VPN work.

I suspect the problem is with the routes. Both AnyConnect and OpenConnect add a number of routes to particular network destinations (the same ones), but have different values for Gateway and Interface. For an example,

AnyConnect

===========================================================================
Interface List
 14...00 05 9a 3c 7a 00 ......Cisco AnyConnect Secure Mobility Client Virtual Miniport Adapter for Windows x64
[...]
===========================================================================


IPv4 Route Table
===========================================================================
Active Routes:
Network Destination        Netmask          Gateway       Interface  Metric
          0.0.0.0          0.0.0.0      192.168.1.1    192.168.1.151     35
[...]
          a.b.c.x  255.255.255.255         On-link           a.b.c.d      2
          a.b.c.y  255.255.255.255         On-link           a.b.c.d      2
          a.b.c.z  255.255.255.255         On-link           a.b.c.d      2
[...]
      192.168.1.1  255.255.255.255         On-link     192.168.1.151     36
[...]
        224.0.0.0        240.0.0.0         On-link           a.b.c.d  10000
[...]
  255.255.255.255  255.255.255.255         On-link           a.b.c.d  10000

OpenConnect

Interface List
[...]
 63...00 ff 8d 2a 8a 57 ......TAP-Windows Adapter V9
[...]
===========================================================================


IPv4 Route Table
===========================================================================
Active Routes:
Network Destination        Netmask          Gateway       Interface  Metric
          0.0.0.0          0.0.0.0      192.168.1.1    192.168.1.151     36
[...]
          a.b.c.x  255.255.255.255      a.b.c.(d+1)    192.168.1.151     36
          a.b.c.y  255.255.255.255      a.b.c.(d+1)    192.168.1.151     36
          a.b.c.z  255.255.255.255      a.b.c.(d+1)    192.168.1.151     36
[...]
        224.0.0.0        240.0.0.0         On-link           a.b.c.d    291
[...]
  255.255.255.255  255.255.255.255         On-link           a.b.c.d    291

Aside from the 0.0.0.0 route, all of these routes are added by connecting to the VPN. For 0.0.0.0, OpenConnect changes the Metric for route 0.0.0.0 to 36 (the values shown in the AnyConnect table are also the same as when I’m disconnected entirely). (For the record, AnyConnect also removes several IPv6 routes, which OpenConnect leaves alone—I don’t think this matters?)

To contrast the additions explicitly, AnyConnect uses “On-link” for the Gateway, while OpenConnect uses an IP address that is almost the same as what AnyConnect uses for Interface. For Interface, OpenConnect uses a private IP address. The IP addresses used for AnyConnect’s Interface and OpenConnect’s Gateway are in a range the client owns.

The metrics are also different—AnyConnect uses 2, which is higher priority than my 0.0.0.0 route (set to 35 when disconnected or connected via AnyConnect), while OpenConnect uses 36—and also changes my 0.0.0.0 route to use 36, so the priority is the same (all other routes on my system use a Metric higher than 36, for the record).

AnyConnect also adds a route for 192.168.1.1, to 192.168.1.151, which is what OpenConnect uses for the Interface. This is the only route that one has and the other lacks.

Both also add routes for 224.0.0.0 and 255.255.255.255, but AnyConnect uses a Metric of 10000 (higher than any other route), while OpenConnect uses 291 (higher than any other addition but lower than some other, existing routes I had before connecting). Interestingly, OpenConnect uses the same Gateway and Interface that AnyConnect does for those two routes, despite using different values for all the other routes.

This is on a Windows 10 Pro x64 machine. Cisco AnyConnect is Version 4.5.03040; OpenConnect GUI is “Version 1.5.3 (32 bit) [...] Based on OpenConnect v7.08.”

I don’t really know that much about routing or VPNs; even coming by this much information took a lot of searching and reading. I didn’t even know the term “split tunneling” before starting this. A lot of the information out there is outdated, too—Windows 10 doesn’t offer a “Use default gateway on remote network” option to uncheck as a lot of other answers here and elsewhere recommend for split tunneling under AnyConnect. I don’t know if there are other pertinent details I can provide, but I’m certainly happy to if anything else is needed. Obviously, I’ve been trying to mask my client’s IP addresses, but I don’t think the specific address should matter much here (I don’t know that there’s any reason to mask them either, but it’s not my information to share so I’m not).


Solution 1:

I was correct that the routes were the issue. It took a lot of trial and error to get right, but eventually updating the routes manually after connecting worked.

Strangely, route CHANGE didn’t work; I had to use route ADD, even though as far as I can tell these routes already existed after I connected. I used the same destination, mask, and gateway, and then METRIC 2 IF 17 (since the “TAP-Windows adapter V9” interface was using 17—I suspect it will change with each restart? disconnecting and reconnecting seems to use 17 consistently for now but as you can see in the question it was using 63 then). After doing this, there were two entries listed in route PRINT for that destination (the one added by OpenConnect and the one I added), and it allowed me to connect.

What I consider absolutely bizarre, though, is that my newly-added route has a Metric of 37—not the 2 I put in the route ADD command, and greater than the 36 that the existing entry had. But whatever, it works.

Luckily, our project maintains a hosts file (for developers to copy into their own hosts file when they start working on it, and that our build tools check against), so I was able to write a batch script to adjust the routes. For the sake of anyone else looking to do this, my batch script looks like this

FOR /F "tokens=1" %%i IN (\path\to\our\development\hosts) DO (
    route ADD %%i MASK 255.255.255.255 a.b.c.d METRIC 2 IF 17
)

The hosts file format is the same as that of the system file: a.b.c.d URL. This script doesn’t support comments (though I imagine the route ADD will simply fail) and I don’t know if blank lines will be a problem or not (but again, probably just a failed route ADD).

I will probably have to adjust it for that 17 since I suspect that will not be constant; when that comes up, I’ll look into how to determine what it is and use a variable for it instead. (And I’ll update this answer, too.)

Solution 2:

Base on KRyan's answer I modified my openconnect script (vpnc-script.js in the openconnect gui folder!) like this and it helped me in a similar situation:

for (var i = 0 ; i < parseInt(env("CISCO_SPLIT_INC")); i++) {
        var network = env("CISCO_SPLIT_INC_" + i + "_ADDR");
        var netmask = env("CISCO_SPLIT_INC_" + i + "_MASK");
        var netmasklen = env("CISCO_SPLIT_INC_" + i + "_MASKLEN");
        exec("route add " + network + " mask " + netmask);
    } 

=>

for (var i = 0 ; i < parseInt(env("CISCO_SPLIT_INC")); i++) {
        var network = env("CISCO_SPLIT_INC_" + i + "_ADDR");
        var netmask = env("CISCO_SPLIT_INC_" + i + "_MASK");
        var netmasklen = env("CISCO_SPLIT_INC_" + i + "_MASKLEN");
        exec("route add " + network + " mask " + netmask + " " + internal_gw+" metric 2 if "+env("TUNIDX"));
    } 

(line 175)

The bug in my opinion is here:

// Calculate the first usable address in subnet
var internal_gw_array = new Array(
    address_array[0] & netmask_array[0],
    address_array[1] & netmask_array[1],
    address_array[2] & netmask_array[2],
    (address_array[3] & netmask_array[3]) + 1
);
var internal_gw = internal_gw_array.join(".");

With a netmask of 255.255.255.255 for my internal IP passed in by var netmask_array = env("INTERNAL_IP4_NETMASK").split("."); , this gateway trick (ultimately a route goes to an interface, the gateway ip is pointless here) seems to fail