比特币源码分析--深入理解区块链 6.点对点网络:与其它节点建立连接和预防Erebus攻击

        点对点(Peer to Peer 简称P2P)网络是无中心服务器、依靠用户群(peers)交换信息的互联网体系,它的作用在于,减低以往网路传输中的节点,以降低资料遗失的风险。与有中心服务器的中央网络系统不同,对等网络的每个用户端既是一个节点,也有服务器的功能,我们通常称一个节点为Peer。使用过Socket网络编程的都知道这样的一个过程:作为客户端一般主动发起连接,连接到指定的IP地址和端口。作为服务器端,在绑定IP地址和端口后,被动接收来自客户端的连接。在比特币的P2P网络体系中,一个节点既可以是客户端 (Client),也可以是服务器(Server),在部署Bitcoin客户端程序后,程序会自动创建Socket服务器用于接收来自其他节点的连接,同时也会主动连接到其它节点,因此Bitcoin是典型的P2P网络应用。在使用Socket通信的应用程序中,客户端和服务器端在程序中执行的一般过程如下:

        上述图中仅仅为了说明作为客户端或服务器在使用Socket网络编程时执行的一般过程,实际上要复杂得多。可以看出P2P网络中作为服务器一方时,要处理的步骤比作为客户端要多,同时服务器还要考虑数据的并发处理和同时应付多个连接请求。下图是无中心服务器的对等网络系统和有中心服务器的中央网络系统拓扑图:

                      
无中心服务器的对等网络系统                               有中心服务器的中央网络系统

Bitcoin使用的网络协议栈

Bitcoin支持IPv4和IPv6,在应用层使用了多种网络协议,下图列出了它使用的主要网络协议:

onion: 是一个用于在Tor网络上寻址特殊用途的顶级域后缀。这种后缀不属于实际的域名,也并未收录于域名根区中。但只要安装了正确的代理软件,如类似于浏览器的网络软件,即可通过Tor服务器发送特定的请求来访问.onion地址。使用这种技术可以使得信息提供商与用户难以被中间经过的网络主机或外界用户所追踪。

I2P:(Invisible Internet Project即“隐形网计划”),是一项混合授权的匿名网络项目。 I2P网络是由I2P路由器以大蒜路由方式组成的表层网络,建立于其上的应用程序可以安全匿名的相互通讯。它可以同时使用UDP及TCP协议,支援UPnP映射。其应用包括匿名上网、聊天、撰写博客和文档传输。

CJDNS: 利用“加密的IPv6”及“公钥加密”來分配网络地址并利用“Distributed Hash Table”进行路由。它能提供近似“零配置网络(Zero-Configuration Networking)”,并且能防范在现有网络中存在的很多和安全、可扩展性相关的问题。

UPNP: 通用即插即用(Universal Plug and Play,简称UPnP)是由“通用即插即用论坛”(UPnP™ Forum)推广的一套网络协议。该协议的目标是使家庭网络(数据共享、通信和娱乐)和公司网络中的各种设备能够相互无缝连接,并简化相关网络的实现。UPnP通过定义和发布基于开放、因特网通讯网协议标准的UPnP设备控制协议来实现这一目标。 UPnP这个概念是从隨插即用(Plug-and-play)衍生而来的,即插即用是一种热拔插技术。

NAT-PMP: NAT端口映射协议(NAT Port Mapping Protocol,缩写NAT-PMP)是一个能自动建立网络地址转换(NAT)设置和端口映射配置而无需用户介入的网络协议。该协议能自动测定NAT网关的外部IPv4地址,并为应用程序提供与对等端交流通信的方法。NAT-PMP于2005年由苹果公司推出,为更常见的ISO标准互联网网关设备协议(被许多NAT路由器实现)的一个替代品。NAT-PMP是端口控制协议(PCP)的前身。

在本文中我们重点说明节点如何使用Stream TCP Socket协议与其他对等节点建立连接。

 Bitcoin P2P网络类图

P2P网络相关类图

 上图列出了相关的类以及该类对应的程序路径,分别说明如下:

1. 网络地址

        跟Bitcoin的网络地址有关的类一共有四个,分别是

        CNetAddr:基类,实现网络地址的存储、读取和转换等基本功能。

        CService: 继承于CNetAddr,增加了端口属性。

        CAddress: 继承于CService增加了节点存活时间、服务功能标记。

        AddrInfo:继承于CAddress,包含节点统计信息。

        以上的类的设计符合面向对象设计原则,避免将所有信息集中在同一个类,通过继承来实现功能迭代,应用于不同的场合。Bitcoin支持的网络地址类型有以下几种:

NET_UNROUTABLE

无法路由的非公网地址

NET_IPV4

IPv4地址

NET_IPV6

IPv6地址

NET_ONION

洋葱网络地址

NET_I2P

I2P地址

NET_CJDNS

CJDNS地址

NET_INTERNAL

内部使用,可使用地址的Hash值来替代地址数据,为了实现地址管理的方便。

2.节点地址管理AddrMan

        AddrMan设计用来管理节点地址,防止黑客攻击和保护正常的节点连接,淘汰过期节点等。

3. 地址数据管理addrdb

        实现节点地址数据的序列化(保存)、反序列化(读取)。

4. 连接管理CConnman

        实现P2P节点的客户端和服务器端功能,实现从DNS种子域名获得节点地址,建立两种类型的连接(inbound/outbound)以及管理连接等。

5. 节点禁止BanMan

        识别违规的节点并阻止与其连接。

Bitcoin网络种子

        初次使用Bitcoin客户端时,你尚未连接到任何对等节点,因此也无法实现数据传输。Bitcoin代码内置了一些默认的种子域名(DNS seed),你可以通过解析这些域名来获得相应的节点(Peer)地址。下图是从DNS种子获得节点IP的流程图:

        DNS种子域名使用了服务过滤,也就是说在提供的域名前面可以加上前缀来识别具有不同服务功能域名,从这些域名获得的IP地址就表明该节点能提供指定的服务。在解析这些域名时,可能返回多个IP地址。下面介绍如何使用服务过滤的DNS种子。

        DNS种子分为有服务过滤的种子和固定种子,不同的Bitcoin网络种子也不相同,下表是Bitcoin在代码中内置的种子: 

网络

服务过滤种子

固定种子

端口

MAIN

seed.bitcoin.sipa.be.

dnsseed.bluematt.me.

dnsseed.bitcoin.dashjr.org.

seed.bitcoinstats.com.

seed.bitcoin.jonasschnelli.ch.

seed.btc.petertodd.org.

seed.bitcoin.sprovoost.nl.

dnsseed.emzy.de.

seed.bitcoin.wiz.biz.

src/chainparamsseeds.h: chainparams_seed_main,后面介绍如何解析。

8333

TESTNET

testnet-seed.bitcoin.jonasschnelli.ch.

seed.tbtc.petertodd.org.

seed.testnet.bitcoin.sprovoost.nl.

testnet-seed.bluematt.me.

src/chainparamsseeds.h: chainparams_seed_test

18333

SIGNET

seed.signet.bitcoin.sprovoost.nl.

178.128.221.177

38333

REGTEST

18444

为了搞清楚服务过滤,我们先了解一下节点能提供哪些服务(ServiceFlags)以及这些服务标记对应的值:

服务标记

功能说明

NODE_NONE

0

NODE_NETWORK

表示该节点能提供完整的区块

1

NODE_BLOOM

表示节点有能力并愿意处理布隆过滤连接

4

NODE_WITNESS

表示可以向节点请求区块和交易,包括见证数据

8

NODE_COMPACT_FILTERS

意味着节点将为基本块过滤请求提供服务

64

NODE_NETWORK_LIMITED

与 NODE_NETWORK 相同,但仅限于提供最后 288 个(2 天)块

1024

 1. 服务过滤

        在解析上面表格中的DNS种子域名时,会通过x<服务标记16进制>.域名的方式拼接出一个新的域名。假设我们想从域名seed.bitcoin.sipa.be.获得具有NODE_NETWORK和NODE_WITNESS服务的节点地址时,先将两个服务标记进行“或”运算,其结果是 NODE_NETWORK | NODE_WITNESS = 9,所以完整的域名就是x9.seed.bitcoin.sipa.be.

        如果要获取具备NODE_NETWORK | NODE_BLOOM | NODE_WITNESS服务的节点地址,那就使用xd.seed.bitcoin.sipa.be. 。

        在SIGNET网络中,有一个IPv4的地址直接作为种子,表明该地址不提供服务过滤,直接作为可连接的节点地址来使用。

2. 其他种子来源

        除了上面的通过系统内置的DNS和固定种子获得节点地址外,还可以通过人工设置参数来添加种子节点。以下是用到的三个参数:

-connect -seednode、-addnode。

这三个参数均可以使用多次,从而可以添加多个节点地址。

-connect的优先级最高,它设置后,Bitcoin客户端将不再使用系统的提供的具有服务过滤的节点。并且它的连接数量没有限制。

-seednode有数量限制,连接到通过它设置的节点数量不超过max outbound规定的数量,该值一般等于11。 这里说的数量限制是指在连接时的节点数量限制,设置参数时的数量并没有限制。

-addnode: 该参数添加的节点在连接时有数量限制,一般不超过8个,用户也可以通过RPC命令动态添加节点。

节点连接

 1. 连接类型

类型

说明

INBOUND

入站连接,当前节点相当于服务器

OUTBOUND_FULL_RELAY

这些是我们用来连接网络的默认连接。 对转发的内容没有限制; 默认情况下,我们转发区块、地址和交易。

MANUAL

用户通过 addnode RPC 命令或 -addnode/-connect 配置选项明确请求的手动连接。

FEELER

试探性连接是用于检查节点是否处于活动状态的短期连接。

BLOCK_RELAY

使用块中继连接来帮助防止分叉

攻击。

ADDR_FETCH

用于请求的短期连接

        除inbound类型之外,其他类型都属于outbound连接。Inbound表示节点具有服务器功能,它被动接收来自其它节点的连接。而outbound连接表示当前节点作为客户端,须主动发起连接请求。 

2. 连接示意图

         手工添加的节点有两个来源:通过命令行bitcoin-cli -addnode动态添加,其次是设置参数-addnode项目。连接到手工添加的节点数量最大不超过8个。

        上图是种子连接示意图,种子节点也有两个来源,一是解析具有服务过滤功能的DNS种子域名后获得的节点IP地址,二是通过参数-seednode设置的节点IP,seednode与不具备服务过滤的种子节点合称为ADDR_FETCH,为用户提供短期连接,上图中ProcessAddrFetch就是连接此类节点。

        解析具有服务过滤功能的种子域名后获得节点IP地址加入到AddrMan管理器,它是一个具有防止黑客攻击,同时能定期管理节点连接,排除恶意节点,维持健康正常连接的模块。但是,当用户设置了连接到指定节点时(通过-connect参数指定),也就是上图的红色部分,这时AddrMan不会发挥作用。指定节点意味着连接数量不受限制,而其它来源连接的节点数量都有最大数量限制。

3. 允许最大连接节点数量

        这里连接节点的数量是指outbound类型的连接,也就是说允许最多连接多少个节点。通过分析/src/net.cpp中的代码得知,最大连接节点的数量由两个重要的信号变量来决定的,如下代码:

if (semOutbound == nullptr) {
        // initialize semaphore
        semOutbound = std::make_unique<CSemaphore>(std::min(m_max_outbound, nMaxConnections));
}

if (semAddnode == nullptr) {
     // initialize semaphore
        semAddnode = std::make_unique<CSemaphore>(nMaxAddnode);
}    

        semOutbound表示outbound类型使用的信号,其中m_max_outbound决定了最大连接数量。该值一般等于11。semAddnode表示通过手工添加节点的方式允许最大的连接数量,其中nMaxAddnode一般等于8。如果系统没有通过参数-connect设置连接到指定节点,那么最大允许连接节点数量=19。如果添加了n个-connect指定的地址,那么最大连接数=19 + n。

3. 通过CSemaphore机制来限制连接节点数量

        Bitcoin中限制连接的数量并不是简单的通过比对两个数字来完成,而是通过一个支持多线程访问的条件变量(condition_variable)来完成的。它的优点很明显,就是在多线程环境下,支持多个线程的并发访问,当使用的数量超过时就一直等待,而当相关资源释放时,使用数量没有达到最大值时又可以继续尝试连接,这种方式比通过简单的轮询或通过线程暂停来实现更加高效和可靠。

抵御理论上的Erebus攻击 

        Erebus攻击允许国家和/或大型网络供应商(如Amazon Web Services) 对比特币交易进行监视、双花或审查。Erebus属于运用比特币P2P特性的一般攻击计划。Erebus在希腊语中意为“影子”,它本身是2015年首次提出的“Eclipse“的衍生词。正如理论上所说,攻击者将尝试尽可能多地连接攻击者想要隔离的节点周围的节点。恶意节点可以通过与它的对等节点连接来影响受害者节点。最终目标是使受害节点的有限的外部连接都连接到恶意方。一旦完成,受害节点会与网络的其他部分隔离。恶意行为者可以控制向受害节点发送事务和信息:这类信息可能与网络的其他正常的通信不尽相同,甚至可能导致区块链分叉或引发审查,下图是Erebus攻击示意图:

 

预防措施:

ASMAP 实验计划:当前Bitcoin推出的asmap实验计划通过将IP地址与网络营运商(ISP)的自治系统AS建立映射。防止同一个ISP过多的IP地址充当一个节点的对等节点,关于如何使用asmap后面再详细介绍。

桶(bucket):将对等节点的IP地址通过与自治系统AS分组并加入随机的熵来计算一个随机值来获得桶的编号,防止同一个AS的IP地址填充满用户端所有的peers节点。

         在从DNS种子域名获取节点地址时,加入到本地接节点数据库peers.dat之前,首先会加入到一个内存表中,该表总共设计有1024个桶(bucket),每个桶可存放64个节点地址。在选择桶的序号时,使用了asmap数据。如下代码:

int AddrInfo::GetNewBucket(const uint256& nKey, const CNetAddr& src, const std::vector<bool>& asmap) const
{
    std::vector<unsigned char> vchSourceGroupKey = src.GetGroup(asmap);
    uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << GetGroup(asmap) << vchSourceGroupKey).GetCheapHash();
    uint64_t hash2 = (CHashWriter(SER_GETHASH, 0) << nKey << vchSourceGroupKey << (hash1 % ADDRMAN_NEW_BUCKETS_PER_SOURCE_GROUP)).GetCheapHash();
    return hash2 % ADDRMAN_NEW_BUCKET_COUNT;
}

int AddrInfo::GetBucketPosition(const uint256& nKey, bool fNew, int nBucket) const
{
    uint64_t hash1 = (CHashWriter(SER_GETHASH, 0) << nKey << (fNew ? uint8_t{'N'} : uint8_t{'K'}) << nBucket << GetKey()).GetCheapHash();
    return hash1 % ADDRMAN_BUCKET_SIZE;
}

         根据asmp获得Group数据,然后与随机数nKey一起组合获得一个uint64_t类型的随机数,再与桶的总数或桶的大小取模运算获得桶的编号和桶的位置,这样可以最大限度防止同一个ISP的IP地址将整个桶填装满,阻止来及国家级或大型ISP的Erebus攻击。

Bitcoin相关代码:

https://github.com/bitcoin/bitcoin/blob/master/src/addrdb.cpp

https://github.com/bitcoin/bitcoin/blob/master/src/addrman.cpp

https://github.com/bitcoin/bitcoin/blob/master/src/net.cpp

猜你喜欢

转载自blog.csdn.net/dragon_trooquant/article/details/122741085