Two OpenVPN daemons can both report a successful tunnel while the gateway sends every client down the same one. Nothing in the handshake decides which tunnel a client uses; the kernel decides, one packet at a time, by walking the policy rules and then the table each rule names. Once you look at it that way, the configuration stops being about OpenVPN options and becomes a routing matrix with two owners.

The kernel decides, not the daemon

A forwarded packet arrives on the LAN interface and the routing code evaluates the rule list in priority order. The first rule whose selector matches names a table, and that table's routes choose the output device. If the table has no matching route, the lookup continues to the next rule, which is the detail that makes a half-finished setup fail quietly rather than loudly.

The tunnel process has no say in that walk. It creates an interface and can install routes, but whether a particular client's packet enters that interface depends entirely on the table it was sent to. Two tunnels with identical imported routes therefore compete, and the last writer wins for everyone. That is why a setup can look balanced in the configuration files and still send all clients through one daemon.

Give each instance its own resource set

Start with names, because reused names are how the two instances end up sharing state by accident. Assign distinct tunnel devices, distinct configuration files, distinct credentials, and distinct service units. Record each instance's outer endpoint and tunnel address in the same table as the policy matrix, so a later change has one place to look.

dev tun-office
route-nopull
pull-filter ignore "redirect-gateway"

dev tun-lab
route-nopull

route-nopull keeps a server's pushed routes out of the local table, which is what you want when the policy tables are supposed to be explicit. Whether a given server also pushes a redirect-gateway option, and whether your client version supports the filter form, is worth checking against the manual rather than assuming.

Write the matrix: source, table, device

For each client group, add a rule that selects a table by source address, then put the intended path in that table:

ip rule add from 10.8.1.0/24 lookup 100 priority 1000
ip rule add from 10.9.1.0/24 lookup 200 priority 1000
ip route add default dev tun-office table 100
ip route add default dev tun-lab table 200

The table needs the destination routes the group should have, plus whatever local or management exceptions keep the gateway usable. A table containing only a default is a full-tunnel policy for that group, which may be exactly right, but it should be a decision rather than an omission.

The tunnel's own transport is not in a client group

Each daemon's encrypted packets originate on the gateway itself, addressed to the outer peer. A rule that matches only a client source prefix leaves those packets alone, which is why source-scoped rules are safer than rules that match everything. A rule with no selector can capture the tunnel's own transport and send it back through a tunnel, and the connection then fails in a way that looks like a provider problem.

Keep the local management session and the underlay path explicitly out of the policy tables. Write those exceptions down next to the matrix; they are part of the design, not a workaround to be rediscovered during an incident.

Selecting a path does not permit it

Forwarding permission is a second decision made later in the path. A firewall rule that accepts packets leaving through the second tunnel does not make the routing code choose it, and a correct table does not grant permission to forward. Both have to exist, and a test that only proves one of them will mislead you about the other. When one group works and the other does not, check these two steps separately instead of rewriting the tables.

Source translation is a third decision, and it should be scoped to the source group and the tunnel device that actually carries it. Add a dedicated chain for the two instances rather than flushing the postrouting chain, because unrelated services may own entries there and a flush takes them out silently.

What happens when one tunnel drops

Decide the failure behaviour in advance. When a tunnel table loses its route, the lookup falls through to the next rule, and the group's traffic continues through the other tunnel or the ordinary uplink without any error. If that is not acceptable, add a terminating rule at a lower priority number that returns an unreachable result for that source, so the loss is visible instead of being absorbed.

Send a fresh request from each source group and record four facts: the table selected, the output device, the translated source, and whether the reply came back through the same path. Then bring one tunnel down and repeat. Two TUN devices in an interface listing prove that both daemons started; they say nothing about which one a given client used. The second run, with one tunnel gone, is the one that shows whether your fallback rule does what you intended.

Pennadoù liammet