#!/bin/sh
# Clawdie Shell — PF Firewall Module
# Purpose: Configure PF with block-all default, SSH protection, jail NAT, glasspane VNC
# POSIX-compliant (no bash-isms)

set -eu

# Configuration (can be overridden for testing)
RC_CONF="${RC_CONF:-/etc/rc.conf}"
LOG_FILE="${LOG_FILE:-/var/log/clawdie-firstboot.log}"
PROGRESS_FILE="${PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}"
PF_CONF="${PF_CONF:-/etc/pf.conf}"
PF_RELOAD_RCD="${PF_RELOAD_RCD:-/usr/local/etc/rc.d/pf_reload}"

# Inputs (caller sets these)
# ASSISTANT_NAME  - used for jail identity (not bridge naming; bridge is always warden0)
# AGENT_NET       - jail subnet (default: 192.168.100.0/24)

# ============================================================================
# MAIN ENTRY POINT
# ============================================================================

clawdie_shell_pf() {
  local LOG_FILE="${LOG_FILE:-/var/log/clawdie-firstboot.log}"
  local PROGRESS_FILE="${PROGRESS_FILE:-/var/log/clawdie-firstboot.progress}"
  local RC_CONF="${RC_CONF:-/etc/rc.conf}"
  local AGENT_NET="${AGENT_NET:-192.168.100.0/24}"

  log_msg "[pf] Setting up firewall..."

  # Bridge is always warden0 (matches AGENTS.md bridge naming convention)
  local BRIDGE="warden0"
  local NET_ADDR="${AGENT_NET%/*}"
  local NET_PREFIX="${AGENT_NET#*/}"
  local BRIDGE_IP
  BRIDGE_IP="$(echo "$NET_ADDR" | awk -F. '{print $1"."$2"."$3".1"}')"

  # Detect external interface via default route
  local EXT_IF
  EXT_IF=$(route -n get default 2>/dev/null | awk '/interface:/ {print $2}')
  if [ -z "$EXT_IF" ]; then
    log_msg "[pf] ERROR: Could not detect external interface"
    return 1
  fi
  log_msg "[pf] ext_if=${EXT_IF}, bridge=${BRIDGE}, net=${AGENT_NET}"

  # Configure agent bridge interface
  # Bridge warden0 — multi-tenant: each agent gets its own bridge + /24 (warden0, warden1, ...)
  printf 'cloned_interfaces="bridge0"\n'          >> "$RC_CONF"
  printf 'ifconfig_bridge0_name="%s"\n' \
      "$BRIDGE"                                   >> "$RC_CONF"
  printf 'ifconfig_%s="inet %s/%s up"\n' \
      "$BRIDGE" "$BRIDGE_IP" "$NET_PREFIX"        >> "$RC_CONF"
  printf 'gateway_enable="YES"\n'                 >> "$RC_CONF"

  # Write pf.conf
  # NAT supernet 192.168.0.0/16 covers all agent subnets — adding a second
  # agent later requires no PF changes, just a new bridge + /24
  local ssh_public_rule
  local tailscale_block=""
  local tailscale_wireguard_rules=""
  if [ "${FEATURE_TAILSCALE:-NO}" = "YES" ] && [ -n "${TAILSCALE_AUTHKEY:-}" ]; then
    ssh_public_rule="block in quick on \$ext_if proto tcp to port 22"
    tailscale_block="# SSH is tailnet-only when installer auths Tailscale during first boot"
    tailscale_wireguard_rules='pass in quick on $ext_if proto udp to port 41641 keep state
pass in quick on $ext_if inet6 proto udp to port 41641 keep state'
  else
    ssh_public_rule="pass in quick on \$ext_if proto tcp to port 22 keep state (max-src-conn 5, max-src-conn-rate 3/60, overload <bruteforce> flush global)"
  fi
  cat > "$PF_CONF" << EOF
# Clawdie-AI firewall — generated by clawdie-firstboot $(date '+%d.%b.%Y' | tr 'A-Z' 'a-z')
# See NETWORKING.md for architecture notes and Tailscale integration path

ext_if="${EXT_IF}"

tailscale_if="tailscale0"

# Multi-tenant: covers all agent subnets — no PF change needed when adding agents
agent_nets="192.168.0.0/16"
# Brute force protection table — blocked after 3 attempts / 60s
table <bruteforce> persist

# NAT for agent jails
nat on \$ext_if from \$agent_nets to any -> (\$ext_if)
# Default: block everything
block all
# Allow established outbound
pass out all keep state
# Agent jail egress
pass quick inet from \$agent_nets to any keep state
# SSH — rate-limited, brute force offenders auto-blocked
block in quick from <bruteforce>
${ssh_public_rule}
# Web (HTTP for Let's Encrypt renewals, HTTPS for agent UI)
pass in quick on \$ext_if proto tcp to port { 80, 443 } keep state
# Tailscale remote access (SSH tunnel to loopback-bound controlplane)
pass in quick on \$tailscale_if proto tcp to port 22 keep state
${tailscale_block}
${tailscale_wireguard_rules}
EOF

  # Install pf_reload rc.d service
  # Fixes the PF/Tailscale cold boot race — see NETWORKING.md: "The cold boot race"
  # Pre-installed even without Tailscale: harmless now, essential when agent adds it
  cat > "$PF_RELOAD_RCD" << 'RCEOF'
#!/bin/sh
# PROVIDE: pf_reload
# REQUIRE: tailscaled
# KEYWORD: shutdown

. /etc/rc.subr
name="pf_reload"
rcvar="pf_reload_enable"
start_cmd="pf_reload_start"
stop_cmd=":"
pf_reload_start() {
    pfctl -f /etc/pf.conf
}
load_rc_config "$name"
run_rc_command "$1"
RCEOF
  chmod 755 "$PF_RELOAD_RCD"
  # Enable PF and pf_reload
  printf 'pf_enable="YES"\n'        >> "$RC_CONF"
  printf 'pf_reload_enable="YES"\n' >> "$RC_CONF"
  log_msg "[pf] Firewall configured: ext_if=${EXT_IF}, bridge=${BRIDGE}"
  if [ "${FEATURE_TAILSCALE:-NO}" = "YES" ] && [ -n "${TAILSCALE_AUTHKEY:-}" ]; then
    log_msg "[pf] SSH restricted to tailscale0; public port 22 blocked"
  else
    log_msg "[pf] Public SSH remains enabled on ${EXT_IF}"
  fi
  echo "[PF] COMPLETE" >> "$PROGRESS_FILE"
}

# ============================================================================
# LOGGING HELPER
# ============================================================================

log_msg() {
  echo "$(date '+%H:%M:%S') $1" | tee -a "$LOG_FILE" 2>/dev/null || true
}

# Only run if executed directly (not during test)
if [ "${SHELL_PF_TEST:-0}" -eq 0 ] && [ "${0##*/}" = "shell-pf.sh" ]; then
  clawdie_shell_pf
fi
