希曼日记

Pangolin内网穿透实战

本页目录 5
  1. 坑一:k8s 服务暴露,envoy 对一切请求返回 404
  2. 坑二:一个 502,四个问题叠在一起
  3. 坑三(插曲):预留端口差点成了救命通道
  4. 收尾:公网上的第一个注册按钮
  5. 三条带得走的

Pangolin 内网穿透:一个 502 背后叠着四个坑

我手里有三堆机器:一台腾讯云上的小 VPS(带公网 IP),家里的 Mac,和一套三节点的 k8s 集群。集群和 Mac 上跑着一堆自托管服务——博客、配置中心、项目管理工具——我想把它们放到公网上用,但一个入站端口都不想在内网侧开。

方案对比没什么悬念:frp 要自己拼配置文件和 TLS,Tailscale Funnel 域名不是自己的,Cloudflare Tunnel 流量要过第三方。最后选了 Pangolin——WireGuard 隧道 + Traefik 反代 + Web 面板一体,内网侧只出站,服务按子域名分流,每个还能带独立的登录墙。

架构长这样:

公网用户 ──HTTPS──> VPS(Pangolin + Gerbil + Traefik, 443)
                      │ WireGuard(内网侧纯出站)
        ┌─────────────┼──────────────┐
   Mac(newt)     k8s(helm newt)   [VPS 本机服务:local site]

DNS 给域名加一条 * 泛解析指到 VPS,配一张泛域名证书,之后每加一个服务都是零 DNS、零证书操作——面板里点一下,5 秒后 xxx.apikv.com 就通了。

部署过程本身没什么可写的,完整的 compose、配置模板和教程我整理在了 deploy 仓库里。这篇想写的是三次踩坑——尤其是中间那次,一个 502 背后叠着四个互相掩护的问题。

坑一:k8s 服务暴露,envoy 对一切请求返回 404

第一批暴露的是集群里的服务。套路是两步:HTTPRoute 追加一条公网 hostname,面板里建资源把 target 指向集群 Gateway 的 ClusterIP。

我把 target 写成了 ClusterIP:80。结果访问任何路径都是 404,而且这个 404 长得特别像"域名配错了",我对着 DNS 和面板检查了半天。

真相:我的 HTTPRoute 的 parentRef 带着 sectionName: https——路由只挂在 443 listener 上,80 上什么都没有,envoy 对 80 进来的一切 Host 都礼貌地返回 404。target 改成 443 走 https 立刻就通(Gateway 的自签证书无所谓,VPS 侧 Traefik 配了 insecureSkipVerify)。

从这次学到的判别技巧:看 404 的响应头server: envoy 说明请求已经进到集群网关了,问题在路由层,不在 DNS 也不在隧道。

坑二:一个 502,四个问题叠在一起

后来我在 VPS 上用 docker 装了 Kaneo(项目管理工具),面板里加好 kaneo.apikv.com,target 填了服务端口。访问:502。

这次我让 AI 助手直接上服务器排查。它做的第一件事不是看配置,而是掐表:

502 响应只用了 0.17 秒——这是 refused,不是 timeout。防火墙和隧道都是好的,是转发目标上没有活着的进程。

然后一层一层往下剥,剥出了四个问题:

1)容器在崩溃循环。 docker inspect 显示 RestartCount=25。日志里 API 连不上 postgres:5432,超时。但 postgres 容器明明是 healthy 的——docker network inspect 揭晓:postgres 根本不在 compose 的 network 里。之前改过一次 compose 后只重建了一部分容器,两个容器留在了两张不同的网络上,服务名解析不到。docker compose up -d --force-recreate 整组重建解决。

2)KANEO_CLIENT_URL=http://localhost:5173 这个值会被注入前端当 API 地址——也就是说每个访问者的浏览器会去请求他自己电脑的 5173。反代场景必须写外部域名 https://kaneo.apikv.com

3)重建时 postgres 绑不上宿主 5432——被另一个跑了两个月的老容器占着。 顺势把端口映射直接删了:kaneo 走容器网连它,宿主端口本来就是多余的暴露面。

4)修完这三个,502 依旧。 AI 没有去猜,而是掏出了一个我不知道的后门:Traefik 的路由是从 Pangolin 的 traefik-config 接口动态拉的,curl 一下就能看到实际生效的转发目标

5-kaneo-service -> ['http://localhost:5173']

实锤了。我在面板里把 target 填成了 localhost:5173——但发起转发的是 Traefik 容器,它的 localhost 是它自己,里面当然没有 5173。正确写法是宿主的内网 IP。

当时我不在面板旁边,AI 干脆备份了 Pangolin 的 sqlite,直接 UPDATE 了 targets 表里那一行。Traefik 5 秒轮询后,HTTP 200。

四个问题各自都不致命,叠在一起就是"怎么修都还是 502"。如果一开始就靠猜,大概会在面板和 DNS 之间来回绕圈;靠"每一步都拿硬证据"(响应耗时、RestartCount、network inspect、traefik-config),整条链路一次剥完。

坑三(插曲):预留端口差点成了救命通道

中间还有一段惊险的:给 VPS 改 SSH 端口时(Ubuntu 24.04 的 socket activation 有坑,另文再写),一度把 22 和新端口全弄失联了。盘点残存通道时发现——Pangolin 预留的 raw TCP 端口还活着,理论上可以在面板建一条资源代理回 127.0.0.1:22,从旁门进去自救。

最后虽然走的是云厂商 VNC,但这件事改变了我对这几个预留端口的看法:它们不只是"以后暴露数据库用的",它们是带外通道。动任何 SSH/防火墙配置之前,先确认一条不依赖它们的路能走通。

收尾:公网上的第一个注册按钮

服务通了不等于完事。Kaneo 这类自托管应用有个共同点:数据库为空时,第一个注册的用户就是管理员。而你的子域名从建好那一刻起就是公网可见的。

所以最后三步:

  1. 自己立刻注册第一个账号;
  2. 应用侧关掉注册(Kaneo 是 DISABLE_REGISTRATION=true)——改完别只看配置,从容器里真的 POST 一次注册接口,看到 403 才算数;
  3. Pangolin 的资源级 SSO 留着当外墙:没有面板 session 的请求直接 302 到认证页,扫描器连你的登录页都看不到。代价是 API/移动端客户端也会被拦,要用集成的时候再关。

三条带得走的

  1. 先判层再排查:refused(快)= 到了但没人听,timeout(慢)= 包没到。一条 nc -zv 能省半小时;
  2. 验证要用行为,不用配置文件:配置写了什么不重要,ss 在听什么、数据库里有几行、接口返回什么才重要;
  3. 给自己留带外通道:VNC、云厂商的命令下发、一个预留端口——你不会需要它,直到你非常需要它。

配置与完整教程:docker 侧在 docker-deploy/pangolin/,k8s 侧在 cloud-native-deploy/pangolin/