Running a single SSH session over VPN
Downsides of VPN
Everybody who’s worked remotely is probably familiar with VPNs. You run some application on your machine, such as Cisco AnyConnect or OpenConnect, which have you type in some credentials, some magic happens in the background, and suddenly you can reach some internal company servers.
You might also be familiar with the downsides of VPNs, such as:
- Not being able to resolve some websites while connected to the VPN.
- Terrible slowdowns as a result of proxying all trafic through the VPN.
- Or just the hassle of having to set up proxy configurations just so you can access the outside world.
This seems dumb to me. I have a perfectly working network connection on my machine already that I use to access the internet when not connected to the VPN, so why can’t I just use that to do all my normal network access, and only use the VPN connection for applications that absolutely need it (such as a single SSH session for example)? The answer is: it’s possible, but doesn’t seem well supported.
How do VPNs work?
Keep in mind that I mostly figured this out on my own with some googling (which didn’t yield much results) and just experimenting with my own setup, a Windows 10 machine. So, I don’t really know what I’m talking about.
I noticed that these VPN clients create a virtual network interface, on Windows this is a TAP-Windows v6 Adapter, which is created by OpenConnect (I believe it uses a TUN driver on Linux, but very similar idea).
These VPN clients also set up several routes in the OS’ routing table. The routing table is assentially a list of IP addresses and masks, with a network interface, gateway, and “Metric” (a sort of priority) associated with them. You can see the table by using route print
(route -n
on Linux) which looks something like this (irrelevant sections removed, and obfuscated for obvious reasons):
IPv4 Route Table
===========================================================================
Active Routes:
Network Destination Netmask Gateway Interface Metric
0.0.0.0 0.0.0.0 142.236.178.1 142.236.178.35 26
127.0.0.0 255.0.0.0 On-link 127.0.0.1 331
127.0.0.1 255.255.255.255 On-link 127.0.0.1 331
127.255.255.255 255.255.255.255 On-link 127.0.0.1 331
142.236.178.0 255.255.255.0 On-link 142.236.178.35 281
142.236.178.35 255.255.255.255 On-link 142.236.178.35 281
142.236.178.255 255.255.255.255 On-link 142.236.178.35 281
224.0.0.0 240.0.0.0 On-link 127.0.0.1 331
224.0.0.0 240.0.0.0 On-link 142.236.178.35 281
255.255.255.255 255.255.255.255 On-link 127.0.0.1 331
255.255.255.255 255.255.255.255 On-link 142.236.178.35 281
===========================================================================
Persistent Routes:
None
When you connect to a VPN using e.g. OpenConnect, some routes are added here (the 1.2.50.2
interface):
IPv4 Route Table
===========================================================================
Active Routes:
Network Destination Netmask Gateway Interface Metric
0.0.0.0 0.0.0.0 142.236.178.1 142.236.178.35 26
0.0.0.0 0.0.0.0 1.2.32.1 1.2.50.2 2
1.2.32.0 255.255.224.0 On-link 1.2.50.2 257
1.2.50.2 255.255.255.255 On-link 1.2.50.2 257
1.2.63.255 255.255.255.255 On-link 1.2.50.2 257
127.0.0.0 255.0.0.0 On-link 127.0.0.1 331
127.0.0.1 255.255.255.255 On-link 127.0.0.1 331
127.255.255.255 255.255.255.255 On-link 127.0.0.1 331
142.236.178.0 255.255.255.0 On-link 142.236.178.35 281
142.236.178.35 255.255.255.255 On-link 142.236.178.35 281
142.236.178.255 255.255.255.255 On-link 142.236.178.35 281
224.0.0.0 240.0.0.0 On-link 127.0.0.1 331
224.0.0.0 240.0.0.0 On-link 142.236.178.35 281
224.0.0.0 240.0.0.0 On-link 1.2.50.2 257
255.255.255.255 255.255.255.255 On-link 127.0.0.1 331
255.255.255.255 255.255.255.255 On-link 142.236.178.35 281
255.255.255.255 255.255.255.255 On-link 1.2.50.2 257
===========================================================================
Persistent Routes:
Network Address Netmask Gateway Address Metric
0.0.0.0 0.0.0.0 1.2.32.1 1
Note the “Metric” collumn. This is essentially the priority by which these routes function. A route with a lower metric will be chosen before one with a higher metric.
The VPN client sets a default route (0.0.0.0
) with a high priority (2
), as a way to catch all outgoing network connections, and route them trough the VPN client’s virtual network interface. At that point the VPN client software takes over and handles to actual tunnelling to the VPN network.
Routing all traffic through my normal internet connection
First step is to change my default route from the VPN network interface back to my normal, physical network interface. This can be done simply by changing the metric of the VPN interface, and setting it to a lower value than my normal interface. In powershell, I can manipulate my network interfaces using a set of (G|S)et-NetIpInterface
functions. To list all network interfaces and their metrics I can use Get-NetIpInterface
(ip link show
on Linux):
> Get-NetIpInterface
ifIndex InterfaceAlias AddressFamily NlMtu(Bytes) InterfaceMetric Dhcp ConnectionState PolicyStore
------- -------------- ------------- ------------ --------------- ---- --------------- -----------
20 OpenConnect IPv6 1300 35 Enabled Connected ActiveStore
10 Ethernet 3 IPv6 1500 25 Enabled Connected ActiveStore
1 Loopback Pseudo-Interface 1 IPv6 4294967295 75 Disabled Connected ActiveStore
20 OpenConnect IPv4 1300 1 Disabled Connected ActiveStore
10 Ethernet 3 IPv4 1500 25 Enabled Connected ActiveStore
1 Loopback Pseudo-Interface 1 IPv4 4294967295 75 Disabled Connected ActiveStore
The OpenConnect interface is the one we want to set to a lower priority. It’s index is 20
, so I can use (with admin permissions) Set-NetIpInterface -InterfaceIndex 20 -InterfaceMetric 35
:
> Set-NetIpInterface -interfaceindex 20 -interfacemetric 35
> Get-NetIpInterface
ifIndex InterfaceAlias AddressFamily NlMtu(Bytes) InterfaceMetric Dhcp ConnectionState PolicyStore
------- -------------- ------------- ------------ --------------- ---- --------------- -----------
20 OpenConnect IPv6 1300 35 Enabled Connected ActiveStore
10 Ethernet 3 IPv6 1500 25 Enabled Connected ActiveStore
1 Loopback Pseudo-Interface 1 IPv6 4294967295 75 Disabled Connected ActiveStore
20 OpenConnect IPv4 1300 35 Disabled Connected ActiveStore
10 Ethernet 3 IPv4 1500 25 Enabled Connected ActiveStore
1 Loopback Pseudo-Interface 1 IPv4 4294967295 75 Disabled Connected ActiveStore
And that’s it… Now when I run e.g. FireFox and try to access the internet, it will use my normal physical network connection to connect to the internet, and I’m no longer burdened by the downsides of going through the VPN.
So, what’s the point? I’ve essentially turned on my VPN, and then turned it off again. Well, the difference is that, even though, by default, network is routed through my normal network connection, I’m still connected to the VPN, so I can now try to find a way to route single connections (e.g. a single SSH session) through the VPN, while not affecting my normal network traffic.
Binding a single SSH session to the VPN’s network interface
Can I bind an ssh
session to the VPN’s network interface explicitly? It look like ssh
has a -b
option where I can specify a bind_address
of the interface to bind to. Great! Except, it doesn’t seem to work (at least not on the Windows OpenSSH client):
> ssh -b 1.2.50.2 my-server -v
OpenSSH_for_Windows_7.7p1, LibreSSL 2.6.5
debug1: Reading configuration data C:\\Users\\Jorn/.ssh/config
debug1: C:\\Users\\Jorn/.ssh/config line 7: Applying options for my-server
debug1: Connecting to my.server.com [123.123.12.8] port 22.
debug1: ssh_create_socket: bound to 1.2.50.2
debug1: connect to address 123.123.12.8 port 22: Invalid argument
ssh: connect to host my.server.com port 22: Invalid argument
It seems like the -b
option is tripping up the actual connection. ssh
also has a -B
option, where you can specify the name of the interface instead, but this just fails with BindInterface not supported on this platform.
(the typical using-Unix-tools-on-Windows experience).
For what it’s worth, I did also try this through WSL, which does except the -b
and -B
, but then fails with a timeout instead. Some trickery seems to be happening that is routing the traffic through the default interface still. Printing the routing table in WSL with route -n
shows the relevant interfaces, but the table doesn’t get updated when I change the metric of one of them. Maybe this actually works on a real Unix machine? (Haven’t tried it out yet).
Workaround
The only workaround I’ve found so far is temporarily setting the VPN interface as highest priority, connecting with ssh, and then setting the priority back.
> Set-NetIpInterface -interfaceindex 20 -interfacemetric 1
# connect with ssh in another shell
> Set-NetIpInterface -interfaceindex 20 -interfacemetric 35
The ssh session will continue to function, which is enough for my purposes.
I’d like to be able to do something similar with FireFox, i.e. bind a single FireFox instance to the VPN network interface, but from googling around, it doesn’t seem that binding a FireFox instance to a particular network interface is supported :(
Closing thoughts
Is this really such a weird idea? Why isn’t this better supported? Or am I just doing something wrong?
When looking things up on the internet one of the alternatives I’ve found is the suggestion to just add the relavant target addresses to my routing table myself. Except, I have no idea what those addresses are. I’m not just accessing things on the VPN, I’m also accessing things that are only resolvable when connecting through the VPN, and I have no way of knowing the range of IP addresses that those things will occupy.
It’s also been suggested that this is such an uncommen case that better support for this is not needed. I strongly disagree. Especially with people working from home increasingly during the COVID-19 pandamic, VPN use (and thus mutliple network interfaces per device) has surely gone up significantly?
FireFox doesn’t seem to support binding to a particular network interface, but maybe there are other browsers that do (I’m not hopeful though).
At least I have gotten what I wanted working at least somewhat, though it annoyingly requires some mechanical manual steps (still have to investigate some wrapper scripts).