IPv6 は人類には早過ぎたんだ

人類は大袈裟ですが、私には厳しかったという話。

IPv6 対応のプログラムとは?

いたるところに書かれているので省略。以下の記事などを参考にどうぞ。

手短にまとめると

  1. gethostbyname(3) ではなく getaddrinfo(3) を使って名前解決する
  2. accept(2)getpeername(2) で返るアドレス情報は struct sockaddr_storage に格納する
  3. listen するソケットは AF_INET6 指定で作ると IPv4/v6 両対応になる

シンプルですよね。ここまでは。

落とし穴1:リンクローカルアドレス

IPv6 のアドレス体系は IPv4 とかなり異なります。最たるものがリンクローカルアドレスです。

リンクローカルアドレスはルーティングされないので、リンク(≒L2ネットワーク)内で一意でありさえすれば良いものです。 すべてのインタフェースは自動的に MAC アドレスから生成されたリンクローカルアドレスを持っています。 そのため、同一のリンクローカルアドレス複数同じホストが持つ場合があります(VLANやブリッジなどで)。

そこで、どのリンクのローカルアドレスかを指定するため、リンクローカルアドレスの後ろには %eth0 のようにスコープを指定することができます。こんな感じ。

fe80::fcff:ffff:fead:beaf%eth1

さて。IPv4 アドレス文字列の解析には inet_aton(3) を使っていたことでしょう。IPv6 アドレス向けには inet_pton(3) が用意されています。

ところが、inet_pton はこのスコープ付リンクローカルアドレスを解釈できません。そもそも IPv6 アドレスを格納する struct in6_addr の 128 bit ではスコープ識別子は格納できないのです。

どうするかというと、getaddrinfo を使います。getaddrinfo が返す struct sockaddr_in6 は以下のような構造体です。

struct sockaddr_in6 {
    sa_family_t sin6_family;        /* AF_INET6 */
    in_port_t   sin6_port;          /* port number */
    uint32_t    sin6_flowinfo;      /* IPv6 flow information */
    struct      in6_addr sin6_addr; /* IPv6 address */
    uint32_t    sin6_scope_id;      /* Scope ID (new in 2.4) */
};

この sin6_scope_id がリンクローカルアドレスが所属するリンクを示す ID になっています(通常は 0)。 たとえば自ホストがある IPv6 アドレスを持っているか判定するには、sin6_addr の比較に加えて、sin6_scope_id の比較も必要になります。以下に実装例を挙げます。

落とし穴2:DNS AAAA レコード

世の中の DNS サーバーには、存在しないレコードへの問い合わせを単に捨てるものがあるそうです(1, 2)。IPv6 アドレスは DNS の AAAA レコードを問い合わせるわけですが、AAAA レコードを持たないホスト名をそのような問題のある DNS サーバーに問い合わせるとタイムアウトまで数秒待たされることになります。

これをアプリケーション側で回避するには、IPv4 アドレスを優先的に問い合わせましょう。以下に実装例を挙げます。

落とし穴3:Ubuntu 12.04 特有の問題

(訂正: 2014-08-26 18:06 Ubuntu 12.04 の問題でした。Ubuntu 14.04 ではパッチの内容が改善され、問題は発生しないようです。)

最近の Linux はデフォルトで IPv6 が有効化されています。IPv6 が有効だと、前述のリンクローカルアドレスが自動的に各インタフェースに設定されます。

さて、getaddrinfo で接続先ホストのアドレスを問い合わせる際に、AI_ADDRCONFIG というオプションが指定できます。以下に説明を引用しますが、自ホストが IPv4(v6) アドレスをもっている場合のみ接続先の IPv4(v6) アドレスを返してもらう最適化のためのオプションです。

If  hints.ai_flags includes the AI_ADDRCONFIG flag, then IPv4 addresses
are returned in the list pointed to by res only if the local system has
at  least  one IPv4 address configured, and IPv6 addresses are returned
only if the local system has at least one IPv6 address configured.  The
loopback  address  is  not  considered  for  this  case  as  valid as a
configured address.

IPv6 が有効な状態ではリンクローカルアドレスが設定されるので、AI_ADDRCONFIG を指定しても IPv6 アドレスを常に返そうという挙動になるわけです。

ところがこの状態で、前述のいけてない DNS サーバーを使い、なおかつ IPv4 を優先して問い合わせる対策をしていないアプリケーションは DNS に AAAA レコードを問い合わせてしまい、タイムアウトまで数秒待たされるという不具合が発生したようです。

この現象に対応するために、Ubuntu 12.04 の glibc には以下のパッチ(debian/patches/any/local-ipv6-lookup.diff)が適用されています。 (Ubuntu 14.04 の local-ipv6-lookup.diff は改善されています。)

--- sysdeps/unix/sysv/linux/check_pf.c~ 2010-04-15 16:09:03.661086635 +0200
+++ sysdeps/unix/sysv/linux/check_pf.c  2010-04-15 17:50:08.401085393 +0200
@@ -178,7 +178,8 @@
                    }
                  else
                    {
-                     if (!IN6_IS_ADDR_LOOPBACK (address))
+                     if (!IN6_IS_ADDR_LOOPBACK (address)
+                         && !IN6_IS_ADDR_LINKLOCAL(address))
                        seen_ipv6 = true;
                    }
                }

このパッチの効果は、AI_ADDRCONFIG が指定されていて IPv6 リンクローカルアドレスが利用できる状態であっても、IPv6 の問い合わせをしないようにする はず のものでしょう。ですが実際には AAAA 問い合わせを抑止するだけでなく、IPv6 リンクローカルアドレス文字列の解析まで抑止してしまいます

どういうことかというと、Ubuntu 12.04 でリンクローカルアドレスだけ使って通信するアプリを作る場合、getaddrinfoAI_ADDRCONFIG フラグが付いていると、リンクローカルアドレスの問い合わせに失敗して通信ができないのです。なんということでしょう。

対応方法としては AI_ADDRCONFIG を付けないようにすればとりあえず回避できます。

まとめ

IPv6 苦労するけど、いつ普及するんでしょうね。おしまい。