spb/cluster-skill
Public
HTML 57.2%
Shell 19.1%
Python 11.2%
CSS 7.4%
JavaScript 5.1%
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