UDP打洞
指用于NAT穿越的一种技术,也即内网穿透
技术。
背景介绍
IPv4枯竭
现阶段大多网络设备是用IPv4地址作为互联网通信地址,IPv4使用32位(4字节)地址,因此只有4,294,967,296(232)个地址可供使用,其间有一些地址是为特殊用途而保留的,如专用网络(约1800万个地址)和多播网络(约2.7个地址),这将减少了互联网中可用的地址。随着时间的变化,接入互联网的设备越来越多,出现了IPv4地址的枯竭的问题。
NAT技术
NAT全称:Network Address Translation
,网络地址转换。是一种在IP数据包通过路由器或防火墙时重写来源IP或目的IP的技术。该技术可以实现多个网络设备共用一个对外IP或公网IP。
因此NAT技术可以缓解IPv4枯竭的问题,目前基本上家用宽带运营商都使用NAT技术为用户赋予IP地址,通常是一层或一栋楼共用一个公网IP(详情根据当地运营商而定)。
假如现有内网设备A
、NAT(1.1.1.2)和公网服务器(1.1.1.1)
- A向服务器发送请求包。
- A请求包经过NAT,NAT从中得到请求的目的地址(1.1.1.1)。
- NAT保存A的地址和A的目的地址(1.1.1.1),并给A随机分配映射一个端口(1234)。
- NAT将A请求包的源地址(A的地址)和源端口,替换为NAT的公网IP和分配给A的端口(1234)。
- NAT将请求包向公网服务器(1.1.1.1)发出。
- 服务器(1.1.1.1)接收到请求包并返回回应包。
- NAT接收到回应包,并从中得到回应的端口(1234)。
- NAT找查记录,发现与端口(1234)对应的设备是A,并且回应包发送者的IP(1.1.1.1)也在其中。(若两者其中一个不满足就会将此包丢弃)
- NAT将回应包的目的IP(1.1.1.2)和目的端口(1234)改为A的地址。
- 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想要直接通信。
- A和B向服务器访问,NAT A和NAT B分别为AB分配映射端口
4530
与6540
。 - 服务器收到请求后,保存IP和端口(
1.1.1.2:4530
,1.1.1.3:6540
)。 - 服务器向A返回B的IP和端口,向B返回A的IP和端口。
- A向B的地址发送数据(NAT B因为没有A的地址记录会丢弃A的包,但是这一步为的是让NAT A记住B的地址,让NAT A以后将带有B地址的包转发给A)。
- B再向A发送数据。(这一步也是让NAT B记住A的地址,但上一步NAT A已经有了B的地址,所以这一次B发送的数据可以正常到达A)。
- 从此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
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
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
[2] UDP - wikipedia