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(详情根据当地运营商而定)。

NAT.jpg

假如现有内网设备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

  1. A和B向服务器访问,NAT A和NAT B分别为AB分配映射端口45306540
  2. 服务器收到请求后,保存IP和端口(1.1.1.2:45301.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()*UDPConnunconnected,而net.DialUDP()*UDPConnconnected的。

如果*UDPConnconnected,读写方法是ReadWrite

如果*UDPConnunconnected,读写方法是ReadFromUDPWriteToUDP(以及ReadFromWriteTo)。

使用ReadFromUDPWriteToUDP进行读写可以指定读写任何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

Last modification:April 6, 2021
如果觉得我的文章对你有用,请随意赞赏