rippled Overlay 网络深入解析:XRPL 对等节点握手协议、会话签名与集群机制
【免费下载链接】rippledDecentralized cryptocurrency blockchain daemon implementing the XRP Ledger protocol in C++项目地址: https://gitcode.com/GitHub_Trending/ri/rippled
导读
本文以 src/xrpld/overlay/README.md 为骨架,系统讲解 XRP Ledger 的 P2P(Overlay)网络:节点之间如何通过 TLS 加密的 TCP/IP 连接与 HTTP Upgrade 握手建立会话、如何用会话签名抵御中间人攻击、如何协商协议版本,以及集群(Cluster)节点之间如何共享负载信息、绕过签名校验并协同抵御 DDoS。读完本文,你将掌握 XRPL 对等协议的完整字段语义、握手的成功/失败流程,以及xrpld.cfg中与 P2P 网络相关的全部配置项。
Overlay 网络:什么是 XRP Ledger 的 P2P 层
XRP Ledger 网络由大量运行xrpld或其他兼容软件的peer(对等节点)组成。每个节点维护多条到其他节点的出站连接,并可选择接受入站连接;这些连接跨越公共互联网与私有局域网。从图论角度看,整个网络是一个有向连通图:顶点是xrpld实例,边是持久的 TCP/IP 连接。节点之间互相收发消息,构成一个叠加在公共/私有互联网之上的overlay network(覆盖网络)。
消息的内容、节点对消息的响应行为,以及在连接建立阶段(握手)交换的信息,共同定义了XRP Ledger 对等协议(peer protocol)。每一对连接都由一个Peer对象表示;Overlay 管理器负责建立、接收并维护这些连接。节点间交换的协议消息使用Google Protocol Buffers序列化——例如TMProposeSet(共识提案)、TMValidation(验证结果)、TMTransaction(交易)等消息类型均定义在 include/xrpl/proto/xrpl.proto 的MessageType枚举中。
每个连接通过其连接类型(connection type)来标识,连接类型影响消息路由行为。目前仅支持一种连接类型:Peer。
连接建立流程:从 TCP 到协议切换
一次完整的对等连接建立在源码 src/xrpld/overlay/detail/ConnectAttempt.cpp 中实现,大致分为以下阶段:
- TCP 连接:出站节点向远端端点发起异步 TCP 连接,并设置约 15 秒的定时器(见
ConnectAttempt::setTimer)。 - TLS 握手:连接建立后立即进行 TLS 加密握手。注意:此阶段不做证书校验(
set_verify_mode(boost::asio::ssl::verify_none)),因为去中心化网络中节点普遍没有 CA 签发的证书,安全性由后续的"会话签名"机制保证。 - 提取会话指纹:TLS 握手完成后,从 SSL 会话中提取本端与对端的
finished消息,生成一个双方一致的共享值(详见"会话签名"一节)。 - 发送 HTTP Upgrade 请求:节点发送一个无消息体的 HTTP 请求,请求头中携带协议版本、网络 ID、节点公钥、会话签名等信息。
- 读取并处理响应:若收到
HTTP 101 Switching Protocols,则验证协议版本与会话签名,成功后将连接升级为对等协议连接,创建PeerImp并加入活动节点列表;若收到 4xx/5xx 或校验失败,则关闭连接。收到503 Service Unavailable时,还会解析响应体 JSON 中的peer-ips字段,把候选节点列表交给 PeerFinder 作为重定向来源(见ConnectAttempt::processResponse)。
HTTP 握手规范
要建立协议连接,节点会先建立一个出站的TLS 加密连接,随后发送一个无消息体的 HTTP 请求。该请求必须满足:
- 使用HTTP/1.1;
- 请求 URI 必须为单个正斜杠 "/"(即服务器根路径)。使用其他 URI 的请求预留给未来的协议实现;
- 使用HTTP/1.1 Upgrade 机制,并通过额外的自定义字段携带与升级相关的协议信息。
不符合上述要求的 HTTP 请求必须返回恰当的 HTTP 错误并关闭连接。当对端收到格式良好的升级请求并校验完协议参数后,会返回HTTP 101响应并切换到请求的协议;否则返回失败信息(例如HTTP 400 Bad Request或HTTP 503 Service Unavailable)。
示例:HTTP 升级请求
GET / HTTP/1.1 User-Agent: xrpld-1.4.0-b1+DEBUG Upgrade: RTXP/1.2, XRPL/2.0 Connection: Upgrade Connect-As: Peer Crawl: public Network-ID: 1 Network-Time: 619234489 Public-Key: n94MvLTiHQJjByfGZzvQewTxQP2qjF6shQcuHwCjh5WoiozBrdpX Session-Signature: MEUCIQCOO8tHOh/tgCSRNe6WwOwmIF6urZ5uSB8l9aAf5q7iRAIgA4aONKBZhpP5RuOuhJP2dP+2UIRioEJcfU4/m4gZdYo= Remote-IP: 192.0.2.79 Closed-Ledger: llRZSKqvNieGpPqbFGnm358pmF1aW96SDIUQcnMh6HI= Previous-Ledger: q4aKbP7sd5wv+EXArwCmQiWZhq9AwBl2p/hCtpGJNsc=从源码看,请求的实际构造在 Handshake.cpp 的makeRequest与buildHandshake中完成:User-Agent取自build_info::getFullVersionString(),Upgrade取自supportedProtocolVersions(),同时还会附带X-Protocol-Ctl头用于协商链接压缩(lz4)、账本重放(ledger replay)、交易/验证降中继(tx/vp reduce-relay)等可选特性。
示例:HTTP 升级响应(成功)
HTTP/1.1 101 Switching Protocols Connection: Upgrade Upgrade: RTXP/1.2 Connect-As: Peer Server: xrpld-1.3.1 Crawl: public Public-Key: n9K1ZXXXzzA3dtgKBuQUnZXkhygMRgZbSo3diFNPVHLMsUG5osJM Session-Signature: MEQCIHMlLGTcGyPvHji7WY2nRM2B0iSBnw9xeDUGW7bPq7IjAiAmy+ofEu+8nOq2eChRTr3wjoKi3EYRqLgzP+q+ORFcig== Network-Time: 619234797 Closed-Ledger: h7HL85W9ywkex+G7p42USVeV5kE04CWK+4DVI19Of8I= Previous-Ledger: EPvIpAD2iavGFyyZYi8REexAXyKGXsi1jMF7OIBY6/Y=示例:HTTP 升级响应(失败:无可用槽位)
HTTP/1.1 503 Service Unavailable Server: xrpld-0.27.0 Remote-Address: 63.104.209.13 Content-Length: 253 Content-Type: application/json {"peer-ips":["54.68.219.39:51235","54.187.191.179:51235", "107.150.55.21:6561","54.186.230.77:51235","54.187.110.243:51235", "85.127.34.221:51235","50.43.33.236:51235","54.187.138.75:51235"]}标准字段
| 字段名 | 请求 | 响应 |
|---|---|---|
User-Agent | ✅ |
User-Agent表示发起请求的节点所使用的软件版本,该值不承担语义含义,但建议实现方标注所用软件版本(对应 RFC 2616 §14.43)。它由makeRequest自动填充为build_info::getFullVersionString(),例如xrpld-1.4.0-b1+DEBUG。
| 字段名 | 请求 | 响应 |
|---|---|---|
Server | ✅ |
Server表示处理请求的节点所用软件版本,同样不承担语义含义,但建议标注版本(RFC 2616 §14.38)。示例响应中的Server: xrpld-1.3.1即由makeResponse填充。
| 字段名 | 请求 | 响应 |
|---|---|---|
Connection | ✅ | ✅ |
Connection的值应为Upgrade,表示正在请求升级该连接(RFC 2616 §14.10)。
| 字段名 | 请求 | 响应 |
|---|---|---|
Upgrade | ✅ | ✅ |
Upgrade属于标准连接升级机制的一部分,请求与响应中都必须出现,用于协商升级完成后的协议版本:
- 请求中是一个逗号分隔的列表,每项表示请求方愿意使用的一个协议版本;
- 响应中必须只包含一个元素,且必须匹配请求列表中的某一项。若服务器不理解请求中的任何协议版本,升级应失败并返回恰当的 HTTP 错误(如
400 Bad Request)。
协议版本的形式为XRPL/后跟点分的主版本号和次版本号,主版本号 ≥ 2、次版本号 ≥ 0(RFC 2616 §14.42)。
自定义字段
| 字段名 | 请求 | 响应 |
|---|---|---|
Connect-As | ✅(强制) | ✅(强制) |
必填的Connect-As字段用于指明所请求的连接类型。请求中为逗号分隔的列表,每项描述一种可能的连接类型,目前只支持peer;响应中必须恰好包含请求列表中的一项。若服务器无法识别请求中的任何连接类型,应返回恰当的 HTTP 错误(如400 Bad Request)。
| 字段名 | 请求 | 响应 |
|---|---|---|
Remote-IP | ◻️(可选) | ◻️(可选) |
可选的Remote-IP字段包含发送方视角下连接远端一端的 IP 地址字符串表示。通过观察来自足够多不同服务器的该字段值,一个发起出站连接的节点可以推断出自己的公网 IP 地址。在verifyHandshake中,若本端已知自己的公网 IP 且对端报告的Remote-IP与之不符,会抛错拒绝连接。
| 字段名 | 请求 | 响应 |
|---|---|---|
Local-IP | ◻️(可选) | ◻️(可选) |
可选的Local-IP字段包含发送方认为属于自己的 IP 地址。接收方可以借此检测 IP 地址不匹配,这可能暗示存在中间人攻击。源码中同样会对公网连接校验该字段与实际远端地址的一致性。
| 字段名 | 请求 | 响应 |
|---|---|---|
Network-ID | ◻️(可选) | ◻️(可选) |
可选的Network-ID用于标识发送方所加入的并行网络。值为32 位无符号整数,已知取值包括:
- 0:主网(main net)
- 1:Ripple 运营的测试网(Test Net)
从 Overlay.h 的注释可知,0、1、2 分别对应主网、测试网与开发网(devnet)。如果配置加入某一网络的服务器收到来自另一网络服务器的连接请求,应返回恰当的 HTTP 错误(如400 Bad Request)。在配置文件中通过[network_id]指定(见下文配置章节),且支持main -> 0、testnet -> 1、devnet -> 2等名称映射。
| 字段名 | 请求 | 响应 |
|---|---|---|
Network-Time | ◻️(可选) | ◻️(可选) |
可选的Network-Time字段报告发送方内部时钟的当前时间。若双方时钟相差超过 20 秒,服务器应拒绝该连接(返回恰当的 HTTP 错误,如400 Bad Request)。强烈建议服务器使用时间同步软件校准时钟。这一 20 秒容差在verifyHandshake中以auto const tolerance = 20s;硬编码实现,若偏移超过容差则抛出"Peer clock is too far off"错误。
| 字段名 | 请求 | 响应 |
|---|---|---|
Public-Key | ✅(强制) | ✅(强制) |
必填的Public-Key字段标识发送服务器的公钥,使用节点公钥的标准base58编码(形如n开头的字符串)。verifyHandshake会解析该字段并强制要求公钥类型为secp256k1,否则拒绝连接。
| 字段名 | 请求 | 响应 |
|---|---|---|
Server-Domain | ◻️(可选) | ◻️(可选) |
可选的Server-Domain字段允许服务器报告其运营的域名,由管理员在配置文件中用[server_domain]键配置。该值目前仅作上报用途,代码未实际使用;外部工具如需采信,应通过在该域名下查找xrp-ledger.toml文件并在其[NODES]键下找到该服务器公钥来核验。发送格式错误的域名会阻止连接建立——verifyHandshake中调用isProperlyFormedTomlDomain校验,不合法即抛出"Invalid server domain"。
| 字段名 | 请求 | 响应 |
|---|---|---|
Session-Signature | ✅(强制) | ✅(强制) |
必填的Session-Signature用于保护对等链路免受特定类型攻击(详见下节)。当前值使用Base64编码,但实现应同时支持Base64与HEX两种编码。签名由本端节点私钥对会话共享值(指纹)计算得出,buildHandshake中使用signDigest(...)生成并base64Encode后写入请求头。
| 字段名 | 请求 | 响应 |
|---|---|---|
Crawl | ◻️(可选) | ◻️(可选) |
可选的Crawl字段用于声明是否希望被对端纳入爬虫报告(crawl reports),可取两个值:
Public:服务器的 IP 地址与端口应被纳入爬虫报告;Private:不应被纳入爬虫报告。字段省略时默认为Private。
| 字段名 | 请求 | 响应 |
|---|---|---|
Closed-Ledger | ◻️(可选) | ◻️(可选) |
若存在,标识发送方认为最近一个已关闭账本的哈希。当前编码为HEX,但为兼容旧版本,实现应同时支持Base64与HEX。buildHandshake从LedgerMaster获取getClosedLedger(),写入账本哈希与父哈希。
| 字段名 | 请求 | 响应 |
|---|---|---|
Previous-Ledger | ◻️(可选) | ◻️(可选) |
若存在,标识发送方认为已关闭账本的父账本哈希。当前编码为Base64,但实现应同时支持Base64与HEX。
附加头
实现方或运维人员可以在请求与响应中附加其他可选字段与取值;实现不应因为存在自己无法理解的字段而拒绝请求——这是前向兼容性的基本要求。
会话签名:无需 CA 的抗中间人机制
即使连接是 SSL/TLS 加密的,攻击者仍可能发动相对廉价的MITM(中间人)攻击:这类攻击极难察觉,且可能让攻击者有能力智能地篡改两端之间交换的消息。如果至少一方持有对方信任的 CA 签发的证书,这一风险可以得到缓解;但在去中心化、无许可的网络中,持有证书并非总是可行(甚至并非可取)。
协议的最终目标,是确保两个端点 A 和 B 知道它们在同一个端到端 SSL/TLS 会话上直接通信,而不是通过攻击者代理的两个独立会话通信。
XRP Ledger 协议利用一个关键事实来预防此类攻击:两台服务器各自拥有节点身份——secp256k1密钥对——并以此将 SSL/TLS 会话强绑定到会话两端的节点身份上。具体做法(对应源码 Handshake.cpp 中的makeSharedValue):
- "伸入" SSL/TLS 会话内部,分别提取本端(
SSL_get_finished)与对端(SSL_get_peer_finished)的finished消息; - 对两条消息分别做 SHA-512 哈希,再异或合并,生成唯一的"指纹"(fingerprint)。按设计,该指纹在 TLS 会话两端应相同;若两次哈希结果相同导致异或为 0,说明会话异常,直接拒绝;
- 该指纹由各端点独立计算,从不经网络传输;
- 每台服务器用自己的节点私钥对指纹签名(签名经加密链路在握手阶段传输),并附上对应的公钥身份;
- 链路每一端都用对方声称的公钥,针对本会话唯一的指纹验证签名。若签名校验失败,连接必须被丢弃。
这一机制的攻防效果清晰明了:
- 若攻击者 Eve 与 Alice、Bob 分别建立两个独立的 SSL 会话,则两个会话的指纹必然不同。Eve 无法用 Alice 的私钥签署她与 Bob 会话的指纹,也无法用 Bob 的私钥签署她与 Alice 会话的指纹,因此 A 和 B 都会意识到有活跃的 MITM 攻击正在进行并关闭连接;
- 若 Eve 只是原样转发字节,则她无法解密 A、B 之间传输的数据,无法智能篡改消息流——虽然仍可能注入延迟或终止链路,但已无法破坏消息完整性。
verifyHandshake中的校验同时实现了两个目标:既验证对方确实持有其声明的节点公钥对应的私钥,又验证 SSL 会话是端到端的而非经过代理中转,并拒绝自连接("Self connection")。
协议版本协商
握手时Upgrade头携带的XRPL/版本列表,由 ProtocolVersion.cpp 负责解析与协商:
- 当前版本支持列表为
{2, 2}与{2, 3}(见kSupportedProtocolList),列表必须以严格升序排列且不允许重复(通过static_assert在编译期保证); parseProtocolVersions使用正则^XRPL/([2-9]|(?:[1-9][0-9]+))\.(0|(?:[1-9][0-9]*))$解析逗号分隔的版本串,返回排序去重后的版本集合;negotiateProtocolVersion取"我方支持列表 ∩ 对端支持列表"中的最大版本作为协商结果;对端响应中的Upgrade必须恰好一个元素且为我方支持的版本,否则握手失败(ConnectAttempt::processResponse中的"Unable to negotiate protocol version")。
XRPL 集群(Clustering)
集群由同一管理机构运营的多台 XRPL 服务器组成,成员之间共享负载信息、分担密码学操作,并提供更高的一致性响应。集群成员通过各自的节点公钥相互识别,彼此交换"正在对它们施加负载的端点"信息与内部负载状态;集群成员之间无需校验来自其他成员消息的密码学签名。集群实现位于 Cluster.h 与 detail/Cluster.cpp。
配置
服务器的节点公钥可从server_info命令的输出中获取,即pubkey_node字段——一个以字母n开头的文本串,且在多次运行间保存在数据库中(可通过[node_seed]配置强制固定节点密钥)。
集群成员在xrpld.cfg的[cluster_nodes]下配置:每行以节点公钥开头,后面可选地跟一个空格与一个友好名称。对应配置示例节选(完整注释见 cfg/xrpld-example.cfg):
# [cluster_nodes] # # To extend full trust to other nodes, place their node public keys here. # Generally, you should only do this for nodes under common administration. # Node public keys start with an 'n'. To give a node a name for identification # place a space after the public key and then the name. # # [cluster_nodes] # nHB9QdDGzLuHgNwbp8TykAvMvErtnPvPZgDPD3MYBtBkCHYrVNdt s1 # nHU4Df2gyC23CWHY7kZQoWKnUtN7WEgqyDuR2KJKXb9WEvNbqKzG s2由于集群成员可以互相引荐其他成员,因此不必在每个成员上配置全部成员:若采用中心辐射(hub and spoke)结构,只需在每个 hub 上配置全部成员,在 spoke 上只配置各 hub 即可——每个 spoke 无需被配置在其它 spoke 上。
新增 spoke 的推荐步骤:
- 在新 spoke 的
[cluster_nodes]中写入每个 hub 的节点公钥; - 启动 spoke 服务器并确定其节点公钥;
- 在每个 hub 上配置新 spoke 的公钥;
- 逐个重启各 hub;
- 重启 spoke。
Cluster::load通过正则解析每行配置(节点公钥 + 可选注释),并拒绝格式错误或无效节点身份的条目。
事务行为
当收到来自集群成员的事务时,若干常规检查会被绕过:
- 签名检查被绕过:因为信任集群成员不会转发签名错误的事务。验证节点(validator)可能希望禁用此特性,宁可以额外负载换取"每个事务都经验证"的更高安全性;
- 本地事务检查被绕过:例如,服务器不会因为来自集群对等节点的事务费用未达到其当前中继费而拒绝它。优先保持集群内部一致,使来自某一集群成员的确认能更可靠地代表整个集群对该事务的接受。
服务器负载信息
集群成员之间交换各自的服务器负载水平。负载水平(load level)本质上是普通费用水平被乘上的倍数,用以得到该服务器中继事务的费用。
一台服务器的有效负载水平(也是其确定中继费用所用的水平)取以下三者中的最高值:
- 本地负载水平(local load level);
- 网络负载水平(network load level);
- 集群负载水平——集群成员报告负载水平的中位数。
Cluster::update会记录每个成员上报的负载费用loadFee与上报时间,并丢弃过期上报;ClusterNode则保存成员的友好名称。
Gossip:集群内的端点负载共享
Gossip是集群成员之间共享"正在对其施加异常高负载的端点(通常为 IPv4 地址)"信息的机制。端点负载管理器会结合 gossip 信息,降低该端点在被警告、断开或封禁前可对本地服务器施加的负载额度。
典型场景:假设攻击者控制大量 IP 地址,可发送足够多的请求压垮一台服务器。没有 gossip 时,他可以用同样的地址压垮集群中所有服务器;有了 gossip,如果他选择用同一 IP 对多台服务器施压,会发现在连接被断开前能施加的负载额度大幅降低。
监控
peers命令会报告集群状态:
cluster对象为集群中每个成员(无论配置的还是由其他成员引荐的)保留一个条目,其中age字段表示距上次听到该服务器消息的秒数;若该服务器正在上报抬高的集群费用,也会一并报告;- 在
peers对象中,集群成员的条目会带有值为true的cluster字段。
相关配置项速查
以下是与 Overlay 网络直接相关的xrpld.cfg配置项(均见 cfg/xrpld-example.cfg):
| 配置项 | 取值/默认值 | 作用 |
|---|---|---|
[ips] | 每行一个地址或域名,可带端口(默认端口 2459,旧服务器常用 51235) | 提供初始对等连接候选列表;内置默认列表如r.ripple.com 51235 |
[ips_fixed] | 每行地址 + 必须的端口 | 始终尝试维护连接的固定对等节点,适合私有网络、验证服务器经公网节点接入、构建集群等场景 |
[peer_private] | 0 或 1,默认 0 | 0:请求对端广播本机地址;1:不广播,只连接已配置的对等节点 |
[peers_max] | 数值 | 期望的最大对等连接数(入站+出站),集群与固定节点不计入 |
[node_seed] | 种子字符串 | 固定节点种子/密钥(用于集群),格式同validation_seed |
[cluster_nodes] | 每行一个n开头的节点公钥(可加友好名称) | 声明受信集群成员 |
[network_id] | 0–4294967295 或名称(main→0、testnet→1、devnet→2) | 声明本服务器所属网络,拒绝来自其他网络的连接 |
[server_domain] | 域名 | 上报Server-Domain握手字段(仅上报用途) |
[compression] | true/false,默认 false | 启用与支持 lz4 压缩的对等节点间的链路压缩 |
源码导读
若希望进一步深入 Overlay 模块的实现,建议按以下路径阅读:
- src/xrpld/overlay/Overlay.h:Overlay 管理器抽象接口(
Setup结构、connect、broadcast、relay、networkID等); - src/xrpld/overlay/detail/ConnectAttempt.cpp:出站连接状态机(TCP 连接 → TLS 握手 → 发送请求 → 处理响应 → 升级为 Peer);
- src/xrpld/overlay/detail/Handshake.cpp:请求/响应构造、共享值生成、握手验证与特性协商头
X-Protocol-Ctl; - src/xrpld/overlay/detail/ProtocolVersion.cpp:
XRPL/x.y版本解析与协商; - src/xrpld/overlay/Cluster.h 与 src/xrpld/overlay/detail/Cluster.cpp:集群成员表、负载上报与
[cluster_nodes]解析; - include/xrpl/proto/xrpl.proto:对等协议消息的 Protobuf 定义(
MessageType、TMProposeSet、TMValidation、TMTransaction等)。
小结
XRP Ledger 的 Overlay 层用"TLS 加密 + HTTP/1.1 Upgrade 握手 + 会话签名"的组合,在无 CA 证书的去中心化前提下既完成了协议版本、网络归属、节点身份与能力的协商,又通过绑定 SSL 会话指纹与secp256k1节点密钥对,从密码学层面封堵了中间人攻击。而集群机制则在"信任边界"内通过绕过签名校验、共享负载水平与端点 gossip,为多节点联合运营提供了更低的延迟、更高的吞吐与更强的抗 DDoS 能力——这两套机制共同构成了 XRPL 对等网络可靠运行的基础。
【免费下载链接】rippledDecentralized cryptocurrency blockchain daemon implementing the XRP Ledger protocol in C++项目地址: https://gitcode.com/GitHub_Trending/ri/rippled
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考