p2p不同内网环境如何通信


P2P

常规CS架构和P2P架构图:

image-20230322164119796

这里就有两个问题了:

  • 现在国内终端基本都是内网下,没有独立的公网IP,如何peerpeer如何通信
  • 怎么发现peer,仍然需要一个中心服务器,或者别的什么东西

NAT类型

:该部分内容完全来自:NAT 原理以及 UDP 穿透

1. 完全锥型

从同一个内网地址端口(192.168.1.1:7777)发起的请求都由 NAT 转换成公网地址端口(1.2.3.4:10000),192.168.1.1:7777 可以收到任意外部主机发到 1.2.3.4:10000 的数据报。

image-20230322173926893

2.受限锥型

受限锥型也称地址受限锥型,在完全锥型的基础上,对 ip 地址进行了限制。

从同一个内网地址端口(192.168.1.1:7777)发起的请求都由 NAT 转换成公网地址端口(1.2.3.4:10000),其访问的服务器为 8.8.8.8:123,只有当 192.168.1.1:77778.8.8.8:123 发送一个报文后,192.168.1.1:7777 才可以收到 8.8.8.8 发往 1.2.3.4:10000 的报文。

image-20230322173954740

3.端口受限锥型

在受限锥型的基础上,对端口也进行了限制。

从同一个内网地址端口(192.168.1.1:7777)发起的请求都由 NAT 转换成公网地址端口(1.2.3.4:10000),其访问的服务器为 8.8.8.8:123,只有当 192.168.1.1:77778.8.8.8:123 发送一个报文后,192.168.1.1:7777 才可以收到 8.8.8.8:123 发往 1.2.3.4:10000 的报文。

image-20230322174024568

4.对称型

在 对称型NAT 中,只有来自于同一个内网地址端口 、且针对同一目标地址端口的请求才被 NAT 转换至同一个公网地址端口,否则的话,NAT 将为之分配一个新的公网地址端口。

如:内网地址端口(192.168.1.1:7777)发起请求到 8.8.8.8:123,由 NAT 转换成公网地址端口(1.2.3.4:10000),随后内网地址端口(192.168.1.1:7777)又发起请求到 9.9.9.9:456,NAT 将分配新的公网地址端口(1.2.3.4:20000)

image-20230322174048646

UDP打洞技术有一个主要的条件:只有当两个NAT都是Cone NAT(或者非NAT的防火墙)时才能工作。

中继

类似frp

UDP打洞

  1. PC1(192.168.1.1:7777) 发送 UDP 请求到 Server(9.9.9.9:1024),此时 Server 可以获取到 PC1 的出口地址端口(也就是 Router1 的出口地址端口) 1.2.3.4:10000,同时 Router1 添加一条映射 192.168.1.1:7777 <=> 1.2.3.4:10000 <=> 9.9.9.9:1024

  2. PC2(192.168.2.1:8888) 同样发送 UDP 请求到 Server,Router2 添加一条映射 192.168.2.1:8888 <=> 5.6.7.8:20000 <=> 9.9.9.9:1024

  3. Server 将 PC2 的出口地址端口(5.6.7.8:20000) 发送给 PC1

  4. Server 将 PC1 的出口地址端口(1.2.3.4:10000) 发送给 PC2

  5. PC1 使用相同的内网地址端口(192.168.1.1:7777)发送 UDP 请求到 PC2 的出口地址端口(Router2 5.6.7.8:20000),此时 Router1 添加一条映射 192.168.1.1:7777 <=> 1.2.3.4:10000 <=> 5.6.7.8:20000,与此同时 Router2 没有关于 1.2.3.4:10000 的映射,这个请求将被 Router2 丢弃

  6. PC2 使用相同的内网地址端口(192.168.2.1:8888)发送 UDP 请求到 PC1 的出口地址端口(Router1 1.2.3.4:10000),此时 Router2 添加一条映射 192.168.2.1:8888 <=> 5.6.7.8:20000 <=> 1.2.3.4:10000,与此同时 Router1 有一条关于 5.6.7.8:20000 的映射(上一步中添加的),Router1 将报文转发给 PC1(192.168.1.1:7777)

  7. 在 Router1 和 Router2 都有了对方的映射关系,此时 PC1 和 PC2 通过 UDP 穿透建立通信。

image-20230322173105367

UDP打洞golang代码

NAT类型测试

image-20230323163119307

python 服务端

import socket
import time

PORT = 9092

server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
address = ("0.0.0.0", PORT)
server_socket.bind(address)
server_socket.settimeout(300)

client_a_b_info = []

def transfer_info():
    
    server_socket.sendto(bytes(("client connected#"+client_a_b_info[1][0]+":"+str(client_a_b_info[1][1])+"\n").encode('utf-8')), client_a_b_info[0])
    time.sleep(1)
    server_socket.sendto(bytes(("client connected#"+client_a_b_info[0][0]+":"+str(client_a_b_info[0][1])+"\n").encode('utf-8')), client_a_b_info[1])
    time.sleep(1)
    print("连接成功,10s后服务端退出")
    time.sleep(10)


while True:
    receive_data, client = server_socket.recvfrom(1024)
    time.sleep(1)
    print("recv udp data: ", receive_data)
    if b"client_upload_info" in receive_data:
        client_a_b_info.append(client)
        if len(client_a_b_info) == 2:
            transfer_info()
            break

测试:

服务端:
$ python3 server.py 
recv udp data:  b'client_upload_info\n'
recv udp data:  b'client_upload_info\n'
连接成功,10s后服务端退出

测试:
$ nc -nu 127.0.0.1 9092
client_upload_info
client_upload_info
client_upload_info
client_upload_info
client connected#127.0.0.1:6840
client connected#127.0.0.1:6840

Golang客户端

基本就是上面步骤里面的实现:

package udp_punch_hole

import (
    "fmt"
    "net"
    "strconv"
    "strings"
    "time"
)

var serverIp = net.ParseIP("1.2.3.4")
var addr = &net.UDPAddr{IP: serverIp, Port: 9092}

func sendUDPMsg(msg string, conn *net.UDPConn) error {
    _, err := conn.Write([]byte(msg))
    return err
}

func recvAnotherPeerInfo(serverConn *net.UDPConn) (conn *net.UDPConn, err error) {
    localAddrStr := serverConn.LocalAddr().String()
    //localAddrIp := net.ParseIP(strings.Split(localAddrStr, ":")[0])
    localAddrPort, err := strconv.Atoi(strings.Split(localAddrStr, ":")[1])
    localAddr := &net.UDPAddr{IP: net.IPv4zero, Port: localAddrPort}

    fmt.Printf("localAddr: %+v\n", localAddr)
    if err != nil {
        return nil, err
    }
    data := make([]byte, 1024)
    for {
        n, _, err := serverConn.ReadFromUDP(data)
        if err != nil {
            return nil, err
        }
        fmt.Printf("RECV: %v\n", string(data[:n]))
        peerInfo := strings.Split(string(data[:n]), "#")[0]
        peerAddrStr := strings.TrimSpace(strings.Split(string(data[:n]), "#")[1])
        if peerInfo != "client connected" {
            continue
        }

        println("对方ip:addr ", peerAddrStr)
        println("自己ip: ", localAddr.String())
        peerIp := strings.Split(peerAddrStr, ":")[0]
        peerPort, _ := strconv.Atoi(strings.Split(peerAddrStr, ":")[1])
        var peerAddr = &net.UDPAddr{IP: net.ParseIP(peerIp), Port: peerPort}

        serverConn.Close()
        udpConn, err := net.DialUDP("udp", localAddr, peerAddr)
        if err != nil {
            println(": ", err.Error())
            return nil, err
        }
        err = sendUDPMsg("for trust usage", udpConn)
        if err != nil {
            return nil, err
        }
        return udpConn, nil
    }
}

func transferMsg(udpConn *net.UDPConn) {
    time.Sleep(1 * time.Second)
    go func() {
        data := make([]byte, 1024)
        for {
            println("waiting...")
            n, _, err := udpConn.ReadFromUDP(data)
            println("done....")
            if err != nil {
                return
            }

            print("recv: ", string(data[:n]))
        }
    }()
    for {
        err := sendUDPMsg("test\n", udpConn)
        if err != nil {
            println(err.Error())
        } else {
            println("发送成功:", udpConn.RemoteAddr().String())
        }
        time.Sleep(3 * time.Second)
    }
}

func udp_hole() error {
    udpConn, _ := net.DialUDP("udp", nil, addr)
    err := sendUDPMsg("client_upload_info.haha", udpConn)
    if err != nil {
        println(err.Error())
        return err
    }
    in, err := recvAnotherPeerInfo(udpConn)
    if err != nil {
        println(err.Error())
        return err
    }
    transferMsg(in)
    return nil
}

不同主机测试:

func Test_udp_hole(t *testing.T) {
    udp_hole()
}

结果:

=== RUN   Test_udp_hole
localAddr: 0.0.0.0:52952
RECV: client connected#202.107.195.215:59791

对方ip:addr  202.107.195.215:59791
自己ip:  0.0.0.0:52952
waiting...
发送成功: 202.107.195.215:59791
done....
recv: for trust usagewaiting...
done....
recv: test
waiting...
发送成功: 202.107.195.215:59791
done....
recv: test
waiting...
发送成功: 202.107.195.215:59791
done....
recv: test
waiting...
发送成功: 202.107.195.215:59791

遇到的问题

想偷个懒,直接一个主机跑,收不到数据:

func Test_udp_hole(t *testing.T) {
    go udp_hole()
    time.Sleep(time.Second * 3)
    go udp_hole()
    time.Sleep(time.Hour)
}

UDP打洞为何不能一个主机下?或者我代码里一个主机下为何不能打洞

没找到原因,TODO

参考文献

P2P通信原理与实现(C++)

Building a BitTorrent client from the ground up in Go

NAT 原理以及 UDP 穿透