为了能够进行udp打洞需要一台公网服务器,具体打洞步骤如下:

  • clientA固定udp包发送端口1198,clientB股东udp包发送端口1198。
  • clientA向server发起请求(此时NAT服务器绑定内外网端口),server端记录下clientA的外网IP和端口(1.1.1.1:111)
  • clientB向server发起请求(此时NAT服务器绑定内外网端口),server端记录下clientB的外网IP和端口(2.2.2.2:222)
  • 当clientA和clientB都接入后,server端把clientA的外网信息告知clientB,把clientB的外网信息告知clientA
  • 开始打洞,clientA向2.2.2.2:222发送第一个udp包,NAT B不知道将这个包转发给谁,丢弃。clientB向1.1.1.1:111发送第一个udp包,NAT A将该包转发给clientA。此时打洞完成。clientA和clientB建立通讯。

以下为代码实现:

main.go

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
package main

import (
	"natbypass/client"
	"natbypass/server"
	"os"
)

func main() {
	args := os.Args
	if "server" == args[1] {
		server.StartServer()
	} else if "client" == args[1] {
		client.StartClient(args[2], args[3], args[4])
	}
}

server.go

 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
package server

import (
	"log"
	. "natbypass/utils"
	"net"
	"strconv"
)

func StartServer() {
	addr, err := net.ResolveUDPAddr("udp", "0.0.0.0:1199")
	CheckErr(err)
	conn, err := net.ListenUDP("udp", addr)
	CheckErr(err)
	defer conn.Close()
	hostA := ""
	hostB := ""
	for {
		data := make([]byte, 1024)
		_, srcIP, err := conn.ReadFromUDP(data)
		CheckErr(err)
		log.Println("srr ip port:", srcIP.Port)
		// peerAddr := conn.RemoteAddr().String()
		if "" == hostA {
			hostA = srcIP.IP.String() + ":" + strconv.Itoa(srcIP.Port)
			log.Println("A source IP:", hostA)
		} else if "" == hostB {
			hostB = srcIP.IP.String() + ":" + strconv.Itoa(srcIP.Port)
			log.Println("B source IP:", hostB)

			addrA, err := net.ResolveUDPAddr("udp", hostA)
			CheckErr(err)
			conn.WriteToUDP([]byte(hostB), addrA)
			addrB, err := net.ResolveUDPAddr("udp", hostB)
			CheckErr(err)
			conn.WriteToUDP([]byte(hostA), addrB)

			log.Println("IP data send finish!")

			hostA = ""
			hostB = ""
		}

	}
}

client.go

 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
package client

import (
	"fmt"
	"log"
	. "natbypass/utils"
	"net"
	"time"
)

func StartClient(clientID, clientAddrStr, serverAddrStr string) {
	// svrAddr, err := net.ResolveUDPAddr("udp", serverAddrStr)
	// CheckErr(err)
	srcAddr, _ := net.ResolveUDPAddr("udp", clientAddrStr)
	serverAddr, _ := net.ResolveUDPAddr("udp", serverAddrStr)
	conn, err := net.DialUDP("udp", srcAddr, serverAddr)
	CheckErr(err)
	_, err = conn.Write([]byte("first msg" + clientID))
	CheckErr(err)

	data := make([]byte, 1024)
	n, err := conn.Read(data)
	CheckErr(err)
	conn.Close()
	peerAddrStr := string(data[:n])
	log.Println("Peer Addr:", peerAddrStr)
	peerAddr, err := net.ResolveUDPAddr("udp", peerAddrStr)
	CheckErr(err)
	peerConn, err := net.DialUDP("udp", srcAddr, peerAddr)
	CheckErr(err)
	go func() {
		for {
			_, err := peerConn.Write([]byte("msg from " + clientID))
			if CheckErr(err) {
				log.Println("send msg successfully")
			}
			time.Sleep(time.Second)
		}
	}()

	for {
		peerData := make([]byte, 1024)
		n, _, err := peerConn.ReadFromUDP(peerData)
		CheckErr(err)
		fmt.Println(string(peerData[:n]))
	}

}

这一份代码实现只支持全锥型(Full Cone)NAT,且不能是受限锥型(Restricted Cone)和端口受限锥型(Port Restricted Cone),但可以对第一个用于打洞的udp包稍加改动建立通信。当有一方为对称型(Symmetric)也是可以用想办法打通的。但双端均为对称型NAT时或一端为端口受限型另一端为对称型则不可以打通。

本端类型 对端类型 是否可打通
全锥型 全锥型
全锥型 受限锥型
全锥型 端口受限锥型
全锥型 对称型
受限锥型 受限锥型
受限锥型 端口受限锥型
受限锥型 对称型
端口受限锥型 端口受限锥型
端口受限锥型 对称锥型 ×
对称锥型 对称锥型 ×

关于NAT的四种类型可以参考以下这篇文章: NAT的四种类型