我们知道,对于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我们就解释到这里。