Linux IPv6默认路由设置中RTNH_F_ONLINK不生效的修复问询
问题背景
需要用C代码实现与命令ip -6 route replace default via $GW6 dev $IFACE proto static metric 1024 onlink pref medium完全一致的IPv6默认路由替换效果,当前代码遇到以下问题:
- 设置
nl_request.r.rtm_flags = RTNH_F_ONLINK;时,路由无法更新 - 移除该设置后路由可更新,但缺少
onlink属性
当前代码
#include <string.h> #include <stdlib.h> #include <stdio.h> #include <sys/types.h> #include <unistd.h> #include <net/if.h> #include <arpa/inet.h> #include <sys/ioctl.h> #include <sys/socket.h> #include <linux/rtnetlink.h> /* Open netlink socket */ static int open_netlink(int af_family) { struct sockaddr_nl saddr; int sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (sock < 0) { perror("Failed to open netlink socket"); return -1; } memset(&saddr, 0, sizeof(saddr)); return sock; } /* Helper structure for ip address data and attributes */ typedef struct { uint8_t family; uint8_t bitlen; unsigned char data[sizeof(struct in6_addr)]; } _inet_addr; #define NLMSG_TAIL(nmsg) \ ((struct rtattr *) (((void *) (nmsg)) + NLMSG_ALIGN((nmsg)->nlmsg_len))) /* Add new data to rtattr */ static int rtattr_add(struct nlmsghdr *n, int maxlen, int type, const void *data, int alen) { int len = RTA_LENGTH(alen); struct rtattr *rta; if (NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len) > maxlen) { fprintf(stderr, "rtattr_add error: message exceeded bound of %d\n", maxlen); return -1; } rta = NLMSG_TAIL(n); rta->rta_type = type; rta->rta_len = len; if (alen) { memcpy(RTA_DATA(rta), data, alen); } n->nlmsg_len = NLMSG_ALIGN(n->nlmsg_len) + RTA_ALIGN(len); return 0; } static int do_route(int sock, int cmd, int flags, _inet_addr *dst, _inet_addr *gw, int def_gw, int if_idx) { struct { struct nlmsghdr n; struct rtmsg r; char buf[4096]; } nl_request; /* Initialize request structure */ nl_request.n.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg)); nl_request.n.nlmsg_flags = NLM_F_REQUEST | flags; nl_request.n.nlmsg_type = cmd; nl_request.r.rtm_dst_len = dst->bitlen; nl_request.r.rtm_family = dst->family; nl_request.r.rtm_table = RT_TABLE_MAIN; nl_request.r.rtm_protocol = RTPROT_BOOT; nl_request.r.rtm_type = RTN_UNICAST; /* Set additional flags if NOT deleting route */ if (cmd != RTM_DELROUTE) { /* Select scope, for simplicity we supports here only IPv6 and IPv4 */ if (nl_request.r.rtm_family == AF_INET6) { nl_request.r.rtm_scope = RT_SCOPE_UNIVERSE; nl_request.r.rtm_flags = RTNH_F_ONLINK; } else { nl_request.r.rtm_scope = RT_SCOPE_LINK; } } /* Set gateway */ if (gw->bitlen != 0) { rtattr_add(&nl_request.n, sizeof(nl_request), RTA_GATEWAY, &gw->data, gw->bitlen / 8); nl_request.r.rtm_scope = 0; nl_request.r.rtm_family = gw->family; /* RTA_PREF = ICMPV6_ROUTER_PREF_MEDIUM */ } /* Set destination network */ rtattr_add(&nl_request.n, sizeof(nl_request), /*RTA_NEWDST*/ RTA_DST, &dst->data, dst->bitlen / 8); if (!def_gw) { /* Set interface */ rtattr_add(&nl_request.n, sizeof(nl_request), RTA_OIF, &if_idx, sizeof(int)); } /* Send message to the netlink */ return send(sock, &nl_request, sizeof(nl_request), 0); } /* Simple parser of the string IP address */ static int read_addr(const char *addr, _inet_addr *res) { if (strchr(addr, ':')) { res->family = AF_INET6; res->bitlen = 128; } else { res->family = AF_INET; res->bitlen = 32; } return inet_pton(res->family, addr, res->data); } int replace_default_gateway(const char* interface_name, const char* gateway_ip, int af_family, int* perr) { int sock_fd, err; struct ifreq ifr; _inet_addr to_addr; _inet_addr gw_addr; int nl_cmd = RTM_NEWROUTE; int nl_flags = NLM_F_CREATE | NLM_F_REPLACE; memset(&to_addr, 0, sizeof(to_addr)); memset(&gw_addr, 0, sizeof(gw_addr)); to_addr.family = (char) af_family; sock_fd = open_netlink(af_family); if (sock_fd < 0) { perr[0] = sock_fd; return -1; } perr[0] = read_addr(gateway_ip, &gw_addr); if( perr[0] != 1 ) { err = -2; goto L1; } if( gw_addr.family != af_family ) { err = -3; goto L1; } memset(&ifr, 0, sizeof(struct ifreq)); strncpy(ifr.ifr_name, interface_name, IFNAMSIZ-1); // Get the ifrindex of the interface perr[0] = ioctl(sock_fd, SIOGIFINDEX, &ifr); if ( perr[0] < 0 ) { err = -2; goto L1; } perr[0] = do_route(sock_fd, nl_cmd, nl_flags, &to_addr, &gw_addr, 1, ifr.ifr_ifindex); err = 0; L1: close(sock_fd); return err; }
问题原因
RTNH_F_ONLINK是**路由下一跳(nexthop)**的属性标志,而非路由条目本身的rtmsg.rtm_flags字段。直接将其设置到rtm_flags会导致netlink消息格式错误,内核无法正确解析,从而路由更新失败;移除该设置后消息格式合法,但缺少下一跳的onlink标志。
另外原代码的netlink socket未绑定地址,可能导致消息发送不稳定。
修复方案
1. 修正netlink socket绑定
在open_netlink函数中添加绑定操作,确保socket能正常通信:
static int open_netlink(int af_family) { struct sockaddr_nl saddr; int sock = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (sock < 0) { perror("Failed to open netlink socket"); return -1; } memset(&saddr, 0, sizeof(saddr)); saddr.nl_family = AF_NETLINK; saddr.nl_pid = getpid(); saddr.nl_groups = 0; if (bind(sock, (struct sockaddr *)&saddr, sizeof(saddr)) < 0) { perror("Failed to bind netlink socket"); close(sock); return -1; } return sock; }
2. 正确传递RTNH_F_ONLINK标志
通过RTA_NH属性传递下一跳信息,将RTNH_F_ONLINK设置到struct rtnexthop的rtnh_flags字段,同时指定出口接口:
修改do_route函数中的网关处理逻辑,替换原有的RTA_GATEWAY添加代码:
/* Set gateway */ if (gw->bitlen != 0) { // 构造下一跳结构 struct rtnexthop nh; memset(&nh, 0, sizeof(nh)); nh.rtnh_family = gw->family; nh.rtnh_len = sizeof(struct rtnexthop); nh.rtnh_flags = RTNH_F_ONLINK; nh.rtnh_ifindex = if_idx; // 添加RTA_NH属性 int ret = rtattr_add(&nl_request.n, sizeof(nl_request), RTA_NH, &nh, sizeof(nh)); if (ret < 0) { return ret; } // 在RTA_NH属性内添加网关地址子属性 struct rtattr *nh_rta = NLMSG_TAIL(&nl_request.n); nh_rta->rta_type = RTA_GATEWAY; nh_rta->rta_len = RTA_LENGTH(gw->bitlen / 8); memcpy(RTA_DATA(nh_rta), gw->data, gw->bitlen / 8); nl_request.n.nlmsg_len += RTA_ALIGN(nh_rta->rta_len); nl_request.r.rtm_scope = RT_SCOPE_UNIVERSE; }
3. 添加metric和preference属性
为了和目标命令完全一致,添加metric 1024和pref medium的设置,在网关处理逻辑后添加:
// 设置metric 1024和pref medium uint32_t metric = 1024; uint32_t pref = ICMPV6_ROUTER_PREF_MEDIUM; // 添加RTA_METRICS容器属性 int ret = rtattr_add(&nl_request.n, sizeof(nl_request), RTA_METRICS, NULL, 0); if (ret < 0) { return ret; } // 在容器内添加metric属性 struct rtattr *metrics_rta = NLMSG_TAIL(&nl_request.n); ret = rtattr_add(metrics_rta, sizeof(nl_request) - ((char*)metrics_rta - (char*)&nl_request), RTAX_METRIC, &metric, sizeof(metric)); if (ret < 0) { return ret; } // 在容器内添加preference属性 ret = rtattr_add(metrics_rta, sizeof(nl_request) - ((char*)metrics_rta - (char*)&nl_request), RTAX_PREF, &pref, sizeof(pref)); if (ret < 0) { return ret; }
4. 移除错误的rtm_flags设置
删除do_route函数中nl_request.r.rtm_flags = RTNH_F_ONLINK;这一行代码。
验证
修复后执行代码,再运行ip -6 route show,可以看到默认路由包含onlink、metric 1024和pref medium属性,与目标命令执行效果完全一致。
内容的提问来源于stack exchange,提问作者p calvin

