SPB Git forge
3commits 1branches 0releases
2.9 MBsize
maindefault branch
6 h agolast push
HTML 57.2% Shell 19.1% Python 11.2% CSS 7.4% JavaScript 5.1%
7.2 KB · 124 lines
Raw Blame History
1#!/usr/bin/env bash2# tunnelctl — MacLustr Tunnel : gestion des pairs WireGuard et des routes Caddy sur la passerelle publique (R9128).3# Usage :4#   tunnelctl peer add <alias> <pubkey>        ajoute/remplace un Mac (IP fixe tirée de /etc/maclustr-tunnel/ipmap)5#   tunnelctl peer rm <alias>6#   tunnelctl peer ls                          pairs + dernier handshake7#   tunnelctl add <domaine> <alias|ip>:<port> [<alias|ip>:<port>…] [--no-tls] [--websocket]8#                                              route publique https://<domaine> → upstream(s) via WireGuard (LB + health si plusieurs)9#   tunnelctl redirect <domaine> <cible>       redirection 308 (ex. apex → www)10#   tunnelctl rm <domaine>11#   tunnelctl ls                               routes publiques12#   tunnelctl status                           wg + caddy + routes13#   tunnelctl json                             même chose en JSON (pour maclustr-agentd)14set -euo pipefail15ETC=/etc/maclustr-tunnel16IPMAP=$ETC/ipmap17PEERS=/etc/wireguard/peers.d18SITES=/etc/caddy/sites19WG_IF=wg020PUBLIC_IP=${PUBLIC_IP:-$(cat /etc/maclustr-tunnel/public_ip 2>/dev/null || echo 51.255.75.61)}21[ "$(id -u)" = 0 ] || exec sudo -E "$0" "$@"22mkdir -p "$ETC" "$PEERS" "$SITES"2324die(){ echo "tunnelctl: $*" >&2; exit 1; }25ip_of(){ awk -v a="$1" '$1==a{print $2}' "$IPMAP"; }26alias_of(){ awk -v ip="$1" '$2==ip{print $1}' "$IPMAP"; }27resolve_upstream(){ # alias:port | ip:port  -> ip:port28  local hp=$1 h=${1%%:*} p=${1##*:}29  [ "$h" != "$p" ] || die "upstream « $hp » : format <alias|ip>:<port>"30  if [[ "$h" =~ ^[0-9.]+$ ]]; then echo "$h:$p"; else local ip; ip=$(ip_of "$h"); [ -n "$ip" ] || die "alias inconnu « $h » (voir $IPMAP)"; echo "$ip:$p"; fi31}32wg_sync(){ # reconstruit wg0.conf = [Interface] + peers.d/*.conf puis applique sans couper les tunnels33  local conf=/etc/wireguard/$WG_IF.conf tmp; tmp=$(mktemp)34  awk '/^\[Peer\]/{exit} {print}' "$conf" > "$tmp"35  for f in "$PEERS"/*.conf; do [ -f "$f" ] && { echo; cat "$f"; } >> "$tmp"; done36  install -m 600 "$tmp" "$conf"; rm -f "$tmp"37  wg syncconf "$WG_IF" <(wg-quick strip "$WG_IF")38}39caddy_reload(){ caddy validate --config /etc/caddy/Caddyfile --adapter caddyfile >/dev/null && systemctl reload caddy; }4041cmd=${1:-status}; shift || true42case "$cmd" in43  peer)44    sub=${1:-ls}; shift || true45    case "$sub" in46      add)47        a=${1:?alias}; pk=${2:?pubkey}; ip=$(ip_of "$a"); [ -n "$ip" ] || die "alias « $a » absent de $IPMAP"48        printf "[Peer]\n# %s\nPublicKey = %s\nAllowedIPs = %s/32\n" "$a" "$pk" "$ip" > "$PEERS/$a.conf"49        wg_sync; echo "pair $a → $ip ajouté" ;;50      rm) a=${1:?alias}; rm -f "$PEERS/$a.conf"; wg_sync; echo "pair $a retiré" ;;51      ls)52        wg show "$WG_IF" dump | tail -n +2 | while IFS=$'\t' read -r pk psk ep allowed hs rx tx ka; do53          ip=${allowed%/32}; a=$(alias_of "$ip"); age="jamais"; [ "$hs" != 0 ] && age="$(( $(date +%s) - hs )) s"54          printf "%-8s %-12s %-22s handshake %-10s rx %6.1f Mo tx %6.1f Mo\n" "${a:-?}" "$ip" "${ep:-—}" "$age" "$(echo "$rx/1048576" | bc -l)" "$(echo "$tx/1048576" | bc -l)"55        done ;;56      *) die "peer add|rm|ls" ;;57    esac ;;58  add)59    d=${1:?domaine}; shift; ups=(); tls=1; ws=060    for x in "$@"; do case "$x" in --no-tls) tls=0;; --websocket) ws=1;; *) ups+=("$(resolve_upstream "$x")");; esac; done61    [ ${#ups[@]} -gt 0 ] || die "au moins un upstream"62    {63      [ $tls = 1 ] && echo "$d {" || echo "http://$d {"64      echo "	import maclustr_errors"   # page MacLustr « service indisponible » (502/503/504) avec contact@spboucher.ai65      echo "	encode zstd gzip"66      echo "	log"   # journal d accès → journald (journalctl -u caddy), filtrable par request.host67      echo "	reverse_proxy ${ups[*]} {"68      if [ ${#ups[@]} -gt 1 ]; then69        echo "		lb_policy first"; echo "		lb_try_duration 5s"70        echo "		health_uri /"; echo "		health_interval 10s"; echo "		health_timeout 4s"; echo "		health_status 2xx 3xx 4xx"71      fi72      echo "		header_up X-Forwarded-Proto {scheme}"; echo "		header_up X-Real-IP {remote_host}"73      echo "		transport http {"; echo "			dial_timeout 5s"; echo "			response_header_timeout 300s"; echo "		}"74      echo "	}"75      echo "}"76    } > "$SITES/$d.caddy"77    if caddy_reload; then echo "route https://$d → ${ups[*]} active (TLS Let's Encrypt automatique si le DNS de $d pointe vers $PUBLIC_IP)"; else rm -f "$SITES/$d.caddy"; caddy_reload || true; die "Caddyfile invalide, route annulée"; fi ;;78  redirect) # tunnelctl redirect <domaine-source> <domaine-cible>  (ex. apex → www), 308 permanent, TLS auto79    s=${1:?source}; t=${2:?cible}80    printf "%s {\n\tredir https://%s{uri} permanent\n}\n" "$s" "$t" > "$SITES/$s.caddy"81    if caddy_reload; then echo "redirection https://$s → https://$t active"; else rm -f "$SITES/$s.caddy"; caddy_reload || true; die "Caddyfile invalide, redirection annulée"; fi ;;82  rm) d=${1:?domaine}; rm -f "$SITES/$d.caddy"; caddy_reload; echo "route $d retirée" ;;83  ls)84    for f in "$SITES"/*.caddy; do [ -f "$f" ] || continue; d=$(basename "$f" .caddy); up=$(awk '/reverse_proxy/{ $1=""; sub(/ *\{ *$/,""); print }' "$f" | sed 's/^ *//'); printf "%-36s → %s\n" "$d" "$up"; done ;;85  json) # état machine-lisible (consommé par maclustr-agentd → apps MacLustr)86    GW_NAME=${GW_NAME:-$(hostname -s | tr a-z A-Z)}87    wg show "$WG_IF" dump 2>/dev/null | tail -n +2 > /tmp/.wgdump.$$ || true88    python3 - "$GW_NAME" "$PUBLIC_IP" "$IPMAP" "$SITES" "/tmp/.wgdump.$$" "$(wg show $WG_IF listen-port 2>/dev/null)" "$(systemctl is-active caddy 2>/dev/null)" <<'PY'89import sys, json, glob, os, re, time90name, pub_ip, ipmap, sites, dump, port, caddy = sys.argv[1:8]91alias_of = {}92for line in open(ipmap):93    p = line.split()94    if len(p) >= 2 and not line.startswith('#'): alias_of[p[1]] = p[0]95peers = []96now = int(time.time())97for line in open(dump):98    f = line.rstrip('\n').split('\t')99    if len(f) < 8: continue100    ip = f[3].replace('/32', '')101    hs = int(f[4] or 0)102    peers.append({"alias": alias_of.get(ip, "?"), "ip": ip, "endpoint": f[2] if f[2] != '(none)' else None,103                  "handshakeS": (now - hs) if hs else None, "rxBytes": int(f[5] or 0), "txBytes": int(f[6] or 0)})104routes = []105for fn in sorted(glob.glob(os.path.join(sites, '*.caddy'))):106    dom = os.path.basename(fn)[:-6]; txt = open(fn).read()107    m = re.search(r'redir\s+(\S+)', txt)108    if m:109        routes.append({"domain": dom, "kind": "redirect", "target": m.group(1).replace('{uri}', ''), "upstreams": []})110        continue111    m = re.search(r'reverse_proxy\s+([^{\n]+)', txt)112    ups = m.group(1).split() if m else []113    routes.append({"domain": dom, "kind": "proxy", "upstreams": [{"addr": u, "alias": alias_of.get(u.split(':')[0], None)} for u in ups]})114print(json.dumps({"gateway": name, "ip": pub_ip, "ts": now,115                  "wg": {"listenPort": int(port or 0), "peers": peers},116                  "caddy": {"active": caddy == "active", "routes": routes}}, ensure_ascii=False))117PY118    rm -f /tmp/.wgdump.$$ ;;119  status)120    echo "== WireGuard $WG_IF ($(wg show $WG_IF listen-port 2>/dev/null) udp) — $(ls "$PEERS"/*.conf 2>/dev/null | wc -l) pairs"; "$0" peer ls121    echo; echo "== Caddy : $(systemctl is-active caddy) — $(ls "$SITES"/*.caddy 2>/dev/null | wc -l) routes"; "$0" ls ;;122  *) sed -n '2,12p' "$0"; exit 1 ;;123esac124