LabHub
学习 学习路径 课程

地址、子网与网关

从里面用外面的地址叫自己

在 LabHub 中继续学习

一句话总结

在家中访问自己的公网域名时,数据包到达路由器后必须折返回来,但很多路由器无法完成这个过程。这称为 Hairpin NAT,也叫 NAT Loopback。

流程图: 折返回来 · 自己 · 来自 1.2.3.4 的响应 · 源地址

为什么需要这些知识

把服务器放在家里,并通过 labhub.hopto.org 对外提供服务时,经常会出现外部访问正常、只有家中笔记本无法访问的情况。原因如下。

  1. 笔记本(192.168.0.10)查询 labhub.hopto.org,得到公网 IP(1.2.3.4)
  2. 笔记本向 1.2.3.4 发送数据包;目标不在自身子网内,因此数据包前往路由器
  3. 路由器识别出 1.2.3.4 就是自己,按照端口转发规则把目标改成 192.168.0.20(服务器)
  4. 服务器响应,但响应源地址是 192.168.0.20,而笔记本等待的是来自 1.2.3.4 的响应
  5. 笔记本认为该响应属于“自己从未发起的连接”,因此将其丢弃

笔记本向公网地址 1.2.3.4 发送数据包,路由器只把目标改成服务器 192.168.0.20 后转发;服务器认为笔记本在同一子网,于是以 192.168.0.20 为源地址直接响应。等待 1.2.3.4 响应的笔记本认为这不是自己建立的连接并将其丢弃

数据包在路由器处像发卡一样掉头,因此得名 Hairpin。要解决问题,路由器不仅要修改目标地址,还必须把源地址改成自己的地址,即同时执行 SNAT。这样服务器会响应路由器,再由路由器把响应送回笔记本。

它是如何运作的

确认症状的方法很简单。

# 밖에서는 되는데 안에서만 안 되는가?
curl -sI https://labhub.hopto.org        # 집 안에서 → 타임아웃
curl -sI http://192.168.0.20             # 사설 주소로 직접 → 됨

如果使用内部地址可以访问,使用域名却不行,就是 Hairpin 问题。

如何解决

如果路由器支持 Hairpin,启用即可。不同厂商使用的设置名称可能不同,如 NAT Loopback、Hairpin NAT 或允许内部访问。

如果不支持,标准解决方案是分离视图 DNS(Split-horizon DNS):在内部查询同一域名时返回私有地址,在外部查询时返回公网地址。

这种方式还有一个优势。Hairpin 会让流量往返路由器,即使是家中内部通信,也会受限于路由器性能。 分离视图 DNS 则会让通信在交换机内结束。

常见误解

用 hosts 文件临时解决。 这样只能让一台笔记本正常,手机和平板仍无法访问。正确做法是在 DNS 层解决。

认为公网 IP 变化后就结束了。 使用 DDNS 时,域名会跟随公网 IP 变化,但写入内部 DNS 的私有地址,在服务器迁移后仍需手动修改。要记住,这意味着必须管理两个位置。

内外看到的内容为何不同

Hairpin 只是“同一个名称会因位置不同而走不同路径”这一更大问题的一个例子。了解由同一根源产生的其他症状,遇到陌生现象时,也能推测应该查看哪里。

证书名称不匹配。 在内部直接使用私有地址访问时,证书中的域名与实际访问地址不同,因此会出现警告。所以,分离视图 DNS 的关键在于使用相同名称,只改变地址。一旦要求用户直接使用地址访问,这个问题就会随之出现。

访问日志中的源地址看起来全部相同。 路由器或代理把源地址改成自己的地址后,服务器日志会把所有请求记录为来自同一地址。这样就无法定位具体用户,按源地址实施的阻断或速率限制也全部失去意义。即使代理通过 header 传递原始地址,只有当该 header 是由我们控制的代理添加时,才能信任。 如果直接相信外部传入的值,任何人都可以伪造自己的地址。

同一服务只有在内部访问时很慢。 流量先发往路由器或网关,再折返回来,物理上明明相邻的设备却要经过更长路径,延迟自然增加。前面所说分离视图 DNS 会让通信在交换机内结束,正是针对这一点。

归纳起来,原则只有一个:无论从哪里访问,都使用相同名称;仅让该名称指向的地址随位置变化。 如果拆分名称,例如另建内部域名,证书、配置和文档都会变成两套。终有一天,只修改其中一套后,二者会悄然出现偏差。而两套中哪一套才正确,往往要等到故障发生后才会知道。保持名称唯一,并非只为方便,而是一种减少事故的机制。即使服务目前只在内部使用,也最好从一开始就配置正式名称和证书,日后向外开放时成本会低得多。

生产环境中真正重要的事

Kubernetes 中也存在同样的问题。Pod 通过自身服务的外部地址(LoadBalancer IP)调用自己时,也会形成 Hairpin。externalTrafficPolicy: Local 模式下,节点不执行 SNAT,此时响应可能无法返回。

因此,在集群内部始终使用服务名称svc.namespace.svc.cluster.local)调用。不要通过外部地址调用自身——仅这一条规则,就能消除一半的 Hairpin 问题。