通过上一节中我们对连接和链路的重新描述,我们可以继续进行源码的分析了。在本节中,我们会开始着重讲述链路的建立,以及链路所基于的OR连接的建立,同时还有部分Libevent调度的再度分析。大家会明白,进行到此处之时,我们已经开始接触Tor系统最底层,最深藏着的连接机制以及调度机制。这个部分,是整个系统的精髓。后期几乎所有的应用请求连接处理等,都是重复地使用该部分的代码。
1. 链路建立以及OR连接的开始,circuit_establish_circuit
我们在此处重新分析下链路建立的函数:
/** Build a new circuit for purpose. If exit
* is defined, then use that as your exit router, else choose a suitable
* exit node.
*
* Also launch a connection to the first OR in the chosen path, if
* it's not open already.
*/
origin_circuit_t *
circuit_establish_circuit(uint8_t purpose, extend_info_t *exit, int flags)
{
origin_circuit_t *circ;
int err_reason = 0;
circ = origin_circuit_init(purpose, flags); //链路初始化;
if (onion_pick_cpath_exit(circ, exit) < 0 || //选取链路出口结点;
onion_populate_cpath(circ) < 0) { //选取链路入口结点及中间结点;
......
}
if ((err_reason = circuit_handle_first_hop(circ)) < 0) { //开始向链路第一个结点发送建立OR连接的请求;(OR连接建立于TLS连接之上)
......
}
return circ;
}
像之前我们所描述的那样,函数内部主用做的工作包括三点:初始化链路结构体;选择链路结点;建立到链路中第一个结点的连接。此处需要说明的是,初始化链路结构体和选择链路结点的操作均是简单的。所以我们在接下来的分析中,不再详细追究这两个部分操作的具体细则,而是将我们的重心放在链路建立的实际操作部分。如果对选择结点部分有疑问,可以细细分析上述的两个结点选择函数,相信在其中可以找到结点选择策略和相关机制。但是,结点如何选择并不是程序执行流程的重点,所以我们接下来还是要着重分析函数:circuit_handle_first_hop。
2. OR连接建立以及链路拓展,circuit_handle_first_hop
在客户端完成链路的初始化和链路中结点的选择之后,即将开始建立到第一个链路结点的OR连接。但是这个时候还有个复用的问题,也就是我们前面提到过的多条链路可以共享一个OR连接的情况。试想一下,当一条链路选择完所有链路中的结点,此时它要向第一个链路结点发送OR连接的请求。如果客户端主机到选中的结点主机已经存在一条OR连接,是否需要重新相连呢?显然,没有必要。所以我们可以在一条OR连接上复用多条链路。于是我们将会看到代码中出现判断是否已经存在可用OR连接的部分操作。接下来我们直接看代码:
/** Start establishing the first hop of our circuit. Figure out what
* OR we should connect to, and if necessary start the connection to
* it. If we're already connected, then send the 'create' cell.
* Return 0 for ok, -reason if circ should be marked-for-close. */
int
circuit_handle_first_hop(origin_circuit_t *circ)
{
......
firsthop = onion_next_hop_in_cpath(circ->cpath); //按序选中链路中第一个未标记为打开的结点;在该函数中,实际上每次选中的都是链路入口结点;
n_conn = connection_or_get_for_extend(firsthop->extend_info->identity_digest, //获得可以复用的OR连接;
&firsthop->extend_info->addr,
&msg,
&should_launch);
if (!n_conn) { /* not currently connected in a useful way. */ //如果没有可以复用的OR连接,则重新建立到首结点的OR连接;
circ->_base.n_hop = extend_info_dup(firsthop->extend_info);
if (should_launch) { //建立到首结点的OR连接执行函数:connection_or_connect
n_conn = connection_or_connect(&firsthop->extend_info->addr,
firsthop->extend_info->port,
firsthop->extend_info->identity_digest);
}
return 0;
} else { /* it's already open. use it. */ //如果有可以复用的OR连接,则发送create包以告知远端结点开启一条新的链路;
circ->_base.n_conn = n_conn;
if ((err_reason = circuit_send_next_onion_skin(circ)) < 0) {
......
}
}
return 0;
}
我们可以理解,复用与否,是针对链路首节点的OR连接而言的。对于其他结点,本地客户端是不直接与他们进行OR层次上的沟通的,而是通过链路拓展来进行交流。所以,针对第一个结点,本地客户端既要实现OR层次的互联,又要完成链路层次的交流,就是发送create包以告知链路的开启。针对其他结点,本地客户端只是通过向第一个结点发送链路层的命令包,以实现链路层面上的沟通。
从上述函数我们可以看到两个关键分支:向第一个结点发起OR连接请求的分支;复用OR连接,直接向第一个结点发起新链路开启命令的分支。我们这里按照系统的常规流程,先分析系统中一个OR连接都没有的情况。也就是说,我们此处默认系统中没有满足链路要求的OR连接,那么程序需要开始建立从本地到链路首结点的OR连接。
3. OR连接建立的细节
OR连接的建立是基于TLS连接的基础之上的,所以要想真正建立可用的OR连接,需要完成TLS握手。但是,握手过程需要通信双方经过数轮交换,很显然在一个函数中等待握手结束是极低效的。OR连接建立的细节部分,我们要将关注的重点放在系统是如何设计非阻塞式的TLS握手过程,从而实现高效运行。以下为代码分析:
/** Launch a new OR connection to addr:port and expect to
* handshake with an OR with identity digest id_digest.
*
* If id_digest is me, do nothing. If we're already connected to it,
* return that connection. If the connect() is in progress, set the
* new conn's state to 'connecting' and return it. If connect() succeeds,
* call connection_tls_start_handshake() on it.
*
* This function is called from router_retry_connections(), for
* ORs connecting to ORs, and circuit_establish_circuit(), for
* OPs connecting to ORs. //此处的注释可以说明本函数的重要性;
*
* Return the launched conn, or NULL if it failed.
*/
or_connection_t *
connection_or_connect(const tor_addr_t *_addr, uint16_t port,
const char *id_digest)
{
......
conn = or_connection_new(tor_addr_family(&addr));
/* set up conn so it's got all the data we need to remember */
connection_or_init_conn_from_address(conn, &addr, port, id_digest, 1);
conn->_base.state = OR_CONN_STATE_CONNECTING;
conn->is_outgoing = 1;
/* If we are using a proxy server, find it and use it. */
r = get_proxy_addrport(&proxy_addr, &proxy_port, &proxy_type, TO_CONN(conn));
if (r == 0) { //不使用代理;
......
} else { //使用代理
......//这个部分牵涉到代理的操作,暂时略去,后期会有代理和Bridge专题;
}
switch (connection_connect(TO_CONN(conn), conn->_base.address, //OR连接socket层次的建立操作;
&addr, port, &socket_error)) {
case -1://建立失败
/* If the connection failed immediately, and we're using
* a proxy, our proxy is down. Don't blame the Tor server. */
......
return NULL;
case 0://建立进行中,此情况为一般情况;
connection_watch_events(TO_CONN(conn), READ_EVENT | WRITE_EVENT); //将OR连接的读写事件加入Libevent事件监听队列;
return conn;
/* case 1: fall through *///建立完成,如果socket连接能够非常迅速地建立,则直接进入TLS握手阶段;
}
//此处开始TLS握手阶段,本函数中只有上述socket连接非常迅速地成功完成连接才会执行到此处;
if (connection_or_finished_connecting(conn) < 0) {
/* already marked for close */
return NULL;
}
return conn;
}
上述函数中,最重要的部分就是函数connetion_connect。该函数的作用是进行socket的建立,但是建立的结果因为进行的是非阻塞式的,所以会有三种结果:建立失败-1,正在建立中0,建立完成1。因为socket连接是非阻塞的,所以正在建立中和建立已完成两种情况均有可能出现。若建立完成,则可以直接开始TLS的握手连接;若建立正在进行中,则需要将连接加入Libevent的事件监听列表,以进行监听和后续操作。实际上,这个部分的重点,就是在连接无法马上完成之时的操作:将正在建立中的连接加入到Libevent事件监听队列。此处,我们就隐约感觉到了Tor系统是如何处理需要一定时间才能完成的连接建立操作的。实际上,Tor系统甚至将连接建立的过程都用Libevent事件调度系统来进行调度。当非阻塞的连接建立过程返回时并未完成连接建立操作,则系统将这样的连接加入到Libevent事件池中,下次事件主循环开始时发现该连接可以被操作,则继续该连接的建立和握手操作等。
介绍到此处,我们将OR连接的建立过程介绍完毕。在这个过程中,最重要的部分,就是对于socket连接建立返回值的处理。此时大家可能还没有见到如何对Libevent事件队列中被激活事件进行处理的主要过程,在后面的文章中,我们会再进入主循环的事件处理分析。实际上,就是对读写函数的分析:
1, conn_write_callback;
2, conn_read_callback;