OpenVPN multihome — VPN Everywhere Except Home
My home VPN had exactly one dead zone: home. From a café, from the office, from cellular — it connects. From my own couch — timeout. This bugged me for a long time, and last night a one-line config change finally killed it.
The setup
The VPN server is my OpenWrt router, running OpenVPN 2.6.14 on udp/1194.
Its WAN address comes from DHCP, so clients connect by DDNS name, vpn.example.com.
Every client profile just says remote vpn.example.com 1194 and works from anywhere.
internet
│
vpn.example.com (DDNS)
→ 203.0.113.7
│
┌────────────────────────────────────────────┐
│ router (OpenWrt) OpenVPN 2.6.14, udp/1194 │
│ │
│ wan ─────────────── 203.0.113.7 │
│ lan ─────────────── 192.168.1.1/24 │
│ tun ─────────────── 10.8.0.1/24 │
└────────────────────────────────────────────┘
│ lan
┌───────────────┼───────────────┐
laptop phone tablet
The symptom
From outside, my laptop connects in about a second. From the LAN, the same profile, the same command, the same DNS name — the TLS handshake just never completes. The server sees the client’s packets; the client never sees a reply it accepts. No error, no log line pointing at the cause. Classic “works everywhere except where I actually sit” bug.
What is actually happening
DNS doesn’t know you’re at home.
vpn.example.com resolves to the WAN IP everywhere, so the laptop sends its handshake to 203.0.113.7:1194 even though it’s two meters from the router.
The packet never leaves the router — its destination is one of its own addresses — but that’s fine, OpenVPN listens on 0.0.0.0, so it receives it.
The reply is where it falls apart.
Without any special handling, the kernel picks the reply’s source address from the routing table: the laptop is on the LAN, so the “best” source is 192.168.1.1.
laptop (192.168.1.100) router
────────────────────────────► udp src 192.168.1.100:52114
dst 203.0.113.7:1194
OpenVPN replies:
routing table says "peer is on lan"
→ source becomes 192.168.1.1
◄──────────────────────────── udp src 192.168.1.1:1194
dst 192.168.1.100:52114
laptop: "I was talking to 203.0.113.7.
who is 192.168.1.1?" → drop. handshake
never finishes.
A UDP “connection” is really just the client remembering which address it dialed, and ignoring datagrams from anyone else. The server answered from a different face of itself, so as far as the client is concerned, nobody ever answered.
And here’s why this bug is so good at hiding: for a client on the internet, the routing table picks the WAN address as reply source — which happens to be the address the client dialed. Reply source matches, everything works. The mismatch only exists for clients reaching the WAN address from the inside. The network worked exactly when I was far away from it.
The fix: one line
# /etc/openvpn/server.conf on the router
proto udp
port 1194
multihome # ← this one
From the man page:
Configure a multi-homed UDP server. […] This option will add some extra lookups to the packet path to ensure that the UDP reply packets are always sent from the address that the client is talking to.
Mechanically, this is IP_PKTINFO.
The kernel now tells OpenVPN the destination address of every incoming datagram, and OpenVPN pins that same address as the source of its reply:
────────────────────────────► dst 203.0.113.7:1194
recvmsg(IP_PKTINFO):
"this packet was addressed to 203.0.113.7"
◄──────────────────────────── src 203.0.113.7:1194 ✓
sendmsg(IP_PKTINFO):
"reply from 203.0.113.7"
Now the reply comes from exactly the address the client dialed, whether that client is on the internet or on my couch.
Fun detail: run openvpn --version and the build flags include [MH/PKTINFO] — the support was sitting there the whole time, waiting for one config line.
Why I didn’t do the usual workarounds
The common fixes for this are all client-side or DNS-side: split-horizon DNS (answer the DDNS name with the LAN IP inside), or hairpin NAT rules on the router. They work, but they add a second code path that only exists at home, and every roaming device silently depends on it.
multihome is one server-side line, and the same .ovpn profile behaves identically on my LAN and on the road.
Two footnotes from the man page: it’s UDP-only (fine — UDP is what you want anyway), and it adds a little per-packet processing (irrelevant at home-router scale).
It also composes nicely with a DHCP WAN: I can’t pin the server to the WAN address with local when that address keeps changing, but a wildcard bind plus multihome gives me correct behavior for every address the router owns.
Applied it to the router at my other site too, for the same reason.
I found this while rotating per-device client certificates — testing each client as I went is exactly what forces you to connect from the LAN, which is exactly where this bug lives. A bug that only appears when you’re closest to the machine. That’s my kind of reminder that the network doesn’t care about physical distance, only about addresses.