linux内核协议栈之IPV6与路由

 【摘要】

【地址格式】

【问题复现】

【路由信息】

【路由添加】

【路由查找】

【原因分析】


注意:请使用谷歌浏览器阅读(IE浏览器排版混乱)


【摘要】

本文将以ipv6为例,以一个实际问题为突破点,向大家介绍一下linux内核TCP/IP协议栈中路由表的管理。

【地址格式】

为便于理解,先介绍一下ipv6地址的基本知识

1. 为了更好的理解 IPv6,比较IPv4和IPv6地址对应关系和区别。

IPv4地址                IPv6地址 

组播地址( 224.0.0.0/4)    IPv6组播地址(FF00::/8)
广播地址               无,只有任播( anycast)地址
未指定地址为 0.0.0 .0      未指定地址为 ::
回路地址为 127.0.0.1       回路地址为 ::1
公用 IP地址             可汇聚全球单播地址
私有地址( 10.0.0 .0/8、172.16.0.0/12和192.168.0.0/16)  本地站点地址( FEC0::/48)
Microsoft自动专用IP寻址自动配置的地址(169.254.0.0/16)  本地链路地址( FE80::/64)
表达方式:点分十进制       表达方式:冒号十六进制式(取消前置零、零压缩)
2. IPv6地址作用域和地址分类
1>IPv6地址指定给接口,一个接口可以指定多个地址。
2>Pv6地址有作用域:
link local地址 本链路有效
site local地址 本区域(站点)内有效,一个site通常是个校园网
global地址 全球有效,即可汇聚全球单播地址
3>Pv6地址分类:
unicast 单播(单点传送)地址
multicast 组播(多点传送)地址
anycast 任播(任意点传送)地址
IPv6没有定义广播地址,其功能由组播地址替代

3. 常见的IPv6地址及其前缀 

::/128      即0:0:0:0:0:0:0:0,只能作为尚未获得正式地址的主机的源地址,不能作为目的地址,不能分配给真实的网络接口。
::1/128      即0:0:0:0:0:0:0:1,回环地址,相当于IPv4中的localhost(127.0.0.1),ping locahost可得到此地址。
2001::/16    全球可聚合地址,由 IANA 按地域和ISP进行分配,是最常用的IPv6地址,属于单播地址。
2002::/16    6 to 4 地址,用于6to4自动构造隧道技术的地址,属于单播地址。
3ffe::/16   早期开始的IPv6 6bone试验网 地址,属于单播地址。
fe80::/10   本地链路地址,用于单一链路,适用于自动配置、邻机发现等,路由器不转发以fe80开头的地址。
ff00::/8    组播地址。
::A.B.C.D    兼容IPv4的IPv6地址,其中<A.B.C.D>代表IPv4地址。自动将IPv6包以隧道方式在IPv4网络中传送的IPv4/IPv6节点将使用这些地址。
::FFFF:A.B.C.D   是IPv4映射过来的IPv6地址,其中<A.B.C.D>代表IPv4地址,例如 ::ffff:202.120.2.30 ,它是在不支持IPv6的网上用于表示IPv4节点。

4 本文主要介绍linux系统下ipv6路由管理的实现方法,其中会穿插介绍一个ipv6相关问题。其中包括kernel源码及一些基本概念介绍,可以略过不看。

Ps:可以只看 问题提出、问题复现、原因分析几个部分了解此问题。

【问题复现】

本文以一个实际问题为切入点进行分析,复现方法如下:

1 设备ip为::192.168.1.108/0时

#ip -6 addr add ::192.168.1.108/0 dev eth0
1>此时路由表中增加前缀路由:

#cat /proc/net/ipv6_route

rt=84380c80 00000000000000000000000000000000 00 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001     eth0

2>此时路由表的根结点上(前缀=0),挂了两个路由项:

 # cat /proc/net/ipv6_route 

rt=84386980 00000000000000000000000000000000 00 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001      eth0   –配置ip时,添加的前缀路由。
rt=8077f880 00000000000000000000000000000000 00 00000000000000000000000000000000 00 00000000000000000000000000000000 ffffffff 00000001 00000002 00200200       lo  –根结点的根路由,属于lo设备。

#ping ::192.168.1.101 (直连pc的ip) –发包过程查到前缀路由表项,所以能通。

# ip route del dev eth0  --删除默认路由后造成如下两个问题:

1)上面路由表中前缀路由消失,设备发包过程查表查到了根结点,发送流程终止,所以ping不同。
2) 删除默认路由后,设备无法再通过命令设置任何路由。
只能通过如下操作,才能重新配置路由: 
#ip addr del ::192.168.1.108/0 dev eth0
#ip addr add ::192.168.1.108/0 dev eth0

此时,设置ip成功添加了前缀路由,而通过命令配置路由无法成功。
因为这两种方式,设置路由时,使用的的flag不同,
#define RTF_UP  0x0001  /* route usable     */
#define RTF_GATEWAY 0x0002  /* destination is a gateway */
前缀路由使用的flag=1;命令设置路由使用flag=3
当flag=3时,设置路由过程中,ip6_route_add->rt6_lookup会查找路由表,查表时要求路由表中必须存在路由表项。即设置的网关地址,必须在路由表项的目的网络中。

2 设备ip为::192.168.1.101/1时:
#ip -6 addr add ::192.168.1.108/1 dev eth0
此时路由表中增加项为前缀路由:

#cat /proc/net/ipv6_route

rt=84388b80 00000000000000000000000000000000 01 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001     eth0

#ping ::192.168.1.101 (直连pc的ip) –发包过程查到该表项,所以能通。

# ip route del dev eth0    --删除默认路由
删除路由后能ping通。此处与ip为::192.168.1.108/0时明显不同。
(此处若故意删除该指定路由结点,ip  -6 route del ::/1 via :: dev eth0,则同样ping不通).

要理解根因,得从路由添加、查找、删除 三个方面,介绍一下,linux操作系统是如何管理ipv6路由表的。

3 实例。

问题在实际设备上的表现:

设备IP = ::192.168.1.108/1   -eth0  其对应的dev-> ifindex =3.
Pc:   ::192.168.1.101 
状态:设备与pc直连。 
问题:设备ping  直连的电脑,
1)当设备 IP =::192.168.1.108/1 时能够ping通。
2)当设备 IP =::192.168.1.108/0 且不重启电脑时,能够ping通。
3)当设备 IP =::192.168.1.108/0 且重启电脑时,不能ping通。  
实际问题分析:

软件设置路由流程 : 先删除默认路由,再设置。

首先,分析一下3)当设备 IP =::192.168.1.108/0 且重启电脑时,不能ping通。
问题的根本原因是:当设备ip地址的前缀为0且系统还不存在默认路由,此时删除默认路由,系统会把前缀路由删除掉。

要理解根因,得从路由添加、查找、删除 三个方面,介绍一下,linux操作系统是如何管理ipv6路由表的。

【路由信息】

分析问题之前,先给出一些设备的基本信息:

1  IP地址信息
# ifconfig
eth0      Link encap:Ethernet  HWaddr 00:34:39:A3:39:34 
          inet addr:192.168.1.101  Bcast:192.168.255.255  Mask:255.255.0.0
          inet6 addr: fe80::234:39ff:fea3:3934/64 Scope:Link
          UP BROADCAST RUNNING MULTICAST  MTU:1500  Metric:1
          RX packets:44 errors:0 dropped:0 overruns:0 frame:0
          TX packets:7 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:1000
          RX bytes:4100 (4.0 KiB)  TX bytes:528 (528.0 B)
          Interrupt:27

lo        Link encap:Local Loopback 
          inet addr:127.0.0.1  Mask:255.0.0.0
          inet6 addr: ::1/128 Scope:Host
          UP LOOPBACK RUNNING  MTU:65536  Metric:1
          RX packets:0 errors:0 dropped:0 overruns:0 frame:0
          TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
          collisions:0 txqueuelen:0
          RX bytes:0 (0.0 B)  TX bytes:0 (0.0 B)

2 路由信息

# cat /proc/net/ipv6_route   --查看设备的ipv6路由信息,以一个条目为例。

fe80000000000000023439fffea33934 80 00000000000000000000000000000000 00 00000000000000000000000000000000 00000000 00000001 00000000 80200001

解释:

fe80000000000000023439fffea33934 80 :为目的网络及前缀。
00000000000000000000000000000000 00:只有主路由表,则默认0。
00000000000000000000000000000000:网关地址。
00000000:rt->rt6i_metric  rt6_select时使用,路由选择的条件。
00000001:rt->dst.__refcnt 路由表管理时使用。
       * __refcnt wants to be on a different cache line from
       * input/output/ops or performance tanks badly
00000000:rt->dst.__use 路由被使用次数。
80200001:rt->rt6i_flags 是否刷新等。

注释1: rt6i_flags等于->

#define RTF_DEFAULT 0x00010000 /* default - learned via ND */
#define RTF_ALLONLINK 0x00020000 /* (deprecated and will be removed)
        fallback, no routers on link */
#define RTF_ADDRCONF 0x00040000 /* addrconf route - RA  */
#define RTF_PREFIX_RT 0x00080000 /* A prefix only route - RA */
#define RTF_ANYCAST 0x00100000 /* Anycast   */
#define RTF_NONEXTHOP 0x00200000 /* route with no nexthop */
#define RTF_EXPIRES 0x00400000
#define RTF_ROUTEINFO 0x00800000 /* route information - RA */
#define RTF_CACHE 0x01000000 /* cache entry   */
#define RTF_FLOW 0x02000000 /* flow significant route */
#define RTF_POLICY 0x04000000 /* policy route   */
#define RTF_PREF(pref) ((pref) << 27)
#define RTF_PREF_MASK 0x18000000
#define RTF_REJECT 0x0200  /* Reject route   */
#define RTF_LOCAL 0x80000000
注释2:RTF_CACHE = 01000000表示系统会刷新该路由信息。30秒会刷掉。
static __inline__ void fib6_start_gc(struct net *net, struct rt6_info *rt)
{
 if (!timer_pending(&net->ipv6.ip6_fib_timer) &&
     (rt->rt6i_flags & (RTF_EXPIRES | RTF_CACHE)))
  mod_timer(&net->ipv6.ip6_fib_timer,
     jiffies + net->ipv6.sysctl.ip6_rt_gc_interval);
}
【路由添加】

首先介绍两个概念:
一 何为路由?
路由包括目的网络地址、目的网络地址前缀长度、网关地址。
如设置路由:ip -6 route add 2001::12/64 via ::192.168.1.1 dev eth0
目的网络地址=2001:12(系统会将其转换为2001::0);
目的网络地址前缀长度=64;
网关=::192.168.1.1
上述路由信息会保存到路由表的路由结点上。内核代码中通常以fn->leaf=rt_info描述
二 何为目的地址?
目的地址可以是用户发包时指定的报文地址(可能是任意网络上)
不过,在添加路由时,查找路由过程用到的目的地址是要添加路由的网关地址。

目的地址不保存在路由表中,路由管理的实质就是找到目的地址在路由表上对应到哪个目的网络上。

1 为什么要介绍路由添加?
根因中提到,前缀路由被删除.
那么需要了解的是,前缀路由是什么,它是何种情况下添加进路由表的?

2 路由表:本文讨论的系统只有主路由表,所以在此只介绍主路由表。
代码注释:主路由表根结点初始函数。
2.1 系统启动过程中初始化主路由表:

static int __net_init fib6_net_init(struct net *net)
{
 size_t size = sizeof(struct hlist_head) * FIB6_TABLE_HASHSZ;
 setup_timer(&net->ipv6.ip6_fib_timer, fib6_gc_timer_cb, (unsigned long)net);
 net->ipv6.rt6_stats = kzalloc(sizeof(*net->ipv6.rt6_stats), GFP_KERNEL);
 if (!net->ipv6.rt6_stats)
  goto out_timer;
 /* Avoid false sharing : Use at least a full cache line */
 size = max_t(size_t, size, L1_CACHE_BYTES);
 net->ipv6.fib_table_hash = kzalloc(size, GFP_KERNEL);
 if (!net->ipv6.fib_table_hash)
  goto out_rt6_stats;
 net->ipv6.fib6_main_tbl = kzalloc(sizeof(*net->ipv6.fib6_main_tbl),
       GFP_KERNEL);
 if (!net->ipv6.fib6_main_tbl)
  goto out_fib_table_hash;
  /* 主路由表ID */
 net->ipv6.fib6_main_tbl->tb6_id = RT6_TABLE_MAIN;
/* 主路由表根节点初始化 ,其中根节点的路由配置信息在下面函数中初始化*/
 net->ipv6.fib6_main_tbl->tb6_root.leaf = net->ipv6.ip6_null_entry;
 net->ipv6.fib6_main_tbl->tb6_root.fn_flags =
  RTN_ROOT | RTN_TL_ROOT | RTN_RTINFO;
 inet_peer_base_init(&net->ipv6.fib6_main_tbl->tb6_peers);
 fib6_tables_init(net);
 return 0;
}
2.2 ip6_null_entry初始化,主路由表的根节点的struct rt6_info *leaf;保存的是lo设备的默认路由信息,如果查找路由过程找到了该路由信息,说明没有匹配的路由 
static int __net_init ip6_route_net_init(struct net *net)
{
 int ret = -ENOMEM;
 memcpy(&net->ipv6.ip6_dst_ops, &ip6_dst_ops_template,
        sizeof(net->ipv6.ip6_dst_ops));
 if (dst_entries_init(&net->ipv6.ip6_dst_ops) < 0)
  goto out_ip6_dst_ops;
  /* 主路由表根节点的路由描述信息 */
 net->ipv6.ip6_null_entry = kmemdup(&ip6_null_entry_template,
        sizeof(*net->ipv6.ip6_null_entry),
        GFP_KERNEL);
 if (!net->ipv6.ip6_null_entry)
  goto out_ip6_dst_entries;
 net->ipv6.ip6_null_entry->dst.path =
  (struct dst_entry *)net->ipv6.ip6_null_entry;
 net->ipv6.ip6_null_entry->dst.ops = &net->ipv6.ip6_dst_ops;
 dst_init_metrics(&net->ipv6.ip6_null_entry->dst,
    ip6_template_metrics, true);
        /* proc下面的配置参数 */
 net->ipv6.sysctl.flush_delay = 0;
 net->ipv6.sysctl.ip6_rt_max_size = 4096;
 net->ipv6.sysctl.ip6_rt_gc_min_interval = HZ / 2;
 net->ipv6.sysctl.ip6_rt_gc_timeout = 60*HZ;
 net->ipv6.sysctl.ip6_rt_gc_interval = 30*HZ;
 net->ipv6.sysctl.ip6_rt_gc_elasticity = 9;
 net->ipv6.sysctl.ip6_rt_mtu_expires = 10*60*HZ;
 net->ipv6.sysctl.ip6_rt_min_advmss = IPV6_MIN_MTU - 20 - 40;
 net->ipv6.ip6_rt_gc_expire = 30*HZ;
}
3 什么情况下系统会添加路由表?

1)主动添加,比如用户通过命令配置路由的过程,就是主动添加。
2)配置IP时,系统会默认添加几条路由信息。
3)接收和发送报文的过程,有可能会添加路由。

4 路由表添加过程
举例:
4.1 路由信息:
# cat /proc/net/ipv6_route
Rt_info5=fe80000000000000023439fffea33934  80      lo
Rt_info4=fe800000000000000000000000000000 40      eth0
Rt_info3=ff020000000000000000000000010002 80      eth0
/*路由信息在普通结点上。(fn_bit=8结点上)*/
Rt_info2=ff000000000000000000000000000000 08      eth0    (node2)
/*路由信息在root结点上。(fn_bit=0路由信息在根结点上)*/
Rt_info1=00000000000000000000000000000000 00      eth0  
/*路由信息在root结点上。(fn_bit=0路由信息在根结点上)*/
Rt_info0=00000000000000000000000000000000 00      lo                                      
根据路由表管理方法,建立路由树状模型如下:
              Root(::/0)
                 |           
            Node5(f::0/4)
        /             \       
Node4(fe80::0/64)     Node2(ff::0/8)
    |                 |
Node6           Node3(ff02::01:02/128)
(fe80000000000000023439fffea33934/128)

4.2 上述路由信息添加到路由表的过程。
一个路由节点node上可以添加多条路由信息rt_info.
1) Rt_info0在root结点上---root(rt_info0);
2) Rt_info1加入路由表时:路由信息加入到根结点上,且之前该结点上已存在lo设备的默认路由Root(lo,eth0)—即root结点上有两条路由,分别是lo和eth0网络设备的:
Root(rt_info0,rt_info1);

3) Rt_info2(目的网络前缀plen=8,目的网络=ff::0/8)加入路由表:
从root(此时root->fn_bit=0)结点开始遍历->
root结点时,可参看代码fib6_add_1:
plen>fn_bit &&!ipv6_prefix_equal(&key->addr, addr, fn->fn_bit)==0
需要在root结点下的结点里继续查找,因为此时,只有root结点,所以一定没有找到合适结点,所以需要新建结点node2(此时node2->fn_bit=8, 目的网络key->addr=ff02::0/8),rt_info2则插入到新结点node2上。
注意如果plen=fn_bit&& ipv6_prefix_equal(&key->addr, addr, fn->fn_bit)==0
则不需要新建路由结点,直接将路由信息插入root就可以。
       Root(rt_info0,rt_info1)
              |
       Node2 (ff::0/8)

4) Rt_info3(目的网络前缀plen=128,目的网络=ff02::01:02/8)加入路由表时:
从root(fn_bit=0)结点开始遍历—>
root结点时:可参看代码:
plen(128)>fn_bit(0) &&!ipv6_prefix_equal(&key->addr, addr, fn->fn_bit)==0
所以,需要在root结点下,继续查找,当查找到node2时。
Nod2结点时:可参看代码:
plen(128)>fn_bit(8) &&!ipv6_prefix_equal(&key->addr, addr, fn->fn_bit)==0
此时key->addr与addr前fn_bit位相等,说明新结点是node2的子结点,但路由表中node2结点已经没有子结点了。
所以需要在node2结点下新建结点node3(此时node3->fn_bit=128,
目的网络key->addr=ff02::01:02/128),把rt_info3插入新建结点node3上.
注意此时如果plen=8则不需要新建结点,路由信息直接插入node2就可以。
   Root
    |
Node2(ff::0/8)
    |
Node3(ff02::01:02/128)

5) Rt_info4(目的网络前缀plen=64,目的网络=fe80::0/64)加入路由表时:
从root(fn_bit=0)结点开始遍历—>
root结点时,此时fn_bit=0:可参看函数fib6_add_1:
plen(64)>fn_bit(0) &&!ipv6_prefix_equal(&key->addr, addr, fn->fn_bit)==0
所以,需要在root结点下,继续查找,当查找到node2时。
Nod2结点时,此时fn_bit=8:可参看代码:
plen(64)>fn_bit(8) &&!ipv6_prefix_equal(&key->addr, addr, fn->fn_bit)==1,
此时key->addr=ff:0,addr=fe80::0
所以需要在node2结点的父结点上新建结点node4,把rt_info3插入新建结点node4上,
又因为 ke->addr与addr第一个不同的bit是第4位:所以还需要添加一个node5,
Node4与node5分别如下:
Node4(node4->fn_bit=64,目的网络key->addr=fe80::0/64))
Node5(node5->fn_bit=4,目的网络key->addr=f::0/4)
                       Root(::/0)
                          |           
                     Node5(f::0/4)
                    /        \       
                Node4(fe80::0/64)     Node2(ff::0/8)
                                      |
                                 Node3(ff02::01:02/128)


6) Rt_info5
(目的网络前缀plen=128,目的网络= fe80000000000000023439fffea33934)加入路由表时:
从root(fn_bit=0)结点开始遍历—>
root结点时,此时fn_bit=0,可参看代码:
plen(128)>fn_bit(0) &&!ipv6_prefix_equal(&key->addr, addr, fn->fn_bit)==0
所以,需要在root结点下,继续查找,下一个结点node5。
Node5结点时,此时fn_bit=4,可参看代码:
plen(128)>fn_bit(4) &&!ipv6_prefix_equal(&key->addr, addr, fn->fn_bit)==0,
此时key->addr=f:0,addr= fe80000000000000023439fffea33934)
所以需要继续查找路由结点node4、node2、node3
Node4结点时,此时fn_bit=64:可参看代码:
plen(128)>fn_bit(64) &&!ipv6_prefix_equal(&key->addr, addr, fn->fn_bit)==0,
此时key->addr=fe80:0,addr= fe80000000000000023439fffea33934,此处说明新建结点是node4结点的子结点。而node4已经没有子结点了。
所以需要在node结点上新建结点node6,把rt_info5插入新建结点node4上,
                      Root(::/0)
                          |           
                      Node5(f::0/4)
                     /        \       
               Node4(fe80::0/64)     Node2(ff::0/8)
                 |                     |
               Node6              Node3(ff02::01:02/128)
               (fe80000000000000023439fffea33934/128)

若plen=n,则路由表中可添加2的n次方个路由结点。
以上,以一个实例介绍了路由表的添加过程. 路由表添加实际上就是把路由信息保存到一个fib6_node结点的rt6_info=leaf链表中,并把该fib6_node结点挂载到以主路由表root结点为根的树上。

代码注释1:添加路由之前,首先会根据配置的路由信息生成cfg结构:
ipv6_route_ioctl->
#ip addr add ::192.168.1.108/1 dev eth0
#ip -6 route add ::192.168.1.101/1 via ::192.168.1.1 dev eth0

static void rtmsg_to_fib6_config(struct net *net,
     struct in6_rtmsg *rtmsg,
     struct fib6_config *cfg)
{
 memset(cfg, 0, sizeof(*cfg));
 cfg->fc_table = RT6_TABLE_MAIN;
 cfg->fc_ifindex = rtmsg->rtmsg_ifindex;
 cfg->fc_metric = rtmsg->rtmsg_metric;
 cfg->fc_expires = rtmsg->rtmsg_info;
 cfg->fc_dst_len = rtmsg->rtmsg_dst_len;/*目的网络前缀长度=1*/
 cfg->fc_src_len = rtmsg->rtmsg_src_len;
 cfg->fc_flags = rtmsg->rtmsg_flags;
 cfg->fc_nlinfo.nl_net = net;
 cfg->fc_dst = rtmsg->rtmsg_dst; /* 目的网络ip=::192.168.1.101*/
 cfg->fc_src = rtmsg->rtmsg_src;
 cfg->fc_gateway = rtmsg->rtmsg_gateway; /*目的网关=::192.168.1.1*/
}

生成了struct fib6_config *cfg后,开始添加路由

代码注释2:ip6_route_add()中根据上文的cfg生成路由信息 struct rt6_info *rt;
rt_info是路由表的基本单位,一个路由结点上可以有几个rt_info. 可以理解为一个路由就是一个rt_info,这个结构包括如下信息:
目的地址=rt6_info ->rt6i_dst
网关=rt->rt6i_gateway
目的地址前缀长度=rt->rt6i_dst.plen
每个rt_info中都有一个dst_entry信息,协议栈收发数据包过程,都根据该信息实现数据收发: 
ip6_route_add->
rt->dst.input = ip6_input;
rt->dst.output = ip6_output;

添加和查找路由表都是以rt_info为基本单位进行的。
比如添加一个路由,会把路由信息保存到一个rt_info然后挂到指定的路由结点(fib_node)上。
路由结点:是内核为方便管理路由表引入的概念,无论添加还是查找路由信息之前,都要根据目的网络找到匹配的路由结点,如果要更深入理解,可以参考文章中的代码注释。
查找路由,最终会查到一个rt_info,其中包含了所需路由信息。如果最后查到的路由信息是网络设备lo的默认路由信息,则认为查表失败,即路由表中没有匹配的路由。

代码注释3:将路由信息插入路由表: ip6_route_add ->__ip6_ins_rt()->fib6_add()
例子:主路由表的结构为:struct fib6_table *fib6_main_tbl;
路由表上的根结点为: struct fib6_node tb6_root—--它是fib6_bable结构体的一个成员变量;
每个路由表项对应一个节点的一个leaf,每个结点都包含若干描述路由配置信息的leaf:struct rt6_info *leaf—--它是fib6_node结构的一个成员变量。

代码注释4:ip6_route_add ->__ip6_ins_rt()->fib6_add-> fib6_add_1
该函数根据目的网络,创建新的路由结点并添加到路由表对应的路由结点树上。

如果路由结点已存在,则不用再创建。

                   fn_node
                 /                 \
        fn_node               fn_node
             /   \                      /       \
 fn_node fn_node    fn_node  fn_node

static struct fib6_node * fib6_add_1(struct fib6_node *root, void *addr,
         int addrlen, int plen,
         int offset, int allow_create,
         int replace_required)
{
 struct fib6_node *fn, *in, *ln;
 struct fib6_node *pn = NULL;
 struct rt6key *key;
 int bit;
 __be32 dir = 0;
 __u32 sernum = fib6_new_sernum();
 RT6_TRACE("fib6_add_1\n");
 /* insert node in tree */
/* root为主路由表中根节点,在上文fib6_net_init中初始化 */
/* ip6_route_net_init初始化root根节点中rt6_info成员,即fn->leaf */
 fn = root;
/* 
从root开始遍历路由表结点的树,根据目的地址及前缀找到合适的路由表项fib6_node 
*/
 do {
  key = (struct rt6key *)((u8 *)fn->leaf + offset);
/* 
如果目的地址前缀小于该结点的fn_bit,
或结点中已有目的地址与要查找的目的地址不相等,则去该节点的父节点中找
*/
  if (plen < fn->fn_bit ||
            /*  要添加目的地址与结点中已有路由的目的地址不同 */
      !ipv6_prefix_equal(&key->addr, addr, fn->fn_bit)) {
   if (!allow_create) {
    if (replace_required) {
     pr_warn("Can't replace route, no match found\n");
     return ERR_PTR(-ENOENT);
    }
    pr_warn("NLM_F_CREATE should be set when creating new route\n");
   }
   goto insert_above;
  }
/*
向fn结点以上添加结点:
以下代码执行条件:目的地址前缀>=该结点的fn_bit,且结点中已有目的地址与要查找的目的地址相等
*/
/*
 如果目的地址前缀等于该节点的fn_bit则 把该路由项插入到这个fn子树中 
 此处完全匹配,即目的网络和前缀都相等
*/
  if (plen == fn->fn_bit) {
   /* clean up an intermediate node */
   if (!(fn->fn_flags & RTN_RTINFO)) {
    rt6_release(fn->leaf);
    fn->leaf = NULL;
   }
   fn->fn_sernum = sernum;
   return fn;
  }
  /*
   * We have more bits to go
   */
  /* Try to walk down on tree. */
  fn->fn_sernum = sernum;
  dir = addr_bit_set(addr, fn->fn_bit);
  pn = fn;
  fn = dir ? fn->right: fn->left;
 } while (fn);
 /*
  * We walked to the bottom of tree.
  * Create new leaf node without children.
  */
/*
 如果目的地址前缀>该结点的fn_bit,则在fn结点以下添加路由结点。
即fn结点的子结点。 
*/
 ln = node_alloc();
 if (!ln)
  return ERR_PTR(-ENOMEM);
 ln->fn_bit = plen;
 ln->parent = pn;
 ln->fn_sernum = sernum;
 if (dir)
  pn->right = ln;
 else
  pn->left  = ln;
 return ln;
向fn结点以上添加结点:
insert_above:
 /*
  * split since we don't have a common prefix anymore or
  * we have a less significant route.
  * we've to insert an intermediate node on the list
  * this new node will point to the one we need to create
  * and the current
  */
 pn = fn->parent;
 /* find 1st bit in difference between the 2 addrs.
    See comment in __ipv6_addr_diff: bit may be an invalid value,
    but if it is >= plen, the value is ignored in any case.
  */
/*要添加的目的地址与结点中现有的目的地址,第一个不同的bit*/
 bit = __ipv6_addr_diff(addr, &key->addr, addrlen);
/*
程序至此,说明:
1 目的地址前缀plen小于fn->fn_bit(fn结点对应的前缀长度).
此时bit<=plen<fn->fn_bit. 此时存在前缀长度为bit的中间结点,它是新建结点和fn结点的上结点。
或者:
 2 plen>=fn->fn_bit 且要添加目的地址与结点中已有路由的目的地址的前fn_bit位不同.
注意此时,fn->fn_bit一定>bit,否则不会到该分支.
此时:若plen>bit 则存在前缀长度为bit的中间结点,它是新建结点和fn结点的上结点
*/
/* 
如果目的网络前缀>bit(第一个不同的位)则添加两个结点in和ln
 /*
  *  (intermediate)[in]
  *           /    \
  * (new leaf node)[ln] (old node)[fn]
  */
此处涉及非常巧妙,可以仔细理解一下
*/
if (plen > bit) {
  in = node_alloc();
  ln = node_alloc();
  if (!in || !ln) {
   if (in)
    node_free(in);
   if (ln)
    node_free(ln);
   return ERR_PTR(-ENOMEM);
  }
  /*
   * new intermediate node.
   * RTN_RTINFO will
   * be off since that an address that chooses one of
   * the branches would not match less specific routes
   * in the other branch
   */
  in->fn_bit = bit;
  in->parent = pn;
  in->leaf = fn->leaf;
  atomic_inc(&in->leaf->rt6i_ref);
  in->fn_sernum = sernum;
  /* update parent pointer */
  if (dir)
   pn->right = in;
  else
   pn->left  = in;
  ln->fn_bit = plen;
  ln->parent = in;
  fn->parent = in;
  ln->fn_sernum = sernum;
  if (addr_bit_set(addr, bit)) {
   in->right = ln;
   in->left  = fn;
  } else {
   in->left  = ln;
   in->right = fn;
  }
此时:若plen<=bit, 则新建结点是fn结点的上结点
 } else { /* plen <= bit */
  /*
   *  (new leaf node)[ln]
   *           /    \
   *      (old node)[fn] NULL
   */
  ln = node_alloc();
  if (!ln)
   return ERR_PTR(-ENOMEM);
  ln->fn_bit = plen;
  ln->parent = pn;
  ln->fn_sernum = sernum;
  if (dir)
   pn->right = ln;
  else
   pn->left  = ln;
  if (addr_bit_set(&key->addr, plen))
   ln->right = fn;
  else
   ln->left  = fn;
  fn->parent = ln;
 }
 return ln;
}
代码注释3:将路由信息插入到路由结点中。
int fib6_add(struct fib6_node *root, struct rt6_info *rt, struct nl_info *info)
{
 struct fib6_node *fn, *pn = NULL;
 int err = -ENOMEM;
 int allow_create = 1;
 int replace_required = 0;
 if (info->nlh) {
  if (!(info->nlh->nlmsg_flags & NLM_F_CREATE))
   allow_create = 0;
  if (info->nlh->nlmsg_flags & NLM_F_REPLACE)
   replace_required = 1;
 }
 if (!allow_create && !replace_required)
  pr_warn("RTM_NEWROUTE with no NLM_F_CREATE or NLM_F_REPLACE\n");
 fn = fib6_add_1(root, &rt->rt6i_dst.addr, sizeof(struct in6_addr),
   rt->rt6i_dst.plen, offsetof(struct rt6_info, rt6i_dst),
   allow_create, replace_required);
 if (IS_ERR(fn)) {
  err = PTR_ERR(fn);
  fn = NULL;
  goto out;
 }
 pn = fn;
/*
将路由信息rt挂载到上面查找到的结点fn上*
/
 err = fib6_add_rt2node(fn, rt, info);
 if (!err) {
  fib6_start_gc(info->nl_net, rt);
  if (!(rt->rt6i_flags & RTF_CACHE))
   fib6_prune_clones(info->nl_net, pn, rt);
 }
out:
 if (err) {
  dst_free(&rt->dst);
 }
 return err;
}

5 路由表信息

以下根据一个路由表实例,介绍每条路由是什么情况下添加的。
上文介绍的路由添加时机:
1)主动添加,比如用户通过命令配置路由的过程,就是主动添加。
2)配置IP时,系统会默认添加几条路由信息。
3)接收和发送报文的过程,有可能会添加路由。

4) 通过/proc/net/ipv6_route,可以了解系统的路由信息,前缀路由也在其中,下面对ipv6_route中的路由信息进行逐条介绍:
为一个设备添加ip时,内核一般会自动新添加几个路由表项,如下:

注释:为便于理解,文中打印出以下几个地址:
1)主路由表的根结点地址:fn=tb6_root=0x8078194c
2)每个路由结点可能包含几个路由信息,其中rt=leaf表示路由结点上第一个路由信息。
其中根结点上的路由信息地址:rt=tb6_root.leaf=0x8077f880,表示网络设备lo的默认路由信息。如果路由查找时,找到了该信息,说明路由表中不存在匹配路由。
3)添加路由信息__ip6_ins_rt之前,需要找到合适的路由结点。

# cat /proc/net/ipv6_route
/*
添加1:为eth0配置ip时,会添加回环路由,目的网络:fe80::234:39ff:fea3:3934/128
设置过程:addrconf_dad_timer-> ipv6_ifa_notify->ip6_ins_rt->__ip6_ins_rt
*/

rt=80decd80 fe80000000000000023439fffea33934 80 00000000000000000000000000000000 00 00000000000000000000000000000000 00000000 00000001 00000000 80200001    lo
/*
添加2:为eth0配置ip时,会添加前缀路由,目的网络:fe80::0/64
设置过程:inet6_addr_add-> addrconf_prefix_route->ip6_route_add ->__ip6_ins_rt
目的网络:根据ip地址得来,如inet6 addr: fe80::234:39ff:fea3:3934/64
         则目的网络为:fe80::0/64,参考addrconf_prefix_route。
该路由表项是固定添加的,无论网络是否连接,都会添加该项。
*/

rt=80decc80 fe800000000000000000000000000000 40 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001     eth0
/*
添加3:为eth0配置ip时,会添加多播路由,目的网络:ff::0/8
设置过程:inet6_addr_add-> addrconf_add_mroute-> ip6_route_add ->__ip6_ins_rt
多播路由信息:包括目的地址、前缀等实在addrconf_add_mroute中生成的。
目的网络:是多播地址ff::0/8
该路由表项是固定添加的,无论网络是否连接,都会添加该项。
*/

rt=80dece80 ff000000000000000000000000000000 08 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000001 00000000 00000001     eth0

/*
添加4:为eth0配置ip时,会添加组播路由,
注意:数据接收过程和发送过程都可能添加路由表
设置过程:ip6_rcv_finish-> ip6_route_input-> fib6_rule_lookup-> ip6_pol_route_input-> ip6_pol_route->__ip6_ins_rt
 当收到的skb_dst(skb)为空时,即设备收到的地址还没有目的地。需要查找路由表确定接收到的报文是发给设备本身的,还是需要转发出去的。如果路由表中没有该项,需要添加路由表。

目的网络:ff02::1/128同一连接上所有结点。IN6ADDR_LINKLOCAL_ALLNODES_INIT
是根据接收报文ip头部目的地址得来的,如:接收报文的目的地址为ff02::1/128
参考ip6_route_input
该路由表项是非固定添加的,需要收到ip报文,会消失。
*/

rt=80dec980 ff020000000000000000000000000001 80 00000000000000000000000000000000 00 00000000000000000000000000000000 00000000 00000000 00000002 01000001     eth0
/*
添加5: ping fe80::c0b0:799e:118f:d0c9时,会添加ping目的路由,目的网络:
ff0200000000000000000001ffa33934/128
设置过程:ip6_rcv_finish-> ip6_route_input-> fib6_rule_lookup-> ip6_pol_route_input-> ip6_pol_route -> ip6_ins_rt –>__ip6_ins_rt
目的网络:ff0200000000000000000001ffa33934/128
该路由表项是ping发起时的,过段时间会消失。
*/

rt=84386680 ff0200000000000000000001ffa33934 80 00000000000000000000000000000000 00 00000000000000000000000000000000 00000000 00000000 00000001 01000001     eth0
/*
添加5: ping fe80::c0b0:799e:118f:d0c9时,会添加ping目的路由,目的网络:
fe80::c0b0:799e:118f:d0c9/128:表示目标网络为ping目的地址。
设置过程:rawv6_sendmsg-> ip6_dst_lookup_flow->ip6_pol_route_output->ip6_pol_route->
ip6_ins_rt(先进行路由查找,未找到再插入路由表)
该路由表项是ping发起时的,过段时间会消失。
*/

rt=84386980 fe80000000000000c0b0799e118fd0c9 80 00000000000000000000000000000000 00 00000000000000000000000000000000 00000000 00000000 00000004 01000001     eth0
/*
未加载驱动就存在该项,目的网络为:0::0/0
主路由表的根结点路由,即是net->ipv6.ip6_null_entry
*/

rt=8077f880 00000000000000000000000000000000 00 00000000000000000000000000000000 00 00000000000000000000000000000000 ffffffff 00000001 00000001 00200200       lo

【路由查找】

对于协议栈来说,软件上一定要查找到路由,才会转发包,否则会终止发送流程。
1) 如果目的地址和设备不在同一子网,那么很容易理解,要有路由网关地址才能转发。
2) 如果目的地址和设备在同一子网,软件上也一定要找到路由表项,否则会终止发送流程,
而此时这个表项一般为配置ip时,默认添加的前缀路由表项。此时不关心网关地址,只要找到表项即可

3) 路由查找代码分析:

正常添加路由命令是通过ip6_pol_route_lookup->fib6_lookup->fib6_looup_1查表;

其他方式查找路由:ip6_pol_route->fib6_lookup->fib6_looup_1  —通过该函数查表.

以ip6_pol_route()为例介绍:

/*
1 路由添加时可能走到该分支:ip6_pol_route_input->
2 路由查找时可能走改分支 :ip6_pol_route_input->
*/
static struct rt6_info *ip6_pol_route(struct net *net, struct fib6_table *table, int oif,
          struct flowi6 *fl6, int flags)
{
 struct fib6_node *fn;
 struct rt6_info *rt, *nrt;
 int strict = 0;
 int attempts = 3;
 int err;
 int reachable = net->ipv6.devconf_all->forwarding ? 0 : RT6_LOOKUP_F_REACHABLE;
 strict |= flags & RT6_LOOKUP_F_IFACE;
        printk("\n\nip6_pol_route\n");
relookup:
 read_lock_bh(&table->tb6_lock);
restart_2:
 fn = fib6_lookup(&table->tb6_root, &fl6->daddr, &fl6->saddr);
restart:
 rt = rt6_select(fn, oif, strict | reachable);

 if (rt->rt6i_nsiblings && oif == 0)
  rt = rt6_multipath_select(rt, fl6);
 BACKTRACK(net, &fl6->saddr);
 if (rt == net->ipv6.ip6_null_entry ||
     rt->rt6i_flags & RTF_CACHE){
            //printk("goto out\n");
            goto out;
       }
 dst_hold(&rt->dst);
 read_unlock_bh(&table->tb6_lock);
//一般不是通过命令配置进去的路由走if分支
 if (!(rt->rt6i_flags & (RTF_NONEXTHOP | RTF_GATEWAY)))
  nrt = rt6_alloc_cow(rt, &fl6->daddr, &fl6->saddr);
 else if (!(rt->dst.flags & DST_HOST))
  nrt = rt6_alloc_clone(rt, &fl6->daddr);
 else{
                //printk("goto1 out2\n");
  goto out2;
         }
 ip6_rt_put(rt);
 rt = nrt ? : net->ipv6.ip6_null_entry;
 dst_hold(&rt->dst);
 if (nrt) {
                //printk("ip6_pol_route->ip6_ins_rt\n");
  err = ip6_ins_rt(nrt);
  if (!err)
   goto out2;
 }
 if (--attempts <= 0){
                printk("goto out2\n");
        goto out2;
       }
 /*
  * Race condition! In the gap, when table->tb6_lock was
  * released someone could insert this route.  Relookup.
  */
 ip6_rt_put(rt);
        printk("goto relookup\n");
        goto relookup;
       
out:
 if (reachable) {
  reachable = 0;
                //printk("goto restart_2\n");
  goto restart_2;
 }
 dst_hold(&rt->dst);
 read_unlock_bh(&table->tb6_lock);
out2:
 rt->dst.lastuse = jiffies;
 rt->dst.__use++;
 return rt;
}
i p6_pol_route->fib6_lookup->fib6_looup_1() 
static struct fib6_node * fib6_lookup_1(struct fib6_node *root,
     struct lookup_args *args)
{
 struct fib6_node *fn;
 __be32 dir;
 if (unlikely(args->offset == 0)){
        printk("fib6_lookup_1 offset=0\n");
        return NULL;
    }
 /*
  * Descend on a tree
  */
 fn = root;
/* 找到前缀长度最大的路由结点 */
 for (;;) {
  struct fib6_node *next;
  dir = addr_bit_set(args->addr, fn->fn_bit);
  next = dir ? fn->right : fn->left;
  if (next) {
   fn = next;
   continue;
  }
  break;
 }
       // printk("\nfib6_lookup_1:fn=0x%x;root=0x%x\n",fn,root);
 while (fn) {
//23093--查表时打印
#if 0    
                printk("fn=0x%x,fn_flags=0x%x\n",
                                    fn,fn->fn_flags);        
#endif
  if (FIB6_SUBTREE(fn) || fn->fn_flags & RTN_RTINFO) {
   struct rt6key *key;
   key = (struct rt6key *) ((u8 *) fn->leaf +
       args->offset);
//23093 查表时打印
#if 0
                        printk("key->plen=%d,fn_flags=0x%x\n",
                                    key->plen,fn->fn_flags);
                   
                        dump_ipv6addr(key->addr);
                        dump_ipv6addr(*args->addr);
                        printk("dumpend\n");
 #endif    
 /*
打印查表过程 key->addr为路由表中缓存的目的网络的地址
key->plen为目的网络的前缀长度。
args->addr为目的地址,
1 在添加路由中进行查表的情况下,
这个目的地址是网关地址。
2 在发送报文查表过程中它是报文的目的地址
此处的实际意义就是找到目的地址在哪个目的网络中。
目的网络的信息保存在fn->leaf中,网关信息保存在以fn为结点的
rt_info中。所以此处可以找到fn
*/
   if (ipv6_prefix_equal(&key->addr, args->addr, key->plen)) {
#ifdef CONFIG_IPV6_SUBTREES
    if (fn->subtree) {
     struct fib6_node *sfn;
     sfn = fib6_lookup_1(fn->subtree,
           args + 1);
     if (!sfn)
      goto backtrack;
     fn = sfn;
    }
#endif
    if (fn->fn_flags & RTN_RTINFO)
     return fn;
   }
  }
#ifdef CONFIG_IPV6_SUBTREES
backtrack:
#endif
  if (fn->fn_flags & RTN_ROOT)
   break;
  fn = fn->parent;
 }
 return NULL;
}

1 通过命令添加路由,需要先查表:

ip6_route_add -rt6_lookup-> ip6_pol_route_lookup()->fib6_lookup().
注意:ip6_pol_route_lookup()和ip6_pol_route虽然都调用fib6_lookup(),但实现也有不同.

添加路由时的查表过程是以网关作为目的地址,而不需要目的地址的前缀长度,查找过程就是要找到目的地址在哪个路由结点(fn)上所保存的目的网络

(fn->leaf)  中。要注意此处目的地址和目的网络的区别。
而其他方式如发包过程中的路由查找,是以报文的目的为目的地址的。查找过程实际上就是找到,目的地址在哪个目的网络中。可参考fib6_lookup_1。

举例:
设备IP   ::192.168.1.108/0   -eth0  其对应的dev-> ifindex =3.
Pc:   ::192.168.1.101
状态:设备与pc直连。
行为:ip -6 route add ::192.168.1.101/0 via ::192.168.1.1 dev eth0
通过ip6_pol_route_lookup查表:
如果前缀为0,则以::192.168.1.1为目的地址进行路由查找,最后可能会匹配到根结点(这是因为根结点上保存的的目的网络(fn->leaf)为0::0/0,因为前缀长度为0,所以认为目的地址在此目的网络中,即ipv6_prefix_equal(0::0, ::192.168.1.1,0) 为真)。根结点对于网络设备lo,lo-> ifindex =1.

ip6_pol_route_lookup->rt6_device_match中会对网络设备进行匹配。
因为我们要查找的是eth0->ifindex=3(eth0)的路由,此处匹配到了lo,所以路由查找失败,进而设置路由也失败了。

注意:一般来说,我们通过命令设置的路由,目的网络和网关ip地址都不在同一子网。
若路由查找到了lo设备的路由信息,则说明路由表中没有能到达目的地址(即网关地址)的路由信息,即设备和网关都是不通的,这显然是不行的。通常情况下,至少存在前缀路由 表项。若路由查找到了前缀路由表项,则说明目的地址与设备在同一子网,前缀为0除外。

2 数据发送过程,先查表,未找到后可能添加路由表。
2.1 数据发送过程,会先查找路由表,确认目的地址,是否在目标网络里,如果没有查找到路由,即查表结果是找到了根结点(前缀为0的路由结点)上lo设备的路由信息。则发送过程失败。

注意1:其实如果查找失败,还要根据rt6i_flags判断是否需要添加路由信息

注意2:系统周期性的向目的地址发包,此时会把目的地址作为目标网络添加到路由表,

因为目的地址是一个,所以目标网络中只有一个地址,所以目标网络的前缀是128,而我们发送报文时,需要查找路由表,此时是从前缀为128的路由结点开始查询的,所以加快了查表过程。

注意3 以目的地址作为目标网络,即前缀为128的路由结点,是RTF_CACHE的
# cat /proc/sys/net/ipv6/route/gc_interval
30 (second)
30s内未使用,则刷新掉,再次发包,需要重新添加该路由表项。
如果30s内有使用,则不需要重新添加了,查找过程就会匹配到对应项。

2.2 路由查找过程:ip6_pol_route->fib6_lookup->fib6_looup_1  —通过该函数查表,

正常添加路由命令是通过ip6_pol_route_lookup->fib6_lookup->fib6_looup_1查表

路由添加:rawv6_sendmsg ->ip6_pol_route->__ip6_ins_rt ->

fib6_lookup_1()查找过程添加打印后的结果:

fib6_lookup_1:fn=0x843864c0;root=0x8078194c
路由查找,首先从plen=128开始匹配,根结点fn= 0x8077f880:
fn=0x843864c0,fn_flags=0x4—正在查找的路由结点,一个节点可能有几个路由信息。
key->plen=128,fn_flags=0x4
2ff:00:00:00:00:00:100:200 –路由表中的目标网络。
2ff:00:00:00:00:00:00:100—要查找的目标网络
Dumpend—未匹配
继续从plen=8开始匹配:
fn=0x843863e0,fn_flags=0x4
key->plen=8,fn_flags=0x4
ff:00:00:00:00:00:00:00
2ff:00:00:00:00:00:00:100
Dumpend—匹配上 fn!=根结点的fn。如果最后找到了根结点的根路由信息,说明目的地址,不在路由表的目标网络中。
调用栈:
 [<80010584>] (show_stack+0x10/0x14) from [<802bec50>] (__ip6_ins_rt+0x10/0x70)
[<802bec50>] (__ip6_ins_rt+0x10/0x70) from [<802bf070>] (ip6_ins_rt+0x2c/0x38)
[<802bf070>] (ip6_ins_rt+0x2c/0x38) from [<802bf450>] (ip6_pol_route+0x3d4/0x47c)
[<802bf450>] (ip6_pol_route+0x3d4/0x47c) from [<802bf528>] 
[<802bf528>] (ip6_pol_route_output+0x14/0x1c) from [<802b336c>] [<802b336c>] 
[<802b3468>] (ip6_dst_lookup_flow+0x2c/0x7c) from [<802ca838>] 
[<802ca838>] (rawv6_sendmsg+0x49c/0xa3c) from [<802361d0>] 
[<80238398>] (__sys_sendmsg+0x4c/0x70) from [<8000da40>] (ret_fast_syscall+0x0/0x30)
3 数据接收过程,先查表,为找到后可能添加路由表。

3.1 当收到的skb_dst(skb)为空时,即设备收到的地址还没有目的地。需要查找路由表确定接收到的报文是发给设备本身的,还是需要转发出去的。如果路由表中没有该项,需要添加路由表。

3.2 ip6_pol_route—通过该函数查表,
正常添加路由命令是通过ip6_pol_route_lookup查表
路由查找,从plen=128开始匹配(fib6_lookup_1()过程添加打印):

fib6_lookup_1:fn=0x84385220;root=0x8078194c
fn=0x84385220,fn_flags=0x4
key->plen=128,fn_flags=0x4
2ff:00:00:00:00:100:8fff:c9d0
2ff:00:00:00:00:00:00:100
Dumpend-未找到
路由查找,从plen=8开始匹配:
fn=0x843853e0,fn_flags=0x4
key->plen=8,fn_flags=0x4
ff:00:00:00:00:00:00:00
2ff:00:00:00:00:00:00:100
Dumpend—找到了。
rt=0x84386080;rt6i_nsiblings=0;oif=3;rt6i_flags=0x1 
CPU: 0 PID: 3 Comm: ksoftirqd/0 Tainted: G           O 3.10.50 #88
[<80012660>] (unwind_backtrace+0x0/0xdc) from [<80010584>] (show_stack+0x10/0x14)
[<80010584>] (show_stack+0x10/0x14) from [<802bec50>] (__ip6_ins_rt+0x10/0x70)
[<802bec50>] (__ip6_ins_rt+0x10/0x70) from [<802bf070>] (ip6_ins_rt+0x2c/0x38)
[<802bf070>] (ip6_ins_rt+0x2c/0x38) from [<802bf450>] (ip6_pol_route+0x3d4/0x47c)
[<802bf450>] (ip6_pol_route+0x3d4/0x47c) from [<802bf50c>] 
[<802bf50c>] (ip6_pol_route_input+0x14/0x1c) from [<802bf5b4>] 
[<802bf5b4>] (ip6_route_input+0x84/0xa8) from [<802b5e2c>] (ip6_rcv_finish+0x6c/0x94)
[<802b5e2c>] (ip6_rcv_finish+0x6c/0x94) from [<80247118>] 
[<80247118>] (__netif_receive_skb_core+0x4ac/0x5a4) from [<7f00089c>] 
[<7f00089c>] (ambeth_napi+0x21c/0x9d8 [ambarella_eth]) from [<80247e00>]
3.3 路由查找一般发生在什么情况下?举例:

设备IP = ::192.168.1.108/0   -eth0  其对应的dev-> ifindex =3.

Pc:   ::192.168.1.101
状态:设备与pc直连。
行为:ping ::192.168.1.101 不通
    当 IP =::192.168.1.108/1 时能够ping通。

数据包发送过程,需要查找下一跳地址。
如原始socket的发送过程中,或者ip头部封装时,核心函数:
注意:为eth0配置ip时,会添加组播路由,此时是通过ip6_pol_route
添加路由的。
ip6_dst_lookup_flow->ip6_route_output->fib6_rule_lookup->ip6_pol_route_output()->ip6_pol_route ->fib6_lookup-> fib6_lookup_1
fib6_lookup_1函数实际是根据目标地址查找对应的路由结点struct fib6_node。

查找规则可以参考fib6_lookup_1函数实现,以下简单陈述:

1) 从前缀长度等于128的结点开始查找fn->fn_bit=128。遍历每个结点上的leaf 即rt_info.
查找1:fn->fn_bit==128的路由节点。
查找2:fn->fn_bit==plen的路由节点(为网络设备配置ip时,默认添加前缀路由,所以存在这样的节点fn->fn_bit==plen)。
查找3:fn->fn_bit=0的路由结点(即根结点)。
根结点上一定包含设备lo的默认路由信息。
Eth0默认路由也在根结点上(ip -6 route add ::/0 via ::192.168.1.1 dev eth0)。
当eth0的ip地址的前缀为0时,它的前缀路由信息也挂载根结点上。   
2) 根结点的根leaf 为网络设备lo对应的路由信息,其对应的dev-> ifindex =1.
rt=8077f880 dst=00000000000000000000000000000000 plen=00 00000000000000000000000000000000 00 00000000000000000000000000000000 ffffffff 00000001 00000001 00200200       lo
3) 匹配目的地址::192.168.1.101与目的网络的地址在目的网络的前缀长度下是否相等
ipv6_prefix_equal(&key->addr, args->addr, key->plen)
易见,如果key->plen=0,那么任何目的网络都能匹配上。
4)如果设备IP: ::192.168.1.108/plen ,plen=0,且fn->fn_bit>0的所有fn结点都不能和目的地址匹配上,此时查找路由的结果是:查找到了根结点。而lo->ifinex !=eth0->ifindex
路由查找失败,发送流程终止。
5)所以,此时网络不通。如果
设备IP: ::192.168.1.108/plen  (plen!=0)
路由查找时,会先匹配到plen!=0的前缀路由结点,而不会匹配到根结点
此时:eth-0->ifinex ==eth0->ifindex
发送流程不会终止。

【原因分析】

一. 回顾:问题复现

本文以一个实际问题为切入点进行分析,复现方法如下:

1 设备ip为::192.168.1.108/0时

#ip -6 addr add ::192.168.1.108/0 dev eth0
1>此时路由表中增加前缀路由:

#cat /proc/net/ipv6_route

rt=84380c80 00000000000000000000000000000000 00 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001     eth0

2>此时路由表的根结点上(前缀=0),挂了两个路由项:

 # cat /proc/net/ipv6_route 

rt=84386980 00000000000000000000000000000000 00 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001      eth0   –配置ip时,添加的前缀路由。
rt=8077f880 00000000000000000000000000000000 00 00000000000000000000000000000000 00 00000000000000000000000000000000 ffffffff 00000001 00000002 00200200       lo   –根结点的根路由,属于lo设备。

#ping ::192.168.1.101 (直连pc的ip) –发包过程查到前缀路由表项,所以能通。

# ip route del dev eth0  --删除默认路由后造成如下两个问题:
1)上面路由表中前缀路由消失,设备发包过程查表查到了根结点,发送流程终止,所以ping不同。
2) 删除默认路由后,设备无法再通过命令设置任何路由。
只能通过如下操作,才能重新配置路由: 
#ip addr del ::192.168.1.108/0 dev eth0
#ip addr add ::192.168.1.108/0 dev eth0

此时,设置ip成功添加了前缀路由,而通过命令配置路由无法成功。
因为这两种方式,设置路由时,使用的的flag不同,
#define RTF_UP  0x0001  /* route usable     */
#define RTF_GATEWAY 0x0002  /* destination is a gateway */
前缀路由使用的flag=1;命令设置路由使用flag=3
当flag=3时,设置路由过程中,ip6_route_add->rt6_lookup会查找路由表,查表时要求路由表中必须存在路由表项。即设置的网关地址,必须在路由表项的目的网络中。

2 设备ip为::192.168.1.101/1时:
#ip -6 addr add ::192.168.1.108/1 dev eth0
此时路由表中增加项为前缀路由:

#cat /proc/net/ipv6_route

rt=84388b80 00000000000000000000000000000000 01 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001     eth0

#ping ::192.168.1.101 (直连pc的ip) –发包过程查到该表项,所以能通。

# ip route del dev eth0    --删除默认路由
删除路由后能ping通。此处与ip为::192.168.1.108/0时明显不同。
(此处若故意删除该指定路由结点,ip  -6 route del ::/1 via :: dev eth0,则同样ping不通).

二 原因分析

问题一

当ip=::192.168.1.108/0时(pc=::192.168.1.101/1),如果删除默认路由,网络一定不通:
1) 这是因为当设备ipv6地址前缀为0时,就算用户还没设置默认路由,此时用户删除默认路由也会成功,原因是删除默认路由时,会把前缀路由当成默认路由删除掉,因为此时前缀路由的目的网络前缀长度为0,刚好和默认路由(目的网络前缀为0)在同一个路由节点。同时也因为前缀为0 任何目的地址在0范围内和目的网络的地址都是相等的。
删除路由时会根据目的地址(网关地址)找到目的网络(目的网络的地址和前缀长度)。前缀路由与目的路由的目的网络前缀长度都为0,所以都能与目的地址匹配,所以当没有默认路由时,会删除前缀路由,默认路由与前缀路由都存在时,会删除一个(删除哪个没有细查)

如上文分析的:再想设置路由或者发送报文,都要先查找路由表,因为没有前缀路由,最后很可能会匹配到根结点(因为如果前缀路由存在,就算不能找到合适的路由,最后也会匹配到前缀路由表项,而不会结束流程) 的lo设备路由信息,查表失败,发送终止。

2) 查找路由时(添加路由表,发送报文都需要查找路由),从前缀长度=128的路由结点开始;删除路由表项时,从前缀长度=0的路由结点开始。

当ip=::192.168.1.108/1时,根结点上只有lo设备的路由信息,所以去删除eth0上的默认路由时不会删除前缀路由。

(注:前缀路由,挂在了fn->fn_bit=1的路由结点上,而不在根结点上。
rt=84388b80 00000000000000000000000000000000 01 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001     eth0)

问题二
ip为::192.168.1.108/1,删除默认路由后,为什么能ping通?
因为ip=::192.168.1.108/1,删除默认路由时不会删除前缀路由,因为前缀路由在fn_bit=1的路由结点上而默认路由在fn_bit=0的路由结点上(此时删除默认路由会失败,因为还不存在默认路由),路由表中还存在前缀路由信息表项(即上述路由表项rt=84388b80  eth0)。所以发包过程,还能找到该项(从前缀长度为128的路由表项开始查找),不会找到根结点上的lo设备路由信息,发送流程不会终止。

问题三
为什么ip=::192.168.1.108/1时,删除默认路由失败?而ip=::192.168.1.108/0时,删除默认路由成功?
     这和路由表的删除操作有关:
删除过程:

static int ip6_route_del(struct fib6_config *cfg)
{
 /* 根据要删除的目标网络(地址及前缀),找到合适的路由节点 */
 fn = fib6_locate(&table->tb6_root,
    &cfg->fc_dst, cfg->fc_dst_len,
    &cfg->fc_src, cfg->fc_src_len);
//打印指定路由结点fib6_node上的所有路由信息
         dump_ipv6rt(fn);
}
fib6_locate调用->
static struct fib6_node * fib6_locate_1(struct fib6_node *root,
     const struct in6_addr *addr,
     int plen, int offset)
{
 struct fib6_node *fn;
 //从根结点开始查fn=table->tb6_root,此时对应lo设备,fn->fn_bit=0
 for (fn = root; fn ; ) {
  struct rt6key *key = (struct rt6key *)((u8 *)fn->leaf + offset);
 
  if (plen < fn->fn_bit ||!ipv6_prefix_equal(&key->addr, addr, fn->fn_bit))
   return NULL;
//易见,如果目标网络的前缀plen=0,则返回根结点(fn_bit=0)
  if (plen == fn->fn_bit)
   return fn;
  if (addr_bit_set(addr, fn->fn_bit))
   fn = fn->right;
  else
   fn = fn->left;
 }
 return NULL;
}
了解了路由表项的删除过程,回头看本问题,就比较清晰了。

1)无论ip=::192.168.1.108/1或ip=::192.168.1.108/0,默认路由对应的目标网络都是0::0/0,因为目标网络的前缀为0,所以删除过程fib6_locate_1会查找到根结点。
2)无论ip=::192.168.1.108/1或ip=::192.168.1.108/0,根结点上一定包含,lo设备的路由信息: 根结点可以理解为fn_bit=0对应的路由结点。
rt=8077f880 00000000000000000000000000000000 00 00000000000000000000000000000000 00 00000000000000000000000000000000 ffffffff 00000001 00000002 00200200       lo –根结点的根路由,属于lo设备。
3) 区别在于当ip=::192.168.1.108/0时,根结点fn上,还包括ip地址的前缀路由
rt=84386980 00000000000000000000000000000000 00 00000000000000000000000000000000 00 00000000000000000000000000000000 00000100 00000000 00000000 00000001      eth0 –配置ip时,添加的前缀路由。

此时去删除默认路由,错把前缀路由当成默认路由删掉了,而以后查找路由表的过程中需要存在和目标网络相匹配的路由结点,否则rt路由信息没有路由结点可以挂载。
又因为设置ip地址时,会默认添加前缀路由,所以一定存在和目标网络相匹配的路由结点,不过一旦前缀路由被删除,就可能不存在这样的结点,因此有可能网络不通)

【总结】
1) 为设备配置ip=::192.168.1.101/0 或::192.168.1.101/1时
系统会生成前缀路由表项。
2) 配置路由过程,是先删除路由,再设置路由。
3) 首先观察删除路由:
Ip为::192.168.1.101/0时,路由能删除成功,且把前缀路由删除掉了。
为::192.168.1.101/1时, 不能删除成功。

4) 接着观察设置路由。
Ip为::192.168.1.101/0时,路由不能设置成功。
原因:设置路由时,要以网关地址作为目的地址,去查找路由表(参考上面介绍的),因为前缀路由被删除,所以直接找到了根结点,根结点是网络设备lo的路由结点,设置失败。
Ip为::192.168.1.101/1时,路由能设置成功。
原因:前缀路由没有删除成功,所以不会找到根结点。


猜你喜欢

转载自blog.csdn.net/eleven_xiy/article/details/72777931