If you own an SSH server and that machine is behind a VPN, then apparently, without any special setup, you are not able to SSH to it with its original public IP from a client outside the VPN. An easy solution is to disconnect the machine from the VPN, so clients outside the VPN can access it. But what if you really really want to use the VPN on that machine?
Disclaimer: I am no expert, so I am not confident that all the procedures I did or all my understanding are correct. What I am sure is only that it works on my machine. That is to say, if you want to try my solution, please use with cautions. Also, any correction is welcome! I hope to learn from real experts!
The underlying issue of the SSH server behind a VPN is that, when a client connects to a server with the server’s public IP, the client expects to get server’s return traffic from the same public IP. But the server is behind a VPN. In other words, when the server tries to return SSH signals, it uses the VPN. Then the client gets the returned signals from VPN’s IP, not the server’s public IP that it’s expecting. So the client declines to accept the signals and drops the SSH connection.
The idea of the solution is to route server’s return SSH signal through its original IP. And this routing is allowed only when the server is responding to the traffic coming to the original IP, not other IPs (i.e., not the traffic coming to VPN’s IP).
Step 1: obtain subnet and mask information
There are many ways to achieve this step. Here is a newbie’s approach.
If you are using NetworkManager, then find the
inet4 information from the output of the command
nmcli. For example, the output may be something like this:
$ nmcli -------------------------------------------------------- ch-us-01.protonvpn.com.udp1194 VPN connection content hidden docker0: connected to docker0 content hidden enp8s0: connected to Wired connection 1 "Intel I211" ethernet (igb), xx:xx:xx:xx:xx:xx, hw, mtu 1500 inet4 18.104.22.168/23 route4 0.0.0.0/0 route4 x.x.x.x/24 tun0: connected to tun0 content hidden eno1: unavailable content hidden lo: unmanaged content hidden DNS configuration: content hidden
enp8s0 is the one connecting to the internet in this example. And the line
inet4 22.214.171.124/23 is the public IPv4 address and the mask. If you don’t know how to calculate the subnet, use any IP calculator on the internet. For example, using this calculator with the IP address (126.96.36.199) and the mask (23), in the output, the line
Network: 188.8.131.52/23 represents the subnet and the mask.
Step 2: obtain gateway information
Next, we need the gateway address. The following command outputs the traffic routes related to the internet interface
$ ip route show dev [INTERFACE]
Again, let’s use the previous example, in which
enp8s0 is the name of the interface. The output may look like:
$ ip route show dev enp8s0 ------------------------------------------------------------------------ default via 184.108.40.206 proto dhcp metric 100 220.127.116.11/23 proto kernel scope link src 18.104.22.168 metric 100 22.214.171.124 proto static scope link metric 100
126.96.36.199 in the line
default via ... is the gateway.
Step 3: add a routing table and rules
First, we add a table for traffic coming to the original public IP and name the able
# ip rule add from [PUBLIC IP] table 128
To my understanding, the above command says, whenever there’s traffic coming to the public IP, the system will look up the routing rules in the table
Next, we add routing rules to the table:
# ip route add table 128 to [SUBNET]/[MASK] dev [INTERFACE]
# ip route add table 128 default via [GATEWAY]
To be honest, I don’t really know what these two commands are doing. I have almost zero knowledge about network stuff. I don’t even really know what are subnet and gateway. Hope someone here can help me to understand.
But what I do know is that after this step, when there’s traffic coming to the public IP, the machine will accept them. And if the machine has to respond to/return the traffics, it will use the default IP, instead of VPN. But the problem is, we may not want the connections other than SSH to go/come through the public IP. So we have to block other connections through a firewall.
Step 4: allow firewall to only accept SSH traffic coming to the original public IP
In this example, I use
iptables to control the kernel-level linux firewall directly. I think other user-friendly interfaces should also work if you have one.
When there is traffic coming to the original public IP and to SSH server’s listening port, we want the system to accept it.
# iptables -A INPUT -d [PUBLIC IP] -p tcp --dport [SSH LISTENING PORT] -j ACCEPT
And for all other traffics coming to the original public IP, but not to SSH server’s port, we want the machine to drop them:
# iptables -A INPUT -d [PUBLIC IP] -j DROP
Now, clients outside the VPN should be able to SSH to the machine, while the machine is still using the VPN.
This doesn’t work if the device is behind NAT 🙁
Probably. If an SSH server using VPN is also behind NAT, solutions may depend on the configuration of the NAT.
But the key is still the same: let the SSH server return its message to clients from the true IP, instead of the VPN’s IP. Though the presence of NAT may cause some troubles when configuring traffic routing.
Hey thanks for this. I’ve played around with this idea off-and-on for years, but your instructions worked. Now just gotta figure out how it works. Thanks!