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 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
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 (123.123.123.123) and the mask (23), in the output, the line Network: 123.123.122.0/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 [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 123.123.123.254 proto dhcp metric 100
123.123.122.0/23 proto kernel scope link src 123.123.123.123 metric 100
123.123.123.123 proto static scope link metric 100
The 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 able 128
:
# 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 128
.
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.
3 Comments
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!