#!/usr/bin/env bash
# ZeroRoute 流媒体解锁 DNS
# 用法：bash <(curl -fsSL https://zeroroute.net/Install.sh)
# 本地 unbound 监听 127.0.0.1:53：domain.txt 内域名 → 解锁 DNS，其余 → 1.1.1.1，上游一律 TCP。
set -uo pipefail

LIST_URL=${LIST_URL:-https://zeroroute.net/domain.txt}
DIR=/etc/unbound/zeroroute   # 放在 /etc/unbound 下，AppArmor 允许 unbound 读取
STATE=$DIR/state
UNIT=/etc/systemd/system/zeroroute-dns.service
RESOLV=/etc/resolv.conf
LOG=/var/log/zeroroute-dns.log
DOMAIN_RE='^[a-z0-9-]+(\.[a-z0-9-]+)+$'

G='\033[32m'; R='\033[31m'; Y='\033[33m'; B='\033[1m'; N='\033[0m'
ok()   { echo -e "${G}✔${N} $*"; }
warn() { echo -e "${Y}!${N} $*"; }
die()  { echo -e "${R}✘ $*${N}"; exit 1; }
TTY=/dev/stdin; { : </dev/tty; } 2>/dev/null && TTY=/dev/tty   # curl|bash 时从终端读
ask()  { local a; read -rp "$1" a <"$TTY" || return 1; echo "$a"; }   # EOF 返回失败，避免死循环

# run "说明" 命令...：后台执行、转圈显示，输出写日志
run() {
  local msg=$1 pid i=0 s='|/-\'; shift
  "$@" >>"$LOG" 2>&1 & pid=$!
  while kill -0 "$pid" 2>/dev/null; do printf "\r  [%s] %s" "${s:i++%4:1}" "$msg"; sleep 0.2; done
  if wait "$pid"; then printf "\r  ${G}✔${N} %s\n" "$msg"; else printf "\r  ${R}✘${N} %s（日志：%s）\n" "$msg" "$LOG"; return 1; fi
}

# ---------- 1. 环境检测与依赖 ----------
check_env() {
  echo -e "${B}[1/2] 检测环境${N}"
  [ "$(id -u)" = 0 ] || die "请使用 root 运行"
  [ -d /run/systemd/system ] || die "仅支持 systemd 系统"
  local dig_pkg
  if command -v apt-get >/dev/null; then
    PM=apt; dig_pkg=dnsutils
    apt-cache show bind9-dnsutils >/dev/null 2>&1 && dig_pkg=bind9-dnsutils
  elif command -v dnf >/dev/null; then PM=dnf; dig_pkg=bind-utils
  elif command -v yum >/dev/null; then PM=yum; dig_pkg=bind-utils
  else die "不支持的系统（需要 apt / dnf / yum）"; fi
  ok "系统：$(. /etc/os-release 2>/dev/null; echo "${PRETTY_NAME:-unknown}")，包管理：$PM"

  local missing=() cmd pkg
  for pair in curl:curl dig:$dig_pkg chattr:e2fsprogs unbound:unbound; do
    cmd=${pair%%:*}; pkg=${pair#*:}
    if command -v "$cmd" >/dev/null || [ -x "/usr/sbin/$cmd" ]; then ok "$cmd 已存在"; else missing+=("$pkg"); fi
  done
  [ ${#missing[@]} = 0 ] && return

  echo -e "${B}[2/2] 安装依赖：${missing[*]}${N}"
  if [ $PM = apt ]; then
    run "更新软件源" apt-get update -q || die "软件源更新失败"
    run "安装 ${missing[*]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y -q "${missing[@]}" || die "依赖安装失败"
  else
    run "安装 ${missing[*]}" $PM install -y -q "${missing[@]}" || die "依赖安装失败"
  fi
  mkdir -p "$DIR"; echo "${missing[*]}" >>"$DIR/pkgs"   # 还原时卸载
}

# ---------- 工具 ----------
valid_ip() {
  if [[ $1 =~ ^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})$ ]]; then
    for o in "${BASH_REMATCH[@]:1}"; do ((10#$o <= 255)) || return 1; done
    return 0
  fi
  [[ $1 == *:*:* && $1 =~ ^[0-9a-fA-F:]+$ ]]
}
tcp_ok() { dig +tcp +time=3 +tries=1 @"$1" "$2" A 2>/dev/null | grep -q 'status: NOERROR'; }

ask_dns() {
  local dns
  while :; do
    dns=$(ask "请输入流媒体解锁 DNS 的 IP：") || die "已取消"
    valid_ip "$dns" || { warn "IP 格式不正确"; continue; }
    if tcp_ok "$dns" netflix.com; then ok "解锁 DNS $dns 支持 TCP 查询"; NEW_DNS=$dns; return; fi
    warn "$dns 的 TCP 53 查询失败（本方案强制 TCP，DNS 需支持 TCP）"
  done
}

fetch_list() {
  curl -fsSL --max-time 30 "$LIST_URL" -o "$DIR/domain.raw" || return 1
  tr -d '\r' <"$DIR/domain.raw" | tr 'A-Z' 'a-z' | grep -E "$DOMAIN_RE" | sort -u >"$DIR/domain.new"; rm -f "$DIR/domain.raw"
  [ -s "$DIR/domain.new" ] || { rm -f "$DIR/domain.new"; return 1; }
  mv -f "$DIR/domain.new" "$DIR/domain.txt"
}

write_conf() {   # $1 = 解锁 DNS
  local user=""; id unbound >/dev/null 2>&1 && user=unbound
  {
    cat <<EOF
server:
  interface: 127.0.0.1
  port: 53
  access-control: 127.0.0.0/8 allow
  username: "$user"
  chroot: ""
  directory: "$DIR"
  pidfile: ""
  use-syslog: yes
  verbosity: 0
  # 解锁 DNS 会改写解析结果，关闭 DNSSEC 校验
  module-config: "iterator"
  # 上游全部走 TCP
  tcp-upstream: yes
  prefetch: yes

forward-zone:
  name: "."
  forward-addr: 1.1.1.1
EOF
    while read -r d; do printf '\nforward-zone:\n  name: "%s"\n  forward-addr: %s\n' "$d" "$1"; done <"$DIR/domain.txt"
  } >"$DIR/unbound.conf.new"
  unbound-checkconf "$DIR/unbound.conf.new" >>"$LOG" 2>&1 || { rm -f "$DIR/unbound.conf.new"; return 1; }
  mv -f "$DIR/unbound.conf.new" "$DIR/unbound.conf"
}

restart_dns() {
  systemctl restart zeroroute-dns >>"$LOG" 2>&1 || return 1
  for _ in 1 2 3 4 5 6 7 8 9 10; do tcp_ok 127.0.0.1 example.com && return 0; sleep 0.5; done
  return 1
}

lock_resolv() {
  chattr -i "$RESOLV" 2>/dev/null
  [ -L "$RESOLV" ] && rm -f "$RESOLV"
  printf 'nameserver 127.0.0.1\nnameserver 1.0.0.1\noptions use-vc\n' >"$RESOLV"
  chattr +i "$RESOLV" 2>/dev/null || warn "文件系统不支持 chattr，$RESOLV 未锁定"
}

# ---------- 安装 ----------
do_install() {
  mkdir -p "$DIR"
  ask_dns; local dns=$NEW_DNS
  run "下载域名列表" fetch_list || die "域名列表下载失败：$LIST_URL"
  ok "域名数：$(wc -l <"$DIR/domain.txt")"
  run "生成 unbound 配置" write_conf "$dns" || die "配置校验失败"

  # 记录原状态，用于还原
  {
    echo "UNLOCK_DNS=$dns"
    # 系统自带 unbound 服务会占用 127.0.0.1:53；原本就在用的才记入还原
    if systemctl is-active -q unbound 2>/dev/null || systemctl is-enabled -q unbound 2>/dev/null; then
      grep -qw unbound "$DIR/pkgs" 2>/dev/null && echo 'DISABLED=""' || echo 'DISABLED="unbound"'
      systemctl disable --now unbound >>"$LOG" 2>&1
    else echo 'DISABLED=""'; fi
    if [ -e "$RESOLV" ] || [ -L "$RESOLV" ]; then
      chattr -i "$RESOLV" 2>/dev/null; cp -a "$RESOLV" "$DIR/resolv.conf.bak"; echo HAD_RESOLV=1
    else echo HAD_RESOLV=0; fi
  } >"$STATE"
  . "$STATE"

  cat >"$UNIT" <<EOF
[Unit]
Description=ZeroRoute DNS (unbound)
After=network-online.target
Wants=network-online.target

[Service]
ExecStart=$(command -v unbound || echo /usr/sbin/unbound) -d -c $DIR/unbound.conf
Restart=on-failure

[Install]
WantedBy=multi-user.target
EOF
  systemctl daemon-reload
  systemctl enable -q zeroroute-dns 2>>"$LOG"
  if ! run "启动本地 DNS 127.0.0.1:53" restart_dns; then
    warn "53 端口占用情况：$(ss -Hlntup 'sport = :53' 2>/dev/null | awk '{print $1, $5, $7}' | tr '\n' ' ')"
    do_restore; die "启动失败，已自动还原"
  fi
  run "写入并锁定 $RESOLV（127.0.0.1 / 1.0.0.1，强制 TCP）" lock_resolv
  ok "安装完成"; show_status
}

# ---------- 维护 ----------
show_status() {
  . "$STATE"
  echo -e "${B}—— 状态 ——${N}"
  echo "  服务：$(systemctl is-active zeroroute-dns)"
  echo "  解锁 DNS：$UNLOCK_DNS（TCP）  其他：1.1.1.1（TCP）"
  echo "  分流域名：$(wc -l <"$DIR/domain.txt") 个"
  echo "  $RESOLV：$(grep -c '^nameserver' "$RESOLV") 个 nameserver，$(lsattr -d "$RESOLV" 2>/dev/null | awk '{print $1}' | grep -q i && echo 已锁定 || echo 未锁定)"
  tcp_ok 127.0.0.1 netflix.com && ok "本地解析正常" || warn "本地解析失败"
}

apply_change() {   # $1 = 解锁 DNS
  cp -a "$DIR/unbound.conf" "$DIR/unbound.conf.old"
  if write_conf "$1" && restart_dns; then
    sed -i "s/^UNLOCK_DNS=.*/UNLOCK_DNS=$1/" "$STATE"; rm -f "$DIR/unbound.conf.old"; return 0
  fi
  mv -f "$DIR/unbound.conf.old" "$DIR/unbound.conf"; systemctl restart zeroroute-dns; return 1
}

do_update() {
  . "$STATE"
  run "更新域名列表" fetch_list || { warn "下载失败，保持原列表"; return; }
  run "重载配置" apply_change "$UNLOCK_DNS" && ok "域名数：$(wc -l <"$DIR/domain.txt")" || warn "失败，已回退原配置"
}

do_change_dns() {
  ask_dns; local dns=$NEW_DNS
  run "切换解锁 DNS 为 $dns" apply_change "$dns" || warn "失败，已回退原配置"
}

do_restore() {
  [ -f "$STATE" ] && . "$STATE"
  chattr -i "$RESOLV" 2>/dev/null
  if [ "${HAD_RESOLV:-0}" = 1 ] && { [ -e "$DIR/resolv.conf.bak" ] || [ -L "$DIR/resolv.conf.bak" ]; }; then
    rm -f "$RESOLV"; cp -a "$DIR/resolv.conf.bak" "$RESOLV"; ok "已恢复原 $RESOLV"
  fi
  systemctl disable --now zeroroute-dns >>"$LOG" 2>&1
  rm -f "$UNIT"; systemctl daemon-reload
  ok "已移除本地 DNS 服务"
  for s in ${DISABLED:-}; do run "恢复 $s" systemctl enable --now "$s"; done
  if [ -s "$DIR/pkgs" ]; then
    local pkgs; pkgs=$(tr '\n' ' ' <"$DIR/pkgs")
    case $PM in
      apt) run "卸载脚本安装的依赖：$pkgs" env DEBIAN_FRONTEND=noninteractive apt-get purge -y -q $pkgs ;;
      *)   run "卸载脚本安装的依赖：$pkgs" $PM remove -y -q $pkgs ;;
    esac
  fi
  rm -rf "$DIR"; ok "还原完成"
}

# ---------- 菜单 ----------
main() {
  echo -e "${B}ZeroRoute 流媒体解锁 DNS${N}\n"
  check_env; echo
  if [ ! -f "$STATE" ]; then
    warn "未安装"
    case $(ask "是否安装？[y/N] " || true) in [yY]*) do_install ;; *) echo "已取消" ;; esac
    return
  fi
  ok "已安装"
  while :; do
    echo -e "\n  1) 查看状态\n  2) 更新域名列表\n  3) 修改解锁 DNS\n  4) 还原（卸载并恢复原 DNS）\n  0) 退出"
    c=$(ask "请选择：") || return
    case $c in
      1) show_status ;;
      2) do_update ;;
      3) do_change_dns ;;
      4) [[ $(ask "确认还原？[y/N] " || true) == [yY]* ]] && { do_restore; return; } ;;
      0) return ;;
    esac
  done
}

main
