Breaking Network Barriers
My infrastructure spans two networks:
- Local Network (10.42.0.0/24): The home base
- Remote Network (192.168.20.0/24): The hypervisor cluster at a secondary location
Connecting them was supposed to be easy. “Just use Tailscale,” they said. “It’s a mesh VPN,” they said.
And they were right… until I tried to reach an LXC container inside the remote Proxmox host.
The Problem: LXC & The Mesh
On my Proxmox host, I run Tailscale. The host has IP 100.64.0.27.91 on the mesh and 192.168.20.100 on the LAN.
I also have an LXC container (192.168.20.196) running on the same Proxmox node.
The Goal: SSH from my laptop (100.x.y.z) to the container (192.168.20.196) seamlessly.
The Reality:
The Proxmox host advertises the 192.168.20.0/24 subnet. My laptop sends the packet. It hits the host. The host forwards it to the container. The container receives it.
The container replies. But the container’s default gateway is the physical router (192.168.20.1), NOT the Proxmox host. The packet goes to the router. The router looks at the destination (100.x.y.z), has no idea what that is (since it’s not on Tailscale), and drops it.
The connection hangs.
The Solution: Policy-Based Routing
To fix this, we need to force traffic destined for the Tailscale mesh (100.64.0.0/10) to go back through the tailscale0 interface, bypassing the default gateway.
We do this on the Proxmox Host via iptables and Linux Policy Routing.
Step 1: Enable IP Forwarding
First, the Proxmox host must act as a router.
# /etc/sysctl.conf
net.ipv4.ip_forward = 1
net.ipv6.conf.all.forwarding = 1
Step 2: Disable Reverse Path Filtering
Linux has a “martian packet” check. If a packet comes in on an interface that isn’t the one it would leave on, Linux drops it. We need to loosen this for our asymmetric routing.
# /etc/sysctl.d/99-tailscale.conf
net.ipv4.conf.tailscale0.rp_filter = 0
net.ipv4.conf.vmbr0.rp_filter = 0
Step 3: The Routing Rules
This is the magic. We tell the kernel: “If a packet is heading to the Tailscale subnet, use a special routing table.”
# Add source route for the mesh
ip route add 100.64.0.0/10 dev tailscale0 table main
# Add the local subnet to a special table (52)
ip route add 192.168.20.0/24 dev vmbr0 src 192.168.20.100 table 52
# Tell Tailscale traffic to use that table
ip rule add iif tailscale0 lookup 52 prio 100
Step 4: NAT Masquerading (SNAT)
Finally, to fool the LXC container into responding to us, we NAT the traffic as it leaves the Proxmox host towards the container. The container sees the request coming from the host (its local neighbor), not the laptop (an alien IP).
iptables -t nat -A POSTROUTING -s 100.0.0.0/8 -o vmbr0 -j SNAT --to-source 192.168.20.100
Now, the flow looks like this:
- Laptop sends packet to
192.168.20.196 - Proxmox Host receives it via Tailscale
- Proxmox Host rewrites source IP to
192.168.20.100(SNAT) - Proxmox Host forwards to Container
- Container replies to
192.168.20.100(the host) - Proxmox Host sees the reply, un-NATs it back to
100.x.y.z - Proxmox Host routes it back down the
tailscale0interface - Laptop receives the reply
Persistence
Wrap all of this into a script in /etc/network/if-up.d/tailscale-routes so it survives reboots.
#!/bin/bash
sleep 5
ip route add 100.64.0.0/10 dev tailscale0 table main 2>/dev/null || true
ip route add 192.168.20.0/24 dev vmbr0 src 192.168.20.100 table 52 2>/dev/null || true
ip rule add iif tailscale0 lookup 52 prio 100 2>/dev/null || true
The Result
I can now ssh [email protected] from a coffee shop in Paris, and it feels like I’m sitting in the server room. No port forwarding. No exposed router admin page. Just pure, encrypted mesh networking.