久光本光的主页 久光本光的主页
  • 首页
  • 知识区
  • 影视区
  • 游戏区
  • 软件区
首页 › 软件区 › stun打洞+cloudflare回源规则,将本地服务放进公网

stun打洞+cloudflare回源规则,将本地服务放进公网

久光本光
5 月前

原理及效果

先说效果:本地路由器或NAS上部署的如vaultwarden这类服务,即使没有公网ipv4,只要有nat1(也就是fullcone全锥形网络),即可通过cloudflare的动态dns及回源规则,配合stun打洞实现近似公网使用的效果

原理:通过stun打洞将本地服务暴露至公网(本方案使用lucky工具,通过触发脚本实现动态dns更新及回源规则更新,将打洞获得的公网端口更新至cloudflare回源规则)

流量路径:用户访问 -> cloudflare -> origin rules的动态端口 -> lucky主机的ip:穿透通道本地端口 -> 部署服务的主机ip:本地服务端口

准备工作

  1. 本地网络开启fullcone,iStoreOS及大多数openwrt固件、iKuai均可一键开启
    stun打洞+cloudflare回源规则,将本地服务放进公网-久光本光的主页

  2. 部署好本地服务后,在路由器的防火墙规则添加对应的端口转发规则(这里是局域网的固定端口)
    stun打洞+cloudflare回源规则,将本地服务放进公网-久光本光的主页

  3. 确保lucky运行的终端已安装curl及jq

    opkg update
    opkg install curl
    opkg install jq
  4. 安装lucky,iStoreOS可以在软件商城一件安装,其余请参考lucky安装文档

操作步骤

  1. 获得cloudflare的zone_id并创建一个api令牌
    api令牌创建:https://dash.cloudflare.com/profile/api-tokens
    权限需要两个:DNS和Origin Rules
    stun打洞+cloudflare回源规则,将本地服务放进公网-久光本光的主页

zone_id在进入cloudflare的域名管理页面右下角
stun打洞+cloudflare回源规则,将本地服务放进公网-久光本光的主页

  1. 确认需要暴露的服务已参考准备工作2添加好防火墙转发规则

  2. 通过lucky开启stun打洞并填写触发脚本
    stun打洞+cloudflare回源规则,将本地服务放进公网-久光本光的主页

务必全部按图设置,不要使用lucky内置端口转发,而必须通过路由器的防火墙端口转发,触发脚本需要填写4个位置,分别是服务名称(随意)、域名、zoneid及api令牌,以下是完整脚本:

SERVICE_NAME="<填写你的服务名称>"
DOMAIN_NAME="<填写你的域名如aa.bb.com>"
CF_ZONE_ID="<cloudflare的zone_id>"
CF_TOKEN="<刚才获取的api令牌>"

# 最大重试次数
MAX_RETRIES=10
RETRY_DELAY=5

# =======================================================
# 2. 接收 Lucky 变量并更新“最新状态”
# =======================================================
# 接收 Lucky 传入的原始变量
INPUT_IP="${ip}"
INPUT_PORT="${port}"

# 定义状态文件路径 (每个服务独立)
STATE_FILE="/tmp/lucky_state_${SERVICE_NAME}.info"

# 【核心修复步骤 1】
# 脚本一启动,立即将最新收到的参数写入状态文件。
# 无论后续排队多久,所有排队的脚本最终读取的都是最后一次写入的文件内容。
echo "${INPUT_IP} ${INPUT_PORT}" > "$STATE_FILE"

# =======================================================
# 3. 全局锁与日志
# =======================================================
GLOBAL_LOCK_FILE="/tmp/lucky_cloudflare_global_update.lock"
LOG_FILE="/tmp/lucky_cf_update.log"

log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')][${SERVICE_NAME}] $1" >> "$LOG_FILE"
}

safe_curl() {
    local method="$1"
    local url="$2"
    local data="$3"
    local count=0
    local response=""

    while [ $count -lt $MAX_RETRIES ]; do
        if [ -n "$data" ]; then
            response=$(curl -s -X "$method" "$url" \
                -H "Authorization: Bearer $CF_TOKEN" \
                -H "Content-Type: application/json" \
                --data "$data")
        else
            response=$(curl -s -X "$method" "$url" \
                -H "Authorization: Bearer $CF_TOKEN" \
                -H "Content-Type: application/json")
        fi

        if echo "$response" | grep -q "success"; then
            echo "$response"
            return 0
        fi

        count=$((count + 1))
        # 如果是连接被拒绝等严重网络错误,稍微多等一会
        sleep $RETRY_DELAY
    done

    log "错误: API请求失败 ($url)"
    return 1
}

# =======================================================
# 4. 后台执行逻辑
# =======================================================
(
    # 随机延时 (保留原有逻辑,缓解并发)
    RANDOM_DELAY=$(awk 'BEGIN{srand(); print int(rand()*3)}')
    sleep $RANDOM_DELAY

    # --- 获取全局锁 ---
    LOCK_WAIT_COUNT=0
    while [ -f "$GLOBAL_LOCK_FILE" ]; do
        LOCK_TIME=$(date -r "$GLOBAL_LOCK_FILE" +%s)
        NOW_TIME=$(date +%s)
        # 锁超时检查 (120秒)
        if [ $((NOW_TIME - LOCK_TIME)) -gt 120 ]; then
            log "检测到死锁,强制释放"
            rm -f "$GLOBAL_LOCK_FILE"
            break
        fi

        if [ $LOCK_WAIT_COUNT -gt 60 ]; then
             log "排队超时,放弃本次执行"
             exit 0
        fi

        sleep 2
        LOCK_WAIT_COUNT=$((LOCK_WAIT_COUNT + 1))
    done

    touch "$GLOBAL_LOCK_FILE"
    trap "rm -f '$GLOBAL_LOCK_FILE'; exit" EXIT TERM INT

    # 日志轮转
    [ -f "$LOG_FILE" ] && [ $(wc -c < "$LOG_FILE") -gt 100000 ] && echo "" > "$LOG_FILE"

    # 【核心修复步骤 2】
    # 拿到锁之后,不使用自己的变量,而是从状态文件读取“真正的最新值”
    if [ -f "$STATE_FILE" ]; then
        read TARGET_IP TARGET_PORT < "$STATE_FILE"
    else
        log "错误: 状态文件丢失"
        rm -f "$GLOBAL_LOCK_FILE"
        exit 1
    fi

    if [ -z "$TARGET_IP" ] || [ -z "$TARGET_PORT" ]; then
        log "错误: 状态文件内容为空"
        rm -f "$GLOBAL_LOCK_FILE"
        exit 1
    fi

    log "开始处理 (最新目标): $DOMAIN_NAME -> $TARGET_IP:$TARGET_PORT"

    # --- A. 更新 DNS A 记录 ---
    DNS_RES=$(safe_curl "GET" "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records?type=A&name=$DOMAIN_NAME" "")
    if [ $? -ne 0 ]; then rm -f "$GLOBAL_LOCK_FILE"; exit 1; fi

    DNS_ID=$(echo "$DNS_RES" | jq -r '.result[0].id')
    CURRENT_DNS_IP=$(echo "$DNS_RES" | jq -r '.result[0].content')

    if [ "$DNS_ID" = "null" ]; then
        log "DNS记录不存在,创建中..."
        safe_curl "POST" "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records" \
            "{\"type\":\"A\",\"name\":\"$DOMAIN_NAME\",\"content\":\"$TARGET_IP\",\"ttl\":60,\"proxied\":true}" > /dev/null
    elif [ "$CURRENT_DNS_IP" != "$TARGET_IP" ]; then
        log "更新 DNS IP ($CURRENT_DNS_IP -> $TARGET_IP)..."
        safe_curl "PATCH" "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/dns_records/$DNS_ID" \
            "{\"content\":\"$TARGET_IP\"}" > /dev/null
    else
        # log "DNS IP 无需更新" # 减少日志噪音
        :
    fi

    # --- B. 更新 Origin Rules ---
    PHASE_RES=$(safe_curl "GET" "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/rulesets/phases/http_request_origin/entrypoint" "")
    if [ $? -ne 0 ]; then rm -f "$GLOBAL_LOCK_FILE"; exit 1; fi
    RULESET_ID=$(echo "$PHASE_RES" | jq -r '.result.id')

    if [ "$RULESET_ID" != "null" ]; then
        RULES_RES=$(safe_curl "GET" "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/rulesets/$RULESET_ID" "")
        if [ $? -ne 0 ]; then rm -f "$GLOBAL_LOCK_FILE"; exit 1; fi

        # 查找同名规则
        # 获取 Rule ID 和当前规则中设定的端口
        TARGET_RULE_DATA=$(echo "$RULES_RES" | jq -r --arg name "$SERVICE_NAME" '(.result.rules // [])[] | select(.description == $name) | "\(.id)|\(.action_parameters.origin.port // 0)"')

        # 处理多条规则重复的情况,只取最后一条,其他的并在后面逻辑清理
        TARGET_RULE_ID=$(echo "$TARGET_RULE_DATA" | tail -n 1 | cut -d "|" -f 1)
        CURRENT_RULE_PORT=$(echo "$TARGET_RULE_DATA" | tail -n 1 | cut -d "|" -f 2)

        # 构造 Payload
        PAYLOAD=$(jq -n \
                    --arg desc "$SERVICE_NAME" \
                    --arg domain "$DOMAIN_NAME" \
                    --argjson port "$TARGET_PORT" \
                    '{
                        description: $desc,
                        expression: ("(http.host eq \"" + $domain + "\")"),
                        action: "route",
                        action_parameters: {origin: {port: $port}}
                    }')

        if [ -z "$TARGET_RULE_ID" ]; then
            log "规则不存在,新建规则..."
            safe_curl "POST" "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/rulesets/$RULESET_ID/rules" "$PAYLOAD" > /dev/null

        else
            # 【优化】只有当 CF 里的端口 和 目标端口 不一致时才调用 API
            if [ "$CURRENT_RULE_PORT" != "$TARGET_PORT" ]; then
                log "端口变更 ($CURRENT_RULE_PORT -> $TARGET_PORT),更新规则..."
                safe_curl "PATCH" "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/rulesets/$RULESET_ID/rules/$TARGET_RULE_ID" "$PAYLOAD" > /dev/null
            else
                log "规则端口 ($CURRENT_RULE_PORT) 已是最新,跳过更新。"
            fi

            # 清理重复规则 (如果有多个同名规则)
            ALL_IDS=$(echo "$RULES_RES" | jq -r --arg name "$SERVICE_NAME" '(.result.rules // [])[] | select(.description == $name) | .id')
            for id in $ALL_IDS; do
                if [ "$id" != "$TARGET_RULE_ID" ]; then
                     log "发现冗余规则,删除 ID: $id"
                     safe_curl "DELETE" "https://api.cloudflare.com/client/v4/zones/$CF_ZONE_ID/rulesets/$RULESET_ID/rules/$id" "" > /dev/null
                fi
            done
        fi
    else
        log "错误: 无法获取 Ruleset ID"
    fi

    rm -f "$GLOBAL_LOCK_FILE"

) >/dev/null 2>&1 &

echo "后台更新任务已排队触发 (State: $INPUT_PORT)"
exit 0
  1. lucky穿透成功后,可以去cloudflare后台确认dns解析和origin rules是否生效
    stun打洞+cloudflare回源规则,将本地服务放进公网-久光本光的主页

  2. 确认是否可以通过域名直接访问你的本地服务

补充说明

使用本方案进行stun穿透,必须保证本地网络连接stun服务器是通过直连(保证3478端口直连)

久光本光

听不见音乐的人以为跳舞的人疯了
34
文章
1
评论
18
喜欢

随机推荐

从史册到人心,一代名臣化身青天阎罗【包拯人物志】
万字解读《庆余年》:被严重低估的深刻立意,和无法跨越的创作陷阱
wordpress主题 Farallon 调整记录
Windows X-Lite 26h1 V3汉化版 下载地址

热门标签

cloudflare (1) cs (1) fps游戏 (2) stun穿透 (1) windows (6) X-Lite (1) X-Lite中文 (1) X-Lite汉化 (1) XOS (2) 万字解读 (1) 人物志 (8) 优化 (2) 公网 (1) 刘慈欣 (3) 包拯 (1) 北宋 (5) 南宋 (1) 历史 (9) 原版windows (2) 反恐精英 (1) 变法 (1) 将军 (2) 小狼毫 (1) 小说解读 (4) 岳飞 (1) 庆余年 (1) 影视杂谈 (1) 文人 (8) 棋王 (1) 游戏剧情 (1) 游戏教程 (1) 游戏解读 (1) 王安石 (1) 王荆公 (1) 科幻 (3) 精神内耗 (1) 精简 (5) 系统优化 (3) 苏东坡 (2) 苏轼 (2) 范仲淹 (1) 西汉 (2) 遗憾 (1) 阿城 (1) 项脊轩志 (1)

猜你喜欢

windows11镜像推荐:X-Lite 26h1 V3汉化版

windows11镜像推荐:X-Lite 26h1 V3汉化版

4 月前
iptables规则持久化指南

iptables规则持久化指南

2 年前
使用github action编译自定义rustdesk客户端指南(内置自己的服务器信息、内置固定密码、内置自定义api服务器)

使用github action编译自定义rustdesk客户端指南(内置自己的服务器信息、内置固定密码、内置自定义api服务器)

1 年前
Windows证书拉取powershell脚本:解决各类软件安装报错(走微软官方服务器)

Windows证书拉取powershell脚本:解决各类软件安装报错(走微软官方服务器)

2 月前
Copyright © 2026 久光本光的主页. Designed by nicetheme.
苏公网安备32100302010906号 苏ICP备2024106004号
  • 首页
  • 知识区
  • 影视区
  • 游戏区
  • 软件区
  • 历史
  • 人物志
  • windows

久光本光

听不见音乐的人以为跳舞的人疯了