From c058f1e7744167d140ebc888cbd38c4003de57f1 Mon Sep 17 00:00:00 2001 From: Luan Camara Date: Mon, 15 Jun 2026 09:54:30 -0300 Subject: [PATCH] tun/mac: make server bypass route idempotent to avoid orphaned route On macOS the server bypass host route (remote /32 via the physical default gateway, installed when redirect-gateway is active) is added with a plain "route add". If a previous session terminated uncleanly (sleep, network change or process crash) the teardown ActionList never runs and a stale copy of this route survives in the routing table. On the next connection the "route add" fails because the route already exists. Command::execute() detects "File exists" and throws; ActionList:: execute() records the route's mark as failed and remove_marked() then drops the *paired delete* from the teardown list (tunsetup.hpp: "since we should not undo failed actions, remove them"). The route is therefore never cleaned up, and because the stale entry still points at the previous network's gateway, once that gateway is no longer on-link every datagram to the server fails with EADDRNOTAVAIL: UDP send exception: send: Can't assign requested address The client then loops RECONNECTING/RESOLVE until it times out, while the server and all other users are unaffected. Make the bypass route installation idempotent by issuing a best-effort "route delete" immediately before the "route add". Deleting a non-existent route is harmless ("not in table", non-fatal), while a stale orphan is removed so the add always installs a fresh route bound to the current gateway and stays managed for teardown. This mirrors the REPLACE semantics already used by the Linux sitnl backend on EEXIST. Signed-off-by: Luan Camara --- openvpn/tun/mac/client/tunsetup.hpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/openvpn/tun/mac/client/tunsetup.hpp b/openvpn/tun/mac/client/tunsetup.hpp index dfe34d6e6..b14e6e346 100644 --- a/openvpn/tun/mac/client/tunsetup.hpp +++ b/openvpn/tun/mac/client/tunsetup.hpp @@ -414,6 +414,19 @@ class Setup : public TunBuilderSetup::Base { Action::Ptr c, d; add_del_route(pull.remote_address.address, 32, gw4.gateway_addr_str(), gw4.iface(), 0, c, d); + // Make the server bypass route idempotent. This host route must + // point at the *current* physical default gateway. If a previous + // session ended uncleanly (sleep, network change or crash) a stale + // copy of this route can survive with an outdated gateway. A plain + // "route add" then fails with "File exists"; execute() flags the add + // as failed and remove_marked() drops the paired delete from the + // teardown list, so the stale route is never cleaned up. Once the old + // gateway is no longer on-link it produces a permanent + // "UDP send: Can't assign requested address" and the client can only + // time out. Issue a best-effort delete first (a no-op "not in table" + // when absent, non-fatal) so the add always installs a fresh route + // bound to the current gateway and stays managed for teardown. + create.add(d); // pre-delete any stale/orphaned copy, then add create.add(c); destroy.add(d); }