Tor源码分析四 — 客户端执行流程(初入主循环)
在上个小节中,已经基本分析了Tor系统的初始化过程。该过程中,最重要的部分,就是对默认配置文件、输入配置文件以及命令行参数进行综合整理,定出最后的配置方案。而后通过配置方案,启动系统的基础部分。这里值得说明的是,Tor系统的所有配置选项,均可以在Tor Manual中找到。并且,根据Tor系统配置成不同的身份,使用的配置选项也会有细微差别。默认的配置固化在代码内部,有兴趣的朋友可以就初始化部分往下深究。另外,笔者会将经自己简要翻译和分析过的Tor Manual在后续章节中给出。
1. 主命令的执行
在初始化过程中,系统将所有参数配置均最终写入到全局变量global_options之中。之后利用get_options()函数就可以随时获取系统配置和系统主命令。系统主命令在tor_main()函数中被使用,用于指示Tor程序启动的目的。
if (tor_init(argc, argv)<0)
return -1;
switch (get_options()->command) {
case CMD_RUN_TOR:
#ifdef NT_SERVICE
nt_service_set_state(SERVICE_RUNNING);
#endif
result = do_main_loop();
break;
case CMD_LIST_FINGERPRINT:
result = do_list_fingerprint();
break;
case CMD_HASH_PASSWORD:
do_hash_password();
result = 0;
break;
case CMD_VERIFY_CONFIG:
printf("Configuration was valid\n");
result = 0;
break;
case CMD_RUN_UNITTESTS: /* only set by test.c */
default:
log_warn(LD_BUG,"Illegal command number %d: internal error.",
get_options()->command);
result = -1;
}
这里我们只分析CMD_RUN_TOR命令下的系统主循环函数,也就是Tor系统的核心函数,不再继续讨论另外的主命令。
2. 主循环的初始化
主循环的初始化代码如下:
/* initialize dns resolve map, spawn workers if needed */
if (dns_init() < 0) {
if (get_options()->ServerDNSAllowBrokenConfig)
log_warn(LD_GENERAL, "Couldn't set up any working nameservers. "
"Network not up yet? Will try again soon.");
else {
log_err(LD_GENERAL,"Error initializing dns subsystem; exiting. To "
"retry instead, set the ServerDNSAllowBrokenResolvConf option.");
}
}
handle_signals(1);
/* load the private keys, if we're supposed to have them, and set up the
* TLS context. */
if (! client_identity_key_is_set()) {
if (init_keys() < 0) {
log_err(LD_BUG,"Error initializing keys; exiting");
return -1;
}
}
/* Set up the packed_cell_t memory pool. */
init_cell_pool();
/* Set up our buckets */
connection_bucket_init();
#ifndef USE_BUFFEREVENTS
stats_prev_global_read_bucket = global_read_bucket;
stats_prev_global_write_bucket = global_write_bucket;
#endif
/* initialize the bootstrap status events to know we're starting up */
control_event_bootstrap(BOOTSTRAP_STATUS_STARTING, 0);
if (trusted_dirs_reload_certs()) {
log_warn(LD_DIR,
"Couldn't load all cached v3 certificates. Starting anyway.");
}
if (router_reload_v2_networkstatus()) {
return -1;
}
if (router_reload_consensus_networkstatus()) {
return -1;
}
/* load the routers file, or assign the defaults. */
if (router_reload_router_list()) {
return -1;
}
/* load the networkstatuses. (This launches a download for new routers as
* appropriate.)
*/
now = time(NULL);
directory_info_has_arrived(now, 1);
if (server_mode(get_options())) {
/* launch cpuworkers. Need to do this *after* we've read the onion key. */
cpu_init();
}
总结来说,主循环的初始化包括这几个部分:DNS,信号处理,密钥,cell池,令牌桶,网络配置,多线程配置。对于配置为客户端身份的Tor系统而言,有些部分实际上没有进行相应处理,此处我们略去那些一般用于服务器的初始化部分,直接讨论客户端关注的初始化部分。
2.1 信号处理初始化
Tor系统中的信处理机制,sock监听机制以及按时回调机制,都是由libevent开源代码库提供的功能完成的。下述handle_signals()函数的部分,将在libevent系统内注册每个信号以及相应的信号处理函数与参数:
if (is_parent) {
for (i = 0; signals[i] >= 0; ++i) {
signal_events[i] = tor_evsignal_new(
tor_libevent_get_base(), signals[i], signal_callback,
(void*)(uintptr_t)signals[i]);
if (event_add(signal_events[i], NULL))
log_warn(LD_BUG, "Error from libevent when adding event for signal %d",
signals[i]);
}
} else {
struct sigaction action;
action.sa_flags = 0;
sigemptyset(&action.sa_mask);
action.sa_handler = SIG_IGN;
sigaction(SIGINT, &action, NULL);
sigaction(SIGTERM, &action, NULL);
sigaction(SIGPIPE, &action, NULL);
sigaction(SIGUSR1, &action, NULL);
sigaction(SIGUSR2, &action, NULL);
sigaction(SIGHUP, &action, NULL);
#ifdef SIGXFSZ
sigaction(SIGXFSZ, &action, NULL);
#endif
}
可见,Tor主进程中的信号处理函数,是signal_callback()函数,其参数是信号值。Tor子进程的信号处理,是用sigaction()函数来实现的,而真正的信号操作则是忽略任何信号。
2.2 密钥初始化和TLS初始化
在讨论密钥初始化和TLS初始化之前,我们必须了解Tor系统中被使用到的所有密钥,以及每个身份和密钥之间的关系。
Tor系统使用到的密钥分三种:(详见Tor FAQ:Tell me about all the kyes Tor uses)
1)Encryption(对称密钥):TLS链路OR与OR之间的对称密钥;Tor Protocol OP与OR之间的对称密钥;
2)Authenticaiton(非对称密钥):属于OR的Onion Key,也就是用于OP与OR之间进行DH密钥交换协议时用到的公钥;
3)Coordination(非对称密钥):属于全体的Identity Key;目录服务器额外拥有的目录签名密钥Signing Key;
客户端拥有单独的Client Identity Key,
non-bridge OR服务器有相同的Client Identity Key和Server Identity Key,
bridge OR服务器有不相同的Client Identity Key和Server Identity Key。
注:non-bridge本质上是不公开发布的bridge,由于不公开的原因,其两身份密钥必须不同。
因为这样的密钥设置,密钥初始化的过程相对就复杂了些。
对于客户端来说,Client Identity Key可以临时生成,使用完毕也可以丢弃。所以,初始化的过程就是简单的生成Client Identity Key,再初始化TLS上下文。
对于服务器来说,Server Identity Key是长期保存的密钥,需要从保存密钥的文件中读取,若是首次启动则生成密钥并保存(一般存于DataDirectory/keys)。Onion Key的情况也是如此。若服务器是目录服务器,则需要更多的操作来进行目录服务器的相关密钥或证书处理。总的说来,密钥初始化和TLS初始化的过程,对整个Tor系统所有身份的密钥都进行了初始化,为Tor系统的正常运行做了密码学上的密钥分配。
我们此处就暂且先讨论客户端密钥初始化和TLS初始化,其代码如下:
/* OP's don't need persistent keys; just make up an identity and
* initialize the TLS context. */
if (!server_mode(options)) {
if (!(prkey = crypto_pk_new()))
return -1;
if (crypto_pk_generate_key(prkey)) {
crypto_pk_free(prkey);
return -1;
}
set_client_identity_key(prkey);
/* Create a TLS context. */
if (router_initialize_tls_context() < 0) {
log_err(LD_GENERAL,"Error creating TLS context for Tor client.");
return -1;
}
return 0;
}
Tor系统中用的公钥系统一般为RSA公钥系统,所以身份密钥初始化是RSA密钥的初始化。TLS上下文初始化就是标准的初始化方式。
最终,在执行完上述代码之后,系统中设置了两个全局变量:client_identitykey,client_tls_context。实际上,若系统配置成服务器身份,还会对另外两个相应的全局变量进行修改:server_identitykey,server_tls_context。
2.3 cell池初始化
cell是Tor Protocol进行协议沟通的基本数据单元,该单元的具体组织形式在Tor系统设计文件tor_design.pdf中已经有详细描述。在近些版本的Tor系统中,cell的具体格式和相关命令也发生了些许的变化,但是基本的格式和思想是没有发生大的改变。Tor系统为了统一管理cell,设计了cell池。其初始化就是对全局cell池的初始化,最终设置全局变量cell_pool。
2.4 令牌桶初始化
令牌桶是用于对Tor系统进行流量控制的系统。系统中这个部分的工作似乎用libevent实现的多,由于没有细细研究,此处就不再细究。
2.5 网络配置
网络配置部分代码如下,共五个主要函数:
if (trusted_dirs_reload_certs()) {
log_warn(LD_DIR,
"Couldn't load all cached v3 certificates. Starting anyway.");
}
if (router_reload_v2_networkstatus()) {
return -1;
}
if (router_reload_consensus_networkstatus()) {
return -1;
}
/* load the routers file, or assign the defaults. */
if (router_reload_router_list()) {
return -1;
}
/* load the networkstatuses. (This launches a download for new routers as
* appropriate.)
*/
now = time(NULL);
directory_info_has_arrived(now, 1);
trusted_dirs_reload_certs():该函数从本地cache-certs文件加载事先已经下载好的各权威目录服务器的证书;在系统第一次启动时,cache-certs文件是不存在的,也就是说该函数实际上在此处不会被真正执行。
router_reload_v2_networkstatus():该函数从本地cache-status文件加载事先已经下载好的网络状态文件;在新版本的系统中,cache-status文件对于客户端而言并不是必须要下载的文件,也就是说此处函数实际上也不会对系统产生影响。
router_reload_consensus_networkstatus():该函数从本地存储的网络共识文件中读取必要信息来初始化系统网络认识;在系统第一次启动时,cache-%s-consensus文件是不存在的,也就是说该函数实际上不会对系统产生影响。
router_reload_router_list():该函数加载系统新的路由列表;而执行函数核心部分需要用到系统全局变量router_list,而该变量由于上述几个函数没有成功执行,也不会被设置,所以本函数的加载系统路由列表也无法成功执行。
directory_info_has_arrived():该函数会在系统成功获取新的网络状态或者服务器描述符时成功执行;而上述的几个尝试获取本地cache的网络状态和路由信息产生了失败,所以该函数也不会对系统执行产生影响。
综上所述,此处的网络配置初始化,在系统第一次启动时是没有成功完成有效操作的。因为笔者按照客户端系统第一次启动流程来编写Tor系统分析,所以这些函数的具体分析和作用会在后续分析中做更多叙述(遇到他们发挥作用的地方)。
于此,介绍完毕主循环初始化的基本内容,接下来就到了主循环的核心循环部分。
3. 主循环核心
主循环的核心主要做了这么几件事情:秒回调设置;令牌桶重填回调设置;事件集主循环。
3.1 秒回调设置
/* set up once-a-second callback. */
if (! second_timer) {
struct timeval one_second;
one_second.tv_sec = 1;
one_second.tv_usec = 0;
second_timer = periodic_timer_new(tor_libevent_get_base(),
&one_second,
second_elapsed_callback,
NULL);
tor_assert(second_timer);
}
秒回调的设置,为维护整个Tor系统做了准备。顾名思义,秒回调,即是Tor系统会每秒钟执行一次此处设置的回调函数second_elapsed_callback()函数。其核心是通过libevent库的定时事件来实现的。此处,我们更关心定时回调函数都做了什么,但是,因为该回调函数非常巨大,无法一次性讲清。笔者将会将该回调函数特意写成一节,细细分析。
3.2 令牌桶重填回调设置
#ifndef USE_BUFFEREVENTS
if (!refill_timer) {
struct timeval refill_interval;
int msecs = get_options()->TokenBucketRefillInterval;
refill_interval.tv_sec = msecs/1000;
refill_interval.tv_usec = (msecs%1000)*1000;
refill_timer = periodic_timer_new(tor_libevent_get_base(),
&refill_interval,
refill_callback,
NULL);
tor_assert(refill_timer);
}
#endif
令牌桶回调设置会在Bufferevent功能未被开启之时起到作用,即默认情况下是被启动的。此处的函数,将读取配置参数来设置固定时间执行的回调函数refill_callback()。其核心也是libevent的定时事件机制。refill_callback函数的内容是对令牌桶的令牌数量的增加操作,此处略去。另外,Bufferevent是libevent库的一种IO缓冲机制,由于笔者对Bufferevent也不甚了解,关于Bufferevent的问题,在分析后期再详细给出说明。
3.3 主循环核心循环
for (;;) {
/* All active linked conns should get their read events activated. */
SMARTLIST_FOREACH(active_linked_connection_lst, connection_t *, conn,
event_active(conn->read_event, EV_READ, 1));
called_loop_once = smartlist_len(active_linked_connection_lst) ? 1 : 0;
update_approx_time(time(NULL));
/* poll until we have an event, or the second ends, or until we have
* some active linked connections to trigger events for. */
loop_result = event_base_loop(tor_libevent_get_base(),
called_loop_once ? EVLOOP_ONCE : 0);
}
此处代码删去许多不重要的部分,留下的是整个Tor系统循环的核心:
系统每次核心循环,从活动linked连接列表中取出所有事件,直接加入到libevent事件池中的活动事件池进行调度运行。并且,若是活动linked连接列表确实有连接,则libevent事件池的调度只执行一次便返回。值得说明的是,直接加入到活动事件池内的事件,被处理完之后就完全从事件池中退出,不会存留。而普通情况下的预备事件在成为活动事件之后,当回调函数执行完毕,预备事件任处在调度池中。当然,若活动linked连接列表没有事件,则libevent的调度会阻塞,直到有事件需要被调度执行。而此时跳出loop的时机,则是活动linked连接列表新增加了连接,也就是调用了前面提到过的connection_start_reading_from_linked_conn()函数。该函数调用tor_event_base_loopexit()函数,令loop返回。