文章目录
用户空间程序如ip和netstat等工具都是通过Netlink接口实现对内核空间中路由表的增删改查,route是通过ioctl()系统调用与内核交互的,这篇笔记介绍了较新的Netlink路由配置接口实现。
涉及的文件有:
源代码路径 | 说明 |
---|---|
include/net/ip_fib.h | IPv4路由数据库头文件 |
core/ipv4/fib_frontend.c | IPv4路由数据库对外接口实现文件 |
命令行
ip route命令详细的介绍可以参考手册ip(8),下面是最常用的命令行参数。
ip route { add | del | change | append | replace } ROUTE
ROUTE := NODE_SPEC [ INFO_SPEC ]
NODE_SPEC := [ TYPE ] PREFIX [ tos TOS ] [ table TABLE_ID ] [ proto RTPROTO ] [ scope SCOPE ]
INFO_SPEC := { NH | nhid ID } OPTIONS FLAGS [ nexthop NH ] ...
NH := [ via [ FAMILY ] ADDRESS ] [ dev STRING ] [ weight NUMBER ] NHFLAGS
可以将命令行中路由项的NODE_SPEC和INFO_SPEC分别与内核路由项中的fib_node、fib_info结构对应。
路由项TYPE:可取的值有unicast、unreachable、blackhole、prohibit、local、broadcast、multicast,对应下面rtmsg->type字段。默认的type为unicast;
PREFIX:路由项目的地址前缀,可以附加’/'指定子网掩码,如192.168.1.0/24,如果不指定子网掩码,那么默认长度为32,这样该路由项表示是一个主机路由;
SCOPE: 路由项的作用域,有link、host、global、数字几种,含义对应下面的rtmsg->scope;
via [ FAMILY ] ADDRESS: 下一跳路由的IP地址;
数据结构
路由项配置信息是作为标准的Netlink消息的数据部分传递到内核的,配置内容主要是rtmsg。
路由消息: rtmsg
struct rtmsg
{
unsigned char rtm_family; // 路由项所属协议族,如AF_INET、AF_INET6;
// 路由项目的地址掩码长度,特别注意是掩码长度,和属性中的RTA_DST配合表示路由项的目的网络
unsigned char rtm_dst_len;
unsigned char rtm_src_len; // 源地址掩码长度
unsigned char rtm_tos; // 路由项的服务质量,对应IP首部的服务字段
// 路由项所述路由表ID,由于只有1个字节,所以取值最大为255,因此用户态能够配置256个路由表,
// 但是内核实际上并不是只能保存256个路由表,内核对此是没有限制的;
unsigned char rtm_table;
unsigned char rtm_protocol; // 路由协议,含义见下方
unsigned char rtm_scope; // 路由项的作用域,含义见下方
unsigned char rtm_type; // 路由项类型,含义见下方
unsigned rtm_flags;
};
对于路由表ID,内核有如下规划,从表ID的定义可以看出,当支持策略路由时(多路由表),用户态程序能够新建的表的ID范围为1~252。
路由表ID | 值 | 说明 |
---|---|---|
RT_TABLE_UNSPEC | 0 | 未知,当操作该路由表时,内核默认会操作main表 |
RT_TABLE_DEFAULT | 253 | default表,用于策略路由的默认路由表 |
RT_TABLE_MAIN | 254 | main表,主路由表,不指定具体要操作的表时就是main表 |
RT_TABLE_LOCAL | 255 | local表,该表维护了本机IP地址相关的路由,由内核自己维护,用户态无法操作 |
对于路由协议,表示的是路由项是如何产生的,可取的值如下,其中常见的是RTPROT_KERNEL和RTPROT_STAIC。
路由协议 | 说明 |
---|---|
RTPROT_UNSPEC | 未知 |
RTPROT_REDIRECT | 该路由项是通过ICMP重定向消息添加的 |
RTPROT_KERNEL | 该路由项是由内核自己添加的,local表的路由大多数属于这一类 |
RTPROT_BOOTUP | 该路由项是在启动时通过boot协议添加的 |
RTPROT_STAIC | 该路由项是由管理员静态添加的,用户态配置的路由属于这一类 |
对于路由项的作用域,表示的是本机距离该路由项表示的目的地址的距离。如下,值(0, 200)之间的数字是可以由管理员自己配置的。值越大,距离本机越近。所以叫它作用域其实是不利于理解的。
路由作用域 | 值 | 说明 |
---|---|---|
RT_SCOPE_UNIVERSE | 0 | 表示目的地和本机距离超过了1跳 |
RT_SCOPE_SITE | 200 | |
RT_SCOPE_LINK | 253 | 表示目的地位于和本机直接相连的网络,如同一局域网内的其它主机地址 |
RT_SCOPE_HOST | 254 | 表示目的地是本地地址 |
RT_SCOPE_NOWHERE | 255 | 表示目的地不存在,这是个保留值 |
对于路由表项的类型,它一定程度上决定了数据包匹配该路由表项后应该采取的动作。
路由表项类型 | 说明 |
---|---|
RTN_UNSPEC | 一个未初始化的值 |
RTN_LOCAL | 该路由项的目的地址是主机本地地址 |
RTN_UNICAST | 该路由是一条到直连或者非直连目的地址的单播路由,添加路由时如果不指定路由类型,那么默认取该值 |
RTN_MULTICAST | 该该路由项的目的地址是一个多播地址 |
RTN_BROADCAST | 该该路由项的目的地址是一个广播地址 |
RTN_ANYCAST | IPv4中不使用 |
RTN_UNREACHABLE | 路由结果是不可达,会给源地址回复主机不可达ICMP报文 |
RTN_BLACKHOLE | 路由结果是不可达,会悄悄的丢弃数据包,不做任何回应 |
RTN_PROHIBIT | 路由结果为不可达,会给源地址回复"communication administratively prohibited"类型的ICMP报文 |
RTN_THROW | 路由结果为不可达,会给源地址回复网络不可达ICMP报文 |
- rtm_flags: 路由标记,
Netlink消息属性
netlink消息中,可以配置如下属性,某些属性和rtmsg中的字段是有重复的,当重复时,以属性为准,不过用户态配置程序也不应该为二者指定不一样的值。
属性 | 说明 |
---|---|
RTA_UNSPEC | 忽略,无意义的属性 |
RTA_DST | 该路由项的目的地址 |
RTA_SRC | 该路由项的源地址 |
RTA_IIF | 该该路由项的输入网络设备索引 |
RTA_OIF | 该该路由项的输出网络设备索引 |
RTA_GATEWAY | 该该路由项的网关地址 |
RTA_PRIORITY | 该该路由项的优先级 |
RTA_PREFSRC | 该该路由项的首选源地址 |
RTA_METRICS | 该该路由项和协议有关的度量值,如RTT等 |
RTA_MULTPATH | 多路径路由下有意义,即该路由项的下一跳地址 |
RTA_PROTOINFO | 基于策略路由的防火墙信息 |
RTA_FLOW | 基于策略路由的分类标签 |
RTA_CACHEINFO | 缓存的路由信息 |
路由项配置: fib_config
在内核中,Netlink消息携带的路由配置信息最后是转换为fib_config对象后向路由数据库配置的,该结构的内容基本上就是上述内容的直接赋值。
struct fib_config {
u8 fc_dst_len;
u8 fc_tos;
u8 fc_protocol;
u8 fc_scope;
u8 fc_type;
/* 3 bytes unused */
u32 fc_table; //路由表ID
__be32 fc_dst;
__be32 fc_gw;
int fc_oif;
u32 fc_flags;
u32 fc_priority;
__be32 fc_prefsrc;
struct nlattr *fc_mx; //保存属性配置中的RTA_METRICS
struct rtnexthop *fc_mp;
int fc_mx_len;
int fc_mp_len;
u32 fc_flow;
u32 fc_nlflags; // Netlink消息标准首部中的flag
// 调用者信息,如pid等
struct nl_info fc_nlinfo;
};
Netlink消息到fib_config转换: rtm_to_fib_config()
static int rtm_to_fib_config(struct net *net, struct sk_buff *skb,
struct nlmsghdr *nlh, struct fib_config *cfg)
{
struct nlattr *attr;
int err, remaining;
struct rtmsg *rtm;
// 校验参数的合法性,如大小、对齐方式等
err = nlmsg_validate(nlh, sizeof(*rtm), RTA_MAX, rtm_ipv4_policy);
if (err < 0)
goto errout;
memset(cfg, 0, sizeof(*cfg));
rtm = nlmsg_data(nlh);
cfg->fc_dst_len = rtm->rtm_dst_len;
cfg->fc_tos = rtm->rtm_tos;
cfg->fc_table = rtm->rtm_table;
cfg->fc_protocol = rtm->rtm_protocol;
cfg->fc_scope = rtm->rtm_scope;
cfg->fc_type = rtm->rtm_type;
cfg->fc_flags = rtm->rtm_flags;
cfg->fc_nlflags = nlh->nlmsg_flags;
cfg->fc_nlinfo.pid = NETLINK_CB(skb).pid;
cfg->fc_nlinfo.nlh = nlh;
cfg->fc_nlinfo.nl_net = net;
if (cfg->fc_type > RTN_MAX) {
err = -EINVAL;
goto errout;
}
// 解析属性,这里要注意的是有些属性和上面的字段是重复的,很显然,当配置了属性时,以属性值为准
nlmsg_for_each_attr(attr, nlh, sizeof(struct rtmsg), remaining) {
switch (nla_type(attr)) {
case RTA_DST:
cfg->fc_dst = nla_get_be32(attr);
break;
case RTA_OIF:
cfg->fc_oif = nla_get_u32(attr);
break;
case RTA_GATEWAY:
cfg->fc_gw = nla_get_be32(attr);
break;
case RTA_PRIORITY:
cfg->fc_priority = nla_get_u32(attr);
break;
case RTA_PREFSRC:
cfg->fc_prefsrc = nla_get_be32(attr);
break;
case RTA_METRICS:
cfg->fc_mx = nla_data(attr);
cfg->fc_mx_len = nla_len(attr);
break;
case RTA_MULTIPATH:
cfg->fc_mp = nla_data(attr);
cfg->fc_mp_len = nla_len(attr);
break;
case RTA_FLOW:
cfg->fc_flow = nla_get_u32(attr);
break;
case RTA_TABLE:
cfg->fc_table = nla_get_u32(attr);
break;
}
}
return 0;
errout:
return err;
}
添加路由项: inet_rtm_newroute()
static int inet_rtm_newroute(struct sk_buff *skb, struct nlmsghdr* nlh, void *arg)
{
struct net *net = skb->sk->sk_net;
struct fib_config cfg;
struct fib_table *tb;
int err;
// 解析路由配置信息到fib_config对象中
err = rtm_to_fib_config(net, skb, nlh, &cfg);
if (err < 0)
goto errout;
// 根据路由表ID,找到对应的路由表fib_table对象,如果还没有对应的路由表,
// 则新建一个路由表,然后返回其指针
tb = fib_new_table(net, cfg.fc_table);
if (tb == NULL) {
err = -ENOBUFS;
goto errout;
}
// 调用路由表的insert()回调,不同的数据结构实现方式不同
err = tb->tb_insert(tb, &cfg);
errout:
return err;
}
删除路由项: inet_rtm_delroute()
实现逻辑和inet_rtm_addroute()基本一致,不再过多赘述。
static int inet_rtm_delroute(struct sk_buff *skb, struct nlmsghdr* nlh, void *arg)
{
struct net *net = skb->sk->sk_net;
struct fib_config cfg;
struct fib_table *tb;
int err;
err = rtm_to_fib_config(net, skb, nlh, &cfg);
if (err < 0)
goto errout;
tb = fib_get_table(net, cfg.fc_table);
if (tb == NULL) {
err = -ESRCH;
goto errout;
}
// 调用路由表的tb_delete()接口
err = tb->tb_delete(tb, &cfg);
errout:
return err;
}
路由项的Dump
当用户空间使用ip route show命令时,会向kernel查询当前路由表中的路由项,在内核中的接口是inet_dump_fib(),该接口由于不会对路由表内容进行修改,这里暂且不分析。