Skip to content

fix(restctl): auto re-initiate dropped connections, default unique to replace - #38

Open
m4ci3k2 wants to merge 1 commit into
dialohq:masterfrom
m4ci3k2:fix/restctl-auto-recover-and-unique-replace
Open

m4ci3k2 wants to merge 1 commit into
dialohq:masterfrom
m4ci3k2:fix/restctl-auto-recover-and-unique-replace

Conversation

@m4ci3k2

@m4ci3k2 m4ci3k2 commented Sep 17, 2026

Copy link
Copy Markdown

Two related bugs found together after a production tunnel silently died

  1. restctl's monitorConnections goroutine already polls swanctl every 5s
    and correctly detects a missing connection/child via checkStatus(), but
    downgradeConnectionStatus only used that to mark the IPSecConnection's
    status Down/Degraded - nothing ever called the existing RestartConnection/
    RestartConnectionChild swanctl-initiate logic to actually recover it.
    Add attemptSelfHeal, invoked from downgradeConnectionStatus for every
    detected failure, with a 30s per-connection/child cooldown so a genuinely
    unreachable peer doesn't get hammered with a re-initiate every 5s tick.

  2. translate() set no default for the IKE-level unique option, so it
    was left unset and charon's own default applies. Reproduced live: a
    re-initiate racing the connection's own start_action/trap policy (e.g.
    during a reauth, or a self-heal initiate racing an in-flight
    acquire) succeeds on both paths, leaving two IKE_SA/CHILD_SA pairs
    sharing one reqid. One of the pair goes untracked by future
    rekey/reauth scheduling and can be silently lost without ever logging
    a delete - this is almost certainly what actually happened to the
    tunnel that started this investigation. Default unique to "replace"
    so charon tears down the older SA to the same identity instead of
    letting both live, which is what the auto re-initiate in (1) now
    depends on being safe to call.

… replace

Two related bugs found together after a production tunnel silently died
and stayed down for 3 days with nothing noticing:

1. restctl's monitorConnections goroutine already polls swanctl every 5s
   and correctly detects a missing connection/child via checkStatus(), but
   downgradeConnectionStatus only used that to mark the IPSecConnection's
   status Down/Degraded - nothing ever called the existing RestartConnection/
   RestartConnectionChild swanctl-initiate logic to actually recover it.
   The self-healing plumbing was half-built: detection worked, recovery
   never fired automatically. Add attemptSelfHeal, invoked from
   downgradeConnectionStatus for every detected failure, with a 30s
   per-connection/child cooldown so a genuinely unreachable peer doesn't
   get hammered with a re-initiate every 5s tick.

2. translate() set no default for the IKE-level `unique` option, so it
   was left unset and charon's own default applies. Reproduced live: a
   re-initiate racing the connection's own start_action/trap policy (e.g.
   during a reauth, or a self-heal initiate racing an in-flight
   acquire) succeeds on both paths, leaving two IKE_SA/CHILD_SA pairs
   sharing one reqid. One of the pair goes untracked by future
   rekey/reauth scheduling and can be silently lost without ever logging
   a delete - this is almost certainly what actually happened to the
   tunnel that started this investigation. Default `unique` to "replace"
   so charon tears down the older SA to the same identity instead of
   letting both live, which is what the auto re-initiate in (1) now
   depends on being safe to call.

Verified: go build/vet/gofmt clean. (1) is inferred from reading the
existing (unused) restart endpoints and the monitor/detect loop; (2) was
reproduced live - manually re-initiating a connection that still had its
start_action trap routed produced exactly this duplicate-SA state,
confirmed via swanctl --list-sas showing two ESTABLISHED IKE_SAs to the
same peer, one carrying zero traffic.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants