P2P 评分系统和网络安全
Number | Category | Status | Author | Organization | Created |
---|---|---|---|---|---|
0007 | Standards Track | Proposal | Jinyang Jiang | Nervos Foundation | 2018-10-02 |
P2P 评分系统和网络安全
简介
本篇 RFC 描述了 CKB P2P 网络层的评分系统,以及基于评分的网络安全策略。
目标
CKB 网络被设计为开放的 P2P 网络,任何节点都能无需许可的加入网络,但网络的开放性同时使得恶意节点也能够加入并对 P2P 网络进行攻击。
同样采用开放性 P2P 网络的比特币和以太坊中都曾有「日蚀攻击」的安全问题。 日蚀攻击的原理是攻击者通过操纵恶意节点占领受害者节点所有的 Peers 连接,以此控制受害者节点可见的网络。
攻击者可以用极少成本实施日蚀攻击,攻击成功后可以操纵受害节点的算力做些恶意行为, 或欺骗受害节点进行双花交易。
参考论文 -- Eclipse Attacks on Bitcoin’s Peer-to-Peer Network
论文中同时提出了几种防范手段, 其中部分已经在比特币主网应用, 本 RFC 参考比特币网络的实现,描述如何在 CKB 网络中正确应用这些措施。
RFC 同时描述了 CKB P2P 网络的评分机制, 结合 CKB 的评分机制,可以使用比特币中成熟的安全措施来处理更加通用的攻击场景。
基于 CKB 的评分机制,我们遵循几条规则来处理恶意 Peers:
- 节点应尽可能的存储已知的 Peers 信息
- 节点需要不断对 Peer 的好行为和坏行为进行评分
- 节点应保留好的(分数高的) Peer,驱逐坏的(分数低) Peer
RFC 描述了客户端应该实现的打分系统和下文的几种安全策略。
Specification
术语
Node
- 节点Peer
- 网络上的其他节点PeerInfo
- 描述 Peer 信息的数据结构PeerStore
- 用于存储 PeerInfo 的组件outbound peer
- 主动发起连接的节点inbound peer
- 被动接受连接的节点max_outbound
- 节点主动连接的 Peers 上限max_inbound
- 节点被动接受的 Peers 上限network group
- 驱逐节点时用到的概念,对 Peer 连接时的 IP 计算,IPv4 取前 16 位,Ipv6 取前 32 位
PeerStore 和 PeerInfo
PeerStore 应该做到持久化存储, 并尽可能多的储存已知的 PeerInfo
PeerInfo 至少包含以下内容
PeerInfo { NodeId, // Peer 的 NodeId ConnectedIP, // 连接时的 IP Direction, // Inbound or Outbound LastConnectedAt, // 最后一次连接的时间 Score // 分数}
评分系统
评分系统需要以下参数
PEER_INIT_SCORE
- Peers 的初始分数BEHAVIOURS
- 节点的行为, 如UNEXPECTED_DISCONNECT
,TIMEOUT
,CONNECTED
等SCORING_SCHEMA
- 描述不同行为对应的分数, 如{"TIMEOUT": -10, "CONNECTED": 10}
BAN_SCORE
- Peer 评分低于此值时会被加入黑名单
网络层应该提供评分接口,允许 sync
, relay
等上层子协议报告 peer 行为,
并根据 peer 行为和 SCORING_SCHEMA
调整 peer 的评分。
peer.score += SCOREING_SCHEMA[BEHAVIOUR]
Peer 的评分是 CKB P2P 网络安全的重要部分,peer 的行为可以分为如下三种:
符合协议的行为: * 如: 从 peer 获取了新的 block、节点成功连接上 peer 。 当 peer 作出符合协议的行为时,节点应上调对 peer 评分, 考虑恶意 Peer 有可能在攻击前进行伪装, 对好行为奖励的分数不应一次性奖励太多, 而是鼓励 peer 长期进行好的行为来积累信用。
可能由于网络异常导致的行为: * 如: peer 异常断开、连接 peer 失败、ping timeout。 对这些行为我们采用宽容性的惩罚,下调对 peer 的评分,但不会一次性下调太多。
明显违反协议的行为: * 如: peer 发送无法解码的内容、peer 发送 invalid block, peer 发送 invalid transaction。 当我们可以确定 peer 存在明显的恶意行为时,对 peer 打低分,如果 peer 评分低于
BAN_SCORE
,将 peer 加入黑名单并禁止连接。
例子:
- peer 1 连接成功,节点报告 peer1
CONNECTED
行为,peer 1 加 10 分 - peer 2 连接超时,节点报告 peer2
TIMEOUT
行为,peer 2 减 10 分 - peer 1 通过
sync
协议发送重复的请求,节点报告 peer 1DUPLICATED_REQUEST_BLOCK
行为,peer 1 减 50 分 - peer 1 被扣分直至低于
BAN_SCORE
, 被断开连接并加入黑名单
BEHAVIOURS
、 SCORING_SCHEMA
等参数不属于共识协议的一部分,CKB 实现应该根据网络实际的情况对参数调整。
节点 outbound peers 的选择策略
日蚀攻击论文中提到了比特币节点重启时的安全问题:
- 攻击者事先利用比特币的节点发现规则填充受害节点的地址列表
- 攻击者等待或诱发受害者节点重启
- 重启后,受害者节点会从 addrman (类似 peer store) 中选择一些地址连接
- 受害节点的所有对外的连接都连接到了恶意 peers 则攻击者攻击成功
CKB 在初始化网络时应该避免这些问题
Outbound peers 连接流程
参数说明:
TRY_SCORE
- 设置一个分数,仅当 PeerInfo 分数高于TRY_SCORE
时节点才会去尝试连接ANCHOR_PEERS
- 锚点 peer 的数量,值应该小于max_outbound
如2
变量:
try_new_outbound_peer
- 设置节点是否该继续发起新的 Outbound 连接
选择一个 outbound peer 的流程:
- 如果当前连接的 outbound peers 小于
ANCHOR_PEERS
执行 2, 否则执行 3 - 选择一个锚点 peer:
- 从 PeerStore 挑选最后连接过的
max_bound
个 outbound peers 作为recent_peers
- 如果
recent_peers
为空则执行 3,否则从recent_peers
中选择分数最高的节点作为 outbound peer 返回
- 从 PeerStore 挑选最后连接过的
- 在 PeerStore 中随机选择一个分数大于
TRY_SCORE
且NetworkGroup
和当前连接的 outbound peers 都不相同的 peer info,如果找不到这样的 peer info 则执行 4,否则将这个 peer info 返回 - 从
boot_nodes
中随机选择一个返回
伪代码
# 找到一个 outbound peer 候选def find_outbound_peer connected_outbound_peers = connected_peers.select{|peer| peer.outbound? && !peer.feeler? } if connected_outbound_peers.length < ANCHOR_PEERS find_anchor_peer() || find_random_peer() || random_boot_node() else find_random_peer() || random_boot_node() endend
def find_anchor_peer last_connected_peers = peer_store.sort_by{|peer| -peer.last_connected_at}.take(max_bound) # 返回最高分的 peer info last_connected_peers.sort_by(&:score).lastend
def find_random_peer connected_outbound_peers = connected_peers.select{|peer| peer.outbound? && !peer.feeler? } exists_network_groups = connected_outbound_peers.map(&:network_group) candidate_peers = peer_store.select do |peer| peer.score >= TRY_SCORE && !exists_network_groups.include?(peer.network_group) end candidate_peers.sampleend
def random_boot_node boot_nodes.sampleend
节点应该重复以上过程,直到节点正在连接的 outbound peers 数量大于等于 max_outbound
并且 try_new_outbound_peer
为 false
。
check_outbound_peers_interval = 15# 每隔几分钟检查 outbound peers 数量loop do sleep(check_outbound_peers_interval) connected_outbound_peers = connected_peers.select{|peer| peer.outbound? && !peer.feeler? } if connected_outbound_peers.length >= max_outbound && !try_new_outbound_peer next end new_outbound_peer = find_outbound_peer() connect_peer(new_outbound_peer)end
try_new_outbound_peer
的作用是在一定时间内无法发现有效消息时,允许节点连接更多的 outbound peers,这个机制在后文介绍。
该策略在节点没有 Peers 时会强制从最近连接过的 outbound peers 中选择,这个行为参考了日蚀攻击论文中的 Anchor Connection 策略。
攻击者需要做到以下条件才可以成功实施日蚀攻击
- 攻击者有
n
个伪装节点(n == ANCHOR_PEERS
) 成为受害者节点的 outbound peers,这些伪装节点同时要拥有最高得分 - 攻击者需要准备至少
max_outbound - ANCHOR_PEERS
个伪装节点地址在受害者节点的 PeerStore,并且受害者节点的随机挑选的max_outbound - ANCHOR_PEERS
个 outbound peers 全部是攻击者的伪装节点。
额外的 outbound peers 连接和驱逐
网络组件应该每隔几分钟检测子协议中的主要协议如 sync
协议是否工作
def sync_maybe_stale now = Time.now # 可以通过上次 Tip 更新时间,出块间隔和当前时间判断 sync 是否正常工作 last_tip_updated_at < now - block_produce_interval * nend
当我们发现 sync
协议无法正常工作时,应该设置 try_new_outbound_peer
变量为 true
,当发现 sync
协议恢复正常时设置 try_new_outbound_peer
为 false
check_sync_stale_at = Time.nowloop_interval = 30check_sync_stale_interval = 15 * 60 #(15 minutes)
loop do sleep(loop_interval) # try evict evict_extra_outbound_peers() now = Time.now if check_sync_stale_at >= now set_try_new_outbound_peer(sync_maybe_stale()) check_sync_stale_at = now + check_sync_stale_interval endend
当 try_new_outbound_peer
为 true
时 CKB 网络将会持续的尝试连接额外的 outbound peers,并每隔几分钟尝试逐出没有用的额外 outbound peers,这个行为防止节点有过多的连接。
def evict_extra_outbound_peers connected_outbound_peers = connected_peers.select{|peer| peer.outbound? && !peer.feeler? } if connected_outbound_peers.length <= max_outbound return end now = Time.now # 找出连接的 outbound peers 中 last_block_announcement_at 最老的 peer evict_target = connected_outbound_peers.sort_by do |peer| peer.last_block_announcement_at end.first if evict_target # 至少连接上这个 peer 一段时间,且当前没有从这个 peer 下载块 if now - evict_target.last_connected_at > MINIMUM_CONNECT_TIME && !is_downloading?(evict_target) disconnect_peer(evict_target) # 防止连接过多的 outbound peer set_try_new_outbound_peer(false) end endend
节点 inbound peers 接受机制
比特币中当节点的被动 peers 连满同时又有新 peer 尝试连接时,节点会对已有 peers 进行驱逐测试(详细请参考 Bitcoin 源码)。
驱逐测试的目的在于节点保留高质量 peer 的同时,驱逐低质量的 peer。
CKB 参考了比特币的驱逐测试,步骤如下:
- 找出当前连接的所有 inbound peers 作为
candidate_peers
- 保护 peers (
N
代表每一步中我们想要保护的 peers 数量):- 从
candidate_peers
找出N
个分数最高的 peers 删除 - 从
candidate_peers
找出N
个 ping 最小的 peers 删除 - 从
candidate_peers
找出N
个最近发送消息给我们的 peers 删除 - 从
candidate_peers
找出candidate_peers.size / 2
个连接时间最久的 peers 删除
- 从
- 按照
network group
对剩余的candidate_peers
分组 - 找出包含最多 peers 的组
- 驱逐组中分数最低的 peer,找不到 peer 驱逐时则拒绝新 peer 的连接
我们基于攻击者难以模拟或操纵的特征来保护一些 peers 免受驱逐,以增强网络的安全性。
Feeler Connection
Feeler Connection 机制的目的在于测试 Peer 是否可以连接。
当节点的 outbound peers 数量达到 max_outbound
限制时,
节点会每隔一段时间(一般是几分钟)主动发起 feeler connection:
- 从 PeerStore 中随机选出一个未连接过的 peer info
- 连接该 peer
- 执行握手协议
- 断开连接
Feeler peer 会被假设为很快断开连接
PeerStore 清理
设置一些参数:
PEER_STORE_LIMIT
- PeerStore 最多可以存储的 PeerInfo 数量
PEER_NOT_SEEN_TIMEOUT
- 用于判断 peer info 是否该被清理,如该值设为 15 天,则表示最近 15 天内连接过的 peer 不会被清理
PeerStore 中存储的 PeerInfo 数量达到 PEER_STORE_LIMIT
时需要清理,过程如下:
- 按照
network group
给 PeerStore 中的 PeerInfo 分组 - 找出包含最多节点的组
- 在组中搜索最近没有连接过的 peers
peer.last_connected_at < Time.now - PEER_NOT_SEEN_TIMEOUT
- 在该集合中找到分数最低的 PeerInfo
candidate_peer_info
- 如果
candidate_peer_info.score < new_peer_info.score
则删掉candidate_peer_info
并插入new_peer_info
,否则不接受new_peer_info