Files
Athena-Deck/deploy/setup_network_helper.py
T

85 lines
4.8 KiB
Python

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