#!/usr/bin/env bash
# naiveproxy-bootstrap v1.0
# One-shot provisioning of a NaiveProxy (Caddy + forwardproxy) server.
# Supports: Debian 12+, Ubuntu 22.04+. Run as root.

set -euo pipefail

VERSION="1.0"

log()  { echo; echo "[$(date +%H:%M:%S)] $*"; }
ok()   { echo "  [OK]   $*"; }
warn() { echo "  [WARN] $*"; }
die()  { echo; echo "ERROR: $*" >&2; exit 1; }

STEP="init"
trap 'die "Failed at line $LINENO during step: ${STEP:-unknown}"' ERR

# === Preflight ===
STEP="preflight"

[[ $EUID -ne 0 ]] && die "Must run as root on a fresh VPS"
command -v apt-get >/dev/null 2>&1 || die "Supports only Debian/Ubuntu family"

export DEBIAN_FRONTEND=noninteractive

log "naiveproxy-bootstrap v$VERSION"

if ! command -v sudo >/dev/null 2>&1; then
    ok "installing sudo (missing on this image)"
    apt-get update -qq
    apt-get install -y -qq sudo
fi

apt-get install -y -qq lsb-release >/dev/null 2>&1 || true
OS_ID=$(lsb_release -si 2>/dev/null | tr 'A-Z' 'a-z' || echo unknown)
OS_CODENAME=$(lsb_release -sc 2>/dev/null || echo unknown)
case "$OS_ID" in
    debian|ubuntu) ok "OS: $OS_ID $OS_CODENAME" ;;
    *) die "Unsupported OS: $OS_ID (need Debian 12+ or Ubuntu 22.04+)" ;;
esac

PUBLIC_IP=""
for svc in ifconfig.me api.ipify.org ipinfo.io/ip icanhazip.com; do
    PUBLIC_IP=$(curl -fsS -4 --max-time 5 "https://$svc" 2>/dev/null || true)
    [ -n "$PUBLIC_IP" ] && break
done
[ -n "$PUBLIC_IP" ] || die "Cannot auto-detect public IP"
ok "Public IP: $PUBLIC_IP"

# IPv6 CIDR from the default route interface — used later for Suricata HOME_NET.
# Empty if the VPS is IPv4-only, handled gracefully in Step S.
PUBLIC_IP6_CIDR=$(ip -6 -o addr show scope global 2>/dev/null | awk '{print $4}' | head -1)
if [ -n "$PUBLIC_IP6_CIDR" ]; then
    ok "Public IPv6: $PUBLIC_IP6_CIDR"
else
    ok "No global IPv6 detected (v4-only)"
fi

# === Banner ===
cat <<'BANNER'

===================================================================
  NaiveProxy Server Bootstrap
===================================================================
  This will set up a NaiveProxy server on this VPS:

    1. Create 'default' user with SSH key (root SSH disabled)
    2. Harden OS: updates, SSH, UFW, fail2ban, journald
    3. Build Caddy with forwardproxy (naive) plugin
    4. Deploy vpn-agent for user management API
    5. Open firewall: 22/tcp, 443/tcp, 80/tcp (ACME)
    6. Start services and verify

  After completion, point your domain's DNS A record to this
  server's IP (no CF proxy / grey cloud) and configure your
  Slack bot to manage users via the /api endpoint.
===================================================================

BANNER

read_tty() {
    local prompt="$1"
    local val
    read -r -p "$prompt" val < /dev/tty
    echo "$val"
}
confirm_tty() {
    local reply
    read -r -n 1 -p "Continue? [y/N]: " reply < /dev/tty
    echo
    [[ "$reply" =~ ^[Yy]$ ]]
}

confirm_tty || die "Aborted by user"

# === Input collection ===
STEP="input collection"

SSH_PUBKEY="${NODE_SSH_PUBKEY:-}"
DOMAIN="${NODE_DOMAIN:-}"
AGENT_TOKEN="${NODE_AGENT_TOKEN:-}"

if [ -z "$SSH_PUBKEY" ]; then
    echo
    echo "Step 1/3: SSH public key for user 'default'"
    echo "  One line, starts with 'ssh-rsa' or 'ssh-ed25519'."
    SSH_PUBKEY=$(read_tty "  > ")
fi
case "$SSH_PUBKEY" in
    "ssh-rsa "*|"ssh-ed25519 "*|"ssh-dss "*|"ecdsa-"*) ok "SSH pubkey looks valid" ;;
    *) die "SSH pubkey format is wrong" ;;
esac

if [ -z "$DOMAIN" ]; then
    echo
    echo "Step 2/3: Domain name for this server"
    echo "  DNS A record must point to $PUBLIC_IP (no CF proxy)."
    echo "  Example: vpn.example.com"
    DOMAIN=$(read_tty "  > ")
fi
[ -n "$DOMAIN" ] || die "Domain is required"
ok "Domain: $DOMAIN"

if [ -z "$AGENT_TOKEN" ]; then
    echo
    echo "Step 3/3: Agent API token"
    echo "  This token authenticates your Slack bot to manage users."
    echo "  Generate one: openssl rand -base64 32"
    echo "  Or press Enter to auto-generate."
    AGENT_TOKEN=$(read_tty "  > ")
    if [ -z "$AGENT_TOKEN" ]; then
        AGENT_TOKEN=$(openssl rand -base64 32)
        ok "Auto-generated token: $AGENT_TOKEN"
        echo "  Save this token — you'll need it for the Slack bot config."
    fi
fi
ok "Agent token set (${#AGENT_TOKEN} chars)"

echo
log "Inputs collected, starting provisioning"

# === Step A: default user ===
STEP="user creation"
log "Creating 'default' user"

if ! id default >/dev/null 2>&1; then
    useradd -m -s /bin/bash default
    ok "user created"
else
    ok "user already exists"
fi
passwd -l default >/dev/null 2>&1 || true

install -d -o default -g default -m 700 /home/default/.ssh
touch /home/default/.ssh/authorized_keys
chown default:default /home/default/.ssh/authorized_keys
chmod 600 /home/default/.ssh/authorized_keys

if ! grep -qxF "$SSH_PUBKEY" /home/default/.ssh/authorized_keys; then
    echo "$SSH_PUBKEY" >> /home/default/.ssh/authorized_keys
    ok "SSH key appended"
else
    ok "SSH key already present"
fi

cat > /etc/sudoers.d/90-default-nopasswd <<'EOF'
default ALL=(ALL) NOPASSWD:ALL
EOF
chmod 0440 /etc/sudoers.d/90-default-nopasswd
visudo -cf /etc/sudoers.d/90-default-nopasswd >/dev/null || die "sudoers validation failed"
ok "sudoers configured"

for u in debian ubuntu; do
    if id "$u" >/dev/null 2>&1 && [ "$u" != "default" ]; then
        pkill -KILL -u "$u" 2>/dev/null || true
        sleep 1
        userdel -r "$u" >/dev/null 2>&1 || true
        ok "removed cloud-init user: $u"
    fi
done
rm -f /etc/sudoers.d/90-cloud-init-users

# === Step B: system update ===
STEP="system update"
log "Updating packages"
apt-get update -qq
apt-get upgrade -y -qq >/dev/null
apt-get autoremove -y -qq >/dev/null
ok "system updated"

# === Step C: base packages ===
STEP="base packages"
log "Installing base packages"
apt-get install -y -qq \
    curl wget git vim nano htop jq ncdu \
    net-tools dnsutils ca-certificates \
    gnupg apt-transport-https software-properties-common \
    unzip ufw fail2ban unattended-upgrades tzdata \
    logrotate needrestart >/dev/null
ok "base packages installed"

# === Step D: timezone ===
STEP="timezone"
timedatectl set-timezone UTC
ok "timezone: UTC"

# === Step E: kernel tuning ===
STEP="sysctl tuning"
log "Applying sysctl tuning"

cat > /etc/sysctl.d/99-naive-node.conf <<'EOF'
net.core.rmem_max = 67108864
net.core.wmem_max = 67108864
net.ipv4.tcp_rmem = 4096 87380 67108864
net.ipv4.tcp_wmem = 4096 65536 67108864
net.core.default_qdisc = fq
net.ipv4.tcp_congestion_control = bbr
fs.file-max = 1048576
net.ipv4.tcp_fastopen = 3
EOF

modprobe tcp_bbr 2>/dev/null || warn "tcp_bbr module load failed"
echo tcp_bbr > /etc/modules-load.d/bbr.conf
sysctl -p /etc/sysctl.d/99-naive-node.conf >/dev/null
ok "sysctl tuning applied (BBR + fq)"

# === Step F: unattended upgrades ===
STEP="unattended upgrades"
log "Enabling unattended security upgrades"

cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF'
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::AutocleanInterval "7";
APT::Periodic::Unattended-Upgrade "1";
EOF

cat > /etc/apt/apt.conf.d/50unattended-upgrades <<'EOF'
Unattended-Upgrade::Origins-Pattern {
    "origin=Debian,codename=${distro_codename},label=Debian";
    "origin=Debian,codename=${distro_codename},label=Debian-Security";
    "origin=Debian,codename=${distro_codename}-security,label=Debian-Security";
    "origin=Ubuntu,archive=${distro_codename}-security";
};
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "03:00";
EOF

systemctl enable --now unattended-upgrades >/dev/null 2>&1 || true
ok "unattended upgrades enabled"

# === Step G: SSH hardening ===
STEP="SSH hardening"
log "Hardening sshd"

rm -f /etc/ssh/sshd_config.d/50-cloud-init.conf

cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF'
PasswordAuthentication no
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
PermitRootLogin no
PubkeyAuthentication yes
X11Forwarding no
MaxAuthTries 3
ClientAliveInterval 300
ClientAliveCountMax 2
LoginGraceTime 30
EOF

if sshd -t; then
    systemctl restart ssh
    ok "sshd hardened, root SSH disabled"
else
    rm -f /etc/ssh/sshd_config.d/99-hardening.conf
    die "sshd config validation failed, rolled back"
fi

# === Step H: UFW ===
STEP="UFW"
log "Configuring UFW"

ufw default deny incoming >/dev/null
ufw default allow outgoing >/dev/null
ufw limit 22/tcp comment 'SSH (rate-limited)' >/dev/null
ufw allow 443/tcp comment 'NaiveProxy + ACME' >/dev/null
ufw allow 80/tcp comment 'ACME HTTP challenge' >/dev/null
ufw --force enable >/dev/null 2>&1
ok "UFW active: 22/tcp rate-limited, 443/tcp, 80/tcp"

# === Step I: fail2ban ===
STEP="fail2ban"
log "Configuring fail2ban"

cat > /etc/fail2ban/jail.d/sshd.local <<'EOF'
[sshd]
enabled = true
port = ssh
backend = systemd
maxretry = 3
findtime = 10m
bantime = 1h
EOF

systemctl enable --now fail2ban >/dev/null 2>&1 || true
systemctl restart fail2ban
sleep 2
if systemctl is-active fail2ban >/dev/null; then
    ok "fail2ban running"
else
    warn "fail2ban not active"
fi

# === Step J: journald cap ===
STEP="journald"
mkdir -p /etc/systemd/journald.conf.d
cat > /etc/systemd/journald.conf.d/size.conf <<'EOF'
[Journal]
SystemMaxUse=500M
SystemKeepFree=1G
EOF
systemctl restart systemd-journald
ok "journald capped at 500M"

# === Step K: Install Go ===
# MANUAL MAINTENANCE: bump GO_VERSION and GO_SHA256 together when upgrading.
# Fetch new sha256 from: https://go.dev/dl/?mode=json&include=all
# CLAUDE.md §4.3 requires verified downloads; this hash is the verification.
STEP="golang"
log "Installing Go (for building Caddy)"

GO_VERSION="1.23.8"
GO_SHA256="45b87381172a58d62c977f27c4683c8681ef36580abecd14fd124d24ca306d3f"
curl -fsSL -o /tmp/go.tar.gz "https://go.dev/dl/go${GO_VERSION}.linux-amd64.tar.gz"
echo "${GO_SHA256}  /tmp/go.tar.gz" | sha256sum -c - >/dev/null || die "Go tarball sha256 mismatch — possible supply chain issue"
rm -rf /usr/local/go && tar -C /usr/local -xzf /tmp/go.tar.gz
rm -f /tmp/go.tar.gz
export PATH=$PATH:/usr/local/go/bin
ok "Go $(/usr/local/go/bin/go version | awk '{print $3}') (sha256 verified)"

# === Step L: Build Caddy with forwardproxy ===
# MANUAL MAINTENANCE: bump FORWARDPROXY_COMMIT when upstream klzgrad/forwardproxy
# gets new commits on the 'naive' branch. Check:
#   curl -fsSL https://api.github.com/repos/klzgrad/forwardproxy/commits/naive
# The branch name alone is NOT pinned — commit SHA is required (CLAUDE.md §4.3,
# supply chain: we don't trust a mutable branch ref for a plugin that handles
# user traffic).
STEP="caddy build"
log "Building Caddy with NaiveProxy plugin (1-2 min)"

FORWARDPROXY_COMMIT="d62c80d3dd2c706b6b87579844d2397bddd18317"
export PATH=$PATH:/usr/local/go/bin:/root/go/bin
GOBIN=/usr/local/bin go install github.com/caddyserver/xcaddy/cmd/xcaddy@latest 2>/dev/null
xcaddy build --with github.com/caddyserver/forwardproxy=github.com/klzgrad/forwardproxy@${FORWARDPROXY_COMMIT} 2>/dev/null
mv caddy /usr/local/bin/caddy
chmod +x /usr/local/bin/caddy
ok "Caddy $(/usr/local/bin/caddy version | head -1) (forwardproxy pinned)"

# === Step M: Install Node.js ===
STEP="nodejs"
log "Installing Node.js (for vpn-agent)"

if ! command -v node >/dev/null 2>&1 || [ "$(node -v | sed 's/v//' | cut -d. -f1)" -lt 18 ]; then
    curl -fsSL https://deb.nodesource.com/setup_20.x | bash - >/dev/null 2>&1
    apt-get install -y -qq nodejs >/dev/null
fi
ok "Node.js $(node -v)"

# === Step N: Caddy config ===
STEP="caddy config"
log "Writing Caddy configuration"

ADMIN_PASS=$(openssl rand -base64 24)

# System user for Caddy. Caddy has historically run as root in this bootstrap,
# but we need a real uid for iptables -m owner --uid-owner match in Step S
# (BT DPI filter). Running as non-root is also just good hygiene —
# CAP_NET_BIND_SERVICE (set in caddy.service) covers binding to :80/:443.
if ! id caddy >/dev/null 2>&1; then
    useradd --system --home-dir /var/lib/caddy --create-home \
            --shell /usr/sbin/nologin caddy
    ok "caddy system user created (uid=$(id -u caddy))"
else
    ok "caddy user already exists"
fi

mkdir -p /etc/caddy /var/www/html /opt/vpn-agent /var/lib/caddy

# Empty vpn-users file
touch /etc/caddy/vpn-users

# Caddyfile
cat > /etc/caddy/Caddyfile <<CADDYEOF
{
  order forward_proxy before file_server
  admin 127.0.0.1:2019
}

:443, $DOMAIN {
  tls admin@$DOMAIN

  forward_proxy {
    basic_auth _admin_ $ADMIN_PASS
    import /etc/caddy/vpn-users
    hide_ip
    hide_via
    probe_resistance
  }

  @api {
    path /api/*
    header Authorization "Bearer $AGENT_TOKEN"
  }
  handle @api {
    reverse_proxy 127.0.0.1:3000
  }

  @api_unauth {
    path /api/*
    not header Authorization "Bearer $AGENT_TOKEN"
  }
  handle @api_unauth {
    respond 404
  }

  file_server {
    root /var/www/html
  }
}
CADDYEOF

chmod 600 /etc/caddy/Caddyfile
# Caddy runs as 'caddy' system user — must own its config, storage, and webroot.
# vpn-users gets rewritten by vpn-agent which runs as root, so leave it owned by caddy
# (agent writes use writeFileSync with root perms which work on any file).
chown -R caddy:caddy /etc/caddy /var/www/html /var/lib/caddy
ok "Caddyfile written, ownership transferred to caddy:caddy"

# Site placeholder
cat > /var/www/html/index.html <<'SITEEOF'
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Moraine Systems</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: #0a0e17; color: #c9d1d9; min-height: 100vh; display: flex; align-items: center; justify-content: center; }
    .c { max-width: 440px; text-align: center; padding: 2rem; }
    h1 { color: #58a6ff; font-weight: 300; font-size: 2rem; margin-bottom: .5rem; }
    p { color: #8b949e; font-size: .95rem; }
    footer { margin-top: 2rem; font-size: .75rem; color: #484f58; }
  </style>
</head>
<body>
  <div class="c">
    <h1>Moraine Systems</h1>
    <p>Infrastructure services</p>
    <footer>&copy; 2026 Moraine Systems Ltd.</footer>
  </div>
</body>
</html>
SITEEOF
ok "site placeholder written"

# === Step O: vpn-agent ===
STEP="vpn-agent"
log "Deploying vpn-agent"

cat > /opt/vpn-agent/package.json <<'AGENTEOF'
{
  "name": "vpn-agent",
  "version": "1.0.0",
  "description": "Lightweight agent for managing NaiveProxy users on Caddy server",
  "main": "app.mjs",
  "type": "module",
  "scripts": {
    "start": "node app.mjs"
  }
}
AGENTEOF

cat > /opt/vpn-agent/app.mjs <<'AGENTEOF'
import {createServer} from 'node:http';
import {syncUsers, getUsers} from './caddy.mjs';

const PORT = process.env.PORT || 3000;
const TOKEN = process.env.AGENT_TOKEN;

if (!TOKEN) {
  console.error('AGENT_TOKEN env var is required');
  process.exit(1);
}

function authorize(req) {
  return req.headers['authorization'] === `Bearer ${TOKEN}`;
}

function readBody(req) {
  return new Promise((resolve, reject) => {
    let data = '';
    req.on('data', chunk => data += chunk);
    req.on('end', () => {
      try {
        resolve(data ? JSON.parse(data) : {});
      } catch {
        reject(new Error('Invalid JSON'));
      }
    });
    req.on('error', reject);
  });
}

function json(res, status, body) {
  res.writeHead(status, {'Content-Type': 'application/json'});
  res.end(JSON.stringify(body));
}

const server = createServer(async (req, res) => {
  if (!authorize(req)) return json(res, 401, {error: 'unauthorized'});

  const path = new URL(req.url, `http://localhost:${PORT}`).pathname;

  try {
    if (req.method === 'POST' && path === '/api/sync') {
      const body = await readBody(req);
      if (!Array.isArray(body.users)) return json(res, 400, {error: 'users array is required'});

      for (const u of body.users) {
        if (!u.username || !u.password) return json(res, 400, {error: 'each user must have username and password'});
      }

      const result = syncUsers(body.users);
      if (result.error) return json(res, 500, result);
      return json(res, 200, result);
    }

    if (req.method === 'GET' && path === '/api/status') {
      const users = getUsers();
      return json(res, 200, {ok: true, users: users.length});
    }

    json(res, 404, {error: 'not found'});
  } catch (e) {
    console.error(e);
    json(res, 500, {error: 'internal error'});
  }
});

server.listen(PORT, '127.0.0.1', () => {
  console.log(`VPN Agent listening on 127.0.0.1:${PORT}`);
});
AGENTEOF

cat > /opt/vpn-agent/caddy.mjs <<'AGENTEOF'
import {readFileSync, writeFileSync, existsSync} from 'node:fs';
import {execSync} from 'node:child_process';

const USERS_FILE = process.env.USERS_FILE || '/etc/caddy/vpn-users';
const CADDYFILE = process.env.CADDYFILE || '/etc/caddy/Caddyfile';

export function syncUsers(users) {
  const backup = existsSync(USERS_FILE) ? readFileSync(USERS_FILE, 'utf-8') : '';

  const lines = users.map(({username, password}) => `basic_auth ${username} ${password}`);
  const content = lines.join('\n') + (lines.length ? '\n' : '');

  if (content === backup) return {success: true, count: users.length, changed: false};

  writeFileSync(USERS_FILE, content);

  const result = validateAndReload(backup);
  if (result.error) return result;

  return {success: true, count: users.length, changed: true};
}

export function getUsers() {
  if (!existsSync(USERS_FILE)) return [];
  const content = readFileSync(USERS_FILE, 'utf-8');
  return content
    .split('\n')
    .map(line => line.trim())
    .filter(line => line.startsWith('basic_auth '))
    .map(line => {
      const parts = line.split(/\s+/);
      return {username: parts[1], password: parts[2]};
    });
}

function validateAndReload(rollbackContent) {
  try {
    execSync(`/usr/local/bin/caddy validate --config ${CADDYFILE}`, {stdio: 'pipe'});
  } catch (e) {
    writeFileSync(USERS_FILE, rollbackContent);
    return {error: 'validate-failed', details: e.stderr?.toString()};
  }

  try {
    execSync('/usr/local/bin/caddy reload --config ' + CADDYFILE, {stdio: 'pipe'});
  } catch (e) {
    return {error: 'reload-failed', details: e.stderr?.toString()};
  }

  return {success: true};
}
AGENTEOF

ok "vpn-agent deployed to /opt/vpn-agent/"

# === Step P: systemd services ===
STEP="systemd"
log "Creating systemd services"

cat > /etc/systemd/system/caddy.service <<'EOF'
[Unit]
Description=Caddy NaiveProxy
After=network-online.target
Wants=network-online.target

[Service]
User=caddy
Group=caddy
# Without XDG_DATA_HOME and with systemd leaving HOME unset, Caddy falls back
# to ./caddy relative to WorkingDirectory — which resolves to /caddy at root.
# Point it explicitly at the user's home so certs live at /var/lib/caddy/caddy.
Environment=XDG_DATA_HOME=/var/lib/caddy
ExecStart=/usr/local/bin/caddy run --config /etc/caddy/Caddyfile
Restart=on-failure
RestartSec=5
AmbientCapabilities=CAP_NET_BIND_SERVICE

[Install]
WantedBy=multi-user.target
EOF

cat > /etc/systemd/system/vpn-agent.service <<AGENTSVC
[Unit]
Description=VPN Agent
After=caddy.service

[Service]
ExecStart=/usr/bin/node /opt/vpn-agent/app.mjs
Restart=on-failure
RestartSec=5
WorkingDirectory=/opt/vpn-agent
Environment=AGENT_TOKEN=$AGENT_TOKEN
Environment=PORT=3000

[Install]
WantedBy=multi-user.target
AGENTSVC

systemctl daemon-reload
ok "systemd services created"

# === Step Q: Start services ===
STEP="start services"
log "Starting services"

systemctl enable --now caddy
sleep 3
if systemctl is-active caddy >/dev/null; then
    ok "Caddy running"
else
    warn "Caddy not active — checking logs"
    journalctl -u caddy --no-pager -n 10
    die "Caddy failed to start"
fi

systemctl enable --now vpn-agent
sleep 2
if systemctl is-active vpn-agent >/dev/null; then
    ok "vpn-agent running"
else
    warn "vpn-agent not active"
fi

# === Step S: BT DPI filter (Suricata + NFQUEUE) ===
# Inline DPI blocking of BitTorrent traffic on Caddy's outbound connections.
# Architecture:
#   - iptables mangle OUTPUT: tags every caddy-owned connection with connmark 0x1
#   - iptables filter OUTPUT + INPUT: queue both directions of marked flows to NFQUEUE 0
#   - Suricata runs in IPS mode (-q 0), inspects flows against ET Open P2P ruleset,
#     issues DROP verdict on BitTorrent matches (tracker announces, peer protocol,
#     LTEP extension messages, DHT, client User-Agents)
#   - --queue-bypass fail-open: if Suricata crashes, NFQUEUE passes packets through
#     instead of blocking the entire proxy
STEP="suricata install"
log "Installing Suricata (BT DPI filter)"

apt-get install -y -qq suricata >/dev/null
# apt starts suricata with default config — stop it before we reconfigure,
# otherwise it holds memory with the full ~50k ET Open ruleset we don't need
systemctl stop suricata >/dev/null 2>&1 || true
ok "Suricata $(suricata --build-info 2>/dev/null | head -1 | awk '{print $4}') installed"

STEP="suricata config"
log "Patching suricata.yaml (rule-path, HOME_NET, eve-log types, stats off)"

# Fix 1: Debian package ships default-rule-path=/etc/suricata/rules but
# suricata-update writes to /var/lib/suricata/rules. Without this, Suricata
# loads zero rules and silently detects nothing.
sed -i 's|^default-rule-path: /etc/suricata/rules|default-rule-path: /var/lib/suricata/rules|' \
    /etc/suricata/suricata.yaml

# Fix 2: HOME_NET defaults to RFC1918 — on a public VPS that means every
# "$HOME_NET any -> $EXTERNAL_NET any" rule never matches because src IP
# isn't in HOME_NET. Substitute the actual server public IPs (v4 + v6).
if [ -n "$PUBLIC_IP6_CIDR" ]; then
    HOME_NET_VALUE="[$PUBLIC_IP/32,$PUBLIC_IP6_CIDR]"
else
    HOME_NET_VALUE="[$PUBLIC_IP/32]"
fi
sed -i "s|HOME_NET: \"\\[192.168.0.0/16,10.0.0.0/8,172.16.0.0/12\\]\"|HOME_NET: \"$HOME_NET_VALUE\"|" \
    /etc/suricata/suricata.yaml

# Fix 3: stats.log output writes ~500 MB/day on its own (hundreds of counter
# lines every 8 seconds). We don't need it — health-check via systemctl is
# enough, and eve.json keeps alerts/drops.
sed -i '/^  - stats:/,/^ *filename: stats.log/{s/enabled: yes/enabled: no/}' \
    /etc/suricata/suricata.yaml

# Fix 4: eve.json default types include flow/http/dns/tls/ssh/fileinfo/anomaly
# which for a proxy box means ~170 MB/day for background. Strip to alert+drop
# only — this uses a Python one-liner because the types: block is multi-line
# YAML and sed range tricks are fragile.
cat > /tmp/patch_suricata_eve.py <<'PYEOF'
import re, sys
path = '/etc/suricata/suricata.yaml'
with open(path) as f:
    c = f.read()
# Match the eve-log types: block and everything until the next 2-space-indented
# output section ("  - <name>:"). Non-greedy match stops at first such boundary.
pat = re.compile(r'(      types:\n)[\s\S]*?(?=^  - )', re.MULTILINE)
new = (
    "      types:\n"
    "        - alert:\n"
    "            tagged-packets: yes\n"
    "        - drop:\n"
    "            alerts: yes\n"
    "            flows: start\n"
)
c2, n = pat.subn(new, c, count=1)
if n != 1:
    sys.exit("failed to patch eve-log types block")
with open(path, 'w') as f:
    f.write(c2)
PYEOF
python3 /tmp/patch_suricata_eve.py
rm -f /tmp/patch_suricata_eve.py

# Validate config before proceeding — catches any sed/patch mishap
suricata -T -c /etc/suricata/suricata.yaml >/dev/null 2>&1 || \
    die "suricata.yaml validation failed after patches"
ok "suricata.yaml patched and validated"

STEP="suricata rules"
log "Configuring ET Open ruleset (P2P only, alert→drop)"

# Narrow the ruleset to just BitTorrent/P2P detection. ET Open ships ~65k
# rules across 60+ categories; we enable emerging-p2p.rules only (~119 rules)
# which covers BT peer protocol, DHT, tracker announces, and client UA strings.
cat > /etc/suricata/disable.conf <<'EOF'
# blanket-disable everything; enable.conf re-enables what we want
re:.*
EOF

cat > /etc/suricata/enable.conf <<'EOF'
# P2P group — includes all BitTorrent signatures from ET Open
group:emerging-p2p.rules
EOF

cat > /etc/suricata/modify.conf <<'EOF'
# convert alert→drop for every enabled rule (we are an IPS, not a passive IDS)
re:.* "^alert" "drop"
EOF

suricata-update enable-source et/open >/dev/null 2>&1
suricata-update 2>&1 | tail -3 || die "suricata-update failed"
ok "ruleset: $(grep -c '^drop ' /var/lib/suricata/rules/suricata.rules 2>/dev/null) drop-action rules loaded"

STEP="suricata logrotate"
# The apt package installs a logrotate config at /etc/logrotate.d/suricata but
# without daily/maxsize, meaning logs could balloon between rotations.
# Tighten it: daily + 50M cap + 7 day retention + compressed.
cat > /etc/logrotate.d/suricata <<'EOF'
/var/log/suricata/*.log
/var/log/suricata/*.json
{
	daily
	rotate 7
	maxsize 50M
	missingok
	compress
	delaycompress
	copytruncate
	sharedscripts
	postrotate
		/bin/kill -HUP $(cat /var/run/suricata.pid 2>/dev/null) 2>/dev/null || true
	endscript
}
EOF
ok "logrotate configured (daily, 50M cap, 7d retention)"

STEP="suricata systemd override"
# Switch Suricata from default af-packet mode to NFQUEUE IPS mode.
# -q 0 binds to queue 0 (our iptables rule sends caddy-owned packets there).
mkdir -p /etc/systemd/system/suricata.service.d
cat > /etc/systemd/system/suricata.service.d/10-nfqueue.conf <<'EOF'
[Service]
ExecStart=
ExecStart=/usr/bin/suricata -D -q 0 -c /etc/suricata/suricata.yaml --pidfile /run/suricata.pid
EOF
ok "systemd override for NFQUEUE mode installed"

STEP="bt-filter iptables"
# iptables rules that route caddy's outbound/inbound flows through NFQUEUE 0.
# Managed as a systemd oneshot so they persist across reboots without needing
# iptables-persistent (which would conflict with UFW's own rule management).
#
# Flow:
#   1. mangle OUTPUT: any packet from caddy uid → CONNMARK 0x1 on the connection
#   2. filter OUTPUT: packets on marked connections → NFQUEUE 0 (Suricata inspects outgoing)
#   3. filter INPUT:  packets on marked connections → NFQUEUE 0 (Suricata inspects replies)
#
# The INPUT rule is critical: without return-path inspection, Suricata never sees
# SYN-ACKs and can't mark the flow as established, so rules with flow:established
# (all ET P2P rules have this) never match.
cat > /usr/local/sbin/bt-filter-rules.sh <<'SCRIPTEOF'
#!/bin/bash
# BT DPI filter iptables rules — routes caddy-owned flows to NFQUEUE 0.
# Invoked by bt-filter-rules.service on start/stop.
set -eu

add_v4() {
    iptables -t mangle -C OUTPUT -m owner --uid-owner caddy -j CONNMARK --set-mark 0x1 2>/dev/null ||         iptables -t mangle -A OUTPUT -m owner --uid-owner caddy -j CONNMARK --set-mark 0x1
    iptables -C OUTPUT -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass 2>/dev/null ||         iptables -I OUTPUT 1 -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass
    iptables -C INPUT  -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass 2>/dev/null ||         iptables -I INPUT  1 -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass
}
add_v6() {
    ip6tables -t mangle -C OUTPUT -m owner --uid-owner caddy -j CONNMARK --set-mark 0x1 2>/dev/null ||         ip6tables -t mangle -A OUTPUT -m owner --uid-owner caddy -j CONNMARK --set-mark 0x1
    ip6tables -C OUTPUT -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass 2>/dev/null ||         ip6tables -I OUTPUT 1 -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass
    ip6tables -C INPUT  -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass 2>/dev/null ||         ip6tables -I INPUT  1 -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass
}
del_v4() {
    iptables -D INPUT  -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass 2>/dev/null || true
    iptables -D OUTPUT -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass 2>/dev/null || true
    iptables -t mangle -D OUTPUT -m owner --uid-owner caddy -j CONNMARK --set-mark 0x1 2>/dev/null || true
}
del_v6() {
    ip6tables -D INPUT  -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass 2>/dev/null || true
    ip6tables -D OUTPUT -m connmark --mark 0x1 -j NFQUEUE --queue-num 0 --queue-bypass 2>/dev/null || true
    ip6tables -t mangle -D OUTPUT -m owner --uid-owner caddy -j CONNMARK --set-mark 0x1 2>/dev/null || true
}

case "${1:-}" in
    add|start)
        add_v4
        add_v6
        ;;
    del|stop)
        del_v4
        del_v6
        ;;
    *)
        echo "usage: $0 {add|del}" >&2
        exit 1
        ;;
esac
SCRIPTEOF
chmod +x /usr/local/sbin/bt-filter-rules.sh

cat > /etc/systemd/system/bt-filter-rules.service <<'EOF'
[Unit]
Description=BT DPI filter iptables rules (caddy→NFQUEUE 0)
After=ufw.service network-online.target
Wants=ufw.service network-online.target
Before=suricata.service
PartOf=suricata.service

[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/sbin/bt-filter-rules.sh add
ExecStop=/usr/local/sbin/bt-filter-rules.sh del

[Install]
WantedBy=multi-user.target
EOF
ok "bt-filter-rules service installed"

STEP="suricata-update cron"
# Daily refresh of ET Open ruleset. Without this, detection capability would
# freeze at whatever we fetched during bootstrap — ET publishes updates for
# new BT clients and obfuscation variants regularly.
cat > /etc/cron.daily/suricata-update <<'EOF'
#!/bin/sh
/usr/bin/suricata-update --quiet 2>&1 | logger -t suricata-update
systemctl reload suricata 2>/dev/null || true
EOF
chmod +x /etc/cron.daily/suricata-update
ok "daily ruleset update scheduled"

STEP="start BT filter"
log "Starting BT filter services"

systemctl daemon-reload
systemctl enable --now bt-filter-rules.service
if ! systemctl is-active bt-filter-rules >/dev/null; then
    die "bt-filter-rules failed to start"
fi

systemctl enable --now suricata
sleep 3
if systemctl is-active suricata >/dev/null; then
    ok "Suricata running in NFQUEUE IPS mode"
else
    warn "Suricata not active — checking logs"
    journalctl -u suricata --no-pager -n 15
    die "Suricata failed to start"
fi

# === Step R: verification ===
STEP="verification"
log "Verifying"

if ss -tln 2>/dev/null | grep -q ':443 '; then
    ok "port 443 listening — Caddy is serving"
else
    warn "port 443 not listening"
fi

if ss -tln 2>/dev/null | grep -q ':3000 '; then
    ok "port 3000 listening — vpn-agent is up"
else
    warn "port 3000 not listening"
fi

# BT DPI filter health
if systemctl is-active suricata >/dev/null 2>&1; then
    BT_RULES=$(grep -c '^drop ' /var/lib/suricata/rules/suricata.rules 2>/dev/null || echo 0)
    ok "Suricata running (NFQUEUE IPS mode, $BT_RULES drop rules)"
else
    warn "Suricata not active — BT filter inactive"
    BT_RULES=0
fi

if iptables -t mangle -C OUTPUT -m owner --uid-owner caddy -j CONNMARK --set-mark 0x1 2>/dev/null; then
    ok "iptables NFQUEUE rules installed for caddy uid"
else
    warn "BT filter iptables rules missing"
fi

# NTP
systemctl enable --now systemd-timesyncd >/dev/null 2>&1 || true
for _ in 1 2 3 4 5; do
    [ "$(timedatectl show -p NTPSynchronized --value 2>/dev/null)" = "yes" ] && break
    sleep 2
done
if [ "$(timedatectl show -p NTPSynchronized --value 2>/dev/null)" = "yes" ]; then
    ok "NTP synchronized"
else
    warn "NTP not synchronized"
fi

# === Summary ===
STEP="done"
cat <<SUMMARY

===================================================================
  NaiveProxy Bootstrap complete
===================================================================
  Public IP     : $PUBLIC_IP
  Domain        : $DOMAIN
  Caddy         : $(/usr/local/bin/caddy version 2>/dev/null | head -1)
  Node.js       : $(node -v 2>/dev/null)
  Agent token   : $AGENT_TOKEN

  Ports:
    443/tcp  — NaiveProxy (HTTPS proxy) + site + API
    80/tcp   — ACME HTTP challenge (auto-renew)
    22/tcp   — SSH (key-only, root disabled)

  Files:
    /etc/caddy/Caddyfile              — main config
    /etc/caddy/vpn-users              — user credentials (managed by agent)
    /opt/vpn-agent/                   — management API
    /var/www/html/                    — site placeholder
    /var/lib/caddy/                   — caddy home (ACME certs, state)
    /etc/suricata/                    — DPI engine config
    /var/lib/suricata/rules/          — active ruleset (daily updated)
    /var/log/suricata/eve.json        — structured alert + drop log
    /usr/local/sbin/bt-filter-rules.sh — iptables rules for BT DPI

  BT filter (Suricata DPI + NFQUEUE):
    Rules loaded   : $BT_RULES ET Open P2P rules (drop action)
    Scope          : caddy uid connmark 0x1, both directions via NFQUEUE 0
    Failsafe       : --queue-bypass (Suricata crash → traffic passes)
    Daily update   : /etc/cron.daily/suricata-update
    Stop filter    : sudo systemctl stop bt-filter-rules suricata
    View drops     : sudo jq 'select(.event_type=="drop")' /var/log/suricata/eve.json

  Next steps:
    1. Ensure DNS A record: $DOMAIN → $PUBLIC_IP (no CF proxy)
    2. Configure Slack bot with:
         API URL:   https://$DOMAIN/api
         Token:     $AGENT_TOKEN
    3. Bot adds users via POST /api/sync
    4. Distribute RB Proxy extension to team
         Proxy:     https://$DOMAIN:443

  Test manually:
    curl https://$DOMAIN/              # should show site
    curl -H "Authorization: Bearer $AGENT_TOKEN" https://$DOMAIN/api/status

  Useful commands:
    sudo systemctl status caddy vpn-agent suricata bt-filter-rules
    sudo journalctl -u caddy -u suricata -n 50
    sudo iptables -L OUTPUT -n -v --line-numbers | head
    cat /etc/caddy/vpn-users
===================================================================
SUMMARY

if [ -f /var/run/reboot-required ]; then
    echo
    echo "!! Kernel was upgraded — reboot recommended: sudo reboot"
fi
