🚀 运行: ...
IP: ...
ISP: 检测中...

K 机 DNS 解锁:dnsmasq 劫持 + sniproxy SNI 代理一键部署(含管理菜单)

这是什么

手里有一台流媒体解锁能力强的 VPS(本文叫 K 机),想让其他小鸡也蹭到它的解锁能力?办法:把 K 机做成专用 DNS。

  • dnsmasq 监听 53 端口:把 Netflix / Disney+ / Prime Video / Dazn 等域名劫持到 K 机本机 IP,其他域名正常向上游(8.8.8.8 / 1.1.1.1)解析
  • sniproxy 监听 443 端口:按 SNI 把流量代理到真实源站
  • iptables:53 端口白名单制,只放行自家机器,其余 DROP(防止被扫成公共 DNS);443 放行

其他 VPS 只需把 DNS 指向 K 机,解锁测试直接变绿。IPv4 / IPv6 双栈可用。

部署步骤

  1. 把 kunlock.sh 传到 K 机的 /root/kunlock.sh
  2. 执行下面两行:
  3. 选 1 安装:公网 IP 自动检测,白名单自动从现行 iptables 导入(已部署过的机器直接覆盖装即可,幂等)
  4. 选 6 运行状态,确认 dnsmasq / sniproxy 都是运行中
  5. 以后直接打 ku 就能呼出管理菜单
chmod +x /root/kunlock.sh
/root/kunlock.sh

管理菜单一览

  • 1 安装
  • 2 卸载(恢复原装:停服务、删配置、恢复原防火墙、卸载软件包)
  • 3 / 4 / 5 开始 / 停止 / 重启
  • 6 运行状态(菜单顶栏也常显两个服务状态)
  • 7 查看白名单
  • 8 编辑白名单(编号增删,支持多个、支持 1-3 区间,改完自动生效,不用重启服务)
  • 9 客户端设置说明(复制粘贴即用)
  • 0 退出

白名单说明

53 端口默认 DROP,只有白名单里的 IP 能查询。127.0.0.1 和本机 IP 默认在列。增删白名单走菜单 8,全自动重建防火墙规则并保存,重启机器也不丢。

客户端设置

在需要解锁的 VPS 上执行,复制粘贴即可:

chattr -i /etc/resolv.conf 2>/dev/null
cp -a /etc/resolv.conf /etc/resolv.conf.bak.kunlock
printf 'nameserver 157.20.104.98n' > /etc/resolv.conf
getent hosts www.netflix.com

getent 返回 K 机 IP 说明劫持生效,然后跑解锁测试脚本,看 Netflix / Disney+ 是否为 SG。

不需要解锁时,一行恢复原来 DNS:

cp -a /etc/resolv.conf.bak.kunlock /etc/resolv.conf

注意:部分系统(DHCP / NetworkManager / systemd-resolved)会覆写 /etc/resolv.conf,若发现被改回,请在对应服务中锁定 DNS。

部署中踩过的坑

  1. Debian 的 dnsmasq 硬塞 --local-service:/usr/share/dnsmasq/init-system-common 里写死了这个参数,公网来源的 DNS 查询会被静默丢弃(日志报 ignoring query from non-local network),tcpdump 只见进包不见回包,极易误诊成防火墙问题。解法:配置里加 listen-address,dnsmasq 会自动忽略 --local-service,且升级不丢。
  2. sniproxy 的 systemd 误判:Debian 包的 unit 没写 Type,daemonize 后 systemd 显示 inactive,其实进程在正常监听。用 drop-in 覆盖 Type=forking + PIDFile 解决。
  3. 443 端口重复绑定:sniproxy 不能同时 listen 0.0.0.0:443 和 listen [::]:443,会报 bind already in use,只留 [::]:443 双栈监听即可。
  4. resolv.conf 写不动:有的机器 /etc/resolv.conf 带了 chattr +i 属性,root 也写不了,先 chattr -i 解锁。

完整脚本 kunlock.sh

#!/bin/bash
###############################################################################
# kunlock - K 机 DNS 解锁管理脚本
# dnsmasq(53) 域名劫持 + sniproxy(443) SNI 代理,供其他 VPS 做 DNS 解锁用
# 快捷键: ku
###############################################################################

RED=$'33[0;31m'
GREEN=$'33[0;32m'
YELLOW=$'33[1;33m'
CYAN=$'33[0;36m'
NC=$'33[0m'

SCRIPT_PATH="$(readlink -f "$0")"
BASE_DIR="/root/kunlock"
BACKUP_DIR="$BASE_DIR/backup"
WL_FILE="$BASE_DIR/whitelist.txt"
DNSMASQ_CONF="/etc/dnsmasq.d/unlock.conf"
SNIPROXY_CONF="/etc/sniproxy.conf"
SNIPROXY_DROPIN="/etc/systemd/system/sniproxy.service.d/forking.conf"
SHORTCUT="/usr/local/bin/ku"

# 劫持域名:v6 组 A/AAAA 都指本机;v4only 组 AAAA 强制走 v4(本机 v6 被屏蔽)
DOMAINS_V6="netflix.com netflix.net nflximg.com nflxvideo.net nflxso.net disneyplus.com disney-plus.net dssott.com bamgrid.com hbomax.com max.com dazn.com"
DOMAINS_V4ONLY="primevideo.com amazonvideo.com"

need_root() {
  if [ "$(id -u)" -ne 0 ]; then
    echo "${RED}请用 root 运行${NC}"
    exit 1
  fi
}

pause() { read -rp "按回车返回..." _; }

get_v4() {
  ip -4 -o route get 8.8.8.8 2>/dev/null | sed -n 's/.*src ([0-9.]*).*/1/p'
}

get_v6() {
  local dev
  dev=$(ip -6 -o route show default 2>/dev/null | awk '{print $5}' | head -1)
  if [ -n "$dev" ]; then
    ip -6 -o addr show dev "$dev" scope global 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | head -1
  fi
}

svc_state() {
  if systemctl is-active --quiet "$1" 2>/dev/null; then
    echo "${GREEN}运行中${NC}"
  else
    echo "${RED}已停止${NC}"
  fi
}

valid_ip() {
  [[ "$1" =~ ^([0-9]{1,3}.){3}[0-9]{1,3}$ ]] || return 1
  local IFS=.
  local -a o=($1)
  local x
  for x in "${o[@]}"; do ((x <= 255)) || return 1; done
  return 0
}

write_dnsmasq_conf() {
  local v4="$1" v6="$2" d
  {
    echo "# kunlock 自动生成,请勿手工修改(用 ku 菜单管理)"
    echo "server=8.8.8.8"
    echo "server=1.1.1.1"
    # listen-address 让 Debian 硬塞的 --local-service 自动失效(公网 DNS 必需)
    echo "listen-address=127.0.0.1"
    echo "listen-address=$v4"
    echo "listen-address=::1"
    [ -n "$v6" ] && echo "listen-address=$v6"
    for d in $DOMAINS_V6; do
      echo "address=/$d/$v4"
      [ -n "$v6" ] && echo "address=/$d/$v6"
    done
    for d in $DOMAINS_V4ONLY; do
      echo "address=/$d/$v4"
      echo "address=/$d/::ffff:$v4"
    done
  } > "$DNSMASQ_CONF"
}

write_sniproxy_conf() {
  local d
  {
    echo "# kunlock 自动生成,请勿手工修改(用 ku 菜单管理)"
    echo "user daemon"
    echo "pidfile /run/sniproxy.pid"
    echo ""
    echo "error_log {"
    echo "    syslog daemon"
    echo "    priority notice"
    echo "}"
    echo ""
    echo "listen [::]:443 {"
    echo "    proto tls"
    echo "    table unlocked"
    echo "}"
    echo ""
    echo "table unlocked {"
    for d in $DOMAINS_V6 $DOMAINS_V4ONLY; do
      printf '    .*\.%s$    *:443n' "$d"
    done
    echo "}"
  } > "$SNIPROXY_CONF"
}

# 从当前 iptables 导入 53 端口白名单(首次安装用)
import_wl() {
  local def4="$1"
  [ -f "$WL_FILE" ] && return 0
  iptables -S INPUT 2>/dev/null | grep -- "--dport 53" | grep " -j ACCEPT" 
    | grep -oP '(?<=-s )[^ ]+' | sed 's|/32$||' | sort -u > "$WL_FILE"
  if [ ! -s "$WL_FILE" ]; then
    echo "127.0.0.1" > "$WL_FILE"
    [ -n "$def4" ] && echo "$def4" >> "$WL_FILE"
  fi
  echo "已导入白名单($(grep -c . "$WL_FILE") 个 IP)"
}

# 按白名单文件重建 53 端口防火墙规则并保存
wl_apply_fw() {
  local n ip
  # 先清掉所有 53 端口规则
  while :; do
    n=$(iptables -L INPUT -n --line-numbers 2>/dev/null | awk '/dpt:(53|domain)/{print $1; exit}')
    [ -z "$n" ] && break
    iptables -D INPUT "$n"
  done
  # 按白名单重建放行
  while read -r ip; do
    [ -z "$ip" ] && continue
    iptables -A INPUT -p udp -s "$ip" --dport 53 -j ACCEPT
    iptables -A INPUT -p tcp -s "$ip" --dport 53 -j ACCEPT
  done < "$WL_FILE"
  iptables -A INPUT -p udp --dport 53 -j DROP
  iptables -A INPUT -p tcp --dport 53 -j DROP
  # 443 放行(幂等)
  iptables -C INPUT -p tcp --dport 443 -j ACCEPT 2>/dev/null 
    || iptables -I INPUT -p tcp --dport 443 -j ACCEPT
  netfilter-persistent save >/dev/null 2>&1
}

wl_view() {
  echo "${CYAN}当前白名单(53 端口放行):${NC}"
  if [ ! -f "$WL_FILE" ] || [ ! -s "$WL_FILE" ]; then
    echo "(空)"
    return
  fi
  local i=1 ip
  while read -r ip; do
    printf " %2d) %sn" "$i" "$ip"
    i=$((i + 1))
  done < "$WL_FILE"
}

wl_add() {
  local inp ip
  read -rp "输入要增加的 IP(空格分隔多个,b 返回): " inp
  [[ "$inp" == "b" ]] && return
  for ip in ${inp//,/ }; do
    if ! valid_ip "$ip"; then
      echo "${RED}跳过非法 IP: $ip${NC}"
      continue
    fi
    if grep -qx "$ip" "$WL_FILE"; then
      echo "${YELLOW}已存在: $ip${NC}"
      continue
    fi
    echo "$ip" >> "$WL_FILE" && echo "${GREEN}已增加: $ip${NC}"
  done
  wl_apply_fw && echo "${GREEN}防火墙已更新并保存${NC}"
  pause
}

wl_del() {
  [ -f "$WL_FILE" ] || { echo "${RED}白名单不存在${NC}"; pause; return; }
  local -a ips
  mapfile -t ips < "$WL_FILE"
  [ ${#ips[@]} -eq 0 ] && { echo "(空)"; pause; return; }
  wl_view
  echo ""
  local inp
  read -rp "输入要删除的编号(空格分隔多个,支持 1-3 区间,b 返回): " inp
  [[ "$inp" == "b" ]] && return
  local -a del=()
  local tok a b i t
  for tok in $inp; do
    if [[ "$tok" =~ ^[0-9]+$ ]]; then
      del+=("$tok")
    elif [[ "$tok" =~ ^([0-9]+)-([0-9]+)$ ]]; then
      a="${BASH_REMATCH[1]}"; b="${BASH_REMATCH[2]}"
      ((a > b)) && { t=$a; a=$b; b=$t; }
      for ((i = a; i <= b; i++)); do del+=("$i"); done
    else
      echo "${YELLOW}忽略无效输入: $tok${NC}"
    fi
  done
  del=($(printf '%sn' "${del[@]}" | sort -nu))
  local n removed=0
  for ((i = ${#del[@]} - 1; i >= 0; i--)); do
    n=${del[$i]}
    if ((n >= 1 && n <= ${#ips[@]})); then
      echo "删除: ${ips[$((n - 1))]}"
      unset 'ips[$((n - 1))]'
      ((removed++))
    else
      echo "${YELLOW}编号超出范围: $n${NC}"
    fi
  done
  ips=("${ips[@]}")
  printf '%sn' "${ips[@]}" > "$WL_FILE"
  if [ $removed -gt 0 ]; then
    wl_apply_fw && echo "${GREEN}已删除 $removed 个,防火墙已更新并保存${NC}"
  fi
  pause
}

wl_edit() {
  if [ ! -f "$WL_FILE" ]; then
    echo "${RED}白名单不存在,请先安装${NC}"
    pause
    return
  fi
  local c
  while :; do
    clear
    wl_view
    echo ""
    echo " a) 增加 IP(可空格分隔多个)"
    echo " d) 删除 IP(编号,空格分隔多个,支持 1-3)"
    echo " b) 返回主菜单"
    read -rp "请选择 [a/d/b]: " c
    case "$c" in
      a | A) wl_add ;;
      d | D) wl_del ;;
      b | B) return ;;
    esac
  done
}

do_install() {
  local KIP4 KIP6
  KIP4=$(get_v4)
  KIP6=$(get_v6)
  [ -z "$KIP4" ] && read -rp "未检测到公网 IPv4,请手动输入: " KIP4
  if [ -z "$KIP4" ]; then echo "${RED}IPv4 为空,退出${NC}"; pause; return 1; fi
  if [ -z "$KIP6" ]; then
    read -rp "未检测到公网 IPv6(可直接回车跳过): " KIP6
  else
    echo "检测到 IPv6: $KIP6"
  fi
  echo "本机 IP: v4=$KIP4 v6=${KIP6:-(无)}"
  echo ""

  mkdir -p "$BASE_DIR" "$BACKUP_DIR"
  # 备份原装(只备一次)
  [ -f /etc/dnsmasq.conf ] && [ ! -f "$BACKUP_DIR/dnsmasq.conf" ] 
    && cp -a /etc/dnsmasq.conf "$BACKUP_DIR/"
  [ -f "$SNIPROXY_CONF" ] && [ ! -f "$BACKUP_DIR/sniproxy.conf" ] 
    && cp -a "$SNIPROXY_CONF" "$BACKUP_DIR/"
  [ ! -f "$BACKUP_DIR/iptables.rules" ] 
    && iptables-save > "$BACKUP_DIR/iptables.rules" 2>/dev/null

  echo "${CYAN}[1/6] 安装软件包...${NC}"
  apt-get update -qq 
    && apt-get install -y -qq dnsmasq sniproxy iptables-persistent 
    || { echo "${RED}apt 安装失败${NC}"; pause; return 1; }

  echo "${CYAN}[2/6] 写 dnsmasq 配置...${NC}"
  grep -q "^conf-dir=/etc/dnsmasq.d" /etc/dnsmasq.conf 2>/dev/null 
    || echo "conf-dir=/etc/dnsmasq.d" >> /etc/dnsmasq.conf
  write_dnsmasq_conf "$KIP4" "$KIP6"

  echo "${CYAN}[3/6] 写 sniproxy 配置...${NC}"
  write_sniproxy_conf
  mkdir -p "$(dirname "$SNIPROXY_DROPIN")"
  cat > "$SNIPROXY_DROPIN" << 'EOF'
[Service]
Type=forking
PIDFile=/run/sniproxy.pid
EOF
  systemctl daemon-reload

  echo "${CYAN}[4/6] 配置防火墙白名单...${NC}"
  import_wl "$KIP4"
  wl_apply_fw

  echo "${CYAN}[5/6] 启动服务...${NC}"
  dnsmasq --test || { echo "${RED}dnsmasq 配置检查失败${NC}"; pause; return 1; }
  systemctl enable --now dnsmasq
  systemctl enable --now sniproxy
  sleep 1

  echo "${CYAN}[6/6] 安装快捷键 ku...${NC}"
  ln -sf "$SCRIPT_PATH" "$SHORTCUT"
  chmod +x "$SHORTCUT"

  echo ""
  echo "${GREEN}安装完成!${NC}"
  do_status
  pause
}

do_uninstall() {
  echo "${YELLOW}将执行:停止服务 → 卸载 dnsmasq/sniproxy → 恢复原配置文件 → 恢复原防火墙 → 删除白名单与快捷键${NC}"
  local c
  read -rp "确认卸载并恢复原装?[y/N]: " c
  [[ "$c" == "y" || "$c" == "Y" ]] || { echo "已取消"; pause; return; }

  systemctl stop dnsmasq sniproxy 2>/dev/null
  systemctl disable dnsmasq sniproxy 2>/dev/null
  rm -f "$DNSMASQ_CONF"
  rm -rf /etc/systemd/system/sniproxy.service.d
  systemctl daemon-reload
  apt-get purge -y -qq dnsmasq sniproxy 2>/dev/null

  # 恢复原配置文件
  [ -f "$BACKUP_DIR/dnsmasq.conf" ] && cp -a "$BACKUP_DIR/dnsmasq.conf" /etc/dnsmasq.conf
  [ -f "$BACKUP_DIR/sniproxy.conf" ] && cp -a "$BACKUP_DIR/sniproxy.conf" "$SNIPROXY_CONF"

  # 恢复原防火墙
  if [ -f "$BACKUP_DIR/iptables.rules" ]; then
    iptables-restore < "$BACKUP_DIR/iptables.rules"
    netfilter-persistent save >/dev/null 2>&1
  fi

  rm -rf "$BASE_DIR" "$SHORTCUT"
  echo "${GREEN}已卸载并恢复原装(脚本本体 $SCRIPT_PATH 未删,需要可手动 rm)${NC}"
  pause
}

do_status() {
  echo "${CYAN}==== 运行状态 ====${NC}"
  printf "dnsmasq : %bn" "$(svc_state dnsmasq)"
  printf "sniproxy: %bn" "$(svc_state sniproxy)"
  echo "--- 监听端口 ---"
  ss -tlnp 2>/dev/null | grep -E ':53b' | head -5 || echo "(53 无监听)"
  ss -tlnp 2>/dev/null | grep -E ':443b' | head -5 || echo "(443 无监听)"
  echo "--- 白名单 ---"
  if [ -f "$WL_FILE" ]; then
    echo "共 $(grep -c . "$WL_FILE") 个 IP(菜单 7 查看明细)"
  else
    echo "(未安装)"
  fi
  echo "--- 快捷键 ---"
  if [ -L "$SHORTCUT" ]; then
    echo "ku -> $(readlink "$SHORTCUT")"
  else
    echo "(未安装)"
  fi
}

client_help() {
  local v4
  v4=$(get_v4)
  [ -z "$v4" ] && v4="<K机公网IP>"
  clear
  echo "${CYAN}========== 客户端设置(在需要解锁的 VPS 上执行,复制粘贴即可) ==========${NC}"
  echo ""
  echo "${YELLOW}第 1 步:备份原 DNS 并指向 K 机${NC}"
  echo "chattr -i /etc/resolv.conf 2>/dev/null"
  echo "cp -a /etc/resolv.conf /etc/resolv.conf.bak.kunlock"
  echo "printf 'nameserver $v4n' > /etc/resolv.conf"
  echo ""
  echo "${YELLOW}第 2 步:验证劫持生效(应返回 $v4)${NC}"
  echo "getent hosts www.netflix.com"
  echo ""
  echo "${YELLOW}第 3 步:跑解锁测试脚本,看 Netflix / Disney+ 是否为 SG${NC}"
  echo ""
  echo "${YELLOW}恢复原来 DNS(不需要解锁时执行)${NC}"
  echo "cp -a /etc/resolv.conf.bak.kunlock /etc/resolv.conf"
  echo ""
  echo "${RED}注意${NC}:部分系统(DHCP / NetworkManager / systemd-resolved)"
  echo "会覆写 /etc/resolv.conf,若发现被改回,请在对应服务中锁定 DNS。"
  echo ""
  echo "${CYAN}=========================================================================${NC}"
  pause
}

need_root

while :; do
  clear
  echo "========================================"
  echo "  K 机 DNS 解锁管理        ${CYAN}[ku]${NC}"
  echo "----------------------------------------"
  printf "  dnsmasq: %b   sniproxy: %bn" "$(svc_state dnsmasq)" "$(svc_state sniproxy)"
  echo "========================================"
  echo " 1. 安装"
  echo " 2. 卸载(恢复原装)"
  echo " 3. 开始"
  echo " 4. 停止"
  echo " 5. 重启"
  echo " 6. 运行状态"
  echo " 7. 查看白名单"
  echo " 8. 编辑白名单"
  echo " 9. 客户端设置说明"
  echo " 0. 退出"
  echo "========================================"
  read -rp "请选择 [0-9]: " ch
  case "$ch" in
    1) do_install ;;
    2) do_uninstall ;;
    3)
      systemctl start dnsmasq sniproxy
      echo "${GREEN}已启动${NC}"
      pause
      ;;
    4)
      systemctl stop dnsmasq sniproxy
      echo "${YELLOW}已停止${NC}"
      pause
      ;;
    5)
      systemctl restart dnsmasq sniproxy
      sleep 1
      echo "${GREEN}已重启${NC}"
      pause
      ;;
    6)
      clear
      do_status
      pause
      ;;
    7)
      clear
      wl_view
      pause
      ;;
    8) wl_edit ;;
    9) client_help ;;
    0) exit 0 ;;
    *) ;;
  esac
done