P2P 网络技术详解

一、P2P技术概述1.1 什么是P2P技术P2P的定义与特点P2P(Peer-to-Peer,点对点)技术是一种网络通信模型,它允许网络中的参与者(节点)直接相互通信和共享资源,而无需依赖中央服务器作为中介。在P2P网络中,每个节点既可以是资源的提供者(服务器),也可以是资源的消费者(客户端)。

P2P网络的主要特点:

去中心化:没有中央控制节点,网络中的每个节点地位平等自组织性:网络能够自动适应节点的加入和离开资源共享:计算能力、存储空间和网络带宽等资源可以在节点间共享可扩展性:随着节点数量增加,网络整体资源和服务能力也相应增加冗余性:数据和服务通常在多个节点上复制,提高了系统的容错能力与传统C/S架构的区别特性传统C/S(客户端/服务器)架构P2P(点对点)架构中心化程度高度中心化,服务器作为核心去中心化,节点平等资源分布资源集中在服务器端资源分布在所有参与节点可扩展性受限于服务器性能,扩展成本高随节点增加自然扩展,成本分摊稳定性服务器故障导致整体服务中断单个节点故障影响有限带宽利用服务器带宽易成为瓶颈带宽需求分散到各节点实现复杂度相对简单,架构清晰较为复杂,需处理各种网络环境安全控制集中式安全控制,相对容易分散式安全控制,挑战较大P2P网络的优势与挑战优势:

高效利用资源:充分利用边缘节点的计算能力、存储空间和网络带宽降低成本:减少对中央服务器的依赖,降低基础设施和运维成本提高可靠性:去中心化结构消除了单点故障风险自然扩展:网络容量随着参与节点的增加而自然增长抗审查性:分布式特性使得网络更难被审查或关闭挑战:

网络穿透问题:NAT和防火墙环境下的连接建立困难节点发现与管理:如何高效发现和管理动态变化的节点安全与信任:缺乏中央权威,节点间的信任建立机制复杂服务质量保障:节点性能和连接质量参差不齐,难以保证一致的服务质量法律与合规:在某些地区面临法律监管挑战,特别是涉及版权内容时资源不均衡:“搭便车"现象(部分节点只消费不贡献)影响网络效率1.2 P2P技术的发展历程早期P2P应用(1999-2005)Napster时代(1999-2001):

1999年,Shawn Fanning创建了Napster,这是第一个广为人知的P2P文件共享应用Napster采用了中心化索引 + P2P传输的混合架构,用户通过中央服务器查找音乐文件,但文件传输直接在用户之间进行2001年因版权诉讼而被迫关闭,但它开创了P2P文件共享的先河去中心化探索(2000-2003):

Gnutella(文件共享网络):2000年发布,首个完全去中心化的P2P网络,采用泛洪(Flooding)搜索机制Freenet:2000年推出,注重匿名性和抗审查能力的 P2P 网络Kazaa/FastTrack:2001年推出,引入了超级节点(Supernode)概念,形成两级架构eDonkey/eMule:2002年出现,引入了服务器网络和文件哈希技术BitTorrent革命(2003-2005):

2001年,Bram Cohen设计了BitTorrent协议,2003年开始广泛应用创新性地引入了分片下载、稀有优先和互惠机制(tit-for-tat)显著提高了大文件分发效率,至今仍是最成功的P2P协议之一现代P2P技术的演进(2005-2020)结构化P2P网络(2005-2010):

**分布式哈希表(DHT)**技术成熟:Kademlia、Chord、Pastry等算法广泛应用BitTorrent网络引入DHT,摆脱了对Tracker服务器的依赖学术界和工业界对P2P网络路由和查找算法进行了深入研究P2P流媒体时代(2008-2015):

P2P技术在视频直播领域得到应用:PPTV、PPS影音等P2P-CDN混合架构出现,如迅雷看看、阿里云PCDNWebRTC(2011年发布)标准化了浏览器中的P2P通信能力区块链与去中心化应用(2009-至今):

2009年比特币网络上线,将P2P技术与密码学、共识机制相结合2015年以太坊推出,支持智能合约,催生了大量去中心化应用(DApps)IPFS(星际文件系统)在2015年推出,致力于构建去中心化的文件存储和访问系统未来发展趋势Web3.0与去中心化互联网:

基于区块链的去中心化身份和数据所有权去中心化存储和计算平台的普及用户直接控制和变现自己的数据和创作内容边缘计算与P2P结合:

利用边缘设备的闲置计算资源构建分布式计算网络降低云计算中心的负载和延迟物联网设备间的直接P2P通信和协作5G/6G网络中的应用:

高带宽、低延迟网络环境为P2P应用提供更好基础设备直连(Device-to-Device,D2D)通信技术与P2P结合移动边缘计算(MEC)中的P2P资源共享安全与隐私增强:

零知识证明等密码学技术在P2P网络中的应用去中心化隐私保护机制的发展抗量子计算的P2P安全协议研究二、NAT类型详解3.1 NAT基础知识NAT(Network Address Translation,网络地址转换)是一种在IP数据包通过路由器或防火墙时重写来源IP地址或目的IP地址的技术。它主要用于解决IPv4地址短缺问题,允许多个设备通过单个公网IP地址访问互联网。

NAT的主要作用:

地址复用:允许多个私有IP地址共享一个公网IP地址网络安全:隐藏内部网络结构,防止外部直接访问内部主机负载均衡:在某些实现中用于分发流量端口复用:通过端口号区分不同内部主机的连接NAT的工作原理NAT通常部署在网络边界设备(如路由器)上,当内部主机向外部发送数据包时,NAT设备会:

来源地址转换:将私有来源IP和端口替换为公网IP和临时端口维护转换表:记录转换关系(内部IP:端口 -> 公网IP:端口)转发数据包:将修改后的数据包发送到外部网络反向转换:当响应数据包返回时,根据转换表还原为内部IP和端口基本NAT转换流程示例:

内部主机A (192.168.1.10:1234) 发送数据到外部服务器B (8.8.8.8:53)NAT设备将来源改为公网IP (203.0.113.1:5678)响应从B返回到203.0.113.1:5678NAT根据表转换为192.168.1.10:1234并转发公网IP与私网IP公网IP(Public IP):

由IANA分配的全球唯一IP地址可在互联网上直接路由示例:203.0.113.1(IPv4),2001:db8::1(IPv6)特点:唯一性、全球可达性、需注册分配私网IP(Private IP):

RFC 1918定义的保留地址段,不在互联网上路由

常见范围:10.0.0.0/8 (10.0.0.0 - 10.255.255.255)172.16.0.0/12 (172.16.0.0 - 172.31.255.255)192.168.0.0/16 (192.168.0.0 - 192.168.255.255)特点:可重复使用、仅限内部网络、需NAT转换才能访问互联网公网IP资源有限,导致NAT的广泛应用,而私网IP允许多个网络独立使用相同地址空间。

3.2 NAT类型分类

3.3 NAT类型检测原理

四、不同网络环境下的P2P连接原理4.1 公网 - 公网

在当前网络模型中,客户端 A 和客户端 B 都位于公网。客户端 A 和 客户端 B 通过以下步骤即可建立 P2P 连接:

客户端 A 向中心服务器上报自身信息,并获取客户端 B 信息;客户端 B 向中心服务器上报自身信息,并获取客户端 A 信息;(PS: 忽略两端获取对端信息时的时间差)客户端 A 根据从中心服务器获取的信息发现 B 具有公网地址,于是 A 直接向 B 发起连接;(PS: 可以互换连接顺序)客户端 B 根据从中心服务器获取的信息发现 A 具有公网地址,因此 B 等待 A 进行连接;4.2 NAT - 公网

在当前网络模型中,客户端 B 位于公网,有公网 IP,客户端 A 位于任意 NAT 后。客户端 A 和 客户端 B 通过以下步骤即可建立 P2P 连接:

客户端 A 向中心服务器上报自身信息,并获取客户端 B 信息;客户端 B 向中心服务器上报自身信息,并获取客户端 A 信息;(PS: 忽略两端获取对端信息时的时间差)客户端 B 根据从中心服务器获取的信息发现 A 位于 NAT 后,因此 B 等待 A 进行连接;客户端 A 根据从中心服务器获取的信息发现 B 位于公网,于是 A 直接向 B 发起连接;4.3 客户端位于同一NAT后

在当前网络模型中,客户端 A 和客户端 B 位于同一任意 NAT 后。客户端 A 和 客户端 B 通过以下步骤即可建立 P2P 连接:

客户端 A 向中心服务器上报自身信息,并获取客户端 B 信息;客户端 B 向中心服务器上报自身信息,并获取客户端 A 信息;(PS: 忽略两端获取对端信息时的时间差)客户端 A 根据从中心服务器获取的信息发现 B 的公网地址和自身相同,猜测 B 可能与自己位于同一内网中,于是 A 尝试直接向 B 发起连接;(PS: 可以互换连接顺序)客户端 B 根据从中心服务器获取的信息发现 A 的公网地址和自身相同,猜测 A 可能与自己位于同一内网中,因此 B 等待 A 进行连接;4.4 客户端分属与不同NAT下

在当前网络模型中,客户端 A 和客户端 B 都位于 NAT 后。客户端 A 和 客户端 B 能否建立 P2P 连接和各自所属 NAT 类型有关。

4.4.1 任意 NAT - (Full Cone NAT或Restricted Cone NAT)

Full Cone NAT、Restricted Cone NAT和Port Restricted Cone NAT都有同样的映射规则:本地地址和端口不变时,映射到 NAT 上的端口不变。

当一端位于 Full Cone NAT或Restricted Cone NAT 下,另一端为任意 NAT 时,通过以下方式可以建立 P2P 连接:

客户端 A 向中心服务器上报自身信息,并获取客户端 B 信息;客户端 B 向中心服务器上报自身信息,并获取客户端 A 信息;(PS: 忽略两端获取对端信息时的时间差)客户端 A 持续向客户端 B 在 NAT 网关2 上映射的公网地址和端口发送数据。对于 Restricted Cone NAT,由于NAT 网关2上还没有放开相应的过滤规则,因此前面客户端A发向客户端B的部分数据包会被丢失;客户端 B 向客户端 A 的公网地址和端口发送数据,用以更新 NAT 网关2的过滤规则;当NAT 网关2的过滤规则被刷新后,客户端 A 发向客户端B的数据便会被 NAT 网关2 接收,并转发给客户端 B;接收数据时,客户端 B 就会知道客户端在 NAT 网关1上映射的端口和地址,此时客户端 B向NAT 网关1上映射的端口和地址发包,客户端A即可收到。此时客户端 A 和客户端 B 成功建立 P2P连接。4.4.2 Easy NAT - Easy NATEasy NAT 代指 RFC3489 所定义的 Full Cone NAT、Restricted Cone NAT、Port Restricted Cone NAT。

当两端都位于 Easy NAT 下时,通过以下方式可以建立 P2P 连接(任意 NAT - (Full Cone NAT或Restricted Cone NAT) 流程类似):

客户端 A 向中心服务器上报自身信息,并获取客户端 B 信息;客户端 B 向中心服务器上报自身信息,并获取客户端 A 信息;(PS: 忽略两端获取对端信息时的时间差)客户端 A 持续向客户端 B 在 NAT 网关2 上映射的公网地址和端口发送数据,由于NAT 网关2上还没有放开相应的过滤规则,因此前面客户端A发向客户端B的部分数据包会被丢失;客户端 B 向客户端 A 的公网地址和端口发送数据,用以更新 NAT 网关2的过滤规则;当NAT 网关2的过滤规则被刷新后,客户端 A 发向客户端B的数据便会被 NAT 网关2 接收,并转发给客户端 B;接收数据时,客户端 B 就会知道客户端在 NAT 网关1上映射的端口和地址,此时客户端 B向NAT 网关1上映射的端口和地址发包,客户端A即可收到。此时客户端 A 和客户端 B 成功建立 P2P连接。4.4.3 Symmetric NAT - Port Restricted Cone NAT

当客户端 A 位于 Symmetric NAT 下,客户端 B 位于 Port Restricted Cone NAT 时,通过以下方式可以建立 P2P 连接:

客户端 A 向中心服务器上报自身信息,并获取客户端 B 信息;客户端 B 向中心服务器上报自身信息,并获取客户端 A 信息;(PS: 忽略两端获取对端信息时的时间差)客户端 A 持续向客户端 B 在 NAT 网关2 上映射的公网地址和端口发送数据。由于 NAT 网关2 没有放开相应的过滤规则,因此客户端 A 发往客户端 B 的数据包会被 NAT 网关2拦截,无法到达客户端 B;;由于客户端 A 在NAT 网关1上映射的端点(IP:Port 中 Port 未知),因此客户端 B 无法向一个明确的端点发送数据包来更新 NAT 网关2 过滤规则。此时就需要通过以下几种方案来让碰撞,让客户端 A 发向客户端 B 的包顺利通过 NAT 网关2。

全端口开放

虽然我们不知道客户端 A 在映射NAT 网关1上映射的端口是多少,但是我们知道,他映射的端口一定是 1024 - 65535 内其中一个,并且一定不是 A 连接中心服务器时使用的端口。

我们可以顺序构造目的端口为 1024 - 65535 的短 TTL 包(短 TTL 包可以让包不走到公网,仅仅用于打开防火墙规则,以免被识别为 Dos 攻击),让 NAT 网关2 开放所有可能端口(相当于将 Port Restricted Cone NAT 变为 Restricted Cone NAT)。

但是经过实际测试发现该方法效果不佳,主要有以下原因:

需要构造大量数据包:平均需要发包 32256 个包 才能碰撞到 客户端 A 在 NAT 网关1上映射的端口。假设客户端 B 的发包速率为 100 p/s,那么就需要五分半才能碰撞到端口;容易触发运营商 QoS 限制:经过实际测试,发现一定时间内无效数据包过多时,运营商会对客户端 B 的包进行大量丢弃,导致丢包率上升。严重情况下运营商会直接全部丢弃客户端 B 的数据包。端口预测

在部分 NAT 上,端口映射具有一定规律 。

比如发往目的 IP 1 时,映射的端口为 22001;发往目的 IP 2 时,映射的端口为 22002;那么我们可以猜测,发往目的 IP 3时使用的端口可能为 22003。

使用此种方案,需要客户端 A 向多个服务器请求来确认自身映射端口,从这些映射的端口中找到可能存在的端口变化规律。

此种方法具有一定可行性,但和 NAT 行为有关,不是一个通用解决方案。

生日攻击

在前面的“全端口开放”方法中,客户端 A 只在 NAT 网关1 上映射了一个端口。但是实际情况下,客户端 A 可以在 NAT 网关1 上映射多个端口。

根据概率论的 生日悖论,可以写发现客户端 A 映射的端口、客户端 B 发包数量与成功概率之间的关系,公式如下所示:

$$

\begin{array}{c}

P_{success} = 1 - \frac{C_{64,512}^{ports} \times C_{64,512-ports}^{packets }}{C_{64,512}^{ports} \times C_{64,512}^{packets}}

\end{array}

$$根据上面的公式绘制出三维图如图所示:

根据函数图我们可以发现,当客户端 A 映射在公网上的端口越多时,建立连接所需的发包数越少,下表中列举了部分数据:

使用端口数\发包数50%80%90%99%10432095901326823807508882043290356751004461030146829022002235177381467300149345493981根据表中数据可以发现:假设客户端 A 占用100个端口,客户端 B 以 100 p/s 的速度进行探测,那么要达到50%的概率仅需 6s,达到 99% 也只需 29s!

根据上面的数据分析可以发现生日攻击是在 Symmetric NAT - Port Restricted Cone NAT 进行 P2P 打洞时一个较为优秀的方案。

4.4.4 Symmetric NAT - Symmetric NAT当两端都是 Symmetric NAT 时,复杂度比 Symmetric NAT - Port Restricted Cone NAT 有了成倍的增长。

假设使用上面的“全端口开发”方案,那么就需要发包 4,161,798,144 次,耗费时间需要一年以上;

即使是使用“生日攻击”方案,一端打开 256 个端口,要达到 50% 的概率需要 54,000 次发包,按照 100 p/s 的发包速率需要9分钟;达到 99% 的成功率需要 170,000 次发包,时间上需要30分钟左右。

即使时间上可以忍受,但 NAT 网关却无法忍受这么多次的发包行为。因为每发一次包,就需要在 NAT 的 session 表上记录一条,想创建一条成功的连接,大部分情况下都会打爆 NAT 的session 表。

因此对于 Symmetric NAT - Symmetric NAT 网络类型,连接建立方案只能选择中继服务器模式。

4.5 客户端位于同一大 NAT 下,但不属于同一内网

在当前网络模型中,客户端 A 和客户端 B 位于同一任意大 NAT 后,但是分属于大 NAT 下的两个小子网中。客户端 A 和 客户端 B 需要通过以下步骤建立连接:

客户端 A 向中心服务器上报自身信息,并获取客户端 B 信息;客户端 B 向中心服务器上报自身信息,并获取客户端 A 信息;(PS: 忽略两端获取对端信息时的时间差)客户端 A 根据从中心服务器获取的信息发现 B 的公网地址和自身相同,猜测 B 可能与自己位于同一内网中,于是 A 尝试直接向 B 发起连接。但是由于 A 和 B不在同一内网中,因此连接建立失败。客户端 A 通过中心服务器通知 B 连接建立失败,需要进行下一步尝试;客户端 A 向 B 暴露的公网 IP 和端口直接发送请求。如果NAT 网关不支持 Hairpin 模式,那么这个数据包会被直接丢弃,导致数据包无法到达 NAT 网关2;如果 NAT 网关开启了 Hairpin 模式,A 发向 B 的流量会被 NAT 网关转发给 NAT 网关2,流程进入下一步;当数据包到达 NAT 网关2后,下面的流程就和 “客户端分属与不同NAT下”时的打洞行为一致了。4.6 多层 NAT

在网络中,存在设备在多层NAT后的情况。

对于这种多层NAT而言,真正有影响的是最靠近公网的那一个 NAT 网关和最靠近设备的那一个 NAT 网关,其余的 NAT 对于客户端和服务端来说都是不可见的,连接不会关心到底经过了多少层NAT。

但是多层 NAT 并非完全没有影响,准确来说,多层NAT 影响的是客户端的端口映射行为。客户端发出的端口映射请求,只有最靠近客户端的那层 NAT 设备会做出响应。其他的NAT设备不会收到客户端的端口映射请求。但是端口映射要产生作用的话,需要的是最靠近公网的 NAT 网关执行端口映射才行。

五、TCP P2P实现原理5.1 TCP P2P 的优势市面上实现 P2P 的产品主要都是以 UDP 协议为主。因为 UDP 是无连接的,能够往任意地址发包,便于实现 P2P 的能力。

但是 UDP 同时也是不可靠的,如果想要实现可靠传输得自己基于 UDP 去实现可靠传输协议,例如 QUIC、KCP、SCTP 等基于 UDP 实现的可靠连接。

但是基于 UDP 实现的可靠传输是位于应用层,运行在用户态的。

而 TCP 协议是操作系统网络栈原生支持的,而且经过这么多年在操作系统内核层面的优化,TCP 性能是十分能打的,如果我们能够基于 TCP 建立 P2P 连接,对于我们应用层来说就会省事很多了。

5.2 实现原理要想实现基于 TCP 的 P2P,那么 TCP 也必须像 UDP 那样向不同地址建立连接时使用同一个 Src IP + Src Port。

要实现这个效果就需要使用 Linux 中的端口重用技术。端口重用技术运行我们使用同一个 Src IP + Src Port 向不同的目的地址发起 TCP 连接。

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

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

package main

import (

"context"

"fmt"

"log"

"net"

"syscall"

"time"

)

func main() {

addr, err := net.ResolveTCPAddr("tcp", ":34567")

if err != nil {

panic(err)

}

dialer := net.Dialer{

LocalAddr: addr,

Control: func(network, address string, c syscall.RawConn) error {

return c.Control(func(fd uintptr) {

syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)

syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, 0xf, 1)

})

},

}

go func() {

listen := net.ListenConfig{

Control: func(network, address string, c syscall.RawConn) error {

return c.Control(func(fd uintptr) {

syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)

syscall.SetsockoptInt(int(fd), syscall.SOL_SOCKET, 0xf, 1)

})

},

}

tcp, err := listen.Listen(context.Background(), "tcp", ":34567")

if err != nil {

panic(err)

}

log.Printf("start listen at %s ...", tcp.Addr())

for {

conn, err := tcp.Accept()

if err != nil {

panic(err)

}

log.Printf("accept new conn: %s -> %s", conn.RemoteAddr(), conn.LocalAddr())

}

}()

conn1, err := dialer.Dial("tcp", "stun server")

if err != nil {

panic(err)

}

log.Printf("%s -> %s", conn1.LocalAddr(), conn1.RemoteAddr())

log.Println("输入对端地址: ")

var peer string

fmt.Scanln(&peer)

log.Printf("对端地址: %s", peer)

for i := 0; i < 10; i++ {

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)

conn, err := dialer.DialContext(ctx, "tcp", peer)

cancel()

if err != nil {

log.Printf("第 %d 次连接 %s 失败: %s", i, peer, err)

time.Sleep(5 * time.Second)

continue

}

log.Printf("%s -> %s", conn.LocalAddr(), conn.RemoteAddr())

break

}

}

上面的 Demo 中,TCP dial 和 listen 在同一个 Src IP + Src Port 上,进行多次尝试之后就能达到与 UDP 一样的 P2P 打洞效果。

[an error occurred while processing the directive]
Copyright © 2088 时代中心网 - 经典游戏活动回顾 All Rights Reserved.
友情链接