Lucky STUN + Cloudflare Worker 实现内网服务双栈访问

前置说明

阅读本文需具备一定的 Lucky 使用经验以及 Cloudflare 使用经验,若没有请自行查阅相关文章后再继续阅读。

本文所有操作均为文字描述,无图演示!

前置条件:

  • Lucky 检测中检测出的 NAT 类型最好为 NAT1
  • 路由器拨号且路由器开启了 UPnP 功能

Lucky 动态域名

假设你的域名为 example.com,推荐配置 A 记录为 *.ipv4.example.com,AAAA 记录为 *.ipv6.example.com。

当然你也可以配置为相同的,如 *.real.example.com。

具体操作步骤请参阅相关文章自行配置。

Lucky Web 服务配置

添加 Web 服务规则,下面介绍需要配置的项。

基础配置

  • Web 服务规则名称:自定义
  • 规则开关:开
  • 操作模式:简易模式
  • 监听类型:全选(IPV4 + IPV6 )
  • 监听端口:100(这里可自定义为你自己的端口)
  • 防火墙自动放行:开
  • TLS:开
  • TLS 最低版本限制:TLS 1.2(默认即可)

子规则

若配置的动态域名 A 记录为 *.ipv4.example.com,AAAA 记录为 *.ipv6.example.com。

添加子规则,下面介绍需要配置的项。

  • 子规则名称:hi(假设这里提供名为 hi 的服务)
  • 子规则开关:开
  • 操作模式:简易模式
  • 服务类型:反向代理
  • 前端地址:
    • hi.ipv4.example.com
    • hi.ipv6.example.com
  • 后端地址:http://127.0.0.1:8080
    (填写为你真正提供服务的地址。可填写 Docker 容器或本地提供的服务)
  • 万事大吉:开
  • 忽略后端 TLS 证书验证:开

其他项可根据需求自行配置。

STUN 穿透配置

添加穿透规则,下面介绍需要配置的项。

基础配置

  • 规则名称:自定义即可
  • 操作模式:简易模式
  • 穿透类型:IPv4-TCP
  • 穿透通道本地端口:0
  • 防火墙自动放行:关
  • UPnP:开(默认不做任何设置,如有需要自行查阅相关文档后设置)
  • NAT-PMP:关
  • UPnP 内部端口自定义:指向你 Lucky Web 服务端口,如 100
  • 不使用Lucky内置端口转发:开(这里推荐启用,方便服务获取来访者真实 IP 地址)
  • 禁用自身转发检测:关
  • 自定义脚本触发:开

自定义脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/bin/sh
# ============================================
# Cloudflare 配置
# ============================================

# Cloudflare 编辑 Worker API Token
CF_WORKER_API_TOKEN=""
# Cloudflare 账户 ID
CF_ACCOUNT_ID=""
# Worker 名称(可自定义)
WORKER_NAME="stun-redirect"
# 主域名
BASE_DOMAIN="example.com"
# IPv4 子域名后缀和端口
V4_SUFFIX="ipv4.example.com"
V4_PORT=${port}
# IPv6 子域名后缀和端口
V6_SUFFIX="ipv6.example.com"
V6_PORT=100

# ============================================
# Worker 代码
# ============================================

WORKER_CODE=$(cat <<EOF
const CONFIG = {
baseDomain: '${BASE_DOMAIN}',
ipv4: {
suffix: '${V4_SUFFIX}',
port: ${V4_PORT}
},
ipv6: {
suffix: '${V6_SUFFIX}',
port: ${V6_PORT}
}
};

addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request));
});

/**
* 请求入口
*/
async function handleRequest(request) {
try {
const corsHeaders = buildCorsHeaders(request);

// OPTIONS 预检
if (request.method === 'OPTIONS') {
return new Response(null, {
status: 204,
headers: corsHeaders
});
}

const url = new URL(request.url);
const subdomain = extractSubdomain(url.hostname);
const networkConfig = getNetworkConfig(request);
const targetUrl = buildRedirectUrl({
originalUrl: url,
subdomain,
networkConfig
});

// 创建原始 307
const redirectResponse = Response.redirect(targetUrl, 307);

// 复制 Header
const headers = new Headers(redirectResponse.headers);

// 添加 CORS
for (const [key, value] of Object.entries(corsHeaders)) {
headers.set(key, value);
}

// 返回最终 307
return new Response(null, {
status: 307,
headers
});
} catch (error) {
return createErrorResponse(error, request);
}
}

/**
* 构建 CORS Header
*/
function buildCorsHeaders(request) {
const origin = request.headers.get('Origin') || '*';

return {
'Access-Control-Allow-Origin': origin,
'Access-Control-Allow-Credentials': 'true',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': '*',
'Access-Control-Max-Age': '86400',
'Vary': 'Origin'
};
}

/**
* 提取子域名
*/
function extractSubdomain(hostname) {
const normalized = hostname.toLowerCase();

if (normalized === CONFIG.baseDomain) {
return '';
}

const suffix = '.' + CONFIG.baseDomain;

if (!normalized.endsWith(suffix)) {
return '';
}

return normalized.slice(0, -suffix.length);
}

/**
* 获取客户端 IP
*/
function getClientIP(request) {
return request.headers.get('CF-Connecting-IP') || '';
}

/**
* 判断是否为 IPv6
*/
function isIPv6Address(ip) {
return ip.includes(':');
}

/**
* 根据客户端 IP 获取网络配置
*/
function getNetworkConfig(request) {
const clientIP = getClientIP(request);

if (isIPv6Address(clientIP)) {
return CONFIG.ipv6;
}

return CONFIG.ipv4;
}

/**
* 构建目标域名
*/
function buildTargetDomain(subdomain, suffix) {
if (!subdomain) {
return suffix;
}

return subdomain + '.' + suffix;
}

/**
* 构建跳转 URL
*/
function buildRedirectUrl({ originalUrl, subdomain, networkConfig }) {
const targetDomain = buildTargetDomain(subdomain, networkConfig.suffix);

return 'https://' + targetDomain + ':' + networkConfig.port + originalUrl.pathname + originalUrl.search;
}

/**
* 创建错误响应
*/
function createErrorResponse(error, request) {
return new Response(error.stack || error.toString(), {
status: 500,
headers: {
'Content-Type': 'text/plain;charset=UTF-8',
...buildCorsHeaders(request)
}
});
}
EOF
)

# ============================================
# 部署 Worker
# ============================================

RESULT=$(curl -s -X PUT \
"https://api.cloudflare.com/client/v4/accounts/${CF_ACCOUNT_ID}/workers/scripts/${WORKER_NAME}" \
-H "Authorization: Bearer ${CF_WORKER_API_TOKEN}" \
-H "Content-Type: application/javascript" \
--data "$WORKER_CODE")

# ============================================
# 输出结果
# ============================================

if echo "$RESULT" | grep -q '"success"[[:space:]]*:[[:space:]]*true'; then
echo "✅ Worker 部署成功"
else
echo "❌ Worker 部署失败"
echo "$RESULT"
fi

Worker 路由

添加路由为:hi.example.com/*。

此时访问 hi.example.com 时,会自动将请求转发到 Lucky Web 服务,至于具体走 IPv4 还是 IPv6,会根据客户端的 IP 地址自动判断。