85 lines
4.8 KiB
Python
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()
|