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

  源码分析到这里,大家应该已经大致了解到Tor系统的前期启动没有做任何的下载操作。前期启动的最关键环节,就是正常开启Libevent的调度机制,从而有条不紊地进行系统内所有子模块的维护等。我们需要再次强调的是,系统的主进程内,是没有做任何直接的获取网络状态,获取路由描述符,获取额外路由信息的操作。系统将这些操作视为需要实时维护的工作,因为所有这些网络信息都有其时限。所以,对这些网络信息的获取,全部置于秒回调函数之后进行。秒回调函数能够控制对这些信息的检测和获取,这些在前一节中已经有部分描述。

  我们将在本文中再次重申秒回调函数中最重要的执行线路,以弄清Tor系统是如何获取网络信息,从而开启链路以至于完成整个系统的成功建立。由于本文中所提到的许多函数嵌套层次非常之多,我们只针对最重要的函数加以说明;同时,我们会略去许多不被执行的函数。这些不被执行的函数,大多情况下是因为执行条件不满足而未能够通过函数内部执行前检测,或更甚者未能通过函数外部的判断语句检测。关于这些情况,请大家自行查看函数细节,此处就不再一个一个详细解释。

1. update_networkstatus_downloads

  前面介绍的秒回调函数之中最重要的维护函数即为事件调度函数。事件调度函数中,大部分的内容由于与时间相关而不会在短期内执行。具体的执行间隔可以参照之前章节中源码的注释。在简要的对该函数进行分析之后,我们发现,客户端系统刚刚启动之时,最终重点调用的代码如下:

static void
run_scheduled_events(time_t now)
{
  ......
  /* 2b. Once per minute, regenerate and upload the descriptor if the old
   * one is inaccurate. */
  ......

  if (time_to_check_descriptor < now && !options->DisableNetwork) {  //每一分钟进行一次网络状态的下载检测
    ......
    time_to_check_descriptor = now + CHECK_DESCRIPTOR_INTERVAL;
    ......

    /* Also, once per minute, check whether we want to download any
     * networkstatus documents.
     */
    update_networkstatus_downloads(now);  //开启网络状态的下载
  }
  ......
} 

  以下为网络状态下载函数的函数体:

/** Launch requests for networkstatus documents and authority certificates as
 * appropriate. */
void
update_networkstatus_downloads(time_t now)
{
  const or_options_t *options = get_options();
  if (should_delay_dir_fetches(options))  //使用Bridge的情况下,若不知道任何可用Bridge的信息,则延迟下载
    return;
  if (authdir_mode_any_main(options) || options->FetchV2Networkstatus)  //权威服务器适当地下载V2网络状态信息,客户端不需要
    update_v2_networkstatus_cache_downloads(now);
  update_consensus_networkstatus_downloads(now);  //首先下载网络共识
  update_certificate_downloads(now);  //其次下载权威服务器证书,没有需要验证的网络共识之前不会执行
}

  依据这样的执行流程,系统开始进入网络共识的下载。值得说明的是,开启了第一次下载之后,当下一次秒回调函数重新执行到此处之时,会发现已经有向权威服务器请求网络共识的连接存在,于是此处的函数便不再执行。接下来我们详细分析获取网络共识所需网络连接的建立:

/** If we want to download a fresh consensus, launch a new download as
 * appropriate. */
static void
update_consensus_networkstatus_downloads(time_t now)
{
  int i;
  const or_options_t *options = get_options();

  for (i=0; i < N_CONSENSUS_FLAVORS; ++i) {
    /* XXXX need some way to download unknown flavors if we are caching. */
    ......

    if (! we_want_to_fetch_flavor(options, i))  //一般作为当前版本的客户端,只希望获取FLAV_MICRODESC类型的网络共识,其他类型全部跳过
      continue;

    c = networkstatus_get_latest_consensus_by_flavor(i);  //当前无最新网络共识则返回为空
    if (! (c && c->valid_after <= now && now <= c->valid_until)) {
      /* No live consensus? Get one now!*/
      time_to_download_next_consensus[i] = now;  //更新需求下载网络共识的时间
    }

    if (time_to_download_next_consensus[i] > now)
      return; /* Wait until the current consensus is older. */

    resource = networkstatus_get_flavor_name(i);  //要发送给目录服务器的请求内容,针对FLAV_MICRODESC类型的网络共识,其值为microdesc

    if (!download_status_is_ready(&consensus_dl_status[i], now,  //网络共识下载状态正常
                                  CONSENSUS_NETWORKSTATUS_MAX_DL_TRIES))
      continue; /* We failed downloading a consensus too recently. */
    if (connection_dir_get_by_purpose_and_resource(  //没有当前正在下载网络共识的连接
                                DIR_PURPOSE_FETCH_CONSENSUS, resource))
      continue; /* There's an in-progress download.*/

    waiting = &consensus_waiting_for_certs[i];  //若有正在等待证书的网络共识,则尽量延长时间以等待证书的获取,以处理该网络共识成为可用状态的情况
    if (waiting->consensus) {
      /* XXXX make sure this doesn't delay sane downloads. */
      if (waiting->set_at + DELAY_WHILE_FETCHING_CERTS > now) {
        continue; /* We're still getting certs for this one. */
      } else {
        if (!waiting->dl_failed) {
          download_status_failed(&consensus_dl_status[i], 0);
          waiting->dl_failed=1;
        }
      }
    }

    log_info(LD_DIR, "Launching %s networkstatus consensus download.",
             networkstatus_get_flavor_name(i));

    //网络共识获取函数
    directory_get_from_dirserver(DIR_PURPOSE_FETCH_CONSENSUS,
                                 ROUTER_PURPOSE_GENERAL, resource,
                                 PDS_RETRY_IF_NO_SERVERS);
  }
}

2. directory_get_from_dirserver

  上面讲述了这么多之后,其实下载网络状态说到底都是从目录服务器下载数据。从目录服务器下载数据的操作,统一使用下述函数:

/** Start a connection to a random running directory server, using
 * connection purpose dir_purpose, intending to fetch descriptors
 * of purpose router_purpose, and requesting resource.
 * Use pds_flags as arguments to router_pick_directory_server()
 * or router_pick_trusteddirserver().
 */
void
directory_get_from_dirserver(uint8_t dir_purpose, uint8_t router_purpose,
                             const char *resource, int pds_flags)
{
    // dir_purpose:向目录服务器建立连接的目的;
  // router_purpose:向目录服务器建立连接的目的之中,与路由相关的目的;(General,Bridge)
    // resource:请求字符串
    // pds_flags:选择目录服务器的标示符(pick directory server)
    ......
}

  该函数篇幅略长,但是其核心的流程较为直观:找到合适的服务器,向服务器发送请求。而寻找合适的服务器这个过程,根据各种条件判断,可能调用以下两个函数中的一个,从而获得服务器的网络状态结构体。该结构体的获得是为了发送服务器请求服务的。两个调用的函数如下:

    rs = router_pick_trusteddirserver(type, pds_flags);
    rs = router_pick_directory_server(type, pds_flags);

  我们不再针对以上两个函数进行分析。因为这两个函数的最终目的我们比较明确。但是,如果希望找到固化在Tor源码内部的9个权威目录服务器的IP地址等信息,则可以深入分析第一个函数。我们可以简单猜测,此处服务器的选择过程应该是基本随机的。回到我们的故事主线,我们发现现在我们需要向一台服务器请求网络共识。而此时除了固化在代码内部的9个权威目录服务器被我们知道以外,其他的服务器信息我们一无所知。所以,这里一定是调用了获取trusteddirserver的函数获取9台权威目录服务器其中一台的IP地址等相关信息。

  选定需要发送请求的目录服务器之后,我们就需要向其发起连接,建立连接完毕之后,尝试发送请求。建立连接和发起请求的整个过程需要经历一个较为复杂的Tor系统内部连接处理的过程。我们不打算将这整个过程的代码一步一步进行分析,而是从比较宏观的角度对整个连接建立的过程进行描述。这里给出整个过程的入口部分,有兴趣的读者可以自行验证和详细分析:

  if (rs)
    directory_initiate_command_routerstatus(rs, dir_purpose,
                                            router_purpose,
                                            get_via_tor,
                                            resource, NULL, 0,
                                            if_modified_since);

3. directory_initiate_command_routerstatus(Dir连接与OR连接的建立)

  该函数是一个更加通用函数的包裹,大家利用代码查看工具往下深究会返现最终的重点函数其实是:directory_initiate_command_rend()

  大家看到这里一般都会对其参数的内容感到头疼,我们此处先将其第一次执行时所用参数简要介绍,然后再详细说明函数内部的实现功能:

/** Same as directory_initiate_command(), but accepts rendezvous data to
 * fetch a hidden service descriptor. */
static void
directory_initiate_command_rend(const char *address, const tor_addr_t *_addr,
                                uint16_t or_port, uint16_t dir_port,
                                int supports_conditional_consensus,
                                int supports_begindir, const char *digest,
                                uint8_t dir_purpose, uint8_t router_purpose,
                                int anonymized_connection,
                                const char *resource,
                                const char *payload, size_t payload_len,
                                time_t if_modified_since,
                                const rend_data_t *rend_query)
{
  // address:发送请求所选取的权威服务器的字符串地址信息;
  // _addr:真正用于发起连接的权威服务器的IP地址等信息;
  // or_port:权威服务器开放的or端口号,用于发起OR连接;
  // dir_port:权威服务器开放的dir端口号,用于请求目录信息;
  // supports_conditional_consensus:与服务器配置有关,指示服务器是否支持有条件的网络共识?这里没有详细理解;
  // supports_begindir:与服务器配置有关,指示权威服务器是否支持直接连接DIR端口;
  // digest:权威服务器身份摘要
  // dir_purpose:前面出现过的目录服务器请求目的;
  // router_purpose:前面出现过的路由请求目的,general或者bridge;
  // anonymized_connection:标志连接是否为匿名的,一般值为0;
  // resource:向目录服务器请求的内容,此处值为microdesc;
  // payload:NULL;
  // payload_len:0;
  // if_modified_since:一般情况下值为0;
  // rend_query:NULL
  ......
}

  这个函数的内部主要执行流程是根据链路的连接选项,尝试新建到服务器的连接。此处我们依据默认情况下,anonymized_connection值为0,support_begindir值为1,进行函数具体执行流程的分析,我们略去许多不执行的内容,有兴趣的朋友可以自行分析:

  //函数先利用已知参数建立DIR连接
  conn = dir_connection_new(tor_addr_family(&addr));

  /* set up conn so it's got all the data we need to remember */
  tor_addr_copy(&conn->_base.addr, &addr);
  conn->_base.port = use_begindir ? or_port : dir_port;
  conn->_base.address = tor_strdup(address);
  memcpy(conn->identity_digest, digest, DIGEST_LEN);

  conn->_base.purpose = dir_purpose;
  conn->router_purpose = router_purpose;

  /* give it an initial state */
  conn->_base.state = DIR_CONN_STATE_CONNECTING;

  /* decide whether we can learn our IP address from this conn */
  conn->dirconn_direct = !anonymized_connection;

  /* copy rendezvous data, if any */
  if (rend_query)
    conn->rend_data = rend_data_dup(rend_query);

  在成功建立Dir连接之后,我们执行如下代码:

  if{
    ......
  } else { /* we want to connect via a tor connection */
    entry_connection_t *linked_conn;
    ......

    /* make an AP connection
     * populate it and add it at the right state
     * hook up both sides
     */
   //建立一个AP连接,为DIR连接服务,所以这两个连接要关联起来;
   //在该函数内部要做的工作非常之多,需要建立一个OR连接为AP连接服务,同时要关联DIR和AP连接等;
   //该函数是连接重点函数!
   linked_conn =
      connection_ap_make_link(TO_CONN(conn),
                              conn->_base.address, conn->_base.port,
                              digest,
                              SESSION_GROUP_DIRCONN, iso_flags,
                              use_begindir, conn->dirconn_direct);
    ......

    //将DIR连接加入连接池;
    if (connection_add(TO_CONN(conn)) < 0) {
      log_warn(LD_NET,"Unable to add connection for link to dirserver.");
      connection_mark_for_close(TO_CONN(conn));
      return;
    }
    conn->_base.state = DIR_CONN_STATE_CLIENT_SENDING;
    /* queue the command on the outbuf */
    directory_send_command(conn, dir_purpose, 0, resource,
                           payload, payload_len,
                           supports_conditional_consensus,
                           if_modified_since);

    //激活DIR连接与AP连接的读写事件,以便两者之间可以开始读写数据。
    connection_watch_events(TO_CONN(conn), READ_EVENT|WRITE_EVENT);
    IF_HAS_BUFFEREVENT(ENTRY_TO_CONN(linked_conn), {
      connection_watch_events(ENTRY_TO_CONN(linked_conn),
                              READ_EVENT|WRITE_EVENT);
    }) ELSE_IF_NO_BUFFEREVENT
      connection_start_reading(ENTRY_TO_CONN(linked_conn));
  }

  通过以上的分析我们可以发现,大量的工作被函数connection_ap_make_link完成。该函数从名字上来看仅仅是个简单的链接函数。但是,实际上,函数的内部还做了许多关于OR连接的操作:建立连接,开启连接等。在这个部分,我们知道了Dir连接和AP连接是怎么被建立和关联起来的,而下个部分,我们将要解释的则是OR连接和AP连接是如何关联在一起的。

4. connection_ap_make_link

  实际上该函数体很短,主要的操作流程就是对AP连接的新建与初始化;针对AP连接与DIR连接进行关联;最后再给新建的AP连接分配一个合适的链路。其中,当AP连接无法找到合适的链路与之相关联之时,系统就会接着进行下一步骤:建立新的链路以满足需求。链路的建立过程,就牵涉到了OR连接的建立。因为链路是建立在OR连接之上的,AP连接是建立在链路之上的。因此才会最终实现OR连接对链路的复用,链路对AP连接的复用。此处我们不打算非常详细的分析每一个连接参数的初始化等繁琐的过程,还是将主要的精力集中在程序主执行流程之中:

/** Make an AP connection_t linked to the connection_t partner. make a
 * new linked connection pair, and attach one side to the conn, connection_add
 * it, initialize it to circuit_wait, and call
 * connection_ap_handshake_attach_circuit(conn) on it.
 *
 * Return the newly created end of the linked connection pair, or -1 if error.
 */
entry_connection_t *
connection_ap_make_link(connection_t *partner,
                        char *address, uint16_t port,
                        const char *digest,
                        int session_group, int isolation_flags,
                        int use_begindir, int want_onehop)
{
  entry_connection_t *conn;
  connection_t *base_conn;

  log_info(LD_APP,"Making internal %s tunnel to %s:%d ...",
           want_onehop ? "direct" : "anonymized",
           safe_str_client(address), port);

  //新建AP连接,以下对其进行初始化赋值;
  conn = entry_connection_new(CONN_TYPE_AP, tor_addr_family(&partner->addr));
  base_conn = ENTRY_TO_CONN(conn);
  base_conn->linked = 1; /* so that we can add it safely below. */

  /* populate conn->socks_request */

  /* leave version at zero, so the socks_reply is empty */
  conn->socks_request->socks_version = 0;
  conn->socks_request->has_finished = 0; /* waiting for 'connected' */
  strlcpy(conn->socks_request->address, address,
          sizeof(conn->socks_request->address));
  conn->socks_request->port = port;
  conn->socks_request->command = SOCKS_COMMAND_CONNECT;
  conn->want_onehop = want_onehop;
  conn->use_begindir = use_begindir;
  if (use_begindir) {
    conn->chosen_exit_name = tor_malloc(HEX_DIGEST_LEN+2);
    conn->chosen_exit_name[0] = '$';
    tor_assert(digest);
    base16_encode(conn->chosen_exit_name+1,HEX_DIGEST_LEN+1,
                  digest, DIGEST_LEN);
  }

  /* Populate isolation fields. */
  conn->socks_request->listener_type = CONN_TYPE_DIR_LISTENER;
  conn->original_dest_address = tor_strdup(address);
  conn->session_group = session_group;
  conn->isolation_flags = isolation_flags;

  base_conn->address = tor_strdup("(Tor_internal)");
  tor_addr_make_unspec(&base_conn->addr);
  base_conn->port = 0;

  //在AP连接初始化完毕之后,将AP连接与DIR连接相关联;
  //实际上,连接partner也未必是DIR连接,但是在我们初步分析系统的时候,用到的主要的连接为DIR连接;
  connection_link_connections(partner, base_conn);

  //将新建的AP连接加入到连接池之中以统一管理;
  if (connection_add(base_conn) < 0) { /* no space, forget it */
    connection_free(base_conn);
    return NULL;
  }

  base_conn->state = AP_CONN_STATE_CIRCUIT_WAIT;

  control_event_stream_status(conn, STREAM_EVENT_NEW, 0);

  /* attaching to a dirty circuit is fine */
  //最核心的AP连接与Circuit链路的关联函数,即为AP连接分配相应链路;
  if (connection_ap_handshake_attach_circuit(conn) < 0) {
    if (!base_conn->marked_for_close)
      connection_mark_unattached_ap(conn, END_STREAM_REASON_CANT_ATTACH);
    return NULL;
  }

  log_info(LD_APP,"... application connection created and linked.");
  return conn;
}

  经过这个函数的分析和以往的分析,我们简要地看到了系统整个网络请求的初步请求流程:系统对网络共识存在下载需求,那么就建立DIR连接连接目录服务器;而建立DIR连接之后,需要进一步建立与之关联的AP连接辅助其进行请求的发送;而AP连接又需要借助链路发送请求,所以链路的加载又必不可少。而后文又会提到,链路的建立,又是基于OR连接,所以最终会导致系统建立OR连接。OR连接,又是建立于TLS连接的基础之上,所以要想完成OR连接的建立,又要先建立TLS连接。这整个流程,就是客户端请求目录服务器服务的流程。实际上这个流程从AP连接段开始,也是所有其他需要发送请求的客户端连接的后半段流程,所以非常的重要。下面我们就往后再分析AP连接是如何选中或建立一个与之相对应的链路。当然,链路的建立,就意味着需要建立链路的基础:OR连接。

5. connection_ap_handshake_attach_circuit

  通常情况下,系统包括两大类的需要关联链路的连接:普通连接;隐藏服务相关连接。所以该函数内部的大逻辑就是根据连接类型的不同进行不同的操作。我们此时只针对普通连接进行分析,暂时不牵涉隐藏服务连接部分的内容。这样的话,我们可以省略函数内部很大一部分的内容:

/** Try to find a safe live circuit for CONN_TYPE_AP connection conn. If
 * we don't find one: if conn cannot be handled by any known nodes,
 * warn and return -1 (conn needs to die, and is maybe already marked);
 * else launch new circuit (if necessary) and return 0.
 * Otherwise, associate conn with a safe live circuit, do the
 * right next step, and return 1.
 */
/* XXXX this function should mark for close whenever it returns -1;
 * its callers shouldn't have to worry about that. */
int
connection_ap_handshake_attach_circuit(entry_connection_t *conn)
{
  ......

  if (!connection_edge_is_rendezvous_stream(ENTRY_TO_EDGE_CONN(conn))) {
    /* we're a general conn */
    origin_circuit_t *circ=NULL;

    if (conn->chosen_exit_name) {
      ...... //对出口结点有要求的连接,需要判断选定的出口结点是否被本机支持等;
    }

    /* find the circuit that we should use, if there is one. */
    retval = circuit_get_open_circ_or_launch(  //获得可用链接,或者开启一条新的链接;
        conn, CIRCUIT_PURPOSE_C_GENERAL, &circ);
    if (retval < 1) // XXX023 if we totally fail, this still returns 0 -RD
      return retval;

    //往下部分是当上一步直接有可用链接的时候执行;
    //也就是说,之前的函数返回值大于等于1之时,代表有现成的链接可供AP连接使用,那么直接关联两者即可;
    log_debug(LD_APP|LD_CIRC,
              "Attaching apconn to circ %d (stream %d sec old).",
              circ->_base.n_circ_id, conn_age);
    /* print the circ's path, so people can figure out which circs are
     * sucking. */
    circuit_log_path(LOG_INFO,LD_APP|LD_CIRC,circ);

    /* We have found a suitable circuit for our conn. Hurray. */
    return connection_ap_handshake_attach_chosen_circuit(conn, circ, NULL);

  } else { /* we're a rendezvous conn */
    ......
    return 0;
  }
}

  看完此处,大家可能会有困惑:没有可用链接的时候,会开启可用链接,但是,开启会那么快吗?链接不是意味着OR连接的建立吗?OR连接的建立不是意味着客户端和服务器成功完成TLS握手吗?这样复杂的过程,怎么能在一个函数当中做完?问题是,怎么能在单进程情况下这么做!关于这些困惑的解答,就是分析函数circuit_get_open_circ_or_launch之中所要关心的问题。带着这些问题,我们接着来看链接打开或建立函数。

6. circuit_get_open_circ_or_launch

  从函数名我们就可以简单进行猜测,该函数的主要作用,就是根据要求进行链路的选择,或者进行链路的建立。所以,本函数之中一定包含两个最重要的部分:链路选择;链路建立。此时,我们就可以对函数进行分析:

/** Find an open circ that we're happy to use for conn and return 1. If
 * there isn't one, and there isn't one on the way, launch one and return
 * 0. If it will never work, return -1.
 *
 * Write the found or in-progress or launched circ into *circp.
 */
static int
circuit_get_open_circ_or_launch(entry_connection_t *conn,
                                uint8_t desired_circuit_purpose,
                                origin_circuit_t **circp)
{
  ...... //变量和判断等;

  //获取合适链路
  circ = circuit_get_best(conn, 1, desired_circuit_purpose,
                          need_uptime, need_internal);

  if (circ) { //若已经成功找到合适链路,则直接在此处返回;
    *circp = circ;
    return 1; /* we're happy */
  }

  if (!want_onehop && !router_have_minimum_dir_info()) {
    ...... //若函数运行至此处,说明无合适链路,并且当前路由信息不够建立合适链路,则做相应获取路由信息的操作;
    return 0;
  }

  ......//对出口策略有要求时做的相关操作,一般情况下无策略要求;

  //经过上面的操作之后,重新尝试获取合适链路;(之前无合适链路,未必现在无合适链路)
  /* is one already on the way? */
  circ = circuit_get_best(conn, 0, desired_circuit_purpose,
                          need_uptime, need_internal);

  if (circ)
    log_debug(LD_CIRC, "one on the way!");
  if (!circ) {
    ...... //直到此处还没有有效的链路可用或正在被开启,恼羞成怒,自力更生开启链路;
           //开启链路的第一步,就是要指定链路中结点,在这个部分就是选中链路出口结点;

   //自行开启链路建立过程;
      circ = circuit_launch_by_extend_info(new_circ_purpose, extend_info,
                                           flags);
  } /* endif (!circ) */

  //结束处理
  if (circ) {
    connection_edge_update_circuit_isolation(conn, circ, 0);
  } else {
    log_info(LD_APP,"No safe circuit (purpose %d) ready for edge ""connection; delaying.",desired_circuit_purpose);
  }
  *circp = circ;
  return 0;
}

  根据我们之前的判断,到此处我们发现:该函数中最重要的起功能性作用的函数为:circuit_get_best;circuit_launch_by_extend_info。前者为在众多链路中选择最合适的链路,后者则为在没有合适链路的情况下开启链路的建立过程。我们这里先对如何选择合适的链路进行简要阐述:链路的选择需要遍历全局链路列表,在遍历的过程中不断比对最好的最合适的链路,一旦链路遍历完成,返回最好的链路选择。若无任何链路被视为合适,则返回为空。大家可能会关心其中的所谓合适是何种含义,但是这并不是程序执行主要流程的重点部分,所以若对该部分感兴趣的读者,可以参看函数:circuit_is_acceptable;circuit_is_better。其中一定有你们希望得到的答案!

  之后,我们再来看看链路建立函数circuit_launch_by_extend_info。那么既然程序流程运行到了这一步,我们也必然能够大致猜测到这个函数的功能了。因为该函数的参数和返回值我们都已经可以明晰地看到。下一个部分我们详细地分析链路建立过程。

7. circuit_launch_by_extend_info

  在开始这个函数的分析之前,我们先要谈谈建立一条Tor链路都需要些什么。我们知道,普通的链路建立,需要三个结点:入口结点,中间结点,出口结点。Tor系统在选择三个结点的时候分别有其各自的需求。对于入口结点,根据客户端的选择和相关配置,可能会被固定为一个或多个固定的结点;对于中间结点,可能会根据客户端对结点的参数需求进行相应选择;对于出口结点,选择的规则则更多,也相对复杂。虽然选择上有这些繁琐的部分,但是对于整个系统的执行过程来说,我们只需要知道,在建立链路的过程中,Tor系统需要通过各种各样选中的配置进行结点的选择。而选择的规则则可以从链路建立的结点选择过程当中看到端倪。

  该函数的操作之中,将参数extend_info用作出口结点,将purpose用作链路建立的目的,将flags用作链路建立的基本指示符。标示符的内容可以包括很多,有兴趣的读者可以翻看具体的函数操作来查看标示符的所有标志及其作用。在这个部分的分析中,我们只将重点的链路建立过程进行分析,不再拘泥于一些细枝末节,以避免妨碍到程序主要流程的清晰表述。

  查看源代码之后,我们发现该函数的内容较少。主要处理分支发生在对链路建立的目标和链路的出口结点是否被指定的判断上。在此处,我们暂时先默认认为单跳链路的建立不需要负责的处理操作,而非单跳的普通连接则可能需要在结点选择上做更多的判断。无论如何,在网络信息下载时,该处的链路是单跳链路,用于客户端直接连接目录服务器获取目录信息,所以必然是单跳的链路。于是乎我们发现,该函数对于单跳链路来说,主要是简单地调用了circuit_establish_circuit函数来进行后续处理。当然,非单跳函数最后也会调用上述函数进行处理。

8.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)
{
  ......

  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) { //完成到链路中第一个结点的链接的建立工作;
    ......
  }
  return circ;
}

  本文讲到此处,本应该接着往下分析链路结构体,链路的初始化和结点选择,以及链路的具体建立过程。但是这些部分非常繁杂,大家在没有宏观概念的情况下,未必在看完代码之后能够马上明白。所以此处,暂时先讲到链路开始建立的部分。接下来我们要做的事情,就是重新梳理,并讲清楚我们为什么要为了下载一个普通的网络信息而做这么复杂的嵌套函数操作,为什么我们需要一种又一种的连接,一个又一个的链接,以及往下的OR连接。这些为什么,只有在我们宏观地讲述完系统构架之后,才会变得明了起来。我们将这个部分的内容,放在下一节中。本节过于冗长,写到后半部分,已经很大程度上跟前述部分隔了许多层的关系。所以,在下一节中,我们通过重新梳理系统连接和链接框架,来更好地讲述连接和链接,以及继续分析链接的建立过程。