俺の知っているコマンドがIPv6で使えないわけがない | サイバーエージェント 公式エンジニアブログ

こんにちは。maginemuです。
ここのところはiOS/Android向けにPIGG PARTYというサービスの開発を行っています。


ピグパーティ[ピグパ]


はじめに

iOS9アプリケーションのIPv6への対応期限が刻々と迫ってきました。


今回IPv6への対応に際してトラブルがあり、原因調査をしたのですが、
もともと全然IPv6の知識が無く、ネットワークに関する知識も大して無く、


かろうじて知っているコマンド群もIPv6でうまく動かなかったりしてハマったので、
知ってる人には当たり前だと思いますが、紹介してみることにします。


その前に少しだけ前提を。


iOS9とIPv6対応

AppleではiOS9以降、IPv6でアプリケーションが正しく動作する必要があるとしています。
そしてその対応期限は2016年初頭とされていて、それ以降は対応していないと審査が通らなくなる…!


対応の仕方についてはこちらを参照します。


※実際にアプリケーションをIPv6に対応させることについては、この記事では触れません。


IPv6 DNS64/NAT64環境を用意しよう

早い話が、El Capitanではoptionを押しながらインターネット共有を選択すると、NAT64ネットワークを作成するチェックボックスが出ます。


適当なマシンでこのオプションでインターネット共有を行い、そこにアクセスすればIPv6の環境が試せます。


ひとつ気をつける必要があるのが、インターネットに接続したい場合にはこのマシンがWi-Fi以外でインターネットに接続できる
必要があります(有線LANとか)。
なので最近のMacbook Proなどを使う場合、Ethernetアダプタとかが必要になるかもしれないです。


これで手元ではiMacxxxx...という感じのネットワークが見つかるようになりました。
これに接続して試していきます。


IPアドレスを確認しよう

何はともあれ、諸々確認しましょう。手元の環境は MacBook Pro (Retina, 15-inch, Mid 2014), OS X 10.11.1 です。
ひとまずifconfig


...
en0: flags=8863<UP,BROADCAST,SMART,RUNNING,SIMPLEX,MULTICAST> mtu 1500
ether aa:bb:cc:dd:ee:ff
inet6 fe80::a8bb:ccff:fedd:eeff%en0 prefixlen 64 scopeid 0x4
inet6 2001:xxxx::a8bb:ccff:fedd:eeff prefixlen 64 optimistic autoconf
inet6 2001:xxxx::xxxx:xxxx:xxxx:xxxx prefixlen 64 autoconf temporary
nd6 options=1<PERFORMNUD>
media: autoselect
status: active
...

en0のインタフェースのところだけ抜粋。inet6 となっているところがIPv6アドレスですね。
IPv6では、ホストひとつに対して複数のアドレスを持つことができます。


fe80から始まるアドレスはリンクローカルなアドレスで、ルータを越えない同一ネットワーク内のアドレスです。
下位64ビットはイーサネットアドレス48ビットを24ビットずつに区切り、間にfffeを挟み、
上から7ビット目を反転させたものになっています。


2001から始まるものはグローバルアドレスです。正確には2000::/3からE000::/3がグローバルユニキャストアドレスだそうです。
2001から始まるものが、いまのところIANAによって割り振られているようです。


グローバルアドレスのうち上のほうはステートレス設定というやつで生成されたもののようです。
これはルータから得られたプレフィクスとMACアドレスベースで生成されるものです。


temporaryというのは一時アドレスというやつで、上記ステートレス設定のものだと、
アドレスからマシンが特定できてしまうので、定期的に変わるアドレスも用意されているみたいです。


IPアドレスの体系に関してはとりあえず、3つのタイプがあって、
またそれぞれ到達範囲:スコープでも分類できるという感じです。


IPv6のアドレス(ステートレス)自動設定については下記などに書いてあります(なるべく確実そうなやつ)。


疎通確認(ping)

とりあえず自分にpingしてみようかな!


$ ping 2001:xxxx::a8bb:ccff:fedd:eeff
ping: cannot resolve 2001:xxxx::a8bb:ccff:fedd:eeff: Unknown host

なん…だと…
IPv6だと何か違うのか…! (ping ipv6とかでググる)


知ってれば何てことないんですが、コマンド違うんですよね。。ping6というのがあります。


$ ping6 2001:xxxx::a8bb:ccff:fedd:eeff
PING6(56=40+8+8 bytes) 2001:xxxx::a8bb:ccff:fedd:eeff --> 2001:xxxx::a8bb:ccff:fedd:eeff
16 bytes from 2001:xxxx::a8bb:ccff:fedd:eeff, icmp_seq=0 hlim=64 time=0.084 ms
16 bytes from 2001:xxxx::a8bb:ccff:fedd:eeff, icmp_seq=1 hlim=64 time=0.210 ms
16 bytes from 2001:xxxx::a8bb:ccff:fedd:eeff, icmp_seq=2 hlim=64 time=0.213 ms
16 bytes from 2001:xxxx::a8bb:ccff:fedd:eeff, icmp_seq=3 hlim=64 time=0.235 ms
^C
--- 2001:xxxx::a8bb:ccff:fedd:eeff ping6 statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 0.084/0.185/0.235/0.059 ms

よしよし。


サーバのアドレスが知りたい(nslookup)

知ってますよ。nslookupですよね。


$ nslookup google.com
Server:  2001:xxxx::1
Address:  2001:xxxx::1#53


Non-authoritative answer:
google.com
Address: 216.58.220.206

あれ…v4のアドレスだけですね。。


あー、さては。


$ man nslookup6
No manual entry for nslookup6

違うの。。。


(ググる)


なるほど。-type=AAAAというオプションか。


$ nslookup -type=AAAA google.com
Server:  2001:xxxx::1
Address:  2001:xxxx::1#53


Non-authoritative answer:
has AAAA address 64:ff9b::d83a:dcce


Authoritative answers can be found from:google.com

おお。取れた。Wikipediaによると


In the Domain Name System hostnames are mapped to IPv6 addresses by AAAA resource records, so-called quad-A records.

だそうです。


ページ取得(wget)

wgetしてみましょう。


$ wget google.com
--2015-12-09 18:46:38--  http://google.com/
Resolving google.com... 64:ff9b::d83a:dcce, 216.58.220.206
Connecting to google.com|64:ff9b::d83a:dcce|:80... connected.
HTTP request sent, awaiting response... 302 Found
Location: http://www.google.co.jp/?gfe_rd=cr&ei=fvhnVumcLMT98we005DwBg [following]
--2015-12-09 18:46:38--  http://www.google.co.jp/?gfe_rd=cr&ei=fvhnVumcLMT98we005DwBg
Resolving www.google.co.jp... 64:ff9b::adc2:75f7, 64:ff9b::adc2:75f8, 64:ff9b::adc2:75ff, ...
Connecting to www.google.co.jp|64:ff9b::adc2:75f7|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: 'index.html'


[ <=>                                                                           ] 19,287      --.-K/s   in 0.009s


2015-12-09 18:46:38 (2.10 MB/s) - 'index.html' saved [19287]

おお。普通にいけた。helpを見ると


-6,  --inet6-only              connect only to IPv6 addresses.

というオプションがあるみたいですね。


IPアドレスだと…


$ wget 64:ff9b::d83a:dcce
--2015-12-09 18:49:38--  ftp://64/ff9b::d83a:dcce
=> 'ff9b::d83a:dcce'
Resolving 64... 0.0.0.64
Connecting to 64|0.0.0.64|:21... failed: Network is unreachable.

おお、ftpだと思われている。では。


$ wget http://64:ff9b::d83a:dcce
http://64:ff9b::d83a:dcce: Bad port number.

ぐぬぬ。ですよねーーー


IPv6のアドレスを指定するときは、そのままだとポート番号と区別が付きません。
[]で括る必要がありそうです。


$ wget http://\[64:ff9b::d83a:dcce\]
--2015-12-09 19:32:48--  http://[64:ff9b::d83a:dcce]/
Connecting to [64:ff9b::d83a:dcce]:80... connected.
HTTP request sent, awaiting response... 302 Found
Location: http://www.google.com/ [following]
--2015-12-09 19:32:48--  http://www.google.com/
Resolving www.google.com... 64:ff9b::adc2:75f3, 64:ff9b::adc2:75f2, 64:ff9b::adc2:75f1, ...
Connecting to www.google.com|64:ff9b::adc2:75f3|:80... connected.
HTTP request sent, awaiting response... 302 Found
Location: http://www.google.co.jp/?gfe_rd=cr&ei=UANoVpuWO6Kg8weR_oCoDQ [following]
--2015-12-09 19:32:48--  http://www.google.co.jp/?gfe_rd=cr&ei=UANoVpuWO6Kg8weR_oCoDQ
Resolving www.google.co.jp... 64:ff9b::adc2:75f8, 64:ff9b::adc2:75f7, 64:ff9b::adc2:75ef, ...
Connecting to www.google.co.jp|64:ff9b::adc2:75f8|:80... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [text/html]
Saving to: 'index.html'


[ <=>                                                                                   ] 19,215      --.-K/s   in 0.03s


2015-12-09 19:32:49 (553 KB/s) - 'index.html' saved [19215]

無事取れました。


link-localアドレスでの通信

一旦ローカルホストでサーバを立てて、それに接続して検証したかったりしますよね。


上述しましたが、IPv6が有効になっているホストでは
まずリンクローカルアドレスが割り当てられます。


このアドレスはiMacやNAT64の環境を用意しなくても使えそうです。


とりあえず適当なhttpサーバを。。nodeとかnpmとかexpressが入ってる前提で
($ npm install -g express)


$ express app && cd app && npm install && DEBUG=app:* npm start


create : app
create : app/package.json
create : app/app.js
create : app/public
create : app/public/javascripts
create : app/public/images
create : app/public/stylesheets
...
node ./bin/www


app:server Listening on port 3000 +0ms

ここで、本質的じゃないのですが、nodeでlistenするときに、IPを指定しないとIPv6でうまくlistenされないようなので
ちょっとソースコードを変更しておきます。


diff --git a/bin/www b/bin/www
index a8c2d36..e79c926 100755
--- a/bin/www
+++ b/bin/www
@@ -25,7 +25,7 @@ var server = http.createServer(app);
Listen on provided port, on all network interfaces.
*/


-server.listen(port);
+server.listen(port, '::0');
server.on('error', onError);
server.on('listening', onListening);


$ wget localhost:3000
--2015-12-11 15:24:56--  http://localhost:3000/
Resolving localhost... ::1, 127.0.0.1, fe80::1
Connecting to localhost|::1|:3000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 170 [text/html]
Saving to: 'index.html'
2015-12-11 15:24:56 (40.5 MB/s) - 'index.html' saved [170/170]

よしよし。ではIPアドレスで。


$ wget http://\[fe80::a8bb:ccff:fedd:eeff\]
--2015-12-11 15:32:03--  http://[fe80::a8bb:ccff:fedd:eeff]:3000/
Connecting to [fe80::a8bb:ccff:fedd:eeff]:3000... failed: No route to host.

あれ。。。


$ ping6 fe80::82e6:50ff:fe14:18be
ping6: UDP connect: No route to host

あれー。。。


調べてみると、どうやら-Iオプションが必要そうです。man ping6を見てみると


-I interface
Source packets with the given interface address.  This flag applies if the ping destination is a mul-
ticast address, or link-local/site-local unicast address.

なるほど。今は送出先がリンクローカルユニキャストアドレスです。
リンクローカルなアドレスを指定するときには、なにかとインタフェース名とか、scope_idといった値を
指定する必要がありそうです。


ifconfigを再度見直してみると


inet6 fe80::a8bb:ccff:fedd:eeff%en0 prefixlen 64 scopeid 0x4

%en0とか、scopeid 0x4とか書いてありますね。


ということで、


$ ping6 -I en0 fe80::a8bb:ccff:fedd:eeff
PING6(56=40+8+8 bytes) fe80::a8bb:ccff:fedd:eeff%en0 --> fe80::a8bb:ccff:fedd:eeff
16 bytes from fe80::a8bb:ccff:fedd:eeff%en0, icmp_seq=0 hlim=64 time=0.100 ms
16 bytes from fe80::a8bb:ccff:fedd:eeff%en0, icmp_seq=1 hlim=64 time=0.104 ms
16 bytes from fe80::a8bb:ccff:fedd:eeff%en0, icmp_seq=2 hlim=64 time=0.209 ms
16 bytes from fe80::a8bb:ccff:fedd:eeff%en0, icmp_seq=3 hlim=64 time=0.232 ms
^C
--- fe80::a8bb:ccff:fedd:eeff ping6 statistics ---
4 packets transmitted, 4 packets received, 0.0% packet loss
round-trip min/avg/max/std-dev = 0.100/0.161/0.232/0.060 ms

うまく通りました。


しかし、wgetは未だにうまくできず。。%en0を付けてみて、


$ wget http://\[fe80::a8bb:ccff:fedd:eeff%en0\]:3000
http://[fe80::a8bb:ccff:fedd:eeff%en0]:3000: Invalid IPv6 numeric address.

だめか。。ちなみにcurlだと…?


$ curl http://\[fe80::82e6:50ff:fe14:18be%en0\]:3000                                                                      ⏎
<!DOCTYPE html><html><head><title>Express</title><link rel="stylesheet" href="/stylesheets/style.css"></head><body><h1>Express</h1><p>Welcome to Express</p></body></html>%

いけたーーーー


scope_idの指定の仕方に関しては、けっこう実装依存のようです。


上記を見てみると、a numeric zone index can be used in the second 16-bit wordとあります。では試しに


$ wget http://\[fe80:4::a8bb:ccff:fedd:eeff\]:3000
--2015-12-11 17:36:13--  http://[fe80:4::a8bb:ccff:fedd:eeff]:3000/
Connecting to fe80:4::a8bb:ccff:fedd:eeff|fe80::a8bb:ccff:fedd:eeff|:3000... connected.
HTTP request sent, awaiting response... 200 OK
Length: 170 [text/html]
Saving to: 'index.html'


100%[======================================================================================>] 170         --.-K/s   in 0s
2015-12-11 17:36:13 (20.3 MB/s) - 'index.html' saved [170/170]

おお。いけました。なんだそれ。。。


ちなみに、GoogleChromeのアドレスバーに打ち込んだところ、


http://[fe80:4::a8bb:ccff:fedd:eeff]:3000/

この形式では取得できましたが、


http://[fe80::a8bb:ccff:fedd:eeff%en0]:3000/

の場合検索ワードと見なされてしまいました。


http://[fe80::a8bb:ccff:fedd:eeff%25en]:3000/

とかエンコーディングしてみても変わらずでした。


ローカルホストのIPでアクセスするだけでこれほどハマるとは。。。


まとめ

少しですが、IPv6環境について紹介させて頂きました。


最初はコマンド自体が合ってなかったので、どこに問題があるのかの
切り分け自体できなくてハマりました。。


IPv4からアドレス空間が広くなっただけでなく、
知らない仕様が色々あることを痛感しました。。


調べてみると、200X年とかの記事が結構ヒットしますね。
今後IPv6環境もいよいよ増える流れかと思いますので、
トラブルシューティングのための準備を整えて行きたいところです。


ではでは!