#!/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() { # Docker embedded DNS 例外(必须在 127.0.0.0/8 DROP 前)── # 容器内 /etc/resolv.conf 默写 `nameserver 127.0.0.11`,挡了所有域名解析全跪。 # 精准放行 53/udp + 53/tcp,不破坏 loopback DROP 基线(其他 127.0.0.0/8 仍挡)。 iptables -A OUTPUT -d 127.0.0.11/32 -p udp --dport 53 -j ACCEPT iptables -A OUTPUT -d 127.0.0.11/32 -p tcp --dport 53 -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 # IPv4 loopback (容器回头打宿主端口;DNS 已上面例外) 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 ── ip6tables 在某些精简镜像 / sysctl ipv6.disable_ipv6=1 下不可用, # 失败降级为 warn(IPv6 没起的话本来也打不通,容忍) 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