208 lines
9.4 KiB
Python
208 lines
9.4 KiB
Python
#!/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)
|