[GH-ISSUE #2176] VPN doesn't reconnect after sleep #967

Open
opened 2026-10-13 20:31:55 +00:00 by bsakaguchi · 14 comments
Owner

Originally created by @MoonNight131 on GitHub (Jan 30, 2026).
Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2176

Describe the bug
VPN doesn't reconnect after sleep or network connections reconnect

To Reproduce
Steps to reproduce the behavior:

  1. Connect to VPN (AmneziaWG 2)
  2. Put OS to sleep (without disconnecting VPN)
  3. Wake up
  4. VPN doesn't reconnected and connection doesn't work

Expected behavior
Connection reconnected after sleep.

Log files

[2026-01-30 21:18:26.626Z] [INFO] Amnezia  : Logging has enabled on AmneziaVPN version 4.8.12.9 f737928
[2026-01-30 21:18:26.626Z] [INFO] Amnezia  : CachyOS (x86_64)
[2026-01-30 21:18:43.093Z] [DEBUG] Amnezia  : Network change
[2026-01-30 21:18:43.093Z] [DEBUG] Amnezia  : VPN was connected before network change: false
[2026-01-30 21:18:43.093Z] [DEBUG] Amnezia  : VPN was not connected before sleep/network change, skipping reconnection

Desktop:

  • CachyOS, Linux 6.18.6, KDE Plasma 6.5.5
Originally created by @MoonNight131 on GitHub (Jan 30, 2026). Original GitHub issue: https://github.com/amnezia-vpn/amnezia-client/issues/2176 **Describe the bug** VPN doesn't reconnect after sleep or network connections reconnect **To Reproduce** Steps to reproduce the behavior: 1. Connect to VPN (AmneziaWG 2) 2. Put OS to sleep (without disconnecting VPN) 3. Wake up 4. VPN doesn't reconnected and connection doesn't work **Expected behavior** Connection reconnected after sleep. **Log files** ```[2026-01-30 21:18:26.616Z] [DEBUG] Amnezia : Logger::setServiceLogsEnabled(): Logs transitioned to be enabled [2026-01-30 21:18:26.626Z] [INFO] Amnezia : Logging has enabled on AmneziaVPN version 4.8.12.9 f737928 [2026-01-30 21:18:26.626Z] [INFO] Amnezia : CachyOS (x86_64) [2026-01-30 21:18:43.093Z] [DEBUG] Amnezia : Network change [2026-01-30 21:18:43.093Z] [DEBUG] Amnezia : VPN was connected before network change: false [2026-01-30 21:18:43.093Z] [DEBUG] Amnezia : VPN was not connected before sleep/network change, skipping reconnection ``` **Desktop:** - CachyOS, Linux 6.18.6, KDE Plasma 6.5.5
Author
Owner

@MoonNight131 commented on GitHub (Jan 30, 2026):

I saw https://github.com/amnezia-vpn/amnezia-client/pull/1894.
But looks like incorrect vpn status after sleep and wake up so skip reconnect.

//amnezia-client/client/vpnconnection.cpp

m_networkChangeHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::networkChange,
            this, [this]() {
                qDebug() << "Network change";
                // Track VPN state before network change (including sleep/wake)
                m_wasConnectedBeforeSleep = isConnected();
                qDebug() << "VPN was connected before network change:" << m_wasConnectedBeforeSleep;
                this->restartConnection();
            });


void VpnConnection::restartConnection()
{
    // Only reconnect if VPN was connected before sleep/network change
    if (!m_wasConnectedBeforeSleep) {
        qDebug() << "VPN was not connected before sleep/network change, skipping reconnection";
        return;
    }
    
    qDebug() << "VPN was connected before sleep/network change, attempting reconnection";
    this->disconnectFromVpn();
#ifdef Q_OS_LINUX
    QThread::msleep(5000);
#endif
    this->connectToVpn(m_serverIndex, m_serverCredentials, m_dockerContainer, m_vpnConfiguration);
    
    // Reset the flag after reconnection attempt
    m_wasConnectedBeforeSleep = false;
}

isConnected returning false but must be true.

<!-- gh-comment-id:3825967167 --> @MoonNight131 commented on GitHub (Jan 30, 2026): I saw https://github.com/amnezia-vpn/amnezia-client/pull/1894. But looks like incorrect vpn status after sleep and wake up so skip reconnect. ```cpp //amnezia-client/client/vpnconnection.cpp m_networkChangeHandle = connect(IpcClient::Interface().data(), &IpcInterfaceReplica::networkChange, this, [this]() { qDebug() << "Network change"; // Track VPN state before network change (including sleep/wake) m_wasConnectedBeforeSleep = isConnected(); qDebug() << "VPN was connected before network change:" << m_wasConnectedBeforeSleep; this->restartConnection(); }); void VpnConnection::restartConnection() { // Only reconnect if VPN was connected before sleep/network change if (!m_wasConnectedBeforeSleep) { qDebug() << "VPN was not connected before sleep/network change, skipping reconnection"; return; } qDebug() << "VPN was connected before sleep/network change, attempting reconnection"; this->disconnectFromVpn(); #ifdef Q_OS_LINUX QThread::msleep(5000); #endif this->connectToVpn(m_serverIndex, m_serverCredentials, m_dockerContainer, m_vpnConfiguration); // Reset the flag after reconnection attempt m_wasConnectedBeforeSleep = false; } ``` `isConnected` returning false but must be true.
Author
Owner

@tollfree commented on GitHub (Feb 1, 2026):

Same issue on macOS 26.2

<!-- gh-comment-id:3830087057 --> @tollfree commented on GitHub (Feb 1, 2026): Same issue on macOS 26.2
Author
Owner

@Aptahar commented on GitHub (Feb 14, 2026):

Same on Ubuntu 25.10 and version 4.8.12.9.
Status says it connected, but no Internet at all until disconnect.

<!-- gh-comment-id:3901594840 --> @Aptahar commented on GitHub (Feb 14, 2026): Same on Ubuntu 25.10 and version 4.8.12.9. Status says it connected, but no Internet at all until disconnect.
Author
Owner

@stiig commented on GitHub (Feb 19, 2026):

same issue in MacOS 26.3
client 4.8.12.9

<!-- gh-comment-id:3925099369 --> @stiig commented on GitHub (Feb 19, 2026): same issue in MacOS 26.3 client 4.8.12.9
Author
Owner

@gofRhythm commented on GitHub (Feb 24, 2026):

Same on android 16
client 4.8.12.9

<!-- gh-comment-id:3949907101 --> @gofRhythm commented on GitHub (Feb 24, 2026): Same on android 16 client 4.8.12.9
Author
Owner

@h4tr3d commented on GitHub (Feb 27, 2026):

Is it possible add external CLI options to notify VPN disconnect and connect? It is possible to use as WA via PM hooks to force VPN reconnections.

<!-- gh-comment-id:3970340473 --> @h4tr3d commented on GitHub (Feb 27, 2026): Is it possible add external CLI options to notify VPN disconnect and connect? It is possible to use as WA via PM hooks to force VPN reconnections.
Author
Owner

@h4tr3d commented on GitHub (Mar 2, 2026):

Sounds like #1000 was not fixed.

<!-- gh-comment-id:3981556713 --> @h4tr3d commented on GitHub (Mar 2, 2026): Sounds like #1000 was not fixed.
Author
Owner

@Skrayvee commented on GitHub (Apr 10, 2026):

macOS 26.4
Issue persists

<!-- gh-comment-id:4222499902 --> @Skrayvee commented on GitHub (Apr 10, 2026): macOS 26.4 Issue persists
Author
Owner

@Aptahar commented on GitHub (Apr 10, 2026):

macOS 26.4 Issue persists

On Ubuntu 25.10 too

<!-- gh-comment-id:4223846751 --> @Aptahar commented on GitHub (Apr 10, 2026): > macOS 26.4 Issue persists On Ubuntu 25.10 too
Author
Owner

@StacySiz commented on GitHub (Apr 13, 2026):

same issue

Amnezia kills internet connection after Mac returns after sleep

<!-- gh-comment-id:4235180736 --> @StacySiz commented on GitHub (Apr 13, 2026): same issue Amnezia kills internet connection after Mac returns after sleep
Author
Owner

@pixelcat-gh commented on GitHub (Apr 20, 2026):

Same issue, Debian

<!-- gh-comment-id:4284142572 --> @pixelcat-gh commented on GitHub (Apr 20, 2026): Same issue, Debian
Author
Owner

@shellgame323 commented on GitHub (Apr 28, 2026):

Same issue on Fedora 43. I thought it would reconnect while keeping the kill switch active (like protonvpn) but I have to manually turn it off and on again.

Edit: seems to be an issue on my end, because now it's working.

<!-- gh-comment-id:4335755132 --> @shellgame323 commented on GitHub (Apr 28, 2026): Same issue on Fedora 43. I thought it would reconnect while keeping the kill switch active (like protonvpn) but I have to manually turn it off and on again. Edit: seems to be an issue on my end, because now it's working.
Author
Owner

@manic-nuked commented on GitHub (Aug 2, 2026):

Same issue on a completely fresh setup, so this isn't specific to old configs or upgraded installs.

  • OS: macOS Tahoe 26.5.2 (25F84)
  • AmneziaVPN app version - Installed on post date.
  • Server: self-hosted, DigitalOcean droplet, Ubuntu, freshly created.

For comparison Outline VPN no reconnect issues.

<!-- gh-comment-id:5158774337 --> @manic-nuked commented on GitHub (Aug 2, 2026): Same issue on a completely fresh setup, so this isn't specific to old configs or upgraded installs. - OS: macOS Tahoe 26.5.2 (25F84) - AmneziaVPN app version - Installed on post date. - Server: self-hosted, DigitalOcean droplet, Ubuntu, freshly created. For comparison Outline VPN no reconnect issues.
Author
Owner

@shidoh commented on GitHub (Sep 8, 2026):

macOS 26.6.2 (25G83), AmneziaWG.app 3.1.4 (16). Not sleep-specific: any link flap on the physical interface does it. Repro: pull the Ethernet cable for 10 s. Traced with log stream --level debug, the extension's own lines are debug-only and never reach log show:

00:47:39 configd            en7 link INACTIVE
00:47:39 nesessionmanager   manager set delegate interface name to (null)
00:47:39 kernel             utun4: is no longer delegating en7
00:47:49 configd            en7 link ACTIVE, DHCP ok, same IP
00:47:49 nesessionmanager   handleChangeEventForRankedInterfaces - stopped 0 status connected   (delegate not restored)
00:47:49 NetworkExtension   Network change detected with unsatisfied route and interface order [utun4, utun4, en7]
00:47:49 NetworkExtension   UDP bind has been updated
00:47:49..00:49:16          Failed to send handshake initiation: ... sendmsg: no route to host
00:49:18 nesessionmanager   Received a stop command from AmneziaWG      (manual disconnect)
00:49:18 nesessionmanager   manager set delegate interface name to en7
00:49:18 NetworkExtension   Network change detected with satisfied route [en7, en7]

nesessionmanager clears the delegate interface when the link drops and sets it again only on a Stopping/Starting transition, never while the session is Running. Until then every send fails with EHOSTUNREACH. The extension does get the path update and rebinds (wgBumpSockets), but an unbound socket without a delegate has no route either. Adapter code is unchanged from upstream wireguard-apple since 2023, so this looks like a macOS 26 regression.

/usr/bin/log show --last 1d --predicate 'process == "kernel" AND eventMessage CONTAINS "delegating"'
/usr/bin/log show --last 1d --predicate 'process CONTAINS "WireGuard" AND eventMessage CONTAINS "sendmsg"'

"no longer delegating" with no "is now delegating" after it is one dead tunnel. The second query gave 1989 lines over a 90 s repro and returns nothing while healthy, so it is usable as a signal: those are error level, unlike the debug-only handshake retries.

Client-side fix, untested: in didReceivePathUpdate on macOS, when the path is unsatisfied but a physical interface is still in availableInterfaces, re-apply setTunnelNetworkSettings, then bump sockets.

Workaround: scutil --nc stop/start <name> restores the delegate, no root needed. LaunchAgent every 3 s, ping through the tunnel, then the sendmsg query decides whether a restart helps. Skips dark wakes. Verified with sudo ifconfig en7 down; sleep 10; sudo ifconfig en7 up: tunnel back 7-8 s after the link returns.

~/bin/awg-watchdog.sh:

#!/bin/zsh

FAILS_NEEDED=${AWG_WD_FAILS:-2}     # consecutive ping failures before we consult the log
RECHECK_EVERY=12                    # re-consult the log at most every 12 failures (~60 s)
LAST_RESORT=60                      # failures (~5 min) after which we restart unconfirmed
GRACE=20                            # seconds after a (re)connect during which we skip checks
COOLDOWN=30                         # seconds after a restart during which we skip checks
MAX_PER_HOUR=6
PING_TIMEOUT=1                      # seconds; a reply takes ~30 ms, a miss costs ~1 s
LOG_WINDOW=90s                      # how far back to look for sendmsg errors
STATE=~/Library/Application\ Support/awg-watchdog
LOG=~/Library/Logs/awg-watchdog.log
mkdir -p "$STATE"
now=$(date +%s)

# Dark wake: the machine is briefly up for maintenance (~1 min every 5) and sleeps
# again. The link flaps on every one, so restarting there is pointless and burns
# the restart budget the real wake needs. IOPMUserTriggeredFullWake cannot detect
# this - it stayed "Yes" through 40+ maintenance wakes on 26.6.2; pmset's own log
# can. It costs ~0.7 s, so it is read once, lazily, on the restart path only.
# wake_ts is the last full wake: restarts before it do not count against the quota.
darkwake=-1; wake_ts=0
wakestate() {
  (( darkwake >= 0 )) && return
  local pm=$(pmset -g log 2>/dev/null)
  local last=$(print -r -- "$pm" | awk -F'\t' '$2 ~ /^(Dark)?Wake from/ {l=$2} END{print l}')
  [[ $last == DarkWake* ]] && darkwake=1 || darkwake=0
  local w=$(print -r -- "$pm" | awk -F'\t' '$2 ~ /^Wake from/ {print $1}' | tail -1 | awk '{print $1, $2}')
  wake_ts=$(date -j -f '%Y-%m-%d %H:%M:%S' "$w" +%s 2>/dev/null || echo 0)
}

log() { print "$(date '+%F %T') $*" >> "$LOG"; logger -t awg-watchdog "$*"; }
reset() { print 0 > "$STATE/$1.fails"; }

connected=("${(@f)$(scutil --nc list | grep '\[VPN:org.amnezia.awg\]' | grep '(Connected)' | sed -E 's/.*"([^"]+)".*/\1/')}")
[[ -z "$connected" ]] && exit 0

for name in $connected; do
  st=$(scutil --nc status "$name")
  utun=$(print -r -- "$st" | awk '/^    InterfaceName : utun/{print $3; exit}')
  # server endpoint and physical interface both live in ExcludedRoutes
  excl=$(print -r -- "$st" | awk '/ExcludedRoutes/{f=1} f&&/DestinationAddress/{d=$3} f&&/InterfaceName/{print d, $3; exit}')
  endpoint=${excl%% *}; phy=${excl##* }
  target=${AWG_WD_TARGET:-$(print -r -- "$st" | awk '/DNSServers/,/\}/' | awk '/^ +[0-9]+ : [0-9.]+$/{print $3; exit}')}
  target=${target:-1.1.1.1}
  changed=$(print -r -- "$st" | awk -F' : ' '/LastStatusChangeTime/{print $2}')
  changed_ts=$(date -j -f '%m/%d/%Y %H:%M:%S' "$changed" +%s 2>/dev/null || echo 0)

  [[ -z "$utun" ]] && { reset "$name"; continue; }
  (( now - changed_ts < GRACE )) && { reset "$name"; continue; }
  phy_up=1
  if [[ -n "$phy" && "$phy" != "$endpoint" ]]; then
    ifconfig "$phy" 2>/dev/null | grep -q 'status: active' || phy_up=0
    ifconfig "$phy" | grep -q 'inet ' || phy_up=0
  fi
  if (( ! phy_up )); then
    print 1 > "$STATE/$name.flap"   # remember it, the delegate is being lost right now
    reset "$name"; continue
  fi
  # the marker survives until traffic passes again, so it is still armed on the
  # real wake no matter how many dark wakes happened in between
  flap=0; [[ -f "$STATE/$name.flap" ]] && flap=1
  last=$(cat "$STATE/$name.last-restart" 2>/dev/null || echo 0)
  (( now - last < COOLDOWN )) && continue

  # stage 1: does anything pass through the tunnel
  if ping -c1 -t$PING_TIMEOUT -b "$utun" "$target" >/dev/null 2>&1; then
    rm -f "$STATE/$name.flap"; reset "$name"; continue
  fi
  fails=$(( $(cat "$STATE/$name.fails" 2>/dev/null || echo 0) + 1 ))
  print $fails > "$STATE/$name.fails"
  # right after a flap one failure is enough: stage 2 below is precise anyway
  if (( ! flap )); then
    (( fails < FAILS_NEEDED )) && continue
    (( (fails - FAILS_NEEDED) % RECHECK_EVERY != 0 )) && continue
  fi

  # stage 2: is the local send path actually broken
  errs=$(/usr/bin/log show --last $LOG_WINDOW --predicate '(process CONTAINS "WireGuard" OR process CONTAINS "Amnezia") AND eventMessage CONTAINS "sendmsg"' --style compact 2>/dev/null | grep -c "${endpoint:-sendmsg}")
  if (( errs == 0 )) && (( fails < LAST_RESORT )); then
    (( fails <= FAILS_NEEDED )) && log "$name: $target via $utun unreachable, but no sendmsg errors for $endpoint - far side, not restarting"
    continue
  fi
  (( errs == 0 )) && reason="no sendmsg errors, last resort after $fails checks" || reason="$errs sendmsg errors for $endpoint"
  wakestate
  if (( darkwake )); then
    (( fails <= FAILS_NEEDED )) && log "$name: $reason, but this is a dark wake - leaving it to the next real wake"
    continue
  fi

  touch "$STATE/$name.restarts"
  awk -v t=$((now-3600)) -v w=$wake_ts '$1 > t && $1 > w' "$STATE/$name.restarts" > "$STATE/$name.restarts.tmp"
  mv "$STATE/$name.restarts.tmp" "$STATE/$name.restarts"
  if (( $(wc -l < "$STATE/$name.restarts") >= MAX_PER_HOUR )); then
    log "$name: would restart ($reason) but $MAX_PER_HOUR restarts/hour reached, skipping"
    continue
  fi

  log "$name: Connected but $target via $utun unreachable x$fails, $reason - restarting"
  scutil --nc stop "$name"; sleep 2; scutil --nc start "$name"
  print $now > "$STATE/$name.last-restart"; print $now >> "$STATE/$name.restarts"; reset "$name"
  rm -f "$STATE/$name.flap"
  (( ! darkwake )) && osascript -e "display notification \"$name restarted\" with title \"AmneziaWG watchdog\"" >/dev/null 2>&1
done

~/Library/LaunchAgents/local.awg-watchdog.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
  <key>Label</key><string>local.awg-watchdog</string>
  <key>ProgramArguments</key><array><string>/Users/YOU/bin/awg-watchdog.sh</string></array>
  <key>StartInterval</key><integer>3</integer>
  <key>RunAtLoad</key><true/>
  <key>ProcessType</key><string>Background</string>
</dict></plist>
chmod +x ~/bin/awg-watchdog.sh
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.awg-watchdog.plist
launchctl bootout gui/$(id -u)/local.awg-watchdog   # to stop

Ping target is the tunnel's DNS server from scutil --nc status; set AWG_WD_TARGET if yours doesn't answer ICMP. Log in ~/Library/Logs/awg-watchdog.log.

<!-- gh-comment-id:5591178692 --> @shidoh commented on GitHub (Sep 8, 2026): macOS 26.6.2 (25G83), AmneziaWG.app 3.1.4 (16). Not sleep-specific: any link flap on the physical interface does it. Repro: pull the Ethernet cable for 10 s. Traced with `log stream --level debug`, the extension's own lines are debug-only and never reach `log show`: ``` 00:47:39 configd en7 link INACTIVE 00:47:39 nesessionmanager manager set delegate interface name to (null) 00:47:39 kernel utun4: is no longer delegating en7 00:47:49 configd en7 link ACTIVE, DHCP ok, same IP 00:47:49 nesessionmanager handleChangeEventForRankedInterfaces - stopped 0 status connected (delegate not restored) 00:47:49 NetworkExtension Network change detected with unsatisfied route and interface order [utun4, utun4, en7] 00:47:49 NetworkExtension UDP bind has been updated 00:47:49..00:49:16 Failed to send handshake initiation: ... sendmsg: no route to host 00:49:18 nesessionmanager Received a stop command from AmneziaWG (manual disconnect) 00:49:18 nesessionmanager manager set delegate interface name to en7 00:49:18 NetworkExtension Network change detected with satisfied route [en7, en7] ``` nesessionmanager clears the delegate interface when the link drops and sets it again only on a Stopping/Starting transition, never while the session is Running. Until then every send fails with EHOSTUNREACH. The extension does get the path update and rebinds (`wgBumpSockets`), but an unbound socket without a delegate has no route either. Adapter code is unchanged from upstream wireguard-apple since 2023, so this looks like a macOS 26 regression. ``` /usr/bin/log show --last 1d --predicate 'process == "kernel" AND eventMessage CONTAINS "delegating"' /usr/bin/log show --last 1d --predicate 'process CONTAINS "WireGuard" AND eventMessage CONTAINS "sendmsg"' ``` "no longer delegating" with no "is now delegating" after it is one dead tunnel. The second query gave 1989 lines over a 90 s repro and returns nothing while healthy, so it is usable as a signal: those are error level, unlike the debug-only handshake retries. Client-side fix, untested: in `didReceivePathUpdate` on macOS, when the path is unsatisfied but a physical interface is still in `availableInterfaces`, re-apply `setTunnelNetworkSettings`, then bump sockets. Workaround: `scutil --nc stop/start <name>` restores the delegate, no root needed. LaunchAgent every 3 s, ping through the tunnel, then the sendmsg query decides whether a restart helps. Skips dark wakes. Verified with `sudo ifconfig en7 down; sleep 10; sudo ifconfig en7 up`: tunnel back 7-8 s after the link returns. `~/bin/awg-watchdog.sh`: ```zsh #!/bin/zsh FAILS_NEEDED=${AWG_WD_FAILS:-2} # consecutive ping failures before we consult the log RECHECK_EVERY=12 # re-consult the log at most every 12 failures (~60 s) LAST_RESORT=60 # failures (~5 min) after which we restart unconfirmed GRACE=20 # seconds after a (re)connect during which we skip checks COOLDOWN=30 # seconds after a restart during which we skip checks MAX_PER_HOUR=6 PING_TIMEOUT=1 # seconds; a reply takes ~30 ms, a miss costs ~1 s LOG_WINDOW=90s # how far back to look for sendmsg errors STATE=~/Library/Application\ Support/awg-watchdog LOG=~/Library/Logs/awg-watchdog.log mkdir -p "$STATE" now=$(date +%s) # Dark wake: the machine is briefly up for maintenance (~1 min every 5) and sleeps # again. The link flaps on every one, so restarting there is pointless and burns # the restart budget the real wake needs. IOPMUserTriggeredFullWake cannot detect # this - it stayed "Yes" through 40+ maintenance wakes on 26.6.2; pmset's own log # can. It costs ~0.7 s, so it is read once, lazily, on the restart path only. # wake_ts is the last full wake: restarts before it do not count against the quota. darkwake=-1; wake_ts=0 wakestate() { (( darkwake >= 0 )) && return local pm=$(pmset -g log 2>/dev/null) local last=$(print -r -- "$pm" | awk -F'\t' '$2 ~ /^(Dark)?Wake from/ {l=$2} END{print l}') [[ $last == DarkWake* ]] && darkwake=1 || darkwake=0 local w=$(print -r -- "$pm" | awk -F'\t' '$2 ~ /^Wake from/ {print $1}' | tail -1 | awk '{print $1, $2}') wake_ts=$(date -j -f '%Y-%m-%d %H:%M:%S' "$w" +%s 2>/dev/null || echo 0) } log() { print "$(date '+%F %T') $*" >> "$LOG"; logger -t awg-watchdog "$*"; } reset() { print 0 > "$STATE/$1.fails"; } connected=("${(@f)$(scutil --nc list | grep '\[VPN:org.amnezia.awg\]' | grep '(Connected)' | sed -E 's/.*"([^"]+)".*/\1/')}") [[ -z "$connected" ]] && exit 0 for name in $connected; do st=$(scutil --nc status "$name") utun=$(print -r -- "$st" | awk '/^ InterfaceName : utun/{print $3; exit}') # server endpoint and physical interface both live in ExcludedRoutes excl=$(print -r -- "$st" | awk '/ExcludedRoutes/{f=1} f&&/DestinationAddress/{d=$3} f&&/InterfaceName/{print d, $3; exit}') endpoint=${excl%% *}; phy=${excl##* } target=${AWG_WD_TARGET:-$(print -r -- "$st" | awk '/DNSServers/,/\}/' | awk '/^ +[0-9]+ : [0-9.]+$/{print $3; exit}')} target=${target:-1.1.1.1} changed=$(print -r -- "$st" | awk -F' : ' '/LastStatusChangeTime/{print $2}') changed_ts=$(date -j -f '%m/%d/%Y %H:%M:%S' "$changed" +%s 2>/dev/null || echo 0) [[ -z "$utun" ]] && { reset "$name"; continue; } (( now - changed_ts < GRACE )) && { reset "$name"; continue; } phy_up=1 if [[ -n "$phy" && "$phy" != "$endpoint" ]]; then ifconfig "$phy" 2>/dev/null | grep -q 'status: active' || phy_up=0 ifconfig "$phy" | grep -q 'inet ' || phy_up=0 fi if (( ! phy_up )); then print 1 > "$STATE/$name.flap" # remember it, the delegate is being lost right now reset "$name"; continue fi # the marker survives until traffic passes again, so it is still armed on the # real wake no matter how many dark wakes happened in between flap=0; [[ -f "$STATE/$name.flap" ]] && flap=1 last=$(cat "$STATE/$name.last-restart" 2>/dev/null || echo 0) (( now - last < COOLDOWN )) && continue # stage 1: does anything pass through the tunnel if ping -c1 -t$PING_TIMEOUT -b "$utun" "$target" >/dev/null 2>&1; then rm -f "$STATE/$name.flap"; reset "$name"; continue fi fails=$(( $(cat "$STATE/$name.fails" 2>/dev/null || echo 0) + 1 )) print $fails > "$STATE/$name.fails" # right after a flap one failure is enough: stage 2 below is precise anyway if (( ! flap )); then (( fails < FAILS_NEEDED )) && continue (( (fails - FAILS_NEEDED) % RECHECK_EVERY != 0 )) && continue fi # stage 2: is the local send path actually broken errs=$(/usr/bin/log show --last $LOG_WINDOW --predicate '(process CONTAINS "WireGuard" OR process CONTAINS "Amnezia") AND eventMessage CONTAINS "sendmsg"' --style compact 2>/dev/null | grep -c "${endpoint:-sendmsg}") if (( errs == 0 )) && (( fails < LAST_RESORT )); then (( fails <= FAILS_NEEDED )) && log "$name: $target via $utun unreachable, but no sendmsg errors for $endpoint - far side, not restarting" continue fi (( errs == 0 )) && reason="no sendmsg errors, last resort after $fails checks" || reason="$errs sendmsg errors for $endpoint" wakestate if (( darkwake )); then (( fails <= FAILS_NEEDED )) && log "$name: $reason, but this is a dark wake - leaving it to the next real wake" continue fi touch "$STATE/$name.restarts" awk -v t=$((now-3600)) -v w=$wake_ts '$1 > t && $1 > w' "$STATE/$name.restarts" > "$STATE/$name.restarts.tmp" mv "$STATE/$name.restarts.tmp" "$STATE/$name.restarts" if (( $(wc -l < "$STATE/$name.restarts") >= MAX_PER_HOUR )); then log "$name: would restart ($reason) but $MAX_PER_HOUR restarts/hour reached, skipping" continue fi log "$name: Connected but $target via $utun unreachable x$fails, $reason - restarting" scutil --nc stop "$name"; sleep 2; scutil --nc start "$name" print $now > "$STATE/$name.last-restart"; print $now >> "$STATE/$name.restarts"; reset "$name" rm -f "$STATE/$name.flap" (( ! darkwake )) && osascript -e "display notification \"$name restarted\" with title \"AmneziaWG watchdog\"" >/dev/null 2>&1 done ``` `~/Library/LaunchAgents/local.awg-watchdog.plist`: ```xml <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"><dict> <key>Label</key><string>local.awg-watchdog</string> <key>ProgramArguments</key><array><string>/Users/YOU/bin/awg-watchdog.sh</string></array> <key>StartInterval</key><integer>3</integer> <key>RunAtLoad</key><true/> <key>ProcessType</key><string>Background</string> </dict></plist> ``` ``` chmod +x ~/bin/awg-watchdog.sh launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/local.awg-watchdog.plist launchctl bootout gui/$(id -u)/local.awg-watchdog # to stop ``` Ping target is the tunnel's DNS server from `scutil --nc status`; set `AWG_WD_TARGET` if yours doesn't answer ICMP. Log in `~/Library/Logs/awg-watchdog.log`.
Sign in to join this conversation.
No labels
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
amnezia-vpn/amnezia-client#967
No description provided.