#!/bin/sh
set -eu

# ALLOWLIST, GOST_PORT, and GOST_MARK must stay in sync with the literals in
# gost.yaml (file.path, addr, so_mark), which cannot read environment variables.
ALLOWLIST=/opt/egress-sidecar/allowlist.txt
GOST_PORT=12345
GOST_MARK=114514
NFTABLES_RULESET_NAME=gost_egress
# Must exceed the allowlist `reload: 1s` interval in gost.yaml so gost has
# picked up the new allowlist before nftables enforcement turns on.
RELOAD_WAIT=2

usage() {
  cat <<'EOF'
Usage:
  network-policy show
  network-policy allow-all
  network-policy deny-all
  network-policy allow [TARGET...]
  network-policy rules

TARGET can be a domain, wildcard domain, IP, CIDR, or IP range. With no targets,
allow mode denies all controlled TCP egress.
EOF
}

ensure_file() {
  mkdir -p "$(dirname "$ALLOWLIST")"
  touch "$ALLOWLIST"
}

# Docker DNATs queries to the compose gateway:53 onto 127.0.0.11, so
# `fib daddr type local` (re-evaluated after DNAT) already allows resolver
# traffic. Podman aardvark-dns / pasta listen on the network gateway
# (e.g. 10.89.x.1) with no such DNAT, and the filter chain would reject
# UDP/TCP 53 (`meta l4proto != tcp reject`). Permit DNS only to nameservers
# listed in resolv.conf — the same trust boundary Docker's embedded resolver
# has. Hostname allowlisting still happens in gost via SNI.
nft_dns_rules() {
  action="$1"
  awk -v action="$action" '
    /^nameserver[[:space:]]+/ {
      ns = $2
      sub(/%.*/, "", ns)
      if (ns ~ /^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$/) {
        printf "    ip daddr %s udp dport 53 %s\n", ns, action
        printf "    ip daddr %s tcp dport 53 %s\n", ns, action
      } else if (ns ~ /:/) {
        printf "    ip6 daddr %s udp dport 53 %s\n", ns, action
        printf "    ip6 daddr %s tcp dport 53 %s\n", ns, action
      }
    }
  ' /etc/resolv.conf
}

setup_nftables() {
  nft --file - <<EOF
add table inet $NFTABLES_RULESET_NAME
flush table inet $NFTABLES_RULESET_NAME

table inet $NFTABLES_RULESET_NAME {
  chain output {
    type nat hook output priority dstnat; policy accept;

    meta mark $GOST_MARK return
    fib daddr type local return
$(nft_dns_rules return)
    meta l4proto tcp redirect to :$GOST_PORT
  }

  chain egress {
    type filter hook output priority filter; policy accept;

    meta mark $GOST_MARK accept
    fib daddr type local accept
$(nft_dns_rules accept)
    ip protocol icmp accept
    ip6 nexthdr icmpv6 accept
    meta l4proto != tcp reject
  }
}
EOF
}

remove_nftables() {
  nft delete table inet "$NFTABLES_RULESET_NAME" 2>/dev/null || true
}

nftables_enabled() {
  nft list table inet "$NFTABLES_RULESET_NAME" >/dev/null 2>&1
}

show() {
  if nftables_enabled; then
    echo "mode: controlled egress"
  else
    echo "mode: allow-all (transparent proxy disabled)"
  fi
  echo "allowlist: $ALLOWLIST"
  if [ ! -s "$ALLOWLIST" ]; then
    if nftables_enabled; then
      echo "(empty: deny all controlled TCP egress)"
    else
      echo "(empty)"
    fi
    return
  fi
  nl --body-numbering=a "$ALLOWLIST"
}

wait_reload() {
  sleep "$RELOAD_WAIT"
}

ensure_file

cmd="${1:-}"
case "$cmd" in
  show)
    show
    ;;
  allow-all)
    remove_nftables
    : > "$ALLOWLIST"
    echo "ok: allow all egress"
    ;;
  deny-all)
    : > "$ALLOWLIST"
    wait_reload
    setup_nftables
    echo "ok: deny all controlled TCP egress"
    ;;
  allow)
    shift
    : > "$ALLOWLIST"
    for item in "$@"; do
      if [ -n "$item" ]; then
        printf '%s\n' "$item" >> "$ALLOWLIST"
      fi
    done
    wait_reload
    setup_nftables
    echo "ok: allowlist applied"
    show
    ;;
  rules)
    if nftables_enabled; then
      nft list table inet "$NFTABLES_RULESET_NAME"
    else
      echo "mode: allow-all"
    fi
    ;;
  *)
    usage
    exit 2
    ;;
esac
