-
Notifications
You must be signed in to change notification settings - Fork 0
Networking NAT and Routing
How client traffic reaches the internet, how the container sets up NAT, and how to choose full vs split tunnel.
VPN client ─► (encrypted) ─► ocserv ─► vpns0 (10.20.0.x) ─► [forward + masquerade] ─► eth0 ─► internet
- The client connects over TLS (or DTLS) to port 443.
- ocserv decrypts and writes the inner packet to a TUN device (
vpns0, …). - The kernel forwards it toward the WAN interface (requires
ip_forward=1). - nftables masquerades the source address to the container's WAN IP so replies can come back.
- Replies are reversed back through the tunnel to the client.
On startup the init-nat service enables forwarding and installs a dedicated nftables table. It's idempotent (rebuilt on each start) and uses the modern inet family so one rule set covers IPv4 (and IPv6 when enabled):
table inet ocserv {
chain forward {
type filter hook forward priority 0; policy accept;
iifname "vpns*" oifname "eth0" accept
iifname "eth0" oifname "vpns*" ct state established,related accept
}
chain postrouting {
type nat hook postrouting priority 100; policy accept;
ip saddr 10.20.0.0/24 oifname "eth0" masquerade
}
}
- The masqueraded subnet comes from
VPN_SUBNET. - The egress interface comes from
WAN_IF. - The tunnel interface pattern comes from
VPN_IF(vpns+→vpns*).
Inspect it live:
docker exec ocserv-server nft list table inet ocservThe image is built with ocserv's nftables firewall backend and ships
nft(notiptables).--cap-add=NET_ADMINis required to install these rules.
For NAT to work, these must agree:
Container (VPN_SUBNET) |
ocserv.conf (ipv4-network/ipv4-netmask) |
|---|---|
10.20.0.0/24 |
10.20.0.0 / 255.255.255.0
|
If they don't match, clients get addresses that nftables never masquerades, and their traffic silently fails to reach the internet.
Controlled in ocserv.conf:
# Full tunnel — ALL client traffic goes through the VPN
route = default
# Split tunnel — only these networks go through the VPN; the rest uses the
# client's normal connection
# route = 10.0.0.0/8
# route = 192.168.1.0/24-
Full tunnel is what you want for privacy / censorship circumvention. Pair it with
tunnel-all-dns = trueso DNS can't leak outside the tunnel. - Split tunnel is for reaching specific internal networks while leaving general browsing on the local link.
Routers (e.g. Keenetic) and full tunnel: even when the server pushes
route = default, a router won't necessarily send its own/its LAN's traffic through the tunnel — that's a router-side policy-based routing decision you configure on the router. See Clients and Devices#keenetic-routers.
IPv6 is off by default in the maintained samples, on purpose.
The failure mode: if you advertise an IPv6 address + route = ::/0 to clients but the container can't actually route IPv6 to the internet (no IPv6 on the Docker bridge, IPV6_NAT=0), client IPv6 traffic is blackholed — it goes into the tunnel and dies, with connections hanging before falling back to IPv4.
To enable IPv6 correctly:
- Give the container's Docker network working IPv6 (enable IPv6 in the Docker daemon / network).
- Set
IPV6_NAT=1(and keepIPV6_FORWARD=1). - In
ocserv.conf, addipv6-network,route = ::/0, and IPv6dnsservers.
Verify the container truly has IPv6 egress before advertising it:
docker exec ocserv-server ping -6 -c2 2606:4700:4700::1111If that fails, leave IPv6 off.
Next: Clients and Devices · Troubleshooting
ocserv-server · MIT License · Built on ocserv + s6-overlay