盲中继滥用防护
AeroNyx 节点在转发加密中继流量之前施加的限制:上一跳速率限制与隔离、重启后仍有效的路由重放抑制、有边界的重试和 CPU 准入,且只使用已签名的元数据和汇总计数器。
盲中继滥用防护是 AeroNyx 节点在为其他节点转发加密中继流量之前所施加的一组限制。它仅凭已签名的路由元数据和本地计数器来遏制滥用或不稳定的节点。它从不解析密文、保留路由历史,也从不暴露谁在与谁通信。
范围
| 控制项 | 状态 |
|---|---|
| 不透明载荷转发 | 已实现 |
| 签名新鲜度检查与路由重放抑制 | 已实现。当节点的聊天中继存储启用时,重放状态在重启后仍可保留 |
| 上一跳速率限制与隔离 | 已实现 |
| 进程级请求与加密运算准入限制 | 已实现 |
| 节点健康状态和 Nodeboard 中的汇总运行时证据 | 已实现 |
| 检查明文或载荷 | 未实现,出于设计考虑 |
| 按用户、按路由或社交图谱的分析 | 未实现,出于设计考虑 |
盲节点规则
中继可以认证路由元数据、执行有边界的策略、转发不透明信封并返回签名回执。它绝不能检查载荷,也绝不能暴露可用于重建路由、用户身份或关系的元数据。具体而言,中继不会记录或暴露:
- 消息、数据包、媒体或记忆的明文
- DNS 查询、目的地、域名、URL 或浏览历史
- 路由 ID、完整路径、端点 URL 或客户端 IP 地址
- 完整公钥、接收方身份、消息 ID 或社交图谱关系
- 私钥、凭证秘密、钱包级流量或解密材料
准入流水线
每个请求在能够使用转发容量之前都要通过以下检查。第 1 至 6 步按此顺序执行。在第 7 步中,发往本节点的请求会进入其自身的终端或洋葱处理流程,而发往其他节点的请求则会在解析出下一跳之后进行预留。
1. request gate: process-wide admission window and in-flight limit (64 requests)
2. crypto admission: a free slot for signature and onion work, or Backpressure
3. authenticate the signed previous hop and envelope
4. previous-hop rate limit and quarantine check
5. envelope metadata: size, age and future-skew limits
6. self-loop check
7. route loop, TTL and next-hop descriptor checks, plus the route replay reservation
8. forward the opaque ciphertext, or peel the terminal layer and deliver
每项决策都基于已签名的路由元数据和本地汇总状态,从不基于解密后的内容。
运行时限制
以下是节点源代码中的当前默认值。它们是运行参数,而非永久性的协议承诺。
| 常量 | 默认值 |
|---|---|
BLIND_RELAY_ROUTE_REPLAY_CAPACITY | 8192 条活跃路由 |
BLIND_RELAY_ROUTE_REPLAY_TTL_SECS | 600 秒 |
PREVIOUS_HOP_RATE_LIMIT(每 PREVIOUS_HOP_RATE_WINDOW_SECS) | 每 60 秒 120 个请求 |
PREVIOUS_HOP_FAILURE_THRESHOLD(每 PREVIOUS_HOP_FAILURE_WINDOW_SECS) | 每 300 秒 12 次计分失败 |
PREVIOUS_HOP_QUARANTINE_SECS | 300 秒 |
MAX_PREVIOUS_HOP_BUCKETS | 4096 个桶 |
BLIND_RELAY_MAX_ENVELOPE_AGE_SECS | 600 秒 |
BLIND_RELAY_MAX_FUTURE_SKEW_SECS | 120 秒 |
BLIND_RELAY_DELIVERY_RECEIPT_MAX_AGE_SECS | 120 秒 |
DEFAULT_MAX_ATTEMPTS(转发重试) | 3 次尝试 |
MAX_IN_FLIGHT_BLIND_RELAY_REQUESTS | 64 个请求 |
上一跳速率限制与隔离
速率限制以上一跳的签名身份为键。同一个上一跳在 60 秒内发送超过 120 个请求,就会对该跳启动为期五分钟的本地隔离。另有一个独立的失败计分,在五分钟内出现 12 次计分失败后也会启动同样的隔离。只有以下原因会计入分数:
invalid_previous_hop | invalid_signature | self_loop | route_loop | ttl_exhausted
传输超时、确认丢失、重复重试和重放冲突不会增加分数,因此健康的节点不会因网络问题而被隔离。桶存储有上限,空闲的桶会被逐出,因此该防护机制不会演变为长期的通信图谱。
路由重放与重试
每个中继请求都携带一个路由 ID。当节点的聊天中继存储启用时([memchain.chat_relay] enabled = true),路由预留和已完成的响应会保存在其 SQLite 数据库中,因此重放保护在重启后仍然有效。否则,节点使用具有相同容量和时间窗口的内存注册表。如果全部 8192 个槽位都被活跃路由占用,新路由会以 replay_capacity 被拒绝,直到有槽位过期。
重复的路由 ID 有以下四种结果之一:
| 情形 | 响应 | 对上一跳的影响 |
|---|---|---|
| 相同请求;首次尝试已完成 | 重放已存储的响应,包括任何由终端签名的回执。不会再次投递或转发任何内容(duplicate_route) | 计为成功 |
| 相同请求;首次尝试仍在进行 | HTTP 503 route_in_flight。发送方可以重试 | 无 |
| 同一路由 ID 下的不同请求 | HTTP 400 replay_conflict | 记录在节点健康状态中。不增加隔离分数 |
| 相同请求,但已存储的响应超过 120 秒 | HTTP 409 replay_response_expired。该路由在其 600 秒窗口的剩余时间内保持预留状态,因此无法再次运行 | 无 |
最后一种结果适用于持久化存储。签名回执只有作为新鲜证据时才有效,因此节点不会在 120 秒后重放回执。
当向下一跳转发因超时、连接错误或请求错误而失败时,节点最多总共尝试三次。每次重试前的短暂延迟由路由 ID、下一跳和尝试次数推导得出。下游节点报告为明确拒绝的失败不会重试。
加密运算准入(运营者容量说明)
签名检查、洋葱剥层和回执签名都是 CPU 密集型操作,因此每个节点进程都会对其加以限制:
- 槽位总数: 主机报告的 CPU 线程数的一半,向上取整,最少 4 个,最多 8 个,因此即使只有一两个线程的主机也有 4 个槽位。
- 入站请求: 比总数少一个槽位,以确保出站路由创建和回执完成始终能够推进。因此在最小的主机上,入站请求也有 3 个槽位。
- 所有槽位都繁忙时: 请求会立即以 HTTP 429 和
backpressure原因被拒绝。它不会在队列中等待。
因此,在小型主机上,中继在突发流量下会较早地卸载负载。backpressure_dropped 计数器不断上升,意味着节点需要更多 CPU 或更少的中继流量。
汇总运行时计数器
节点在其心跳中上报粗粒度的累计计数器和新鲜度时间戳,位于:
system_stats.discovery_status.peer_store.runtime.blind_relay
system_stats.discovery_status.peer_store.blind_relay_quality
system_stats.discovery_status.peer_store.peer_health_summary
| 计数器组 | 字段 |
|---|---|
| 接收与处置 | received、terminal、forwarded、rejected |
| 校验与保护 | invalid_signature、envelope_too_large、ttl_exhausted、no_route、invalid_endpoint、loop_detected、replay_dropped、timestamp_rejected、rate_limited、quarantined、quarantine_started |
| 传输与重试 | backpressure_dropped、forward_failed、retry_attempted、retry_succeeded、retry_exhausted |
| 合成探测 | probe_attempted、probe_succeeded、probe_failed、two_hop_probe_attempted、two_hop_probe_succeeded、two_hop_probe_failed |
| 真实投递与新鲜度 | verified_client_onion_deliveries、last_verified_client_onion_delivery_at、last_accepted_at、last_event_at |
这些计数器描述的是节点运行情况。它们不是消息日志或计费数据,也不能证明任何特定对话曾经发生。
证据质量
质量摘要将四类证据区分开来:已接受的不透明中继工作、合成可达性探测、合成两跳探测,以及针对客户端发起的投递的终端签名回执。real_relay_ready 要求有一份新鲜且经过认证的客户端投递回执。合成证据从不会被报告为用户流量。
status | 含义 |
|---|---|
idle | 尚无中继或探测活动 |
observing | 已有部分证据,但尚未确认就绪 |
stale | 先前的成功证据已不再新鲜 |
ready | 有新鲜的已接受工作或符合条件的证明,且没有活跃的传输问题 |
protecting | 滥用保护已被触发,同时中继仍在继续工作 |
degraded | 转发或探测失败需要调查 |
attention | 背压或重试耗尽需要运营者及时处理 |
proof_scope 指明所见到的最强证据:client_message_delivery、relay_acceptance、message_delivery、control_plane、single_hop_control_plane、attempted 或 none。
不暴露流量关系的节点健康状态
peer_health_summary 让运营者能够找出出现故障或被隔离的节点,而无需看到谁在与谁通信。
包含: 缩短的节点 ID 前缀、粗粒度的健康和描述符状态、gossip 和路由成功的新鲜度、路由成功和失败的汇总计数、隔离状态、剩余时间以及粗粒度的原因分桶。
从不包含: 完整的节点公钥、路由 ID、端点列表、加密数据块或载荷哈希、消息 ID、接收方身份、客户端 IP 地址、目的地、DNS 数据或社交图谱关系。
源代码索引
| 层级 | 在节点仓库中的路径 | 作用 |
|---|---|---|
| 中继编排 | crates/aeronyx-server/src/api/chat_peer/blind_relay.rs | 请求闸门、加密运算准入、认证、环路和 TTL 检查、重放结果、转发和终端处理 |
| HTTP 接口与限制 | crates/aeronyx-server/src/api/chat_peer.rs | 路由、请求体和在途限制、错误码和原因分桶 |
| 滥用策略 | crates/aeronyx-server/src/api/chat_peer_abuse_guard.rs | 上一跳速率限制、失败计分和隔离 |
| 重放注册表 | crates/aeronyx-server/src/api/chat_peer_replay.rs | 内存注册表和已存储响应的格式 |
| 持久化重放存储 | crates/aeronyx-server/src/services/chat_relay_blind_route_store.rs | SQLite 路由预留和响应 |
| 重试策略 | crates/aeronyx-server/src/api/chat_peer_retry.rs | 可重试失败与明确失败的区分以及重试时机 |
| 计数器与质量 | crates/aeronyx-server/src/services/peer_store.rs 和 services/peer_store/status.rs | 汇总计数器、就绪状态和证明分类 |
| 健康上报 | crates/aeronyx-server/src/api/vpn_health.rs 和 management/reporter.rs | 本地健康 JSON 和心跳载荷 |
节点软件以 AGPL-3.0 许可在 github.com/AeroNyxNetwork/AeroNyx 开源。路由的构建方式请参阅 节点发现与加密多跳中继。
<!-- faq:start -->常见问题
中继节点能读取或记录它转发的流量吗?
不能。中继检查已签名的路由元数据并转发不透明信封。它只保留汇总计数器,从不保留载荷、路由历史或接收方身份。
当节点重试同一路由时会发生什么?
如果首次尝试已完成,节点会重放其已存储的响应,不会再次投递任何内容。如果首次尝试仍在进行,节点会返回可重试的 route_in_flight。同一路由 ID 下的不同请求会以 replay_conflict 被拒绝。
重放保护在节点重启后仍然有效吗?
是的,前提是节点的聊天中继存储已启用。此时路由预留和已完成的响应会保存在 SQLite 中。如果没有该存储,节点会使用重启时即被清空的内存注册表。
为什么某个节点会被隔离?
节点在 60 秒内发送超过 120 个请求,或在五分钟内出现 12 次计分的校验失败后,会被隔离五分钟。超时、确认丢失和重复重试不计为失败。
为什么我的节点在负载下会返回 backpressure?
因为其所有中继加密运算槽位都处于繁忙状态。节点为签名和洋葱运算分配约为其 CPU 线程数一半的槽位(最少 4 个,最多 8 个),并会立即拒绝超出的请求,而不是将其排队。
<!-- faq:end -->