#!/usr/bin/env bash
# ============================================================
# alpha-aisandbox — the wall between AI containers and this server, as root.
#
# Called by the panel through sudo. AI images are strangers' code that people
# run out of curiosity, and a container on the default bridge can reach every
# service the host listens on — MariaDB, Redis, the LXD bridge, other
# containers. This helper builds a network where none of that is reachable and
# the internet still is (models have to download from somewhere).
#
# What "sandboxed" concretely means here:
#   - its own bridge (172.30.99.0/24), never the default docker0
#   - INPUT: the host accepts nothing the sandbox initiates — replies to
#     connections the HOST opened (the panel talking to the model API) are the
#     only packets allowed in
#   - DOCKER-USER (forwarded): traffic to every private range is dropped —
#     10/8 (WireGuard, OpenVPN, LXD all live there), 172.16/12 (every other
#     docker bridge), 192.168/16, link-local, CGNAT. Sandbox-to-itself is
#     returned first so two AI containers may talk to each other.
#   - everything else (the public internet) is left alone
#
# ⚠️ ROOT-OWNED AND SELF-CONTAINED ON PURPOSE. It must never source anything
# from /var/www/alpha — that tree is writable by www-data, and root sourcing
# www-data-writable code is a straight path from "web bug" to "root shell".
#
# ⚠️ Rules do not survive a reboot; the network does. The @reboot cron re-arms,
# and the panel calls `ensure` before every runtime start as a belt-and-braces.
#
# Usage: alpha-aisandbox ensure | status | teardown
# ============================================================
set -u

NET=alpha-ai
SUBNET=172.30.99.0/24
BRIDGE=br-alphaai

# The five ranges nothing in the sandbox may talk to.
PRIVATE="10.0.0.0/8 172.16.0.0/12 192.168.0.0/16 169.254.0.0/16 100.64.0.0/10"

have_rule() { iptables -C "$@" 2>/dev/null; }

ensure() {
  command -v docker >/dev/null 2>&1 || { echo "ERR docker not installed"; exit 1; }
  docker network inspect "$NET" >/dev/null 2>&1 || \
    docker network create --subnet "$SUBNET" \
      -o com.docker.network.bridge.name="$BRIDGE" "$NET" >/dev/null || {
        echo "ERR could not create network $NET"; exit 1; }

  # INPUT — the host side. Replies first, then the wall. Position 1 and 2 so
  # nothing ufw manages sits in front of them.
  have_rule INPUT -s "$SUBNET" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT || \
    iptables -I INPUT 1 -s "$SUBNET" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
  have_rule INPUT -s "$SUBNET" -j DROP || \
    iptables -I INPUT 2 -s "$SUBNET" -j DROP

  # DOCKER-USER — the forwarded side. Intra-sandbox RETURN must sit ABOVE the
  # 172.16/12 drop, because the sandbox subnet is itself inside that range:
  # ordered wrong, the sandbox cannot even talk to itself.
  have_rule DOCKER-USER -s "$SUBNET" -d "$SUBNET" -j RETURN || \
    iptables -I DOCKER-USER 1 -s "$SUBNET" -d "$SUBNET" -j RETURN
  for dst in $PRIVATE; do
    have_rule DOCKER-USER -s "$SUBNET" -d "$dst" -j DROP || \
      iptables -I DOCKER-USER 2 -s "$SUBNET" -d "$dst" -j DROP
  done
  echo "OK sandbox armed ($NET $SUBNET)"
}

status() {
  local ok=1
  docker network inspect "$NET" >/dev/null 2>&1 && echo "network OK" || { echo "network MISSING"; ok=0; }
  have_rule INPUT -s "$SUBNET" -j DROP && echo "input-wall OK" || { echo "input-wall MISSING"; ok=0; }
  local n=0
  for dst in $PRIVATE; do
    have_rule DOCKER-USER -s "$SUBNET" -d "$dst" -j DROP && n=$((n+1))
  done
  [ "$n" -eq 5 ] && echo "forward-wall OK" || { echo "forward-wall MISSING ($n/5)"; ok=0; }
  [ "$ok" -eq 1 ] && echo "ARMED" || echo "DISARMED"
}

teardown() {
  iptables -D INPUT -s "$SUBNET" -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT 2>/dev/null
  iptables -D INPUT -s "$SUBNET" -j DROP 2>/dev/null
  iptables -D DOCKER-USER -s "$SUBNET" -d "$SUBNET" -j RETURN 2>/dev/null
  for dst in $PRIVATE; do
    iptables -D DOCKER-USER -s "$SUBNET" -d "$dst" -j DROP 2>/dev/null
  done
  docker network rm "$NET" >/dev/null 2>&1
  echo "OK sandbox removed"
}

case "${1:-}" in
  ensure)   ensure ;;
  status)   status ;;
  teardown) teardown ;;
  *) echo "usage: alpha-aisandbox ensure|status|teardown"; exit 2 ;;
esac
