Improve VPN status reporting and recovery

This commit is contained in:
2026-10-13 06:53:52 +03:00
parent fdb15ae462
commit 7a716b4c9c
188 changed files with 6953 additions and 1911 deletions
+207
View File
@@ -0,0 +1,207 @@
#!/usr/bin/env python3
"""Privileged stop ownership, cancellation races and DNS preservation; no real signals."""
from pathlib import Path
import subprocess
import sys
import tempfile
sys.dont_write_bytecode = True
from dns_recovery import Sandbox, check
TOKEN = '11111111-2222-3333-4444-555555555555'
ATTEMPT = '22222222-2222-3333-4444-555555555555'
def call(s, operation, *args, ok=True, **extra):
env = dict(s.environment(), SUDO_UID='501')
env.update(extra)
result = subprocess.run([str(s.helper), operation, *args], env=env,
text=True, capture_output=True, timeout=20)
assert (result.returncode == 0) == ok, (result.stdout, result.stderr)
s.read()
return result.stdout.strip()
def begin(s):
s.update(rows=[[200, 1, '/bin/bash /fake/vpn-lemanapro.sh --json'],
[300, 200, 'python /fake/openconnect-lite']], uids={'400': 0})
call(s, '--begin', TOKEN, '200', TEST_BEGIN='1')
def connect(s, dns='10.200.0.53', attempt=ATTEMPT, engine=400):
s.data.update(owner=engine, uids={str(engine): 0})
s.data['rows'].append([engine, 300, '/opt/homebrew/bin/openconnect --cookie-on-stdin'])
s.save()
call(s, '--vpnc', TOKEN, attempt, reason='connect', TEST_HOOK='1', INTERNAL_IP4_DNS=dns)
def owned_stop(s):
begin(s); connect(s)
before = s.journal.read_bytes()
s.data['rows'].append([900, 1, '/opt/homebrew/bin/openconnect --other-vpn'])
s.save()
assert call(s, '--stop', TOKEN) == 'stopping'
assert s.events().splitlines() == ['stock connect', 'signal 400']
assert s.journal.read_bytes() == before
assert s.data['services']['Wi-Fi'] == ['10.200.0.53']
# Stop released the native lock; the disconnect hook can restore custom DNS.
s.data['rows'].pop(); s.save()
call(s, '--vpnc', TOKEN, reason='disconnect', TEST_HOOK='1', INTERNAL_IP4_DNS='10.200.0.53')
assert s.data['services']['Wi-Fi'] == [] and not s.journal.exists()
def cancelled_sso(s):
begin(s)
assert call(s, '--stop', TOKEN) == 'stopping'
assert set(s.events().splitlines()) == {'signal 200', 'signal 300'}
s.data['rows'].append([400, 300, '/opt/homebrew/bin/openconnect'])
s.save()
# A child that starts after the stop snapshot must never configure the network.
call(s, '--vpnc', TOKEN, ok=False, reason='pre-init', TEST_HOOK='1')
assert 'stock' not in s.events() and not s.journal.exists()
def rejected(s):
begin(s); connect(s)
for args, env in [((TOKEN.replace('1', '9'),), {}), ((TOKEN,), {'SUDO_UID': '502'})]:
call(s, '--stop', *args, ok=False, **env)
assert 'signal' not in s.events()
call(s, '--begin', TOKEN.replace('1', '9'), '200', ok=False, TEST_BEGIN='1')
s.update(boot='boot-two')
call(s, '--stop', TOKEN, ok=False)
assert 'signal' not in s.events()
def reused(s):
begin(s); connect(s)
s.update(starts={'200': 'Wed Oct 7 13:00:00 2026', '400': 'Wed Oct 7 13:00:00 2026'})
assert call(s, '--stop', TOKEN) == 'stopping'
assert 'signal' not in s.events(), 'Reused PIDs must never receive a signal'
def reuse_during_stop(s):
begin(s); connect(s)
s.update(reuse_on_uid='400')
call(s, '--stop', TOKEN)
assert 'signal' not in s.events(), 'Recheck engine identity immediately before signaling'
def no_dns(s):
begin(s); connect(s, dns='')
assert not s.journal.exists()
call(s, '--stop', TOKEN)
assert s.events().splitlines() == ['stock connect', 'signal 400']
def failures(s):
begin(s); connect(s)
s.update(ps_fail=True)
call(s, '--stop', TOKEN, ok=False)
assert 'signal' not in s.events()
s.update(ps_fail=False, kill_fail=True)
call(s, '--stop', TOKEN, ok=False)
assert s.journal.exists()
assert s.events().splitlines() == ['stock connect', 'signal 400']
def hook_ownership(s):
begin(s)
s.data['rows'].append([400, 1, '/opt/homebrew/bin/openconnect'])
s.save()
call(s, '--vpnc', TOKEN, ok=False, reason='connect', TEST_HOOK='1')
assert not s.journal.exists() and not s.events()
s.data['rows'].pop(); s.save(); connect(s)
s.update(uids={'400': 502})
call(s, '--stop', TOKEN, ok=False)
assert 'signal' not in s.events()
s.update(uids={'400': 0})
s.data['rows'][-1][2] = '/usr/bin/sleep 100'
s.save()
call(s, '--stop', TOKEN, ok=False)
assert 'signal' not in s.events()
def protected(s):
begin(s)
session = s.path/'root/session'
saved = s.path/'saved'
session.rename(saved); session.symlink_to(saved)
call(s, '--stop', TOKEN, ok=False)
assert not s.events() and saved.exists()
def retry_recovery(s):
begin(s); connect(s)
before = s.journal.read_bytes()
assert call(s, '--recover-session', TOKEN, TEST_BEGIN='1') == 'active'
assert s.journal.read_bytes() == before
s.data['rows'] = s.data['rows'][:2]; s.save()
assert call(s, '--recover-session', TOKEN, TEST_BEGIN='1') == 'active', 'SSO still blocks retry'
s.data['rows'] = s.data['rows'][:1]; s.save()
for command in ['/opt/homebrew/bin/openconnect --other-vpn', '/bin/bash /fake/vpn-lemanapro.sh --json']:
s.data['rows'].append([900, 1, command]); s.save()
assert call(s, '--recover-session', TOKEN, TEST_BEGIN='1') == 'active'
assert s.journal.read_bytes() == before
s.data['rows'].pop(); s.save()
call(s, '--recover-session', TOKEN, ok=False) # unrelated caller
call(s, '--recover-session', TOKEN, ok=False, TEST_BEGIN='1', SUDO_UID='502')
call(s, '--recover-session', TOKEN.replace('1', '9'), ok=False, TEST_BEGIN='1')
s.update(starts={'200': 'Wed Oct 7 13:00:00 2026'})
call(s, '--recover-session', TOKEN, ok=False, TEST_BEGIN='1')
s.update(starts={})
assert call(s, '--recover-session', TOKEN, TEST_BEGIN='1') == 'restored'
assert not s.journal.exists() and s.data['services'] == {'Wi-Fi': [], 'USB LAN': ['1.1.1.1']}
def restart_engine(s):
begin(s); connect(s)
before = s.journal.read_bytes()
s.data['rows'].append([900, 1, '/opt/homebrew/bin/openconnect --other-vpn']); s.save()
assert call(s, '--restart', TOKEN, ATTEMPT) == 'stopping'
assert s.events().splitlines() == ['stock connect', 'signal 400']
assert (s.path/'root/session').read_text().strip().split('\t')[8] == '0'
assert s.journal.read_bytes() == before
s.data['rows'] = s.data['rows'][:1]; s.save()
assert call(s, '--recover-session', TOKEN, TEST_BEGIN='1') == 'restored'
s.data['rows'].append([300, 200, 'python /fake/openconnect-lite']); s.save()
connect(s)
assert s.data['services']['Wi-Fi'] == ['10.200.0.53'], 'New tunnel may reuse the registered CLI'
call(s, '--stop', TOKEN)
previous = s.events()
assert call(s, '--restart', TOKEN, ATTEMPT) == 'cancelled'
assert s.events() == previous, 'Restart cannot override Stop'
assert call(s, '--recover-session', TOKEN, TEST_BEGIN='1') == 'active'
s.data['rows'] = s.data['rows'][:1]; s.save()
assert call(s, '--recover-session', TOKEN, TEST_BEGIN='1') == 'cancelled'
assert not s.journal.exists() and s.data['services']['Wi-Fi'] == []
def restart_without_engine(s):
begin(s)
assert call(s, '--restart', TOKEN, ATTEMPT) == 'stale'
assert not s.events(), 'Restart must not kill the CLI or authentication children'
connect(s)
call(s, '--restart', TOKEN, ATTEMPT, ok=False, SUDO_UID='502')
call(s, '--restart', TOKEN.replace('1', '9'), ATTEMPT, ok=False)
s.update(starts={'400': 'Wed Oct 7 13:00:00 2026'})
call(s, '--restart', TOKEN, ATTEMPT)
assert 'signal' not in s.events(), 'Restart must not signal a reused engine PID'
def stale_restart(s):
begin(s); connect(s)
old_request = ('--restart', TOKEN, ATTEMPT)
s.data['rows'] = s.data['rows'][:1]; s.save()
assert call(s, '--recover-session', TOKEN, TEST_BEGIN='1') == 'restored'
s.data['rows'].append([300, 200, 'python /fake/openconnect-lite']); s.save()
next_attempt = ATTEMPT.replace('2', 'a')
connect(s, attempt=next_attempt, engine=401)
previous = s.events()
assert call(s, *old_request) == 'stale'
assert s.events() == previous, 'Delayed old request must leave replacement engine alive'
call(s, '--restart', TOKEN, next_attempt)
assert s.events().splitlines()[-1] == 'signal 401'
def legacy_session(s):
begin(s); connect(s)
record = s.path/'root/session'
fields = record.read_text().strip().split('\t')[:9]
fields[0] = '1'
record.write_text('\t'.join(fields)+'\n')
assert call(s, '--restart', TOKEN, ATTEMPT) == 'stale'
assert call(s, '--stop', TOKEN) == 'stopping'
assert s.events().splitlines()[-1] == 'signal 400', 'Existing session stays safely stoppable after upgrade'
with tempfile.TemporaryDirectory(prefix='lemana-stop-') as temporary:
for name, test in [('owned-engine-only-and-dns', owned_stop), ('cancel-before-engine', cancelled_sso),
('foreign-session-user-and-boot', rejected), ('pid-reuse', reused),
('pid-reuse-during-stop', reuse_during_stop),
('tunnel-without-dns', no_dns), ('inspection-and-signal-errors', failures),
('hook-ancestry-and-engine-identity', hook_ownership),
('protected-session-record', protected), ('retry-recovery-ownership', retry_recovery),
('restart-engine-and-cancel', restart_engine), ('restart-without-engine', restart_without_engine),
('delayed-restart-keeps-new-engine', stale_restart), ('legacy-session-upgrade', legacy_session)]:
check(Path(temporary), name, test)