fix(nordvpn-client): validate live utun persistence before dns pinning
This commit is contained in:
@@ -69,13 +69,18 @@ Current macOS backend:
|
|||||||
- NordLynx/WireGuard
|
- NordLynx/WireGuard
|
||||||
- `wireguard-go`
|
- `wireguard-go`
|
||||||
- `wireguard-tools`
|
- `wireguard-tools`
|
||||||
- NordVPN DNS in the generated WireGuard config:
|
- explicit macOS DNS management on eligible physical services:
|
||||||
- `103.86.96.100`
|
- `103.86.96.100`
|
||||||
- `103.86.99.100`
|
- `103.86.99.100`
|
||||||
|
|
||||||
Important behavior:
|
Important behavior:
|
||||||
|
|
||||||
- `NordVPN.app` may remain installed, but the automated backend does not reuse app login state.
|
- `NordVPN.app` may remain installed, but the automated backend does not reuse app login state.
|
||||||
|
- the generated WireGuard config intentionally stays free of `DNS = ...` so `wg-quick` does not rewrite every macOS network service behind the skill’s back.
|
||||||
|
- during `connect`, the skill first proves the tunnel is stable with a bounded persistence gate that reuses the allowed helper `probe` action and a verified public exit.
|
||||||
|
- during `connect`, the skill snapshots current DNS/search-domain settings on eligible physical services and then applies NordVPN DNS only after that stable gate and one last liveness check succeed.
|
||||||
|
- during `disconnect`, or after a failed/stale teardown, the skill restores the saved DNS/search-domain snapshot.
|
||||||
|
- if persistence or exit verification fails, the skill rolls back before DNS is pinned and resumes Tailscale if it stopped it.
|
||||||
- The skill automatically suspends Tailscale before connect if Tailscale is active.
|
- The skill automatically suspends Tailscale before connect if Tailscale is active.
|
||||||
- The skill resumes Tailscale after disconnect, or after a failed connect, if it stopped it.
|
- The skill resumes Tailscale after disconnect, or after a failed connect, if it stopped it.
|
||||||
- The Homebrew NordVPN app does not need to be uninstalled.
|
- The Homebrew NordVPN app does not need to be uninstalled.
|
||||||
@@ -144,6 +149,8 @@ Add this exact rule:
|
|||||||
stefano ALL=(root) NOPASSWD: /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh probe, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh up, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh down
|
stefano ALL=(root) NOPASSWD: /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh probe, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh up, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh down
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Do not add extra helper actions just for persistence checks unless you are also updating host sudoers. The current implementation intentionally rides the persistence check on `probe` so the existing `probe/up/down` rule remains sufficient.
|
||||||
|
|
||||||
If you run the repo copy directly instead of the installed OpenClaw skill, adjust the helper path accordingly.
|
If you run the repo copy directly instead of the installed OpenClaw skill, adjust the helper path accordingly.
|
||||||
|
|
||||||
## Common Flows
|
## Common Flows
|
||||||
@@ -188,7 +195,9 @@ Expected macOS behavior:
|
|||||||
- stop Tailscale if active
|
- stop Tailscale if active
|
||||||
- select a NordVPN server for the target
|
- select a NordVPN server for the target
|
||||||
- bring up the WireGuard tunnel
|
- bring up the WireGuard tunnel
|
||||||
|
- prove persistence of the live `utun*` runtime via the helper `probe` path
|
||||||
- verify the public exit location
|
- verify the public exit location
|
||||||
|
- run one final liveness check before applying NordVPN DNS
|
||||||
- return JSON describing the chosen server and final verified location
|
- return JSON describing the chosen server and final verified location
|
||||||
|
|
||||||
### Verify
|
### Verify
|
||||||
@@ -209,6 +218,7 @@ Expected macOS behavior:
|
|||||||
|
|
||||||
- attempt `wg-quick down` whenever there is active or residual NordVPN WireGuard state
|
- attempt `wg-quick down` whenever there is active or residual NordVPN WireGuard state
|
||||||
- remove stale local NordVPN state files after teardown
|
- remove stale local NordVPN state files after teardown
|
||||||
|
- restore automatic DNS when the saved DNS snapshot is obviously just NordVPN-pinned leftovers
|
||||||
- resume Tailscale if the skill had suspended it
|
- resume Tailscale if the skill had suspended it
|
||||||
|
|
||||||
## Output Model
|
## Output Model
|
||||||
|
|||||||
@@ -41,6 +41,11 @@ node scripts/nordvpn-client.js status --debug
|
|||||||
- use NordLynx/WireGuard through `wireguard-go` and `wireguard-tools`
|
- use NordLynx/WireGuard through `wireguard-go` and `wireguard-tools`
|
||||||
- `install` bootstraps them with Homebrew
|
- `install` bootstraps them with Homebrew
|
||||||
- `login` validates the token for the WireGuard backend
|
- `login` validates the token for the WireGuard backend
|
||||||
|
- the generated WireGuard config stays free of `DNS = ...`
|
||||||
|
- `connect` now requires a bounded persistence gate plus a verified exit before success is declared
|
||||||
|
- the skill snapshots and applies NordVPN DNS only to eligible physical services while connected
|
||||||
|
- NordVPN DNS is applied only after the tunnel remains up and the final liveness check still shows the requested exit
|
||||||
|
- `disconnect` restores the saved DNS/search-domain state even if the tunnel state is stale
|
||||||
- Tailscale is suspended before connect and resumed after disconnect or failed connect
|
- Tailscale is suspended before connect and resumed after disconnect or failed connect
|
||||||
- `NordVPN.app` may remain installed but is only the manual fallback
|
- `NordVPN.app` may remain installed but is only the manual fallback
|
||||||
|
|
||||||
@@ -75,6 +80,10 @@ Exact `visudo` rule for the installed OpenClaw skill:
|
|||||||
stefano ALL=(root) NOPASSWD: /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh probe, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh up, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh down
|
stefano ALL=(root) NOPASSWD: /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh probe, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh up, /Users/stefano/.openclaw/workspace/skills/nordvpn-client/scripts/nordvpn-wireguard-helper.sh down
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Operational note:
|
||||||
|
|
||||||
|
- the persistence gate reuses the already-allowed `probe` action to confirm the live `utun*` WireGuard runtime and does not require extra sudoers actions beyond `probe`, `up`, and `down`
|
||||||
|
|
||||||
## Agent Guidance
|
## Agent Guidance
|
||||||
|
|
||||||
- run `status` first when the machine state is unclear
|
- run `status` first when the machine state is unclear
|
||||||
@@ -83,6 +92,7 @@ stefano ALL=(root) NOPASSWD: /Users/stefano/.openclaw/workspace/skills/nordvpn-c
|
|||||||
- use `connect` before location-sensitive skills such as `web-automation`
|
- use `connect` before location-sensitive skills such as `web-automation`
|
||||||
- use `verify` after connect when you need an explicit location check
|
- use `verify` after connect when you need an explicit location check
|
||||||
- use `disconnect` after the follow-up task
|
- use `disconnect` after the follow-up task
|
||||||
|
- if `connect` fails its persistence or final verification gate, treat that as a safe rollback, not a partial success
|
||||||
|
|
||||||
## Output Rules
|
## Output Rules
|
||||||
|
|
||||||
@@ -98,6 +108,7 @@ stefano ALL=(root) NOPASSWD: /Users/stefano/.openclaw/workspace/skills/nordvpn-c
|
|||||||
- connect succeeds but final state looks inconsistent:
|
- connect succeeds but final state looks inconsistent:
|
||||||
- rely on the verified public IP/location first
|
- rely on the verified public IP/location first
|
||||||
- then inspect `status --debug`
|
- then inspect `status --debug`
|
||||||
|
- `verified: true` but `persistence.stable: false` should not happen anymore; if it does, the skill should roll back instead of pinning DNS
|
||||||
- disconnect should leave:
|
- disconnect should leave:
|
||||||
- normal public IP restored
|
- normal public IP restored
|
||||||
- no active WireGuard state
|
- no active WireGuard state
|
||||||
|
|||||||
@@ -781,6 +781,7 @@ function parseMacWireguardHelperStatus(output) {
|
|||||||
return {
|
return {
|
||||||
active: ["1", "true", "yes", "on"].includes(`${parsed.active || ""}`.toLowerCase()),
|
active: ["1", "true", "yes", "on"].includes(`${parsed.active || ""}`.toLowerCase()),
|
||||||
interfaceName: parsed.interfaceName || MAC_WG_INTERFACE,
|
interfaceName: parsed.interfaceName || MAC_WG_INTERFACE,
|
||||||
|
wireguardInterface: parsed.wireguardInterface || null,
|
||||||
configPath: parsed.configPath || null,
|
configPath: parsed.configPath || null,
|
||||||
raw: `${output || ""}`.trim(),
|
raw: `${output || ""}`.trim(),
|
||||||
};
|
};
|
||||||
@@ -788,7 +789,7 @@ function parseMacWireguardHelperStatus(output) {
|
|||||||
|
|
||||||
async function getMacWireguardHelperStatus(installProbe, options = {}) {
|
async function getMacWireguardHelperStatus(installProbe, options = {}) {
|
||||||
const runSudoWireguardFn = options.runSudoWireguard || runSudoWireguard;
|
const runSudoWireguardFn = options.runSudoWireguard || runSudoWireguard;
|
||||||
const result = await runSudoWireguardFn(installProbe, "status");
|
const result = await runSudoWireguardFn(installProbe, "probe");
|
||||||
const parsed = parseMacWireguardHelperStatus(result.stdout || result.stderr || "");
|
const parsed = parseMacWireguardHelperStatus(result.stdout || result.stderr || "");
|
||||||
return {
|
return {
|
||||||
...parsed,
|
...parsed,
|
||||||
@@ -1012,8 +1013,7 @@ async function probeMacWireguard() {
|
|||||||
const helperPath = fileExists(MAC_WG_HELPER_PATH) ? MAC_WG_HELPER_PATH : null;
|
const helperPath = fileExists(MAC_WG_HELPER_PATH) ? MAC_WG_HELPER_PATH : null;
|
||||||
const helperSecurity = inspectMacWireguardHelperSecurity(helperPath);
|
const helperSecurity = inspectMacWireguardHelperSecurity(helperPath);
|
||||||
const sudoProbe = helperPath ? await runExec("sudo", ["-n", helperPath, "probe"]) : { ok: false };
|
const sudoProbe = helperPath ? await runExec("sudo", ["-n", helperPath, "probe"]) : { ok: false };
|
||||||
const helperStatus =
|
const helperStatus = helperPath && sudoProbe.ok ? parseMacWireguardHelperStatus(sudoProbe.stdout || sudoProbe.stderr || "") : null;
|
||||||
helperPath && sudoProbe.ok ? parseMacWireguardHelperStatus((await runExec("sudo", ["-n", helperPath, "status"])).stdout) : null;
|
|
||||||
let active = false;
|
let active = false;
|
||||||
let showRaw = "";
|
let showRaw = "";
|
||||||
let endpoint = "";
|
let endpoint = "";
|
||||||
@@ -1668,7 +1668,6 @@ async function disconnectNordvpn(installProbe) {
|
|||||||
if (!down.ok) {
|
if (!down.ok) {
|
||||||
const message = (down.stderr || down.stdout || down.error).trim();
|
const message = (down.stderr || down.stdout || down.error).trim();
|
||||||
if (isBenignMacWireguardAbsentError(message)) {
|
if (isBenignMacWireguardAbsentError(message)) {
|
||||||
await runSudoWireguard(installProbe, "cleanup");
|
|
||||||
const dnsState = await restoreMacDnsIfNeeded();
|
const dnsState = await restoreMacDnsIfNeeded();
|
||||||
const cleaned = cleanupMacWireguardAndDnsState();
|
const cleaned = cleanupMacWireguardAndDnsState();
|
||||||
const tailscale = await resumeMacTailscaleIfNeeded();
|
const tailscale = await resumeMacTailscaleIfNeeded();
|
||||||
@@ -1683,7 +1682,6 @@ async function disconnectNordvpn(installProbe) {
|
|||||||
}
|
}
|
||||||
throw new Error(message || "wg-quick down failed");
|
throw new Error(message || "wg-quick down failed");
|
||||||
}
|
}
|
||||||
await runSudoWireguard(installProbe, "cleanup");
|
|
||||||
const dnsState = await restoreMacDnsIfNeeded();
|
const dnsState = await restoreMacDnsIfNeeded();
|
||||||
const cleaned = cleanupMacWireguardAndDnsState();
|
const cleaned = cleanupMacWireguardAndDnsState();
|
||||||
const tailscale = await resumeMacTailscaleIfNeeded();
|
const tailscale = await resumeMacTailscaleIfNeeded();
|
||||||
|
|||||||
@@ -18,21 +18,30 @@ WG_INTERFACE="nordvpnctl"
|
|||||||
PATH="/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
PATH="/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||||
export PATH
|
export PATH
|
||||||
|
|
||||||
if [ "$ACTION" = "probe" ]; then
|
if [ "$ACTION" = "probe" ] || [ "$ACTION" = "status" ]; then
|
||||||
test -x "$WG_QUICK"
|
test -x "$WG_QUICK"
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$ACTION" = "status" ]; then
|
|
||||||
ACTIVE=0
|
ACTIVE=0
|
||||||
if [ -x "$WG" ] && "$WG" show "$WG_INTERFACE" >/dev/null 2>&1; then
|
RUNTIME_INTERFACE=""
|
||||||
|
if [ -x "$WG" ]; then
|
||||||
|
RUNTIME_INTERFACE=$("$WG" show interfaces 2>/dev/null | awk 'NF { print $1; exit }')
|
||||||
|
fi
|
||||||
|
if [ -n "$RUNTIME_INTERFACE" ]; then
|
||||||
|
ACTIVE=1
|
||||||
|
elif [ -x "$WG" ] && "$WG" show "$WG_INTERFACE" >/dev/null 2>&1; then
|
||||||
ACTIVE=1
|
ACTIVE=1
|
||||||
elif /sbin/ifconfig "$WG_INTERFACE" >/dev/null 2>&1; then
|
elif /sbin/ifconfig "$WG_INTERFACE" >/dev/null 2>&1; then
|
||||||
ACTIVE=1
|
ACTIVE=1
|
||||||
|
elif pgrep -f "wg-quick up $WG_CONFIG" >/dev/null 2>&1; then
|
||||||
|
ACTIVE=1
|
||||||
|
elif pgrep -f "wireguard-go utun" >/dev/null 2>&1; then
|
||||||
|
ACTIVE=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "active=$ACTIVE"
|
echo "active=$ACTIVE"
|
||||||
echo "interfaceName=$WG_INTERFACE"
|
echo "interfaceName=$WG_INTERFACE"
|
||||||
|
if [ -n "$RUNTIME_INTERFACE" ]; then
|
||||||
|
echo "wireguardInterface=$RUNTIME_INTERFACE"
|
||||||
|
fi
|
||||||
if [ -f "$WG_CONFIG" ]; then
|
if [ -f "$WG_CONFIG" ]; then
|
||||||
echo "configPath=$WG_CONFIG"
|
echo "configPath=$WG_CONFIG"
|
||||||
fi
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user