2.4 NETLINK_ROUTE 消息:不只是路由
上一节我们在 Generic Netlink 的机制里打转,把消息头、TLV、验证策略这些东西拆了个底朝天。现在地基打好了,是时候把视线收回到网络子系统本身,看看那些「老资格」的 Netlink 协议家族是怎么工作的。
其中最重量级的,当属 NETLINK_ROUTE。
别被名字骗了——虽然它叫 Route Netlink,但这哥们儿管的远不止路由表那么简单。它是整个网络配置的「总管家」,手里攥着网卡、地址、邻居表、流量控制这一大摊子事儿。
具体来说,NETLINK_ROUTE 的消息家族(Family)按功能划分成了好几支人马:
- LINK:管网络接口的,比如
eth0的启动、关闭、改名。 - ADDR:管 IP 地址的添加和删除。
- ROUTE:这才是真正的路由消息,管 FIB(转发信息库)。
- NEIGH:管邻居子系统,也就是 ARP 和 ND(Neighbor Discovery)表。
- RULE:管策略路由规则的。
- QDISC、TCLASS、ACTION:这一块是 QoS(流量控制)相关的,排队规则、流量分类、动作处理都在这儿。
- NEIGHTBL:邻居表本身的配置。
- ADDRLABEL:地址标签(通常用于 IPv6)。
CRUD 与消息类型的映射
不管是上面哪个家族,它们的消息类型设计都遵循一套非常直观的 CRUD(增删改查)逻辑。
对于大多数对象(比如路由 Route、地址 Addr、邻居 Neigh),都只有三种操作:
- 创建:对应
RTM_NEWXXX消息(如RTM_NEWROUTE)。 - 删除:对应
RTM_DELXXX消息(如RTM_DELROUTE)。 - 查询:对应
RTM_GETXXX消息(如RTM_GETROUTE)。
这就像标准的数据库操作。
但有一个特例,那就是 LINK(网络接口)。网卡的配置有时候比单纯的「增删查」要复杂一点——你可能只想修改某个参数(比如 MTU),而不是把网卡删了再重建。所以,除了上面那三个标配消息,Link 家族多了一个修改专用的消息:RTM_SETLINK。
当操作出错时:nlmsgerr 结构
我们在写用户态程序时,最怕的不是拒绝,而是沉默——你发一个请求过去,内核没反应,你根本不知道是没收到还是处理挂了。
Netlink 协议的设计者显然考虑到了这一点。他们设计了一套标准的报错机制,无论你是通过标准 Netlink 还是 Generic Netlink 通信,这套机制都通用。
这一切都封装在一个 nlmsgerr 结构体里:
struct nlmsgerr {
int error; /* 负数表示标准 errno 错误码,0 表示成功/ACK */
struct nlmsghdr msg; /* 触发错误的原始请求消息头 */
};
你可以把这个结构体想象成一张「退件单」。
当你发送的消息有问题——比如 nlmsg_type 填了一个内核不认识的值——内核不会干瞪眼,它会给你回发一个 Netlink 消息。这个回包的消息头类型是 NLMSG_ERROR,紧接着就是上面的 nlmsgerr 结构体。
这时候会发生一个有趣的操作:
如果 error 字段不为 0(比如是 -EOPNOTSUPP),这张「退件单」的后面还会附上你最初发出去的那条消息的头部(也就是 msg 字段)。
为什么要这么做?
想想看,你在处理多线程或异步通信的时候,可能同时发出了好几个请求。当其中一个错误返回时,你怎么知道是哪个请求炸了?
内核很贴心地把你发的请求头原封不动贴回来,你只需要比对这个包头里的序列号,就能立刻对号入座,找到是哪次操作出错了。
图 2-4 展示了这个报错消息的内存布局:
[图 2-4:Netlink 错误消息布局]
- Netlink Header (type = NLMSG_ERROR)
- Error Code (int) : 比如
-EINVAL或-EOPNOTSUPP- Original Request Header : 原始请求的
nlmsghdr(仅当 error != 0 时存在)- Original Payload : (注:通常代码里只附带 header,不附带整个 payload,除非有特殊调试需求)
ACK 机制的微妙之处
有时候我发请求,不是为了修改配置,就是为了确认「你收到了没」。这时候就需要 ACK(确认应答)机制。
请求 ACK 的方式很简单:在发送消息的 Flag 里加上 NLM_F_ACK。
内核收到后,如果处理没问题,就会回给你一个 NLMSG_ERROR 类型的消息。但注意,这里有个反直觉的设计:
即使是成功(ACK),内核回的消息类型也是 NLMSG_ERROR。
区别在于 nlmsgerr 结构里的 error 字段:
- 如果
error为 0,这就不是一张「退件单」,而是一张「签收单」。 - 这时候,
msg字段(原始请求头)不会被附在回包里——因为成功了,内核认为没必要把垃圾再塞回给你。
如果你想抠细节,可以去翻 net/netlink/af_netlink.c 里的 netlink_ack() 函数,那几行代码把上面的逻辑写得清清楚楚。
走到这一步
我们现在手里有了 NETLINK_ROUTE 的消息类型清单(LINK, ADDR, ROUTE...),脑子里也有了错误处理和 ACK 的模型。
这只是「说明书」。接下来我们要进入实战环节,用这些东西去操作内核里的路由表(FIB)。我们要看看,用一条 RTM_NEWROUTE 消息塞进一条路由,到底需要把哪些 TLV 属性像俄罗斯套娃一样塞进消息体里。