Tor源码分析十 -- 连接和链路

时间:2021-10-05 09:19:44

  源码分析到这个部分,为了让大家明白源码中的编码逻辑,不得不开始从头梳理程序内部的复杂连接和链接组织形式。否则大家后期会更加一头雾水。笔者开始分析源码之时,没有这些宏观的概念,只能死嚼代码,硬猜硬想,再加以检查代码进行验证,才得以明白程序的主要框架逻辑。如果再以猜测验证的模式向大家讲述源码,必定会越来越混乱。所以,在本节之中,我们会将系统中所有的连接类型,链路类型和他们之间的关系和代码之中的关联方式尽量讲明。若大家遇到不明晰的部分,可以参照代码进行查阅。

  在我们进行详细分析之前,先再次给出连接和链接的框架位置图。这个简单的层次图帮助我们理解不同连接和链接所处层次的位置关系,其实已经在我们分析OR连接源码之时给出,之时当时没有进行过多的深入介绍。

                DIR连接,LISTENER连接                  |

              -----------------              |

                AP连接,EXIT连接……        Tor协议上层             |

              ----------------------------   |  应用层

                Circuit链路……           Tor协议中层             |

              ----------------------------   |

                OR连接……                Tor协议下层             |

              ------------------------------------

                TLS连接              传输层


1. 连接

  很明显,系统中的连接类型多种多样,各有各自的不同职能。在此处,我们先罗列出所有系统中存在的连接,其后再对一些我们比较重视的连接类型进行相关说明。

//OR监听连接:本地用于监听远端传来的OR请求,为每个新请求建立一个OR连接;OR监听连接本地全局只有一个;
#define CONN_TYPE_OR_LISTENER 3
//OR连接:基于TLS连接的,主要负责Tor系统内主机之间相互通信的连接;OR连接数量表征本地与多少台主机建立了互联关系,因为他们是一一对应的;
#define CONN_TYPE_OR 4
//EXIT连接:翻译为出口连接是因为该连接会与远端服务器建立socket连接并传递数据;
#define CONN_TYPE_EXIT 5
//AP监听连接:本地用于监听本地应用程序的服务请求而设立的监听连接,为每个服务请求建立一个AP连接;AP监听连接本地全局只有一个;
#define CONN_TYPE_AP_LISTENER 6
//AP连接:基于链路的,主要负责为客户端请求寻找合适链路和传递数据;AP连接数量表征本地应用程序发出的连接请求数量,因为他们是一一对应的;
#define CONN_TYPE_AP 7
//DIR监听连接:Tor系统目录服务器用于监听Tor系统内主机发出的目录相关请求,也就是说该类型连接只在目录服务器上存在;
#define CONN_TYPE_DIR_LISTENER 8
//DIR连接:Tor客户端向Tor目录服务器发送目录请求时需要新建的连接,该连接需要通过AP连接为其转发请求,也就是说类似于普通应用程序的请求连接;
#define CONN_TYPE_DIR 9
//CPUWORKER连接:用于在程序开启多进程解密服务的时候提供进程间通信,详细过程可以参看前面分析过的cpuworker部分文章;正常情况下未被使用;
#define CONN_TYPE_CPUWORKER 10
//CONTROL监听连接:本地用于监听本地应用程序传递的控制指令或请求;(后期专门讲述,此处暂略)
#define CONN_TYPE_CONTROL_LISTENER 11
//CONTROL连接:本地用于控制消息处理的连接;(后期专门讲述,此处暂略)
#define CONN_TYPE_CONTROL 12
//以下三种连接由于非常少见,此处我们暂时略去;
#define CONN_TYPE_AP_TRANS_LISTENER 13
#define CONN_TYPE_AP_NATD_LISTENER 14
#define CONN_TYPE_AP_DNS_LISTENER 15
  很明显地,通过分析上面这些连接类型我们发现,对于客户端而言,最重要的连接类型无非就是DIR,AP,OR三种连接。这些连接之间的相互关联和作用帮助客户端应用程序将应用请求送入Tor系统,并再通过Tor系统的封装等操作将数据成功送出到远端目的地。下面我们来简要描述下整个系统运转的过程。

  1. 应用请求的发送

    应用程序在想要使用Tor程序提供的匿名服务之时,需要通过Tor客户端程序进行数据发送的代理。这里我们使用最简单的浏览器作为例子。当浏览器想要实现匿名浏览网页时,就要修改其代理服务配置,通过本机的代理进行请求的发送。一般情况下,我们知道,此时代理会被设置为IP:Port。而这里所选用的IP和端口号,就是Tor程序配置文件内部所写入的监听应用程序请求的IP和端口号。在配置文件中,配置它们的参数名为SocksListenAddress与SocksPort。

    显然,这里的IP和端口号意味着Tor程序一定会开启监听连接来对该地址该端口进行持续监听。而执行此处监听的连接类型,就是AP监听连接。

    如果观察仔细,我们还会在默认配置文件之中注意到配置参数ControlPort。该参数指定Tor程序监听的控制端口号。同理,我们可以知道Tor程序一定会开启监听连接监听控制端口接收到的请求。而执行此监听的连接类型,就是CONTROL监听连接。

    通过以上的描述,我们知道为什么AP监听连接和CONTROL监听连接全局只能有一个。因为他们无非就是基于本地地址的socket监听连接,只需要各有一个。并且我们也可以知道,就像服务器接收连接获得socket一样,Tor程序也会获得每个监听到的请求所相对应的socket连接。对这些连接的封装就形成了AP连接和CONTROL连接。

  2. 应用请求的处理

    应用请求的发送是通过本地socket进行的操作。如果大家对socket编程不陌生的话,想必在上述部分描述完之后,已经不会再有疑问。现在的问题是,获得了AP连接之后,数据向哪里转发,如何转发。此时我们假设AP连接已经收到了应用程序发送而来的数据,它需要将这些数据通过Tor系统匿名地发送出去。那么此时,我们就要问,Tor系统的消息是如何传递的呢?回顾Tor的论文,Tor的技术文档,Tor的相关介绍,我们能回忆起Tor系统的流复用机制,链路复用机制。相关框架如下:

               AP Stream 1    <-->

               AP Stream 2    <-->    Circuit 1  <->

               AP Stream 3    <-->

                            Circuit 2   <->    Local OR Connection  <=====>  Remote OR Connection

                            Circuit 3   <->

    多个AP连接对应的AP流复用同一条链路;多条链路,复用同一个OR连接。是的,系统就是这样设计的。但是,我们想知道的是这样的设计对于代码而言意味着什么,对于程序员而言,需要关心的又是什么。我们先从每个部分所代表的宏观含义开始说起。

    AP Stream:AP连接接收到应用程序请求之后形成的数据流,该数据流向是可以双向的。也就是说,这是一个应用程序的一次数据请求,例如浏览器上一张网页内的一次数据请求。对于程序员而言,这就意味着AP连接需要有读写缓冲区以临时存放数据,并随时准备发出数据。重要的是,流的建立基于链路的建立。

    Circuit:Tor系统链路。链路一般情况下是由三个洋葱路由组成,形成Tor网络中的一条私有通道。整条通道的完整路径只有客户端自己知道,因为链路内的所有路由结点,均是由客户端挑选而来的。链路中的结点,只知道其前驱和后继结点,而不知道其他任何结点,这样就保障了系统的匿名性。对程序员而言,链路的建立一定需要选择三个链路结点,向第一个结点发送链路建立请求,之后通过向第一个结点发送链路拓展请求建立整条链路。而链路请求的发送,基于OR连接的建立。

    OR Connection:Tor系统内两主机之间形成的保密连接,建立于TLS连接之上。

    上述三者描述完之后,我们就更加希望知道,这三者所对应的代码的对应内容以及他们之间是如何交互,如何交互数据的。那么,此时我们就需要用到在前边的章节之中已经出现过的系统框图。大家此时再回顾整个系统框图,应该会有豁然开朗的感觉了。系统框图的详细说明,此处就不再展开,代码细节过于繁多。要往细节里描述的内容太过繁杂,以至于笔者都没有信心往下写,所以代码还是期待大家自己一点点看完吧。

    但是我们的问题还没有完全说清,所以此处再重新描述一遍请求的处理过程。实际上,在应用程序请求传进Tor程序之后,只是暂存在AP连接的缓冲区之内。AP连接需要找到合适的链接以发送数据,所以需要选择现有链接中的最优一条或者新建一条链接。这就是为什么我们在前面的分析之中会出现类似的代码逻辑的原因。此处,我们更关心在没有链接的情况下,是如何新建链接的。事实上在链接未完成建立之前,应用程序请求数据一直呆在AP连接的缓冲区内部。而在此期间,链路开始建立。链路建立的首要工作,就是选择三个结点,之后是向第一个结点发送OR连接请求,最后再从高层建立整个链路。有了这样的基本思路,我们便可以开始链路建立基本过程的代码分析了。


2. 链路

  在分析代码之前,我们还想再次详细地描述链路的相关内容。因为链路似乎是整个Tor系统的核心。不论从链路结点的选择,链路的加密的过程,链路建立的协议等任何一方面来说,我们都没有详细地深入探讨过是如何在代码中成功实现的。在这里,我们仅仅针对链路的加密的过程做一些描述,不再对结点选择和协议深入讲解。因为结点选择策略对程序的执行流程而言,并未起到关键的作用;协议的内容,我们也已经在前面的握手协议部分做过详细讲解。

  那么,链路的存在,是如何实现洋葱路由式的加密呢?该问题也能陈述为,链路对层层加密和层层解密的相关操作是如何简便地实现的?在这里我们分情况讨论,首先我们讨论链路为原始链路的情况,也就是说先讨论由客户端持有的链路结构体origin_circuit。

  在原始链路的结构体中,我们发现了这样一个成员变量:crypt_path_t *cpath。通过分析这个成员变量,我们发现他形成了一个双向链表。为了说明清晰,我们将该结构体的代码贴于此处:

/** Holds accounting information for a single step in the layered encryption
* performed by a circuit. Used only at the client edge of a circuit. */
//从注释可以看出,这个结构体,只有在客户端上的链路结构体才会使用,也就是说只有origin_circuit用到了他;
//而目的,我们在此时已然可以猜到:维护链路结点双向链表,存储所有结点相关的加解密,摘要,流量控制,简要身份信息等;
//以上说到的相关信息,均可以在结构体中找到对应项:
typedef struct crypt_path_t {
uint32_t magic; //唯一标识码;

/* crypto environments */
//远离客户端为后向,接近客户端为前向;
 /** Encryption key and counter for cells heading towards the OR at this step. */
//前向加密资料,包括密钥,IV等信息;(尾节点无后向加密资料-forward)
 crypto_cipher_t *f_crypto;
/** Encryption key and counter for cells heading back from the OR at this step. */
//后向加密资料,包括密钥,IV等信息;(首节点无前向加密资料-backward)
 crypto_cipher_t *b_crypto;

/** Digest state for cells heading towards the OR at this step. */
//前向摘要资料,哈希算法和状态等;
 crypto_digest_t *f_digest; /* for integrity checking */
/** Digest state for cells heading away from the OR at this step. */
//后向摘要资料,哈希算法和状态等;
 crypto_digest_t *b_digest;

/** Current state of Diffie-Hellman key negotiation with the OR at this step. */
//客户端与该节点的DH密钥协商状态;
 crypto_dh_t *dh_handshake_state;

......

/** Information to extend to the OR at this step. */
//该节点的相关信息;
 extend_info_t *extend_info;

/** Is the circuit built to this step? Must be one of:
* - CPATH_STATE_CLOSED (The circuit has not been extended to this step)
* - CPATH_STATE_AWAITING_KEYS (We have sent an EXTEND/CREATE to this step
* and not received an EXTENDED/CREATED)
* - CPATH_STATE_OPEN (The circuit has been extended to this step) */
//当前该节点在链路中的状态;
 uint8_t state;
#define CPATH_STATE_CLOSED 0
#define CPATH_STATE_AWAITING_KEYS 1
#define CPATH_STATE_OPEN 2

//该节点在链路中的下一个节点的对应结构体;
 struct crypt_path_t *next; /**< Link to next crypt_path_t in the circuit.
* (The list is circular, so the last node
* links to the first.) */
//该节点在链路中的上一个节点的对应结构体;
 struct crypt_path_t *prev; /**< Link to previous crypt_path_t in the
* circuit. */

//客户端用于针对该节点的流量控制,写控制;
int package_window; /**< How many cells are we allowed to originate ending
* at this step? */
//客户端用于针对该节点的流量控制,读控制;
 int deliver_window; /**< How many cells are we willing to deliver originating
* at this step? */
} crypt_path_t;
  有了上面这个接头体,客户端的Tor程序就可以很随意的组织链路,控制加解密,摘要等。后面的链路操作代码也变得相对容易看懂了。这就是原始链路的情况,下面我们简要说明中间链路的情况。实际上,中间链路的结构体名为or_circuit。所谓的中间链路的结构体,指的是在链路的中间结点或末尾结点上所使用的链路结构体。该链路结构体,不需要知道链路的完整信息,而只要知道本节点到前后两结点的密码环境和摘要环境即可。因为说到底,中间链路的结构体,只不过是起到转发的作用,不需要过多的信息。这里所说的一切,均可以在代码中窥见端倪,请大家有兴趣的话查阅代码,自行体会。

  自此大家应该能自行回答关于详细在链路中洋葱式加解密的问题。当链路建立完成,客户端中所有的crypt_path_t结构体都会被成功填充,也就拥有了与每个节点之间共享的前后向密钥。消息传出时,客户端先按照从近及远的次序用各结点后向密钥层层加密消息,然后发出。发送出去的消息经过层层后向密钥解密,最终会在最后一个节点被完全解密,送出链路。消息传回时,每个经过的结点利用前向密钥加密,到达客户端之后,客户端按照从近及远的次序用各节点前向密钥层层解密消息,最终得到传回的消息。这个过程,也就是大家所熟悉的洋葱加解密过程了。

  讲完这个部分,大家阅读代码时候所遇到的关于加解密的部分,应该就不会再困惑。接下去的章节中,我们就要继续分析代码,接着讲客户端的执行流程分析完毕。