netstatやlsofでLISTENしているポートを調べることはよくあると思いますが、
LISTENしているアドレスポートが IPv6形式で表示されてもIPv4でアクセスできる罠があるので、その説明をします。
罠の説明
例えばCentOS 6.7にてhttpdをyumから入れて、httpdをスタートさせた後に、netstatを実行すると以下の様に表示されます。
# netstat -natp | grep LISTEN
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1084/sshd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1165/master
tcp 0 0 :::80 :::* LISTEN 2316/httpd
tcp 0 0 :::22 :::* LISTEN 1084/sshd
tcp 0 0 ::1:25 :::* LISTEN 1165/master
sshdについては0.0.0.0:22
というIPv4形式の出力と、:::22
というIPv6形式の出力の両方が表示されています。
それに対して、httpdは:::80
というIPv6形式の出力しか表示されていません。
lsofでも、sshdはIpv4とIPv6の両方がでますが
# lsof -i:22
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
sshd 1084 root 3u IPv4 10943 0t0 TCP *:ssh (LISTEN)
sshd 1084 root 4u IPv6 10954 0t0 TCP *:ssh (LISTEN)
httpdはIPv6のみが表示されます
# lsof -i:80
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
httpd 2316 root 4u IPv6 30431 0t0 TCP *:http (LISTEN)
一見するとhttpdはIPv6でしかアクセスできないように見えます。
しかし!
実際には、IPv4でアクセスできます。
# telnet 192.168.1.241 80
Trying 192.168.1.241...
Connected to 192.168.1.241.
Escape character is '^]'.
パケットキャプチャしてもIPv4で接続しているのがわかります!
試しに、OSの設定を変えてIPv6を無効にすると、IPv6表現の表示は消えることが分かりました。
# netstat -napt | grep LISTEN
tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1271/httpd
tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 968/sshd
tcp 0 0 127.0.0.1:25 0.0.0.0:* LISTEN 1059/master
つまり、IPv6が有効な状態でしか起きない問題ということです。
これは一体なぜなのか・・・
この問題は、ググるといくつかヒットします
- Netstat not displaying all listening ports when using IPv4 and IPv6
- HTTPD listening in IPv6, according netstat, but reacheable in IPv4
しかしどこにも明快な答えはないようです。
試しにnetstatのソースコードを軽く検索してみたら、すぐに発見
/*
* Construct an Internet address representation.
* If the nflag has been supplied, give
* numeric value, otherwise try for symbolic name.
*/
static char *
inetname(sa)
struct sockaddr *sa;
{
char *cp = 0;
static char line[NI_MAXHOST];
struct hostent *hp;
struct netent *np;
struct in_addr in;
#ifdef INET6
if (sa->sa_family == AF_INET6) {
if (memcmp(&((struct sockaddr_in6 *)sa)->sin6_addr,
&in6addr_any, sizeof(in6addr_any)) == 0)
strcpy(line, "*");
else
getnameinfo(sa, sa->sa_len, line, sizeof(line), NULL, 0,
nflag ? NI_NUMERICHOST : 0);
return (line);
}
#endif
この部分で文字列を生成しているっぽいけど、#ifdef INET6
はIPv6が有効の時のみに通るのでしょう。
そしてif (sa->sa_family == AF_INET6) {
の部分は、ソケットアドレス構造体のsa_familyがIPv6のものかどうかを判定する分岐で、これが真だとIPv6の表現を計算して即リターンしているようです。
うーん、つまり、sa構造体をどのように作るかで、文字列表現が変わってるということなのでしょう。
さらに調べていくと、IPV6のmapページに決定的な内容を発見
v4-mapped-on-v6 アドレス型を用いることで、 IPv4 接続も v6 API で扱うことができる。 こうすれば、プログラムは v6 の API をサポートするだけで、 両方のプロトコルをサポートできる。 v4-mapped-on-v6 アドレス型は C ライブラリ内部のアドレスを 扱う関数によって透過的に処理される。
IPv4 と IPv6 はローカルポート空間を共有する。 IPv4 の接続 (またはパケット) を IPv6 ソケットが取得すると、 発信元アドレスが v6 にマップされ、その接続 (パケット) も v6 にマップされる。
さらに
IPV6_V6ONLY (Linux 2.4.21 以降および 2.6 以降)
このフラグを真 (0 以外) に設定すると、そのソケットは IPv6 パケットだけを 送受信するように制限される。 この場合、IPv4 アプリケーションと IPv6 アプリケーションが同時に 一つのポートをバインドできる。
このフラグを偽 (0) に設定すると、そのソケットはパケットの送受信に IPv6 アドレスと IPv4-mapped IPv6 アドレスの両方を使用できる。
引き数はブール値の入った整数へのポインターである。
このフラグのデフォルト値はファイル /proc/sys/net/ipv6/bindv6only の内容により定義される。 このファイルのデフォルト値は 0 (偽) である。
完全にわかった。
:::80
というのは0.0.0.0:80
のIPv4-mapped IPv6アドレスであり、
sshdはIPV6_V6ONLYが偽であるため、IPv4とIPv6の両方のポートが見えたが、
httpdはIPV6_V6ONLYが真であるため、IPv6のポートしか見えないということだ。
結論
OSのIPv6設定が有効の場合で、かつデーモンがIPV6_V6ONLYフラグを真にしてソケットを作成していた場合、
netstatやlsofではIPv6の「IPv4-mapped IPv6アドレス」しか見えていなくても、IPv4でもアクセスできる