#!/bin/bash
# Sandbox container init (DESIGN §7.5 #1 网络 blocklist 协议)。
#
# 启动流程:配 iptables OUTPUT blocklist → sleep infinity 等 `docker exec` 进来。
# 以 root 跑(需要 NET_ADMIN),docker exec 进来的 --user 1000 进程拿不到 cap。
#
# Stage C 协议:任一规则 apply 失败 → `set -e` 退出,容器立即终止;视为部署未完成,
# 上层 ensure() 会 raise。
set -euo pipefail

apply_resolv_conf() {
    # 覆写 /etc/resolv.conf 直接指公网 DNS,绕过 docker embedded DNS(127.0.0.11)。
    # docker user-defined bridge network 默 resolv.conf = nameserver 127.0.0.11,
    # embedded DNS 转发到 docker daemon 上游 ── 腾讯云轻量等场景 daemon 探测
    # systemd-resolved 失败 → embedded DNS forward 不出去 → 全跪。`--dns` flag 只
    # 改 daemon 上游不动 resolv.conf,在 user-defined network 上无效。
    #
    # 失败 robust:resolv.conf 在某些 docker / kernel 组合下是 ro mount,写不进
    # 不能让 init.sh 整体退出(set -e),仅 warn 后继续跑 iptables 等其他启动逻辑;
    # 此时容器仍能跑 shell / run_python,只是 DNS 解析跪 ── 比容器直接退好。
    if [ -z "${ZCBOT_DNS:-}" ]; then
        return 0
    fi
    local tmp
    tmp="$(mktemp 2>/dev/null)" || tmp="/tmp/resolv.conf.tmp.$$"
    : > "$tmp"
    for ip in $(echo "$ZCBOT_DNS" | tr ',' ' '); do
        if [ -n "$ip" ]; then
            echo "nameserver $ip" >> "$tmp"
        fi
    done
    if cat "$tmp" > /etc/resolv.conf 2>/dev/null; then
        echo "[init] /etc/resolv.conf set:"
        cat /etc/resolv.conf
    else
        echo "[init] WARN: cannot write /etc/resolv.conf (ro mount?);"  \
             "DNS via embedded 127.0.0.11 will be used as fallback" >&2
    fi
    rm -f "$tmp"
}

apply_blocklist() {
    # 容器 netns 自环放行(必须是第一条)── 容器里的 127.0.0.1 是容器**自己**的 lo,
    # bridge 网络下出不了 netns、到不了宿主,DROP 它防不到任何东西,只会误伤容器内
    # 进程间 IPC。实锤事故(2026-07,task 9dcae061 终案):puppeteer 控制 chromium 走
    # 127.0.0.1 上的 DevTools WebSocket,被 127.0.0.0/8 DROP 掐死 → mmdc 渲染 90 天
    # 0 成功。特征:恒定 ~2m15s 超时(TCP SYN 重试打满)、user CPU 近 0、[object Object];
    # 且各 probe 因 `--entrypoint bash` 绕过本脚本没跑 iptables,全都"能渲"→ 长期误判。
    # docker embedded DNS(127.0.0.11:53)也走 lo,原先单独的 53 端口豁免一并收编。
    iptables -A OUTPUT -o lo -j ACCEPT

    # §7.5 #1 红线段(任一缺失视为 Stage C 未完成):
    iptables -A OUTPUT -d 169.254.0.0/16 -j DROP    # cloud metadata (Capital One 2019 SSRF)
    iptables -A OUTPUT -d 127.0.0.0/8   -j DROP    # 非 lo 出口的 127/8(正常路由不存在,belt-and-suspenders;自环已在首条放行)
    iptables -A OUTPUT -d 10.0.0.0/8    -j DROP    # 内网段 A
    iptables -A OUTPUT -d 172.16.0.0/12 -j DROP    # 内网段 B(Docker 默认 bridge 网段在此)
    iptables -A OUTPUT -d 192.168.0.0/16 -j DROP   # 内网段 C
    iptables -A OUTPUT -d 100.64.0.0/10 -j DROP    # CGNAT(云平台常用)

    # IPv6 loopback ── 同 v4:自环放行在前(chromium/node 偶有 localhost→::1 解析路径),
    # ::1 DROP 留作 belt-and-suspenders。ip6tables 在某些精简镜像 / sysctl
    # ipv6.disable_ipv6=1 下不可用,失败降级为 warn(IPv6 没起的话本来也打不通,容忍)
    ip6tables -A OUTPUT -o lo -j ACCEPT 2>/dev/null || true
    if ! ip6tables -A OUTPUT -d ::1 -j DROP 2>/dev/null; then
        echo "[init] ip6tables ::1 rule skipped (ip6tables unavailable or v6 disabled)" >&2
    fi

    # ZCBOT_PG_IPS=10.x.x.x,172.x.x.x[/prefix],...
    # 部署侧把 PG 实际 IP 显式 block ── 即使已落在内网三段,defense-in-depth(§7.5 #1)。
    if [ -n "${ZCBOT_PG_IPS:-}" ]; then
        IFS=',' read -ra _pg_ips <<< "$ZCBOT_PG_IPS"
        for ip in "${_pg_ips[@]}"; do
            ip_trim="$(echo "$ip" | tr -d '[:space:]')"
            [ -z "$ip_trim" ] && continue
            iptables -A OUTPUT -d "$ip_trim" -j DROP
            echo "[init] blocked PG IP: $ip_trim"
        done
    fi
}

apply_resolv_conf
apply_blocklist
echo "[init] iptables OUTPUT blocklist applied:"
iptables -L OUTPUT -n --line-numbers
echo "[init] sleeping forever, ready for docker exec."

# tini 是 PID 1;这里把 sleep 作为 tini 的子进程,信号能正常转发(docker stop → SIGTERM
# → tini 转给 sleep → 容器干净退出)。exec 避免再 fork 一层。
exec sleep infinity
