netstatやlsofでLISTENしているアドレスポートが IPv6と表示されてもIPv4でアクセスできる罠


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のソースコードを軽く検索してみたら、すぐに発見

/*
 * 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:80IPv4-mapped IPv6アドレスであり、
sshdはIPV6_V6ONLYが偽であるため、IPv4とIPv6の両方のポートが見えたが、
httpdはIPV6_V6ONLYが真であるため、IPv6のポートしか見えないということだ。

結論

OSのIPv6設定が有効の場合で、かつデーモンがIPV6_V6ONLYフラグを真にしてソケットを作成していた場合、
netstatやlsofではIPv6の「IPv4-mapped IPv6アドレス」しか見えていなくても、IPv4でもアクセスできる