记一次校园网 K8S 集群内网穿透之旅:从 Cloudflare 到 FRP + Nginx

前言

最近手里有一个部署在校园网内部 K8S 集群上的项目(Satellite Micro)。环境很典型:服务器有教育网 IP(223.2.x.x),出站流量虽然有些限制但基本能通,但入站流量被学校防火墙像铁桶一样挡在外面。

为了让外网(或者说在北京的领导们)能访问到 K8S 里的前端和 API,我开始了一场内网穿透的折腾之旅。

第一阶段:尝试 Cloudflare Tunnel(以失败告终)

最开始我想用 Cloudflare Tunnel (Zero Trust),毕竟它是云原生方案,不需要自己买 VPS,而且配置起来显得很“现代化”。

1. 部署尝试

我用的是Cloudflare官网的Zero Trust,需要在网络里新建连接器,也即tunnel,另在 K8S 里部署了 cloudflared 的 Deployment,配置了 Token。理论上,它应该主动向 Cloudflare 边缘节点发起连接,建立隧道。

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
apiVersion: apps/v1  
kind: Deployment
metadata:
name: cloudflare
namespace: satellite-micro
labels:
app: cloudflare
spec:
replicas: 1 # 只有1个副本也够用,生产环境可以设为2
selector:
matchLabels:
app: cloudflare
template:
metadata:
labels:
app: cloudflare
spec:
containers:
- name: cloudflare
image: 223.2.44.251/satellite/cloudflare:latest
imagePullPolicy: IfNotPresent
args:
- tunnel
# 必须加上这个,防止容器尝试自动升级导致权限问题
- --no-autoupdate
- run
- --protocol
- http2
- --token
- eyJhIjoiNTRiYWRhYTRkYjUxZmNlYTJiNDlmYjgxZDM0MWNkM2QiLCJ0IjoiYmZhNDk3OWYtOWE1OC00YzBhLTgyNjAtYTgyYWYyNjY3Y2Q5IiwicyI6Ik4yTTVNRE5oT1RZdFlURTRZUzAwWkdOa0xXSmlORGd0TjJZMFpqVXpaRGMwWlRobCJ9
env:
- name: TUNNEL_PROTOCOL
value: http2
resources:
requests:
cpu: 10m
memory: 64Mi
limits:
cpu: 500m
memory: 256Mi
restartPolicy: Always

2. 遇到的坑

Pod 启动后,日志直接红了一片:

  • UDP 被拦: 起初报错 failed to dial to edge with quic。这是因为 Cloudflare 默认使用 QUIC (基于 UDP) 协议,而校园网防火墙对 UDP 并不友好。

  • 改用 TCP 依然跪: 我通过环境变量 TUNNEL_PROTOCOL: http2 强制它走 TCP。结果报错变成了 dial tcp 198.41.200.193:7844: i/o timeout

3. 放弃原因

经过排查发现,Cloudflare Tunnel 建立连接需要访问边缘节点的 7844 端口。然而,严苛的教育网防火墙似乎只放行了标准的 80/443 端口,7844 这种非标端口直接被掐断了。

虽然心有不甘,但“网络层物理封锁”难以逾越,遂放弃 CF 方案。


第二阶段:回归 FRP —— 端口伪装与全链路打通

既然免费的午餐吃不到,还是得靠自己。我有一台阿里云的 VPS(114.55.x.x),决定用 FRP 方案。为了骗过校园网防火墙,核心思路是:把 FRP 流量伪装成普通的 HTTPS 流量(走 443 端口)

1. VPS 服务端配置 (frps)

为了防止被识别为非 HTTP 流量导致连接重置(EOF),我在服务端强制开启了 TLS。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# frps.toml
# frps.toml
# 服务端监听端口,必须是 443,这是穿透校园网的关键
bindPort = 443

# 授权令牌,相当于密码,客户端也要填一样的
auth.token = "OpenGMS123456"
transport.tls.force = true # 关键!强制 TLS,防止防火墙 DPI 检测

# (可选)如果你想看 FRP 的仪表盘,配置下面这几行
webServer.addr = "0.0.0.0"
webServer.port = 7500
webServer.user = "admin"
webServer.password = "admin"

2. K8S 客户端配置 (frpc)

在 K8S 中,我通过 ConfigMap 挂载配置,注意 TOML 格式的坑,全局配置必须写在 [[proxies]] 上面

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
# frpc.toml (ConfigMap)
apiVersion: v1
kind: ConfigMap
metadata:
name: frpc-config
namespace: satellite-micro # 建议把 FRP 放在和服务同一个命名空间,方便管理
data:
frpc.toml: |
# ==============================
# 连接 VPS 的配置
# ==============================
serverAddr = "114.55.142.175" # 【修改这里】你的 VPS 公网 IP
serverPort = 443 # 必须对应 frps.toml 里的 bindPort (443)
auth.token = "OpenGMS123456" # 【修改这里】必须和 frps.toml 里的 token 一致
transport.tls.enable = true
# ==============================
# 穿透规则:卫星前端服务
# ==============================
[[proxies]]
name = "satellite-frontend"
type = "tcp"

# K8S 内部 DNS 地址 (服务名.命名空间.svc)
localIP = "satellite-front-v2.satellite-micro.svc"

# 你的 Service 端口
localPort = 5173

# 【关键】外网 VPS 监听端口
remotePort = 5173

---
apiVersion: apps/v1
kind: Deployment
metadata:
name: frpc-satellite
namespace: satellite-micro # 部署在同一个命名空间
labels:
app: frpc-satellite
spec:
replicas: 1
selector:
matchLabels:
app: frpc-satellite
template:
metadata:
labels:
app: frpc-satellite
spec:
containers:
- name: frpc
image: 223.2.44.251/satellite/frpc:latest
resources:
limits:
cpu: 200m
memory: 128Mi
volumeMounts:
- name: config
mountPath: /etc/frp/frpc.toml
subPath: frpc.toml
volumes:
- name: config
configMap:
name: frpc-config

3. 一个小插曲:神奇的 i/o timeout

配置好后,日志一度报 i/o timeout。查了半天防火墙和安全组,最后发现竟然是因为K8S 宿主机的校园网登录认证过期了……

1
curl -k -c /tmp/school_cookies.txt -b /tmp/school_cookies.txt   "http://172.28.255.156:801/eportal/portal/login?callback=dr1003&login_method=1&user_account=%2C0%2C学号&user_password=密码"   -v -L -D /tmp/login_headers.txt -o /tmp/login_body.txt

在宿主机上写个脚本 curl 模拟登录后,FRP 瞬间显示 start proxy success。此时,我已经可以通过 http://114.55.142.175:5173 访问到前端页面了。


第三阶段:解决 403 Forbidden —— 后端代码与 Nginx 的博弈

原本以为网络通了就万事大吉,结果登录时前端报错:

Status Code: 403 Forbidden Request URL: http://114.55.142.175:5173/api/user/login

奇怪的是,如果我用内网 IP (223.2.x.x) 访问,一切正常。

1. 原理分析:Spring Boot 的 CORS 校验

看了后端 Java 代码才恍然大悟。后端配置了 Spring Security 的 CORS 策略:

1
2
3
4
5
6
7
8
// 后端代码片段
registry.addMapping("/**")
.allowedOriginPatterns(
"http://localhost:*",
"http://223.*", // 👈 信任校园网/内网 IP
"http://192.*"
// 😱 唯独没有信任我的 VPS IP (114.*)
);

案情还原:

  1. 当我通过 FRP 访问时,浏览器发出的 HTTP 请求头中,Origin 是 http://114.55.142.175:5173

  2. Nginx 默认透传了这个 Header 给后端。

  3. 后端保安一看:“114 开头的?不在白名单里,拒绝!”

2. 解决方案:Nginx “偷渡”法

为了解决这个问题,我有两个选择:要么改 Java 代码重新打包镜像(太慢),要么改前端 Nginx 配置(快)。

我选择了后者。利用 Nginx 修改请求头,把 Origin 抹除,让后端误以为这是一个“同源请求”或“非跨域请求”。

修改前端 Nginx 的 location /api/ 配置:

1
2
3
4
5
6
7
location /api/ {
proxy_pass http://20.1.59.49:8999/api/v1/;

# 👇 关键修改:把身份证撕了
proxy_set_header Origin "";
proxy_set_header Referer "";
}

3. 结果

配置生效后,Nginx 在转发请求前去掉了 Origin 头。Spring Boot 收到请求后,发现没有 Origin 信息,默认跳过了 CORS 检查机制。

刷新浏览器,登录成功,接口返回 200 OK!


总结

这次折腾让我深刻理解了网络链路的每一环:

  1. 物理层/防火墙层: 校园网环境下,标准端口(443)+ TLS 加密是穿透防火墙的最稳方案,UDP 和非标端口基本没戏。

  2. 网络层: FRP 在 K8S 里配合 ConfigMap 使用非常灵活,但要注意宿主机的网络连通性(别忘了登录校园网账号!)。

  3. 应用层: 网络通了不代表应用能用。同源策略后端白名单是跨域访问的两大拦路虎。通过 Nginx 清洗 Header 是一种非常实用的运维级规避手段。

希望这篇踩坑记录能帮到同样在校园网里挣扎的 K8S 玩家们。


记一次校园网 K8S 集群内网穿透之旅:从 Cloudflare 到 FRP + Nginx
http://example.com/2025/12/05/记一次校园网-K8S-集群内网穿透之旅:从-Cloudflare-到-FRP-Nginx/
作者
Lingkai Shi
发布于
2025年12月5日
许可协议