Tor源码分析十一 — 客户端执行流程(网络信息的下载续)

  通过上一节中我们对连接和链路的重新描述,我们可以继续进行源码的分析了。在本节中,我们会开始着重讲述链路的建立,以及链路所基于的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;