最近偶尔发现一个比较奇怪的现象,netstat 查看监听的服务端口时,却只显示了 tcp6 的监控, 但是服务明明是可以通过 tcp4 的 ipv4 地址访问的,那为什么没有显示 tcp4 的监听呢?
    以 sshd 监听的 22 端口为例:

    1. # netstat -tlnp | grep :22
    2. tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN 1444/sshd
    3. tcp6 0 0 :::22 :::* LISTEN 1444/sshd

    可以看到,netstat 显示表示 sshd 既监听在 ipv4 的地址,又监听在 ipv6 的地址。
    而再看看 httpd 进程:

    1. # netstat -tlnp | grep :80
    2. tcp6 0 0 :::80 :::* LISTEN 19837/httpd

    却发现只显示了监听在 ipv6 的地址上 ,但是,通过 ipv4 的地址明明是可以访问访问的。
    下面来看下怎样解释这个现象。
    首先,关闭 ipv6 并且重启 httpd:

    1. # sysctl net.ipv6.conf.all.disable_ipv6=1
    2. # systemctl restart httpd

    现在,看下 httpd 监听的地址:

    1. # netstat -tlnp | grep :80
    2. tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 33697/httpd

    可以看到,已经只监听到 ipv4 地址了。
    那为什么在 ipv6 开启的时候,netstat 只显示了 tcp6 的监听而非像 sshd 那样既显示 tcp 又显示 tcp6 的监听呢?
    我们下载 httpd 的源码看一看,在代码 server/listen.c 的 open_listeners() 函数中, 有相关注释:

    1. /* If we have the unspecified IPv4 address (0.0.0.0) and
    2. * the unspecified IPv6 address (::) is next, we need to
    3. * swap the order of these in the list. We always try to
    4. * bind to IPv6 first, then IPv4, since an IPv6 socket
    5. * might be able to receive IPv4 packets if V6ONLY is not
    6. * enabled, but never the other way around.
    7. * ... 省略 ...
    8. */
    9. /*如果我们有未指定的 IPv4 地址 (0.0.0.0) 和 未指定的 IPv6 地址 (::)接下来,
    10. *我们需要在列表中交换这些的顺序。我们总是尝试先绑定到 IPv6,然后绑定到 IPv4。
    11. *因为如果V6ONLY未启用,IPv6套接字可能能够接收IPv4数据包,但相反确不可以(如果
    12. *先绑定IPv4,IPv4套接字可以接受IPv4,但是不能接受IPv6数据包)。
    13. *
    14. */

    上面提到,ipv6 实际上是可以处理 ipv4 的请求的当 V6ONLY 没有开启的时候,反之不然; 那么 V6ONLY 是在什么时候开启呢?
    继续 follow 代码到 make_sock() 函数,可以发现如下代码:

    1. #if APR_HAVE_IPV6
    2. #ifdef AP_ENABLE_V4_MAPPED
    3. int v6only_setting = 0;
    4. #else
    5. int v6only_setting = 1;
    6. #endif
    7. #endif

    在这个函数中,可以看到如果监听的地址是 ipv6,那么会去设置 IPV6_V6ONLY 这个 socket 选项, 现在,关键是看 AP_ENABLE_V4_MAPPED 是怎么定义的。
    在 configure(注意,如果是直接通过代码数获取的,可能没有这个文件,而只有 configure.ac/in 文件)文件中, 可以找到:

    1. # Check whether --enable-v4-mapped was given.
    2. if test "${enable_v4_mapped+set}" = set; then :
    3. enableval=$enable_v4_mapped;
    4. v4mapped=$enableval
    5. else
    6. case $host in
    7. *freebsd5*|*netbsd*|*openbsd*)
    8. v4mapped=no
    9. ;;
    10. *)
    11. v4mapped=yes
    12. ;;
    13. esac
    14. if ap_mpm_is_enabled winnt; then
    15. v4mapped=no
    16. fi
    17. fi
    18. if test $v4mapped = "yes" -a $ac_cv_define_APR_HAVE_IPV6 = "yes"; then
    19. $as_echo "#define AP_ENABLE_V4_MAPPED 1" >>confdefs.h

    所以,在 Linux 中,默认情况下,AP_ENABLE_V4_MAPPED 是 1,那么 httpd 就会直接监听 ipv6, 因为此时 ipv6 的 socket 能够处理 ipv4 的请求;另外,bind() 系统调用会对用户空间的进程透明处理 ipv6 没有开启的情况,此时会监听到 ipv4。
    而如果我们在编译 httpd 的时候使用 —disable-v4-mapped 参数禁止 ipv4 mapped,那么默认情况下, httpd 会分别监听在 ipv4 和 ipv6,而非只监听 ipv6,如下所示:

    1. # netstat -tlnp | grep :80
    2. tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 40576/httpd
    3. tcp6 0 0 :::80 :::* LISTEN 40576/httpd

    而,如果在 /etc/httpd/conf/httpd.conf 中将 Listen 设置为只监听 ipv6 地址,如下:

    1. Listen :::80

    那么,将可以看到 netstat 只显示 tcp6 的监听:

    1. # systemctl restart httpd
    2. # netstat -tlnp | grep :80
    3. tcp6 0 0 :::80 :::* LISTEN 40980/httpd

    并且,你会发现现在不能通过 ipv4 地址访问 httpd 了。

    1. # telnet 192.168.1.100 80
    2. Trying 192.168.1.100...
    3. telnet: Unable to connect to remote host: Connection refused

    所以,netstat 只是很真实的显示监听的端口而已,但是需要注意 ipv6 实际上在 Linux 上也支持 ipv4。
    Posted on Mar 05, 2017