-
Notifications
You must be signed in to change notification settings - Fork 0
Gateway Mode
Gateway mode sends your VPN clients' traffic out through an upstream VPN container instead of straight out the host. Typical use: chain ocserv in front of a commercial VPN (e.g. NordVPN) so clients connect to your own OpenConnect server but exit with the commercial VPN's IP.
client ──openconnect──▶ ocserv ──policy route──▶ nordvpn ──tun0──▶ internet
10.20.0.x SNAT→172.28.0.3 (FORWARD_FROM) commercial exit IP
ocserv keeps its own network namespace and its inbound listener works normally; only the client subnet is steered to the upstream gateway.
Set VPN_GATEWAY to the upstream container's IP on the shared Docker network:
| Variable | Default | Description |
|---|---|---|
VPN_GATEWAY |
(unset) | Upstream gateway IP. Steers VPN_SUBNET to it and installs the kill switch. Unset = normal standalone ocserv. |
VPN_GATEWAY6 |
(unset) | Upstream IPv6 gateway. Set it to route the IPv6 client subnet too; unset = forwarded client IPv6 is dropped. |
VPN_GATEWAY_TABLE |
100 |
Routing table used for the gateway default route. |
VPN_GATEWAY_RULE_PRIO |
1000 |
Priority of the from <VPN_SUBNET> policy rule. |
When VPN_GATEWAY is unset, the init-vpngw service is a no-op — ocserv behaves exactly as a standalone server (including normal IPv6).
-
Source-based policy routing —
init-vpngwaddsip rule from <VPN_SUBNET> lookup <table>and a default route in that table viaVPN_GATEWAY. Only client-sourced packets follow it; ocserv's own traffic and the inbound listener keep the main default route. -
Masquerade —
init-natalready SNATs clients to the container's own address, so the upstream sees a Docker-subnet source and needs no return route to your client subnet. -
Kill switch — a dedicated nft table (
inet ocserv_gw) drops any client packet that would egress the WAN by a next-hop other than the gateway. See below.
Pointing ocserv's default route at the upstream breaks inbound: a reply to a connecting client would follow the default route into the upstream tunnel and exit with the wrong source IP, so the client drops it. Source-based policy routing avoids this — the listener's replies stay on the main route, only client traffic is redirected.
The policy route already forces client traffic to the gateway, but the kill switch makes leaks impossible if that route is ever missing:
table inet ocserv_gw {
chain forward {
type filter hook forward priority -10; policy accept;
ip saddr 10.20.0.0/24 oifname "eth0" rt ip nexthop != 172.28.0.2 drop
ip6 saddr fd20:…::/64 oifname "eth0" rt ip6 nexthop != fd00::2 drop # if VPN_GATEWAY6 set
# meta nfproto ipv6 iifname "vpns*" drop # if VPN_GATEWAY6 unset
}
}
What happens when the upstream is unavailable:
| Situation | Result |
|---|---|
| Upstream tunnel down (container up) | Forwarded client packets have no tun0 to exit on the upstream; the upstream's FORWARD policy DROP blocks them. No leak.
|
| Upstream container stopped/absent | The gateway IP doesn't answer ARP; the policy-routed packets are dropped at ocserv. No leak. |
| Policy route somehow missing | The next-hop guard above drops client traffic instead of letting it fall through to the host. No leak. |
In every case clients lose internet rather than leaking out the host's real IP.
-
Upstream is IPv4-only (the NordVPN container is, by default): leave
VPN_GATEWAY6unset. Forwarded client IPv6 is dropped so it can't bypass the IPv4 policy rule. -
Upstream is dual-stack: set
VPN_GATEWAY6to its IPv6 address. ocserv policy-routesIPV6_SUBNETto it with the same fail-closed next-hop guard. This also needs working IPv6 on the Docker network and the upstream forwarding IPv6 (see Networking NAT and Routing#ipv6).
The upstream must forward the Docker subnet out its tunnel. The companion NordVPN image does this with FORWARD_FROM:
networks:
vpnnet:
ipam:
config:
- subnet: 172.28.0.0/24
services:
nordvpn:
image: azinchen/nordvpn:latest
cap_add: [NET_ADMIN]
devices: [/dev/net/tun]
sysctls:
- net.ipv4.ip_forward=1
environment:
- USER=service_username
- PASS=service_password
- COUNTRY=Netherlands
- FORWARD_FROM=172.28.0.0/24 # let the Docker net route out the tunnel
networks:
vpnnet:
ipv4_address: 172.28.0.2
ocserv:
image: azinchen/ocserv-server:latest
cap_add: [NET_ADMIN]
devices: [/dev/net/tun]
sysctls:
- net.ipv4.ip_forward=1
environment:
- VPN_SUBNET=10.20.0.0/24
- VPN_GATEWAY=172.28.0.2 # the nordvpn container
ports:
- "443:443/tcp" # published normally on ocserv itself
- "443:443/udp"
volumes:
- ./config:/etc/ocserv
networks:
vpnnet:
ipv4_address: 172.28.0.3FORWARD_FROM must list the subnet ocserv SNATs into (the Docker network, 172.28.0.0/24), not the client subnet.
# policy routing on ocserv
docker exec ocserv-server ip rule
docker exec ocserv-server ip route show table 100
# kill switch
docker exec ocserv-server nft list table inet ocserv_gw
# a connected client's exit IP should equal the upstream's, not the host's
docker exec ocserv-server sh -c 'curl -s https://1.1.1.1/cdn-cgi/trace | grep ^ip='ocserv-server · MIT License · Built on ocserv + s6-overlay