🔥 星光编译者 · 个人主页
📚 学习专栏: 《C/C++ 成长笔记》 · 《Linux 实践手册》 · 《数据结构与算法》
🌄 向云端飞扬,编译属于自己的代码星河。
☕ 写在开篇
你好,这里是 星光编译者。
这里记录我在 C/C++、Linux、数据结构与算法 学习中遇到的真实问题、亲手验证过的代码,以及那些容易被忽略的实现细节。
比起简单罗列结论,我更愿意从问题出发,把一个知识点的来由讲清楚,把"为什么会这样"和"应该怎样解决"说明白,让每一次踩坑都沉淀成可以复用的经验。
如果这篇记录能帮你少绕一点路,或让某个模糊的地方忽然变得清晰,那么这次分享便有了意义。愿我们在一次次阅读、编译与调试中稳步向前,慢慢搭起属于自己的技术世界。
🔥 本文定位:学完 IP、端口和路由后,把家庭网络、公司出口、网站网关与远程访问串成一条完整的通信链路。
💡 学习目标:读懂 NAT/NAPT 的转换过程;分清正向代理、反向代理与地址转换;理解端口映射、公网中继和打洞各自解决什么问题;能够按连接路径定位访问失败的位置。
📌 阅读范围 :以 IPv4、TCP/UDP 和常见 HTTP 代理为主。文中的
192.0.2.0/24、198.51.100.0/24、203.0.113.0/24是文档示例地址,用来表示不同公网角色,不能直接用于真实部署。所有报文和输出均标明其教学或实测属性。
在家里,手机、电脑和平板明明各有一个 192.168.x.x 地址,访问网站时却可能共用同一个公网 IP。
反过来,在自己的电脑上启动一个 HTTP 服务,本机能打开,同一 Wi-Fi 下的手机也能打开,换成移动网络以后却访问失败了。
再看网站部署:浏览器访问的是一个域名,真正处理请求的却可能是后面几十台服务器;这些服务器还可以随时扩容,用户并不需要重新配置地址。
这三种现象背后,有三个经常一起出现、却不能混为一谈的概念:
- NAT:改变报文中的地址信息,让不同地址空间之间的通信能够衔接起来。
- 代理服务:由一个中间端点接收并转交通信,可能进一步理解请求、做路由、缓存或认证。
- 内网穿透:为外部访问内网服务建立一条可用路径,具体可以依靠映射、中继或直接连接建立机制。
理解它们,不能只背一句"都是中转"。真正需要回答的是:哪个字段变了,谁建立了连接,返回的数据凭什么找得到原来的主机?
文章目录
- [为什么需要 NAT:私有地址如何访问公网](#为什么需要 NAT:私有地址如何访问公网)
- [拆开 NAPT:两台主机如何共用一个公网 IP](#拆开 NAPT:两台主机如何共用一个公网 IP)
- [NAT 的状态与限制:为什么出去容易,进来困难](#NAT 的状态与限制:为什么出去容易,进来困难)
- 正向代理和反向代理:关键在于代表谁
- [NAT 与代理:修改报文和接入连接的区别](#NAT 与代理:修改报文和接入连接的区别)
- [走进反向代理:一条 HTTP 请求如何到达后端](#走进反向代理:一条 HTTP 请求如何到达后端)
- 公网中继:内网主动建立通道,外部通过入口访问
- 内网打洞:交换地址以后,为什么还要探测
- 方案怎么选:可达性、成本与访问控制
- 离线实验:亲手观察映射、回包与过期
- [Linux 排查:沿着连接路径逐段定位](#Linux 排查:沿着连接路径逐段定位)
- 易错点与自测:把概念落到具体条件
一、为什么需要 NAT:私有地址如何访问公网
1.1 地址不够用,不意味着内网设备不能有地址
IPv4 地址只有 32 位,理论空间为 2^32。扣除各种专用用途后,能够用于全球公网分配的空间更有限。
但是,一家公司可能有上千台终端,一个家庭也可能同时连接几十个设备。如果每个设备都长期独占一个公网 IPv4 地址,成本和分配压力都会很大。
常见办法是把地址的使用范围分开:内部网络使用私有地址,对外通信时由出口使用公网地址。RFC 1918 定义了三段常见私有地址:
| 私有地址块 | 地址范围 | 常见写法 |
|---|---|---|
10.0.0.0/8 |
10.0.0.0~10.255.255.255 |
10.x.x.x |
172.16.0.0/12 |
172.16.0.0~172.31.255.255 |
注意不是全部 172.x.x.x |
192.168.0.0/16 |
192.168.0.0~192.168.255.255 |
192.168.x.x |
它们可以在彼此隔离的网络中重复使用,但同一个需要直接互通的地址域内,地址仍然必须避免冲突 。两家公司各自使用 192.168.1.10 没问题,等到通过 VPN 把两个网段连接起来,重叠地址就可能让路由无法区分目的地。RFC 1918:私有地址空间

图中的两个家庭可以各自拥有 10.0.0.10,因为它们属于不同的内部网络。外部服务器看到的是各自出口的地址,不能只凭"某台机器叫 10.0.0.10"就在互联网上找到它。
1.2 私有 IP 的问题,是作用域与回程路由
假设电脑 A 的地址是 10.0.0.10,服务器 S 的地址是 198.51.100.80。如果直接把下面的报文送入公共互联网:
text
源 IP:10.0.0.10
目的 IP:198.51.100.80
服务器即使收到了,也无法通过公共互联网唯一地确定"应该回复到哪一个 10.0.0.10"。许多网络都可能在内部使用这个地址。
因此,私有地址并不是一种"低权限公网 IP"。它在特定内部网络中有效,不能作为全球统一可路由的主机标识直接使用。
出口 NAT 可以在转发时把源地址换成适合外部通信的地址,并保留转换关系。外部回复先到出口,再由出口还原为内部地址。
这里先建立一个重要认识:NAT 不替代路由。 路由负责决定数据从哪里出去、交给哪一跳;NAT 负责在需要的位置改写地址。没有正确的路由,地址即使改对了,数据仍然送不出去。
1.3 NAT、Basic NAT、NAPT 不是完全相同的范围
NAT 是 Network Address Translation,网络地址转换。日常说"家里的路由器开了 NAT",经常实际指向带端口转换的 NAPT。
| 名称 | 主要转换内容 | 如何理解 |
|---|---|---|
| Basic NAT | IP 地址 | 内部地址映射到外部地址,可能使用地址池 |
| NAPT | IP 地址与 TCP/UDP 端口 | 多个内部端点复用外部地址 |
| PAT | 常见产品或教材中的端口地址转换称呼 | 通常讨论的也是带端口转换的形式 |
Basic NAT 的地址映射不等于"任意多台主机同时共用一个地址"。当多个内部端点需要共用公网 IPv4 地址时,端口信息通常参与区分。这也是 NAPT 最容易观察到的价值。RFC 3022:Basic NAT 与 NAPT
还有一个边界:NAT 并非只能实现"私网转公网",也能用于其他地址空间转换场景。本文把家庭出口作为主例,是因为这个场景最容易解释映射与回包。
二、拆开 NAPT:两台主机如何共用一个公网 IP
2.1 先固定一组地址,避免讲到一半换场景
后面的报文推演使用以下角色:
| 角色 | 地址或端点 |
|---|---|
| 内网主机 A | 10.0.0.10:51000 |
| 内网主机 B | 10.0.0.11:51000 |
| NAT 网关的外部地址 | 203.0.113.10 |
| 外部 HTTPS 服务器 S | 198.51.100.80:443 |
A、B 都使用源端口 51000 并不冲突,因为它们的源 IP 不同。端口号从来不是在整个互联网中独立编号的身份。
假设两台主机都建立 TCP 连接,网关分别分配外部端口 40001、40002。这些端口是为了讲解而选定的数字,真实设备可能保留原端口,也可能根据冲突与分配策略改成其他端口。
2.2 发出去的时候,哪些字段变了
主机 A 发出的 TCP 报文,在 NAT 两侧分别表现为:
text
内侧:TCP 10.0.0.10:51000 → 198.51.100.80:443
外侧:TCP 203.0.113.10:40001 → 198.51.100.80:443
主机 B 则是:
text
内侧:TCP 10.0.0.11:51000 → 198.51.100.80:443
外侧:TCP 203.0.113.10:40002 → 198.51.100.80:443
这个场景主要改写的是源 IP 和源端口,目的服务器没有改变。服务器看到来自同一个公网 IP 的两个 TCP 端点,依然能够区分两条连接。

教学上可以把映射表简化为:
| 协议 | 内部端点 | 外部端点 | 远端端点 |
|---|---|---|---|
| TCP | 10.0.0.10:51000 |
203.0.113.10:40001 |
198.51.100.80:443 |
| TCP | 10.0.0.11:51000 |
203.0.113.10:40002 |
198.51.100.80:443 |
这里特意保留"协议"和"远端端点"两列,避免把真实 NAT 表误解成只有两个字符串的字典。设备还可能维护方向、超时、TCP 状态、过滤条件等信息,具体查找键和端口复用策略取决于实现。
2.3 回包的时候,网关如何找到 A
服务器回复 A 时,先使用自己看到的通信对端:
text
到达网关外侧:TCP 198.51.100.80:443 → 203.0.113.10:40001
离开网关内侧:TCP 198.51.100.80:443 → 10.0.0.10:51000
网关匹配有效映射和相关状态,恢复内部目的地址与端口,再按照内部路由转发。回复 B 时则匹配 40002,恢复成 10.0.0.11:51000。
从应用视角看,A 仍然在和服务器 S 通信。普通地址转换并没有把网关变成这个 HTTP 请求的业务处理者,A 也不需要自行在报文正文里填写"请回复给 10.0.0.10"。
但这并不表示任意到达 40001 的包都会被接收。映射解决"送给谁",过滤和状态检查解决"允许谁送"。 二者要分开理解,后面讲打洞时还会用到。
2.4 不只是把字符串替换一下
IP 地址和端口都是二进制首部字段。修改这些字段时,相关校验也要保持一致:IPv4 首部校验和受地址变化影响;TCP 校验和,以及存在校验和的 UDP 报文,其伪首部包含 IP 地址,端口又在传输层首部中。
因此,NAPT 需要相应地调整校验信息。抓包时如果遇到校验和异常,也要考虑发送端网卡校验和卸载,不应只凭主机上的一份抓包就认定 NAT 转换错误。
另外,地址可能出现在应用数据里。某些协议会在正文中携带"请连接到我的 IP 和端口",仅改 IP 首部未必足够。这也是部分协议需要显式 NAT 适配、应用网关或中继的原因。
2.5 一个公网 IP,是否最多只有 65535 条连接
这是一个经常出现、但前提不完整的说法。
端口字段确实是 16 位,但"端口取值数量"和"某设备支持的总连接数"不能直接画等号。实际容量会受下面几个因素影响:
- 可分配端口范围、保留端口和既有服务占用;
- TCP 与 UDP 的协议区分;
- 是否允许面向不同远端复用外部端口;
- 公网地址池大小;
- 状态表容量、内存、处理能力与超时策略。
在一个简化模型里,如果固定同一公网 IP、同一协议、同一远端端点,并要求每条活动连接使用不同的外部源端口,那么可用端口数确实构成约束。但把这个特定模型直接推广到所有 NAT 设备,就会得到过于绝对的结论。
三、NAT 的状态与限制:为什么出去容易,进来困难
3.1 动态映射从哪里来,又什么时候消失
常见的有状态 NAT 会在观察到首个符合条件的出站报文时分配或建立相应状态。对于 TCP,这通常发生在连接建立阶段,但不能误解为必须等三次握手全部结束以后才能创建记录。
TCP 连接正常关闭后,状态也未必在看到一个 FIN 的瞬间被删除。设备需要结合连接阶段、超时和资源回收规则处理。RFC 5382:TCP NAT 行为UDP 没有 TCP 式握手与挥手,NAT 往往通过活动刷新和空闲计时维护记录。
因此,应用隔一段时间就"突然收不到包",可能并不是对端程序崩了,而是中间状态已经过期。
保活报文的作用,是在适当条件下维持路径上的状态,并帮助应用发现故障。它不能保证所有设备都采用同一个超时,也不能让一条已失效的连接自动恢复原状。
3.2 外面直接访问公网端口,为什么不一定进得来
假设 A 在 10.0.0.10:8080 启动了一个服务。外部访问者尝试:
text
访问者 → 203.0.113.10:18080
如果网关没有既有会话状态,也没有预先配置目标映射,它无法凭空知道 18080 应该交给哪一台内网主机。包可能被丢弃、被拒绝,或者被网关自己的服务接收,具体取决于配置。

这就是"内网设备主动访问外面"和"外部主动访问内网设备"并不对称的原因。前者能创建相应状态,后者需要已有的目标对应关系和放行条件。
所以,准确的表述应该是:仅依赖常见的动态出站映射时,外部通常不能随意发起到任意内部服务的新连接。 不能简化为"NAT 后面的服务永远无法从外部访问"。
3.3 端口映射:提前告诉网关交给谁
如果你能管理具有可达公网地址的出口,可以建立如下规则:
text
TCP 203.0.113.10:18080 → 10.0.0.10:8080
外部访问 18080,网关把目的端点改为内部服务。这里涉及目的地址转换,通常称为 DNAT;家庭路由器界面里常叫"端口转发"或"虚拟服务器"。
一条能工作的规则,还需要配套条件:内部服务确实监听目标地址与端口;网关允许转发;主机防火墙允许接收;返回流量能走回负责转换的路径;运营商和上游没有挡住入口。
NAT 规则、路由与防火墙放行是不同的事情。 只填了一条映射规则,并不能证明这三部分都已经满足。
当内部机器访问自己的公网入口时,还可能依赖 NAT 回环,也叫 hairpin NAT。外网能访问、同一内网反而打不开,不一定是映射本身错了,还要检查回环支持和内外 DNS 解析策略。
3.4 双层 NAT 与运营商 NAT
实际网络可能是这样:
text
电脑 → 家庭路由器 NAT → 运营商 CGN → 公共互联网
如果家庭路由器 WAN 地址属于私有网段,或属于 100.64.0.0/10 的共享地址空间,通常说明它前面还有一层内部或运营商地址域。100.64.0.0/10 专门用于共享地址场景,不是 RFC 1918 的第四段私有地址。RFC 6598:共享地址空间
这时,在家庭路由器上配置端口映射,可能只解决了最里面一层。外部报文连运营商边界都过不来,自然到不了家庭路由器。
排查时可以比较路由器 WAN 地址与可信外部服务观察到的出口地址,但二者不同只能提示存在其他地址转换、代理出口或复杂链路,不能仅凭一个数字就断定具体拓扑。
3.5 NAT 带来的成本,不只有"地址改一下"
状态表需要内存,查找与改写消耗处理资源;端口与表项会耗尽;路径改变可能让后续报文落到没有对应状态的设备上;应用在报文中携带地址时,还可能出现协商问题。
设备故障也不能一概说成"有热备就一定不断"或"只要切换就必定全断"。关键是新设备是否具备所需的转换、连接与过滤状态,以及地址接管、流量路径和切换时延是否满足要求。单纯复制配置文件,并不等于复制了正在活动的所有连接状态。
最后,NAT 本身不等于防火墙,更不等于加密。某些默认入站限制确实降低了暴露面,但访问策略仍需要单独管理;在明文协议中,做了地址转换也不会让业务内容变成密文。
四、正向代理和反向代理:关键在于代表谁
4.1 先看共同结构,再看服务对象
从图上看,两类代理都可能画成:
text
客户端 → 代理 → 目标服务
所以,单看箭头向左还是向右,根本区分不了正向与反向。"正向"和"反向"描述的是代理在访问关系中代表哪一侧,而不是流量只能单向流动。

正向代理 通常服务于客户端一侧。客户端或其网络策略选择代理,并通过代理访问原本想访问的目标。反向代理通常服务于网站或服务提供方,作为该服务的对外入口,把收到的请求交给后端。
HTTP 规范中,代理与作为服务端入口的网关有明确的角色区分;工程上常把后一类称为反向代理。RFC 9110:HTTP 中间节点
4.2 正向代理:我想访问谁,告诉代理
设想公司统一设置一个出口代理,员工通过它访问允许的外部网站。以显式 HTTP 代理为例,客户端知道自己在使用代理,并向它表达目标地址。
对于明文 HTTP,请求行可以使用绝对形式:
http
GET http://example.com/docs HTTP/1.1
Host: example.com
代理收到请求后,可能做身份验证、目标检查、缓存判断,然后向目标服务器发出相应请求。目标服务器在网络层看到的连接来源,通常是代理或代理所在出口的地址。
但"经过代理"不等于"完全匿名"。代理可能记录访问,应用可能通过账号、Cookie 等识别用户,某些代理还会显式传递来源信息。网络层地址变化,只解决了身份线索中的一部分。
4.3 HTTPS 正向代理:CONNECT 之后是谁在加密
访问 HTTPS 时,常见显式 HTTP 代理使用 CONNECT 建立隧道:
http
CONNECT example.com:443 HTTP/1.1
Host: example.com:443
代理建立到目标的 TCP 通路,并成功响应后,客户端再通过该通路与目标站点进行 TLS 通信。
在普通 CONNECT 隧道模式下,代理可以知道客户端请求的目标主机与端口,但不会因为转发了 TLS 字节,就自动获得 HTTPS 正文的明文。除非另行实施 TLS 检查并配置相应信任,否则不能把"代理可缓存 HTTP"直接推广为"代理可以随意缓存所有 HTTPS 内容"。
这也是为什么学习代理时,要区分"理解并处理 HTTP 消息"和"转发一段加密字节流"。二者都能帮助访问,但可见信息和处理能力不一样。
4.4 反向代理:用户访问一个服务入口
假设用户访问 https://shop.example.com。域名解析到服务入口,入口再根据域名、路径或负载策略,把请求交给不同后端。
对于用户来说,反向代理就是这个网站的服务端入口;用户通常不需要为这次访问单独配置一个代理地址,也不需要知道内部到底有多少实例。
后端可以放在同一台机器的不同端口、同一内网的其他主机,或者其他可达网络中。反向代理必须能够到达后端,但后端并不因"私有"这个词就必须与代理处在同一个物理机房。
它能做的事情包括:基于路径分流、负载均衡、TLS 终止、请求限流、缓存、统一认证接入,以及隐藏无需公开的后端拓扑。哪些能力实际启用,取决于软件和配置。
4.5 缓存不是区分正向、反向代理的标准
一个很容易讲错的例子是:"帮别人代购时是正向代理,自己囤货以后就变成反向代理。"
这个类比把"有没有库存"与"代表哪一侧"混在了一起。囤货更接近缓存策略;仅仅缓存了内容,并不会自动改变代理角色。
换一个更清楚的例子:你委托采购员去不同商店买东西,采购员代表你,接近正向代理。商店安排统一前台接单,再由不同仓库履约,前台代表商店,接近反向代理。采购员和商店前台都可能缓存信息或提前备货,但这不是两者的定义。
同理,负载均衡也不能用来单独定义代理类别。网站入口将请求分发给后端,是反向代理的典型用途;"任何做负载分配的东西都叫反向代理"仍然太宽泛。
五、NAT 与代理:修改报文和接入连接的区别
5.1 同样经过中间节点,连接边界却不同
在典型的 TCP NAPT 场景中,网关对经过的报文进行地址和端口转换,并跟踪状态。客户端与目标服务器仍然承担这条 TCP 通信的端点职责,网关不因为做 NAT 就获得一个"替服务器处理 HTTP 请求"的应用进程。
而典型 TCP/HTTP 代理会接入客户端一侧的连接,并使用另一侧连接与上游通信。两侧 TCP 连接各自拥有序列号空间、拥塞控制和生命周期。代理需要在这些连接之间转发数据,可能还会复用上游连接。

图中画两条连接,是为了说明连接边界,不是说每个 HTTP 请求永远恰好新建两条 TCP 连接。真实系统可能使用持久连接、连接池或多路复用。
5.2 从 Linux Socket 的角度理解
客户端程序正常连接服务器时,通常只执行类似下面的操作:
cpp
// 省略地址初始化和错误处理,仅展示 API 角色。
connect(fd, server_addr, server_addr_len);
是否经过 NAT,往往由网络路径决定,应用不需要另起一个"与 NAT 路由器交换业务数据"的 Socket。
典型代理程序的逻辑则多了一层:接受客户端连接,确定上游,再建立或取得上游连接,在两侧之间转发。完整实现还要处理部分读写、背压、半关闭、超时和异常退出。
text
客户端连接 → accept 得到下游 Socket
确定目标 → connect 或复用上游 Socket
传递数据 → 在两侧处理读写与缓冲
关闭连接 → 分别处理两侧状态
这段流程只是概念骨架,不能把一个阻塞的"读一次、写一次"循环当成可用于生产环境的代理。
5.3 用一张表固定区别
| 比较点 | 常见 NAT/NAPT | 典型 TCP/HTTP 代理 |
|---|---|---|
| 处理对象 | 经过设备的网络报文及转换状态 | 一侧接入的连接、字节流或应用消息 |
| 主要变化 | IP 地址;NAPT 还处理端口 | 接入并转交通信,可能处理内容与协议 |
| 是否天然理解 URL | 通常不理解 | HTTP 代理可以理解 |
| TCP 连接关系 | 通常不因转换而拆成两个代理端连接 | 常见实现存在下游、上游两个连接边界 |
| 客户端如何使用 | 通常由路由路径隐式经过 | 可能显式配置,也可能通过服务入口使用 |
| 常见目的 | 地址复用、地址域衔接 | 访问控制、服务入口、应用路由等 |
这里用"常见"和"典型",是因为现实设备可能把 NAT、包过滤、透明代理、负载均衡组合在一起。不能只看设备名称就判断内部机制。
5.4 不要用"硬件"和"软件"划分它们
NAT 可以由 Linux 内核和软件规则实现,并非只能存在于家用路由器芯片中;代理也可以运行在专用网关设备上。部署载体不能替代功能定义。
同样,把 NAT 说成"只处理第三层"也不够完整:NAPT 明确涉及 TCP/UDP 端口,实际实现还需要维护协议相关状态。另一方面,代理不全是理解 HTTP 的七层代理,也存在只转发 TCP 字节流或 UDP 数据的实现。
更可靠的判断方式是看实际行为:是修改经过的报文字段,还是在中间接入通信,再以另一个端点身份向上游发送?
六、走进反向代理:一条 HTTP 请求如何到达后端
6.1 一个最小的入口分流场景
假设对外提供一个统一 HTTP 入口:
text
访问 /api/ 开头的请求 → 应用服务 127.0.0.1:8080
其他路径 → Web 服务 127.0.0.1:8081

下面是一份独立实验环境的 Nginx 配置示例 ,使用 127.0.0.1:18080 作为入口。它用于理解请求路由,不应直接覆盖现有服务器配置。使用前需要准备两个后端服务,并在实验环境中检查配置与响应。
nginx
events {}
http {
server {
listen 127.0.0.1:18080;
server_name localhost;
location /api/ {
proxy_pass http://127.0.0.1:8080/;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
proxy_pass http://127.0.0.1:8081;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
}
在已经安装 Nginx 的隔离实验环境中,可以先检查该文件语法:
bash
nginx -t -c /absolute/path/nginx-demo.conf
配置语法检查通过,只能说明配置可被解析。真正验证响应前,还需要相应后端正在监听,并按自己的实验环境启动该配置。
6.2 proxy_pass 后面有没有斜杠,会影响路径
在上述普通前缀 location /api/ 中:
nginx
proxy_pass http://127.0.0.1:8080/;
包含 URI 部分 /,会将匹配的 /api/ 前缀替换为 /。因此:
text
客户端请求:/api/users?id=7
后端收到: /users?id=7
如果改为不带 URI 的形式:
nginx
proxy_pass http://127.0.0.1:8080;
在这里没有额外 rewrite 的条件下,原请求路径会保留为 /api/users?id=7。这一小处差异,足以导致"代理通了,后端却一直返回 404"。正则 location、变量和 rewrite 会引入额外规则,不能仅凭"有斜杠就删前缀"概括所有配置。Nginx:proxy_pass 与 URI 替换
6.3 后端看到的 IP,不一定是访问者的 IP
代理重新建立上游连接后,后端在 Socket 层看到的对端,通常是代理。因此,应用想记录原始访问者地址,可能需要由可信代理传递信息,例如 X-Forwarded-For、Forwarded 或约定的其他字段。
但这些字段不是天然可信的身份证明。外部客户端也能自行发送一个同名请求头。后端必须知道哪些代理是可信入口,以及从哪一端、按什么规则解析代理链,不能把任意请求头当成真实 IP。
示例中的 X-Real-IP 由入口使用自己的 $remote_addr 覆盖设置,便于解释单层入口情形。如果入口前面还有 CDN 或负载均衡器,它看到的直接对端又变了,需要重新梳理信任边界。
6.4 TLS 终止、缓存、CDN 都需要分别看
反向代理可以终止客户端 TLS,再用 HTTP 或 HTTPS 访问后端。前端使用 HTTPS,只说明客户端到入口的这一段受 TLS 保护,不能自动推出入口到后端也加密了。
缓存同样需要规则。适合公开复用的静态资源与带用户身份的私有响应,不能不加区分地共享。启用一个代理服务,并不意味着所有请求都能安全缓存。
CDN 的边缘节点经常承担反向代理和缓存功能,但完整 CDN 还涉及节点分布、调度、回源、刷新等机制。理解反向代理有助于理解 CDN 的访问路径,却不能把整套 CDN 简化成"一台缓存服务器"。
对初学者来说,这一节最重要的顺序是:先确认入口能到后端,再看路径与请求头是否符合预期,最后才谈缓存、限流和性能优化。基础转发没通时,增加更多功能通常只会让问题更难定位。
七、公网中继:内网主动建立通道,外部通过入口访问
7.1 不能控制出口,能不能换一个连接发起方向
如果没有可配置的公网入口,直接等待外部来连接内网服务通常行不通。但内网主机往往能够主动访问一个公网服务。
于是,可以让内网侧程序先向公网中继建立通道。外部访问者连接公网入口后,中继再通过已经建立或按需建立的通道,把数据交给内网侧程序,最终交给本地服务。

图中最容易误解的地方,是"建立连接的方向"和"业务数据的方向"不完全相同。虽然通道由内网一侧发起,但建立后的 TCP 连接本来就可以双向传输数据。外部请求因此可以沿通道被转交进来。
它没有神奇地取消 NAT,而是使用了原本允许的出站路径。
7.2 用 frp 的角色理解这个过程
frp 的基本角色可以这样记:
frps:通常部署在可达的公网主机上,提供入口及转发能力。frpc:通常部署在内网服务一侧,连接frps并配置需要代理的服务。- 本地服务:真正处理业务,例如内网 HTTP 服务。
- 外部访问者:连接公网入口,间接访问本地服务。
这是角色说明,具体代理类型、认证与参数应以所使用版本的官方文档为准。frp:组件与代理概念
以普通 TCP 中继为例,可以把业务路径画成:
text
访问者 → 公网监听端口 → 中继通道 → 内网客户端 → 本地服务
控制连接和承载业务的工作连接可能分离,也可能使用复用机制,不能简单认定"每个工具永远只用一条长连接运送所有数据"。本文用一个逻辑通道表达其作用,不规定具体实现的连接数量。
7.3 三个端口经常被混为一个
下面是一组自定义教学配置角色,并非软件默认值:
| 端口 | 谁在使用 | 作用 |
|---|---|---|
公网服务端 7000 |
内网客户端连接服务端 | 连接与控制入口 |
公网业务端口 18080 |
外部访问者连接 | 暴露给访问者的业务入口 |
内网服务 8080 |
内网客户端连接本地应用 | 真正提供业务响应 |
客户端能够登录服务端,不代表 18080 已经成功监听;18080 可以连接,也不代表内网 8080 有服务。
如果公网端口被占用、规则未加载、客户端离线或本地目标地址写错,就会在不同环节失败。因此,不要只盯着"登录成功"四个字判断整条链路健康。
7.4 127.0.0.1 永远属于当前网络命名空间
这是部署中很常见的一类问题:本地应用运行在宿主机,穿透客户端却运行在容器里,配置仍填写 127.0.0.1:8080。
对于容器内程序,127.0.0.1 指向容器自己的回环接口,未必能到达宿主机服务。如果两个组件不共享网络命名空间,应该使用它们之间真实可达的地址与端口。
同样,在公网服务器上访问 127.0.0.1:8080,也不会自动访问到家里的电脑。阅读配置时,每看到一个回环地址,都要补问一句:这行配置是由哪个进程、在哪个网络环境里解释的?
7.5 中继的代价与访问控制
普通公网中继的业务流量会经过中继节点。因此,公网带宽、节点位置和服务负载都会影响体验;中继断开,也会影响访问。
并且,客户端连接中继时使用的认证,不一定等于访问者访问业务时的认证。允许某个客户端注册隧道,与允许某个人读取内网服务,是两层不同的问题。
发布服务前要明确:谁可以访问入口,入口是否加密,后端是否仍要求身份验证,日志中能否追踪失败原因。内网穿透把服务变得可达,并不会自动替应用补上登录与授权。
八、内网打洞:交换地址以后,为什么还要探测
8.1 中继搬运数据,打洞尝试建立直接路径
假设 A、B 都位于各自的 NAT 后面。普通中继可以让双方都连接公网节点,再通过节点交换业务数据。
打洞希望进一步减少中继负担:利用双方主动发出的探测报文,在各自出口形成可用的映射与过滤状态,从而尝试让 A、B 直接通信。
这里的"洞"是对允许路径和状态的形象描述,不是修改某个防火墙软件的代码漏洞。

为了把核心讲清楚,下面使用 UDP。TCP 打洞也存在,但还会受到连接建立方式、操作系统行为与 NAT 实现等因素影响,不宜直接套用 UDP 的步骤。
8.2 第一步:先知道自己在外面看起来是谁
A 用某个 UDP Socket 向公网协助节点发包。该节点能观察到一个外部端点,例如 203.0.113.10:40001,并把观察结果告诉 A。
B 同样可能得到 198.51.100.20:50001。双方再借助约定的信令机制交换候选地址。
STUN 可以帮助应用获得服务器所观察到的映射地址,也参与连接性检查等流程,但它不是一个"调用一次就保证打洞成功"的完整方案。RFC 8489:STUN
另外,向 STUN 服务器查询的 Socket 与随后发送业务或探测的 Socket,要符合具体方案对端点一致性的要求。如果刚查完就关闭 Socket,再换一个本地端口发包,前面的映射结果可能已经不适用了。
8.3 第二步:双方朝对方候选端点发送探测
拿到候选地址以后,A 向 B 的候选端点发包,B 也向 A 的候选端点发包。
这个过程有两层作用:让各自 NAT 看到向对方发送的出站流量;验证"对方候选地址"是否真的能形成一条双向通路。
第一批探测可能丢失,因为对侧还没建立相应过滤状态。实际方案通常需要协调时机、重试、认证和超时处理,不能把"一边发了一个包"当成完整连接建立。
探测成功以后,业务数据才有条件走直接路径。协助节点可能继续承担信令,但不一定继续转发每一份业务报文。
8.4 映射行为与过滤行为,是两个独立问题
理解打洞失败,先分清下面两件事:
| 行为 | 它在回答什么问题 | 直观例子 |
|---|---|---|
| 映射行为 | 同一个内部端点访问不同远端时,外部端点是否改变 | 发给协助节点用 40001,发给对端会不会变成 40009 |
| 过滤行为 | 对外部端点发来的报文,哪些来源可以被转发进来 | 是否必须先给这个远端 IP 或 IP:端口发过包 |
RFC 4787 将 UDP NAT 行为拆开描述,避免仅靠"圆锥型""对称型"等旧标签推断所有特征。RFC 4787:映射与过滤行为
最典型的问题是:A 向协助节点发送时获得 40001,但向 B 发送时 NAT 改为使用另一个外部端口。B 仍朝旧候选端口发送,就可能走错目标。
即使映射保持稳定,过滤规则、UDP 出站限制、多级 NAT、映射过期和网络切换,也可能让直接路径失败。交换了公网 IP 和端口,只是拿到了候选信息,不是拿到了可达性证明。
8.5 STUN、TURN、ICE 分别做什么
这三个缩写经常一起出现,但不能互相替换:
| 名称 | 主要作用 | 不应误解为 |
|---|---|---|
| STUN | 提供映射发现、连接性检查等基础机制 | 自动完成全部穿透与业务转发 |
| TURN | 提供可用的中继地址与数据转发路径 | 仅查询公网 IP 的服务 |
| ICE | 组织候选收集、连接性检查与候选对选择 | 某一种固定 NAT 类型 |
ICE 可以收集不同来源的候选地址,对候选对进行检查,并选择实际可用的通信路径。TURN 则在需要时提供中继候选,使无法直接连通的端点仍有机会通信。RFC 8445:ICE、RFC 8656:TURN
这也是很多实时通信系统"能直连就直连,不行就中继"的技术基础。实际策略还受隐私、企业网络与业务要求影响,不是永远把直连作为唯一目标。
8.6 打洞成功以后,还要维持与保护通信
网络路径会变化。移动设备切换 Wi-Fi 与蜂窝网络,或者 NAT 回收空闲记录,都可能让原来有效的候选失效。应用需要保活、失败检测,必要时重新检查或重建路径。
同时,UDP 本身不保证可靠送达,也不自动认证对端。打洞解决的是可达性,业务仍需按需要处理身份验证、加密、重放防护、丢包和拥塞控制。
一个更完整的理解是:发现候选只是开始,确认双向路径是第二步,维持一条安全且符合业务语义的通信关系,还需要上层协议继续工作。
九、方案怎么选:可达性、成本与访问控制
9.1 先看自己能控制哪个边界

| 方案 | 主要前提 | 业务流量是否经过公网中继 | 典型取舍 |
|---|---|---|---|
| 端口映射 | 有可达入口,并能控制相关网关与放行规则 | 通常不需要 | 路径直接,但受公网地址和上游限制 |
| 公网中继 | 内网能连接中继,中继入口可被访问 | 普通中继模式需要 | 容易组织统一入口,但增加带宽与节点依赖 |
| 打洞直连 | 双方运行协作程序,网络行为允许形成通路 | 直连成功后通常不需要承载业务 | 可能降低绕行,但成功率与路径稳定性不保证 |
如果需求是"把一个内网 Web 服务给外部浏览器访问",有公网入口时可以评估端口映射和反向代理;没有可控制入口时,中继通常更容易构建可理解的访问路径。
如果需求是"两个都装有客户端的终端长期互通",打洞配合中继回退更值得考虑。不要因为某个产品名称里有"穿透",就假设所有连接都已经变成点对点直连。
9.2 域名、DDNS 与穿透是不同问题
域名解决"用什么名字找到地址",DDNS 解决"地址变化时怎样更新名字对应的记录"。它们本身不会替网关新增转发规则,也不会让运营商 NAT 自动放行入站连接。
因此,给自己的宽带绑定一个域名以后,如果入口仍不可达,访问照样失败。排查时可以先用正确地址测试链路,再验证域名解析是否符合预期,避免把命名问题与连通问题搅在一起。
9.3 有全球 IPv6 地址,也需要访问策略
如果终端与访问者都具备可用的 IPv6 路径,某些场景可以减少对 IPv4 NAT 穿透的依赖。但这仍然要求地址有效、路由可达、服务监听正确,以及防火墙允许必要流量。
IPv6 地址"足够多"并不代表服务应该对所有人开放,也不保证只具备 IPv4 的访问者可以直接连接它。网络层可达与业务层授权,始终要分别处理。
9.4 一个实际的方案判断顺序
先问服务给谁用:公开网站、少数同事,还是自己远程管理。然后看双方网络能力、是否能安装客户端、能否控制入口,以及是否需要固定域名和标准浏览器访问。
确定这些条件后,再比较性能与成本。对一个低频调试页面来说,稳定易排查的中继可能已经足够;对高带宽实时通信来说,直连成功率与中继回退成本则更重要。
不要先选一个听起来先进的机制,再把业务硬塞进去。网络方案的好坏,最终要用访问者能否稳定、安全地完成任务来判断。
十、离线实验:亲手观察映射、回包与过期
10.1 为什么先做一个小模型
真实 NAT 实验需要多个网络端点、路由与抓包条件。为了先把映射关系看清楚,文章包提供 code/nat_mapping_lab.py,使用 Python 标准库在内存中模拟几种关键行为。
这个实验不发送网络报文,不配置网卡,不修改防火墙,也不声称复现了完整 Linux conntrack。 它将映射与过滤拆成两个可选择的教学规则,用可重复的输出帮助检查理解。

脚本用一个可手动推进的逻辑时钟模拟超时,不会真的等待几十秒。为方便观察,每个新映射都分配不同的外部端口;这种策略只是模型选择,不代表所有真实设备的实现。
10.2 模型中最重要的三个操作
python
public = nat.outbound("UDP", inside, remote, now=0)
target = nat.inbound("UDP", public, remote, now=1)
nat.expire(now=100)
第一行模拟内部向外发送,创建或刷新映射。第二行模拟外部数据到达,查找映射并检查来源。第三行清理超时状态。
模型中的 inside、remote、public 都是 (IP, port) 形式的元组。它不会根据字符串里有没有 192.168 来决定报文方向,方向由调用的接口明确表达。
两种映射策略的教学键分别是:
text
端点无关映射:协议 + 内部 IP:端口
地址和端口相关映射:协议 + 内部 IP:端口 + 远端 IP:端口
过滤策略另外设置,可以选择允许任何来源使用有效映射,或只允许内部之前联系过的精确远端端点。这样的拆分能直接展示:相同映射策略,可以配上不同的过滤策略。
10.3 运行与检查项
在文章目录中运行:
bash
python code/nat_mapping_lab.py
# Linux 环境也可以使用 python3
脚本会依次检查:
- 两台内部主机使用相同源端口时,获得不同外部端点。
- 返回数据分别恢复到正确的内部主机。
- 已存在映射,但陌生来源不符合过滤策略时仍被拒绝。
- 推进逻辑时间后,过期映射不再接受回包。
- 同一内部端点访问不同远端时,两种映射策略产生不同结果。
- 在相同的端点无关映射下,切换过滤策略可以改变陌生来源是否被接受。
- TCP 与 UDP 在模型中分开识别。
这是模型运行时的输出形式:
text
[PASS] distinct clients and reverse delivery
[PASS] filtering is separate from mapping
[PASS] expired mappings reject replies
[PASS] endpoint-independent mapping reuses endpoint
[PASS] endpoint-dependent mapping changes endpoint
[PASS] endpoint-independent filtering accepts new peer
[PASS] TCP and UDP are separate
ALL 7 CHECKS PASSED
文章包中的检查记录单独列出实际运行结果。这里的输出证明的是离线模型中的断言通过,不能据此判断某个家庭路由器属于哪一种 NAT 行为。
10.4 从输出回到打洞问题
把"远端 1"理解成协助节点,把"远端 2"理解成通信对端。如果更换远端后外部端点变了,协助节点观察到的地址就不能未经验证直接当作面对所有节点都有效的入口。
再把过滤策略改一下:即使外部端点不变,陌生来源仍可能被拒绝。这说明仅知道映射地址,并不能替代实际的双向连接性检查。
模型没有实现抓包、TCP 状态机、ICMP 处理、分片重组、端口冲突回收和真实打洞流程。读完代码后,应该带走的是判断思路,而不是把这个字典当成生产 NAT 实现。
十一、Linux 排查:沿着连接路径逐段定位
11.1 第一步,先确认服务真的在监听
在提供服务的 Linux 主机上观察:
bash
ss -lntp
ss -lnup
ip -br addr
ip route
ss -lntp 关注 TCP 监听,ss -lnup 关注 UDP 绑定情况,ip 命令帮助确认接口和路由。查看其他用户进程的信息时可能需要相应权限。
如果服务只监听 127.0.0.1:8080,同机代理通常可以访问它,同一局域网中的其他机器却不能直接连接这个回环地址。如果希望其他主机访问,需要按实验需求监听正确接口,同时保留必要访问控制。
注意 0.0.0.0 是常见的监听通配地址,不是发给朋友用于远程访问的具体目的地址。
11.2 第二步,在服务所在环境访问后端
如果测试的是 HTTP 服务,可以先在正确的网络环境中访问:
bash
curl --noproxy '*' -v http://127.0.0.1:8080/
--noproxy '*' 用于避免这次本地检查意外经过环境变量配置的 HTTP 代理。若后端不在回环接口,应替换为真实可达地址。容器场景要明确这条命令在哪个容器或宿主机上执行。
本地 HTTP 都不通时,优先检查服务启动、监听端口与协议。对 HTTPS 服务使用 http://,或者对普通 TCP 服务按 HTTP 访问,都可能产生误导性的错误。
11.3 第三步,从入口所在节点访问后端
反向代理能否访问应用,要在代理所在环境中验证;中继客户端能否访问本地目标,要在中继客户端所在环境中验证。
"我在自己的终端能打开"只证明了你的终端这一条路径,不证明代理进程拥有同样的 DNS、路由、容器网络和权限。
如果后端返回 404,先看路径是否被错误保留或替换;如果入口返回 502,检查上游连接、协议与有效响应;如果返回 504,关注等待上游时发生的超时。状态码只是排查方向,还要结合入口与后端日志判断。
11.4 第四步,确认公网入口与真实外部路径
端口映射场景,检查路由器 WAN 地址、上游层级、映射协议和目标端口。TCP 映射不会自动覆盖同号 UDP 端口。
中继场景,分别确认客户端注册状态、公网业务监听、本地目标可达性和访问者权限。不要只检查控制端口。
验证入站时,使用一条真正的外部网络,例如手机移动数据。仍在家庭 Wi-Fi 中访问公网地址,可能混入 NAT 回环因素,结果无法直接代表外网访问能力。
11.5 第五步,在合适位置抓包
在自己管理的 Linux 环境里,可以针对目标端口观察,例如:
bash
sudo tcpdump -ni any 'tcp port 8080 or tcp port 18080'
这只是一个过滤表达式示例,接口、端口和权限应按实际拓扑调整。抓包位置不同,看到的地址也不同:NAT 内外侧的端点本来就可能不一致;代理的下游与上游也可能是两条不同连接。
如果要判断 NAT 改了什么,应对比转换前后的对应报文;如果要判断代理哪一段失败,应分别看客户端到入口、入口到后端两侧。仅在一个普通终端上抓包,不能看到整条网络路径上的所有行为。
11.6 用症状缩小范围,不要用猜测替代证据
| 现象 | 优先检查 |
|---|---|
| 本机都访问不了 | 服务进程、监听地址、端口、协议 |
| 本机能访问,局域网不能 | 回环监听、主机防火墙、接口与网络隔离 |
| 局域网能访问,公网不能 | 入口地址、上游 NAT、映射与放行、运营商限制 |
| 隧道登录成功,业务不通 | 业务端口是否监听、本地目标是否可达、代理规则 |
| 代理返回 404 | URI、Host、应用路由;必要时查后端访问日志 |
| 闲置一会儿就断 | NAT/防火墙空闲状态、代理超时、应用保活与重连 |
| 打洞失败但中继可用 | 候选地址、映射/过滤行为、UDP 限制与连接性检查 |
最有价值的排查结论通常不是"网络有问题",而是"连接已经到达公网入口,但入口无法连接配置的后端"。定位到某一段,修复才有明确对象。
十二、易错点与自测:把概念落到具体条件
12.1 把常见错误换成带前提的说法
| 容易写错的结论 | 更准确的理解 |
|---|---|
| 私有 IP 可以随便重复 | 隔离地址域可以复用,需要互通的范围内仍要避免冲突 |
| NAT 只替换 IP | Basic NAT 侧重地址,NAPT 还会处理端口与相关状态 |
| TCP 断开时映射立即删除 | 删除受协议状态、超时与实现策略影响 |
| NAT 后面绝对不能开服务 | 可通过明确映射、可用通道等方式建立入站路径 |
| 有 NAT 就安全且加密 | NAT 不等于访问控制,也不提供业务加密 |
| 缓存了就是反向代理 | 缓存是能力,正反向描述代理代表哪一侧 |
| 代理都能看到 HTTPS 正文 | 普通 CONNECT 隧道不会自动解密业务 TLS |
| 反向代理能直达任意内网 | 代理自身仍需要到后端的可达网络路径 |
| 域名或 DDNS 能解决入站不通 | 解析与更新地址不会自动建立映射和放行 |
| 知道公网 IP 和端口就能打洞 | 地址只是候选,必须验证实际双向可达性 |
| STUN 就是转发全部业务的中继 | STUN、TURN、ICE 分工不同 |
| 设备热备就等于连接热备 | 还要考虑状态同步、接管与流量路径 |
12.2 六个问题,检查自己是否真的理解
问题一:A、B 都从源端口 51000 访问同一台服务器,为什么不会必然冲突?
因为内部 IP 不同,原始端点可以区分;NAPT 可为它们分配不同外部端口,让服务器看到两个不同的通信端点。
问题二:映射表里已经有 40001,陌生主机发往这个端口的包一定会进来吗?
不一定。还要看映射是否有效、协议是否匹配、过滤行为与防火墙是否允许,以及其他相关状态检查。
问题三:公司出口代理缓存了网页,它变成反向代理了吗?
没有。缓存不改变代理代表客户端访问外部目标的角色。
问题四:内网穿透客户端主动连接公网,为什么外部请求还能进来?
因为已建立的通道可以双向传递数据。公网入口接收访问请求后,通过通道交给内网侧,再由内网侧连接本地服务。
问题五:STUN 返回的外部端点为什么还要实测?
因为它描述的是服务器在特定通信条件下观察到的映射。访问另一远端时映射可能改变,过滤策略也可能不允许对方直接进入。
问题六:公网中继客户端显示在线,但网页打不开,第一步该看什么?
先把控制通道与业务路径分开,确认本地服务可达、代理规则生效、公网业务端口监听,再沿访问者到入口的路径逐段定位。
12.3 把这一篇接回网络协议栈
学到这里,可以把一次访问重新放回完整路径:应用决定请求什么,传输层提供端口与通信语义,网络层选择路由,链路层把报文交给下一跳。NAT 可能在路径上改写端点,代理可能引入新的连接边界,穿透方案则为原本难以到达的服务建立可用路径。
以后再遇到"本地能打开,外面打不开",先画出主机、网关、代理和中继的位置,标出谁主动连接谁,再把每一段的地址、端口与协议写下来。很多看似玄学的网络故障,到这一步就会变成能够逐段验证的具体问题。