已弃用 - SSU 已被 SSU2 替代。i2pd 在 2.44.0 版本(API 0.9.56)2022年11月中移除了 SSU 支持。Java I2P 在 2.4.0 版本(API 0.9.61)2023年12月中移除了 SSU 支持。
SSU(在I2P文档和用户界面中也常被称为"UDP")是I2P中实现的两种传输协议 之一。另一种是NTCP2 。对NTCP 的支持已被移除。
SSU在I2P版本0.6中引入。在标准的I2P安装中,router同时使用NTCP和SSU进行出站连接。从版本0.9.8开始支持SSU-over-IPv6。
SSU 被称为"半可靠"是因为它会重复重传未确认的消息,但仅限于最大次数。超过该次数后,消息将被丢弃。
SSU 服务
与NTCP传输协议一样,SSU提供可靠、加密、面向连接的点对点数据传输。SSU独有的功能还包括IP检测和NAT穿越服务,包括:
- 使用 introducers 进行协作式 NAT/防火墙穿透
- 通过检查传入数据包和 peer testing 进行本地 IP 检测
- 向 NTCP 通信防火墙状态和本地 IP,以及任一项的变更
- 向 router 和用户界面通信防火墙状态和本地 IP,以及任一项的变更
Router 地址规范
以下属性存储在 netDb 中。
- 传输名称: SSU
- caps: [B,C,4,6] 见下文 。
- host: IP(IPv4 或 IPv6)。 允许缩短的 IPv6 地址(带有"::")。 如果在防火墙后,可能存在也可能不存在。 主机名之前是允许的,但从 0.9.32 版本开始已弃用。参见提案 141。
- iexp[0-2]: 此介绍者的过期时间。 ASCII 数字,自纪元开始的秒数。 仅在防火墙后且需要介绍者时存在。 可选(即使此介绍者的其他属性存在)。 从 0.9.30 版本开始,提案 133。
- ihost[0-2]: 介绍者的 IP(IPv4 或 IPv6)。 主机名之前是允许的,但从 0.9.32 版本开始已弃用。参见提案 141。 允许缩短的 IPv6 地址(带有"::")。 仅在防火墙后且需要介绍者时存在。 见下文 。
- ikey[0-2]: 介绍者的 Base 64 介绍密钥。见下文 。 仅在防火墙后且需要介绍者时存在。 见下文 。
- iport[0-2]: 介绍者的端口 1024 - 65535。 仅在防火墙后且需要介绍者时存在。 见下文 。
- itag[0-2]: 介绍者的标签 1 - (2^32 - 1) ASCII 数字。 仅在防火墙后且需要介绍者时存在。 见下文 。
- key: Base 64 介绍密钥。见下文 。
- mtu: 可选。默认和最大值为 1484。最小值为 620。 对于 IPv6 必须存在,其最小值为 1280,最大值为 1488 (在 0.9.28 版本之前最大值为 1472)。 IPv6 MTU 必须是 16 的倍数。 (IPv4 MTU + 4)必须是 16 的倍数。 见下文 。
- port: 1024 - 65535 如果在防火墙后,可能存在也可能不存在。
协议详情
拥塞控制
SSU只需要半可靠传输、TCP友好操作以及高吞吐量能力,这为拥塞控制提供了很大的灵活性。下面概述的拥塞控制算法既要在带宽使用上高效,又要实现简单。
数据包根据 router 的策略进行调度,注意不要超过 router 的出站容量或超过远程对等节点的测量容量。测量容量的运作方式类似于 TCP 的慢启动和拥塞避免,在发送容量上采用加性增长,面对拥塞时采用乘性减少。与 TCP 不同的是,router 可能在给定时间段或重传次数后放弃某些消息,同时继续传输其他消息。
拥塞检测技术也与TCP不同,因为每个消息都有自己独特且非连续的标识符,并且每个消息都有大小限制——最多32KB。为了高效地向发送方传输这种反馈,接收方会定期包含一个完全ACK的消息标识符列表,还可能包含部分接收消息的位字段,其中每个位代表一个片段的接收状态。如果重复的片段到达,应该再次ACK该消息,或者如果消息仍未完全接收,应该重新传输位字段并包含任何新的更新。
当前实现不会将数据包填充到任何特定大小,而是只将单个消息片段放入数据包中并发送(注意不要超过 MTU)。
MTU
从 router 版本 0.8.12 开始,IPv4 使用两个 MTU 值:620 和 1484。MTU 值会根据重传数据包的百分比进行调整。
对于这两个 MTU 值,希望 (MTU % 16) == 12,这样在减去 28 字节的 IP/UDP 头部后,有效载荷部分是 16 字节的倍数,以满足加密需求。
对于较小的 MTU 值,需要将 2646 字节的可变 tunnel 构建消息高效地打包到多个数据包中;使用 620 字节的 MTU,它可以很好地装入 5 个数据包中。
根据测量结果,1492字节几乎适合所有合理小的I2NP消息(较大的I2NP消息可能达到1900到4500字节,这无论如何都无法适应实际网络的MTU)。
在 0.8.9 - 0.8.11 版本中,MTU 值分别为 608 和 1492。在 0.8.9 版本之前,大 MTU 值为 1350。
从 0.8.12 版本开始,最大接收数据包大小为 1571 字节。对于 0.8.9 - 0.8.11 版本,该值为 1535 字节。在 0.8.9 版本之前,该值为 2048 字节。
从 0.9.2 版本开始,如果 router 的网络接口 MTU 小于 1484,它将在 netDb 中发布这一信息,其他 router 在建立连接时应当遵循这一设置。
对于IPv6,最小MTU是1280。IPv6 IP/UDP头部是48字节,所以我们使用满足(MTU % 16 == 0)的MTU,1280满足这个条件。IPv6的最大MTU是1488。(在0.9.28版本之前最大值是1472)。
消息大小限制
虽然最大消息大小名义上是32KB,但实际限制有所不同。协议将分片数量限制为7位,即128个。然而,当前实现将每条消息限制为最多64个分片,当使用608 MTU时,这足以支持64 * 534 = 33.3 KB。由于捆绑的leaseSet和会话密钥的开销,应用层的实际限制大约低6KB,约为26KB。要将UDP传输限制提高到32KB以上,还需要进一步的工作。对于使用更大MTU的连接,可以支持更大的消息。
空闲超时
空闲超时和连接关闭由各端点自行决定,可能会有所不同。当前实现会在连接数接近配置的最大值时降低超时时间,在连接数较少时提高超时时间。建议的最小超时时间为两分钟或更长,建议的最大超时时间为十分钟或更长。
密钥
所有使用的加密都是AES256/CBC,使用32字节密钥和16字节初始化向量。当Alice与Bob发起会话时,MAC和会话密钥作为DH交换的一部分进行协商,然后分别用于HMAC和加密。在DH交换过程中,Bob的公开可知introKey用于MAC和加密。
初始消息和后续回复都使用响应者(Bob)的 introKey - 响应者不需要知道请求者(Alice)的 introKey。Bob 使用的 DSA 签名密钥应该在 Alice 联系他时就已经被 Alice 知道,但 Alice 的 DSA 密钥可能还不被 Bob 所知。
收到消息后,接收方会检查"发送方"IP地址和端口与所有已建立的会话 - 如果有匹配,则在HMAC中测试该会话的MAC密钥。如果这些都无法验证或没有匹配的IP地址,接收方会在MAC中尝试使用其introKey。如果无法验证,数据包将被丢弃。如果验证成功,则根据消息类型进行解释,不过如果接收方过载,仍可能会丢弃该数据包。
如果Alice和Bob已经建立了会话,但Alice由于某种原因丢失了密钥并希望联系Bob,她可以随时通过SessionRequest和相关消息简单地建立新的会话。如果Bob丢失了密钥但Alice不知道这一点,她会首先尝试通过发送设置了wantReply标志的DataMessage来提醒他回复,如果Bob持续无法回复,她将假设密钥已丢失并重新建立新的密钥。
对于 DH 密钥协商,使用 RFC3526 2048位 MODP 组(#14):
p = 2^2048 - 2^1984 - 1 + 2^64 * { [2^1918 pi] + 124476 }
g = 2
这些是用于I2P的ElGamal加密 的相同p和g值。
重放防护
在 SSU 层的重放攻击防护通过拒绝时间戳过于陈旧或重复使用初始化向量(IV)的数据包来实现。为了检测重复的 IV,采用了一系列布隆过滤器来定期"衰减",以便只检测最近添加的 IV。
DataMessage中使用的messageId在SSU传输层之上的层定义,并透明地传递。这些ID没有特定的顺序——实际上,它们很可能是完全随机的。SSU层不会尝试进行messageId重放防护——更高层应该考虑这一点。
寻址
要联系SSU对等节点,需要两组信息中的一组:直接地址(用于对等节点可公开访问的情况),或间接地址(用于通过第三方来介绍对等节点)。对等节点可拥有的地址数量没有限制。
Direct: host, port, introKey, options
Indirect: tag, relayhost, port, relayIntroKey, targetIntroKey, options
每个地址还可能公开一系列选项 - 该特定节点的特殊功能。有关可用功能的列表,请参见下文 。
地址、选项和功能发布在网络数据库 中。
直接会话建立
当无需第三方进行NAT穿越时,使用直接会话建立。消息序列如下:
连接建立(直连)
Alice 直接连接到 Bob。从 0.9.8 版本开始支持 IPv6。
Alice Bob
SessionRequest --------------------->
<--------------------- SessionCreated
SessionConfirmed ------------------->
<--------------------- DeliveryStatusMessage
<--------------------- DatabaseStoreMessage
DatabaseStoreMessage --------------->
Data <--------------------------> Data
收到 SessionConfirmed 消息后,Bob 发送一个小的 DeliveryStatus 消息 作为确认。在此消息中,4 字节的消息 ID 设置为随机数,8 字节的"到达时间"设置为当前网络范围的 ID,即 2(0x0000000000000002)。
发送状态消息后,对等节点通常会交换包含其 RouterInfos 的 DatabaseStore 消息 ,但这并不是必需的。
状态消息的类型或其内容似乎并不重要。最初添加它是因为 DatabaseStore 消息会延迟几秒钟;由于现在存储会立即发送,或许可以取消状态消息。
介绍
介绍密钥通过外部渠道(网络数据库)传递,在0.9.47版本之前,它们传统上与router Hash相同,但从0.9.48版本开始可能是随机的。在建立会话密钥时必须使用这些密钥。对于间接地址,节点必须首先联系中继主机,并要求它们介绍在该中继主机上以给定标签已知的节点。如果可能,中继主机会向目标节点发送消息,告诉它们联系请求节点,同时也向请求节点提供目标节点所在的IP和端口。此外,建立连接的节点必须已经知道它们要连接的节点的公钥(但不需要知道任何中介中继节点的公钥)。
通过第三方介绍进行间接会话建立对于高效的NAT穿越是必要的。Charlie是一个位于NAT或防火墙后面的router,不允许未经请求的入站UDP数据包,他首先联系一些对等节点,选择其中一些作为介绍人。这些对等节点(Bob、Bill、Betty等)为Charlie提供一个介绍标签——一个4字节的随机数——然后Charlie将其公开作为联系他的方法。Alice是一个拥有Charlie已发布联系方法的router,她首先向一个或多个介绍人发送RelayRequest数据包,请求每个介绍人将她介绍给Charlie(提供介绍标签来识别Charlie)。然后Bob向Charlie转发一个RelayIntro数据包,包含Alice的公共IP和端口号,接着向Alice发回一个RelayResponse数据包,包含Charlie的公共IP和端口号。当Charlie收到RelayIntro数据包时,他向Alice的IP和端口发送一个小的随机数据包(在他的NAT/防火墙中打洞),当Alice收到Bob的RelayResponse数据包时,她开始与指定的IP和端口建立新的全方向会话。
连接建立(通过介绍者间接建立)
Alice 首先连接到引荐人 Bob,Bob 将请求转发给 Charlie。
Alice Bob Charlie
RelayRequest ---------------------->
<-------------- RelayResponse RelayIntro ----------->
<-------------------------------------------- HolePunch (data ignored)
SessionRequest -------------------------------------------->
<-------------------------------------------- SessionCreated
SessionConfirmed ------------------------------------------>
<-------------------------------------------- DeliveryStatusMessage
<-------------------------------------------- DatabaseStoreMessage
DatabaseStoreMessage -------------------------------------->
Data <--------------------------------------------------> Data
在打洞完成后,Alice 和 Charlie 之间的会话建立过程与直接建立相同。
IPv6 注意事项
从版本 0.9.8 开始支持 IPv6。发布的中继地址可以是 IPv4 或 IPv6,Alice-Bob 通信可以通过 IPv4 或 IPv6 进行。在 0.9.49 版本之前,Bob-Charlie 和 Alice-Charlie 通信仅通过 IPv4 进行。从 0.9.50 版本开始支持 IPv6 中继。详细信息请参阅规范。
虽然规范从 0.9.8 版本开始就进行了更改,但通过 IPv6 进行的 Alice-Bob 通信直到 0.9.50 版本才真正得到支持。早期版本的 Java router 错误地为 IPv6 地址发布了 ‘C’ 能力,尽管它们实际上并不通过 IPv6 充当介绍者。因此,router 应该只在 router 版本为 0.9.50 或更高版本时才信任 IPv6 地址上的 ‘C’ 能力。
节点测试
通过一系列PeerTest消息的序列,可以实现对等节点协作可达性测试的自动化。通过正确执行,对等节点将能够确定自己的可达性状态,并可以相应地更新其行为。测试过程相当简单:
Alice Bob Charlie
PeerTest ------------------->
PeerTest-------------------->
<-------------------PeerTest
<-------------------PeerTest
<------------------------------------------PeerTest
PeerTest------------------------------------------>
<------------------------------------------PeerTest
每个 PeerTest 消息都携带一个由 Alice 初始化的 nonce,用于标识测试系列本身。如果 Alice 没有收到她期望的特定消息,她将相应地重传,并且根据接收到的数据或缺失的消息,她将知道自己的可达性。可能达到的各种最终状态如下:
如果她没有收到来自Bob的响应,她会重传一定次数,但如果始终没有收到响应,她就会知道她的防火墙或NAT配置有误,即使是直接响应出站数据包的入站UDP数据包也被拒绝了。或者,Bob可能宕机了或无法让Charlie回复。
如果 Alice 没有从第三方(Charlie)收到包含预期随机数的 PeerTest 消息,她会向 Bob 重新发送初始请求,最多重传一定次数,即使她已经收到了 Bob 的回复。如果 Charlie 的第一条消息仍然无法通过,但 Bob 的消息可以,她就知道自己位于 NAT 或防火墙后面,该设备拒绝未经请求的连接尝试,并且端口转发没有正常工作(Bob 提供的 IP 和端口应该被转发)。
如果Alice收到了Bob的PeerTest消息和Charlie的两个PeerTest消息,但Bob和Charlie第二个消息中包含的IP和端口号不匹配,她就知道自己处于对称NAT后面,该NAT会为联系的每个peer重写她的所有出站数据包,使用不同的"来源"端口。她需要显式转发一个端口,并始终保持该端口开放以实现远程连接,忽略进一步的端口发现。
如果 Alice 收到了 Charlie 的第一条消息但没有收到第二条消息,她会向 Charlie 重传她的 PeerTest 消息最多一定次数,但如果没有收到响应,她就知道 Charlie 要么处于混乱状态,要么已经离线。
Alice应该从已知的看起来能够参与peer测试的peers中任意选择Bob。Bob反过来应该从他所知道的看起来能够参与peer测试且与Bob和Alice都不在同一IP上的peers中任意选择Charlie。如果出现第一个错误情况(Alice没有从Bob那里收到PeerTest消息),Alice可以决定指定一个新的peer作为Bob,并使用不同的nonce重新尝试。
Alice 的介绍密钥包含在所有 PeerTest 消息中,这样 Charlie 就能在不知道任何额外信息的情况下联系她。从 0.9.15 版本开始,Alice 必须与 Bob 建立已有会话,以防止欺骗攻击。Alice 不能与 Charlie 建立已有会话,这样对等测试才有效。Alice 可以继续与 Charlie 建立会话,但这不是必需的。
IPv6 注意事项
在0.9.26版本之前,仅支持IPv4地址测试。只支持IPv4地址测试。因此,所有Alice-Bob和Alice-Charlie通信都必须通过IPv4进行。然而,Bob-Charlie通信可以通过IPv4或IPv6进行。当在PeerTest消息中指定时,Alice的地址必须是4字节。从0.9.27版本开始,支持IPv6地址测试,如果Bob和Charlie在其发布的IPv6地址中通过’B’能力标识表示支持,那么Alice-Bob和Alice-Charlie通信也可以通过IPv6进行。详情请参见提案126 。
在 0.9.50 版本发布之前,Alice 通过她希望测试的传输方式(IPv4 或 IPv6)上的现有会话向 Bob 发送请求。当 Bob 通过 IPv4 收到来自 Alice 的请求时,Bob 必须选择一个公布 IPv4 地址的 Charlie。当 Bob 通过 IPv6 收到来自 Alice 的请求时,Bob 必须选择一个公布 IPv6 地址的 Charlie。实际的 Bob-Charlie 通信可能通过 IPv4 或 IPv6 进行(即,独立于 Alice 的地址类型)。
从 0.9.50 版本开始,如果消息是通过 IPv6 发送给 IPv4 对等节点测试,或者(从 0.9.50 版本开始)通过 IPv4 发送给 IPv6 对等节点测试,Alice 必须包含她的介绍地址和端口。
详情请参阅提案 158 。
传输窗口、ACK 和重传
DATA 消息可能包含完整消息的 ACK 以及消息各个片段的部分 ACK。详细信息请参见协议规范页面 的数据消息部分。
窗口、ACK 和重传策略的详细信息在此处未指定。请参阅 Java 代码了解当前实现。在建立阶段和对等测试期间,router 应实现指数退避重传。对于已建立的连接,router 应实现可调整的传输窗口、RTT 估计和超时,类似于 TCP 或 streaming 。请参阅代码了解初始、最小和最大参数。
安全性
UDP 源地址当然可能被伪造。此外,特定 SSU 消息(RelayRequest、RelayResponse、RelayIntro、PeerTest)中包含的 IP 和端口可能不是合法的。另外,某些操作和响应可能需要进行速率限制。
验证的详细信息在此处未做具体说明。实现者应在适当的地方添加防御措施。
对等节点能力
一个或多个能力可以在"caps"选项中发布。能力可以按任意顺序排列,但建议使用"BC46"顺序,以确保各实现之间的一致性。
B : 如果对等节点地址包含 ‘B’ 能力,这意味着它们愿意并且能够作为 ‘Bob’ 或 ‘Charlie’ 参与对等节点测试。在 0.9.26 版本之前,IPv6 地址不支持对等节点测试,如果 IPv6 地址中存在 ‘B’ 能力,必须忽略它。从 0.9.27 版本开始,IPv6 地址支持对等节点测试,IPv6 地址中 ‘B’ 能力的存在或缺失表示实际的支持情况(或不支持)。
C:如果对等节点地址包含 ‘C’ 能力,这意味着它们愿意并能够通过该地址充当介绍者 - 为无法直接到达的 Charlie 充当介绍者 Bob。在 0.9.50 版本发布之前,Java router 错误地为 IPv6 地址发布了 ‘C’ 能力,尽管 IPv6 介绍者功能并未完全实现。因此,router 应该假设 0.9.50 之前的版本无法通过 IPv6 充当介绍者,即使宣传了 ‘C’ 能力。
4 : 从 0.9.50 版本开始,表示出站 IPv4 能力。如果在 host 字段中发布了 IP,则此能力不是必需的。如果这是一个带有 IPv4 介绍人的地址,应包含 ‘4’。如果 router 是隐藏的,‘4’ 和 ‘6’ 可以在单个地址中组合。
6 : 从 0.9.50 版本开始,表示出站 IPv6 能力。如果在 host 字段中发布了 IP 地址,则不需要此能力标识。如果这是一个带有 introducer 用于 IPv6 介绍的地址,应包含 ‘6’(目前不支持)。如果 router 是隐藏的,‘4’ 和 ‘6’ 可以在单个地址中组合使用。
未来工作
注意:这些问题将在 SSU2 开发过程中得到解决。
对当前 SSU 性能的分析,包括评估窗口大小调整和其他参数,以及调整协议实现以提高性能,是未来工作的一个主题。
当前实现重复发送相同数据包的确认信息,这不必要地增加了开销。
默认的小型 MTU 值 620 应该进行分析并可能需要增加。 当前的 MTU 调整策略应该进行评估。 1730 字节的流式库数据包能否装入 3 个小型 SSU 数据包中?可能不行。
协议应该扩展为在设置期间交换 MTU。
密钥更新目前未实现,也永远不会实现。
RelayIntro 和 RelayResponse 中 ‘challenge’ 字段的潜在用途,以及 SessionRequest 和 SessionCreated 中 padding 字段的使用,都没有文档记录。
一组固定的数据包大小可能适合进一步向外部对手隐藏数据分片,但在那之前,tunnel、garlic 和端到端填充应该足以满足大多数需求。
SessionCreated 和 SessionConfirmed 中的签名时间似乎未使用或未验证。