记一次Client无法获取IPv6地址问题的分析过程

近日SQA报了一个bug,对路由器经过6天左右的压力测试后,无论是有线设备还是无线设备都拿不到IPv6地址了。经过层层分析发现可能是kernel内存泄漏。本文便记录这一问题的分析过程。

检查网络状态

首先打开Router的console,使用ifconfig br0(br0是Router LAN端的桥接地址)查看当前的网络状态

$ ifconfig br0
br0       Link encap:Ethernet  HWaddr A0:63:91:A7:63:07  
          inet addr:192.168.27.1  Bcast:192.168.27.255  Mask:255.255.255.0
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:530995052 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1012058984 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:108097854210 (100.6 GiB)  TX bytes:1331062907089 (1.2 TiB)

可以看到,Router自身也只有IPv4地址,没有IPv6地址,所以问题出在Router自身,不可能是Client配置错误,那为什么Router自己都没有拿到IPv6呢?下面来分析压力测试过程保存的console log.

分析console log

在压力测试的前几天,从以下log可知Router是有IPv6的,Router貌似正常运行几天后就失去了Ipv6

br0       Link encap:Ethernet  HWaddr A0:63:91:A7:63:07  
          inet addr:192.168.27.1  Bcast:192.168.27.255  Mask:255.255.255.0
          inet6 addr: 2002:76a7:859d:0:44f3:adff:fef9:f52/64 Scope:Global
          inet6 addr: fe80::a263:91ff:fea7:6307/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:1184678 errors:0 dropped:0 overruns:0 frame:0
          TX packets:1476645 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:1200677624 (1.1 GiB)  TX bytes:1867999218 (1.7 GiB)

其中的IPv6地址如下:

inet6 addr: 2002:76a7:859d:0:44f3:adff:fef9:f52/64 Scope:Global
inet6 addr: fe80::a263:91ff:fea7:6307/64 Scope:Link

第一个IPv6是用来与外部互联网通信的地址,第二个以fe80::开头的称为链路本地地址(Link-local address),与IPv4中的169.254.0.0/16地址类似,此类地址不需要与外部网络通信,通常用于本地主机间的相互通讯。

IPv6相关进程分析

好了,现在的问题是什么导致IPv6忽然不见了,我最初想法是Router的dhcpv6服务器挂了,但是ps |grep dhcp后发现是正常的。

$ ps |grep dhcp
 2951 root        368 S   udhcpd /tmp/udhcpd.conf
28323 root        352 S   /usr/sbin/dhcp6s -3 -c /tmp/dhcp6s.conf br0

此处说明一点,由于WAN端使用的PPPoE拨号上网方式,所以没有启动dhcp6c(用于获取ipv6的客户端程序)。那是不是还有其它相关的进程挂了呢?为此我问了下组长(大牛),发现确实还有个radvd进程!对比前后log中的ps输出发现确实是这个进程挂了!!!

路由广播守护(The Router Advertisement Daemon,简称:radvd)是一个符合RFC 2461使用邻居发现协议用于实现IPv6地址本地链接广播和IPv6路由前缀的开源软件。该软件是给系统管理员用于实现在IPv6下对主机进行无状态自动配置地址。
--- 维基百科

总之,radvd也是IPv6必不可少的进程,而且莫名其妙的挂了。

radvd进程退出原因分析

为了分析radvd进程退出原因,我尝试手动重启进程

$ /usr/sbin/radvd -C /tmp/radvd.conf
[Feb 26 06:29:51] radvd: syntax error in /tmp/radvd.conf, line 19: {
[Feb 26 06:29:51] radvd: error parsing or activating the config file: /tmp/radvd.conf

提示配置文件第19语法错误,那来看一下/tmp/radvd.conf

$ cat /tmp/radvd.conf
interface br0 {
        AdvSendAdvert on;
        AdvCurHopLimit 64;
        MinRtrAdvInterval 198;
        MaxRtrAdvInterval 600;
        AdvOtherConfigFlag on;
        AdvDefaultLifetime 1800;
        AdvReachableTime 0;
        AdvRetransTimer 0;
        AdvDefaultPreference low;
        AdvHomeAgentFlag off;
        AdvManagedFlag off;
        prefix 2002:24e0:69cf:0::/64 {
                AdvOnLink on;
                AdvAutonomous on;
                AdvValidLifetime 2400;
                AdvPreferredLifetime 1800;
        };
        RDNSS  {
                AdvRDNSSPreference 8;
                AdvRDNSSLifetime 1200;
        };
};

第19行RDNSS {与正常情况RDNSS fe80::a263:91ff:fea7:6307 {相比少了链路本地地址,也就是fe80::开头的地址。那么这个问题是怎么产生的呢?为什么会少参数呢?这就需要来看具体的代码了。

net6conf脚本分析

分析GUI相关代码后发现,在点击Router的IPv6配置页面的Apply按钮后,后台会执行net6conf restart,那么就需要分析net6conf代码

$ grep -rn fe80:: net6conf
net6conf:97:    $IP -6 addr add fe80::$eui64/64 dev $bridge
net6conf:103:   $IP -6 addr add fe80::$ipv6_interface_id/64 dev $bridge

使用grep初步定位到链路本地地址的生成处,由于net6conf是个shell脚本,所以可以在代码附近添加一行set -x,然后手动执行net6conf restart,那么代码执行过程就会很详细的打印出来,接着找到生成链路本地地址的那一部分

+ /usr/sbin/ip -6 addr add fe80::a263:91ff:fea7:6307/64 dev br0
RTNETLINK answers: Cannot allocate memory

OK,总算找到问题的关键所在了,程序在添加地址时出现了无法内存分配的错误。

无法分配内存原因分析

对于RTNETLINK answers: Cannot allocate memory,常规思路当然是内存不足了,但是通过free查看还有200M+绝对足够。然后谷歌一下,貌似遇到类似问题的也不少,比如IPv6 routing/neighbor table suspected memory leak,也是跑了几天就出这个问题了。

大部分的解决方案是修改一个系统级的配置参数net.ipv6.route.max_size,这个参数定义了IPv6路由表的最大尺寸,默认值为4096Bytes,将其改大一点就可以了。

$ sysctl net.ipv6.route.max_size
net.ipv6.route.max_size = 4096
$ sysctl -w net.ipv6.route.max_size=16384
net.ipv6.route.max_size = 16384
$ /usr/sbin/ip -6 addr add fe80::a263:91ff:fea7:6307/64 dev br0

修改完后重启IPv6,发现一切都回归正常了,Oh yeah😀

那么最后还剩一个问题,按理说net.ipv6.route.max_size是系统自带的默认参数,参数值也是经过实践验证的,一般来说是够用的,为什么会出现不够用的情况?依据网上的资料显示,可能是kernel memory leak, 具体原因还需进一步测试和验证,但至少目前增大这个参数确实能够解决问题!

参考