Compare commits

...
3 Commits
Author SHA1 Message Date
duffyduckandClaude Opus 4.8 94af8995cf feat(host-agent): systemd-Service-Installer + SNI-Doku ueberall
install-service.sh: installiert Binary (/usr/local/bin) + .env (/etc/aria-host-
agent/.env, 0600) + systemd-Unit und macht enable --now. .env-Pfad als Argument
ODER ohne Argument ein ncurses-Dateidialog (dialog --fselect, bietet Installation
von dialog an). Findet die Binary unter dist/ bzw. ./ oder als 2. Argument.

Doku aktualisiert (RVS_SNI fuer RZ-interne Clients + Installer):
- host-agent/README: Installer-Abschnitt + TLS/SNI-Abschnitt.
- README (zentral): Installer + SNI im Host-Agent-Abschnitt (Verweis auf
  Satellit/Compute-Nodes).
- satellite/README + xtts/README: RVS_SNI-Hinweis fuer Boxen/Satelliten im
  RVS-Netz.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:27:07 +02:00
duffyduckandClaude Opus 4.8 286a71e47c feat(xtts): RVS_SNI in allen Compute-Bridges — AI-Box im RZ-Netz auf interne IP
Damit eine AI-Box (f5tts/whisper/voxtral/llm-adapter) auch im selben Netz wie
der RVS direkt auf dessen interne IP verbinden kann (kein NAT-Hairpin ueber den
externen Hostnamen), ohne am SNI/Cert zu scheitern: RVS_HOST=<ip> + RVS_SNI=<name>.
server_hostname im ssl-Context, gated auf use_tls (respektiert den TLS-Fallback).
Leer = Verhalten wie bisher. Szenario: zweite Box im RZ + eine zuhause.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:22:27 +02:00
duffyduckandClaude Opus 4.8 f4221bf436 feat(satellite): RVS_SNI — wie beim Host-Agent (interne IP + Cert-Hostname)
Gleiche Faehigkeit wie beim host-agent: ein Satellit im selben Netz wie der RVS
kann direkt auf die interne IP verbinden und trotzdem den Cert-Namen im TLS-SNI
praesentieren (Caddy findet sonst kein Zertifikat -> tlsv1 alert internal error).
Nuetzlich z.B. fuer einen RZ-internen Satelliten, nur um Credentials zu hinterlegen.
RVS_HOST=<ip> + RVS_SNI=<name>; leer = Verhalten wie bisher.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-24 17:19:59 +02:00
12 changed files with 207 additions and 16 deletions
+18 -1
View File
@@ -970,7 +970,24 @@ cp .env.example .env # RVS-Zugang + CONTROL_ENABLED=true eintragen
chmod +x aria-host-agent && ./aria-host-agent
```
Oder als Dienst: `host-agent/aria-host-agent.service` (systemd).
**Als systemd-Dienst** (Dauerbetrieb) — der Installer kopiert Binary + `.env`
an ihre Plätze und richtet den Dienst ein:
```bash
sudo ./install-service.sh /pfad/zur/.env # .env-Pfad direkt
sudo ./install-service.sh # oder: ncurses-Dateidialog (dialog)
```
→ Binary nach `/usr/local/bin`, `.env` nach `/etc/aria-host-agent/.env` (0600),
Unit installiert + `enable --now`. Danach `journalctl -u aria-host-agent -f`.
### TLS / SNI — Agent im selben Netz wie der RVS
Verbindet der Agent direkt auf die **interne RVS-IP** (statt über den externen
Hostnamen per NAT-Hairpin), scheitert TLS sonst am fehlenden Cert für die IP
(`tlsv1 alert internal error`). Dann in der `.env`: `RVS_HOST=<interne-ip>` +
`RVS_SNI=<zert-name>` (z.B. `mobil.hacker-net.de`). Gilt genauso für Satelliten
und Compute-Nodes im RZ-Netz (`RVS_SNI` in deren `.env`).
### sudo
+36 -1
View File
@@ -53,7 +53,42 @@ Overlay stapeln. Zwei Auswege:
1. `dist/aria-host-agent` auf den Ziel-Rechner kopieren.
2. `.env.example` → `.env` daneben, RVS-Zugang + `CONTROL_ENABLED=true` eintragen.
3. Starten: `chmod +x aria-host-agent && ./aria-host-agent`
— oder als Dienst: siehe `aria-host-agent.service`.
### Als systemd-Dienst (empfohlen für Dauerbetrieb)
Der Installer kopiert Binary + `.env` an ihre Plätze und richtet den Dienst ein:
```bash
# .env-Pfad direkt übergeben:
sudo ./install-service.sh /pfad/zur/.env
# ODER ohne Argument -> ncurses-Dateidialog (dialog) zum Auswählen der .env:
sudo ./install-service.sh
```
Er legt ab:
- Binary → `/usr/local/bin/aria-host-agent`
- `.env` → `/etc/aria-host-agent/.env` (Rechte `0600`, enthält Token/Passwörter)
- Unit → `/etc/systemd/system/aria-host-agent.service`, dann `enable --now`.
Danach: `systemctl status aria-host-agent` · `journalctl -u aria-host-agent -f`.
Die Binary sucht er unter `dist/aria-host-agent` bzw. `./aria-host-agent` (oder
2. Argument). Braucht `dialog` für den Dateibrowser (bietet die Installation an).
## TLS / SNI — Agent im selben Netz wie der RVS
Steht der Rechner im **selben Netz wie der RVS** (z.B. Rechenzentrum) und soll
direkt auf dessen **interne IP** verbinden (kein NAT-Hairpin über den externen
Hostnamen), scheitert der TLS-Handshake sonst an `tlsv1 alert internal error`
(Caddy hat kein Zertifikat für die IP). Lösung — in der `.env`:
```
RVS_HOST=172.0.2.34 # interne RVS-IP
RVS_SNI=mobil.hacker-net.de # Name, für den das Caddy-Zert gilt
```
Der Agent verbindet dann auf die IP, präsentiert aber den Namen im TLS-SNI.
Zuhause / im Normalfall `RVS_SNI` leer lassen und den Hostnamen als `RVS_HOST`.
## sudo
+75
View File
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# ARIA Host-Agent — Service-Installer (systemd).
#
# Installiert die Binary nach /usr/local/bin, die .env nach /etc/aria-host-agent
# und richtet den systemd-Dienst ein (enable + start).
#
# Nutzung:
# sudo ./install-service.sh /pfad/zur/.env # .env-Pfad direkt uebergeben
# sudo ./install-service.sh # ncurses-Dateidialog (dialog)
#
# Die Binary wird unter ./dist/aria-host-agent oder ./aria-host-agent erwartet
# (vorher ./build.sh oder ./build-native.sh ausfuehren), oder als 2. Argument.
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
BIN_DST="/usr/local/bin/aria-host-agent"
ETC_DIR="/etc/aria-host-agent"
UNIT_DST="/etc/systemd/system/aria-host-agent.service"
die() { echo "FEHLER: $*" >&2; exit 1; }
[ "$(id -u)" -eq 0 ] || die "Bitte mit sudo/root ausfuehren: sudo $0 $*"
# ── Binary finden (Arg 2 > dist/ > ./) ──────────────────────────────
BIN_SRC="${2:-}"
if [ -z "$BIN_SRC" ]; then
if [ -x "$SCRIPT_DIR/dist/aria-host-agent" ]; then BIN_SRC="$SCRIPT_DIR/dist/aria-host-agent"
elif [ -x "$SCRIPT_DIR/aria-host-agent" ]; then BIN_SRC="$SCRIPT_DIR/aria-host-agent"
fi
fi
[ -n "$BIN_SRC" ] && [ -f "$BIN_SRC" ] || die "Binary nicht gefunden. Erst bauen (./build.sh) oder als 2. Argument uebergeben."
# ── .env bestimmen: Arg 1, sonst ncurses-Dateidialog ────────────────
ENV_SRC="${1:-}"
if [ -z "$ENV_SRC" ]; then
if ! command -v dialog >/dev/null 2>&1; then
echo "Kein .env-Pfad uebergeben und 'dialog' ist nicht installiert."
read -r -p "dialog jetzt installieren (apt)? [j/N] " a
case "$a" in
j|J|y|Y) (apt-get update && apt-get install -y dialog) || die "dialog-Installation fehlgeschlagen — .env-Pfad bitte als Argument uebergeben." ;;
*) die "Ohne dialog bitte den .env-Pfad als Argument uebergeben: sudo $0 /pfad/zur/.env" ;;
esac
fi
# dialog --fselect: Dateibrowser; --stdout gibt die Auswahl auf stdout.
START_DIR="${SUDO_USER:+/home/$SUDO_USER/}"
[ -d "$START_DIR" ] || START_DIR="$SCRIPT_DIR/"
ENV_SRC="$(dialog --stdout --title 'ARIA Host-Agent — .env auswaehlen' \
--fselect "$START_DIR" 14 72)" || true
clear
[ -n "$ENV_SRC" ] || die "Abgebrochen — keine .env ausgewaehlt."
fi
[ -f "$ENV_SRC" ] || die ".env nicht gefunden: $ENV_SRC"
echo "Binary : $BIN_SRC"
echo ".env : $ENV_SRC"
# ── Installieren ────────────────────────────────────────────────────
install -m 0755 "$BIN_SRC" "$BIN_DST"
install -d -m 0755 "$ETC_DIR"
install -m 0600 "$ENV_SRC" "$ETC_DIR/.env" # 0600: enthaelt Token/Passwoerter
if [ -f "$SCRIPT_DIR/aria-host-agent.service" ]; then
install -m 0644 "$SCRIPT_DIR/aria-host-agent.service" "$UNIT_DST"
else
die "aria-host-agent.service nicht gefunden neben dem Installer."
fi
systemctl daemon-reload
systemctl enable --now aria-host-agent
echo
echo "✓ Installiert & gestartet."
echo " Status: systemctl status aria-host-agent"
echo " Log: journalctl -u aria-host-agent -f"
echo " .env: $ETC_DIR/.env (aendern -> systemctl restart aria-host-agent)"
+6
View File
@@ -7,6 +7,12 @@ RVS_HOST=rvs.example.de
RVS_PORT=443
RVS_TLS=true
RVS_TOKEN=
# RVS_SNI: nur noetig, wenn RVS_HOST eine IP ist (Satellit im selben Netz wie der
# RVS, direkt auf die interne IP). Dann hier den Zertifikats-/Hostnamen angeben,
# damit der TLS-Handshake (SNI) passt. Sonst leer lassen.
# RVS_HOST=172.0.2.34
# RVS_SNI=mobil.hacker-net.de
RVS_SNI=
# ─── Identitaet / Adresse dieses Satelliten ────────────────────────
# SATELLITE_ID = technisch eindeutig (a-z0-9-_), Default = Hostname-Slug.
+5
View File
@@ -48,6 +48,11 @@ python satellite.py # liest .env automatisch
den Satelliten erreicht). `SATELLITE_LOCATION` ist der Name, über den ARIA das Netz
anspricht („Büro").
**Satellit im selben Netz wie der RVS** (z.B. Rechenzentrum, direkt auf die
interne IP statt NAT-Hairpin): `RVS_HOST=<interne-ip>` + **`RVS_SNI=<zert-name>`**
(der Name, für den das Caddy-Zertifikat gilt). Ohne das scheitert TLS an
`tlsv1 alert internal error`. Zuhause / normal: `RVS_SNI` leer lassen.
**Kontrolle:** im Log/Diagnostic muss `primary_ip` im Ziel-LAN liegen
(`192.168.177.x`) — steht da `192.168.65.x` oder `172.x`, sitzt der Satellit im
falschen (Docker-)Netz.
+15 -3
View File
@@ -107,6 +107,10 @@ RVS_HOST = os.environ.get("RVS_HOST", "")
RVS_PORT = int(os.environ.get("RVS_PORT", "443") or "443")
RVS_TLS = _env_bool("RVS_TLS", True)
RVS_TOKEN = os.environ.get("RVS_TOKEN", "")
# TLS-Hostname (SNI + Zertifikatspruefung), falls RVS_HOST eine IP ist — z.B. ein
# Satellit im selben Netz wie der RVS, der direkt auf die interne IP verbindet
# (das Caddy-Zertifikat gilt aber fuer den Namen). Leer = SNI = RVS_HOST.
RVS_SNI = os.environ.get("RVS_SNI", "").strip()
SATELLITE_ID = (os.environ.get("SATELLITE_ID") or _default_id()).strip()
SATELLITE_LOCATION = (os.environ.get("SATELLITE_LOCATION") or SATELLITE_ID).strip()
@@ -1316,13 +1320,21 @@ class Satellite:
_creds_load()
asyncio.create_task(self._periodic_scan())
backoff = 1
# SNI-/Cert-Override: auf eine IP verbinden, aber den Namen im TLS-
# Handshake praesentieren (Caddy-Cert gilt fuer den Namen).
connect_kwargs = {"max_size": 8 * 1024 * 1024,
"ping_interval": 20, "ping_timeout": 20}
if RVS_TLS and RVS_SNI:
import ssl
connect_kwargs["ssl"] = ssl.create_default_context()
connect_kwargs["server_hostname"] = RVS_SNI
while True:
proto = "wss" if RVS_TLS else "ws"
url = f"{proto}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}"
try:
logger.info("Verbinde mit RVS %s://%s:%s …", proto, RVS_HOST, RVS_PORT)
async with websockets.connect(url, max_size=8 * 1024 * 1024,
ping_interval=20, ping_timeout=20) as ws:
logger.info("Verbinde mit RVS %s://%s:%s%s …", proto, RVS_HOST, RVS_PORT,
f" (SNI {RVS_SNI})" if RVS_SNI else "")
async with websockets.connect(url, **connect_kwargs) as ws:
self.ws = ws
backoff = 1
await self._hello(log=True)
+7
View File
@@ -43,6 +43,13 @@ RVS_PORT=444
RVS_TLS=true
RVS_TLS_FALLBACK=true
RVS_TOKEN=dein_token_hier
# RVS_SNI: nur noetig, wenn die Box im SELBEN Netz wie der RVS steht und direkt
# auf dessen interne IP verbinden soll (kein NAT-Hairpin ueber den externen
# Hostnamen). Dann RVS_HOST=<interne-ip> und hier den Zertifikats-/Hostnamen, fuer
# den Caddy sein Cert hat. Zuhause / normal: leer lassen.
# RVS_HOST=172.0.2.34
# RVS_SNI=mobil.hacker-net.de
RVS_SNI=
# ─── Optional ─────────────────────────────────────
# HF_TOKEN= # nur falls ein HF-gated Modell (z.B. Voxtral) geladen wird
+4
View File
@@ -58,6 +58,10 @@ GPU-Test + Stack-Start.
24-GB-Karte: `docker compose stop whisper-bridge && docker compose --profile voxtral up -d --build`.
- **Erster Start lädt Modelle** (mehrere GB via HuggingFace nach `xtts/hf-cache`
+ `xtts/models`) — genug Platz (1 TB NVMe ✓) und etwas Geduld.
- **Box im selben Netz wie der RVS** (z.B. Rechenzentrum, direkt auf die interne
IP statt NAT-Hairpin): in `xtts/.env` `RVS_HOST=<interne-ip>` + **`RVS_SNI=<zert-name>`**
(Name, für den das Caddy-Zert gilt). Ohne das scheitert TLS an `tlsv1 alert
internal error`. Zuhause / normal: `RVS_SNI` leer lassen.
## Verifizieren
+10 -2
View File
@@ -51,6 +51,9 @@ RVS_PORT = int(os.getenv("RVS_PORT", "443"))
RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true"
RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true"
RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip()
# TLS-Hostname (SNI + Cert), falls RVS_HOST eine IP ist (Box im selben Netz wie
# der RVS, direkt auf die interne IP). Leer = SNI = RVS_HOST.
RVS_SNI = os.getenv("RVS_SNI", "").strip()
# F5-TTS Konfiguration
# ─────────────────────────────────────────────────────────────────
@@ -880,9 +883,14 @@ async def run_loop(runner: F5Runner) -> None:
url = f"{scheme}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}"
masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url
connect_kwargs = {"ping_interval": 20, "ping_timeout": 10, "max_size": 50 * 1024 * 1024}
if use_tls and RVS_SNI:
import ssl
connect_kwargs["ssl"] = ssl.create_default_context()
connect_kwargs["server_hostname"] = RVS_SNI
try:
logger.info("Verbinde zu RVS: %s", masked)
async with websockets.connect(url, ping_interval=20, ping_timeout=10, max_size=50 * 1024 * 1024) as ws:
logger.info("Verbinde zu RVS: %s%s", masked, f" (SNI {RVS_SNI})" if (use_tls and RVS_SNI) else "")
async with websockets.connect(url, **connect_kwargs) as ws:
logger.info("RVS verbunden")
retry_s = 2
tls_fallback_tried = False
+11 -4
View File
@@ -43,6 +43,9 @@ RVS_PORT = os.getenv("RVS_PORT", "443").strip()
RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true"
RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true"
RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip()
# TLS-Hostname (SNI + Cert), falls RVS_HOST eine IP ist (Box im selben Netz wie
# der RVS, direkt auf die interne IP). Leer = SNI = RVS_HOST.
RVS_SNI = os.getenv("RVS_SNI", "").strip()
LLAMA_URL = os.getenv("LLAMA_URL", "http://llama:8081").rstrip("/")
LLM_MODEL = os.getenv("LLM_MODEL", "qwen3-8b")
@@ -419,11 +422,15 @@ async def _run() -> None:
scheme = "wss" if use_tls else "ws"
url = f"{scheme}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}"
masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url
connect_kwargs = {"ping_interval": 20, "ping_timeout": 10, "max_size": 16 * 1024 * 1024}
if use_tls and RVS_SNI:
import ssl
connect_kwargs["ssl"] = ssl.create_default_context()
connect_kwargs["server_hostname"] = RVS_SNI
try:
logger.info("Verbinde zu RVS: %s (llama=%s)", masked, LLAMA_URL)
async with websockets.connect(
url, ping_interval=20, ping_timeout=10, max_size=16 * 1024 * 1024
) as ws:
logger.info("Verbinde zu RVS: %s%s (llama=%s)", masked,
f" (SNI {RVS_SNI})" if (use_tls and RVS_SNI) else "", LLAMA_URL)
async with websockets.connect(url, **connect_kwargs) as ws:
logger.info("RVS verbunden — llm-adapter online")
retry_s = 2
tls_fallback_tried = False
+10 -3
View File
@@ -53,6 +53,9 @@ RVS_PORT = int(os.getenv("RVS_PORT", "443"))
RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true"
RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true"
RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip()
# TLS-Hostname (SNI + Cert), falls RVS_HOST eine IP ist (Box im selben Netz wie
# der RVS, direkt auf die interne IP). Leer = SNI = RVS_HOST.
RVS_SNI = os.getenv("RVS_SNI", "").strip()
VOXTRAL_MODEL = os.getenv("VOXTRAL_MODEL", "mistralai/Voxtral-Mini-3B-2507")
VOXTRAL_LANGUAGE = os.getenv("VOXTRAL_LANGUAGE", "de")
@@ -744,10 +747,14 @@ async def run_loop(sessions: SessionManager) -> None:
scheme = "wss" if use_tls else "ws"
url = f"{scheme}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}"
masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url
connect_kwargs = {"ping_interval": 20, "ping_timeout": 10, "max_size": 50 * 1024 * 1024}
if use_tls and RVS_SNI:
import ssl
connect_kwargs["ssl"] = ssl.create_default_context()
connect_kwargs["server_hostname"] = RVS_SNI
try:
logger.info("Verbinde zu RVS: %s", masked)
async with websockets.connect(url, ping_interval=20, ping_timeout=10,
max_size=50 * 1024 * 1024) as ws:
logger.info("Verbinde zu RVS: %s%s", masked, f" (SNI {RVS_SNI})" if (use_tls and RVS_SNI) else "")
async with websockets.connect(url, **connect_kwargs) as ws:
logger.info("RVS verbunden")
retry_s = 2
tls_fallback_tried = False
+10 -2
View File
@@ -53,6 +53,9 @@ RVS_PORT = int(os.getenv("RVS_PORT", "443"))
RVS_TLS = os.getenv("RVS_TLS", "true").lower() == "true"
RVS_TLS_FALLBACK = os.getenv("RVS_TLS_FALLBACK", "true").lower() == "true"
RVS_TOKEN = os.getenv("RVS_TOKEN", "").strip()
# TLS-Hostname (SNI + Cert), falls RVS_HOST eine IP ist (Box im selben Netz wie
# der RVS, direkt auf die interne IP). Leer = SNI = RVS_HOST.
RVS_SNI = os.getenv("RVS_SNI", "").strip()
WHISPER_MODEL = os.getenv("WHISPER_MODEL", "small")
WHISPER_DEVICE = os.getenv("WHISPER_DEVICE", "cuda")
@@ -876,9 +879,14 @@ async def run_loop(runner: WhisperRunner, sessions: SessionManager) -> None:
scheme = "wss" if use_tls else "ws"
url = f"{scheme}://{RVS_HOST}:{RVS_PORT}?token={RVS_TOKEN}"
masked = url.replace(RVS_TOKEN, "***") if RVS_TOKEN else url
connect_kwargs = {"ping_interval": 20, "ping_timeout": 10, "max_size": 50 * 1024 * 1024}
if use_tls and RVS_SNI:
import ssl
connect_kwargs["ssl"] = ssl.create_default_context()
connect_kwargs["server_hostname"] = RVS_SNI
try:
logger.info("Verbinde zu RVS: %s", masked)
async with websockets.connect(url, ping_interval=20, ping_timeout=10, max_size=50 * 1024 * 1024) as ws:
logger.info("Verbinde zu RVS: %s%s", masked, f" (SNI {RVS_SNI})" if (use_tls and RVS_SNI) else "")
async with websockets.connect(url, **connect_kwargs) as ws:
logger.info("RVS verbunden")
retry_s = 2
tls_fallback_tried = False