背景
家里弱电箱放着一台 F7005TV3,感觉性能还是不错的,但是有 NAT 回流的问题,在家访问 NAS 非常麻烦,经常切账号。
最简单就是搭个 DNS 内网服务器,将域名指向内网设备,但是会存在 dns 的切换延迟,内网可以设置 ttl 为 1s,出到外网可以立即更新成公网 IP,但是从外网回到内网就要看域名解析的 TTL 时间了,国内普遍最低是 10 分钟(花钱可以修改),也就是意味着从外网进到内网最长会有 10 分钟时间无法使用域名访问,如果可以接受的话,就不用看这个文章了,去搭建 DNS 服务吧。
也舍不得花三五百买个硬路由,软路由不想 all in one,所以搞了光猫的 telnet 权限,研究 NAT 回流问题。
这个过程中学习到了很多知识,也成功解决了 NAT 回流问题,所以分享一下我的折腾经过,给大家提供一个思路。
硬件
需要
NAT 原理
授人以鱼不如授人以渔,简单讲一下原理吧,省略了很多,只简单说下涉及到的。
当客户端发起请求时,数据包会先进入光猫,再从光猫出去。在这两个过程中,防火墙会根据相应规则进行匹配和处理。
- 进:PREROUTING 表
- 出:POSTROUTING 表
还有一些其他的,我就忽略了不讲了,下面举几个例子,了解为什么会出现 NAT 回流问题。
初始情况下,这两个表中没有自定义规则,仅包含防火墙的默认配置。
内网 IP 访问内网 IP
192.168.1.20 发起请求:
- 请求包:192.168.1.20:1111 > 192.168.1.10:2222
- 从 lan 口进,经过 PREROUTING,未匹配规则,包不修改
- 经过 POSTROUTING,未匹配规则,包不修改,从 lan 口出
- 192.168.1.10 收到请求包:192.168.1.20:1111 > 192.168.1.10:2222
192.168.1.10 发出响应:
- 响应包:192.168.1.10:2222 > 192.168.1.20:1111
- 从 lan 口进,经过 PREROUTING,未匹配规则,包不修改
- 经过 POSTROUTING,未匹配规则,包不修改,从 lan 口出
- 192.168.1.20 收到响应包:192.168.1.10:2222 > 192.168.1.20:1111
两者正常通讯。
外网访问内网
在这个情况下,肯定是做了端口映射的,假设光猫公网 IP 为 1.1.1.1,在光猫自带的界面添加端口映射 1000 > 192.168.1.10:1688
那么 PREROUTING 会新增一条规则:
- 从 wan 口来的
- 源地址为任意地址,任意端口
- 目标地址为 1.1.1.1,端口为 1000
当匹配到数据包后,将数据包目标地址修改为 192.168.1.10,端口为 1688,同时会记录下这个修改信息
举例子好理解,假设外网 IP 为 8.8.8.8,所以流程为:
8.8.8.8 发起请求:
- 请求包:8.8.8.8:1234 > 1.1.1.1:1000
- 从 wan 口进,经过 PREROUTING,匹配规则,此时请求包修改为:8.8.8.8:1234 > 192.168.1.10:1688
- 记录修改规则:1.1.1.1:1000-192.168.1.10:1688 (当然代码里不是这样的,我只是简单写一下方便理解)
- 经过 POSTROUTING,未匹配规则,包不修改,从 lan 口出
- 192.168.1.10 收到请求包:8.8.8.8:1234 > 192.168.1.10:1688
192.168.1.10 发出响应:
- 响应包:192.168.1.10:1688 > 8.8.8.8:1234
- 从 lan 口进,经过 PREROUTING,未匹配规则,包不修改
- POSTROUTING 时命中 PREROUTING 阶段的修改规则,此时响应包修改为:1.1.1.1:1000 > 8.8.8.8:1234
- 8.8.8.8 收到响应包:1.1.1.1:1000 > 8.8.8.8:1234
内外网设备正常通讯。(这里省略了很多,方便理解)
内网通过域名访问内网:NAT 回流故障
现在来看下,为什么会出现 NAT 回流:
192.168.1.20 以域名发起请求:
- 请求包:192.168.1.20:1111 > 1.1.1.1:1000
- 从 lan 口进,经过 PREROUTING,未匹配规则,包不修改
- 经过 POSTROUTING,未匹配规则,包不修改,从 lan 口出
- 192.168.1.10 收不到数据包
这是第一个问题,光猫添加的域名解析是匹配从 wan 口进的数据包,所以 NAT 回流的第一步,就是把这个规则改为数据包从 wan 和 lan 的都匹配。
假设完成了这个修改,那我们重新看看步骤:
192.168.1.20 以域名发起请求:
- 请求包:192.168.1.20:1111 > 1.1.1.1:1000
- 从 lan 口进,经过 PREROUTING,匹配规则,此时请求包修改为:192.168.1.20:1111 > 192.168.1.10:1688
- 记录修改规则:1.1.1.1:1000-192.168.1.10:1688
- 经过 POSTROUTING,未匹配规则,包不修改,从 lan 口出
- 192.168.1.10 收到数据包:192.168.1.20:1111 > 192.168.1.10:1688
192.168.1.10 发出响应:
- 响应包:192.168.1.10:1688 > 192.168.1.20:1111
- 从 lan 口进,经过 PREROUTING,未匹配规则,包不修改
- 经过 POSTROUTING,未匹配规则,包不修改,从 lan 口出
- 192.168.1.20 收到响应包:192.168.1.10:1688 > 192.168.1.20:1111
此时,192.168.1.20 认为自己在和 1.1.1.1:1000 交互,怎么来了个 192.168.1.10:1688 的包?那么直接抛弃。
所以这是 NAT 回流的第二个问题,接受和响应不一致,而修改这个问题,就需要从整个流程来解决了。
内网通过域名访问内网:NAT 流程修改
其实也简单,我们知道 192.168.1.10 收到的数据包是 192.168.1.20:1111 > 192.168.1.10:1688,这显然是不行的。
所以我们需要光猫做一个中间人,让 10 认为自己在和光猫沟通,发的包是给光猫的,然后光猫再转成公网 1.1.1.1:1000 给 20 接收,这样就皆大欢喜了。
所以需要 2 个规则:
PREROUTING:
- 从任意口来的(wan 匹配外网的设备,lan 匹配内网的,所以光猫创建的没用)
- 源地址为任意地址,任意端口
- 目标地址为 1.1.1.1,端口为 1000
当匹配到数据包后,将数据包目标地址修改为 192.168.1.10,端口为 1688,同时会记录下这个修改信息
POSTROUTING:
- 从 lan 口出去的
- 源地址是 192.168.1.0/24 的内外设备
- 目标地址为 192.168.1.10,端口 1688
- 修改源地址为 192.168.1.1,端口任意
匹配到后,将内网走向 192.168.1.10:1688 的数据包的源地址改为 192.168.1.1,端口任意
这样的话,整个流程就为:
192.168.1.20 以域名发起请求:
- 请求包:192.168.1.20:1111 > 1.1.1.1:1000
- 从 lan 口进,经过 PREROUTING,匹配规则,此时请求包修改为:192.168.1.20:1111 > 192.168.1.10:1688
- 记录修改规则:1.1.1.1:1000-192.168.1.10:1688
- 经过 POSTROUTING,匹配规则,此时请求包修改为:192.168.1.1:1234 > 192.168.1.10:1688
- 记录修改规则:192.168.1.20:1111-192.168.1.1:1234
- 192.168.1.10 收到数据包:192.168.1.1:1234 > 192.168.1.10:1688
192.168.1.10 发出响应:
- 响应包:192.168.1.10:1688 > 192.168.1.1:1234
- 从 lan 口进,PREROUTING 时命中 POSTROUTING 阶段的修改规则,修改包为:192.168.1.10:1688 > 192.168.1.20:1111
- POSTROUTING 时命中 PREROUTING 阶段的修改规则,修改包为:1.1.1.1:1000 > 192.168.1.20:1111
- 192.168.1.20 收到响应包:1.1.1.1:1000 > 192.168.1.20:1111
此时设备就正常沟通了(修改规则的命中,是 POSTROUTING 互相 PREROUTING 命中的,也就是 2 层 NAT 的转换)。
方案分享
知道原理后,找对应的方案就比较简单了,就是 2 个规则,有 telnet 后连接上去创建就行,每个人可能都不一样,分享下我的方案吧。
我图省事,就直接全部端口走 192.168.1.2 了,等同于 DMZ,因为每一个端口解析就要创建一条匹配规则,操作起来太麻烦了。
而 192.168.1.2 是我的 iStoreOS 旁路由,在这里做端口映射方便些。
代码
这是直接使用的代码,你可以测试一下是否正常,正常的话,再考虑做自动任务
iptables -t nat -A PREROUTING -d <替换你的公网IP> -j DNAT --to-destination 192.168.1.2
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -d 192.168.1.2 -j SNAT --to-source 192.168.1.1
如果是端口映射而且还需要指定协议的话,就这两条命令:
iptables -t nat -A PREROUTING -d <替换你的公网IP> -p tcp --dport 1111 -j DNAT --to-destination 192.168.1.10:2222
iptables -t nat -I POSTROUTING 1 -s 192.168.1.0/24 -d 192.168.1.10 -p tcp --dport 2222 -j SNAT --to-source 192.168.1.1
而使用这两个命令还不行的话,还需要修改一下路由策略,因为我不停踩坑调试后发现,内网以域名访问的数据包,竟然从 wan 口出去了,所以如果你还不行的话,需要这个代码:
# 查看所有策略
ip rule list
# 将内网策略放在公网之上,最后数值越小优先级越高,检查一下公网 IP 的优先级时多少,然后把这个数值填小一点。
ip rule add from 192.168.1.0/24 to 192.168.1.0/24 lookup main priority 500
到了这一步,你应该是可以在内网以域名访问服务器了。
(如果还不行,那我确实不知道了,我是这样的完成的,原理也就那些,可以借助 AI 去询问一下)
自动任务
当然上面的命令,在光猫重启后就 g 了,然后因为公网 ip 是动态的,所以需要用自动任务去做自动更新。
在光猫上运行自动脚本太麻烦了,很多地方是只读的,我就没有在光猫上跑脚本,而是在光猫的用户目录里写了脚本(用户目录重启不会丢失),然后用 Lucky 去定时跑。
在光猫里运行:
启动脚本(在这里,我把公网的匹配放在了 ipset 规则里,然后直接更新 ipset 的规则,可以看到下面的更新 IP 脚本)
cat > /userconfig/NAT_INIT << 'EOF'
#!/bin/sh
INIT=$(ip rule list | grep -c "from 192.168.1.0/24 to 192.168.1.0/24 lookup main")
if [ "$INIT" -gt 0 ]; then
echo "IS INIT"
else
ip rule add from 192.168.1.0/24 to 192.168.1.0/24 lookup main priority 500
ipset create wan_ip hash:ip
iptables -t nat -A PREROUTING -m set --match-set wan_ip dst -j DNAT --to-destination 192.168.1.2
iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -d 192.168.1.2 -j SNAT --to-source 192.168.1.1
echo "INIT OK"
fi
EOF
chmod +x /userconfig/NAT_INIT
更新 IP
cat > /userconfig/NAT_AUTO_WAN << 'EOF'
#!/bin/sh
CURRENT_IP=$(curl -s ipv4.ddnspod.com | tail -n 1)
IPSET_IP=$(ipset list wan_ip | tail -n 1)
if [ "$CURRENT_IP" = "$IPSET_IP" ]; then
echo "IP === ${CURRENT_IP}"
else
ipset flush wan_ip
ipset add wan_ip $CURRENT_IP
echo "IP Update ${CURRENT_IP}"
fi
EOF
chmod +x /userconfig/NAT_AUTO_WAN
然后上面 2 个文件在光猫里创建好后,可以去 Lucky 创建定时任务:
#!/bin/sh
HOST="192.168.1.1"
PORT="23"
USER="root"
PASS="Zte521"
{
sleep 1
echo "$USER"
sleep 1
echo "$PASS"
sleep 2
echo "/userconfig/NAT_INIT"
sleep 2
echo "/userconfig/NAT_AUTO_WAN"
sleep 1
echo
sleep 3
echo "exit"
} | nc -v -w 10 "$HOST" "$PORT"
最后
如果有错别字可以提醒下,希望你能像我一样顺利,转载请注明出处哈。
感觉用 AI 学习新知识确实很方便,haha,终于不用纠结要不要买硬路由或者用软路由了。