Files
archy/scripts/tests/c01_network_v4_pid1.py

351 lines
25 KiB
Python

#!/usr/bin/python3
"""Private PID1 network composition: real gate, DNS-SD, Podman and relay.
The native executable is a test binary running the real gate/DNS-SD components;
this fixture does not claim full C01 time/history or installed-node acceptance.
"""
import ctypes,hashlib,http.client,json,os,shutil,signal,socket,sqlite3,ssl,struct,subprocess,sys,time
from pathlib import Path
ROOT=Path('/opt/network-fixture')
sys.path.insert(0,str(ROOT/'scripts'))
import c01_network_owner as network
import c01_network_client as client
import c01_network_plan as plan
def run(*args,check=True,timeout=60):
r=subprocess.run(args,capture_output=True,text=True,timeout=timeout)
if check and r.returncode:raise RuntimeError(f'{args[0]} exit={r.returncode}: {r.stderr}')
return r.stdout
def put(path,text,mode=0o644):
path=Path(path);path.parent.mkdir(parents=True,exist_ok=True);path.write_text(text);path.chmod(mode)
def wait(fn,message,seconds=45):
end=time.monotonic()+seconds
while time.monotonic()<end:
try:
result=fn()
if result:return result
except (FileNotFoundError,ConnectionError,OSError,ValueError,RuntimeError):pass
time.sleep(.1)
raise RuntimeError(message)
def user(*args):
# Even image import may initialize Podman's retained rootless namespace.
# Every runtime command must start in the appointed runtime, not fixture host.
return run('nsenter','--net=/run/netns/archipelago-runtime','--','runuser','-u','archipelago','--','env','XDG_RUNTIME_DIR=/run/user/1000','DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus',*args)
def supervisor():
import c01_network_client
group=Path('/sys/fs/cgroup/system.slice/archipelago.service/c01-application');group.mkdir()
io_group=group.with_name('c01-host-io');io_group.mkdir()
host_listener=socket.socket(socket.AF_UNIX,socket.SOCK_SEQPACKET)
host_listener.bind('/run/c01-fixture-managed/host.sock');os.chown('/run/c01-fixture-managed/host.sock',61001,61001)
host_listener.listen(1);host_listener.settimeout(15)
def child_setup():
(group/'cgroup.procs').write_text(str(os.getpid()))
os.setgroups([]);os.setgid(61001);os.setuid(61001)
env=dict(os.environ,C01_NETWORK_FIXTURE='1',C01_DNS_ROLE='managed')
child=subprocess.Popen([str(ROOT/'native-test'),'--ignored','--exact','appgate::listener::network_fixture::serve','--nocapture'],env=env,preexec_fn=child_setup)
lease=None;io_child=None
try:
accepted,_=host_listener.accept()
assert struct.unpack('3i',accepted.getsockopt(socket.SOL_SOCKET,socket.SO_PEERCRED,12))==(child.pid,61001,61001)
put('/run/c01-fixture-hostio/peer.json',json.dumps({'pid':child.pid,'uid':61001,'gid':61001}),0o600)
accepted_fd=accepted.fileno()
def io_setup():
(io_group/'cgroup.procs').write_text(str(os.getpid()))
network.namespaces.enter_host_worker(ctypes.CDLL(None,use_errno=True))
os.dup2(accepted_fd,3,inheritable=True)
if accepted_fd!=3:os.close(accepted_fd)
io_child=subprocess.Popen([str(ROOT/'native-test'),'--ignored','--exact','host_access::tests::network_fixture_owner','--nocapture'],
env=dict(os.environ,C01_NETWORK_FIXTURE='1'),pass_fds=tuple({3,accepted_fd}),preexec_fn=io_setup)
accepted.close();host_listener.close()
wait(lambda:json.loads(Path('/run/c01-fixture-managed/ready.json').read_text()).get('pid')==child.pid,'native gate/DNS-SD readiness')
lease=c01_network_client.NetworkLease(runtime_uid=1000)
names=[name for name,route in lease.policy.routes.items() if route['target']=='managed']
end=time.monotonic()+40
while True:
if child.poll() is not None:raise RuntimeError('native fixture exited')
lease.register_managed_listeners(child.pid,names)
if all(lease.request({'method':'inspect','name':name}).get('producer') for name in names):break
if time.monotonic()>end:raise RuntimeError('managed producer registration deadline')
time.sleep(.1)
lease.require_targets(names)
put('/run/network-supervisor-ready.json',json.dumps({'pid':child.pid,'listeners':len(lease.listeners)}),0o600)
while child.poll() is None:
if io_child.poll() is not None:raise RuntimeError('host fact owner exited')
lease.check();time.sleep(.1)
finally:
if child.poll() is None:child.kill()
child.wait()
if io_child is not None:
if io_child.poll() is None:io_child.kill()
io_child.wait()
host_listener.close()
if lease:lease.close()
def echo_fips():
with socket.socket() as listener:
listener.setsockopt(socket.SOL_SOCKET,socket.SO_REUSEADDR,1)
listener.bind(('0.0.0.0',8443));listener.listen(16)
while True:
connection,_=listener.accept()
with connection:connection.sendall(b'fips-transport:'+connection.recv(1024))
def request(address,port,session=None,tls=False,path='/'):
raw=socket.create_connection((address,port),timeout=5)
if tls:
context=ssl.create_default_context(cafile='/run/c01-fixture-managed/cert.pem')
raw=context.wrap_socket(raw,server_hostname='localhost')
with raw:
headers=f'GET {path} HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n'
if session:headers+='Cookie: session='+session+'\r\n'
raw.sendall((headers+'\r\n').encode())
response=http.client.HTTPResponse(raw);response.begin()
return response.status,response.read(65536)
def main():
assert os.geteuid()==0 and socket.gethostname()=='c01-network-v4-fixture'
assert Path('/run/.containerenv').exists() and os.environ.get('C01_NETWORK_FIXTURE')=='1'
put('/run/c01-network-fixture','private-network-composition\n')
# Never write through a runtime-provided resolver bind mount. Detach only
# this disposable container's mount, then create its own resolver file.
if any(line.split()[4]=='/etc/resolv.conf' for line in Path('/proc/self/mountinfo').read_text().splitlines()):
run('umount','/etc/resolv.conf')
if Path('/etc/resolv.conf').is_symlink():Path('/etc/resolv.conf').unlink()
put('/etc/resolv.conf','nameserver 100.100.100.100\n')
run('ip','link','add','wan0','type','dummy');run('ip','link','set','wan0','up')
for address in ('192.0.2.2/24','192.0.2.3/32','198.51.100.2/32'):run('ip','address','add',address,'dev','wan0')
run('ip','-6','address','add','fd00:10::2/64','dev','wan0')
run('ip','-6','address','add','fd00:10::3/128','dev','wan0')
run('ip','route','add','default','via','192.0.2.1','dev','wan0','src','192.0.2.2')
run('ip','-6','route','add','default','via','fe80::ffff','dev','wan0')
run('ip','link','add','fips0','type','dummy');run('ip','link','set','fips0','up')
run('ip','-6','address','add','fd00:20::2/128','dev','fips0')
put('/etc/fips/fips.pub','79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798\n')
put('/etc/fips/fips.key','0'*63+'1\n',0o600)
put('/run/c01-fixture-hostio/data/.fixture','synthetic-empty-host-data\n',0o600)
put('/home/operator-fixture/private','synthetic-operator-canary\n',0o600)
put('/run/user/2000/private','synthetic-other-user-canary\n',0o600)
for role in ('managed','runtime'):
run('ip','netns','add','archipelago-'+role)
# Netlink setup needs only the network namespace. Unlike `ip -n`,
# nsenter does not attempt an unrelated sysfs remount in a user namespace.
run('nsenter','--net=/run/netns/archipelago-'+role,'ip','link','set','lo','up')
for name,uid in (('archipelago',1000),('c01-manager',61001)):
if not run('getent','passwd',name,check=False):run('useradd','--uid',str(uid),'--create-home',name)
run('systemctl','stop','user@1000.service',check=False)
run('usermod','-G','','-s','/usr/sbin/nologin','archipelago')
for name in ('subuid','subgid'):put('/etc/'+name,'archipelago:100000:65536\n')
for path in ('/var/lib/archipelago/containers/storage','/var/lib/archipelago/fixture-data','/run/c01-fixture-managed'):
run('install','-d','-o','61001' if path.endswith('managed') else '1000','-g','61001' if path.endswith('managed') else '1000','-m','0700',path)
put('/run/c01-fixture-host/.placeholder','')
put('/var/lib/archipelago/fixture-data/marker','backend-retained-marker\n')
marker=Path('/var/lib/archipelago/fixture-data/marker').stat().st_ino
run('install','-d','-o','1000','-g','1000','-m','0700','/home/archipelago/.config','/home/archipelago/.config/containers')
put('/home/archipelago/.config/containers/storage.conf','[storage]\ndriver="vfs"\nrunroot="/run/user/1000/containers"\ngraphroot="/var/lib/archipelago/containers/storage"\n')
put('/home/archipelago/.config/containers/containers.conf','[containers]\nkeyring=false\n')
for source in (ROOT/'scripts').glob('*.py'):put('/usr/lib/archipelago/'+source.name,source.read_text())
for name in ('archipelago-c01-network.service','archipelago-c01-network-provision.service','archipelago-c01-kiosk-endpoints.service'):
put('/etc/systemd/system/'+name,(ROOT/'configs'/name).read_text())
shutil.copyfile(ROOT/'nostr-rs-relay','/usr/local/bin/nostr-rs-relay');Path('/usr/local/bin/nostr-rs-relay').chmod(0o755)
run('install','-d','-o','1000','-g','1000','-m','0700','/var/lib/archipelago/nostr-relay')
put('/var/lib/archipelago/nostr-relay/config.toml',(ROOT/'configs/nostr-relay-config.toml').read_text())
put('/etc/systemd/user/archipelago-runtime-nostr-relay.service',(ROOT/'configs/archipelago-runtime-nostr-relay.service').read_text())
put('/etc/systemd/system/user@1000.service.d/network.conf','[Service]\nNetworkNamespacePath=/run/netns/archipelago-runtime\n')
put('/etc/systemd/system/archipelago-fips.service',f'[Service]\nExecStart=/usr/bin/python3 {ROOT}/driver.py fips\n')
runtime=dict(runtime_user='archipelago',runtime_uid=1000,runtime_gid=1000,manager_uid=61001,manager_gid=61001,
user_cgroup='/user.slice/user-1000.slice/user@1000.service')
put('/etc/archipelago-c01/runtime.json',json.dumps(runtime),0o600)
# Import is filesystem-only; no installed-node store or network is used.
run('systemctl','daemon-reload');run('systemctl','start','user-runtime-dir@1000.service')
# Fixture creation needs the real user manager for enabled-mode cgroups.
# No container workload is started until the owned uplinks are ready.
run('systemctl','start','user@1000.service')
image=user('podman','import',str(ROOT/'backend.tar'),'localhost/network-fixture:current').strip().splitlines()[-1].removeprefix('sha256:')
assert len(image)==64,image
retained_id=user('podman','--cgroup-manager=systemd','create','--name','fixture-retained','--network=host',
'--cgroups=enabled','--cgroup-parent=user.slice','--security-opt=no-new-privileges',image,'/server','127.0.0.1','19005').strip().splitlines()[-1]
retained_unit='archipelago-retained-fixture-'+retained_id+'.service'
with sqlite3.connect('file:/var/lib/archipelago/containers/storage/db.sql?mode=ro',uri=True) as db:
raw=db.execute('SELECT JSON FROM ContainerConfig WHERE ID=?',(retained_id,)).fetchone()[0]
retained_digest=hashlib.sha256(json.dumps(json.loads(raw),sort_keys=True,separators=(',',':'),ensure_ascii=False).encode()).hexdigest()
put('/etc/systemd/user/'+retained_unit,f'''[Service]
Type=exec
Delegate=yes
Restart=no
KillMode=mixed
SuccessExitStatus=137 143
ExecStart=/usr/bin/podman start --attach {retained_id}
ExecStop=/usr/bin/podman stop --ignore --time 1 {retained_id}
''')
manifests=[];routes=[]
def route(name,source,target,port,addresses,target_address,target_port,uid,group,doc=None):
projection=None
if doc:
if doc not in manifests:manifests.append(doc)
projection=network.digest(network.canonical(doc))
routes.append(dict(name=name,source=source,target=target,protocol='udp' if '/udp/' in name else 'tcp',port=port,
source_addresses=addresses,target_address=target_address,target_port=target_port,target_uid=uid,target_cgroup=group,
app_id=doc['app_id'] if doc else None,projection_sha256=projection))
appgroup='/system.slice/archipelago.service/c01-application'
gate_doc=dict(app_id='fixture',manifest_sha256='a'*64,loopback_ports=[['tcp',18001]],target_cgroup=appgroup)
# >253 real source sockets exercises the complete production lease protocol.
for n in range(17):route(f'gate/fixture/tcp/{18001+n}/host','host','managed',18001+n,
[f'127.20.{n}.{i}' for i in range(1,17)],'127.0.0.2',18001,61001,appgroup,gate_doc)
route('gate/fixture/tcp/19001/host','host','managed',19001,['192.0.2.2','fd00:10::2'],'127.0.0.2',18001,61001,appgroup,gate_doc)
for name,port,address in (('fixture',18001,'127.0.0.1'),('fixture-alias',19003,'198.51.100.2')):
group=runtime['user_cgroup']+'/app.slice/'+name+'.service'
source='managed' if name=='fixture' else 'host'
bindings=[source,'tcp',port,'127.0.0.1' if source=='managed' else address]
doc=dict(app_id=name,manifest_sha256='b'*64,loopback_ports=[['tcp',port]],target_cgroup=group,
source_bindings=[bindings],target_addresses=[address],producer={'kind':'container-payload','container_name':name,'image_id':image})
mapped=name=='fixture-alias'
if mapped:doc['producer']['payload_principal']=[100099,100100]
route(f'app/{name}/tcp/{port}/{source}',source,'runtime',port,[bindings[3]],address,port,100099 if mapped else 1000,group,doc)
put('/etc/systemd/user/'+name+'.service',f'''[Service]
ExecStart=/usr/bin/podman run --pull=never --name={name} {'--user=100:101' if mapped else ''} --network=host --cgroups=split --security-opt=no-new-privileges -v /var/lib/archipelago/fixture-data:/data:ro {image} /server {address} {port}
Delegate=yes
ExecStop=/usr/bin/podman stop --time=1 {name}
ExecStopPost=-/usr/bin/podman rm {name}
Restart=no
''')
route('fips-tcp6','host','host',8443,['::'],'127.0.0.1',8443,0,'/system.slice/archipelago-fips.service')
retained_group=runtime['user_cgroup']+'/app.slice/'+retained_unit
retained_doc=dict(app_id='retained-fixture',manifest_sha256=retained_digest,loopback_ports=[['tcp',19005]],target_cgroup=retained_group,
source_bindings=[['host','tcp',19005,'127.0.0.1']],target_addresses=['127.0.0.1'],
producer=dict(kind='retained-container-v1',unit=retained_unit,container_id=retained_id,image_id=image,
config_sha256=retained_digest,scope_kind='bound-libpod-scope',payload_principal=[1000,1000]))
route('app/retained-fixture/tcp/19005/host','host','runtime',19005,['127.0.0.1'],'127.0.0.1',19005,1000,retained_group,retained_doc)
relay_group=runtime['user_cgroup']+'/app.slice/archipelago-runtime-nostr-relay.service'
relay_doc=dict(app_id='nostr-relay',manifest_sha256=hashlib.sha256((ROOT/'configs/archipelago-runtime-nostr-relay.service').read_bytes()).hexdigest(),
loopback_ports=[['tcp',7777]],target_cgroup=relay_group,source_bindings=[['host','tcp',7777,'0.0.0.0'],['host','tcp',7777,'::']],
target_addresses=['127.0.0.1'],producer={'kind':'exact'})
route('app/nostr-relay/tcp/7777/host','host','runtime',7777,['0.0.0.0','::'],'127.0.0.1',7777,1000,relay_group,relay_doc)
for family,address,target in ((4,'0.0.0.0','127.0.0.1'),(6,'::','::1')):
route(f'native/mdns{family}/udp/5353/host','host','managed',5353,[address],target,5353,61001,appgroup)
route('native/website-fips/tcp/32000/host','host','managed',32000,['fd00:20::2'],'127.0.0.1',32000,61001,appgroup)
route('native/website-tor/tcp/32100/host','host','managed',32100,['127.0.0.1'],'127.0.0.1',32100,61001,appgroup)
route('native/peer/tcp/5679/host','host','managed',5679,['fd00:20::2'],'127.0.0.1',5679,61001,appgroup)
namespaces={role:list(network.namespaces.process_namespace(1) if role=='host' else network.namespaces.appointed(role)) for role in ('host','managed','runtime')}
pasta=Path('/usr/bin/pasta.avx2')
policy=dict(version=4,namespaces=namespaces,manifests=manifests,routes=routes,
uplink=dict(executable=str(pasta),sha256=hashlib.sha256(pasta.read_bytes()).hexdigest(),wan=['wan0','192.0.2.1','192.0.2.2'],
dns=['1.1.1.1'],routing='host-dual-stack',dns_host='100.100.100.100',runtime_addresses=['198.51.100.2']),
mdns=dict(interface='wan0',ifindex=socket.if_nametoindex('wan0'),ipv4='192.0.2.2'))
print(json.dumps(plan.validate(policy,runtime)),flush=True)
put(network.CONFIG,network.canonical(policy).decode(),0o600)
put('/etc/archipelago-c01/kiosk-endpoints.json',json.dumps(dict(version=1,policy_sha256=network.digest(network.canonical(policy)),
executables={'/usr/local/bin/nostr-rs-relay':hashlib.sha256(Path('/usr/local/bin/nostr-rs-relay').read_bytes()).hexdigest()},
routes=['app/nostr-relay/tcp/7777/host'])),0o600)
put('/etc/systemd/system/archipelago.service',f'''[Service]
Type=exec
ExecStart=/usr/bin/python3 {ROOT}/driver.py supervisor
NetworkNamespacePath=/run/netns/archipelago-managed
Delegate=yes
DelegateSubgroup=supervisor
NoNewPrivileges=yes
AmbientCapabilities=CAP_SETUID CAP_SETGID
KillMode=control-group
TimeoutStopSec=5
''')
put('/etc/systemd/system/fixture-mdns-host.service',f'''[Service]
Environment=C01_NETWORK_FIXTURE=1 C01_DNS_ROLE=host
ExecStart={ROOT}/native-test --ignored --exact appgate::listener::network_fixture::serve --nocapture
KillMode=control-group
''')
run('openssl','req','-x509','-newkey','rsa:2048','-nodes','-keyout','/run/c01-fixture-managed/key.pem',
'-out','/run/c01-fixture-managed/cert.pem','-days','1','-subj','/CN=localhost','-addext','subjectAltName=DNS:localhost')
for name in ('key.pem','cert.pem'):os.chown('/run/c01-fixture-managed/'+name,61001,61001)
run('systemctl','daemon-reload');run('systemctl','start','archipelago-fips.service','archipelago-c01-network.service')
run('systemctl','start','user@1000.service');user('systemctl','--user','daemon-reload')
user('systemctl','--user','start','fixture.service','fixture-alias.service','archipelago-runtime-nostr-relay.service',retained_unit)
for name,app_id in (('fixture','fixture'),('fixture-alias','fixture-alias'),('fixture-retained','retained-fixture')):
pid=wait(lambda:int(user('podman','inspect','--format','{{.State.Pid}}',name).strip()),'actual payload PID')
actual=network.process(pid)
if name=='fixture-alias':assert actual['uids']==[100099]*4 and actual['gids']==[100100]*4,actual
assert list(actual['net'])==policy['namespaces']['runtime'],{'container':name,'process':actual,'expected_runtime':policy['namespaces']['runtime']}
print(json.dumps({'payload':name,'process':actual}),flush=True)
observed_policy=network.Policy()
try:
declared=next(r for r in observed_policy.routes.values() if r['app_id']==app_id and r['target']=='runtime')
observations=[]
for entry in Path(f'/proc/{pid}/fd').iterdir():
if not os.readlink(entry).startswith('socket:['):continue
try:
pinned=network.Target(observed_policy,declared,pid,int(entry.name))
pinned.close();observations.append({'fd':entry.name,'qualified':True})
except (OSError,ValueError) as error:observations.append({'fd':entry.name,'refusal':str(error)})
print(json.dumps({'payload_socket_admission':name,'observations':observations}),flush=True)
finally:observed_policy.close()
run('systemctl','start','archipelago-c01-network-provision.service','archipelago-c01-kiosk-endpoints.service','fixture-mdns-host.service','archipelago.service')
assert Path('/home/operator-fixture/private').read_text()=='synthetic-operator-canary\n'
assert Path('/run/user/2000/private').read_text()=='synthetic-other-user-canary\n'
for unit in ('archipelago-c01-network.service','archipelago-c01-network-provision.service'):
pid=int(run('systemctl','show',unit,'--property=MainPID','--value').strip());assert pid>1
run('nsenter','--target',str(pid),'--mount','--root','--wd=/','--','python3','-c',
"from pathlib import Path; assert Path('/run/user/1000/systemd/private').is_socket(); assert not Path('/home/operator-fixture/private').exists(); assert not Path('/run/user/2000/private').exists()")
wait(lambda:Path('/run/network-supervisor-ready.json').exists(),'complete native listener registration')
authentication_diagnostic=[]
def authenticated():
session=json.loads(Path('/run/c01-fixture-managed/ready.json').read_text())['fixture_session']
status,body=request('192.0.2.2',19001,session)
if not authentication_diagnostic:
authentication_diagnostic.append(True)
print(json.dumps({'first_gate_response':status,'body_prefix':body[:160].decode(errors='replace')}),flush=True)
return session if status==200 and b'backend-retained-marker' in body else None
session=wait(authenticated,'authenticated gate reaches actual runtime container')
for address in ('192.0.2.2','fd00:10::2'):
for tls in (False,True):
status,body=request(address,19001,tls=tls)
assert status==401 and b'backend-retained-marker' not in body and b'archipelago' in body.lower(),(status,body[:100])
status,body=request(address,19001,session,tls)
assert status==200 and b'backend-retained-marker' in body,(status,body[:100])
for role in ('host','managed'):
seen=wait(lambda:json.loads(Path(f'/run/c01-fixture-{role}/seen.json').read_text()),'genuine DNS-SD '+role)
assert seen['discovered']!=role and seen['addresses']
print(json.dumps({'dns_sd_role':role,**seen}),flush=True)
assert b'backend-retained-marker' in request('198.51.100.2',19003)[1]
user('systemctl','--user','restart','fixture-alias.service')
wait(lambda:b'backend-retained-marker' in request('198.51.100.2',19003)[1],'mapped non-root payload recreation and registration')
assert b'website-fips-marker' in request('fd00:20::2',32000)[1]
assert b'website-tor-marker' in request('127.0.0.1',32100)[1]
assert request('fd00:20::2',5679,path='/health')[0]==200
assert request('fd00:20::2',5679,path='/api/rpc')[0]==403
wait(lambda:Path('/run/c01-fixture-managed/gate-facts.json').exists(),'actual authenticated gate public facts')
facts=json.loads(Path('/run/c01-fixture-managed/host-facts.json').read_text())
assert facts['address']=='fd00:20::2' and facts['service_state']=='active'
for address in ('127.0.0.1','::1'):
def relay_ready():
with socket.create_connection((address,7777),timeout=3) as stream:
stream.sendall(b'GET / HTTP/1.1\r\nHost: localhost\r\nAccept: application/nostr+json\r\nConnection: close\r\n\r\n')
response=http.client.HTTPResponse(stream);response.begin()
return response.status==200 and b'supported_nips' in response.read(65536)
wait(relay_ready,'real relay IPv4/IPv6 publication')
with socket.create_connection(('::1',8443),timeout=5) as stream:
stream.sendall(b'opaque-noise-transport');assert stream.recv(1024)==b'fips-transport:opaque-noise-transport'
before=user('podman','inspect','--format','{{.Id}}','fixture').strip()
user('systemctl','--user','restart','fixture.service')
def recreated_id():
current=user('podman','inspect','--format','{{.Id}}','fixture').strip()
return current if current!=before else None
after=wait(recreated_id,'actual recreated container identity')
assert before!=after
wait(authenticated,'actual recreated payload re-registration')
assert Path('/var/lib/archipelago/fixture-data/marker').stat().st_ino==marker
wait(lambda:b'backend-retained-marker' in request('127.0.0.1',19005)[1],'exact retained-ID external-scope publication')
retained_before=user('podman','inspect','--format','{{.Id}}','fixture-retained').strip()
user('systemctl','--user','restart',retained_unit)
wait(lambda:b'backend-retained-marker' in request('127.0.0.1',19005)[1],'retained keeper invocation re-registration')
assert user('podman','inspect','--format','{{.Id}}','fixture-retained').strip()==retained_before==retained_id
print(json.dumps({'passed':True,'real_gate_http_tls_ipv4_ipv6':True,'actual_podman_recreation':True,
'mapped_nonroot_payload_recreation':True,
'data_inode_preserved':True,'private_alias':True,'fips_transport_ipv6':True,'exact_id_external_scope_restart':True,
'observer_user_bus_visible_operator_homes_hidden':True,
'complete_listener_lease':json.loads(Path('/run/network-supervisor-ready.json').read_text())['listeners'],
'seams':['isolated fake WAN','test-only native process, not time/history admission','synthetic backend image','opaque FIPS transport echo']}),flush=True)
if __name__=='__main__':
if sys.argv[1:] == ['supervisor']:supervisor()
elif sys.argv[1:] == ['fips']:echo_fips()
elif not sys.argv[1:]:main()
else:raise ValueError('fixed fixture modes only')