If you own an SSH server and that machine is behind a VPN, then 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 want to keep using the VPN on that machine?
Disclaimer: I am no expert, so I am not confident that all the procedures I used or all of my understanding are correct. What I am sure of is only that it works on my machine. That is to say, if you want to try my solution, please use caution. Also, any correction is welcome. I hope to learn from real experts.
The underlying issue of an SSH server behind a VPN is that when a client connects to a server with the server’s public IP, the client expects to receive the 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 traffic, it uses the VPN. Then the client receives the return traffic from the VPN’s IP, not the server’s original public IP that it is expecting. So the client rejects the packets and drops the SSH connection.
The idea of the solution is to route the server’s return SSH traffic through its original IP. And this routing is allowed only when the server is responding to traffic coming to the original IP, not to other IPs (i.e., not to traffic coming to the VPN’s IP).
Step 1: obtain subnet and mask information
There are many ways to do this. Here is a beginner’s approach. If you are using NetworkManager, then find the inet4 information in the output of the nmcli command. For example, the output may look 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 123.123.123.123/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*The interface enp8s0 is the one connecting to the internet in this example. And the line inet4 123.123.123.123/23 gives 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, if you use this calculator with the IP address 123.123.123.123 and the mask 23, the line Network: 123.123.122.0/23 in the output represents the subnet and the mask.
Step 2: obtain gateway information
Next, we need the gateway address. The following command outputs the routes related to the internet interface [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 this:
$ ip route show dev enp8s0------------------------------------------------------------------------default via 123.123.123.254 proto dhcp metric 100123.123.122.0/23 proto kernel scope link src 123.123.123.123 metric 100123.123.123.123 proto static scope link metric 100The IP 123.123.123.254 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 table 128:
# ip rule add from [PUBLIC IP] table 128To my understanding, the above command says that whenever there is traffic coming to the public IP, the system will look up the routing rules in table 128. Next, we add routing rules to that 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 of networking. I don’t even really know what subnet and gateway mean in detail. I hope someone can help me understand them better.
But what I do know is that after this step, when there is traffic coming to the public IP, the machine will accept it. And if the machine has to respond to that traffic, it will use the default IP instead of the VPN. But the problem is that we may not want connections other than SSH to go through the public IP. So we have to block other connections with 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 the 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 ACCEPTAnd for all other traffic coming to the original public IP, but not to the SSH server’s port, we want the machine to drop it:
# iptables -A INPUT -d [PUBLIC IP] -j DROPNow clients outside the VPN should be able to SSH to the machine while the machine is still using the VPN.