2026-08-19工程实践21 min read

从一条 ngrok 命令说起:内网穿透到底是怎么「穿」过去的

以 ngrok 为例,把内网穿透的完整链路拆开讲清楚——NAT 为什么进不来、反向隧道怎么建、边缘服务器怎么路由、泛域名和证书如何配合,以及 frp/cloudflared 与它的异同。

网络ngrok内网穿透DNSTLS

你在本地起了个 FastAPI,想让手机、同事或者微信支付的回调访问到它。敲一行 ngrok http 8000,几秒钟后拿到一个 https://xxxx.ngrok-free.app,全世界都能访问。这一行命令背后,网络层、传输层、应用层、DNS、TLS 全部参与了一遍。这篇文章把它们全拆开。

一、问题的根源:你的电脑「能出不能进」

先搞清楚为什么本地服务天然不能被外网访问。原因不是防火墙「设置了不让进」这么简单,而是三层叠加:

1. 你没有公网 IP。 家里路由器分配给你的是 192.168.x.x 这类私有地址,全世界几十亿设备都在用同一段地址,它在公网上没有意义。路由器本身可能拿到一个公网 IP,但更多情况下运营商在上面又套了一层 NAT(CGNAT),你的路由器 WAN 口拿到的也不是公网地址——通常是专门划给运营商级 NAT 的 100.64.0.0/10 共享地址段(RFC 6598,刻意与家庭内网的私有网段错开以免两侧冲突),也有运营商直接用 10.x 私网段。也就是说,从外面看,根本不存在一个能「指向你」的地址。

2. NAT 只维护出站映射。 NAT 的工作方式是:当你主动向外发起连接时,它记一条映射表项 (内网IP:端口) ↔ (公网IP:端口),回来的包按这张表转回给你。一个从外面主动进来的包,表里没有对应项,NAT 不知道该给谁,直接丢弃。

3. 防火墙默认「出站放行、入站拒绝」。 就算你有公网 IP,家庭路由器、公司网关、校园网、云安全组的默认策略也都是这一条。

但这三条限制有一个共同的漏洞:它们都不拦你主动发出去的连接。你可以打开任何网站、连任何服务器,一旦连接建立,NAT 表项存在,服务器往这条连接里回写的数据能一路回到你的进程。内网穿透全部的魔法,就建立在这一个不对称性上。

二、核心思想:把门反过来开

既然外面进不来,那就由里面主动伸一根管子出去。

ngrok 的架构里有两个角色:

  • agent:跑在你电脑上的那个命令行程序。
  • 边缘服务器(edge):ngrok 部署在全球各地、拥有公网 IP 和域名的服务器集群。

流程是:agent 主动向边缘建立一条持久的出站连接,然后什么都不做,只保持这条连接活着。外部请求先打到边缘,边缘不去连你(连不上),而是把请求往这条已经存在的连接里写;agent 在另一头读出来,转发到 localhost:8000,响应再原路写回。

sequenceDiagram
    participant C as 外部客户端
    participant E as ngrok 边缘服务器
    participant A as ngrok agent(你的电脑)
    participant L as localhost:8000

    Note over A,E: ① 启动阶段:agent 主动出站
    A->>E: TLS 连接到 connect.ngrok-agent.com:443
    A->>E: 注册隧道:http → localhost:8000
    E-->>A: 分配域名 a1b2c3d4.ngrok-free.app
    Note over A,E: 连接保持不断,定期心跳

    Note over C,L: ② 请求阶段:流量沿隧道倒灌
    C->>E: HTTPS GET a1b2c3d4.ngrok-free.app/api
    E->>E: 按 Host/SNI 查表 → 找到对应 agent 连接
    E->>A: 在既有连接内新开一个逻辑流,写入请求
    A->>L: 新建 TCP 连接到 127.0.0.1:8000
    L-->>A: 响应
    A-->>E: 沿同一逻辑流写回
    E-->>C: 响应

注意整个过程中没有任何一条「入站到你电脑」的连接。从 NAT 和防火墙的视角看,只有一条你自己发起的、去往 443 端口的 TLS 会话在正常收发数据。这就是「穿透」的真实含义:不是打穿了什么,而是绕开了「入站」这个概念本身。

三、逐层拆解:一条命令背后的细节

# 一次性;配置文件位置因系统而异(macOS 在 ~/Library/Application Support/ngrok/,
# Linux 在 ~/.config/ngrok/),ngrok config check 可打印实际路径
ngrok config add-authtoken 2xxxxxxxxxxxxxxxxx
ngrok http 8000

3.1 为什么走 443

agent 连接边缘用的是 TLS over TCP 443。这是有意为之:443 是 HTTPS 端口,几乎没有任何网络会拦它——公司内网、校园网、酒店 Wi-Fi 都得放行,否则用户连网页都打不开。选一个「不可能被封」的端口,是所有隧道工具的共同做法。

3.2 多路复用:一条连接里跑无数请求

agent 和边缘之间只有一条物理 TCP 连接,但外部可能同时有几十个请求打进来。ngrok 在这条 TLS 连接之上跑了一层多路复用协议(早期开源版叫 muxado,思路与 yamux、HTTP/2 的 stream 一致):把一条物理连接切成很多逻辑流(stream),每个外部请求对应一个流,互不阻塞。

这也是为什么隧道建立后你会看到「连接一直保持」却几乎没有流量——那是心跳包在维持 NAT 表项和检测断线。

3.3 隧道注册与域名分配

agent 连上后发送注册请求:我要一个 HTTP 端点,转发到 localhost:8000。边缘校验 authtoken,查你的账户额度,然后分配一个域名并把映射关系记入路由表:

a1b2c3d4.ngrok-free.app  →  账户 zephyr  →  agent 连接 #4823  →  隧道 "http:8000"

终端里那行 Forwarding https://a1b2c3d4.ngrok-free.app -> http://localhost:8000 就是这一步的结果。

3.4 边缘如何知道请求给谁:Host 头与 SNI

外部浏览器访问 https://a1b2c3d4.ngrok-free.app/api。请求到达边缘时,边缘需要在成千上万条隧道里找到对的那条。它有两个信息可用:

  • SNI(Server Name Indication):TLS 握手第一个包(ClientHello)里明文携带的目标域名。边缘在解密之前就能看到它,据此决定用哪张证书、路由到哪。
  • Host 头:HTTP 请求头里的域名字段,TLS 解密后可见。

两者结合就能唯一定位到某个用户的某条隧道。这也解释了 HTTP 隧道和 TCP 隧道在设计上的根本区别——下一节展开。

四、三种隧道模式:边缘「看得到多少」

ngrok http 8000        # 七层:边缘终止 TLS,看得到明文 HTTP
ngrok tcp 22           # 四层:边缘只转发字节流
ngrok tls 8443         # 端到端 TLS:边缘按 SNI 路由但不解密
flowchart LR
    subgraph TLS["ngrok tls(端到端)"]
        direction LR
        S1[客户端] -- TLS --> S2[边缘<br/>看 SNI 路由 · 不解密] -- 隧道 --> S3[agent] --> S4[localhost]
    end
    subgraph TCP["ngrok tcp(四层)"]
        direction LR
        T1[客户端] -- 任意协议 --> T2[边缘<br/>按端口路由 · 不解析] -- 隧道 --> T3[agent] --> T4[localhost]
    end
    subgraph HTTP["ngrok http(七层)"]
        direction LR
        H1[客户端] -- TLS --> H2[边缘<br/>解密 · 解析 HTTP · 可注入] -- 隧道 --> H3[agent] --> H4[localhost]
    end

ngrok http:TLS 在边缘终止,边缘拿到明文 HTTP,利与弊都由此而来:它能做很多七层功能——localhost:4040 的本地 Web 检查界面能回放每个请求、--oauth=google--basic-auth 能在边缘拦截未授权请求、Traffic Policy 能改写头和路径;免费版那个 interstitial 警告页也是边缘在转发前直接替你返回的——浏览器流量会先收到一整页警告,点了确认请求才真正到达你的服务。代价是 ngrok 在协议上处于能看到你流量的位置。

ngrok tcp:四层转发,边缘只看到字节流,SSH、数据库、游戏服务器什么都能过。但 TCP 没有 Host 头或 SNI 可供路由,边缘只能用端口号区分不同用户的隧道,所以每条 TCP 隧道要独占边缘上的一个端口,分配到的地址形如 0.tcp.ngrok.io:14523。这也是 TCP 地址在定价上更贵、免费版要绑卡的原因——端口是稀缺资源,域名不是。

ngrok tls:利用 SNI 明文可见的特性,边缘只读取 SNI 做路由,不解密,端到端加密到你的本地服务。你需要自己在本地服务上配证书。

五、DNS 层:为什么随机子域名可以秒级可用

ngrok 给你分配 a1b2c3d4 时,并没有去 DNS 里加一条记录。它靠的是泛域名解析

先回顾一下域名的层级——从右往左,越靠右越上级:

a1b2c3d4 . ngrok-free . app .
   ↑           ↑         ↑   ↑
 三级/子域名   二级域名   顶级域  根

谁拥有上一级,谁就能随意开下一级,不限数量、不用花钱。ngrok 买了 ngrok-free.app,就能在它下面派生几百万个子域名。而它在权威 DNS 里只写一条记录:

*.ngrok-free.app    A    <边缘节点 IP(一组,anycast 就近路由)>

* 的含义是「凡是没有单独配置过的子域名,都返回这个」。所以任何随机子域名在 DNS 层面都指向同一批边缘服务器;DNS 只负责「把人送到 ngrok 门口」,「进哪个房间」由边缘在应用层靠 SNI/Host 决定。

flowchart LR
    B[浏览器] --> R[递归解析器<br/>运营商 / 8.8.8.8]
    R -->|".app 归谁管?"| ROOT["根服务器 ."]
    R -->|"ngrok-free.app 归谁管?"| TLD[".app 顶级域服务器"]
    R -->|"a1b2c3d4 是多少?"| AUTH["ngrok-free.app 权威<br/>*.ngrok-free.app → 边缘 IP"]
    AUTH -->|"返回 IP,带 TTL"| R
    R --> B

同样的道理用在证书上:ngrok 提前申请了 *.ngrok-free.app通配符证书,一张证书覆盖所有一层子域名,随机域名一出生就有 HTTPS。注意通配符只匹配一层:*.example.com 匹配 a.example.com,不匹配 a.b.example.com

自带域名怎么接进来

付费用户可以用自己的域名,比如 demo.zephyrxiang.com。做法是在你自己的 DNS 里加一条 CNAME:

demo.zephyrxiang.com   CNAME   xxxx.ngrok-cname.com

CNAME 是「别名」——解析器读到它会接着去查目标名字,最终仍落到 ngrok 的边缘 IP。用 CNAME 而不是 A 记录的原因是:边缘 IP 由 ngrok 维护、随时可能变,你写死 IP 就得跟着改;CNAME 把这个责任交还给 ngrok。之后 ngrok 通过 Let's Encrypt 为 demo.zephyrxiang.com 单独签一张证书(用 HTTP-01 验证,边缘代你响应验证请求)。

六、TLS 层:证书在这条链路里的位置

HTTPS 解决两件事:身份加密。加密靠 Diffie-Hellman 协商密钥就能做到,但没有身份的加密是空的——中间人可以同时冒充两头。证书解决的就是身份:它是一份由 CA 签名的声明——「a1b2c3d4.ngrok-free.app 的公钥是 XXXX」。浏览器验证三件事:CA 签名有效、证书上的域名与地址栏域名匹配(不要求逐字相等——*.ngrok-free.app 这张通配符证书能覆盖 a1b2c3d4.ngrok-free.app,靠的正是通配符匹配规则)、服务器持有与证书公钥对应的私钥。三步都过,浏览器才会信任这条连接。

严格说时序上并不是「先验证、后协商」:TLS 1.3 里 DH 密钥交换在握手第一个往返就完成了,证书和签名反而是用协商出的握手密钥加密后才发来的;把身份和密钥绑定在一起的,是服务器用证书私钥对整个握手记录(含双方的 DH 公钥)所做的签名。验证不过,握手中止,协商好的密钥直接作废。

在 ngrok 的三种模式里,「服务器」这个角色分别由谁扮演:

模式 谁持有面向客户端的证书 谁能看到明文
http ngrok 边缘(通配符证书) ngrok 边缘
tls 你的本地服务 只有你
tcp 取决于你的协议 取决于你的协议

这就是为什么 ngrok http 用起来最方便(证书 ngrok 全包),而对隐私敏感的场景要考虑 tls 或自建方案。

七、常用命令,顺着原理理解

# 固定域名:注册隧道时指定域名而不是随机分配
# (免费版每个账号自动分配一个固定的 dev domain,名字随机生成、不可自选,
#   在 dashboard 的 Domains 页查看,形如 abc123xyz.ngrok-free.dev;
#   想自选前缀或绑自有域名需要付费版)
ngrok http --url=abc123xyz.ngrok-free.dev 8000

# 本地服务本身是 HTTPS:agent → localhost 那一跳也走 TLS
ngrok http https://localhost:8443

# 本地服务按 Host 头做虚拟主机、不认识 ngrok 域名:让 agent 转发前重写 Host
ngrok http --host-header=rewrite 8000

# 在边缘加一道门:未授权请求根本到不了你本机
ngrok http --basic-auth="user:pass" 8000
ngrok http --oauth=google --oauth-allow-domain=example.com 8000

# 多条隧道一起起
ngrok start --all

两个与老教程有出入的地方:--region 已随 agent v3.5.0(2023 年底)废弃——v3 agent 统一连接 connect.ngrok-agent.com,由 ngrok 自动路由到延迟最低的接入点,确要固定区域时改用配置文件里的 connect_url--basic-auth--oauth--host-header 这类便捷旗标目前仍可用,但官方文档已把它们标记为 deprecated,推荐的替代是 Traffic Policy,也就是把这些边缘能力收进同一套流量规则配置里。

配置文件的本质就是一张「名字 → 公网 URL → 本地上游地址」映射表,协议编码在 URL 的 scheme 里:

version: 3
agent:
  authtoken: 2xxxxxxxxxxxxxxxxx
endpoints:
  - name: api
    url: https://abc123xyz.ngrok-free.dev
    upstream:
      url: 8000
  - name: ssh
    url: tcp://1.tcp.ngrok.io:12345   # TCP 地址需先在 dashboard 预留
    upstream:
      url: 22

老教程里的 tunnels:proto:/addr: 是 version 2 的写法,v3 只把它作为兼容遗留暂时保留,官方已公告弃用。

一个容易误解的点:--host-header=rewrite 不是默认行为,也不是「能到 localhost 的原因」。agent 是直接向 127.0.0.1:8000 发起 TCP 连接的,TCP 层已经决定了目的地;Host 头只是 HTTP 里的一个文本字段,默认原样透传(值是 ngrok 域名)。改写它只是为了照顾那些按 Host 分流、不认识 ngrok 域名的本地服务。

调试 webhook 时打开 http://localhost:4040:那是 agent 自己起的本地 Web 界面,能看到每个流经隧道的请求,点 Replay 可以重放,不用反复去第三方后台触发。

八、「边缘」到底远不远

「边缘」这个词的意思恰恰是离你近。ngrok 在美国、欧洲、亚太、印度、澳洲、日本、南美等地都有接入点,agent 默认自动选延迟最低的。叫「边缘」是相对集中式中心机房而言——它是整张网络最外围、最贴近用户和 agent 的那一层。外部访客连到离他近的边缘,你的 agent 连到离你近的边缘,中间由 ngrok 自己的骨干网把两端接起来。

对国内用户有一个现实问题:ngrok 在中国大陆没有节点,agent 通常会接到日本或新加坡,来回要过一次国际出口,延迟一般在几十到一两百毫秒,稳定性看当天的国际链路。这也是国内很多人转向自建 frp 的原因——你决定「边缘」放在哪。

九、同类工具的异同:frp、cloudflared、Tailscale

frp 和 cloudflared 的底层原理和 ngrok 一模一样:客户端主动出站建长连接 + 服务端按域名/端口路由 + 多路复用反向推流,区别只在谁来当「边缘」、以及边缘做多少事。Tailscale 是另一个路数:它靠 STUN 探测配合 NAT 打洞,让两台设备直接建立 WireGuard 点对点加密连接——打洞利用的仍是第一节那个「NAT 只维护出站映射」的不对称性,双方各自主动向外发包造出映射,再借这两条映射互连;打洞失败(比如 UDP 被整段封掉)才退回 DERP 中继,而 DERP 只按公钥盲转端到端加密的包,不解密、也没有域名/端口路由这一层。

工具 谁提供边缘 路由方式 适合场景
ngrok ngrok 托管,全球节点 域名(http/tls)/ 端口(tcp) 快速演示、调 webhook、临时暴露
frp 自己的云服务器跑 frps 域名 / 端口,全部可配 长期稳定、国内低延迟、不想经第三方
cloudflared Cloudflare 全球网络 域名 + Cloudflare Access 想白嫖 CDN/防护、域名已托管在 Cloudflare
Tailscale 尽量点对点,DERP 中继兜底 私有网络内 IP 自己设备之间互访,不对公网暴露

如果你手上有一台云服务器,frp 的部署就是把 frps 放到服务器上、frpc 放到本地,再在你的 DNS 里加一条 *.demo.zephyrxiang.com → 服务器 IP 的泛解析——你就拥有了一个自己的迷你 ngrok,不限额度、没有 interstitial 插页、流量不经第三方,代价是证书和高可用要自己管。

十、内网穿透 vs 正式部署

最后强调一个边界:内网穿透是**「开门」,不是「发布」**。服务仍然跑在你的电脑上,电脑一关链接就死,带宽和稳定性取决于你家的网。它适合演示、调试、临时访问、把 NAS/树莓派/本地大模型暴露给自己用。真正要给用户长期使用的服务,应该部署到云服务器或 PaaS 上,让服务本身住在有公网地址的机器里,而不是靠一根管子把流量引回自家电脑。

小结

一句话概括 ngrok:一个有公网地址的反向代理 + 一条由内网主动打出去的持久隧道。代理收到请求后不主动连你(连不上),而是往这条隧道里写,agent 在另一头接住转发到 localhost。

围绕这条主线,各层各司其职:

  • NAT/防火墙层:允许出站、拒绝入站——隧道利用了前者绕开后者。
  • 传输层:一条 TLS over 443 的长连接,上面多路复用出无数逻辑流。
  • DNS 层:一条泛域名记录把所有子域名汇到边缘;CNAME 让自带域名接入。
  • TLS 层:通配符证书让随机域名秒开 HTTPS;三种模式决定谁持证、谁能看到明文。
  • 应用层:边缘靠 SNI/Host 查表路由到具体隧道,四层模式退化为按端口路由。

理解了这条链路,frp、cloudflared 之类的工具就都是同一个模型的不同实现,配置项对应到哪一层也一目了然。