#!/bin/bash
# shellcheck disable=SC2088,SC1111
# ==========================================================================
#  saarwerk-lab.sh · Laborskript zum Buch „Linux-Administration lernen“
#  Fehler finden. Systeme reparieren. Sicher betreiben.
#
#  Holger Reibold · Brain-Media.de · Begleitmaterial zu AdminMission
#  Version 0.9 · Missionen 1 bis 50
#
#  Das Skript verwandelt eine frisch installierte Debian-13-VM in den
#  Server saarwerk-web01 der fiktiven Saarwerk Maschinenbau GmbH. Es baut
#  auf Wunsch den Fehler einer Mission ein und prüft hinterher, ob Sie ihn
#  behoben haben.
#
#    sudo ./saarwerk-lab.sh install      Saarwerk-Umgebung einrichten
#    sudo ./saarwerk-lab.sh mission 32   Fehler aus Mission 32 einbauen
#    sudo ./saarwerk-lab.sh check 32     Ziele von Mission 32 prüfen
#    sudo ./saarwerk-lab.sh reset        Eingebaute Fehler zurücknehmen
#    sudo ./saarwerk-lab.sh list         Alle Missionen anzeigen
#    sudo ./saarwerk-lab.sh status       Zustand des Labors anzeigen
#
#  Bei Missionen mit einer Antwort geben Sie diese beim Prüfen mit an:
#    sudo ./saarwerk-lab.sh check 1 saarwerk-web01
#
#  Wichtig: Das Skript verändert das System tiefgreifend (Rechnername,
#  Netz, Firewall, Benutzer, Dienste). Verwenden Sie es nur in einer
#  eigenen Übungs-VM, niemals auf einem produktiven Server. Legen Sie vor
#  der Installation einen Schnappschuss der VM an.
#
#  Lesen Sie das Skript, bevor Sie es mit root-Rechten ausführen. Das gilt
#  für jedes Skript aus dem Internet, auch für dieses.
#
#  So ist das Labor aufgebaut
#  --------------------------
#  Der Server hat zusätzlich zu seiner echten Netzwerkkarte eine
#  Schnittstelle eth0 im Saarwerk-Netz 10.20.0.0/24. Dahinter simuliert
#  das Skript in zwei Netzwerk-Namensräumen (ip netns) die übrigen
#  Rechner der Firma: Gateway 10.20.0.1, Nameserver 10.20.0.2, Intranet,
#  Paket-Repository, Monitoring, den Debian-Mirror mirror.example.org und
#  den Prüfstand im Labornetz 10.30.0.0/24. Was Sie dort mit ip, ping,
#  curl, dig oder ufw beobachten, passiert tatsächlich im Kernel.
#
#  Ihr eigener Internetzugang und Ihre SSH-Verbindung laufen weiter über
#  die echte Netzwerkkarte. Eine Regel im Policy-Routing (ip rule) sorgt
#  dafür, dass nur Ziele im Saarwerk-Netz über eth0 laufen. Deshalb
#  sperren Sie sich auch bei den Netzwerkmissionen nicht aus.
# ==========================================================================

set -o pipefail
umask 022
export LC_ALL=C.UTF-8 LANG=C.UTF-8

VERSION='0.9'
START_PWD=$PWD

LAB_USER=trainee
STATE=/var/lib/saarwerk-lab
CONF=/etc/saarwerk-lab
LIBDIR=/usr/local/lib/saarwerk-lab
SELF_INSTALLED=/usr/local/sbin/saarwerk-lab
NS_NETZ=sw-netz
NS_LABOR=sw-labor
HOST=saarwerk-web01
DOMAIN=saarwerk.test
IP_WEB=10.20.0.15
IP_GW=10.20.0.1
IP_NS=10.20.0.2
UPLINK_TABLE=120
MISSION_MAX=50

# ---------------------------------------------------------------- Ausgabe

if [ -t 1 ]; then
  C_OK=$'\e[32m'; C_ERR=$'\e[31m'; C_HI=$'\e[1m'; C_DIM=$'\e[2m'; C_0=$'\e[0m'
else
  C_OK=''; C_ERR=''; C_HI=''; C_DIM=''; C_0=''
fi

info()  { printf '%s\n' "$*"; }
schritt() { printf '%s==>%s %s\n' "$C_HI" "$C_0" "$*"; }
warnung() { printf '%sWarnung:%s %s\n' "$C_ERR" "$C_0" "$*" >&2; }
abbruch() { printf '%sFehler:%s %s\n' "$C_ERR" "$C_0" "$*" >&2; exit 1; }
leise() { "$@" >/dev/null 2>&1; }

# Schreibt Text aus der Standardeingabe in eine Datei und setzt Besitz und Rechte.
# datei PFAD BESITZER:GRUPPE MODUS
datei() {
  local p=$1 og=$2 m=$3
  mkdir -p "$(dirname "$p")"
  cat > "$p"
  chown "$og" "$p"
  chmod "$m" "$p"
}
# verzeichnis PFAD BESITZER:GRUPPE MODUS
verzeichnis() {
  mkdir -p "$1"
  chown "$2" "$1"
  chmod "$3" "$1"
}
# Legt eine Datei bestimmter Größe an, ohne Platz zu belegen (sparse).
leerdatei() {
  local p=$1 og=$2 m=$3 groesse=$4
  mkdir -p "$(dirname "$p")"
  : > "$p"
  [ "$groesse" != 0 ] && truncate -s "$groesse" "$p"
  chown "$og" "$p"
  chmod "$m" "$p"
}

home_von() { getent passwd "$1" | cut -d: -f6; }
TH() { home_von "$LAB_USER"; }

als_trainee() { setpriv --reuid="$LAB_USER" --regid="$LAB_USER" --init-groups env -i HOME="$(TH)" USER="$LAB_USER" LOGNAME="$LAB_USER" PATH=/usr/local/bin:/usr/bin:/bin "$@"; }
als() { local u=$1; shift; setpriv --reuid="$u" --regid="$(id -gn "$u")" --init-groups "$@"; }

ns() { local n=$1; shift; ip netns exec "$n" "$@"; }

# Interaktive Shell von trainee befragen (liest ~/.bashrc).
trainee_shell() { als_trainee bash -ic "$1" 2>/dev/null </dev/null; }

sha() { printf '%s' "$1" | sha256sum | cut -c1-16; }

# ==========================================================================
#  Daten der Saarwerk-Welt (identisch mit dem Spiel)
# ==========================================================================

nginx_site() {
cat <<'EOF'
server {
    listen 80 default_server;
    listen 443 ssl;
    server_name saarwerk-web01.saarwerk.test portal.saarwerk.test;

    ssl_certificate     /etc/ssl/saarwerk/web01.crt;
    ssl_certificate_key /etc/ssl/saarwerk/web01.key;

    root /var/www/saarwerk;
    index index.html;

    location / {
        try_files $uri $uri/ =404;
    }

    location /uploads/ {
        client_max_body_size 20m;
    }
}
EOF
}

api_conf() {
cat <<EOF
# Saarwerk API · Konfiguration
# Nach Änderungen: systemctl restart saarwerk-api

bind = ${1:-$IP_WEB}
port = 8080
log_level = info
log_file = /var/log/saarwerk/api.log
EOF
}

api_unit() {
cat <<'EOF'
[Unit]
Description=Saarwerk API (Auftragsdaten)
After=network-online.target

[Service]
User=nwapi
ExecStart=/opt/saarwerk/bin/saarwerk-api --config /etc/saarwerk/api.conf
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF
}

motd_text() {
printf '\nSaarwerk GmbH · saarwerk-web01\nNur für autorisierte Personen. Zugriffe werden protokolliert.\n\n'
}

hosts_text() {
cat <<EOF
127.0.0.1	localhost
127.0.1.1	${1:-$HOST}.$DOMAIN	${1:-$HOST}

# The following lines are desirable for IPv6 capable hosts
::1     localhost ip6-localhost ip6-loopback
ff02::1 ip6-allnodes
ff02::2 ip6-allrouters
EOF
}

resolv_text() {
cat <<'EOF'
# Saarwerk-IT · statische Resolver-Konfiguration
search saarwerk.test
nameserver 10.20.0.2
EOF
}

# Saarwerk-Block in /etc/network/interfaces
interfaces_block() {
cat <<EOF
# Saarwerk · statische Konfiguration
auto eth0
iface eth0 inet static
    address ${1:-$IP_WEB}/24
    gateway $IP_GW
EOF
}

backup_script() {
cat <<'EOF'
#!/bin/bash
# Saarwerk · Prüfung der nächtlichen Sicherungen
set -euo pipefail
SOLL=14
IST=$(ls /srv/backup/nightly | wc -l)
echo "Backup-Prüfung Saarwerk"
echo "  $IST von $SOLL Sicherungen vorhanden"
[ "$IST" -eq "$SOLL" ] && echo "Status: OK" || echo "Status: FEHLER"
EOF
}

inventur_2025() {
cat <<'EOF'
name;standort;system;status
saarwerk-web01;RZ Halle 2;Debian 13;aktiv
saarwerk-db01;RZ Halle 2;Debian 12;aktiv
saarwerk-files;RZ Halle 2;Windows Server 2022;aktiv
saarwerk-erp;RZ Halle 2;SLES 12 SP5;EOL
saarwerk-print;Verwaltung;Windows Server 2012 R2;EOL
saarwerk-mon;RZ Halle 2;Debian 13;aktiv
saarwerk-backup;Halle 5;Debian 12;aktiv
saarwerk-cam;Pforte;Ubuntu 18.04;EOL
saarwerk-legacy;Keller;Debian 10;EOL
EOF
}

inventur_2024() {
cat <<'EOF'
name;standort;system;status
saarwerk-web01;RZ Halle 2;Debian 12;aktiv
saarwerk-files;RZ Halle 2;Windows Server 2016;aktiv
saarwerk-erp;RZ Halle 2;SLES 12 SP5;EOL
saarwerk-print;Verwaltung;Windows Server 2012 R2;EOL
saarwerk-cam;Pforte;Ubuntu 18.04;EOL
saarwerk-legacy;Keller;Debian 10;EOL
saarwerk-fax;Verwaltung;Windows Server 2008;EOL
saarwerk-zeit;Halle 1;Windows 7;EOL
EOF
}

app_log() {
cat <<'EOF'
2026-10-19 00:00:01 INFO [cron] Nachtlauf gestartet
2026-10-19 00:15:22 INFO [export] Export Auftragsdaten begonnen
2026-10-19 00:47:10 WARN [mail] Zustellung verzögert: mx.saarwerk.test antwortet langsam
2026-10-19 01:02:44 ERROR [export] Verbindung zu repo.saarwerk.test abgebrochen
2026-10-19 01:30:00 INFO [backup] Sicherung abgeschlossen (14 Sätze)
2026-10-19 01:42:13 ERROR [export] Wiederholung 1/3 fehlgeschlagen
2026-10-19 02:13:44 ERROR [api] Zeitüberschreitung bei Anfrage /v1/auftraege
2026-10-19 02:42:13 ERROR [export] Wiederholung 2/3 fehlgeschlagen
2026-10-19 03:00:02 INFO [reporting] Monatsbericht gestartet
2026-10-19 03:42:13 ERROR [export] Wiederholung 3/3 fehlgeschlagen
2026-10-19 03:42:14 ERROR [export] Export abgebrochen, Daten unvollständig
2026-10-19 04:05:51 ERROR [mail] Zustellung an buchhaltung@saarwerk.test endgültig fehlgeschlagen
2026-10-19 05:12:09 WARN [api] Antwortzeit über 2000 ms
2026-10-19 05:31:37 ERROR [api] Datenbankverbindung zurückgesetzt
2026-10-19 06:58:05 INFO [api] Dienst gestartet
EOF
}

olaf_history() {
cat <<'EOF'
ls -la
df -h
sudo apt update
sudo apt upgrade -y
systemctl status nginx
cd /etc/nginx
ls sites-enabled/
sudo cp sites-enabled/saarwerk /root/saarwerk.vorher
sudo nano /etc/nginx/sites-enabled/saarwerk
sudo nginx -t
sudo systemctl reload nginx
curl -I http://localhost
cd
journalctl -u saarwerk-api -n 20
sudo systemctl restart saarwerk-api
tail -f /var/log/saarwerk/app.log
crontab -l
sudo ufw status
du -sh /var/log/*
exit
EOF
}

telefonliste() {
cat <<'EOF'
name;abteilung;durchwahl
Abel;Konstruktion;210
Brandt;Personal;213
Celik;Fertigung;216
Demir;Konstruktion;219
Engel;Einkauf;222
Fischer;Fertigung;225
Graf;Buchhaltung;228
Hoffmann;IT;231
Ivanova;Qualitätssicherung;234
Jung;Vertrieb;237
Keller;Vertrieb;240
Lang;Logistik;243
Meier;Fertigung;246
Nowak;Logistik;249
Otto;Einkauf;252
Petersen;Buchhaltung;255
Roth;IT;258
Schulz;Fertigung;261
Tran;Konstruktion;264
Ulrich;Vertrieb;267
Vogel;Qualitätssicherung;270
Weber;Fertigung;273
Yilmaz;Logistik;276
Zander;Personal;279
EOF
}

ABTEILUNGEN='Buchhaltung|Einkauf|Fertigung|IT|Konstruktion|Logistik|Personal|Qualitätssicherung|Vertrieb'

EXPORT_DATEIEN=(export-2026-09-29.csv export-2026-09-30.csv export-2026-10-01.csv export-2026-10-02.csv
  export-2026-10-05.csv export-2026-10-06.csv export-2026-10-07.csv)

EINGANG_PDF=(rechnung_0815.pdf angebot_4711.pdf lieferschein_22.pdf)
EINGANG_BILDER=(foto_halle2.jpg logo.png IMG_2041.JPG)
EINGANG_DOK=(protokoll_0910.odt vertrag_entwurf.docx)

BETA_BEHALTEN=(README.md plan/meilensteine.odt daten/messung_01.csv daten/messung_02.csv bericht.tmp.odt notizen.txt)
BETA_TMP=(daten/messung_01.csv.tmp daten/cache/a1.tmp daten/cache/a2.tmp plan/~lock.meilensteine.odt.tmp .~sitzung.tmp)

# Pseudozufall wie im Spiel: x = (x * 1103515245 + 12345) mod 2^31.
# Das Spiel rechnet in JavaScript mit Gleitkommazahlen (double). awk tut
# dasselbe, deshalb entstehen hier exakt dieselben Protokolle wie im Spiel.
AWK_RNG='function r() { x = (x * 1103515245 + 12345) % 2147483648; return x / 2147483648 }'

# Importprotokoll von Mission 9 (4989 Zeilen, Prüfsumme in der letzten)
import_log() {
  awk "$AWK_RNG"'
    function zeit(ms,  s) { s = int(ms / 1000); return sprintf("2026-10-19 %02d:%02d:%02d", 2 + int(s / 3600), int(s / 60) % 60, s % 60) }
    BEGIN {
      x = 7; t = 0
      print zeit(0) " INFO import: Lauf gestartet, Quelle repo.saarwerk.test/erp/export.csv"
      for (i = 1; i <= 4987; i++) {
        t += 900 + int(r() * 900)
        if (i % 613 == 0) print zeit(t) " WARN import: Datensatz " i " ohne Kostenstelle, übernommen mit 0000"
        else print zeit(t) " INFO import: Datensatz " i " übernommen"
      }
      print zeit(t + 4000) " INFO import: Lauf beendet, 4987 Datensätze, Prüfsumme 7F3A-22C1"
    }'
}

# Zugriffsprotokoll von Mission 20 (437 Anfragen im Oktober)
access_log() {
  awk "$AWK_RNG"'
    BEGIN {
      split("10.20.0.23 10.20.0.31 10.20.0.31 10.20.0.31 10.20.0.44 10.20.0.57 10.20.0.31 10.20.0.23 10.20.0.68 10.20.0.50", ips, " ")
      split("/ / /index.html /uploads/ /portal/auftraege /portal/urlaub /favicon.ico /alt/formular.php /wp-login.php /hilfe", pfade, " ")
      x = 42
      for (i = 0; i < 437; i++) {
        tag = 1 + int(i / 23); ip = ips[1 + int(r() * 10)]; pfad = pfade[1 + int(r() * 10)]
        if (pfad == "/alt/formular.php" || pfad == "/wp-login.php" || pfad == "/favicon.ico") st = 404
        else if (pfad == "/uploads/" && r() < 0.3) st = 413
        else st = 200
        hh = 7 + int(r() * 10); mm = int(r() * 60); ss = int(r() * 60)
        groesse = (st == 200) ? 800 + int(r() * 9000) : 162
        printf "%s - - [%02d/Oct/2026:%02d:%02d:%02d +0200] \"%s %s HTTP/1.1\" %d %d \"-\" \"Mozilla/5.0 (X11; Linux x86_64)\"\n", ip, tag, hh, mm, ss, (pfad == "/uploads/" ? "POST" : "GET"), pfad, st, groesse
      }
    }'
}
ACCESS_N=437

# ==========================================================================
#  Kleine Programme der Saarwerk-Welt
#  Sie ersetzen die Firmensoftware durch einfache Attrappen in Python,
#  verhalten sich gegenüber Ihnen aber wie echte Dienste.
# ==========================================================================

# Die Saarwerk-API: liest api.conf, lauscht auf bind:port, schreibt ins Log.
prog_api() {
cat <<'EOF'
#!/usr/bin/python3
# Saarwerk API 3.4 (Labor-Attrappe zum Buch "Linux-Administration lernen")
import argparse, json, os, socket, sys, time
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

a = argparse.ArgumentParser()
a.add_argument('--config', default='/etc/saarwerk/api.conf')
args = a.parse_args()

# Unter systemd erhalten Fehlermeldungen das Präfix <3> (Priorität err).
PRIO = '<3>' if os.environ.get('JOURNAL_STREAM') else ''

def fatal(*zeilen):
    for z in zeilen:
        print(PRIO + 'saarwerk-api: ' + z, flush=True, file=sys.stdout if PRIO else sys.stderr)
    sys.exit(1)

conf = {'bind': '127.0.0.1', 'port': '8080', 'log_file': '/var/log/saarwerk/api.log'}
try:
    with open(args.config, encoding='utf-8') as f:
        for zeile in f:
            zeile = zeile.split('#', 1)[0].strip()
            if '=' in zeile:
                k, v = (t.strip() for t in zeile.split('=', 1))
                conf[k] = v
except OSError as e:
    fatal('Konfiguration %s nicht lesbar: %s' % (args.config, e.strerror))

log = conf['log_file']
st = os.statvfs(os.path.dirname(log) or '/')
if st.f_bavail * st.f_frsize < 4 * 1024 * 1024:
    fatal('open %s: no space left on device' % log, 'fatal: Protokolldatei nicht beschreibbar, Abbruch')
if not conf['port'].isdigit():
    fatal('Konfigurationsfehler: port ist keine Zahl')
bind, port = conf['bind'], int(conf['port'])

class Api(BaseHTTPRequestHandler):
    server_version = 'saarwerk-api/3.4'
    sys_version = ''
    def do_GET(self):
        body = json.dumps({'status': 'ok', 'service': 'saarwerk-api', 'version': '3.4.1'}).encode()
        self.send_response(200)
        self.send_header('Content-Type', 'application/json')
        self.send_header('Content-Length', str(len(body)))
        self.end_headers()
        if self.command != 'HEAD':
            self.wfile.write(body)
    do_HEAD = do_GET
    def log_message(self, *a):
        pass

try:
    srv = ThreadingHTTPServer((bind, port), Api)
except OSError as e:
    grund = {98: 'address already in use', 99: 'cannot assign requested address', 13: 'permission denied'}.get(e.errno, e.strerror)
    fatal('listen tcp %s:%d: bind: %s' % (bind, port, grund))
try:
    with open(log, 'a', encoding='utf-8') as f:
        f.write(time.strftime('%Y-%m-%d %H:%M:%S') + ' INFO api: gestartet auf %s:%d\n' % (bind, port))
except OSError as e:
    fatal('open %s: %s' % (log, e.strerror.lower()), 'fatal: Protokolldatei nicht beschreibbar, Abbruch')
print('saarwerk-api: Listening on %s:%d' % (bind, port), flush=True)
srv.serve_forever()
EOF
}

# Export-Dienst (Mission 36): hält seine Debug-Logdatei dauerhaft geöffnet.
prog_export() {
cat <<'EOF'
#!/usr/bin/python3
# Saarwerk Datenexport (Labor-Attrappe): schreibt im Debug-Modus fleißig mit.
import sys, time
pfad = '/var/log/saarwerk/export-debug.log'
f = open(pfad, 'a', encoding='utf-8')
print('nw-export: Debug-Protokoll ' + pfad, flush=True)
while True:
    try:
        f.write(time.strftime('%Y-%m-%d %H:%M:%S') + ' DEBUG export: warte auf Auftragsdaten\n')
        f.flush()
    except OSError:
        pass
    time.sleep(30)
EOF
}

# Abgleichsdienst (Mission 35): scheitert mit echten Fehlermeldungen.
prog_sync() {
cat <<'EOF'
#!/bin/sh
# Saarwerk Abgleich mit dem ERP (Labor-Attrappe)
p=''; [ -n "$JOURNAL_STREAM" ] && p='<3>'
echo "${p}nw-sync: Zertifikat von repo.saarwerk.test abgelaufen am 18.10.2026"
echo "${p}nw-sync: Abgleich abgebrochen, keine Daten übertragen"
exit 2
EOF
}

# Berichtsgenerator (Mission 34): rechnet und rechnet und rechnet.
prog_reportgen() {
cat <<'EOF'
#!/bin/bash
# Saarwerk Reporting · Monatsbericht (Labor-Attrappe)
while :; do :; done
EOF
}

# Die übrigen Rechner im Saarwerk-Netz: einfache Web-Server je Adresse.
prog_netzsim() {
cat <<'EOF'
#!/usr/bin/python3
# Saarwerk-Labor: simulierte Web-Server im Namensraum sw-netz bzw. sw-labor
import ssl, sys, threading
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

SEITEN = {
    'netz': [
        ('10.20.0.30', 80, False, 'nginx', '<h1>Saarwerk Intranet</h1>'),
        ('10.20.0.30', 443, True, 'nginx', '<h1>Saarwerk Intranet</h1>'),
        ('10.20.0.40', 443, True, 'nginx', 'Saarwerk Paket-Repository'),
        ('10.20.0.50', 443, True, 'nginx', 'Saarwerk Monitoring'),
        ('203.0.113.10', 80, False, 'Apache', 'Debian Mirror'),
        ('203.0.113.10', 443, True, 'Apache', 'Debian Mirror'),
    ],
    'labor': [
        ('10.30.0.10', 80, False, 'Pruefstand-PS3', 'Prüfstand 3 · Messwerte'),
    ],
}
ZERT = '/etc/saarwerk-lab/netz.crt'
SCHL = '/etc/saarwerk-lab/netz.key'

def handler(name, text):
    body = (text + '\n').encode('utf-8')
    class H(BaseHTTPRequestHandler):
        def version_string(self):
            return name
        def do_GET(self):
            self.send_response(200)
            self.send_header('Content-Type', 'text/html; charset=utf-8')
            self.send_header('Content-Length', str(len(body)))
            self.end_headers()
            if self.command != 'HEAD':
                self.wfile.write(body)
        do_HEAD = do_GET
        def log_message(self, *a):
            pass
    return H

def starte(ip, port, tls, name, text):
    srv = ThreadingHTTPServer((ip, port), handler(name, text))
    if tls:
        ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
        ctx.load_cert_chain(ZERT, SCHL)
        srv.socket = ctx.wrap_socket(srv.socket, server_side=True)
    threading.Thread(target=srv.serve_forever, daemon=True).start()

for s in SEITEN[sys.argv[1]]:
    starte(*s)
print('Saarwerk-Netz: %d Web-Server bereit' % len(SEITEN[sys.argv[1]]), flush=True)
threading.Event().wait()
EOF
}

# ==========================================================================
#  Das simulierte Saarwerk-Netz
#
#    Server (dieser Rechner)            Namensraum sw-netz
#    eth0 (Bridge) 10.20.0.15  ───────  lan0  10.20.0.1   Gateway
#          │                                  10.20.0.2   Nameserver (dnsmasq)
#          │                                  10.20.0.23  ws-lena
#          │                                  10.20.0.30  intranet
#          │                                  10.20.0.31  Arbeitsplatz Lena
#          │                                  10.20.0.40  repo
#          │                                  10.20.0.50  monitoring
#          │                                  10.20.10.1  Gateway Server-Netz
#          │                            lo    203.0.113.10 mirror.example.org
#          │                            up0   169.254.20.2 Ausgang ins Internet
#          │                                  (nur für den Nameserver)
#          │                          Namensraum sw-labor
#          └──────────────────────────  lan0  10.20.0.254 Laborrouter
#                                       lo    10.30.0.10  Prüfstand 3
#
#  Die echte Netzwerkkarte bleibt unangetastet. Ziele im Saarwerk-Netz
#  (10.20.0.0/16, 10.30.0.0/16, 203.0.113.0/24) entscheidet die
#  Routingtabelle main, alle anderen Ziele die Tabelle uplink (120) mit
#  der echten Standardroute.
# ==========================================================================

SAARWERK_NETZE=(10.20.0.0/16 10.30.0.0/16 203.0.113.0/24)

uplink_lesen() { REAL_IF=''; REAL_GW=''; [ -r "$CONF/uplink" ] && . "$CONF/uplink"; }

# Echte Standardroute in die Tabelle uplink übernehmen und aus main entfernen.
uplink_pflegen() {
  uplink_lesen
  [ -n "$REAL_IF" ] || return 0
  ip link show "$REAL_IF" >/dev/null 2>&1 || return 0
  local gw
  gw=$(ip -4 route show default dev "$REAL_IF" 2>/dev/null | awk '/via/ {print $3; exit}')
  if [ -n "$gw" ] && [ "$gw" != "$REAL_GW" ]; then
    REAL_GW=$gw
    printf 'REAL_IF=%s\nREAL_GW=%s\n' "$REAL_IF" "$REAL_GW" > "$CONF/uplink"
  fi
  if [ -n "$REAL_GW" ]; then
    ip route replace default via "$REAL_GW" dev "$REAL_IF" table "$UPLINK_TABLE" 2>/dev/null
  else
    ip route replace default dev "$REAL_IF" table "$UPLINK_TABLE" 2>/dev/null
  fi
  while ip -4 route show default dev "$REAL_IF" | grep -q .; do
    ip route del default dev "$REAL_IF" 2>/dev/null || break
  done
  return 0
}

regel() { # regel PRIORITÄT REST...
  local p=$1; shift
  ip rule show pref "$p" | grep -q . || ip rule add pref "$p" "$@"
}

ipt() { # ipt TABELLE KETTE REGEL...: Regel einfügen, falls sie fehlt
  local t=$1 k=$2; shift 2
  iptables -t "$t" -C "$k" "$@" 2>/dev/null || iptables -t "$t" -I "$k" 1 "$@"
}

veth_zu() { # veth_zu NAMENSRAUM HOST_SEITE
  local n=$1 h=$2
  if ! ip link show "$h" >/dev/null 2>&1; then
    ip link add "$h" type veth peer name lan0 netns "$n"
  fi
  ip link set "$h" master eth0 up
  ip -n "$n" link set lo up
  ip -n "$n" link set lan0 up
}

# Baut das Netz auf, soweit es fehlt. Ändert nichts an eth0 selbst.
netz_start() {
  leise modprobe bridge; leise modprobe veth
  if ! ip link show eth0 >/dev/null 2>&1; then
    ip link add eth0 type bridge
    ip link set eth0 address 52:54:00:3a:9c:15
  fi
  ip link set eth0 up

  for n in "$NS_NETZ" "$NS_LABOR"; do
    [ -e "/run/netns/$n" ] || ip netns add "$n"
  done

  # sw-netz: Gateway, Nameserver, Intranet, Repository, Monitoring, Mirror
  veth_zu "$NS_NETZ" sw-netz0
  local a
  if ! ip -n "$NS_NETZ" -4 addr show lan0 | grep -q "10.20.0.1/"; then
    ip -n "$NS_NETZ" addr add 10.20.0.1/24 dev lan0
  fi
  for a in 10.20.0.2 10.20.0.23 10.20.0.30 10.20.0.31 10.20.0.40 10.20.0.50; do
    ip -n "$NS_NETZ" addr replace "$a/24" dev lan0
  done
  ip -n "$NS_NETZ" addr replace 10.20.10.1/24 dev lan0
  ip -n "$NS_NETZ" addr replace 203.0.113.10/32 dev lo
  ns "$NS_NETZ" sysctl -qw net.ipv4.ip_forward=1 net.ipv4.conf.all.send_redirects=0 net.ipv4.conf.lan0.send_redirects=0
  ip -n "$NS_NETZ" route replace unreachable 10.0.0.0/8
  ip -n "$NS_NETZ" route replace unreachable 203.0.113.0/24

  # Ausgang des Nameservers ins Internet (über den Server, mit NAT)
  if ! ip link show sw-up >/dev/null 2>&1; then
    ip link add sw-up type veth peer name up0 netns "$NS_NETZ"
  fi
  ip addr replace 169.254.20.1/30 dev sw-up
  ip link set sw-up up
  ip -n "$NS_NETZ" addr replace 169.254.20.2/30 dev up0
  ip -n "$NS_NETZ" link set up0 up
  ip -n "$NS_NETZ" route replace default via 169.254.20.1 dev up0

  # sw-labor: Laborrouter und Prüfstand
  veth_zu "$NS_LABOR" sw-labor0
  ip -n "$NS_LABOR" addr replace 10.20.0.254/24 dev lan0
  ip -n "$NS_LABOR" addr replace 10.30.0.10/32 dev lo

  # Policy-Routing auf dem Server
  grep -qs "^$UPLINK_TABLE uplink" /etc/iproute2/rt_tables.d/saarwerk-lab.conf 2>/dev/null || {
    mkdir -p /etc/iproute2/rt_tables.d
    echo "$UPLINK_TABLE uplink" > /etc/iproute2/rt_tables.d/saarwerk-lab.conf
  }
  regel 900 lookup main suppress_prefixlength 0
  regel 905 iif sw-up lookup "$UPLINK_TABLE"
  local i=0 netz
  for netz in "${SAARWERK_NETZE[@]}"; do
    regel $(( 910 + i )) to "$netz" lookup main
    regel $(( 915 + i )) to "$netz" unreachable
    i=$(( i + 1 ))
  done
  regel 920 lookup "$UPLINK_TABLE"
  uplink_pflegen

  # Weiterleitung und NAT nur für den Nameserver-Ausgang
  sysctl -qw net.ipv4.ip_forward=1
  uplink_lesen
  if [ -n "$REAL_IF" ]; then
    ipt nat POSTROUTING -s 169.254.20.0/30 -o "$REAL_IF" -j MASQUERADE
  fi
  if ! iptables -C ufw-before-forward -i sw-up -j ACCEPT 2>/dev/null; then
    ipt filter FORWARD -o sw-up -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
    ipt filter FORWARD -i sw-up -j ACCEPT
  fi
  return 0
}

netz_ok() {
  ip link show eth0 >/dev/null 2>&1 && [ -e "/run/netns/$NS_NETZ" ] && [ -e "/run/netns/$NS_LABOR" ] &&
    ip -n "$NS_NETZ" -4 addr show lan0 2>/dev/null | grep -q 10.20.0.2/ &&
    ip rule show pref 920 | grep -q .
}

netz_dienste_starten() {
  local d
  for d in saarwerk-lab-dns saarwerk-lab-web saarwerk-lab-pruefstand; do
    systemctl is-active -q "$d" || leise systemctl restart "$d"
  done
}

# Stellt eth0 auf den Ausgangszustand: 10.20.0.15/24, Standardroute über 10.20.0.1.
eth0_grundzustand() {
  local soll_addr soll_rt ist_addr ist_rt
  soll_addr="$IP_WEB/24"
  ist_addr=$(ip -4 -o addr show dev eth0 2>/dev/null | awk '{print $4}' | sort | tr '\n' ' ')
  ist_rt=$(ip -4 route show dev eth0 2>/dev/null | grep -v 'proto kernel' | sort | tr '\n' ' ')
  soll_rt="default via $IP_GW "
  if [ "$ist_addr" = "$soll_addr " ] && [ "$ist_rt" = "$soll_rt" ]; then return 0; fi
  leise ifdown --force eth0
  ip addr flush dev eth0 2>/dev/null
  ip route flush dev eth0 2>/dev/null
  if ! leise ifup --force eth0; then
    ip addr replace "$IP_WEB/24" dev eth0
    ip route replace default via "$IP_GW" dev eth0
  fi
}

dnsmasq_conf() {
  local up
  echo '# Saarwerk-Labor · Nameserver ns1.saarwerk.test (10.20.0.2)'
  echo 'no-resolv'
  echo 'no-hosts'
  echo 'bind-interfaces'
  echo 'listen-address=10.20.0.2'
  echo 'user=nobody'
  echo 'group=nogroup'
  echo 'pid-file=/run/saarwerk-lab-dnsmasq.pid'
  echo 'local=/saarwerk.test/'
  while read -r up; do [ -n "$up" ] && echo "server=$up"; done < "$CONF/upstream-dns"
  cat <<'EOF'
address=/saarwerk-web01.saarwerk.test/10.20.0.15
address=/portal.saarwerk.test/10.20.0.15
address=/api.saarwerk.test/10.20.0.15
address=/gw.saarwerk.test/10.20.0.1
address=/ns1.saarwerk.test/10.20.0.2
address=/ws-lena.saarwerk.test/10.20.0.23
address=/intranet.saarwerk.test/10.20.0.30
address=/repo.saarwerk.test/10.20.0.40
address=/monitoring.saarwerk.test/10.20.0.50
address=/mirror.example.org/203.0.113.10
address=/update.example.org/203.0.113.10
EOF
}

units_schreiben() {
  datei /etc/systemd/system/saarwerk-lab-netz.service root:root 644 <<EOF
[Unit]
Description=Saarwerk-Labor: simuliertes Firmennetz (Bridge eth0, Namensräume)
DefaultDependencies=no
After=local-fs.target
Before=network-pre.target networking.service ufw.service
Wants=network-pre.target

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=$SELF_INSTALLED _netz-start

[Install]
WantedBy=multi-user.target
EOF
  datei /etc/systemd/system/saarwerk-lab-dns.service root:root 644 <<'EOF'
[Unit]
Description=Saarwerk-Labor: Nameserver ns1.saarwerk.test
Requires=saarwerk-lab-netz.service
After=saarwerk-lab-netz.service

[Service]
ExecStart=/usr/sbin/ip netns exec sw-netz /usr/sbin/dnsmasq -k --conf-file=/etc/saarwerk-lab/dnsmasq.conf
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF
  datei /etc/systemd/system/saarwerk-lab-web.service root:root 644 <<EOF
[Unit]
Description=Saarwerk-Labor: Intranet, Repository, Monitoring und Mirror
Requires=saarwerk-lab-netz.service
After=saarwerk-lab-netz.service

[Service]
ExecStart=/usr/sbin/ip netns exec sw-netz /usr/bin/python3 $LIBDIR/netzsim.py netz
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF
  datei /etc/systemd/system/saarwerk-lab-pruefstand.service root:root 644 <<EOF
[Unit]
Description=Saarwerk-Labor: Prüfstand 3 im Labornetz
Requires=saarwerk-lab-netz.service
After=saarwerk-lab-netz.service

[Service]
ExecStart=/usr/sbin/ip netns exec sw-labor /usr/bin/python3 $LIBDIR/netzsim.py labor
Restart=always
RestartSec=2

[Install]
WantedBy=multi-user.target
EOF
  datei /etc/systemd/system/saarwerk-api.service root:root 644 < <(api_unit)
  systemctl daemon-reload
}

# ==========================================================================
#  install: Saarwerk-Umgebung einrichten
# ==========================================================================

PAKETE=(nginx ufw lsof curl bind9-dnsutils rsync tree redis-server dnsmasq-base python3 openssl
  ifupdown iproute2 iptables cron logrotate sudo ca-certificates nano psmisc openssh-client openssh-server)

pruefe_voraussetzungen() {
  [ "$(id -u)" = 0 ] || abbruch "Bitte mit sudo aufrufen: sudo $0 $*"
  getent passwd "$LAB_USER" >/dev/null ||
    abbruch "Der Benutzer $LAB_USER fehlt. Legen Sie ihn wie in Kapitel 2 beschrieben an (adduser $LAB_USER) und starten Sie die Installation erneut."
  local os_id os_ver os_name
  os_id=$(. /etc/os-release 2>/dev/null; echo "${ID:-}")
  os_ver=$(. /etc/os-release 2>/dev/null; echo "${VERSION_ID:-}")
  os_name=$(. /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-unbekannt}")
  if [ "${1:-}" = install ] && { [ "$os_id" != debian ] || [ "$os_ver" != 13 ]; }; then
    warnung "Das Labor ist für Debian 13 gebaut, gefunden: $os_name. Abweichungen sind möglich."
  fi
}

uplink_ermitteln() {
  [ -s "$CONF/uplink" ] && return 0
  local zeile dev gw
  zeile=$(ip -4 route show default | grep -v ' dev eth0' | head -n1)
  [ -n "$zeile" ] || abbruch "Keine Standardroute gefunden. Hat die VM eine funktionierende Netzwerkverbindung?"
  dev=$(awk '{for (i = 1; i < NF; i++) if ($i == "dev") print $(i + 1)}' <<<"$zeile")
  gw=$(awk '{for (i = 1; i < NF; i++) if ($i == "via") print $(i + 1)}' <<<"$zeile")
  [ "$dev" = eth0 ] && abbruch "Die echte Netzwerkkarte heißt eth0. Das Labor braucht diesen Namen für das Saarwerk-Netz. Bitte verwenden Sie eine VM mit vorhersagbaren Schnittstellennamen (z. B. ens18 oder enp0s3)."
  printf 'REAL_IF=%s\nREAL_GW=%s\n' "$dev" "$gw" > "$CONF/uplink"
  # Überschneidet sich das echte Netz mit dem Saarwerk-Netz?
  local netz
  netz=$(ip -4 -o addr show dev "$dev" | awk '{print $4}')
  case $netz in
    10.20.*|10.30.*|203.0.113.*) abbruch "Ihr echtes Netz ($netz) überschneidet sich mit dem Saarwerk-Netz 10.20.0.0/16, 10.30.0.0/16. Bitte verwenden Sie ein anderes Netz für die VM." ;;
  esac
}

upstream_dns_ermitteln() {
  [ -s "$CONF/upstream-dns" ] && return 0
  local q ns liste=''
  for q in /etc/resolv.conf /run/systemd/resolve/resolv.conf; do
    [ -r "$q" ] || continue
    grep -q 'Saarwerk' "$q" && continue
    for ns in $(awk '/^nameserver/ {print $2}' "$q"); do
      case $ns in 127.*|::1|10.20.*|*:*) ;; *) liste+="$ns"$'\n' ;; esac
    done
  done
  uplink_lesen
  [ -z "$liste" ] && [ -n "$REAL_GW" ] && liste="$REAL_GW"$'\n'
  liste+=$'9.9.9.9\n'
  printf '%s' "$liste" | awk 'NF && !seen[$0]++' > "$CONF/upstream-dns"
}

zertifikate_erzeugen() {
  [ -s "$CONF/ca.crt" ] && [ -s /etc/ssl/saarwerk/web01.crt ] && return 0
  local t
  t=$(mktemp -d)
  openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj '/O=Saarwerk Labor/CN=Saarwerk Labor CA' \
    -keyout "$CONF/ca.key" -out "$CONF/ca.crt" >/dev/null 2>&1 || abbruch "openssl konnte die Labor-CA nicht erzeugen."
  chmod 600 "$CONF/ca.key"
  zert() { # zert NAME SCHLÜSSEL ZERT SAN
    openssl req -newkey rsa:2048 -nodes -subj "/O=Saarwerk Maschinenbau GmbH/CN=$1" -keyout "$2" -out "$t/req.csr" >/dev/null 2>&1
    printf 'subjectAltName=%s\nextendedKeyUsage=serverAuth\n' "$4" > "$t/ext"
    openssl x509 -req -in "$t/req.csr" -CA "$CONF/ca.crt" -CAkey "$CONF/ca.key" -CAcreateserial -days 3650 \
      -extfile "$t/ext" -out "$3" >/dev/null 2>&1
  }
  mkdir -p /etc/ssl/saarwerk
  zert saarwerk-web01.saarwerk.test /etc/ssl/saarwerk/web01.key /etc/ssl/saarwerk/web01.crt \
    'DNS:saarwerk-web01.saarwerk.test,DNS:portal.saarwerk.test,DNS:localhost,IP:10.20.0.15,IP:127.0.0.1'
  zert intranet.saarwerk.test "$CONF/netz.key" "$CONF/netz.crt" \
    'DNS:intranet.saarwerk.test,DNS:intranet,DNS:repo.saarwerk.test,DNS:repo,DNS:monitoring.saarwerk.test,DNS:monitoring,DNS:mirror.example.org,DNS:update.example.org,IP:10.20.0.30,IP:10.20.0.40,IP:10.20.0.50,IP:203.0.113.10'
  chmod 600 /etc/ssl/saarwerk/web01.key "$CONF/netz.key"
  cp "$CONF/ca.crt" /usr/local/share/ca-certificates/saarwerk-labor.crt
  leise update-ca-certificates
  rm -rf "$t"
}

# Benutzer anlegen: konto NAME KOMMENTAR [system]
konto() {
  if ! id "$1" >/dev/null 2>&1; then
    if [ "${3:-}" = system ]; then
      useradd --system --home-dir /opt/saarwerk --no-create-home --shell /usr/sbin/nologin --comment "$2" "$1"
    else
      useradd --create-home --shell /bin/bash --comment "$2" "$1"
      # Zufallskennwort: Das Konto ist gültig, aber niemand kennt das Kennwort.
      echo "$1:$(openssl rand -hex 16)" | chpasswd
    fi
  fi
}

benutzer_anlegen() {
  local g
  for g in projekt-alpha vertrieb; do getent group "$g" >/dev/null || groupadd "$g"; done
  konto nwapi 'Saarwerk API' system
  konto nwexport 'Saarwerk Export' system
  konto lena 'Lena Hoffmann,IT-Leitung,,'
  konto hr 'Personalabteilung,,,'
  konto reporting 'Reporting-Dienst,,,'
  konto aylin 'Aylin Demir,Projekt Alpha,,'
  konto jonas 'Jonas Keller,Vertrieb,,'
  konto olaf 'Olaf Brenner,IT,,'
}

ufw_vorbereiten() {
  # Weiterleitung für den Nameserver-Ausgang erlauben (siehe Netzplan oben)
  if [ -f /etc/ufw/sysctl.conf ]; then
    sed -i 's|^#\?net/ipv4/ip_forward=.*|net/ipv4/ip_forward=1|' /etc/ufw/sysctl.conf
  fi
  if [ -f /etc/ufw/before.rules ] && ! grep -q 'Saarwerk-Labor' /etc/ufw/before.rules; then
    sed -i '/^# End required lines/a\
\
# Saarwerk-Labor: Nameserver ns1 darf ins Internet (Schnittstelle sw-up)\
-A ufw-before-forward -i sw-up -j ACCEPT\
-A ufw-before-forward -o sw-up -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT' /etc/ufw/before.rules
  fi
  if [ -f /etc/default/ufw ]; then
    sed -i 's/^IPV6=.*/IPV6=yes/' /etc/default/ufw
  fi
}

dhcp_zaehmen() {
  # Der DHCP-Client soll weder /etc/resolv.conf überschreiben noch die
  # Standardroute zurück in die Tabelle main schreiben.
  if [ -d /etc/dhcp ]; then
    mkdir -p /etc/dhcp/dhclient-enter-hooks.d /etc/dhcp/dhclient-exit-hooks.d
    printf '# Saarwerk-Labor: resolv.conf nicht anfassen\nmake_resolv_conf() { :; }\n' > /etc/dhcp/dhclient-enter-hooks.d/saarwerk-lab
    printf '# Saarwerk-Labor: Standardroute in Tabelle uplink verschieben\n%s _uplink >/dev/null 2>&1 || true\n' "$SELF_INSTALLED" > /etc/dhcp/dhclient-exit-hooks.d/saarwerk-lab
  fi
  if [ -f /etc/dhcpcd.conf ] && ! grep -q 'Saarwerk-Labor' /etc/dhcpcd.conf; then
    printf '\n# Saarwerk-Labor: resolv.conf nicht anfassen\nnohook resolv.conf\n' >> /etc/dhcpcd.conf
  fi
  if [ -d /etc/dhcpcd.exit-hook ] || command -v dhcpcd >/dev/null 2>&1; then
    printf '# Saarwerk-Labor\n%s _uplink >/dev/null 2>&1 || true\n' "$SELF_INSTALLED" > /etc/dhcpcd.exit-hook
  fi
  mkdir -p /etc/network/if-up.d
  printf '#!/bin/sh\n# Saarwerk-Labor: echte Standardroute in Tabelle uplink verschieben\n%s _uplink >/dev/null 2>&1 || true\n' "$SELF_INSTALLED" > /etc/network/if-up.d/saarwerk-lab
  chmod 755 /etc/network/if-up.d/saarwerk-lab
}

cmd_install() {
  pruefe_voraussetzungen install
  info "${C_HI}Saarwerk-Labor $VERSION: Installation${C_0}"
  info "Das Skript richtet diese VM als saarwerk-web01 der Saarwerk Maschinenbau GmbH ein."
  echo

  mkdir -p "$STATE/orig" "$STATE/keys" "$CONF" "$LIBDIR"
  chmod 700 "$STATE" "$CONF"

  schritt "Pakete installieren"
  export DEBIAN_FRONTEND=noninteractive
  leise apt-get update
  apt-get install -y -q "${PAKETE[@]}" >/dev/null 2>"$STATE/apt.log" ||
    abbruch "Die Paketinstallation ist gescheitert, Details in $STATE/apt.log"

  schritt "Ausgangsdateien sichern"
  local f n
  for f in /etc/hosts /etc/resolv.conf /etc/network/interfaces /etc/motd /etc/nginx/sites-enabled/default; do
    n=$(echo "$f" | tr / _)
    if [ -e "$f" ] && [ ! -e "$STATE/orig/$n" ]; then cp -a "$f" "$STATE/orig/$n"; fi
  done
  if [ -L /etc/resolv.conf ]; then
    warnung "/etc/resolv.conf ist ein Link ($(readlink /etc/resolv.conf)). Das Labor ersetzt ihn durch eine statische Datei."
    leise systemctl disable --now systemd-resolved
    rm -f /etc/resolv.conf
  fi
  # Basis der interfaces-Datei ohne Saarwerk-Block
  if [ ! -e "$STATE/interfaces.basis" ]; then
    sed '/^# Saarwerk · statische Konfiguration/,$d' /etc/network/interfaces > "$STATE/interfaces.basis"
  fi
  # Redis-Konfiguration aus dem Paket sichern
  [ -e "$STATE/orig/redis.conf" ] || cp -a /etc/redis/redis.conf "$STATE/orig/redis.conf"

  schritt "Programm nach $SELF_INSTALLED kopieren"
  if [ "$(readlink -f "$0")" != "$SELF_INSTALLED" ]; then install -m 755 "$0" "$SELF_INSTALLED"; fi
  prog_netzsim > "$LIBDIR/netzsim.py"
  prog_export > "$LIBDIR/nw-export"
  prog_sync > "$LIBDIR/nw-sync"
  chmod 755 "$LIBDIR/nw-export" "$LIBDIR/nw-sync"

  schritt "Netz ermitteln und Zertifikate erzeugen"
  uplink_ermitteln
  upstream_dns_ermitteln
  zertifikate_erzeugen
  dnsmasq_conf > "$CONF/dnsmasq.conf"

  schritt "Benutzer und Gruppen anlegen"
  benutzer_anlegen
  usermod -aG sudo,adm "$LAB_USER"
  # Schlüssel für die Sitzung von Lena in Mission 4
  [ -s "$STATE/keys/lena" ] || leise ssh-keygen -q -t ed25519 -N '' -C 'lena@ws-lena' -f "$STATE/keys/lena"
  cp "$(TH)/.bashrc" "$STATE/orig/trainee.bashrc" 2>/dev/null || true

  schritt "Simuliertes Firmennetz aufbauen"
  units_schreiben
  ufw_vorbereiten
  dhcp_zaehmen
  netz_start
  systemctl enable -q saarwerk-lab-netz saarwerk-lab-dns saarwerk-lab-web saarwerk-lab-pruefstand
  systemctl start saarwerk-lab-netz
  netz_dienste_starten

  schritt "Saarwerk-Welt herstellen"
  [ -s "$STATE/orig/redis.conf" ] || abbruch "Redis-Konfiguration nicht gefunden."
  grundzustand

  echo
  info "${C_OK}Fertig.${C_0} saarwerk-web01 ist bereit."
  info "Wichtig: Melden Sie sich einmal ab und wieder an, damit $LAB_USER die neue Gruppe adm erhält."
  info "Danach zum Beispiel: sudo $0 mission 32"
  date '+%F %T' > "$STATE/installiert"
}

# ==========================================================================
#  grundzustand: die Saarwerk-Welt so herstellen, wie sie im Spiel beginnt.
#  reset und mission rufen diese Funktion auf. Sie ist idempotent.
# ==========================================================================

bashrc_original() {
  local o="$STATE/orig/trainee.bashrc"
  [ -s "$o" ] && install -m 644 -o "$LAB_USER" -g "$LAB_USER" "$o" "$(TH)/.bashrc"
}

papierkorb() { # Missionsergebnisse von trainee nicht löschen, sondern beiseitelegen
  local p ziel
  ziel="$(TH)/.saarwerk-lab-papierkorb/$(date +%Y%m%d-%H%M%S)"
  for p in "$@"; do
    [ -e "$p" ] || continue
    mkdir -p "$ziel"
    mv "$p" "$ziel/" 2>/dev/null && PAPIERKORB_GENUTZT=1
  done
  [ -d "$ziel" ] && chown -R "$LAB_USER:" "$(TH)/.saarwerk-lab-papierkorb"
  return 0
}

grundzustand() {
  local th; th=$(TH)

  # --- Missionsprozesse und Zusatzdienste beenden
  leise systemctl stop saarwerk-lab-sitzung
  leise systemctl reset-failed saarwerk-lab-sitzung
  leise pkill -x reportgen
  leise pkill -f '^python3 -m http.server 80'
  local u
  for u in saarwerk-export saarwerk-sync; do
    if [ -e "/etc/systemd/system/$u.service" ]; then
      leise systemctl disable --now "$u"
      rm -f "/etc/systemd/system/$u.service"
      leise systemctl reset-failed "$u"
    fi
  done
  for u in $(systemctl list-units --all --plain --no-legend 'run-*.timer' 2>/dev/null | awk '{print $1}'); do
    leise systemctl stop "$u"
  done
  rm -f /var/log/saarwerk/export-debug.log
  systemctl daemon-reload

  # --- Rechnername und Namensauflösung
  if [ "$(hostname)" != "$HOST" ] || [ "$(cat /etc/hostname 2>/dev/null)" != "$HOST" ]; then
    echo "$HOST" > /etc/hostname
    hostname "$HOST"
    leise hostnamectl set-hostname "$HOST"
  fi
  hosts_text > /etc/hosts
  [ -L /etc/resolv.conf ] && rm -f /etc/resolv.conf
  resolv_text > /etc/resolv.conf

  # --- Netz
  { cat "$STATE/interfaces.basis"; echo; interfaces_block; } > /etc/network/interfaces
  netz_ok || netz_start
  netz_dienste_starten
  eth0_grundzustand
  ip route show dev eth0 | grep -q '^10.30.0.0/24' && ip route del 10.30.0.0/24
  rm -f /usr/local/sbin/netz-zurueck /root/umzug-checkliste.txt /root/interfaces.vorher

  # --- Benutzer, Gruppen, Konten
  benutzer_anlegen
  usermod -aG sudo,adm "$LAB_USER"
  usermod -G sudo,projekt-alpha lena
  usermod -G '' hr; usermod -G '' reporting
  usermod -G projekt-alpha aylin
  usermod -G vertrieb jonas
  usermod -G sudo,adm olaf
  usermod -U olaf 2>/dev/null; chage -E -1 olaf
  for u in lena hr reporting aylin jonas olaf; do chmod 700 "$(home_von "$u")"; done

  # --- Home-Verzeichnisse
  case $(cat "$STATE/aktiv" 2>/dev/null) in 8|28) bashrc_original ;; esac
  papierkorb "$th/notizen.txt" "$th/.uebergabe" "$th/exporte" "$th/berichte" "$th/eingang" "$th/abteilungen.txt"
  datei "$th/willkommen.txt" "$LAB_USER:$LAB_USER" 644 <<'EOF'
Willkommen bei Saarwerk!

Du bist als "trainee" angemeldet und gehörst zu den Gruppen sudo und adm.
Administrative Befehle führst du mit sudo aus.

Lena Hoffmann, IT-Leitung
EOF
  datei /home/lena/todo.txt lena:lena 600 <<<$'- Neue Administration einarbeiten\n- Monitoring für API prüfen'
  rm -f /home/lena/.bash_history
  datei /home/olaf/projekte/notizen.txt olaf:olaf 640 <<<$'Offene Punkte web01:\n- Logrotation für /var/log/saarwerk fehlt\n- Backup-Prüfung per cron einrichten'
  verzeichnis /home/olaf/.ssh olaf:olaf 700
  datei /home/olaf/.ssh/authorized_keys olaf:olaf 600 <<<'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIK7olafBrennerLaptop2024xQ olaf@nb-olaf'
  verzeichnis /home/lena/.ssh lena:lena 700
  datei /home/lena/.ssh/authorized_keys lena:lena 600 < <(printf 'from="10.20.0.31" %s\n' "$(cat "$STATE/keys/lena.pub")")

  # --- /etc
  motd_text | datei /etc/motd root:root 644
  rm -rf /etc/saarwerk
  api_conf | datei /etc/saarwerk/api.conf root:nwapi 640
  rm -f /etc/logrotate.d/saarwerk
  local c
  for c in /etc/cron.d/*; do
    [ -f "$c" ] && grep -q 'backup-check' "$c" && rm -f "$c"
  done

  # --- Programme
  verzeichnis /opt/saarwerk root:root 755
  prog_api | datei /opt/saarwerk/bin/saarwerk-api root:root 755
  install -m 755 "$LIBDIR/nw-export" /opt/saarwerk/bin/nw-export
  install -m 755 "$LIBDIR/nw-sync" /opt/saarwerk/bin/nw-sync
  backup_script | datei /opt/saarwerk/bin/backup-check.sh root:root 755
  prog_reportgen | datei /opt/reporting/bin/reportgen root:root 755
  rm -f /usr/local/bin/nw-helper

  # --- Daten unter /srv
  rm -rf /srv/verwaltung /srv/austausch /srv/projekte /srv/backup
  verzeichnis /srv root:root 755
  verzeichnis /srv/verwaltung root:root 755
  verzeichnis /srv/verwaltung/personal hr:root 750
  leerdatei /srv/verwaltung/personal/urlaub_2026.ods hr:root 640 48K
  inventur_2024 | datei /srv/verwaltung/archiv/2024/q4/inventur_server.csv lena:lena 644
  inventur_2025 | datei /srv/verwaltung/archiv/2025/q4/inventur_server.csv lena:lena 644
  datei /srv/verwaltung/archiv/2025/q4/protokoll_inventur.txt lena:lena 644 <<<$'Inventur Q4/2025 abgeschlossen am 18.12.2025.\nVerantwortlich: L. Hoffmann'
  leerdatei /srv/verwaltung/vorlagen/serveranfrage.odt lena:lena 644 22K
  verzeichnis /srv/austausch root:root 755
  verzeichnis /srv/austausch/personal hr:hr 755
  datei /srv/austausch/personal/gehaltsliste_2026.csv hr:hr 640 <<<$'personalnr;name;abteilung;brutto\n1001;(geschwärzt);Konstruktion;(geschwärzt)'
  verzeichnis /srv/projekte root:root 755
  verzeichnis /srv/backup root:adm 750
  verzeichnis /srv/backup/nightly root:adm 750
  local tag
  for tag in 06 07 08 09 10 11 12 13 14 15 16 17 18 19; do
    : > "/srv/backup/nightly/web01-2026-10-$tag.tar.gz"
  done
  chmod 640 /srv/backup/nightly/*

  # --- Web
  verzeichnis /var/www/saarwerk root:root 755
  datei /var/www/saarwerk/index.html root:root 644 <<<'<!doctype html><title>Saarwerk Portal</title><h1>Saarwerk Mitarbeiterportal</h1>'
  verzeichnis /var/www/saarwerk/uploads www-data:www-data 755
  rm -f /etc/nginx/sites-enabled/default /var/backups/nginx/saarwerk.2026-10-12
  rmdir /var/backups/nginx 2>/dev/null
  if ! cmp -s <(nginx_site) /etc/nginx/sites-enabled/saarwerk; then
    rm -f /etc/nginx/sites-enabled/saarwerk
    nginx_site | datei /etc/nginx/sites-enabled/saarwerk root:root 644
    leise systemctl restart nginx
  fi
  systemctl is-active -q nginx || leise systemctl restart nginx
  leise systemctl enable nginx

  # --- Logs
  verzeichnis /var/log/saarwerk nwapi:adm 755
  [ -f /var/log/saarwerk/api.log ] || : > /var/log/saarwerk/api.log
  chown nwapi:adm /var/log/saarwerk/api.log; chmod 640 /var/log/saarwerk/api.log
  app_log | datei /var/log/saarwerk/app.log nwapi:adm 640
  rm -f /var/log/saarwerk/import.log

  # --- Dienste
  leise systemctl disable --now redis-server
  if ! cmp -s "$STATE/orig/redis.conf" /etc/redis/redis.conf; then cp -a "$STATE/orig/redis.conf" /etc/redis/redis.conf; fi
  leise systemctl enable cron
  systemctl is-active -q cron || leise systemctl start cron
  leise systemctl enable saarwerk-api
  leise systemctl reset-failed saarwerk-api
  if ! systemctl is-active -q saarwerk-api || ! ss -Htln "sport = :8080" | grep -q "$IP_WEB:8080"; then
    leise systemctl restart saarwerk-api
  fi

  # --- Firewall
  ufw_grundzustand

  rm -f "$STATE/aktiv" "$STATE/merker"
}

ssh_zusatzports() { ss -Htlnp 2>/dev/null | awk '/sshd/ {n = split($4, a, ":"); print a[n]}' | sort -u | grep -vx 22; }
ufw_soll() {
  printf '%s\n' '22/tcp ALLOW Anywhere' '80/tcp ALLOW Anywhere' '443/tcp ALLOW 10.20.0.0/24' '8080/tcp ALLOW 10.20.0.0/24'
  local p; for p in $(ssh_zusatzports); do echo "$p/tcp ALLOW Anywhere"; done
}
ufw_ist() {
  ufw status 2>/dev/null | awk '/^--/ {f = 1; next} f && NF' | sed 's/  */ /g; s/ $//'
}
# Vergleich ohne die IPv6-Zeilen (nicht jede VM hat IPv6)
ufw_passt() { [ "$(ufw_ist | grep -v '(v6)')" = "$(ufw_soll)" ] && ufw status verbose | grep -q 'Default: deny (incoming), allow (outgoing)'; }
ufw_grundzustand() {
  local ssh_port
  if ufw status | grep -q '^Status: active' && ufw_passt; then return 0; fi
  leise ufw --force disable
  while ufw status numbered 2>/dev/null | grep -q '^\['; do leise ufw --force delete 1 || break; done
  leise ufw default deny incoming
  leise ufw default allow outgoing
  leise ufw default deny routed
  leise ufw allow 22/tcp
  leise ufw allow 80/tcp
  leise ufw allow from 10.20.0.0/24 to any port 443 proto tcp
  leise ufw allow from 10.20.0.0/24 to any port 8080 proto tcp
  # Lauscht sshd auf einem anderen Port, bleibt dieser ebenfalls offen.
  for ssh_port in $(ssh_zusatzports); do leise ufw allow "$ssh_port/tcp"; done
  leise ufw --force enable
  netz_start
}

# ==========================================================================
#  Missionen: Fehler einbauen (m_N) und Ziele prüfen (c_N)
# ==========================================================================

ZIELE_ALLE=0
ZIELE_OK=0
ANTWORT=''
MISSION=0

ziel() { # ziel TEXT BEFEHL...
  local text=$1; shift
  ZIELE_ALLE=$(( ZIELE_ALLE + 1 ))
  if "$@" >/dev/null 2>&1; then
    ZIELE_OK=$(( ZIELE_OK + 1 ))
    printf '  %s[ok]%s    %s\n' "$C_OK" "$C_0" "$text"
  else
    printf '  %s[fehlt]%s %s\n' "$C_ERR" "$C_0" "$text"
  fi
}

# Antworten liegen nur als Prüfsumme im Skript, damit ein Blick in den
# Quelltext die Lösung nicht verrät.
antwort_normal() {
  local v
  v=$(printf '%s' "$ANTWORT" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
  case $MISSION in
    1) v=${v,,}; v=${v%.saarwerk.test} ;;
    2|9) v=${v^^} ;;
    3) v=$(grep -o '[0-9]\{4\}' <<<"$v" | sort | paste -sd, -) ;;
    4) v=${v,,} ;;
    5) case $v in t|-t|--t|--sort=time) v=t ;; esac ;;
    7) v=${v%/} ;;
    14) v=${v,,}; v=${v#[}; v=${v%]}; v=${v#saarwerk-} ;;
    18) case $v in /etc/saarwerk/reporting.d/*|reporting.d/*) v=${v##*/} ;; esac ;;
    35) v=${v,,}; v=${v%.service}; [ "$v" = nw-sync ] && v=saarwerk-sync ;;
  esac
  printf '%s' "$v"
}
declare -A ANTWORT_SHA=([1]=1a7feec32ce59f55 [2]=cdaf7dfe3983b083 [3]=df9413d0858ea5c8 [4]=cc967443070ab409
  [5]=e3b98a4da31a127d [7]=b1079c3dd707f91f [9]=00349a17a53c42b2 [11]=4b227777d4dd1fc6 [14]=d46aee08cc49f6d1
  [18]=0b57c09632f4944f [35]=a78ea43507ce48d4 [41]=d6c38e3ade449859)

antwort_richtig() {
  [ -n "$ANTWORT" ] || return 1
  local v; v=$(antwort_normal)
  case $MISSION in
    21) [ "$v" = "$(grep -cE ':/(usr/)?bin/bash$' /etc/passwd)" ] ;;
    31) [ "$v" = "$(pgrep -c -f '^nginx: worker')" ] ;;
    *) [ "$(sha "$v")" = "${ANTWORT_SHA[$MISSION]}" ] ;;
  esac
}
ziel_antwort() {
  if [ -z "$ANTWORT" ]; then
    ZIELE_ALLE=$(( ZIELE_ALLE + 1 ))
    printf '  %s[fehlt]%s %s\n' "$C_ERR" "$C_0" "$1"
    printf '          Antwort mitgeben: sudo %s check %d "IHRE ANTWORT"\n' "$0" "$MISSION"
  else
    ziel "$1" antwort_richtig
  fi
}

merke() { echo "$1=$2" >> "$STATE/merker"; }
gemerkt() { awk -F= -v k="$1" '$1 == k {v = $2} END {print v}' "$STATE/merker" 2>/dev/null; }

aktiv() { systemctl is-active -q "$1"; }
modus() { stat -c %a "$1"; }
bit() { (( 8#$(modus "$1") & 8#$2 )); }
kein_bit() { ! bit "$1" "$2"; }
http_code() { curl -s -o /dev/null -w '%{http_code}' --max-time "${2:-5}" "$1"; }
http_200() { [ "$(http_code "$@")" = 200 ]; }
portal_nginx() { curl -sI --max-time 5 http://localhost/ | grep -qi '^server: nginx' && http_200 http://localhost/; }
gleich() { [ "$1" = "$2" ]; }
datei_gleich_inhalt() { cmp -s "$1" <("${@:2}"); }
in_gruppe() { id -nG "$1" | tr ' ' '\n' | grep -qx "$2"; }

# Die Shell, aus der das Skript aufgerufen wurde (über sudo hinweg)
aufrufer_cwd() {
  local pid=$PPID comm uid
  while [ "${pid:-0}" -gt 1 ]; do
    comm=$(cat "/proc/$pid/comm" 2>/dev/null)
    uid=$(awk '/^Uid:/ {print $2}' "/proc/$pid/status" 2>/dev/null)
    if [[ $comm =~ ^(bash|zsh|sh|dash|fish)$ ]] && [ "$uid" != 0 ]; then
      readlink "/proc/$pid/cwd"; return
    fi
    pid=$(awk '/^PPid:/ {print $2}' "/proc/$pid/status" 2>/dev/null)
  done
  echo "$START_PWD"
}

# Den eth0-Abschnitt aus /etc/network/interfaces ausgeben
eth0_abschnitt() {
  awk '/^[[:space:]]*iface[[:space:]]+eth0[[:space:]]/ {f = 1; print; next}
       f && /^[[:space:]]*(iface|auto|allow-|source|mapping)/ {f = 0}
       f' /etc/network/interfaces
}

eth0_umstellen() { # eth0_umstellen ADRESSE: interfaces und laufende Konfiguration
  { cat "$STATE/interfaces.basis"; echo; interfaces_block "$1"; } > /etc/network/interfaces
  leise ifdown --force eth0
  ip addr flush dev eth0; ip route flush dev eth0
  leise ifup --force eth0 || { ip addr replace "$1/24" dev eth0; ip route replace default via "$IP_GW" dev eth0; }
}

beta_anlegen() {
  local f b=/srv/projekte/beta
  verzeichnis "$b" "$LAB_USER:$LAB_USER" 755
  for f in plan daten daten/cache; do verzeichnis "$b/$f" "$LAB_USER:$LAB_USER" 755; done
  for f in "${BETA_BEHALTEN[@]}" "${BETA_TMP[@]}"; do
    case $f in
      *.tmp) leerdatei "$b/$f" "$LAB_USER:$LAB_USER" 644 3M ;;
      *.md|*.txt|*.csv) datei "$b/$f" "$LAB_USER:$LAB_USER" 644 <<<"Projekt Beta: $f" ;;
      *) leerdatei "$b/$f" "$LAB_USER:$LAB_USER" 644 40K ;;
    esac
  done
}

# Datei über einen Filter ändern, ohne sie zu ersetzen (Inode bleibt gleich).
aendern() { # aendern DATEI BEFEHL...
  local f=$1 t; shift
  t=$(mktemp)
  "$@" < "$f" > "$t" && cat "$t" > "$f"
  rm -f "$t"
}

nginx_fehler_log() { cat >> /var/log/nginx/error.log; }

# Hat nginx seine Konfiguration nach der letzten Änderung der Datei neu
# eingelesen? Bei reload und restart startet nginx neue Worker-Prozesse.
nginx_neu_geladen() {
  local m w t
  m=$(stat -c %Y "$1") || return 1
  for w in $(pgrep -f '^nginx: worker'); do
    t=$(date -d "$(ps -o lstart= -p "$w")" +%s 2>/dev/null) || continue
    [ "$t" -ge $(( m - 2 )) ] && return 0  # ps rundet auf Sekunden
  done
  return 1
}

# ------------------------------------------------ Kapitel 1 · Shell und Navigation

c_1() { ziel_antwort 'Rechnername korrekt gemeldet'; }

m_2() {
  local th; th=$(TH)
  datei "$th/notizen.txt" "$LAB_USER:$LAB_USER" 644 <<'EOF'
Hier steht nichts Wichtiges.
Die echte Übergabe liegt woanders. Versteckt, wie es sich für Admins gehört.
EOF
  datei "$th/.uebergabe" "$LAB_USER:$LAB_USER" 600 <<'EOF'
Übergabe saarwerk-web01

Hallo Nachfolgerin, hallo Nachfolger,

1. Die Server laufen, aber nicht alles sauber. Schau in die Logs.
2. Rechte auf /srv sind historisch gewachsen.
3. Die Firewall ist aktiv. Bitte nicht einfach abschalten.

Codewort für Lena: LEUCHTTURM

Viel Erfolg!
Olaf
EOF
}
c_2() { ziel_antwort 'Codewort korrekt gemeldet'; }

m_3() { datei /srv/verwaltung/archiv/LIESMICH.txt lena:lena 644 <<<'Archiv der Verwaltung. Ein Ordner pro Jahr.'; }
c_3() {
  ziel 'Sie befinden sich im Archivverzeichnis' gleich "$(aufrufer_cwd)" /srv/verwaltung/archiv
  ziel_antwort 'Jahrgänge korrekt gemeldet'
}

m_4() {
  # Lena meldet sich von ihrem Arbeitsplatz 10.20.0.31 per SSH an und
  # bearbeitet eine Datei. Die Sitzung ist echt: who, w und ps sehen sie.
  leise systemctl stop saarwerk-lab-sitzung
  systemd-run --quiet --collect --unit=saarwerk-lab-sitzung /bin/sh -c \
    "sleep infinity | ip netns exec $NS_NETZ ssh -tt -q -i $STATE/keys/lena -b 10.20.0.31 -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null lena@$IP_WEB nano /srv/verwaltung/vorlagen/serveranfrage.txt"
  local i
  for i in 1 2 3 4 5 6 7 8 9 10; do
    pgrep -u lena -x nano >/dev/null && return 0
    sleep 1
  done
  warnung "Die SSH-Sitzung von lena kam nicht zustande. Läuft sshd und lauscht auf Port 22 (ss -tln)?"
}
c_4() { ziel_antwort 'Angemeldeten Benutzer korrekt gemeldet'; }

c_5() { ziel_antwort 'Option korrekt gemeldet'; }

m_6() {
  local i f
  for i in "${!EXPORT_DATEIEN[@]}"; do
    f=${EXPORT_DATEIEN[i]}
    printf 'auftrag;betrag\n%d;%d,00\n' $(( 4000 + i )) $(( 1200 + i * 37 )) | datei "/srv/verwaltung/exporte/$f" reporting:reporting 644
  done
}
c_6() {
  local th; th=$(TH)
  okt_da() { local f; for f in "${EXPORT_DATEIEN[@]}"; do [[ $f == *2026-10* ]] || continue; [ -f "$th/exporte/$f" ] || return 1; done; }
  sept_weg() { [ -d "$th/exporte" ] && ! compgen -G "$th/exporte/*2026-09*" >/dev/null; }
  orig_da() { local f; for f in "${EXPORT_DATEIEN[@]}"; do [ -f "/srv/verwaltung/exporte/$f" ] || return 1; done; }
  ziel 'Alle Oktober-Exporte liegen in ~/exporte' okt_da
  ziel 'Keine September-Exporte in ~/exporte' sept_weg
  ziel 'Die Originale sind unverändert vorhanden' orig_da
}

m_7() {
  olaf_history | datei /srv/verwaltung/olaf-uebergabe/.bash_history lena:lena 644
  datei /srv/verwaltung/olaf-uebergabe/LIESMICH.txt lena:lena 644 <<<'Sicherung aus /home/olaf, erstellt am 30.09.2026 (LH)'
}
c_7() { ziel_antwort 'Bearbeitete Datei korrekt gemeldet'; }

m_8() {
  bashrc_original
  sed -i '/\/opt\/saarwerk\/bin/d' "$(TH)/.bashrc"
}
c_8() {
  local pfad; pfad=$(trainee_shell 'echo "$PATH"')
  pfad_neu() { tr ':' '\n' <<<"$pfad" | grep -qx /opt/saarwerk/bin; }
  pfad_alt() { tr ':' '\n' <<<"$pfad" | grep -qx /usr/bin; }
  bashrc_pfad() { grep -E '^\s*(export\s+)?PATH=' "$(TH)/.bashrc" | grep -E '\$\{?PATH\}?' | grep -q /opt/saarwerk/bin; }
  per_name() { [ "$(trainee_shell 'command -v backup-check.sh')" = /opt/saarwerk/bin/backup-check.sh ] && trainee_shell 'backup-check.sh'; }
  ziel 'Eine neue Shell hat /opt/saarwerk/bin im PATH' pfad_neu
  ziel 'Die Standardbefehle funktionieren weiterhin' pfad_alt
  ziel '~/.bashrc ergänzt PATH dauerhaft, ohne ihn zu überschreiben' bashrc_pfad
  ziel 'backup-check.sh startet über seinen Namen' per_name
}

m_9() { import_log | datei /var/log/saarwerk/import.log nwapi:adm 640; }
c_9() { ziel_antwort 'Prüfsumme korrekt gemeldet'; }

c_10() {
  local b; b="$(TH)/berichte"
  inventur_da() { local f; for f in "$b"/*; do [ -f "$f" ] && cmp -s "$f" <(inventur_2025) && return 0; done; return 1; }
  ziel 'Verzeichnis ~/berichte existiert' test -d "$b" -a "$(stat -c %U "$b" 2>/dev/null)" = "$LAB_USER"
  ziel 'host.txt enthält den Rechnernamen' gleich "$(tr -d '[:space:]' < "$b/host.txt" 2>/dev/null)" "$HOST"
  ziel 'sitzungen.txt listet die angemeldeten Benutzer' grep -qE "^${LAB_USER}[[:space:]]+pts/" "$b/sitzungen.txt"
  ziel 'Kopie der Inventur Q4/2025 liegt im Ordner' inventur_da
}

# ------------------------------------------------ Kapitel 2 · Dateien und Textverarbeitung

c_11() { ziel_antwort 'Anzahl korrekt gemeldet'; }

m_12() {
  local e f; e="$(TH)/eingang"
  verzeichnis "$e" "$LAB_USER:$LAB_USER" 755
  for f in "${EINGANG_PDF[@]}" "${EINGANG_BILDER[@]}" "${EINGANG_DOK[@]}"; do leerdatei "$e/$f" "$LAB_USER:$LAB_USER" 644 90K; done
}
c_12() {
  local e; e="$(TH)/eingang"
  alle_in() { local d=$1 f; shift; for f in "$@"; do [ -f "$e/$d/$f" ] || return 1; done; }
  keine_losen() { [ -d "$e" ] && [ -z "$(find "$e" -maxdepth 1 -type f)" ]; }
  ziel 'Alle PDF-Dateien liegen in pdf' alle_in pdf "${EINGANG_PDF[@]}"
  ziel 'Alle Bilder liegen in bilder (auch die mit großer Endung)' alle_in bilder "${EINGANG_BILDER[@]}"
  ziel 'Alle Textdokumente liegen in dokumente' alle_in dokumente "${EINGANG_DOK[@]}"
  ziel 'Keine losen Dateien mehr in ~/eingang' keine_losen
}

m_13() { beta_anlegen; }
c_13() {
  keine_tmp() { [ -d /srv/projekte/beta ] && [ -z "$(find /srv/projekte/beta -type f -name '*.tmp')" ]; }
  rest_da() { local f; for f in "${BETA_BEHALTEN[@]}"; do [ -f "/srv/projekte/beta/$f" ] || return 1; done; }
  ziel 'Keine .tmp-Dateien mehr im Projekt' keine_tmp
  ziel 'Alle anderen Dateien sind noch da' rest_da
}

c_14() { ziel_antwort 'Komponente korrekt gemeldet'; }

m_15() {
  nginx_site | datei /var/backups/nginx/saarwerk.2026-10-12 root:root 644
  touch -d '2026-10-12 18:04:00' /var/backups/nginx/saarwerk.2026-10-12
  sed -i 's/client_max_body_size 20m;/client_max_body_size 1m;/' /etc/nginx/sites-enabled/saarwerk
  leise systemctl reload nginx
  nginx_fehler_log <<'EOF'
2026/10/19 08:20:12 [error] 813#813: *88 client intended to send too large body: 4718592 bytes, client: 10.20.0.31, server: portal.saarwerk.test, request: "POST /uploads/ HTTP/1.1"
2026/10/19 08:33:40 [error] 814#814: *97 client intended to send too large body: 2310144 bytes, client: 10.20.0.31, server: portal.saarwerk.test, request: "POST /uploads/ HTTP/1.1"
EOF
}
c_15() {
  local f=/etc/nginx/sites-enabled/saarwerk
  ziel 'Die Konfiguration entspricht wieder der Sicherung' datei_gleich_inhalt "$f" nginx_site
  geladen() { aktiv nginx && datei_gleich_inhalt "$f" nginx_site && nginx_neu_geladen "$f"; }
  ziel 'nginx hat die korrigierte Konfiguration geladen' geladen
}

c_16() {
  alt_da() { grep -qF 'Nur für autorisierte Personen. Zugriffe werden protokolliert.' /etc/motd && grep -qF 'Saarwerk GmbH · saarwerk-web01' /etc/motd; }
  neu_da() { grep -qi wartung /etc/motd && grep -qE '24\.[[:space:]]?10\.' /etc/motd; }
  sicher() { [ "$(stat -c %U /etc/motd)" = root ] && kein_bit /etc/motd 022; }
  ziel 'Der bisherige Text ist erhalten' alt_da
  ziel 'Der Wartungshinweis mit Datum steht in /etc/motd' neu_da
  ziel '/etc/motd gehört weiterhin root und ist nicht für alle beschreibbar' sicher
}

m_17() { telefonliste | datei /srv/verwaltung/telefonliste.csv hr:hr 644; }
c_17() {
  local f; f="$(TH)/abteilungen.txt"
  liste_ok() { [ "$(sed 's/^[[:space:]]*//; s/[[:space:]]*$//' "$f" | grep -v '^$' | grep -vx abteilung | paste -sd'|' -)" = "$ABTEILUNGEN" ]; }
  ziel '~/abteilungen.txt existiert' test -f "$f"
  ziel 'Jede Abteilung genau einmal, alphabetisch sortiert' liste_ok
  ziel 'Die Kopfzeile „abteilung“ ist nicht in der Liste' bash -c "test -f '$f' && ! grep -qx abteilung '$f'"
}

m_18() {
  datei /etc/saarwerk/export.conf root:root 644 <<'EOF'
# Saarwerk Export
# db_host = 10.20.0.12   (alt, bis Juni 2025)
db_host = db02.saarwerk.test
db_name = erp
EOF
  datei /etc/saarwerk/reporting.d/monatsbericht.conf root:root 644 <<'EOF'
# Monatsbericht
quelle = erp
db_host = 10.20.0.12
db_port = 5432
ausgabe = /srv/verwaltung/berichte
EOF
  datei /etc/saarwerk/reporting.d/wochenbericht.conf root:root 644 <<'EOF'
# Wochenbericht
quelle = erp
db_host = db02.saarwerk.test
db_port = 5432
EOF
  datei /etc/saarwerk/mail.conf root:root 644 <<'EOF'
smtp = mx.saarwerk.test
absender = server@saarwerk.test
EOF
}
c_18() { ziel_antwort 'Datei mit aktivem Verweis korrekt gemeldet'; }

m_19() { beta_anlegen; }
c_19() {
  local a=/srv/backup/projekt-beta.tar.gz
  alles_drin() {
    local liste f; liste=$(tar -tzf "$a" 2>/dev/null) || return 1
    for f in "${BETA_BEHALTEN[@]}" "${BETA_TMP[@]}"; do
      grep -qxE "(\./)?(srv/projekte/)?(beta/)?$(sed 's/[.~]/\\&/g' <<<"$f")" <<<"$liste" || return 1
    done
  }
  ordner_da() { local f; for f in "${BETA_BEHALTEN[@]}"; do [ -f "/srv/projekte/beta/$f" ] || return 1; done; }
  ziel 'Das Archiv /srv/backup/projekt-beta.tar.gz existiert' tar -tf "$a"
  ziel 'Das Archiv ist mit gzip komprimiert' gzip -t "$a"
  ziel 'Alle Projektdateien sind im Archiv' alles_drin
  ziel 'Der Projektordner ist noch vorhanden' ordner_da
}

m_20() { access_log > /var/log/nginx/access.log; }
c_20() {
  local f; f="$(TH)/berichte/oktober.txt"
  ziel 'Anfragen insgesamt korrekt' grep -qE "^Anfragen:[[:space:]]*${ACCESS_N}[[:space:]]*$" "$f"
  ziel 'Anzahl der 404-Fehler korrekt' grep -qE '^Fehler 404:[[:space:]]*147[[:space:]]*$' "$f"
  ziel 'Häufigste IP-Adresse korrekt' grep -qE '^Top-IP:[[:space:]]*10\.20\.0\.31[[:space:]]*$' "$f"
}

# ------------------------------------------------ Kapitel 3 · Benutzer und Berechtigungen

c_21() { ziel_antwort 'Anzahl korrekt gemeldet'; }

m_22() { chmod 666 /srv/austausch/personal/gehaltsliste_2026.csv; }
c_22() {
  local f=/srv/austausch/personal/gehaltsliste_2026.csv
  ziel 'Andere Benutzer können die Datei weder lesen noch ändern' kein_bit "$f" 007
  ziel 'Die Personalabteilung (Besitzer hr) kann weiter lesen und schreiben' \
    bash -c "[ \"\$(stat -c %U '$f')\" = hr ] && (( (8#\$(stat -c %a '$f') & 8#600) == 8#600 ))"
}

m_23() { chmod 644 /opt/saarwerk/bin/backup-check.sh; }
c_23() {
  local f=/opt/saarwerk/bin/backup-check.sh
  ziel 'trainee kann das Skript ausführen' als_trainee test -r "$f" -a -x "$f"
  ziel 'Nur root darf das Skript ändern' bash -c "[ \"\$(stat -c %U '$f')\" = root ] && ! (( 8#\$(stat -c %a '$f') & 8#022 ))"
  ziel 'Probelauf als trainee erfolgreich' als_trainee "$f"
}

m_24() {
  chown root:root /var/www/saarwerk/uploads; chmod 755 /var/www/saarwerk/uploads
  nginx_fehler_log <<'EOF'
2026/10/19 08:12:44 [crit] 813#813: *41 open() "/var/www/saarwerk/uploads/angebot_4711.pdf" failed (13: Permission denied), client: 10.20.0.31, server: portal.saarwerk.test, request: "POST /uploads/ HTTP/1.1"
2026/10/19 08:29:03 [crit] 814#814: *57 open() "/var/www/saarwerk/uploads/angebot_4712.pdf" failed (13: Permission denied), client: 10.20.0.31, server: portal.saarwerk.test, request: "POST /uploads/ HTTP/1.1"
EOF
}
c_24() {
  local d=/var/www/saarwerk/uploads
  ziel 'www-data kann in /var/www/saarwerk/uploads schreiben' als www-data test -w "$d" -a -x "$d"
  ziel 'Das Verzeichnis ist nicht für alle beschreibbar' kein_bit "$d" 002
}

c_25() {
  ziel 'jonas ist Mitglied von projekt-alpha' in_gruppe jonas projekt-alpha
  ziel 'jonas ist weiterhin Mitglied von vertrieb' in_gruppe jonas vertrieb
}

m_26() {
  verzeichnis /srv/austausch/ablage root:root 777
  leerdatei /srv/austausch/ablage/reisekosten_graf.ods hr:hr 644 22K
  leerdatei /srv/austausch/ablage/messeplanung.odt jonas:jonas 644 31K
  leerdatei /srv/austausch/ablage/kantine_speiseplan.pdf lena:lena 644 120K
}
c_26() {
  local d=/srv/austausch/ablage
  dateien_da() { local f; for f in reisekosten_graf.ods messeplanung.odt kantine_speiseplan.pdf; do [ -f "$d/$f" ] || return 1; done; }
  ziel 'Das Sticky-Bit ist gesetzt' bit "$d" 1000
  ziel 'Alle dürfen weiterhin Dateien anlegen' bash -c "(( (8#\$(stat -c %a '$d') & 8#003) == 8#003 ))"
  ziel 'Die vorhandenen Dateien sind noch da' dateien_da
}

m_27() {
  datei /usr/local/bin/nw-helper root:root 4755 <<'EOF'
#!/bin/sh
# nw-helper 1.2 · Hilfsprogramm aus einem alten Projekt (Herkunft unklar)
echo "nw-helper 1.2"
EOF
}
c_27() {
  local f=/usr/local/bin/nw-helper
  ziel 'nw-helper hat kein SUID-Bit mehr' bash -c "[ -e '$f' ] && ! (( 8#\$(stat -c %a '$f') & 8#4000 ))"
  ziel 'passwd und sudo funktionieren weiterhin (SUID erhalten)' bash -c '(( 8#$(stat -c %a /usr/bin/passwd) & 8#4000 )) && (( 8#$(stat -c %a /usr/bin/sudo) & 8#4000 ))'
  ziel 'nw-helper wurde nicht gelöscht (Beweissicherung)' test -e "$f"
}

m_28() { bashrc_original; sed -i '/^[[:space:]]*umask[[:space:]]/d' "$(TH)/.bashrc"; }
c_28() {
  ziel 'Eine neue Shell arbeitet mit umask 027' gleich "$(trainee_shell umask)" 0027
  ziel '~/.bashrc setzt umask 027 dauerhaft' grep -qE '^[[:space:]]*umask[[:space:]]+0?027[[:space:]]*$' "$(TH)/.bashrc"
}

c_29() {
  gesperrt() {
    [ "$(passwd -S olaf | awk '{print $2}')" = L ] && return 0
    local e; e=$(getent shadow olaf | cut -d: -f8)
    [ -n "$e" ] && [ "$e" -le $(( $(date +%s) / 86400 )) ]
  }
  ziel 'Das Konto olaf ist gesperrt' gesperrt
  ziel 'olaf ist nicht mehr in der Gruppe sudo' bash -c 'id olaf >/dev/null && ! id -nG olaf | tr " " "\n" | grep -qx sudo'
  ziel 'Konto und Home-Verzeichnis existieren weiter' bash -c 'id olaf && test -f /home/olaf/projekte/notizen.txt'
}

m_30() {
  local a=/srv/projekte/alpha
  verzeichnis "$a" lena:lena 777
  leerdatei "$a/vertrag_kunde.odt" lena:lena 666 184K
  leerdatei "$a/planung.ods" aylin:aylin 644 61K
  verzeichnis "$a/protokolle" lena:lena 777
  datei "$a/protokolle/kickoff_0910.txt" aylin:aylin 644 <<<$'Kickoff Projekt Alpha\nTeilnehmende: Demir, Hoffmann, Kundenvertretung'
}
c_30() {
  local a=/srv/projekte/alpha
  ziel 'Ordner und Inhalt gehören zur Gruppe projekt-alpha' bash -c "[ -d $a ] && [ -z \"\$(find $a ! -group projekt-alpha)\" ]"
  ziel 'Die Gruppe hat vollen Zugriff auf den Ordner' bash -c "(( (8#\$(stat -c %a $a) & 8#070) == 8#070 ))"
  ziel 'Andere haben auf nichts mehr Zugriff' bash -c "[ -d $a ] && [ -z \"\$(find $a -perm /007)\" ]"
  ziel 'Lena ist weiterhin Besitzerin des Ordners' gleich "$(stat -c %U "$a" 2>/dev/null)" lena
}

# ------------------------------------------------ Kapitel 4 · Prozesse, Dienste und Logs

c_31() { ziel_antwort 'Anzahl korrekt gemeldet'; }

m_32() {
  sed -i 's/server_name saarwerk-web01/server_nmae saarwerk-web01/' /etc/nginx/sites-enabled/saarwerk
  leise systemctl restart nginx
}
c_32() {
  ziel 'Konfiguration fehlerfrei' nginx -t
  ziel 'nginx läuft' aktiv nginx
  ziel 'Das Portal antwortet mit HTTP 200' portal_nginx
}

m_33() { leise systemctl disable --now saarwerk-api; }
c_33() {
  ziel 'saarwerk-api läuft' aktiv saarwerk-api
  ziel 'saarwerk-api startet beim Booten automatisch' systemctl is-enabled -q saarwerk-api
}

m_34() {
  ( cd / && setsid setpriv --reuid=reporting --regid=reporting --init-groups \
      /opt/reporting/bin/reportgen --monthly --rebuild-all </dev/null >/dev/null 2>&1 & )
  sleep 1
  merke reportgen "$(pgrep -x reportgen | head -n1)"
}
c_34() {
  ziel "reportgen (PID $(gemerkt reportgen)) läuft nicht mehr" bash -c '! pgrep -x reportgen'
  ziel 'nginx, ssh und saarwerk-api laufen weiter' bash -c 'systemctl is-active -q nginx ssh saarwerk-api'
  ziel 'Ihre eigene Sitzung lebt noch' pgrep -u "$LAB_USER" -x bash
}

m_35() {
  datei /etc/systemd/system/saarwerk-sync.service root:root 644 <<'EOF'
[Unit]
Description=Saarwerk Abgleich mit dem ERP
After=network-online.target

[Service]
User=nwapi
ExecStart=/opt/saarwerk/bin/nw-sync --erp repo.saarwerk.test
SyslogIdentifier=nw-sync

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  leise systemctl start saarwerk-sync
}
c_35() { ziel_antwort 'Fehlerhaften Dienst korrekt gemeldet'; }

m_36() {
  datei /etc/systemd/system/saarwerk-export.service root:root 644 <<'EOF'
[Unit]
Description=Saarwerk Datenexport
After=network-online.target

[Service]
User=nwexport
ExecStart=/opt/saarwerk/bin/nw-export --daemon --debug
SyslogIdentifier=nw-export

[Install]
WantedBy=multi-user.target
EOF
  local log=/var/log/saarwerk/export-debug.log frei
  : > "$log"; chown nwexport:adm "$log"; chmod 640 "$log"
  systemctl daemon-reload
  leise systemctl start saarwerk-export
  sleep 1
  # Die Platte bis auf 1 MB füllen (aus Sicht normaler Benutzer). Die für
  # root reservierten Blöcke bleiben frei, damit das System bedienbar bleibt.
  frei=$(df -B1 --output=avail /var/log/saarwerk | tail -n1 | tr -d ' ')
  if [ "${frei:-0}" -gt 2097152 ]; then
    fallocate -o "$(stat -c %s "$log")" -l $(( frei - 1048576 )) "$log" ||
      warnung "fallocate ist gescheitert. Unterstützt das Dateisystem es?"
  fi
  leise systemctl restart saarwerk-api
}
c_36() {
  ziel "Dateisystem $(df --output=target /var/log/saarwerk | tail -n1) unter 90 % belegt" \
    bash -c '[ "$(df --output=pcent /var/log/saarwerk | tail -n1 | tr -dc 0-9)" -lt 90 ]'
  ziel 'saarwerk-api läuft wieder' aktiv saarwerk-api
}

m_37() { truncate -s 820M /var/log/saarwerk/app.log; }
c_37() {
  local f=/etc/logrotate.d/saarwerk
  # Liefert die Anweisungen des Blocks für /var/log/saarwerk/*.log
  block() { awk '/\/var\/log\/saarwerk\/\*\.log/ && /\{/ {f = 1; next} /\/var\/log\/saarwerk\/\*\.log/ {s = 1; next} s && /^[[:space:]]*\{/ {f = 1; s = 0; next} f && /\}/ {exit} f {print}' "$f" 2>/dev/null | sed 's/#.*//'; }
  fehlerfrei() { [ -f "$f" ] && block | grep -q . && ! logrotate -d "$f" 2>&1 | grep -qi '^error'; }
  umfang() {
    local b; b=$(block)
    grep -qwE 'daily|weekly' <<<"$b" &&
      [ "$(awk '$1 == "rotate" {print $2}' <<<"$b" | tail -n1)" -ge 7 ] 2>/dev/null &&
      grep -qw compress <<<"$b" && ! grep -qw nocompress <<<"$b"
  }
  ziel '/etc/logrotate.d/saarwerk existiert und gehört root' bash -c "[ \"\$(stat -c %U '$f')\" = root ] && ! (( 8#\$(stat -c %a '$f') & 8#022 ))"
  ziel 'Die Konfiguration ist fehlerfrei und erfasst /var/log/saarwerk/*.log' fehlerfrei
  ziel 'Täglich oder wöchentlich, mindestens 7 Stände, komprimiert' umfang
  pflicht() { local b; b=$(block); grep -qw missingok <<<"$b" && grep -qw notifempty <<<"$b"; }
  ziel 'missingok und notifempty gesetzt' pflicht
}

c_38() {
  auftrag() {
    local d
    for d in /etc/cron.d/*; do
      [ -f "$d" ] || continue
      # cron ignoriert Dateinamen mit Punkt oder anderen Sonderzeichen
      [[ $(basename "$d") =~ ^[A-Za-z0-9_-]+$ ]] || continue
      grep -qE '^[[:space:]]*0[[:space:]]+7[[:space:]]+\*[[:space:]]+\*[[:space:]]+\*[[:space:]]+root[[:space:]]+/opt/saarwerk/bin/backup-check\.sh[[:space:]]*$' "$d" && return 0
    done
    return 1
  }
  sicher() {
    local d n=0
    for d in /etc/cron.d/*; do
      grep -qs backup-check "$d" || continue
      n=1
      [ "$(stat -c %U "$d")" = root ] && ! (( 8#$(stat -c %a "$d") & 8#022 )) || return 1
    done
    [ "$n" = 1 ]
  }
  ziel 'cron kennt einen Auftrag für backup-check.sh um 07:00 Uhr als root' auftrag
  ziel 'Die Datei gehört root und ist nur für root beschreibbar' sicher
}

m_39() {
  sed -i 's|^    index index.html;$|    index index.html;\n    add_header X-Frame-Options SAMEORIGIN;\n    add_header X-Content-Type-Options nosniff;|' /etc/nginx/sites-enabled/saarwerk
  merke nginx_pid "$(cat /run/nginx.pid 2>/dev/null)"
}
c_39() {
  geladen() { grep -q X-Frame-Options /etc/nginx/sites-enabled/saarwerk && curl -sI --max-time 5 http://localhost/ | grep -qi '^x-frame-options'; }
  ziel 'nginx hat die neue Konfiguration geladen' geladen
  ziel "Der nginx-Master läuft unverändert weiter (PID $(gemerkt nginx_pid))" \
    bash -c "systemctl is-active -q nginx && [ \"\$(cat /run/nginx.pid)\" = '$(gemerkt nginx_pid)' ]"
}

m_40() {
  leise systemctl stop nginx
  ( cd /tmp && setsid python3 -m http.server 80 </dev/null >/dev/null 2>&1 & )
  sleep 1
  leise systemctl start nginx
  leise systemctl disable saarwerk-api
  datei /home/lena/.bash_history lena:lena 600 <<'EOF'
cd /tmp
sudo systemctl stop nginx
sudo python3 -m http.server 80 &
EOF
  touch -d '2026-10-19 08:12' /home/lena/.bash_history
}
c_40() {
  local f; f="$(TH)/berichte/montag.txt"
  ziel 'Der Testserver auf Port 80 ist beendet' bash -c "! pgrep -f '^python3 -m http[.]server 80'"
  nginx_antwortet() { aktiv nginx && portal_nginx; }
  ziel 'nginx läuft und beantwortet Port 80' nginx_antwortet
  ziel 'saarwerk-api läuft und ist für den Autostart aktiviert' bash -c 'systemctl is-enabled -q saarwerk-api && systemctl is-active -q saarwerk-api'
  ziel 'nginx-Konfiguration unverändert' datei_gleich_inhalt /etc/nginx/sites-enabled/saarwerk nginx_site
  ziel '~/berichte/montag.txt nennt den Verursacher' bash -c "grep -qiE 'python|http\.server' '$f' && grep -qiE '80|port|nginx' '$f'"
}

# ------------------------------------------------ Kapitel 7 · Netzwerke und Diagnose (Buch)

c_41() { ziel_antwort 'Gateway korrekt gemeldet'; }

m_42() {
  aendern /etc/hosts sed "s|^127.0.1.1\t$HOST.$DOMAIN\t$HOST\$|&\n# Umzug Intranet, temporär (2023)\n10.20.0.99\tintranet.saarwerk.test\tintranet|"
}
c_42() {
  ziel 'intranet.saarwerk.test löst auf 10.20.0.30 auf' gleich "$(getent hosts intranet.saarwerk.test | awk '{print $1; exit}')" 10.20.0.30
  ziel 'Das Intranet antwortet mit HTTP 200' http_200 http://intranet.saarwerk.test/
  ziel 'localhost und der eigene Rechnername sind in /etc/hosts erhalten' \
    bash -c "grep -qE '^127\.0\.0\.1[[:space:]]+localhost' /etc/hosts && grep -qE '^127\.0\.1\.1[[:space:]]+.*$HOST' /etc/hosts"
}

m_43() {
  sed -i "s|^bind = $IP_WEB|# Absicherung nach Pentest, 17.10. (MS)\nbind = 127.0.0.1|" /etc/saarwerk/api.conf
  leise systemctl restart saarwerk-api
}
c_43() {
  ziel 'saarwerk-api läuft' aktiv saarwerk-api
  ziel 'Die API lauscht auf der Netzwerkadresse (Port 8080)' \
    bash -c "ss -Htlnp 'sport = :8080' | grep saarwerk-api | awk '{print \$4}' | grep -qE '^($IP_WEB|0\.0\.0\.0|\*|\[::\]):8080$'"
}

m_44() { leise ufw --force delete allow from 10.20.0.0/24 to any port 443 proto tcp; }
c_44() {
  von_lena() { ns "$NS_NETZ" curl -sk -o /dev/null -w '%{http_code}' --interface 10.20.0.23 --max-time 4 "https://$IP_WEB/" | grep -qx 200; }
  ssh_offen() {
    ns "$NS_NETZ" python3 -c "
import socket; s = socket.socket(); s.settimeout(4); s.bind(('10.20.0.23', 0)); s.connect(('$IP_WEB', 22))"
  }
  ziel 'Die Firewall ist aktiv' bash -c 'ufw status | grep -q "^Status: active"'
  ziel 'Rechner im Büronetz erreichen Port 443 (Test von 10.20.0.23)' von_lena
  ziel 'SSH ist weiterhin erlaubt' ssh_offen
}

m_45() {
  ip route del default via "$IP_GW" dev eth0 2>/dev/null
  datei /etc/resolv.conf root:root 644 <<'EOF'
# Saarwerk-IT · statische Resolver-Konfiguration
# Umbau 17.10.: neuer DNS (siehe Ticket NET-212)
search saarwerk.test
nameserver 10.20.0.253
EOF
}
c_45() {
  ziel 'Standardroute über 10.20.0.1 vorhanden' bash -c "ip -4 route show default | grep -q 'via $IP_GW '"
  ziel 'Namensauflösung per DNS funktioniert wieder' gleich "$(dig +short +time=2 +tries=1 mirror.example.org A 2>/dev/null | tail -n1)" 203.0.113.10
  ziel 'https://mirror.example.org antwortet' http_200 https://mirror.example.org/
}

m_46() {
  local c=/etc/redis/redis.conf
  sed -i 's/^\(bind[[:space:]]\)/# \1/; s/^\(protected-mode[[:space:]]\)/# \1/' "$c"
  cat >> "$c" <<'EOF'

# Saarwerk: Zugriff fürs Monitoring geöffnet (Ticket MON-77)
# Nur Zugriffe von diesen Adressen annehmen
# bind 127.0.0.1 -::1
bind 0.0.0.0
protected-mode no
EOF
  leise systemctl enable redis-server
  leise systemctl restart redis-server
}
c_46() {
  nur_lokal() {
    local l; l=$(ss -Htln 'sport = :6379' | awk '{print $4}')
    [ -n "$l" ] && ! grep -vqE '^(127\.0\.0\.1|\[::1\]):6379$' <<<"$l"
  }
  dauerhaft() {
    local b; b=$(grep -E '^[[:space:]]*bind[[:space:]]' /etc/redis/redis.conf | tail -n1 | sed 's/^[[:space:]]*bind[[:space:]]*//')
    [ -n "$b" ] && ! tr ' ' '\n' <<<"$b" | grep -v '^$' | grep -vqE '^-?(127\.0\.0\.1|::1)$'
  }
  ziel 'Port 6379 ist von außen nicht mehr erreichbar' nur_lokal
  ziel 'redis-server läuft' aktiv redis-server
  ziel 'Die Einstellung bleibt nach einem Neustart erhalten' dauerhaft
  ziel 'saarwerk-api läuft weiter' aktiv saarwerk-api
}

m_47() {
  datei /etc/resolv.conf root:root 644 <<'EOF'
# Saarwerk-IT · statische Resolver-Konfiguration
# angepasst 16.10. (Netzumbau, Ticket NET-208): Suchdomain entfernt, wird nicht gebraucht
nameserver 10.20.0.2
EOF
}
c_47() {
  aufl() { getent hosts "$1" | awk '{print $1; exit}'; }
  ziel 'intranet wird zu 10.20.0.30 aufgelöst' gleich "$(aufl intranet)" 10.20.0.30
  ziel 'Auch repo und monitoring funktionieren als Kurzname' bash -c "[ '$(aufl repo)' = 10.20.0.40 ] && [ '$(aufl monitoring)' = 10.20.0.50 ]"
  ziel 'Der Nameserver 10.20.0.2 ist weiterhin eingetragen' grep -qE '^[[:space:]]*nameserver[[:space:]]+10\.20\.0\.2[[:space:]]*$' /etc/resolv.conf
}

c_48() {
  ziel 'Route 10.30.0.0/24 über 10.20.0.254 ist aktiv' bash -c "ip route show 10.30.0.0/24 | grep -q 'via 10.20.0.254'"
  ziel 'Der Prüfstand 10.30.0.10 antwortet per HTTP' http_200 http://10.30.0.10/
  route_dauerhaft() { eth0_abschnitt | grep -qE '^[[:space:]]*(post-)?up[[:space:]]+(/usr/sbin/|/sbin/)?ip[[:space:]]+(-4[[:space:]]+)?route[[:space:]]+(add|replace)[[:space:]]+10\.30\.0\.0/24[[:space:]]+via[[:space:]]+10\.20\.0\.254'; }
  ziel 'Die Route ist in /etc/network/interfaces dauerhaft eingetragen' route_dauerhaft
  ziel 'Die Standardroute über 10.20.0.1 ist unverändert' bash -c "ip -4 route show default | grep -q 'via $IP_GW dev eth0'"
}

m_49() {
  eth0_umstellen 10.20.0.16
  sed -i "s|^bind = $IP_WEB|bind = 10.20.0.16|" /etc/saarwerk/api.conf
  leise systemctl restart saarwerk-api
  hostname saarwerk-web02
}
c_49() {
  ziel 'Der laufende Rechnername ist saarwerk-web02' gleich "$(hostname)" saarwerk-web02
  ziel '/etc/hostname enthält saarwerk-web02' gleich "$(tr -d '[:space:]' < /etc/hostname)" saarwerk-web02
  ziel '/etc/hosts löst saarwerk-web02 auf 127.0.1.1 auf' grep -qE '^127\.0\.1\.1[[:space:]]+.*\bsaarwerk-web02\b' /etc/hosts
  ziel 'Der alte Name saarwerk-web01 ist nicht mehr lokal eingetragen' bash -c '! grep -q "\bsaarwerk-web01\b" /etc/hosts'
}

m_50() {
  datei /usr/local/sbin/netz-zurueck root:root 750 <<'EOF'
#!/bin/bash
# Notfall: alte Netzkonfiguration zurückspielen
cp /root/interfaces.vorher /etc/network/interfaces
systemctl restart networking
EOF
  datei /root/umzug-checkliste.txt root:root 600 <<'EOF'
Umzug 10.20.0.0/24 -> 10.20.10.0/24
1. Sicherung: cp /etc/network/interfaces /root/interfaces.vorher
2. Rückfallschutz starten (systemd-run --on-active=10m /usr/local/sbin/netz-zurueck)
3. interfaces anpassen, networking neu starten
4. Dienste mit fester Adresse anpassen
5. Tests, dann Rückfallschutz abbrechen
EOF
}
c_50() {
  dauerhaft() {
    local a; a=$(eth0_abschnitt)
    { grep -qE '^[[:space:]]*address[[:space:]]+10\.20\.10\.15/24[[:space:]]*$' <<<"$a" ||
      { grep -qE '^[[:space:]]*address[[:space:]]+10\.20\.10\.15[[:space:]]*$' <<<"$a" && grep -qE '^[[:space:]]*netmask[[:space:]]+255\.255\.255\.0[[:space:]]*$' <<<"$a"; }; } &&
      grep -qE '^[[:space:]]*gateway[[:space:]]+10\.20\.10\.1[[:space:]]*$' <<<"$a"
  }
  ziel 'eth0 hat die Adresse 10.20.10.15/24' bash -c "ip -4 -o addr show dev eth0 | grep -q ' 10.20.10.15/24 '"
  ziel 'Die Standardroute führt über 10.20.10.1' bash -c "ip -4 route show default | grep -q 'via 10.20.10.1 '"
  ziel 'Die neue Adresse steht dauerhaft in /etc/network/interfaces' dauerhaft
  ziel 'saarwerk-api lauscht auf 10.20.10.15:8080' bash -c "systemctl is-active -q saarwerk-api && ss -Htlnp 'sport = :8080' | grep saarwerk-api | grep -q '10.20.10.15:8080'"
  ziel 'https://mirror.example.org antwortet' http_200 https://mirror.example.org/
  ziel 'Kein Rückfallschutz mehr offen, der alles zurückdreht' bash -c '[ -z "$(systemctl list-units --plain --no-legend --state=active "run-*.timer" 2>/dev/null)" ]'
}

# ==========================================================================
#  Tickets der Missionen (Texte aus AdminMission)
# ==========================================================================

ticket() {
  T_KAPITEL=''; T_TITEL=''; T_VON=''; T_TEXT=''; T_AUFTRAG=''
  case $1 in
    1) T_KAPITEL='Missionen 1 bis 10 · Shell und Navigation'; T_TITEL='Erster Login'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Willkommen an Bord! Bevor du loslegst, eine Kleinigkeit: Wir haben drei Server mit fast identischem Prompt. Melde mir bitte, auf welchem Rechner du gerade gelandet bist.'
       T_AUFTRAG='Ermittle den Rechnernamen und trage ihn als Antwort ein.' ;;
    2) T_KAPITEL='Missionen 1 bis 10 · Shell und Navigation'; T_TITEL='Die Übergabenotiz'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Olaf hat dir eine Übergabenotiz in dein Home-Verzeichnis gelegt. Er war ein Freund versteckter Dateien. Darin steht ein Codewort, das wir für die Übergabe vereinbart hatten.'
       T_AUFTRAG='Finde die Übergabenotiz in deinem Home-Verzeichnis und melde das Codewort.' ;;
    3) T_KAPITEL='Missionen 1 bis 10 · Shell und Navigation'; T_TITEL='Der Weg ins Archiv'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Unser Verwaltungsarchiv liegt unter /srv/verwaltung/archiv. Ich will wissen, für welche Jahre dort schon Unterlagen abgelegt sind. Wechsle bitte dorthin, damit du gleich weiterarbeiten kannst.'
       T_AUFTRAG='Wechsle in das Archivverzeichnis, bleib dort und melde die Jahrgänge, die darin als Ordner liegen.' ;;
    4) T_KAPITEL='Missionen 1 bis 10 · Shell und Navigation'; T_TITEL='Wer ist noch da?'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Gleich wollte ich den Server kurz neu starten. Vorher muss ich wissen, ob außer dir noch jemand angemeldet ist. Ich will niemandem die Arbeit kaputt machen.'
       T_AUFTRAG='Finde heraus, welcher andere Benutzer gerade auf saarwerk-web01 angemeldet ist, und melde den Benutzernamen.' ;;
    5) T_KAPITEL='Missionen 1 bis 10 · Shell und Navigation'; T_TITEL='Hilfe zur Selbsthilfe'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Kleine Denksportaufgabe für zwischendurch: Ich will in einem Ordner sofort sehen, welche Datei zuletzt geändert wurde. ls kann die Ausgabe nach Änderungszeit sortieren. Mit welcher Option? Bitte nachschlagen, nicht raten.'
       T_AUFTRAG='Finde in der Dokumentation von ls die Option, die nach Änderungszeit sortiert, und melde sie.' ;;
    6) T_KAPITEL='Missionen 1 bis 10 · Shell und Navigation'; T_TITEL=Platzhalter; T_VON='Petra Graf (Buchhaltung)'
       T_TEXT='Ich brauche für die Monatsabstimmung alle Exporte aus dem Oktober. Sie liegen unter /srv/verwaltung/exporte, zusammen mit älteren Dateien. Bitte kopiere mir nur die Oktober-Dateien in einen Ordner exporte in deinem Home-Verzeichnis. Die Originale müssen bleiben, wo sie sind.'
       T_AUFTRAG='Kopiere alle Exporte vom Oktober 2026 nach ~/exporte, aber keine Dateien aus dem September.' ;;
    7) T_KAPITEL='Missionen 1 bis 10 · Shell und Navigation'; T_TITEL='Olafs letzte Spuren'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Olaf hat kurz vor seinem Weggang an der Webserver-Konfiguration gearbeitet. Was genau, hat er nicht dokumentiert. Zum Glück haben wir seinen Befehlsverlauf gesichert, er liegt unter /srv/verwaltung/olaf-uebergabe.'
       T_AUFTRAG='Finde in Olafs Befehlsverlauf heraus, welche Datei er mit einem Editor bearbeitet hat, und melde den vollständigen Pfad.' ;;
    8) T_KAPITEL='Missionen 1 bis 10 · Shell und Navigation'; T_TITEL='Befehl nicht gefunden'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Unser Prüfskript backup-check.sh liegt unter /opt/saarwerk/bin. Ich tippe jedes Mal den ganzen Pfad. Kannst du es so einrichten, dass du es einfach mit backup-check.sh aufrufen kannst? Und zwar auch nach der nächsten Anmeldung.'
       T_AUFTRAG='Ergänze die Variable PATH um /opt/saarwerk/bin, jetzt und dauerhaft in ~/.bashrc, und starte das Skript nur über seinen Namen.' ;;
    9) T_KAPITEL='Missionen 1 bis 10 · Shell und Navigation'; T_TITEL='Fünftausend Zeilen'; T_VON='Sven Albers (Monitoring)'
       T_TEXT='Der nächtliche ERP-Import hat ein Protokoll von fast fünftausend Zeilen geschrieben: /var/log/saarwerk/import.log. Uns interessiert nur die Prüfsumme, die der Import ganz am Ende ausgibt. Die gleiche ich mit dem ERP ab.'
       T_AUFTRAG='Lies die letzte Zeile des Importprotokolls und melde die Prüfsumme.' ;;
    10) T_KAPITEL='Missionen 1 bis 10 · Shell und Navigation'; T_TITEL='Der Übergabebericht'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Danke für die schnellen Antworten! Für die Dokumentation brauche ich alles an einem Ort: Lege in deinem Home-Verzeichnis einen Ordner berichte an. Darin soll eine Datei host.txt mit dem Rechnernamen liegen, eine Datei sitzungen.txt mit den aktuell angemeldeten Benutzern und eine Kopie der Inventur Q4/2025 aus dem Archiv.'
       T_AUFTRAG='Erstelle ~/berichte mit host.txt (Rechnername), sitzungen.txt (Ausgabe von who) und einer Kopie der Inventurliste Q4/2025.' ;;
    11) T_KAPITEL='Missionen 11 bis 20 · Dateien und Textverarbeitung'; T_TITEL='Die Inventurliste'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Der Wirtschaftsprüfer fragt, wie viele unserer Server ein Betriebssystem ohne Herstellerunterstützung haben. Die Antwort steht in der Server-Inventur vom vierten Quartal 2025. Irgendwo unter /srv, ich weiß nicht mehr genau wo.'
       T_AUFTRAG='Finde die Inventurliste Q4/2025 und melde, wie viele Server den Status EOL haben.' ;;
    12) T_KAPITEL='Missionen 11 bis 20 · Dateien und Textverarbeitung'; T_TITEL='Ordnung im Eingang'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Ich habe dir den Inhalt eines alten USB-Sticks von Olaf nach ~/eingang kopiert. Da liegt alles durcheinander. Bitte sortiere es in drei Unterordner: pdf, bilder und dokumente. Es darf nichts verloren gehen.'
       T_AUFTRAG='Lege in ~/eingang die Ordner pdf, bilder und dokumente an und verschiebe jede Datei in den passenden Ordner.' ;;
    13) T_KAPITEL='Missionen 11 bis 20 · Dateien und Textverarbeitung'; T_TITEL='Aufräumen ohne Kollateralschaden'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Du übernimmst das abgeschlossene Projekt beta unter /srv/projekte/beta. Olafs Programme haben überall temporäre Dateien mit der Endung .tmp hinterlassen, auch in Unterordnern. Die müssen weg. Alles andere bleibt bitte, wie es ist.'
       T_AUFTRAG='Lösche alle Dateien mit der Endung .tmp unter /srv/projekte/beta, auch in Unterordnern und versteckte. Alle anderen Dateien bleiben erhalten.' ;;
    14) T_KAPITEL='Missionen 11 bis 20 · Dateien und Textverarbeitung'; T_TITEL='Was war heute Nacht los?'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Heute Morgen gab es Beschwerden über fehlende Daten. Die Anwendung schreibt nach /var/log/saarwerk/app.log. Jede Zeile nennt in eckigen Klammern die Komponente.'
       T_AUFTRAG='Welche Komponente hat in der Nacht die meisten Fehler (ERROR) gemeldet? Trage ihren Namen ein.' ;;
    15) T_KAPITEL='Missionen 11 bis 20 · Dateien und Textverarbeitung'; T_TITEL='Was hat sich geändert?'; T_VON='Jonas Keller (Vertrieb)'
       T_TEXT='Seit letzter Woche kann ich im Portal keine größeren Angebote mehr hochladen. Alles über ein Megabyte bricht mit „413 Request Entity Too Large“ ab. Lena sagt, unter /var/backups/nginx liegt eine Sicherung der Webserver-Konfiguration vom 12.10., als noch alles ging.'
       T_AUFTRAG='Vergleiche die aktuelle nginx-Konfiguration mit der Sicherung, stelle den alten Wert wieder her und lass nginx die Konfiguration neu laden.' ;;
    16) T_KAPITEL='Missionen 11 bis 20 · Dateien und Textverarbeitung'; T_TITEL='Hinweis beim Anmelden'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Am Samstag, 24.10., ist von 06:00 bis 10:00 Uhr Wartung. Alle, die sich auf web01 anmelden, sollen das sofort sehen. Bitte ergänze die Login-Meldung in /etc/motd um einen Hinweis. Der bestehende Text muss bleiben.'
       T_AUFTRAG='Hänge an /etc/motd eine Zeile mit dem Wartungstermin an (Datum 24.10. und das Wort Wartung), ohne den vorhandenen Text zu verlieren.' ;;
    17) T_KAPITEL='Missionen 11 bis 20 · Dateien und Textverarbeitung'; T_TITEL='Die Telefonliste'; T_VON='Markus Brandt (Personalabteilung)'
       T_TEXT='Für das neue Organigramm brauche ich eine Liste aller Abteilungen, jede nur einmal und alphabetisch sortiert. Die Daten stecken in der Telefonliste unter /srv/verwaltung/telefonliste.csv. Bitte als ~/abteilungen.txt, eine Abteilung pro Zeile.'
       T_AUFTRAG='Erzeuge ~/abteilungen.txt mit allen Abteilungen aus der Telefonliste, jede genau einmal, alphabetisch sortiert, ohne die Kopfzeile.' ;;
    18) T_KAPITEL='Missionen 11 bis 20 · Dateien und Textverarbeitung'; T_TITEL='Die alte Datenbank'; T_VON='Daniel Otto (Datenbankteam)'
       T_TEXT='Am Freitag schalten wir den alten Datenbankserver 10.20.0.12 endgültig ab. Bitte prüft auf web01, ob unter /etc/saarwerk noch irgendetwas aktiv auf diese Adresse zeigt. Auskommentierte Altlasten sind egal.'
       T_AUFTRAG='Durchsuche /etc/saarwerk rekursiv nach 10.20.0.12 und melde die Datei, die die Adresse noch aktiv verwendet.' ;;
    19) T_KAPITEL='Missionen 11 bis 20 · Dateien und Textverarbeitung'; T_TITEL='Ab ins Archiv'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Projekt beta ist abgeschlossen. Bitte pack den kompletten Ordner /srv/projekte/beta in ein komprimiertes Archiv unter /srv/backup/projekt-beta.tar.gz. Den Ordner selbst löschen wir erst, wenn die Revision das Archiv abgenommen hat.'
       T_AUFTRAG='Erstelle /srv/backup/projekt-beta.tar.gz mit dem gesamten Projektordner und prüfe den Inhalt. Der Ordner bleibt bestehen.' ;;
    20) T_KAPITEL='Missionen 11 bis 20 · Dateien und Textverarbeitung'; T_TITEL='Der Monatsbericht'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Die Geschäftsführung will Zahlen zum Portal. Bitte werte das Zugriffsprotokoll /var/log/nginx/access.log aus und schreib mir drei Zeilen nach ~/berichte/oktober.txt: Wie viele Anfragen gab es insgesamt, wie viele davon endeten mit Status 404, und von welcher IP-Adresse kamen die meisten Anfragen.'
       T_AUFTRAG='Erstelle ~/berichte/oktober.txt mit genau diesen Zeilen: „Anfragen: N“, „Fehler 404: N“ und „Top-IP: ADRESSE“.' ;;
    21) T_KAPITEL='Missionen 21 bis 30 · Benutzer und Berechtigungen'; T_TITEL='Wer darf sich anmelden?'; T_VON='Stefanie Roth (Informationssicherheit)'
       T_TEXT='Für die Zugriffskontrolle brauche ich eine erste Zahl: Wie viele Konten auf web01 haben die Bash als Login-Shell? Dienstkonten mit nologin sollen nicht mitgezählt werden.'
       T_AUFTRAG='Werte /etc/passwd aus und melde, wie viele Konten /bin/bash als Login-Shell haben.' ;;
    22) T_KAPITEL='Missionen 21 bis 30 · Benutzer und Berechtigungen'; T_TITEL='Gehälter für alle'; T_VON='Markus Brandt (Personalabteilung)'
       T_TEXT='Peinlich: Ein Auszubildender hat mir erzählt, dass er die Gehaltsliste in der Austauschablage öffnen konnte. Die Datei liegt unter /srv/austausch/personal. Wir in der Personalabteilung müssen sie weiter bearbeiten können, sonst niemand.'
       T_AUFTRAG='Entziehe allen anderen Benutzern den Zugriff auf die Gehaltsliste, ohne der Personalabteilung den Zugriff zu nehmen.' ;;
    23) T_KAPITEL='Missionen 21 bis 30 · Benutzer und Berechtigungen'; T_TITEL='Das Prüfskript startet nicht'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Unser Skript zur Kontrolle der nächtlichen Sicherungen liegt unter /opt/saarwerk/bin/backup-check.sh. Seit dem Umzug auf den neuen Server lässt es sich nicht mehr starten. Bitte reparieren und einmal laufen lassen. Und bitte so, dass es weiterhin nur root ändern kann.'
       T_AUFTRAG='Mache das Skript ausführbar, ohne es für andere beschreibbar zu machen, und führe es aus.' ;;
    24) T_KAPITEL='Missionen 21 bis 30 · Benutzer und Berechtigungen'; T_TITEL='Uploads schlagen fehl'; T_VON='Jonas Keller (Vertrieb)'
       T_TEXT='Im Mitarbeiterportal kann ich keine Angebote mehr hochladen. Es kommt nur "Fehler 500". Früher ging das. Kannst du dir das ansehen?'
       T_AUFTRAG='Finde die Ursache im Fehlerprotokoll des Webservers und sorge dafür, dass der Webserver (Benutzer www-data) in das Upload-Verzeichnis schreiben kann.' ;;
    25) T_KAPITEL='Missionen 21 bis 30 · Benutzer und Berechtigungen'; T_TITEL='Neu im Projekt'; T_VON='Aylin Demir (Projektleitung Alpha)'
       T_TEXT='Jonas Keller aus dem Vertrieb arbeitet ab sofort im Projekt Alpha mit. Bitte nimm ihn in die Gruppe projekt-alpha auf. Wichtig: Er muss weiter auf die Vertriebsablage zugreifen können.'
       T_AUFTRAG='Füge den Benutzer jonas der Gruppe projekt-alpha hinzu, ohne ihm seine bisherigen Gruppen zu nehmen.' ;;
    26) T_KAPITEL='Missionen 21 bis 30 · Benutzer und Berechtigungen'; T_TITEL='Wer hat meine Datei gelöscht?'; T_VON='Petra Graf (Buchhaltung)'
       T_TEXT='In der gemeinsamen Ablage /srv/austausch/ablage kann jeder Dateien ablegen, das ist gewollt. Aber heute früh war meine Datei weg, gelöscht von jemand anderem! Es soll weiterhin jeder ablegen dürfen, aber nur noch eigene Dateien löschen.'
       T_AUFTRAG='Sorge dafür, dass in /srv/austausch/ablage weiterhin alle Dateien anlegen, aber nur noch eigene löschen oder umbenennen können.' ;;
    27) T_KAPITEL='Missionen 21 bis 30 · Benutzer und Berechtigungen'; T_TITEL='Ein Bit zu viel'; T_VON='Stefanie Roth (Informationssicherheit)'
       T_TEXT='Bei einer Stichprobe ist uns aufgefallen, dass auf saarwerk-web01 ein selbstgebautes Werkzeug mit SUID-Bit liegt. Damit läuft es mit den Rechten seines Besitzers, also root, egal wer es startet. Das ist ein Einfallstor.'
       T_AUFTRAG='Finde die Datei mit SUID-Bit, die nicht zum Betriebssystem gehört, und entferne dort das SUID-Bit. Die Systemprogramme müssen unverändert bleiben.' ;;
    28) T_KAPITEL='Missionen 21 bis 30 · Benutzer und Berechtigungen'; T_TITEL='Zu offen von Anfang an'; T_VON='Stefanie Roth (Informationssicherheit)'
       T_TEXT='Mir ist aufgefallen, dass neue Dateien von dir für alle Benutzer lesbar sind. Für Admin-Konten wollen wir das nicht. Bitte stell deine Standardrechte so ein, dass neue Dateien nur für dich und deine Gruppe lesbar sind und andere gar nichts dürfen. Sofort und dauerhaft.'
       T_AUFTRAG='Setze die umask auf 027, in der laufenden Sitzung und dauerhaft in ~/.bashrc.' ;;
    29) T_KAPITEL='Missionen 21 bis 30 · Benutzer und Berechtigungen'; T_TITEL=Ausgeschieden; T_VON='Markus Brandt (Personalabteilung)'
       T_TEXT='Olaf Brenner ist seit dem 30.09. nicht mehr bei uns. Bei der Prüfung ist aufgefallen, dass sein Konto auf web01 noch aktiv ist und sogar Administratorrechte hat. Bitte sperren. Löschen dürfen wir es wegen der Aufbewahrungspflicht noch nicht, seine Dateien müssen bleiben.'
       T_AUFTRAG='Sperre das Konto olaf, entziehe ihm die sudo-Rechte und behalte Konto und Home-Verzeichnis.' ;;
    30) T_KAPITEL='Missionen 21 bis 30 · Benutzer und Berechtigungen'; T_TITEL='Projekt Alpha'; T_VON='Aylin Demir (Projektleitung Alpha)'
       T_TEXT='Unser Projektordner /srv/projekte/alpha steht seit dem Start offen wie ein Scheunentor. Darin liegt ein Kundenvertrag. Ab heute sollen nur Mitglieder der Gruppe projekt-alpha zugreifen, dafür aber vollständig. Lena bleibt Besitzerin.'
       T_AUFTRAG='Ordne den Ordner samt Inhalt der Gruppe projekt-alpha zu, gib der Gruppe vollen Zugriff auf den Ordner und entziehe allen anderen jeden Zugriff.' ;;
    31) T_KAPITEL='Missionen 31 bis 40 · Prozesse, Dienste und Logs'; T_TITEL='Wie viele Arbeiter?'; T_VON='Sven Albers (Monitoring)'
       T_TEXT='Ich richte gerade die Prozessüberwachung für nginx ein. Dafür muss ich wissen, wie viele Worker-Prozesse nginx auf web01 neben dem Master-Prozess betreibt.'
       T_AUFTRAG='Ermittle die Anzahl der nginx-Worker-Prozesse und melde sie.' ;;
    32) T_KAPITEL='Missionen 31 bis 40 · Prozesse, Dienste und Logs'; T_TITEL='Der Webserver schweigt'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Das Mitarbeiterportal ist seit dem Neustart heute früh nicht erreichbar. Ich vermute, beim Wartungsfenster hat jemand an der nginx-Konfiguration gearbeitet.'
       T_AUFTRAG='Finde heraus, warum nginx nicht startet, korrigiere die Ursache und bringe das Portal wieder online.' ;;
    33) T_KAPITEL='Missionen 31 bis 40 · Prozesse, Dienste und Logs'; T_TITEL='Nach dem Neustart weg'; T_VON='Monitoring (automatischer Alarm)'
       T_TEXT='ALARM saarwerk-api: HTTP-Prüfung auf 10.20.0.15:8080 fehlgeschlagen seit 06:58 Uhr. Der Dienst lief bis zum Wartungsneustart am Wochenende ohne Probleme.'
       T_AUFTRAG='Bringe die Saarwerk-API wieder zum Laufen und sorge dafür, dass sie auch nach dem nächsten Neustart automatisch startet.' ;;
    34) T_KAPITEL='Missionen 31 bis 40 · Prozesse, Dienste und Logs'; T_TITEL='Ein Prozess frisst die CPU'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='saarwerk-web01 ist seit heute Nacht quälend langsam. Ich tippe auf einen Prozess, der hängen geblieben ist. Bitte finde ihn und beende ihn. Aber nur ihn: Webserver, SSH und API dürfen nicht unterbrochen werden.'
       T_AUFTRAG='Finde den Prozess mit der höchsten CPU-Last und beende ihn gezielt.' ;;
    35) T_KAPITEL='Missionen 31 bis 40 · Prozesse, Dienste und Logs'; T_TITEL='Rot im Journal'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Seit dem Neustart heute früh meldet irgendein Dienst Fehler, das sehe ich an der roten Lampe im Monitoring. Welcher, weiß ich nicht. Bitte schau ins Journal, aber nur auf die echten Fehler. Warnungen interessieren mich gerade nicht.'
       T_AUFTRAG='Filtere das Journal seit dem letzten Start nach Fehlermeldungen (Priorität err) und melde den Dienst, der sie verursacht.' ;;
    36) T_KAPITEL='Missionen 31 bis 40 · Prozesse, Dienste und Logs'; T_TITEL='Die Platte ist voll'; T_VON='Monitoring (automatischer Alarm)'
       T_TEXT='ALARM saarwerk-web01: Dateisystem / zu 100 % belegt. Folgefehler: saarwerk-api.service failed. Seit Freitag läuft der Export-Dienst im Debug-Modus. Ob das zusammenhängt?'
       T_AUFTRAG='Finde heraus, was die Platte füllt, schaffe mindestens 10 % Platz und bringe die API wieder zum Laufen.' ;;
    37) T_KAPITEL='Missionen 31 bis 40 · Prozesse, Dienste und Logs'; T_TITEL='Damit es nicht wieder passiert'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Die volle Platte war ein Weckruf. Die Protokolle unter /var/log/saarwerk werden nie rotiert und wachsen einfach weiter. Bitte richte eine Logrotation ein: täglich oder wöchentlich, mindestens sieben alte Stände aufheben, alte Stände komprimieren. Fehlende oder leere Dateien sollen keinen Fehler auslösen.'
       T_AUFTRAG='Lege /etc/logrotate.d/saarwerk für /var/log/saarwerk/*.log an und teste die Konfiguration mit logrotate im Probelauf.' ;;
    38) T_KAPITEL='Missionen 31 bis 40 · Prozesse, Dienste und Logs'; T_TITEL='Jeden Morgen um sieben'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Ich will das Prüfskript für die Sicherungen nicht mehr jeden Morgen von Hand starten. Bitte richte es so ein, dass /opt/saarwerk/bin/backup-check.sh jeden Tag um 7:00 Uhr als root läuft. Wir legen solche Aufträge immer als eigene Datei unter /etc/cron.d ab.'
       T_AUFTRAG='Lege unter /etc/cron.d eine Datei an, die backup-check.sh täglich um 07:00 Uhr als root ausführt.' ;;
    39) T_KAPITEL='Missionen 31 bis 40 · Prozesse, Dienste und Logs'; T_TITEL='Neu laden, nicht neu starten'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Ich habe in der nginx-Konfiguration einen Sicherheits-Header ergänzt. Bitte aktivier die Änderung. Aber Vorsicht: Gerade laden dreißig Kolleginnen und Kollegen Angebote hoch. Die Verbindungen dürfen nicht abreißen, also kein Neustart.'
       T_AUFTRAG='Prüfe die geänderte Konfiguration und aktiviere sie, ohne nginx neu zu starten.' ;;
    40) T_KAPITEL='Missionen 31 bis 40 · Prozesse, Dienste und Logs'; T_TITEL='Montag, 08:47 Uhr'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Alles brennt: Das Portal ist weg, und die API ist laut Monitoring nach dem letzten Neustart wieder nicht im Autostart. Mir hat jemand erzählt, dass heute früh „nur kurz etwas getestet“ wurde. Bitte bring beides in Ordnung, ohne an der nginx-Konfiguration herumzuschrauben, und schreib mir danach zwei Sätze, was los war, nach ~/berichte/montag.txt.'
       T_AUFTRAG='Bringe nginx wieder online, ohne die Konfiguration zu ändern, sichere den Autostart der API und dokumentiere die Ursache in ~/berichte/montag.txt.' ;;
    41) T_KAPITEL='Missionen 41 bis 50 · Netzwerke und Diagnose (Buchmissionen)'; T_TITEL='Wo hängt der Server?'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Für die Netzdokumentation brauche ich von jedem Server das Standard-Gateway, also den Router, über den er alles außerhalb des eigenen Netzes erreicht.'
       T_AUFTRAG='Ermittle das Standard-Gateway von saarwerk-web01 und trage die IP-Adresse ein.' ;;
    42) T_KAPITEL='Missionen 41 bis 50 · Netzwerke und Diagnose (Buchmissionen)'; T_TITEL='Das falsche Intranet'; T_VON='Jonas Keller (Vertrieb)'
       T_TEXT='Vom Portal aus funktioniert der Link zum Intranet nicht mehr. Auf meinem Laptop geht intranet.saarwerk.test aber problemlos. Seltsam, oder?'
       T_AUFTRAG='Finde heraus, warum saarwerk-web01 das Intranet nicht erreicht, und behebe die Ursache, ohne die übrigen Einträge zu zerstören.' ;;
    43) T_KAPITEL='Missionen 41 bis 50 · Netzwerke und Diagnose (Buchmissionen)'; T_TITEL='Nur für sich selbst'; T_VON='Sven Albers (Monitoring)'
       T_TEXT='Mein Monitoring auf 10.20.0.50 meldet die API als ausgefallen. Wenn ich mich auf dem Server anmelde und curl http://localhost:8080 aufrufe, kommt eine Antwort. Von außen aber nicht. Was ist da los?'
       T_AUFTRAG='Sorge dafür, dass die API wieder im Netz erreichbar ist.' ;;
    44) T_KAPITEL='Missionen 41 bis 50 · Netzwerke und Diagnose (Buchmissionen)'; T_TITEL='Die Firewall sagt nein'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Das Portal soll ab heute nur noch per HTTPS genutzt werden. nginx lauscht schon auf Port 443, aber aus dem Büronetz 10.20.0.0/24 kommt niemand durch. Und bitte nicht einfach die Firewall abschalten.'
       T_AUFTRAG='Erlaube HTTPS (Port 443/tcp) für das Büronetz, ohne die Firewall abzuschalten oder den SSH-Zugang zu verlieren.' ;;
    45) T_KAPITEL='Missionen 41 bis 50 · Netzwerke und Diagnose (Buchmissionen)'; T_TITEL=Funkstille; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Die Sicherheitsupdates laufen seit dem Netzumbau am Wochenende nicht mehr. Der Server erreicht unseren externen Paketspiegel mirror.example.org nicht. Intern funktioniert alles, sagt das Netzwerkteam. Bitte finde den Fehler, oder die Fehler.'
       T_AUFTRAG='Stelle die Verbindung zu https://mirror.example.org wieder her. Behebe die Ursachen, statt sie zu umgehen.' ;;
    46) T_KAPITEL='Missionen 41 bis 50 · Netzwerke und Diagnose (Buchmissionen)'; T_TITEL='Offen wie ein Scheunentor'; T_VON='Stefanie Roth (Informationssicherheit)'
       T_TEXT='Unser externer Scan hat auf web01 einen offenen Port 6379 gefunden. Das ist Redis, der Zwischenspeicher der API. Redis gehört nicht ins Netz, schon gar nicht ohne Kennwort. Die API läuft auf demselben Server und braucht Redis nur lokal.'
       T_AUFTRAG='Sorge dafür, dass Redis nur noch auf der lokalen Schnittstelle lauscht, und zwar dauerhaft. Redis und die API laufen danach weiter.' ;;
    47) T_KAPITEL='Missionen 41 bis 50 · Netzwerke und Diagnose (Buchmissionen)'; T_TITEL='Kurze Namen'; T_VON='Jonas Keller (Vertrieb)'
       T_TEXT='Die neuen Skripte vom Reporting rufen den Intranet-Server einfach als intranet auf, ohne Domain. Auf allen anderen Servern klappt das. Auf web01 kommt „Could not resolve host: intranet“. Mit dem vollen Namen intranet.saarwerk.test geht es.'
       T_AUFTRAG='Sorge dafür, dass web01 kurze Namen wie intranet in der Domäne saarwerk.test auflöst, ohne einzelne Namen fest einzutragen.' ;;
    48) T_KAPITEL='Missionen 41 bis 50 · Netzwerke und Diagnose (Buchmissionen)'; T_TITEL='Der Weg ins Labor'; T_VON='Dr. Mehmet Celik (Prüflabor)'
       T_TEXT='Unsere Messrechner stehen im Labornetz 10.30.0.0/24. Das Netz hängt nicht am normalen Gateway, sondern am Laborrouter 10.20.0.254. web01 soll ab heute die Messwerte vom Prüfstand 10.30.0.10 abholen, kommt aber nicht hin. Bitte dauerhaft einrichten, der Server wird jeden Monat neu gestartet.'
       T_AUFTRAG='Richte eine Route ins Labornetz 10.30.0.0/24 über 10.20.0.254 ein, sofort und dauerhaft in /etc/network/interfaces. Die Standardroute bleibt unverändert.' ;;
    49) T_KAPITEL='Missionen 41 bis 50 · Netzwerke und Diagnose (Buchmissionen)'; T_TITEL='Der Klon'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Der neue Webserver ist ein Klon von web01 und hat schon die Adresse 10.20.0.16. Er meldet sich aber noch als saarwerk-web01. Unsere Alarm-Mails tragen deshalb den falschen Absender, und sudo meckert seit dem Umbenennungsversuch von gestern. Er soll dauerhaft saarwerk-web02 heißen.'
       T_AUFTRAG='Benenne den Server dauerhaft in saarwerk-web02 um und sorge dafür, dass der neue Name lokal aufgelöst wird.' ;;
    50) T_KAPITEL='Missionen 41 bis 50 · Netzwerke und Diagnose (Buchmissionen)'; T_TITEL='Umzug ins Server-Netz'; T_VON='Lena Hoffmann (IT-Leitung)'
       T_TEXT='Heute Abend ziehen alle Server in das neue Server-Netz 10.20.10.0/24. web01 bekommt 10.20.10.15/24, das neue Gateway ist 10.20.10.1. Der DNS-Server 10.20.0.2 bleibt und ist über das Gateway erreichbar. Du arbeitest über die Notfallkonsole des Hypervisors, deine Sitzung bleibt also bestehen. Trotzdem: Sichere dich ab, bevor du das Netz umstellst. Und denk an die API, die an die alte Adresse gebunden ist.'
       T_AUFTRAG='Stelle web01 dauerhaft auf 10.20.10.15/24 mit Gateway 10.20.10.1 um, wende die Konfiguration an und bringe die API auf der neuen Adresse zum Laufen. Danach muss https://mirror.example.org erreichbar sein.' ;;
  esac
}

# ==========================================================================
#  Befehle
# ==========================================================================

nummer_pruefen() {
  [[ ${1:-} =~ ^[0-9]+$ ]] || abbruch "Bitte eine Missionsnummer angeben, zum Beispiel: sudo $0 $2 32"
  local n=$(( 10#$1 ))
  if (( n < 1 || n > 100 )); then abbruch "Missionen gibt es von 1 bis 100."; fi
  if (( n > MISSION_MAX )); then
    abbruch "Mission $n ist in dieser Version des Laborskripts ($VERSION) noch nicht enthalten. Eine aktuelle Fassung finden Sie im Downloadbereich zum Buch."
  fi
  MISSION=$n
}

installiert() {
  [ -e "$STATE/installiert" ] || abbruch "Das Labor ist noch nicht eingerichtet. Zuerst: sudo $0 install"
}

umbrechen() { fold -s -w "${1:-74}" | sed "s/^/${2:-}/;s/[[:space:]]*$//"; }

cmd_mission() {
  nummer_pruefen "${1:-}" mission
  installiert
  schritt "Ausgangszustand herstellen"
  grundzustand
  schritt "Fehler aus Mission $MISSION einbauen"
  if declare -F "m_$MISSION" >/dev/null; then "m_$MISSION"; fi
  echo "$MISSION" > "$STATE/aktiv"
  echo
  ticket "$MISSION"
  printf '%sMission %d: %s%s\n' "$C_HI" "$MISSION" "$T_TITEL" "$C_0"
  printf 'Von: %s\n\n' "$T_VON"
  printf '%s\n' "$T_TEXT" | umbrechen 74
  echo
  printf '%sAuftrag:%s ' "$C_HI" "$C_0"
  printf '%s\n' "$T_AUFTRAG" | umbrechen 74 | sed '2,$s/^/         /'
  echo
  if [ -n "${ANTWORT_SHA[$MISSION]:-}" ] || [ "$MISSION" = 21 ] || [ "$MISSION" = 31 ]; then
    info "Antwort prüfen:  sudo $0 check $MISSION \"IHRE ANTWORT\""
  else
    info "Ergebnis prüfen: sudo $0 check $MISSION"
  fi
}

cmd_check() {
  nummer_pruefen "${1:-}" check
  installiert
  ANTWORT=${2:-}
  local aktiv_nr; aktiv_nr=$(cat "$STATE/aktiv" 2>/dev/null)
  if [ -n "$aktiv_nr" ] && [ "$aktiv_nr" != "$MISSION" ]; then
    warnung "Eingebaut ist gerade Mission $aktiv_nr. Für Mission $MISSION zuerst: sudo $0 mission $MISSION"
  fi
  ticket "$MISSION"
  printf 'Mission %d: %s\n' "$MISSION" "$T_TITEL"
  "c_$MISSION"
  if (( ZIELE_OK == ZIELE_ALLE )); then
    printf '%sAlle Ziele erfüllt. Mission %d ist abgeschlossen.%s\n' "$C_OK" "$MISSION" "$C_0"
    return 0
  fi
  printf '%d von %d Zielen erfüllt.\n' "$ZIELE_OK" "$ZIELE_ALLE"
  return 1
}

cmd_reset() {
  installiert
  schritt "Eingebaute Fehler zurücknehmen"
  grundzustand
  info "Erledigt. saarwerk-web01 ist im Ausgangszustand."
  [ -n "${PAPIERKORB_GENUTZT:-}" ] &&
    info "Ihre Missionsergebnisse (z. B. ~/berichte) liegen unter ~/.saarwerk-lab-papierkorb."
  return 0
}

cmd_list() {
  local n kap='' aktiv_nr; aktiv_nr=$(cat "$STATE/aktiv" 2>/dev/null)
  for (( n = 1; n <= MISSION_MAX; n++ )); do
    ticket "$n"
    if [ "$T_KAPITEL" != "$kap" ]; then
      kap=$T_KAPITEL
      printf '\n%s%s%s\n' "$C_HI" "$kap" "$C_0"
    fi
    if [ "$n" = "$aktiv_nr" ]; then
      printf '  %3d  %s  %s(eingebaut)%s\n' "$n" "$T_TITEL" "$C_OK" "$C_0"
    else
      printf '  %3d  %s\n' "$n" "$T_TITEL"
    fi
  done
  echo
  if (( MISSION_MAX < 100 )); then
    info "Diese Fassung ($VERSION) enthält die Missionen 1 bis $MISSION_MAX. Die Missionen $(( MISSION_MAX + 1 )) bis 100 folgen."
  fi
}

cmd_status() {
  installiert
  local a; a=$(cat "$STATE/aktiv" 2>/dev/null)
  info "Saarwerk-Labor $VERSION, eingerichtet am $(cat "$STATE/installiert")"
  if [ -n "$a" ]; then ticket "$a"; info "Eingebaut: Mission $a ($T_TITEL)"; else info "Eingebaut: keine Mission (Ausgangszustand)"; fi
  echo
  pz() { if "$@" >/dev/null 2>&1; then printf '  %s[ok]%s    ' "$C_OK" "$C_0"; else printf '  %s[fehlt]%s ' "$C_ERR" "$C_0"; fi; }
  pz netz_ok; echo 'Simuliertes Firmennetz (Bridge eth0, Namensräume sw-netz und sw-labor)'
  pz systemctl is-active -q saarwerk-lab-dns; echo 'Nameserver 10.20.0.2'
  pz systemctl is-active -q saarwerk-lab-web; echo 'Intranet, Repository, Monitoring, Mirror'
  pz systemctl is-active -q saarwerk-lab-pruefstand; echo 'Prüfstand 10.30.0.10'
  uplink_lesen
  pz bash -c "ip route show default table $UPLINK_TABLE | grep -q ."; echo "Eigener Internetzugang über $REAL_IF (Tabelle uplink)"
  pz bash -c "ip netns exec $NS_NETZ dig +short +time=2 +tries=1 debian.org @10.20.0.2 | grep -q ."; echo 'Nameserver erreicht das Internet'
}

cmd_hilfe() {
  cat <<EOF
saarwerk-lab.sh $VERSION · Laborskript zum Buch „Linux-Administration lernen“

  sudo $0 install          Saarwerk-Umgebung einmalig einrichten
  sudo $0 mission NR       Den Fehler aus Mission NR einbauen
  sudo $0 check NR         Prüfen, ob die Ziele von Mission NR erfüllt sind
  sudo $0 check NR ANTWORT Antwort einer Mission prüfen
  sudo $0 reset            Alle eingebauten Fehler zurücknehmen
  sudo $0 list             Alle Missionen mit Titel anzeigen
  sudo $0 status           Zustand des Labors anzeigen
EOF
}

main() {
  local befehl=${1:-hilfe}; shift || true
  case $befehl in
    install) cmd_install "$@" ;;
    mission) pruefe_voraussetzungen; cmd_mission "$@" ;;
    check|pruefen) pruefe_voraussetzungen; cmd_check "$@" ;;
    reset) pruefe_voraussetzungen; cmd_reset ;;
    list|liste) cmd_list ;;
    status) pruefe_voraussetzungen; cmd_status ;;
    _netz-start) netz_start ;;
    _uplink) uplink_pflegen ;;
    hilfe|help|-h|--help) cmd_hilfe ;;
    *) cmd_hilfe; exit 2 ;;
  esac
}

main "$@"
