家宽无公网IP使用lucky进行STUN穿透搭配cloudflare配置重定向实现无视端口变化

背景

我的 All in One 主机,兢兢业业,已稳定运行近两年,承载着无数服务,也承载着我的心血。搭建之初,我曾细细记录,写下《8505 处理器家庭组网 PVE 下 iKuai、OpenWrt、CentOS、DSM、Windows 等系统 All In One 实践记录》,以为万事俱备,风调雨顺,岂料世事无常,人祸难防。

本来一切井然有序,系统运转如常,然今日却落得如此田地。只因某些见不得光的原因,我的公网 IP 竟莫名其妙地蒸发了。这篇文章的诞生,原非我所愿,然电信之所作所为,实在令人不吐不快,故此留一笔,以作纪念,也以警后来者。

我的电信宽带,自打安装之日起,便享有公网 IP 之恩泽。彼时,政策尚未收紧,天高地阔,数据自由流通。然而世事无常,宽带将于 4 月 1 日到期,近月来,电信营销人员如幽灵一般,三番五次来电,苦口婆心劝我更改套餐。我再三确认,套餐变更不影响公网 IP,方才点头应允。

3 月 27 日,短信如期而至,告知套餐已更改。我本不以为意,然而午后风波骤起,Dify 服务倏然失灵,nc 一测,端口悉数封闭。我仍不信邪,连上 frp 试验,果然 Dify 仍可运行。再看 ddns 解析,一切如旧。这才恍然大悟,公网已亡!

我立刻拨通电话,找电信讨说法,又联系了负责套餐变更的营销人员。孰料对方支支吾吾,竟说自己早已得知更改会影响公网 IP,因此未予上报。换言之,我的套餐变更,并非此人所为,而是另有黑手暗中操作!

愤怒之下,我即刻投诉,质问电信:何以未得我允,擅自改我套餐?更可恨者,竟使我公网 IP 化为乌有,令我自建服务顷刻崩塌。至今四日已过,其间周末无奈,百无聊赖之际,我只得埋头折腾公网穿透,以求重现往日之自由。

过程概述

解决访问和带宽上限问题

世人皆知,光有一台主机并不能改变命运,得有公网 IP 才能真正实现“自由”。可惜,我的自由,被电信夺走了。

lucky 之名,我早有耳闻(类似者尚有 natmapNatter)。然因往昔自持公网 IP,可直接 DDNS 连接回家,又备有 frp 中转与直连梯子,遂对“打洞”一事嗤之以鼻。如今遭此横祸,才不得不低头研究。细读 lucky 官网文档,再翻阅几篇博客,已然胸有成竹。

幸好,我的光猫本就桥接模式,爱快拨号,无需折腾光猫设定,直在爱快中设 DMZ 主机为 lucky 部署之机器 IP(置于 pve 上,以防 pve 的 Ubuntu 崩溃)。随后,在 lucky 穿透配置 Web 页面,将 Ubuntu 之 443 端口穿透至公网,立刻便获得一随机端口。我遂以此端口配合 DDNS 域名访问,果然,服务畅通无阻!

更妙者,我所有 web 服务及 ocserv 皆以 haproxy SNI 分发,只须一端口穿透,便可访问全部服务,顿时如拨云见日,豁然开朗。

至此,访问之难已解。先前 frp 亦可勉强使用,然其带宽仅 30M,且须绕道香港,家宽上行之优势尽毁。如今得 lucky 相助,不仅带宽恢复,延迟亦大幅降低,稳定性提升百倍。然美中不足者,乃穿透端口随时变换,且仅能支持 http,若 SSH、RDP、VNC 之流,仍需另开端口(若有高人知晓端口共享之法,敬请不吝赐教)。

解决端口变化在外知晓问题

天幸,lucky 体贴人心,自带 WebHook 及自定义脚本触发,可于端口变化时自动执行脚本,并发送请求(更妙者,还配有重试机制,实乃大智慧)。

于是,我开通网易 SMTP,写一脚本,使端口变更时自动邮件通知 QQ 邮箱(微信绑定邮箱,查阅尤为便捷),脚本在文章末尾提供。

解决访问web服务端口输入问题

虽可随时掌握最新端口,然其变幻无常,终非长久之计,尤对我辈强迫症与完美主义者而言,实在难以容忍。此时,cloudflare 之动态重定向功能映入眼帘,可自定义表达式处理源地址,遂欣然尝试。

初时,我误以为重定向域名须解析,后发现可直接开启代理(若使用别处服务商域名,只须修改 DNS 至 cloudflare 的亦可,唯此举需于 cloudflare 重新配置原服务商域名解析记录)。

代理既开,cloudflare 之动态重定向大功告成。操作要点:在 cloudflare 域名规则下编辑规则,保存后,于开发者工具或抓包工具中获取域名及规则 ID(cf本身会调接口)。

最终,实现所有对 xx.lucky.yourdomain.com 之 HTTP 请求,皆被重定向至 https://xx.ddns.yourdomain.com:#{port}。至此,重定向问题一举解决。

附图

lucky stun穿透列表

cloudflare 规则设置

这里规则名和表达式随便写,只是为了获取规则相关的参数,lucky的 webhook 调用后会更新

lucky stun 穿透设置

注意点

  • 自定义脚本和webhook里,lucky 提供的用于字符串匹配替换的目标字符串不一样,一个是 $ ,一个是 #,若不细察,便要一头撞在南墙上
  • lucky 如果是 docker 部署的,得进入容器安装命令所需的依赖,建议用宿主机部署添加计划任务
  • cloudflare api 令牌自行申请,权限类型别选错,须谨记:最小化控制,权限应设 区域 → 单一重定向
  • lucky webhook接口调用成功的字符串切勿多写少写空格之流

WebHook 相关

请求地址

https://api.cloudflare.com/client/v4/zones/your_domain_id/rulesets/your_ruleset_id/rules/your_rule_id

类型

PATCH

Header

Authorization: Bearer gFnpCZFKU524z_your_api_secret

Body:

{
    "action": "redirect",
    "action_parameters": {
      "from_value": {
        "preserve_query_string": true,
        "status_code": 307,
        "target_url": {
          "expression": "wildcard_replace(http.request.full_uri, \"*://*.lucky.yourdomain.com/*\", \"https://${2}.ddns.yourdomain.com:#{port}/${3}\")"
        }
      }
    },
    "description": "redirect to ddns server",
    "enabled": true,
    "expression": "(http.host wildcard \"*.lucky.yourdomain.com\")",
    "ref": "be425e021dbf408087b1b0f1844cc8b6"
}

邮件通知脚本

/etc/lucky/custom_scripts/stun_email_notify.sh --rule_name "${ruleName}" --port "${port}"

stun_email_notify.sh

#!/bin/bash

# set -euo pipefail

# */1 * * * * test -z "$(pidof lucky)" && /etc/lucky/lucky  >/dev/null 2>&1

log_message() {
  # 检测是否在终端环境中,并且终端支持颜色
  if [ -t 1 ] && [ "$(tput colors)" -ge 8 ]; then
    RED="\033[31m"
    GREEN="\033[32m"
    BLUE="\033[34m"
    YELLOW="\033[33m"
    NO_COLOR="\033[0m"
    DIVIDER="\033[;33m -------------------------------- \033[0m"
  else
    RED=""
    GREEN=""
    BLUE=""
    YELLOW=""
    NO_COLOR=""
    DIVIDER=" -------------------------------- "
  fi

  case "$1" in
  error)
    color="$RED"
    printf "${DIVIDER}\n"
    ;;
  success) color="$GREEN" ;;
  blue) color="$BLUE" ;;
  warning) color="$YELLOW" ;;
  info) color="$NO_COLOR" ;;
  *) color="$NO_COLOR" ;;
  esac

  # 使用printf代替echo,避免不支持-e选项的问题
  printf "${color}%s${NO_COLOR}\n" "${@:2}"

  # 打印分隔线在 error 消息后
  if [ "$1" = "error" ]; then
    printf "${DIVIDER}\n"
  fi
}

display_help() {
  echo -e "${YELLOW}使用方法: $0 [选项]${NO_COLOR}"
  echo
  log_message blue "该脚本用于在 Lucky STUN 穿透规则端口变化时,发送邮件通知"
  echo
  echo "作者: VanJay"
  echo "版本: 1.0.0"
  echo "邮箱: vanjay.dev@gmail.com"
  echo
  echo "选项:"

  echo "  --rule_name 规则名称       必填"
  echo "  --port 新端口              必填"
  echo "  -help | -h                 显示此帮助信息并退出"

  echo
  echo "示例:"
  echo "  $0 --rule_name PVE_Ubuntu_Haproxy_UPnP --port 15322"
  exit 0
}

DEFAULT_RULE_NAME="${ruleName}"
DEFAULT_PORT="${port}"

RULE_NAME=""
OUT_PORT=""

# 解析命令行参数
while [ "$#" -gt 0 ]; do
  case "$1" in
  --rule_name)
    shift
    if [ -z "$1" ]; then
      RULE_NAME="${DEFAULT_RULE_NAME}"
    else
      RULE_NAME="$1"
    fi
    ;;
  --port)
    shift
    # 检查端口是否为数字
    if ! [ "$1" -eq "$1" ] 2>/dev/null; then
      log_message error "无效端口号: $1"
      exit 1
    fi
    OUT_PORT="${1:-${DEFAULT_PORT}}"
    ;;
  -help | -h)
    display_help
    ;;
  *)
    log_message error "未知选项 $1"
    display_help
    ;;
  esac
  shift
done

if [ -z "${RULE_NAME}" ]; then
  log_message error "规则名称为空"
  display_help
  exit 1
fi

if [ -z "${OUT_PORT}" ]; then
  log_message error "端口为空"
  display_help
  exit 1
fi

log_message warning "规则名称: $RULE_NAME"
log_message warning "端口: $OUT_PORT"

echo "开始发送邮件"

# 构建标准MIME格式邮件内容
build_mail_content() {
  local boundary="----=_NextPart_$(date +%s%N | sha1sum | cut -c1-32)"
  
  cat <<EOF
From: your_from_email@163.com
To: your_email@qq.com
Subject: =?UTF-8?B?$(echo "穿透规则 ${RULE_NAME} 端口变化" | base64)?=
Content-Type: multipart/mixed; boundary="$boundary"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit

--$boundary
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: quoted-printable

规则名称: ${RULE_NAME}
当前端口: ${OUT_PORT}

--$boundary
Content-Type: application/octet-stream; name="$(basename "$mail_file").txt"
Content-Transfer-Encoding: base64
Content-Disposition: attachment; filename="$(basename "$mail_file").txt"

$(base64 < "$mail_file")
--$boundary--
EOF
}
mail_file=$(mktemp -t "${RULE_NAME}_mail.XXXXXX") || { exit 1; }
build_mail_content > "$mail_file"
# echo -e "From: your_from_email@163.com\nTo: your_email@qq.com\nSubject: 穿透规则 ${RULE_NAME} 端口变化\n\n当前端口:${OUT_PORT}\n规则名称: ${RULE_NAME}" >"$mail_file"

# 发送邮件(带错误重试)
max_retry=3
count=0
exit_code=1

while [ $count -lt $max_retry ]; do
  cmd="curl --url 'smtps://smtp.163.com:465' --user 'your_from_email:your_api_secret' --mail-from 'your_from_email@163.com' --mail-rcpt 'your_email@qq.com' -T "$mail_file" 2>&1"
  log_message warning "执行邮件发送命令: $cmd"
  log_message blue "$(echo "邮件内容:\n$(cat "$mail_file")")"
  response=$(eval "$cmd")
  exit_code=$?
  [ $exit_code -eq 0 ] && break
  count=$((count + 1))
  log_message warning "⚠️ 邮件发送失败,正在重试 (第 $count 次)..."
  sleep 5
done

# 判断最终状态
if [ $exit_code -eq 0 ]; then
  echo "✅ 邮件发送成功"
else
  echo "❌ 邮件发送失败(错误码: $exit_code)"
  echo "错误详情: $response"
  exit $exit_code # 传递错误码供外部捕获
fi