Tor源码文件分析 — Cpuworker

  我们知道,对于Tor的服务器来说,有的时候因为其访问量巨大,不得不采取一些相应机制来保证服务的正常提供。在服务器编程里,我们经常可以用到的技术,例如线程池,多路复用等。Tor程序,在大多数情况下,都是单进程运行的,几乎没有哪里用到多线程的操作。正因为如此,Tor的主进程才绝对不允许出现阻塞式的操作。但是,唯独在一处,Tor为了提高自身效率,利用了线程池类似的机制。这个部分就是Cpuworker。本文就主要介绍该模块的作用和实现机制。

  简单的说,Cpuworker存在的目的,是为了利用线程池的机制分担Tor主进程的压力,帮助其在接收到CREATE请求时计算对称密钥。下面进行具体的过程描述:

  1. 系统启动时根据主机CPU数量,初始化cpuworker线程池;(linux中线程和进程基本无差别)

    cpuworkers_rotate()

    1.1 根据配置文件配置选项,自适应地检测CPU数量或固定设置CPU数量;

      spawn_enough_cpuworkers()

    1.2 根据CPU数量,开启cpuworker线程;(最大数目为16,最小数目为1)

      spawn_cpuworker()

    1.3 开启线程之前,创建sockpair,创建cpuworker连接,关联cpuworker连接与sockpair[0];

    1.4 开启线程之时,设置线程执行函数cpuworker_main,关联线程与sockpair[1];

    1.5 开启线程之后,将cpuworker连接加入系统连接池,同时从此线程与Tor主进程之间的通信方式为sockpair,类似socket的读写操作;

  2. 在成功完成以上操作之后,系统达到如下效果:

    2.1 当Tor主进程收到某主机发来的CREATE包时,检查连接池内是否有空闲的cpuworker连接;

      assign_onionskin_to_cpuworker()

    2.2 若有空闲cpuworker连接,则将CREATE包内容写入该连接;

      connection_write_to_buf()

      2.2.1 写入该连接则会激活该连接,使其将数据进一步写到sockpair[0]内,亦即传递给对应的线程,线程利用sockpair[1]来读取数据;

      2.2.2 线程的主函数是阻塞式得等待数据,一旦数据到达,则开始处理,并在处理完毕之后,将结果写回sockpair[1],亦即传递回主进程cpuworker连接;

      cpuworker_main()

      2.2.3 主进程处理cpuworker读事件就是根据对应的链路返回对应的CREATED包,其中包括DH密钥交换协议第二部分密钥和生成的对称密钥的摘要等;

    2.3 若无空闲cpuworker连接,则将CREATE包挂起,在适当时候再写入空闲连接;

      onion_pending_add()

  上述整个过程,省略了很多细节部分,大家各自参照原函数进行进一步分析和理解。此处对几点再进行强调:Tor系统的连接多种多样,我们前面提过AP连接等内部连接,实际上Cpuworker连接也是内部连接,虽然他是用sockpair来完成的;sockpair是一种进程间通信机制,在众多的通信机制中,这种机制对Tor系统最为合适,所以选用了这种方式;Cpuworker线程的主要工作内容是对称密钥的生成。

  这里我们再对称密钥的生成进行说明:

  1. 在链路建立的过程中,Tor服务器应该首先接收到CREATE包。CREATE包的负载部分具有如下格式:(针对TAP握手方式)

     Payload := PK(Padding || Symmetric key || First part of g^x) || SK(Second part of g^x)

     PK:利用服务器的Onion Key进行公钥加密;

     SK:利用Symmetric Key进行对称加密;

     Padding:填充字节,长度一般为42B

     Symmetric Key:用于加密第二部分内容的对称密钥;

     g^x:DH密钥交换协议第一部分密钥材料。

  2. 服务器接收到CREATE包之后,就要决定DH密钥交换协议的第二部分密钥材料g^y,从而计算出对称密钥。就是因为这个部分的密钥操作过程稍微会耗上一部分的时间,不适合在Tor主进程中进行操作,所以Tor程序利用cpuworker机制,开启线程专门为这种操作提供服务。在处理玩这些操作之后,Tor主进程生成CREATED包,返回给指定的链路。CREATED包的结构如下:

     Payload := DH_key || Digest

     DH_key:DH密钥交换协议第二部分密钥材料g^y;

     Digest:对称密钥首20字节作为摘要;

  3. 其实,对称密钥生成之后被作为材料,截断成五个部分:

     Key Material := Digest || f_digest || b_digest || f_crypto || b_crypto

   第一部分20字节,被用作对称密钥摘要做为CREATED包负载的一个部分;

   第二部分20字节,被用作前向数据摘要计算的密钥;

   第三部分20字节,被用作后向数据摘要计算的密钥;

   第四部分16字节,被用作前向数据加密的密钥;

   第五部分16字节,被用作后向数据加密的密钥。

   前向和后向的不同,在于数据是远离OP还是靠近OP。若是远离OP,则称为前向;若是靠近OP,则称为后向。这里的这些密钥,是针对OP与OR之间而言的,不是OR与OR之间的密钥。也就是说,这些密钥是洋葱密钥,用于层层包裹数据,或者层层解密数据。我们可以知道的是,前向密钥,是用于数据远离OP时,所以是用来层层解密的;后向密钥,是用于数据靠近OP时,所以是用来层层加密的。OP端存有链路中所有OR的所有这些前后向密钥,所以它可以完成全加密和全解密。总的来说,也就是不要忘记,这里的密钥协商,是OP建立链路之时,OP与OR之间进行的协商。

  4. 另外,在cpuworker的使用过程中,规定了一些请求和应答的格式。这个部分很简单,就不再多说,大家可以参照cpuworker_main函数开头部分的注释进行理解。此处只再对TAG进行简单说明。TAG的作用是进行连接ID和链路ID的记录,方便请求处理完成之后,快速找到对应的原始链路。快速查询方法就是利用之前我们提到过的哈希映射表,这里就不再多说:

     Map(circ_id, or_conn)  –>  circ

  因为通过上述的讲解之后,Cpuworker.c源码部分没有任何的理解起来的难度,所以此处就不再多源文件进行冗余的描述,大家可以遵循上述思路进行代码的浏览。应该来说,很快的就可以将cpuworker的工作和整个Tor系统的工作机制联系到一起。那么,关于cpuworker我们就解释到这里。