Add isolated WireGuard module with guarded access modes and server setup
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""Local management via fixed SSH target or container-private Unix RPC."""
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import secrets
|
||||
import subprocess
|
||||
import threading
|
||||
import time
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
SSH = ['ssh', '-i', str(Path.home()/'.ssh/athena_key'), '-o', 'BatchMode=yes', '-o', 'ConnectTimeout=5', '-o', 'StrictHostKeyChecking=yes', 'root@192.168.1.212']
|
||||
|
||||
class NetworkClient:
|
||||
def __init__(self):
|
||||
self.local = os.environ.get('DECK_NETWORK_SOCKET') == '1'
|
||||
self.job = None
|
||||
self.lock = threading.Lock()
|
||||
self.cached = None
|
||||
self.cached_at = 0
|
||||
|
||||
def call(self, action, values=None, ingress='management'):
|
||||
data = dict(values or {}, action=action)
|
||||
if self.local:
|
||||
from network.rpc import request
|
||||
data.update(ingress=ingress, proxy_token=os.environ.get('DECK_PROXY_TOKEN', ''))
|
||||
return request(data)
|
||||
result = subprocess.run(SSH + ['docker exec -i athena-deck-network python3 -m network.rpc'], input=json.dumps(data), text=True, capture_output=True, timeout=35)
|
||||
try:
|
||||
value = json.loads(result.stdout)
|
||||
except ValueError:
|
||||
raise ValueError('Deck-Netzwerkmodul auf Athena nicht erreichbar oder noch nicht installiert.') from None
|
||||
if result.returncode or ('error' in value and 'installed' not in value):
|
||||
raise ValueError(value.get('error', 'Netzwerkaktion fehlgeschlagen.'))
|
||||
self.cached = None
|
||||
return value
|
||||
|
||||
def status(self, ingress='management'):
|
||||
with self.lock:
|
||||
if self.job and self.job['state'] == 'running':
|
||||
return dict(installed=False, state='installing', job=self.job.copy(), ingress=ingress)
|
||||
if self.cached is not None and time.monotonic()-self.cached_at < 3:
|
||||
return dict(self.cached, ingress=ingress, job=self.job)
|
||||
try:
|
||||
result = self.call('status', ingress=ingress)
|
||||
result['reachable'] = True
|
||||
except (ValueError, OSError, subprocess.SubprocessError):
|
||||
result = dict(installed=False, reachable=False, state='unavailable', error='Netzwerkmodul noch nicht installiert oder Athena nicht erreichbar.')
|
||||
self.cached = result
|
||||
self.cached_at = time.monotonic()
|
||||
return dict(result, ingress=ingress, job=self.job)
|
||||
|
||||
def install(self, password):
|
||||
if self.local:
|
||||
raise ValueError('Die Server-Instanz ist bereits installiert.')
|
||||
if not isinstance(password, str) or not 16 <= len(password) <= 256:
|
||||
raise ValueError('Bitte ein eigenes Deck-Passwort mit mindestens 16 Zeichen setzen.')
|
||||
with self.lock:
|
||||
if self.job and self.job['state'] == 'running':
|
||||
raise ValueError('Installation läuft bereits.')
|
||||
salt = secrets.token_bytes(16)
|
||||
auth = dict(salt=salt.hex(), hash=hashlib.pbkdf2_hmac('sha256', password.encode(), salt, 600000).hex())
|
||||
self.job = dict(state='running', message='Eigener Deck-Container wird gebaut und gestartet. Das kann mehrere Minuten dauern.')
|
||||
threading.Thread(target=self._install, args=(auth,), daemon=True).start()
|
||||
return dict(job=self.job.copy())
|
||||
|
||||
def _install(self, auth):
|
||||
try:
|
||||
# Import the source allowlist without executing the installer entrypoint.
|
||||
from network.install_remote import FILES
|
||||
payload = dict(auth=auth, files={name:base64.b64encode((ROOT/name).read_bytes()).decode() for name in FILES})
|
||||
script = (ROOT/'network/install_remote.py').read_text()
|
||||
# Remote command contains only fixed trusted program text, never user input.
|
||||
import shlex
|
||||
command = 'python3 -c ' + shlex.quote(script)
|
||||
result = subprocess.run(SSH+[command], input=json.dumps(payload), text=True, capture_output=True, timeout=900)
|
||||
value = json.loads(result.stdout)
|
||||
if result.returncode or ('error' in value and 'installed' not in value):
|
||||
raise ValueError(value.get('error', 'Installation fehlgeschlagen.'))
|
||||
# Confirm helper readiness; don't report a successful installation from docker run alone.
|
||||
for _ in range(20):
|
||||
try:
|
||||
self.call('status')
|
||||
break
|
||||
except (ValueError, OSError, subprocess.SubprocessError):
|
||||
time.sleep(1)
|
||||
else:
|
||||
raise ValueError('Container angelegt, aber Netzwerk-Helper nicht bereit. Installation prüfen.')
|
||||
self.job = dict(state='complete', message='Netzwerkmodul installiert. Jetzt eine eigene WireGuard-Konfiguration importieren.')
|
||||
except Exception as exc:
|
||||
self.job = dict(state='failed', message=str(exc) if isinstance(exc, ValueError) and not isinstance(exc, json.JSONDecodeError) else 'Installation fehlgeschlagen; SSH und Docker prüfen.')
|
||||
finally:
|
||||
self.cached = None
|
||||
Reference in New Issue
Block a user