Picture a Linux host that terminates a VPN and also runs a container on a macvlan network. The LAN is 192.168.60.0/24, the container holds 192.168.60.210, and VPN clients arrive from 10.95.0.0/24. The client reaches the service, the reply never comes back, and every firewall rule on the host looks correct. Two separate constraints produce that symptom, and fixing one of them leaves the other in place.

Two constraints, one symptom

The first constraint is isolation: Docker's macvlan driver does not let a container talk to its own host through the parent interface. Traffic between the host stack and the container has no path, no matter how the routes on the host are written.

The second constraint is the return path. The container's default gateway is usually the LAN router, so a reply addressed to a VPN client leaves through that router. If the router has no route back to the VPN pool, the reply is dropped somewhere in the middle.

Those two failures look identical from the client: a request that arrives and an answer that never does. They need different repairs, and a broad masquerade rule can hide which one is still broken. That is why the first test is a capture on the container side rather than another firewall change. If the request never appears there, the isolation constraint is the one in play.

Build the host-side shim, narrowly

A second macvlan interface on the host, attached to the same parent in bridge mode, gives the host stack its own identity on that LAN segment. Reserve an address outside the DHCP pool and outside the container range for it, and route only the container through it.

ip link add macvlan-vpn link eth0 type macvlan mode bridge
ip address add 192.168.60.250/32 dev macvlan-vpn
ip link set macvlan-vpn up
ip route add 192.168.60.210/32 dev macvlan-vpn src 192.168.60.250

The first command creates the interface in bridge mode, which is the documented default for this driver and the mode you want for host-container traffic. The second assigns a /32 so the address does not collide with the LAN prefix, and the fourth sends only the container's address through the shim.

Do not redirect the whole LAN through the new interface. The ordinary connected route stays as it is, and the host route is deliberately narrow so that one container gains a path without the rest of the segment changing behaviour. A default route pointing at the shim would be a different design with different consequences. Keep it specific and the blast radius stays small.

These commands change networking on a live host. On an appliance or a managed platform, use that platform's supported configuration mechanism, because an interactive root-shell change may not survive an upgrade.

Decide how the source is preserved

With the shim in place, the container can reach the host, and the question becomes what source address the reply carries. Preserving the original VPN source is the cleaner model: the container sees the real client address, and the service's logs and access rules can use it.

For that to work, the container's network configuration must send traffic for 10.95.0.0/24 to 192.168.60.250 as its gateway for this path. That is a supported route or gateway setting inside the container's own configuration, not a host-side trick. Confirm it from inside the container rather than assuming the setting took effect. A default gateway that still points at the LAN router will keep sending replies into the same hole.

If the approved design hides the VPN sources instead, scope the translation narrowly: the real VPN pool, the shim as the outgoing interface, and the specific container and service.

iptables -t nat -A POSTROUTING -s 10.95.0.0/24 -o macvlan-vpn -j SNAT --to-source 192.168.60.250

Match the shim as the egress interface, not the tunnel. A rule that names the tunnel as the outgoing interface will not match a packet headed toward the container, which is the mistake this kind of setup usually contains. Keep the rule as narrow as the sentence above and review it when the pool changes.

Verify from the client, not the host

A request from the host itself, bound to the shim address, proves that the shim can reach the container. It does not prove that the container can return traffic to a preserved 10.95.0.x source, because the source address in that test is the shim's own.

curl -sS --interface 192.168.60.250 -o /dev/null -w '%{http_code}\n' http://192.168.60.210:8080/health
sudo tcpdump -ni macvlan-vpn host 192.168.60.210

The first command tests the host-to-container leg, and the second shows what actually crosses the shim while a real VPN client makes the request. Look at the source address on the way in and whether a reply appears with the same flow.

Forwarding has to be enabled on the host, and the forward path needs an allowance for the VPN pool to the shim on the service port plus established replies. A host INPUT rule does not authorize forwarded traffic, which is a common reason the packet is dropped after routing looks correct.

Run the real application with real credentials, then check that a second, unrelated service on the same container still refuses you. If the only way to make it work was to open every forwarding rule, the design is not finished. Note which source address the service recorded as well, because that is what its own access rules will use from now on.

Allakkat attuumassuteqartut