From 7a1c2f08b81c8120170250e751c76120d8fa2b02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zolt=C3=A1n=20Papp?= Date: Thu, 9 Apr 2026 17:19:29 +0200 Subject: [PATCH] [client] Handle EEXIST when adding routes on macOS/BSD On macOS, routes from a previous session can survive across sleep/wake cycles even though cleanup reports success. When the new engine instance tries to add the same route, it fails with "file exists" (EEXIST), leaving the route untracked by the route manager while the OS still has it pointing at a stale interface. Handle this by detecting EEXIST on RTM_ADD, removing the stale route, and retrying. This matches how Linux silently handles existing routes via netlink. --- .../routemanager/systemops/systemops_unix.go | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/client/internal/routemanager/systemops/systemops_unix.go b/client/internal/routemanager/systemops/systemops_unix.go index 7089178fb..5cca0c8c8 100644 --- a/client/internal/routemanager/systemops/systemops_unix.go +++ b/client/internal/routemanager/systemops/systemops_unix.go @@ -105,7 +105,20 @@ func (r *SysOps) FlushMarkedRoutes() error { } func (r *SysOps) addToRouteTable(prefix netip.Prefix, nexthop Nexthop) error { - return r.routeSocket(unix.RTM_ADD, prefix, nexthop) + if err := r.routeSocket(unix.RTM_ADD, prefix, nexthop); err != nil { + if !errors.Is(err, unix.EEXIST) { + return err + } + + // Route already exists from a previous session that wasn't cleaned up properly + // (e.g. macOS sleep/wake). Remove the stale route and retry. + log.Infof("Route for %s already exists, replacing with new route", prefix) + if err := r.routeSocket(unix.RTM_DELETE, prefix, nexthop); err != nil { + log.Warnf("Failed to remove stale route for %s: %v", prefix, err) + } + return r.routeSocket(unix.RTM_ADD, prefix, nexthop) + } + return nil } func (r *SysOps) removeFromRouteTable(prefix netip.Prefix, nexthop Nexthop) error {