#!/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: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')