局域网通过DDNS域名+端口访问自建服务的解决方案
因为家里房屋翻新,整个网络拓扑结构得以升级,外加我个人有很多自建服务的需求,所以在这一年多的时间里折腾了不少东西,也遇到了不少问题。要说在所有问题里遇到的最棘手的问题,就是内外网难以通过同一种方式访问服务。
注:本文内容针对 IPv4 NAT 所导致的问题,IPv6 DDNS 并不受此影响。
举个例子
我路由器设置了DDNS域名:xxx.ddns.com
,并在主路由防火墙设置中将LAN
内软路由192.168.0.25
的8080
端口映射到了WAN
的8088
端口,于是乎我可以在学校宿舍通过xxx.ddns.com:8088
远程访问家里软路由的配置页面。
但是问题在于,如果我想通过LAN
来访问软路由的配置页面,我则只能通过192.168.0.25:8080
的方式访问,直接访问xxx.ddns.com:8088
会被拒绝连接。
如果服务比较少,或只在LAN
/WAN
使用的情况下还好说,大不了所有东西都上双栈(为LAN
和WAN
分别设置两套配置)。但是我现在用到的服务有几十个,而且经常在家里和外面跑,并且很多服务不支持/不能良好支持两套配置的协同,这就要求我必须找到一种方法让内外网都可以通过域名来访问对应的服务。
问题分析
简化一下上面的案例:
- 通过DDNS域名+端口的方式访问某个服务(如:
xxx.ddns.com:8088
),在WAN
可以正常使用,在LAN
被拒绝连接。
这里的DDNS域名在内外网都可以被正常解析成对应的WAN
IP地址,所以问题可以进一步简化:
- 通过
WAN
IP+端口的方式访问某个服务(如:111.222.333.444:8088
),在WAN
可以正常使用,在LAN
被拒绝连接。
造成这一问题的主要原因是NAT本身,因为我使用的服务大多是基于TCP的,而TCP的三次握手会因为NAT的缘故失败。
依旧是举个例子:LAN
内的PC(192.168.0.10
)想要通过主路由(192.168.0.1
)的WAN
IP(111.222.333.444
)与LAN
内的服务器(192.168.0.254
)建立TCP链接。
对于PC(192.168.0.10
),数据包的发送路径是192.168.0.10 -> 111.222.333.444
,并期待111.222.333.444
的回复。数据包也正确通过主路由(192.168.0.1
),并转发给了服务器(192.168.0.254
)。
而问题出现在数据包的回复上,服务器(192.168.0.254
)向主路由(192.168.0.1
)回复数据包,而主路由发现目的地址是LAN
内的PC(192.168.0.10
),就直接将数据转发给了PC(192.168.0.10
),于是数据包的回复路径变成了192.168.0.254 -> 192.168.0.10
,而PC(192.168.0.10
)所期待的是111.222.333.444
的回复,来自服务器(192.168.0.254
)的“理应接收”的数据包被直接丢弃,进而造成了所我所遇到的问题。
一个题外话,TCP存在三次握手的设计,所以才会遇到问题,那么不需要握手确认的UDP是不是就没有这个问题呢?事实的确如此,因为UDP不需要“回复”来建立链接,所以基于UDP的数据包不受NAT影响,比如Wake on LAN的Magic Packet,但是问题在于至少我个人而言基于UDP的服务太少了。
问题解决
标准答案
其实这一问题是有“标准答案”的,即NAT环回(NAT Loopback)或者SNAT(Source Network Address Translation)。
NAT环回的本质就是SNAT,只是具体操作起来可能有所区别。工作原理是在路由器收到服务器转发的数据包时,手动对其做一些手脚,将其IP段改成自己的WAN
IP地址,再转发给PC,进而实现正常建立TCP连接。
但是这个“标准答案”有个问题,就是很难操作甚至无法操作。大多数人的家庭主路由都不是OpenWRT,这些家庭路由器的功能是严重缺失的,可能根本没有“NAT环回”这个选项,更别提“SNAT”这个需要手动操作iptables
的方法。
我的主路由是TPLink的商用级路由器TL-R479GP-AC 8.0,虽然它的端口映射(TPLink称“虚拟服务器”)是默认支持
LAN
的NAT回环的,并且也支持单独设置回环地址,但我个人尝试后发现不生效,网上提供的其他TPLink设置NAT回环方案也不适用于我的路由器。
通用答案
因为我们最终还是通过DDNS域名来访问服务,而非直接使用IP,因此我们可以在域名 -> IP
这个过程使用一些手段,即:DNS劫持。
其原理就是在LAN
内直接将DDNS域名解析到具体的某个LAN
IP,这样通过DDNS访问某项服务,其本质和直接通过LAN
IP访问是一样的。
实现方案一(主路由DNS静态解析)
比如我们在主路由已经添加了DDNS域名xxx.ddns.com
,WAN
IP为111.222.333.444
,服务器LAN
IP为192.168.0.254
。
此时我们添加一条DNS静态解析:xxx.ddns.com -> 192.168.0.254
,可以实现效果:
WAN
访问:xxx.ddns.com -> 111.222.333.444 -> 192.168.0.254
;LAN
访问:xxx.ddns.com -> 192.168.0.254
。
直接在主路由上实现DNS静态解析的优点是连接至局域网即可完成域名解析的切换,不需要终端过多设置。而问题在于“DNS静态解析”这一功能在大多数家用路由器上也被阉割。
实现方案二(修改终端hosts
)
比如我们在主路由已经添加了DDNS域名xxx.ddns.com
,WAN
IP为111.222.333.444
,服务器LAN
IP为192.168.0.254
。
我们可以添加一条hosts
:192.168.0.254 xxx.ddns.com
,此时即可在LAN
中通过xxx.ddns.com
访问服务器。
但是这种方案的灵活度十分之低,复杂度又过高。每次离开LAN
都需要手动改回hosts
,等进入LAN
再改过来。这种方法麻烦至极,唯一优点在于要求极低,只要终端支持修改hosts
都可以(手机和平板寄了,但大多数PC都是支持修改hosts
的)。
实现方案三(内网设置DNS服务器)
如果你的主路由不支持方案一的DNS静态解析,也不想使用/无法使用方案二的修改hosts
,那么还有一个办法是自建一个DNS服务器。
如果你家里有其他支持DNS解析的设备(如:软路由、群晖、树莓派),那么你可以将方案一的做法应用于这些设备,建立起一个DNS服务器。然后你可以将路由器或终端设备的DNS指向DNS服务器,进而实现相同的效果。
这种做法的缺点在于你需要一个能稳定在线的单独设备做为DNS服务器存在,当然买一块Linux开发板的成本也不高(别买成单片机)。
问题
DNS劫持虽然简单粗暴,但是不是所有问题都能解决。
比如DNS解析是域名 -> IP
,端口被忽视了。所以如果你的主路由映射了多台LAN
内设备的端口,那么你只能选择其中一个LAN
IP做域名映射,其他设备上的服务没办法通过同一个域名访问。
我个人的解决办法也很简单粗暴,多弄几个DDNS IP地址就好了(xxx.ddns.com
和yyy.ddns.com
分别映射到内网两台设备上,两台设备同时做DDNS使外网同样可用)
再一个问题是现代设备为了加快DNS解析速度,都会设置DNS缓存,所以你切换WAN
/LAN
环境时,DDNS域名可能没办法一下正确解析。
我个人的解决办法同样简单粗暴,直接清理DNS缓存。对于Windows系统可以通过cmd
命令ipconfig /flushdns
来清空,浏览器(这里只给Edge,其他浏览器的方案自行搜索)可以通过edge://net-internals/#dns
来清空。