Libp2p是什么?
Libp2p是用于构建P2P网络的模块化网络堆栈和库,源自开源项目IPFS,模块化设计使它能够用来构建各种去中心化应用的P2P网络层。目前,知名区块链项目Ethereum 2.0、Pokdot、BitXHub都选择基于Libp2p库搭建系统网络层。Libp2p作为P2P网络协议栈,它是通过解决实际问题不断成长的,可以认为是构建P2P网络经验的积累。
Libp2p解决了哪些问题?
Libp2p作为网络协议栈,主要解决两个问题:
1.节点发现
节点发现用来发现P2P网络中的其它节点及维护节点在线状态,并且根据节点状态调整网络连接,构建稳定的网络拓扑。
2. 数据传输
数据传输负责节点间数据的流转。为了支持各种异构的网络设备互连,Libp2p核心要求之一就是传输层不可知,Libp2p支持不同的传输层协议,例如TCP、UDP、QUIC等。在传输层建立连接后还需要考虑网络数据的隐私安全,Libp2p对传输通道加密,节点间通过加密通道进行通信。为了高效传输数据,Libp2p支持对连接进行多路流复用从而支持节点间多个并发流通信。
本文主要讨论Libp2p解决数据传输问题方案,代码基于go-libp2p v0.9.2。
Libp2p是如何解决数据传输问题的?
传输层不可知
传输层不可知是指Libp2p支持多种传输层协议,例如TCP、UDP、QUIC等,应用程序开发者在使用Libp2p库时不需要知道完成数据传输使用的传输层协议,Libp2p会根据远程节点地址信息自动完成协议选择。
地址定义
数据传输建立在节点连接的基础上,在可以拨号远程节点并建立连接之前,需要知道远程节点的监听地址。因为每种传输协议都有自己的地址格式,所以Libp2p使用一种称为“multiaddr”的编码方案来统一不同的协议的地址格式。
TCP/IP传输协议“multiaddr”的描述如下:
/ip4/127.0.0.1/tcp/9999
UDP传输协议“multiaddr”的描述如下:
/ip4/127.0.0.1/udp/9998
用这种描述方式来代替127.0.0.1:9999的好处是什么呢?“multiaddr”能更明确的描述使用的协议,如127.0.0.1属于IPv4协议,9999属于TCP协议,9998属于UDP协议。
以上为“multiaddr”描述的节点监听地址,当拨号一个节点时也是使用“multiaddr”,但需要加上远程节点的ID,例如:
/ip4/192.168.100.100/tcp/9999/p2p/QmcEPrat8ShnCph8WjkREzt5CPXF2RwhYxYBALDcLC1iV6
这样就知道对方使用IP4,地址:192.168.100.100,TCP协议,端口:9999,是一个P2P节点,节点ID:QmcEPrat8ShnCph8WjkREzt5CPXF2RwhYxYBALDcLC1iV6。
节点ID定义
节点ID是一串字符,由节点公钥的hash产生,并进行base58编码,节点ID是全网唯一的,拨号时使用节点ID可以有效解决中间人攻击问题。
支持多种传输协议
Libp2p的Swarm模块负责将多个传输层组合到一个接口中,从而允许应用程序拨号节点,而不必指定要使用的传输层。它还负责协议协商、多路流复用、建立安全通信等接口升级操作。
图1 Swarm类图
Network接口是Libp2p对外提供服务的接口,Swarm是Network接口的具体实现。Libp2p在Swarm中维护现有连接的状态,维护支持的传输层协议。Swarm通过以下操作支持多传输协议:
- 为了支持多种传输层协议,新建节点时(NewNode)会将节点支持的传输层协议通过AddTransport加入Swarm的transports结构中。
- 当节点打开监听(Listen)时,Swarm模块会调用TransportForListening获取监听地址对应的传输层协议,并调用相应的传输层协议的Listen函数。
- 当节点主动连接(Dial)其它节点时,Swarm模块会调用TransportForDialing获取拨号地址使用的传输协议,并调用相应的传输层协议的Dial函数。
图2 Swarm模块拨号、监听处理流程图
Libp2p通过“multiaddr”的编码方案来统一不同协议的地址格式,在Swarm模块根据“multiaddr”解析协议并调用相应协议的接口完成具体操作,从而达到了应用层不需要关注使用的传输层协议的目的。
数据安全传输
以上过程在节点间建立了连接,Libp2p是如何保证传输数据隐私的?这里以TCP协议为例进行展开介绍。
图3 传输加密相关模块类图
TcpTransport是TCP的传输层实现模块,其中组合了Upgrader模块,Upgrader负责把一个原始的TCP连接升级为支持加密,支持多路流复用的连接。secio和tls是两个实现了SecureTransport接口的库,Libp2p库默认使用secio加密库。
图4 传输加密处理流程图
以上为TCP收到一个远程节点连接请求的调用流程,主动拨号远程节点调用过程类似。这里使用secio包的runHandshakeSync函数对连接进行加密。
secio库密钥交换使用Diffie-Hellman密钥协商算法(secio协议具体内容)。当然也可以使用tls对连接进行加密。
多路流复用
Libp2p应用程序通常会在节点之间打开许多独立的通信流,并且可能会与某个远程节点同时打开多个并发流。多路流复用允许与远程节点建立一次连接即可完成整个生命周期的数据收发,同样只需要处理一次NAT操作,因为和同一个远程节点所有的流都共享相同的底层传输连接。在配置Libp2p时,会启用流复用模块,Swarm将在拨号远程节点和侦听连接时使用它们。如果远程节点支持相同的多路流复用实现,则Swarm将在建立连接时选择并使用它;如果拨号Swarm已经与之建立连接的远程节点,则新建流将自动在现有连接上进行多路复用。
图5 多路流复用相关模块类图
upgrader的muxer模块负责将加密后的连接升级为支持多路流复用的连接。MuxedConn为多路流复用操作接口,multiplex为多路流复用操作具体实现,负责具体的流创建及管理。除了默认的multiplex,Libp2p还支持yamux、spdy、muxado等不同的多路复用器实现。
图6 多路流复用处理流程图
还是以监听为例,收到连接请求后,Upgrader的Secure模块首先会将连接升级为加密连接。然后通过NewMultiplex创建多路复用器实例,多路复用器实例把接口升级为支持流复用的接口。
MuxedConn的OpenStream接口用于向远程节点发送新建流请求,AcceptStream接口用于接收远程节点创建流的请求。
由于节点间通信多个流使用的仍是同一个连接,为了区别不同的流,multiplex模块实际上是对发送的数据添加了header字段。
图7 流数据格式示意图
header高61bits为stream id,每次新建流时stream id会自增。
header低3位表示消息类型,总共定义了4种流消息:
newStreamTag = 0 // 创建流消息
messageTag= 2 // 数据消息
closeTag= 4 // 关闭流消息
resetTag= 6 // 重置流消息
Libp2p通过upgrader的muxer模块将普通接口升级为支持流复用的接口,节点间数据传输时会在连接上通过流进行并行数据收发,而不是新建连接,从而减少了节点间新建连接的消耗。为了区别不同的流上的信息,流复用器对收发数据添加了表示流信息的header字段。
总结
从Libp2p解决数据传输问题能够看到Libp2p有很多小组件组成,解决相同问题小组件遵循相同接口,可以根据使用场景进行替换,各组件库可以单独开发升级,而不会对其它部分产生影响。“multiaddr”的地址编码方案使基于Libp2p的应用层开发不需要关注底层使用的传输层协议。节点间可以通过协议协商选择共同支持的加密模块对数据传输通道进行加密,从而保证数据隐私性。