Loading... # UDP打洞 指用于NAT穿越的一种技术,也即`内网穿透`技术。 ## 背景介绍 ### IPv4枯竭 现阶段大多网络设备是用IPv4地址作为互联网通信地址,IPv4使用32位(4字节)地址,因此只有4,294,967,296(2<sup>32</sup>)个地址可供使用,其间有一些地址是为特殊用途而保留的,如专用网络(约1800万个地址)和多播网络(约2.7个地址),这将减少了互联网中可用的地址。随着时间的变化,接入互联网的设备越来越多,出现了IPv4地址的枯竭的问题。 ### NAT技术 NAT全称:`Network Address Translation`,网络地址转换。是一种在IP数据包通过路由器或防火墙时重写来源IP或目的IP的技术。**该技术可以实现多个网络设备共用一个对外IP或公网IP**。 因此NAT技术可以缓解IPv4枯竭的问题,目前基本上家用宽带运营商都使用NAT技术为用户赋予IP地址,通常是一层或一栋楼共用一个公网IP(详情根据当地运营商而定)。 <!-- NAT内网设备要访问公网IP时,NAT设备如路由器,将内网设备的MAC地址和对应的内网IP保存在路由表中,将内网设备的请求地址映射到一个随机端口。路由器将路由器的公网IP作为`来源IP`,映射的端口作为`来源端口`,封包去请求目标公网设备,公网设备收到请求后对请求进行回应,公网设备根据请求中的`来源IP`和`来源端口`进行回应,路由器收到回应后会根据端口去查找内网设备地址再发给内网设备。 --> ![NAT.jpg][1] 假如现有内网设备`A`、NAT(1.1.1.2)和公网服务器(1.1.1.1) 1. A向服务器发送请求包。 2. A请求包经过NAT,NAT从中得到请求的目的地址(1.1.1.1)。 3. NAT保存A的地址和A的目的地址(1.1.1.1),并给A随机分配映射一个端口(1234)。 4. NAT将A请求包的源地址(A的地址)和源端口,替换为NAT的公网IP和分配给A的端口(1234)。 5. NAT将请求包向公网服务器(1.1.1.1)发出。 6. 服务器(1.1.1.1)接收到请求包并返回回应包。 7. NAT接收到回应包,并从中得到回应的端口(1234)。 8. NAT找查记录,发现与端口(1234)对应的设备是A,并且回应包发送者的IP(1.1.1.1)也在其中。(若两者其中一个不满足就会将此包丢弃) 9. NAT将回应包的目的IP(1.1.1.2)和目的端口(1234)改为A的地址。 10. NAT将回应包发送给A。 默认情况下在NAT内网的设备可以主动访问公网IP的设备,但公网IP设备却不能主动访问NAT内网的设备(因为NAT没有记录,对应上述例子第8步). ## 打洞 因为NAT技术的原因,造成两个NAT内网的设备无法直接通信(例如:不能直接访问没有公网IP的电脑)。 **打洞**顾名思义,就是在NAT内网中打出一个洞,让两个不同内网的设备直接访问。 ### 原理 从上述NAT技术的第4步和第9步,可以看出NAT需要将内网设备映射到一个端口并保存其访问的公网地址和端口,才可以使内网设备和公网设备通信。所以打洞给关键一步就是让两个设备相互访问对方公网对应的IP和端口,让自己的NAT保存记录。 假如现有内网设备A、NAT A(1.1.1.2)、内网设备B、NAT B(1.1.1.3)以及公网服务器(1.1.1.1),内网设备A和内网设备B想要直接通信。 ![UDPASS.jpg][2] 1. A和B向服务器访问,NAT A和NAT B分别为AB分配映射端口`4530`与`6540`。 2. 服务器收到请求后,保存IP和端口(`1.1.1.2:4530`,`1.1.1.3:6540`)。 3. 服务器向A返回B的IP和端口,向B返回A的IP和端口。 4. A向B的地址发送数据(NAT B因为没有A的地址记录会丢弃A的包,但是这一步为的是让NAT A记住B的地址,让NAT A以后将带有B地址的包转发给A)。 5. B再向A发送数据。(这一步也是让NAT B记住A的地址,但上一步NAT A已经有了B的地址,所以这一次B发送的数据可以正常到达A)。 6. 从此A和B就可以直接双向发送数据,洞就被打通了。 ### UDP UDP全称:User Datagram Protocol,用户数据报协议。是一种无连接的协议。 UDP负责将数据发送出去,但对方有没有接收到就不是UDP责任了(就算是错误的IP或端口,UDP也照样发送),因此也是一种不可靠的协议。 但是就很适合使用UDP来做NAT打洞,因为在上述原理第4步的过程中,NAT B没有A的记录就将A的包给丢弃了,在计划里这一步丢包是必然的,而UDP不管对方收到或丢弃。若是用其他可靠协议比如TCP,可能会造成打洞失败的情况,而解决办法就是修改系统内核,但这又非常麻烦。 ## 代码实现 这里使用Golang进行网络编程。 客户端之所以使用`net.ListenUDP()`而不是`net.DialUDP()`,是因为`net.ListenUDP()`的`*UDPConn`是`unconnected`,而`net.DialUDP()`的`*UDPConn`是`connected`的。 如果`*UDPConn`是`connected`,读写方法是`Read`和`Write`。 如果`*UDPConn`是`unconnected`,读写方法是`ReadFromUDP`和`WriteToUDP`(以及`ReadFrom`和`WriteTo`)。 使用`ReadFromUDP`和`WriteToUDP`进行读写可以指定读写任何UDP地址,而且`net.ListenUDP()`还可以固定本机UDP端口不被系统更换。 ### 服务端demo ```go type j struct { Name string `json:"name"` //客户端名 Addr string `json:"addr"` //地址 To string `json:"to"` //要连接的客户端名 } dev := make(map[string]string) //客户端地址 addr, _ := net.ResolveUDPAddr("udp", "0.0.0.0:7091") //服务器端口为7091 conn, err := net.ListenUDP("udp", addr) if err != nil { fmt.Println(err) os.Exit(1) } defer conn.Close() for { data := make([]byte, 512) n, remoteAddr, err := conn.ReadFromUDP(data) //获取客户端地址 if err != nil { fmt.Println("f", err) return } var J j json.Unmarshal(data[:n], &J) fmt.Println(J) dev[J.Name] = remoteAddr.String() //客户端注册 if !strings.EqualFold(J.To, "")&& dev[J.To]!="" { outA,_:=json.Marshal(&j{ Name: J.To, Addr: dev[J.To], }) outB,_:=json.Marshal(&j{ Name: J.Name, Addr: remoteAddr.String(), }) conn.WriteToUDP(outA,remoteAddr) adr,_:=net.ResolveUDPAddr("udp",dev[J.To]) conn.WriteToUDP(outB,adr) } } ``` ### 客户端demo ```go type j struct { Name string `json:"name"` Addr string `json:"addr"` To string `json:"to"` } var name = flag.String("name", "", "") //客户端名 var to = flag.String("to", "", "") //要连接的客户端 var msg = flag.String("msg", "", "") //发送消息 var server_ip = flag.String("server", "1.1.1.1", "") //服务器IP var server_port = flag.Int("sport", 9091, "") //服务器端口 flag.Parse() conn, _ := net.ListenUDP("udp", &net.UDPAddr{ IP: net.ParseIP("0.0.0.0"), Port: 9091, }) defer conn.Close() put, _ := json.Marshal(&j{ Name: *name, To: *to, }) conn.WriteToUDP(put, &net.UDPAddr{ IP: net.ParseIP(*server_ip), Port: *server_port, }) res := make([]byte, 128) n, _, _ := conn.ReadFromUDP(res) var J j json.Unmarshal(res[:n], &J) fmt.Println(string(res[:n])) if *to != "" { time.Sleep(1 * time.Second) } adr, _ := net.ResolveUDPAddr("udp", J.Addr) conn.WriteToUDP([]byte("qwe"), adr) n, _, _ = conn.ReadFromUDP(res) fmt.Println(string(res[:n])) ``` 使用: A运行:`./main -name A` B运行:`./main -name B -to A -msg "qwe"` ## 参考 [1] [NAT - wikipedia](https://zh.wikipedia.org/wiki/%E7%BD%91%E7%BB%9C%E5%9C%B0%E5%9D%80%E8%BD%AC%E6%8D%A2) [2] [UDP - wikipedia](https://zh.wikipedia.org/wiki/%E7%94%A8%E6%88%B7%E6%95%B0%E6%8D%AE%E6%8A%A5%E5%8D%8F%E8%AE%AE) [1]: https://rehtt.com/usr/uploads/2021/02/2491971208.jpg [2]: https://rehtt.com/usr/uploads/2021/02/3444258407.jpg Last modification:April 6, 2021 © Allow specification reprint Support Appreciate the author AliPayWeChat Like 1 如果觉得我的文章对你有用,请随意赞赏