跳到主要内容

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:管策略路由规则的。
  • QDISCTCLASSACTION:这一块是 QoS(流量控制)相关的,排队规则、流量分类、动作处理都在这儿。
  • NEIGHTBL:邻居表本身的配置。
  • ADDRLABEL:地址标签(通常用于 IPv6)。

CRUD 与消息类型的映射

不管是上面哪个家族,它们的消息类型设计都遵循一套非常直观的 CRUD(增删改查)逻辑。

对于大多数对象(比如路由 Route、地址 Addr、邻居 Neigh),都只有三种操作:

  1. 创建:对应 RTM_NEWXXX 消息(如 RTM_NEWROUTE)。
  2. 删除:对应 RTM_DELXXX 消息(如 RTM_DELROUTE)。
  3. 查询:对应 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 字段:

  • 如果 error0,这就不是一张「退件单」,而是一张「签收单」。
  • 这时候,msg 字段(原始请求头)不会被附在回包里——因为成功了,内核认为没必要把垃圾再塞回给你。

如果你想抠细节,可以去翻 net/netlink/af_netlink.c 里的 netlink_ack() 函数,那几行代码把上面的逻辑写得清清楚楚。

走到这一步

我们现在手里有了 NETLINK_ROUTE 的消息类型清单(LINK, ADDR, ROUTE...),脑子里也有了错误处理和 ACK 的模型。

这只是「说明书」。接下来我们要进入实战环节,用这些东西去操作内核里的路由表(FIB)。我们要看看,用一条 RTM_NEWROUTE 消息塞进一条路由,到底需要把哪些 TLV 属性像俄罗斯套娃一样塞进消息体里。