一、初识NAT

NAT 类型

NAT 分为四种类型,分别为 NAT1234 🙃

  1. NAT1 - 完全锥形NAT(Full Clone NAT)
  2. NAT2 - 限制性锥形NAT(Restricted Clone NAT)
  3. NAT3 - 端口限制性锥形NAT( Port Restricted Clone NAT)
  4. NAT4 - 对称式NAT(Symmetric NAT)

NAT1234
怎么判断呢,先从 绿色 箭头看起。

现在假设

192.168.0.100 通过 8000 端口 访问 114.135.246.909001 端口

经过NAT之后,会形成这样一种结果:

  1. 内网的 192.168.0.100:8000120.230.117.10:9999 NAT公网IP,形成绑定关系
  2. 服务端 114.135.246.90:9001120.230.117.10:9999 NAT公网IP,访问

然后如果:
8000 端口 能收到 紫色 发来的消息,就是 NAT1
8000 端口 收不到 紫色 发来的消息,却能收到 浅蓝色 线发来的消息,就是 NAT2
8000 端口 收不到 紫色浅蓝色 线发来的消息,只能收到 绿色 线返回来的消息,就是 NAT3 | NAT4

NAT3 和 NAT4 的区别:
NAT3 是 8000 端口与 9999 端口形成绑定关系,不管通过 8000 端口访问谁,·都是从 9999 端口出去
NAT4 是 8000 端口与 9999 端口形成的绑定关系是有条件的,8000 端口访问同一 ip:port 时,都会从 9999 端口出去,但通过 8000 端口访问其他 ip:port 时,就会绑定 NAT 其他端口,比如 9990

我这移动大内网,一般都是NAT3,手机开数据流量,基本都是NAT4

NAT 打洞原理

打洞,说白了就是让 NAT 留下一条访问记录
谁来打洞,就在谁的 NAT 上,留下一条对方的记录,这样在对方访问自己的时候,不会因为自己的 NAT 因为缺少记录而把数据包丢弃

( 对于NAT来说,因为没记录,所以也不知道转发给内网的谁,就算知道 如果没有记录,则数据包的来源也不一定合法 )



二、UDP打洞

看图

UDP打洞

这里我们通过一个服务端,然后让A B两个内网主机进行UDP打洞并通信。适用于 NAT123

说明:
A、B 分别为两个不同内网下的主机
NA、NB分别问 A 和 B 的 NAT 出口
中间的 90.20.88.99 为服务端主机,下方称为 S

图解:

  1. 首先我们让 A:8001S:9001 建立UDP通信
    此时 NA 就会留下 内网的 A:8001NA:3333 的绑定,NA:3333S:9001 的访问
    即:A:8001 - NA:3333 - S:9001

  2. 接着让 B 也执行同样的操作
    此时 NB 就会留下 内网的 B:6000NB:4444 的绑定,NB:4444S:9001 的访问
    即:B:6000 - NB:4444 - S:9001

  3. 此时 S 知道 NANB,且还知道 (看着上面的两个‘即’ 继续往下看)
    A 通信可以用 S:9001NA:3333 发送消息
    B 通信可以用 S:9001NB:4444 发送消息
    若让 A - B 直接通信,得让 A 知道 NBB 知道 NA
    所以
    NB:4444 通过 S:9001 发送给 NA:3333A 知道可以通过 NB:4444B 通信
    NA:3333 通过 S:9001 发送给 NB:4444B 知道可以通过 NA:3333A 通信

  4. 若此时 B:6000 - NB:4444 直接向 NA:3333 进行 UDP 通信
    NA 会因为不存在 A:8001 - NA:3333 - NB:4444 这条记录,而将数据包丢弃 (同理 A 奕是如此,此时双方是对等的)
    但此时,在 NB 中则会留下 B:6000 - NB:4444 - NA:3333 这样一条记录,这一步就是所谓的 “打洞”

  5. NB:4444NA:3333 发送完之后,此时 A:8001 - NA:3333 再向 NB:4444 发送 UDP 数据包时
    因为 NB 有了 B:6000 - NB:4444 - NA:3333 这样一条记录,所以会将 NA:3333 发来的数据包转发到 B:6000
    并且此时在 NA 也会留下 A:8001 - NA:3333 - NB:4444 这条记录

  6. 之后 因为
    NA 有:
    A:8001 - NA:3333 - NB:4444
    A:8001 - NA:3333 - S:9001
    NB 有:
    B:6000 - NB:4444 - NA:3333
    B:6000 - NB:4444 - S:9001
    所以 AB 都可以与 S 直接通信,AB 也可以直接通信

看码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
// client.js

const dgram = require("dgram");

const option = process.argv.splice(2)

let localPORT = 6677
const SERVER_PORT = option[0] || 9001
const SERVER_ADDR = option[1] || "114.135.246.90"

let localInfo = ""
let serverInfo = ""

let timeout = setInterval(() => {
createClient()
}, 1000)

let getConnect = (client) => {
console.log(`与${serverInfo}连接成功。`);

client.send("")

// process.stdin.on('data', data => {
// let sendData = data.toString().trim()
// client.send(sendData);
// })
}

let getMessage = (client, data, rinfo) => {
console.log(`[${rinfo.address}:${rinfo.port}]:${data.toString()}`)

let res = JSON.parse(data.toString())
if (res.msg == "wait") {

}
if (res.msg == "toClient") {
let second = res.second
createNewClient(second.addr, second.port)
client.close()
}
if (res.msg == "toServer") {
let first = res.first
createServer(first.addr, first.port)
client.close()
}

}

function createClient() {

const client = dgram.createSocket("udp4");
try {

client.bind(localPORT, "", () => {
clearInterval(timeout)
})

client.connect(SERVER_PORT, SERVER_ADDR)

client.on('connect', () => getConnect(client))

client.on('message', (data, rinfo) => {
getMessage(client, data, rinfo)
})

client.on("listening", (res) => {
localInfo = `[${client.address().address}:${client.address().port}]`
serverInfo = `[${SERVER_ADDR}:${SERVER_PORT}]`
console.log(`${localInfo} - ${serverInfo}`);
// client.send(`${localInfo}-已连接-serverInfo`,SERVER_PORT,SERVER_ADDR)
})

client.on("close", (res) => {
// console.log("close", "正在关闭");
})

client.on("error", (err) => {
console.log("error", "发生一个错误");
console.log("正在重连...");
localPORT++
// createClient()
})

} catch (error) {
console.log("错啦!!!")
console.log(error)
}

}

function createNewClient(serverAddr, serverPort) {

const client = dgram.createSocket("udp4");

try {

client.bind(localPORT)

client.connect(serverPort, serverAddr)

client.on('connect', () => {
console.log(`与${serverInfo}连接成功。`);
client.send(`${localInfo}`)
console.log("回车发送消息:");

process.stdin.on('data', data => {
let sendData = data.toString().trim()
console.log(`${localInfo}: ${sendData}`);
client.send(sendData);
})
})

client.on('message', (data, rinfo) => {
console.log(`[${rinfo.address}:${rinfo.port}]: ${data.toString()}`)
})

client.on("listening", (res) => {
localInfo = `[${client.address().address}:${client.address().port}]`
serverInfo = `[${serverAddr}:${serverPort}]`
console.log(`${localInfo} - ${serverInfo}`);
// client.send(`${localInfo}-已连接-serverInfo`,SERVER_PORT,SERVER_ADDR)
})

client.on("close", (res) => {
console.log("close", res);
})

client.on("error", (err) => {
console.log("error", "发生一个错误");
console.log("正在重连...");
localPORT++
// createClient()
})

} catch (error) {
console.log("错啦!!!")
console.log(error)
}
}

function createServer(clientAddr, clientPort) {

const server = dgram.createSocket('udp4')
server.bind(localPORT)

// 开洞
server.send("", clientPort, clientAddr)
// 通知服务端准备好了
server.send("ready", SERVER_PORT, SERVER_ADDR)

server.on("listening", () => {
console.log(`start to listening: ${server.address().address}:${server.address().port}`)

console.log("回车发送消息:");
process.stdin.on('data', data => {
let sendData = data.toString().trim()
console.log(`${localInfo}: ${sendData}`);
server.send(sendData, clientPort, clientAddr);
})
})

server.on("connect", (res) => {
console.log("connect:", res);
})

server.on("message", (msg, rinfo) => {
console.log(`[${rinfo.address}:${rinfo.port}]: ${msg.toString()}`)
let message = msg.toString()
// server.send(message, clientPort, clientAddr)
})

server.on("close", (res) => {
console.log("close", res);
})

server.on("error", (err) => {
console.log("error", err);
})
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// server.js

const dgram = require('dgram')

const server = dgram.createSocket('udp4')

let first = ""
let second = ""
let alive = {}

server.bind(9001)

server.on("listening", () => {
console.log(`start to listening: ${server.address().address}:${server.address().port}`)
})

server.on("connect", (res) => {
console.log("connect:", res);
})

server.on("message", (message, rinfo) => {
let msg = message.toString()
console.log(`[${rinfo.address}:${rinfo.port}]:${msg.toString()}`)

let count = Object.keys(alive).length
if (count == 0) {
first = {
addr: rinfo.address,
port: rinfo.port
}
server.send(`{"res": "wait"}`, rinfo.port, rinfo.address)
}
else if(count == 1){
second = {
addr: rinfo.address,
port: rinfo.port
}
let res = {
first,
msg: "toServer"
}
server.send(JSON.stringify(res), rinfo.port, rinfo.address)
}
alive[`${rinfo.address}:${rinfo.port}`] = {
addr: rinfo.address,
port: rinfo.port
}
console.log(alive)

if (msg == "ready") {
let res = {
second,
msg: "toClient"
}
server.send(JSON.stringify(res), first.port, first.addr)
alive = {}
}


})

server.on("close", (res) => {
console.log("close", res);
})

server.on("error", (err) => {
console.log("error", err);
})

这是个小的 demo,分别对应 client.jsserver.js
A 和 B 通用 client.js,S 部署 server.js 即可

用的时候先在 S 启动 server.js
然后分别在 A B 启动 client.js 即可,连接成功后
若将 S 中的 server.js停掉,A B仍能正常通信,则试验成功


三、TCP打洞

TCP打洞

TCP打洞,打洞的原理其实还是那个原理,就是在NAT上留下记录。

TCP的过程就不细说了,但是TCP打洞会比UDP打洞困难得多 (因为我也没成功)

主要是因为,TCP和UDP不同:

UDP是无链接,同一个端口既可以发送 同时可以监听接收。同一个端口,可以给很多人发送,也可以接收很多人发来的信息

但TCP是套接字,自己的 IP:PORT 是与对方的 IP:PORT 是绑定的,期间端口处于被占用的状态,只能发送或者是收到response回来的信息,不能直接从 发送状态 改为 监听状态,需要先进行断开 或者 设置为端口复用

但在端口复用的过程中,NAT可能会因此而改变端口,其原因可能是因为处于 TIME_WAIT 状态,又或是像 NAT4 一样处理新的TCP 连接

所以 此时想要通过TCP打洞,建立可靠的TCP连接,这时候就要搬出 TCP 状态迁移图
TCP状态迁移图
这个时候,想要提高成功打通的概率,可以让双方互发SYN,以达到 同时打开 的效果
因为一个端口发送失败,可能会导致这个端口在短时间内不能再使用

所以我想到的一个方法就是:
先让 A 向 S 发送正常的 TCP 连接,让 S 知道了 A-NA 的 TCP 端口
然后 S 向 NA 端口 往后的 10个端口 同时循环发送 SYN,A 也向 S 发送10+次 SYN,这样就有极大地概率命中其中一个端口
只要 NA 放行了一个任意一个 S 发来的 SYN,就能进行完整握手,建立起TCP连接通信

当然啦,如果这里的 S 是公网IP,则 A 向 S 建立的连接都可以连上
这里主要是想实现的是 S 向 A 主动建立起的连接
因为如果 S 能主动向A建立连接,S 就可以换成 B-NB
最终的效果其实就是 NA 和 NB 相互不断发送 SYN,以同时打开 NA 和 NB 的通道 建立连接


四、结语

最后再说两句吧…

后来才知道,原来这就是叫 p2p…..好吧是我浅薄了


然后关于UDP打洞的部分,我给我的小伙伴讲,给他讲完了之后
他说了一句:“你这是在建立 UDP 的可靠连接嘛”,然后两个人看着图都沉默了……


然后TCP部分,我确实没能在两个大NAT网上打通,所以我也好奇和请教一下大佬们,有什么好的解决方案实现TCP打洞和建立连接的呢
因为如果光用UDP打洞实现连接之后,简单的通信还好,但如果是用来传输文件就好像不太现实?还要手写校验和重传机制?还要注意MTU、数据包大小?

或者像酷狗、迅雷这种利用客户端的,又是怎么实现的呢

如果可以的话,能不能有个 node.js 的 demo,C语言真的好难顶…


另外:
如果觉得颜色有点花里胡哨的话,之后会在这 NAT打洞 同步更新一篇吧



参考文章