现在很多个人宽带已经没有动态公网IP了,这使得在家庭内部署一些网络服务失去了意义

判断是否具备公网IP:

  1. 访问 http://cip.cc ,获得当前的公网 IP
  2. 查看路由器或者拨号软路由拨号后的 IP 是否相同,不同的话大概率是 Nat 网络
  3. 进一步验证,将路由器的页面管理端口修改至 9090 ,访问 http://public-ip:9090

如果访问不到,则说明处于 Nat 网络中,在 Nat 内网中部署的服务需要暴露出去的话,则需要进行 Nat 打洞

1. NAT网络

1.1. NAT

NAT 的全称是 Network Address Translation,即网络地址转换,指的是路由器等网络设备,在传输数据的过程中,改变数据中的 IP 地址的一种技术

NAT 技术示意图

@startuml
skinparam packageStyle rectangle

package "NAT 内网 1" #LightBlue {
  [PC1]
  [网关1]
}

package "NAT 内网 2" #LightGreen {
  [PC2]
  [网关2]
}

[PC1] -right-> [网关1] : 内部网络 \n (私有IP:端口 192.168.1.2:12345)
[网关1] -right-> [互联网] : \n 端口转换 \n (公网IP:端口 203.0.113.1:23456)
[互联网] -right-> [网关2] : \n 端口转换 \n (公网IP:端口 198.51.100.1:34567)
[网关2] -right-> [PC2] : 内部网络 \n (私有IP:端口 10.0.0.2:54321)

@enduml

随着全球联网设备越来越多,但 IPv4 地址资源有限,所以 NAT 技术在 IPV6 普及前都会是相当广泛的应用

1.2. NAT类型

NAT 分为好几种类型:

  1. Full Cone NAT:“完全圆锥形 NAT”,内部设备与某个外部服务建立tcp链接后(如示意图),在未关闭该tcp链路前,任何外部设备可以通过此链路端口访问到内部端口
  2. Restricted Cone NAT:“受限圆锥形 NAT”,在 Full Cone NAT 基础上,增加对访问链路的设备的 IP 限制,仅允许 tcp 链路的目标设备发送 tcp 数据包到内部设备
  • Port-Restricted Cone NAT:“端口受限圆锥形 NAT”,在 Restricted Cone NAT 的基础上,再增加端口检查,仅允许 tcp 链路的目标设备和目标端口发送tcp数据包到内部设备
  • Symmetric NAT:“对称 NAT”,与前三种不同,对称 NAT 会为每个新连接分配一个新的外部端口,即使是从同一个内部端口发起的连接

有许多服务依赖与网络通信,尤其是 P2P、游戏等,例如在Xbox、PlayStation上,可以检测对网络类型进行检测,结果分别如下:

  • open:拥有公网IP,最佳网络环境
  • moderate:对应前三种 NAT 类型,可用的网络环境
  • strict: 对称NAT,不可用的网络环境

1.3. 打洞

NAT 打洞技术是指通过一系列的技巧和协议,尝试在 NAT 上创建临时的映射或规则,使得两个设备可以在 NAT 网络后进行直接通信

Full Cone NAT 是最容易打洞的环境,而且对访问来源没有限制,可用于 NAS 文件分享、Web 服务等

而对于 Restricted Cone NATPort-Restricted Cone NAT 来说,打洞的效果则要打些折扣,打洞过程如下:

@startuml
!define PC1_COLOR #FFCCFF
!define PC2_COLOR #CCFFCC

participant PC1 as "PC1" << (C,PC1_COLOR) >>
participant NAT_PC1 as "NAT (PC1)" << (C,PC1_COLOR) >>
participant C as "服务器 (C)" << (C,lightgray) >>
participant NAT_PC2 as "NAT (PC2)" << (C,PC2_COLOR) >>
participant PC2 as "PC2" << (C,PC2_COLOR) >>

PC1 -> C : 注册并获取公共 IP:端口 (PC1_pub)
PC2 -> C : 注册并获取公共 IP:端口 (PC2_pub)
C -> PC1 : 发送 PC2 的公共 IP:端口 (PC2_pub)
C -> PC2 : 发送 PC1 的公共 IP:端口 (PC1_pub)

PC1 -> NAT_PC1 : 发送数据包到 PC2_pub
NAT_PC1 -> PC2 : 转发数据包到 PC2_pub

PC2 -> NAT_PC2 : 发送数据包到 PC1_pub
NAT_PC2 -> PC1 : 转发数据包到 PC1_pub

PC1 -> PC2 : 建立直接通信
PC2 -> PC1 : 建立直接通信

@enduml

Restricted Cone NATPort-Restricted Cone NAT 的打洞流程复杂,且对客户端也有一定要求,但基本也满足游戏对战、P2P下载等服务

2. NAT 打洞

按照前面介绍的 NAT 类型,明确可以进行内网穿透的是 Full Cone NATRestricted Cone NATPort-Restricted Cone NAT三中

对称NAT 打洞成功率很低,Full Cone NAT 是最理想的打洞环境

2.1. 检测 NAT 类型

Windows:

Linux/MacOS:

Windows下载后运行即可,其他操作系统可以使用 pystun 来做检测,以 pystun 为例

在 debian12 下为例,首先安装 Python 的包管理器:

# 根据你的 python 版本来安装虚拟环境套件
sudo apt install python3.xx-venv

创建一个虚拟环境,然后安装 pystun:

python3.xx-venv -m venv venv
source venv/bin/active

安装 pystun :

pip install pystun

运行 pystun :

pystun3

下面是我的输出

(venv) ~$ pystun3
NAT Type: Full Cone
External IP: 12.125.45.119
External Port: 6622

可以看到我的 nat 类型是 Full Cone NAT

2.2. NAT 打洞实践

natmap 用于从ISP NAT公网地址到本地私有地址建立 TCP/UDP 端口映射

以 web 服务为例,运行一个 librespeed 测速服务为例,docker 运行如下:

sudo docker run \
  -e PUID=1000 \
  -e PGID=1000 \
  -e TZ=Etc/UTC \
  -e PASSWORD=chancel \
  -p 8080:80 \
  lscr.io/linuxserver/librespeed:latest

访问 http://localhost:8080 并测速,结果如下:

然后下载 natmap 并编译:

git clone --recursive https://github.com/heiher/natmap.git
cd natmap
make

# 可选
cp bin/natmap /usr/bin/natmap

运行 natmap 建立一个端口映射

/usr/bin/natmap -i ppp0 -s turn.cloudflare.com -h qq.com -t localhost -p 8080

参数解析:

  • -i:指定网卡,这里指定了宽带拨号产生的ppp0
  • -s:使用的外网 TURN 服务器
  • -h: 对于tcp而言,需要一个保持 tcp 长连接的对象,这里填了腾讯的服务器
  • -t/-p:指内网设备的IP和端口

运行输出如下:

/usr/bin/natmap -i ppp0 -s turn.cloudflare.com -h qq.com -t 127.0.0.1 -p 8080
12.125.45.119 4475 2001::223d:2c91:82b3 46701 tcp 14.213.121.3

第一个输出即公网IP,第二个输出则是公网端口,在其他网络下,访问 http://12.125.45.119:4475 后测速:

可以看到测速的结果有接近 50 Mbps,我的宽带是 100Mbps

2.3. 自动更新

从前面对 nat 技术的介绍不难看出,映射到公网IP和公网端口会随时间改变,所以可以结合 DDNS 等技术来实现长时间打洞效果

https://api.chancel.me/rest/api/v1/anyjson 接口为例实现长期打洞,该 API 使用如下:

$ curl -X POST https://api.chancel.me/rest/api/v1/anyjson?id=hello -H "Content-Type: application/json" -d '{"lihua": "hi!"}'
# 返回状态 1 ,表示存储成功
{"status":1,"msg":"Data stored successfully","data":null,"version":"V1.0.0"}

$ curl -X GET https://api.chancel.me/rest/api/v1/anyjson?id=hello
{"lihua":"hi!"}

这个接口可用于临时存储 json,所以写一个 bash 脚本 natmap.sh 如下:

#!/bin/sh

/usr/bin/curl -X POST https://api.chancel.me/rest/api/v1/anyjson?id=secret -H "Content-Type: application/json" -d "{\"ip\": \"${1}\",\"port\":\"${2}\"}"

其中 $1$2 是由 natmap 传入的,即公网IP和公网端口,这样 natmap 会在打洞成功后执行该脚本将打洞结果上传到接口中

运行试试:

chmod +x natmap.sh
/usr/bin/natmap -i ppp0 -s turn.cloudflare.com -h qq.com -t 127.0.0.1 -p 8080 -e natmap.sh

此时在其他网络中,使用curl来获取打洞结果

curl https://api.chancel.me/rest/api/v1/anyjson\?id\=secret
{"ip":"12.125.45.119","port":"4467"}

可以看到,成功获取到了打洞结果,在其他网络中就可以利用这个结果来访问对应服务

以上为示例,可以灵活结合 DDNS 技术来实现各种内网服务穿透

最后,结合 supervisor 来实现 natmap 后台自动运行:

[program:natmap]
command=/opt/natmap/natmap -i ppp0 -s turn.cloudflare.com -h qq.com -t 127.0.0.1 -p 22 -e /opt/natmap/natmap.sh
autostart=true
autorestart=true
stdout_logfile=/var/log/supervisor/%(program_name)s.log
stderr_logfile=/var/log/supervisor/%(program_name)s.log
stdout_logfile_maxbytes=32MB
user=app

3. 对称NAT

2024年后,广东大部分地区的宽带都变成了 Symmetric NAT,这非常糟糕

在光猫拨号情况下,即使是 Full Cone NAT 也会被识别为 Symmetric NAT

所以需要将光猫更改为桥接,拨号由路由器/软路由来拨号,再结合DMZ来做网络类型测试

光猫更改为桥接,这一点需要光猫的管理员账户,可以通过

  • 找装宽带运维的师傅要
  • 淘宝找店铺解决