libcurl の CURLOPT_INTERFACE のソースコードを読んでみた話

5月になりました. 5月の連休を満喫している真っ最中です…(笑)

そういやこのところ全く Tech ネタを書いてないことに気が付きました…

…ということで…URL転送ライブラリで有名なオープンソース libcurl で、通信するインタフェースを指定するオプション CURLOPT_INTERFACE がどうやって実現されているか、をソースコードを読んで調べてみた話を書きます.

CURLOPT_INTERFACE 

CURLOPT_INTERFACE は公式ドキュメントによると

CURLOPT_INTERFACE – source interface for outgoing traffic

https://curl.se/libcurl/c/CURLOPT_INTERFACE.html

とあり、トラフィックを出力するインタフェースを明示的に指定するオプションです.

形式は

#include <curl/curl.h>
 
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_INTERFACE, char *interface);

そして,

Pass a char * as parameter. This sets the interface name to use as outgoing network interface. The name can be an interface name, an IP address, or a host name.

https://curl.se/libcurl/c/CURLOPT_INTERFACE.html

指定するのは eth0 等のインタフェース名だけではなく特定のIPアドレスやホスト名も可能と書かれてます.

指定したインタフェース名が動作するシステムに存在しない場合,  CURLE_INTERFACE_FAILED  を返す、と公式ドキュメントに記載されています.

また、インタフェース名、ホスト名の指定については以下の様に記載されています.

If the parameter starts with “if!” then it is treated as only as interface name and no attempt will ever be named to do treat it as an IP address or to do name resolution on it. If the parameter starts with “host!” it is treated as either an IP address or a hostname. Hostnames are resolved synchronously. Using the if! format is highly recommended when using the multi interfaces to avoid allowing the code to block. 

https://curl.se/libcurl/c/CURLOPT_INTERFACE.html

すなわち, 以下の様に指定するように, と記載されています.

  • eth0 等のインタフェース名を指定する場合は, インタフェース名の前に”if!“をつけて, “if!eth0“の様に指定する.
  • hogehoge 等、ホスト名を指定する場合, 先頭に “host!” をつけて, “host!hogehoge” と指定する.

インタフェース名が指定された場合の動作に絞った調査を行った結果を記します.

調査対象としてlibcurlのソースコードはこちらから取得しました.

CURLOPT_INTERFACE のパラメータの受け渡し

curl_easy_setopt(CURLOPT_INTERFACE) で指定されたパラメータは./ib/setopt.c にある以下の関数で取り込まれます.

/*
 * Do not make Curl_vsetopt() static: it is called from
 * packages/OS400/ccsidcurl.c.
 */
CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
{

この関数Curl_vsetopt()は巨大な関数ですが、以下の行で指定したパラメータが取り込まれます.

  case CURLOPT_INTERFACE:
    /*
     * Set what interface or address/hostname to bind the socket to when
     * performing an operation and thus what from-IP your connection will use.
     */
    result = Curl_setstropt(&data->set.str[STRING_DEVICE],
                            va_arg(param, char *));
    break;

CURLOPT_INTERFACEで指定したパラメータは data->set.str[STRING_DEVICE] に格納されます.

指定されたパラメータが使われる bindlocal()

指定したインタフェース名が実際に使用されるのは ./lib/connect.c bindlocal() です.

static CURLcode bindlocal(struct Curl_easy *data,
                          curl_socket_t sockfd, int af, unsigned int scope)
{

関数の冒頭でCURLOPT_INTERFACEで指定されたパラメータがインタフェースなのかホストなのかを判定しています.

  const char *dev = data->set.str[STRING_DEVICE];
  int error;

  /*************************************************************
   * Select device to bind socket to
   *************************************************************/
  if(!dev && !port)
    /* no local kind of binding was requested */
    return CURLE_OK;

  memset(&sa, 0, sizeof(struct Curl_sockaddr_storage));

  if(dev && (strlen(dev)<255) ) {
    char myhost[256] = "";
    int done = 0; /* -1 for error, 1 for address found */
    bool is_interface = FALSE;
    bool is_host = FALSE;
    static const char *if_prefix = "if!";
    static const char *host_prefix = "host!";

    if(strncmp(if_prefix, dev, strlen(if_prefix)) == 0) {
      dev += strlen(if_prefix);
      is_interface = TRUE;
    }
    else if(strncmp(host_prefix, dev, strlen(host_prefix)) == 0) {
      dev += strlen(host_prefix);
      is_host = TRUE;
    }

指定されたパラメータの中に prefix である “if!” あるいは “host!” が含まれていないかで判定を行っています.

判定の結果はそれぞれ, is_interface, is_host に格納されます.

setsockopt(SO_BINDTODEVICE) によるインタフェース指定.

指定したパラメータがインタフェース名である場合, まず setsockopt(SO_BINDTODEVICE) でソケット sockfd とインタフェースを紐づけることを試みます.

以下のコードの19行目がそれに該当します.

  /* interface */
    if(!is_host) {
#ifdef SO_BINDTODEVICE
      /* I am not sure any other OSs than Linux that provide this feature,
       * and at the least I cannot test. --Ben
       *
       * This feature allows one to tightly bind the local socket to a
       * particular interface.  This will force even requests to other
       * local interfaces to go out the external interface.
       *
       *
       * Only bind to the interface when specified as interface, not just
       * as a hostname or ip address.
       *
       * interface might be a VRF, eg: vrf-blue, which means it cannot be
       * converted to an IP address and would fail Curl_if2ip. Simply try
       * to use it straight away.
       */
      if(setsockopt(sockfd, SOL_SOCKET, SO_BINDTODEVICE,
                    dev, (curl_socklen_t)strlen(dev) + 1) == 0) {
        /* This is typically "errno 1, error: Operation not permitted" if
         * you're not running as root or another suitable privileged
         * user.
         * If it succeeds it means the parameter was a valid interface and
         * not an IP address. Return immediately.
         */
        return CURLE_OK;
      }
#endif

以前, setsockopt(SO_BINDTODEVICE) については拙ブログに書いたことがあります.

ここに書いた通り,setsockopt(SO_BINDTODEVICE) の実行には ROOT 権限, あるいは  CAP_NET_RAW capabilityが必要です.

ソースコードのコメントにもそのような意味のことが書かれてます.

このコードを一般ユーザーで CAP_NET_RAW capabilityが無い状態で実行した場合, setsockopt(SO_BINDTODEVICE) は Operation not permittedで失敗しますが, bindlocal()関数はこの場でエラーとせず、後述する処理を実行します.

一般ユーザーで動作させた場合のふるまい

setsockopt(SO_BINDTODEVICE) を一般ユーザーで動作させ, Operation not permitted で処理失敗した後の動作ですが、まず指定されたインタフェースに割り付けられた IP アドレスを Curl_if2i() 関数で取得します.

      switch(Curl_if2ip(af,
#ifdef ENABLE_IPV6
                        scope, conn->scope_id,
#endif
                        dev, myhost, sizeof(myhost))) {
        case IF2IP_NOT_FOUND:
          if(is_interface) {
            /* Do not fall back to treating it as a host name */
            failf(data, "Couldn't bind to interface '%s'", dev);
            return CURLE_INTERFACE_FAILED;
          }
          break;
        case IF2IP_AF_NOT_SUPPORTED:
          /* Signal the caller to try another address family if available */
          return CURLE_UNSUPPORTED_PROTOCOL;
        case IF2IP_FOUND:
          is_interface = TRUE;
          /*
           * We now have the numerical IP address in the 'myhost' buffer
           */
          infof(data, "Local Interface %s is ip %s using address family %i",
                dev, myhost, af);
          done = 1;
          break;
      }

Curl_if2i() 関数で指定したインタフェースが存在しない、あるいは設定されたIP アドレスが取得できない場合、エラーとして処理が終了します.

Curl_if2i() 関数の中身をのぞいてみます.

Curl_if2i() 関数は ./lib/if2ip.c にあります. getifaddrs() によってシステム上のすべてのインタフェースの情報を一度に収集します.

#if defined(HAVE_GETIFADDRS)

if2ip_result_t Curl_if2ip(int af,
#ifdef ENABLE_IPV6
                          unsigned int remote_scope,
                          unsigned int local_scope_id,
#endif
                          const char *interf,
                          char *buf, int buf_size)
{
  struct ifaddrs *iface, *head;
  if2ip_result_t res = IF2IP_NOT_FOUND;

#if defined(ENABLE_IPV6) && \
    !defined(HAVE_SOCKADDR_IN6_SIN6_SCOPE_ID)
  (void) local_scope_id;
#endif

  if(getifaddrs(&head) >= 0) {
    for(iface = head; iface != NULL; iface = iface->ifa_next) {
      if(iface->ifa_addr) {
        if(iface->ifa_addr->sa_family == af) {
          if(strcasecompare(iface->ifa_name, interf)) {
            void *addr;
            const char *ip;
            char scope[12] = "";
            char ipstr[64];

取得した struct ifaddrs のリスト構造のデータから該当するインタフェースを探します.

該当するインタフェースがあればIPアドレス情報を取得します.

余談ですが, getifaddrs() で取得したインタフェースアドレス情報の領域を使い終わった後、  freeifaddrs() で返却することをお忘れなく.

bind() と setsockopt(IP_BIND_ADDRESS_NO_PORT)

指定されたインタフェースの IP アドレスが取得できれば、通信で使用するソケットのソース IP アドレスを bind() で指定します.

以下のソースコードの 5行目がそれに該当します.

#ifdef IP_BIND_ADDRESS_NO_PORT
  (void)setsockopt(sockfd, SOL_IP, IP_BIND_ADDRESS_NO_PORT, &on, sizeof(on));
#endif
  for(;;) {
    if(bind(sockfd, sock, sizeof_sa) >= 0) {
      /* we succeeded to bind */
      struct Curl_sockaddr_storage add;
      curl_socklen_t size = sizeof(add);
      memset(&add, 0, sizeof(struct Curl_sockaddr_storage));
      if(getsockname(sockfd, (struct sockaddr *) &add, &size) < 0) {
        char buffer[STRERROR_LEN];
        data->state.os_errno = error = SOCKERRNO;
        failf(data, "getsockname() failed with errno %d: %s",
              error, Curl_strerror(error, buffer, sizeof(buffer)));
        return CURLE_INTERFACE_FAILED;
      }
      infof(data, "Local port: %hu", port);
      conn->bits.bound = TRUE;
      return CURLE_OK;
    }

connect() する際、あらかじめ souce IP address を bind() で指定することで出力するインタフェースの指定を実現しています.

bind() の前、上記コードの2行目に setsockopt(IP_BIND_ADDRESS_NO_PORT) が実行されています.

一般的なクライアントのふるまいとして,

  1. socket() によるソケットの生成.
  2. connect() によるサーバーへの接続. その際、source IP は  INADDR_ANY, source port は 0 に指定. クライアント側の Linux kernel にそれらの設定を委ねます.
  3. その結果, source IP address はシステムのルーティングの結果より決まり, source port は エフェメラルポート (ephemeral port) , すなわち 一時的ポート番号から自動的に決定されます.

となります.

このコードではクライアントが明示的に source IP を bind()によって指定しますが、source Port についてはエフェメラルポートから自動的に割り付けるため 0 を指定します.

そうした場合, bind() 実行時にもエフェメラルポートの中から1個、ポート番号が予約され、さらに connect() を実行時にもエフェメラルポートの中からポート番号が1個消費されることになります.

エフェメラルポートの範囲は /proc/sys/net/ipv4/ip_local_port_range に記載されており, デフォルトでは 49152から65535まで使用することが可能です.

この範囲が十分大きい場合、ポート番号の消費は問題になりませんが、上記の範囲を狭めて使用できるポート番号を少なくした場合, bind() と connect() の両方でポート番号の消費は問題となりえます.

setsockopt(IP_BIND_ADDRESS_NO_PORT) は bind() 実行時にポート番号の予約をせず、無駄なポート番号の消費を防ぐことが出来ます.

setsockopt(IP_BIND_ADDRESS_NO_PORT) を指定したときの Linux kernel の動作も興味深いので、後日そのソースコードを調査した結果を書きたいと思います.

setsockopt(SO_BINDTODEVICE) した場合, あるいは bind() で source IP address を指定した場合のふるまい

setsockopt(SO_BINDTODEVICE) した場合, あるいは bind() で source IP address を指定した場合, 出力するインタフェースに関連した Routing Table を参照します.

以前書いた記事には少々事実誤認があったので修正しておきました.

例えば、指定したインタフェースがインターネットに接続している場合, そのインタフェースに紐づいた default route の設定が必要となります.

複数の default route の設定は可能で、setsockopt(SO_BINDTODEVICE) した場合, あるいは bind() で source IP address を指定しない場合, routing table に設定されているメトリック値が小さいものが選択されます.

setsockopt(SO_BINDTODEVICE) した場合, あるいは bind() で source IP address を指定された場合, そのインタフェースに紐づいた routing table の設定値が参照され, その中に default route が設定されていれば, 指定された Router を経由して外のネットワークにトラフィックが出ていくことになります.

Linux kernel の IP stack で Routig Table の参照するコードはかなり深いので、これから時間をみて読み進めたいと思います.

では.