Your service works on the box, the tunnel is up, and the browser on your desk still times out. Nothing is broken in the usual sense: the application is listening inside a network namespace, and that stack is invisible to the LAN. Its loopback belongs to the namespace, so a client on another machine has no address that reaches the port. One deliberate hop, or one veth pair, fixes it without moving the rest of the host.

Why nothing answers on the LAN address

A namespace is a complete network stack: interfaces, routes, firewall rules and a private loopback. A process launched through ip netns exec sees that stack and nothing else, so the address it binds resolves inside the namespace alone. The host's own 127.0.0.1 is a different loopback entirely.

That is why curl on the host fails while the same command inside the namespace succeeds. The application is healthy; the address simply does not exist in the host's stack.

Publish one port with a socat hop

socat bridges the two stacks in user space, which is enough when a single TCP port has to cross. The host side listens on the LAN and, for each accepted connection, starts a second socat inside the namespace that connects to the app's loopback. The inner process reaches the application exactly as a local client would. Nothing about the namespace routing changes, so the app keeps using the tunnel as its only exit.

socat TCP-LISTEN:9100,reuseaddr,fork EXEC:'ip netns exec vpn socat STDIO tcp-connect:127.0.0.1:9100',nofork

The outer process needs root or the capability to enter the namespace. If the VPN drops, the app loses its path and the published port fails closed, which is the behaviour you want from a jail.

Use a veth pair when more than one port crosses

A veth pair is the right answer when several ports or protocols must cross, or when other hosts need to initiate connections. One end stays in the host, the other moves into the namespace, and a small /30 gives both sides a stable address. A host route then sends the namespace address over the pair.

ip link add veth0 type veth peer name veth1
ip link set veth1 netns vpn
ip addr add 10.77.0.1/30 dev veth0
ip link set veth0 up
ip netns exec vpn ip addr add 10.77.0.2/30 dev veth1
ip netns exec vpn ip link set veth1 up

Do not add a default route through the host unless you intend to break the jail. The tunnel should remain the exit, so only the published service address is reachable over the pair. Give the namespace its own resolver file as well, because ip netns exec reads /etc/netns/vpn/resolv.conf and without it name resolution inside the jail is empty.

Test from another machine, not the host

Run the test from a second machine, because the host can reach a local listener through paths that say nothing about the LAN. Check what the application actually bound inside the namespace first, then connect to the published address from outside. Binding to every address there does not widen the jail, because the wildcard still belongs to the namespace and the LAN cannot see it.

ip netns exec vpn ss -lntp

If the host connection works and the LAN connection does not, the missing piece is the hop or the route, not the bind address. Start OpenVPN inside the namespace rather than moving a live tun device into it, since moving the interface takes it down and can clear routes you then cannot repair from outside. Keep the recovery commands on the console, not inside the jail.

Zvinyorwa zvakafanana