#!/usr/bin/env python3 """Install the native Deck host network service, initially disconnected. Does not stop existing gateways, import secrets or change default routes. """ import argparse import ipaddress import json import os from pathlib import Path import shutil import socket import time import subprocess MARKER='# Athena Deck native network service' def main(): p=argparse.ArgumentParser();p.add_argument('--client-uid',type=int,required=True);p.add_argument('--client-gid',type=int,required=True);p.add_argument('--lan-address',required=True);p.add_argument('--gui-port',type=int,default=8108);p.add_argument('--install-tools',action='store_true');p.add_argument('--ports',default='8108,8120,8121,8122,8123,8124');a=p.parse_args() if os.geteuid()!=0 or not Path('/run/systemd/system').is_dir():raise SystemExit('Root auf Debian mit systemd erforderlich.') if a.client_uid<1 or a.client_gid<1:raise SystemExit('Unprivilegierte Deck-UID/GID erforderlich.') address=ipaddress.ip_address(a.lan_address) if address.version!=4 or address.is_loopback or address.is_unspecified:raise SystemExit('Konkrete Host-LAN-Adresse erforderlich.') ports=sorted(set(int(v) for v in a.ports.split(','))) if a.gui_port not in ports or any(not 1024<=v<=65535 for v in ports) or len(ports)>16:raise SystemExit('Maximal 16 feste, nicht privilegierte Ports einschließlich GUI-Port.') if a.install_tools and (not shutil.which('wg') or not shutil.which('ip')): subprocess.run(['apt-get','update'],check=True) subprocess.run(['apt-get','install','-y','--no-install-recommends','wireguard-tools','iproute2'],check=True) if not shutil.which('wg') or not shutil.which('ip'):raise SystemExit('Zuerst Debian-Pakete wireguard-tools und iproute2 installieren. Kein Kernel- oder Treiberupdate erforderlich.') base=Path('/opt/athena-deck-network');state=Path('/var/lib/athena-deck-network');unit=Path('/etc/systemd/system/athena-deck-network.service') if any(p.is_symlink() for p in (base,state,unit)):raise SystemExit('Symlink-Ziel nicht erlaubt.') if unit.exists() and not unit.read_text().startswith(MARKER):raise SystemExit('Dienstname belegt.') if base.exists() and not (base/'managed-by-deck').exists():raise SystemExit('Installationsverzeichnis belegt.') base.mkdir(mode=0o755,exist_ok=True);(base/'managed-by-deck').touch();(base/'network').mkdir(exist_ok=True) source=Path(__file__).resolve().parent.parent/'network' for name in ('__init__.py','native.py','config.py','policy.py'):shutil.copyfile(source/name,base/'network'/name) state.mkdir(mode=0o700,exist_ok=True);state.chmod(0o700) policy=state/'policy.json';interfaces=json.loads(subprocess.check_output(['ip','-j','address','show'],text=True)) interface=next((r['ifname'] for r in interfaces if any(v.get('local')==str(address) for v in r.get('addr_info',[]))),None) if not interface:raise SystemExit('LAN-Adresse ist nicht auf diesem Host vorhanden.') value={'lan_address':str(address),'lan_interface':interface,'gui_port':a.gui_port,'ports':ports} if policy.exists() and json.loads(policy.read_text())!=value:raise SystemExit('Bestehende Dienst-Portdefinition nicht automatisch ändern.') policy.write_text(json.dumps(value));policy.chmod(0o600) unit.write_text(MARKER+f''' [Unit] Description=Athena Deck native WireGuard and restricted access service After=network-online.target Wants=network-online.target [Service] Type=simple WorkingDirectory={base} ExecStart=/usr/bin/python3 -m network.native --client-uid {a.client_uid} --client-gid {a.client_gid} Restart=on-failure RuntimeDirectory=athena-deck-network RuntimeDirectoryMode=0755 RuntimeDirectoryPreserve=yes StateDirectory=athena-deck-network StateDirectoryMode=0700 UMask=0077 NoNewPrivileges=true ProtectSystem=strict ProtectHome=true PrivateTmp=true ProtectKernelTunables=true ProtectKernelModules=true ProtectControlGroups=true CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_RAW CAP_CHOWN RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK ReadWritePaths=/var/lib/athena-deck-network /run/athena-deck-network [Install] WantedBy=multi-user.target ''');unit.chmod(0o644) subprocess.run(['systemctl','daemon-reload'],check=True) subprocess.run(['systemctl','enable','athena-deck-network.service'],check=True,capture_output=True) subprocess.run(['systemctl','restart','athena-deck-network.service'],check=True) for _ in range(30): try: with socket.socket(socket.AF_UNIX) as sock: sock.settimeout(2);sock.connect('/run/athena-deck-network/control.sock');sock.sendall(b'{"action":"status"}\n') with sock.makefile('rb') as f:status=json.loads(f.readline(65536)) if status.get('installed'):break except (OSError,ValueError):pass time.sleep(.2) else:raise SystemExit('Nativer Dienst nicht bereit; systemctl status athena-deck-network.service prüfen.') print('Nativer Netzwerkdienst installiert. Bestehender Gateway und dessen Konfiguration unverändert.') if __name__=='__main__':main()