struct sockaddr_nl 结构体 由来、含义以及使用——获取Linux路由表

本文深入探讨了Linux中用户态与内核态之间的交互机制,特别是使用netlink套接字进行通信的方法。重点介绍了netlink套接字的特性、生成与绑定过程,以及如何使用nlmsghdr和特定数据结构进行通信操作,最终通过实例展示了如何从内核读取IPV4路由表信息。
Linux 用户态与内核态的交互
  在 Linux 2.4 版以后版本的内核中,几乎全部的中断过程与用户态进程的通信都是使用 netlink 套接字实现的,例如iprote2网络管理工具,它与内核的交互就全部使用了netlink,著名的内核包过滤框架Netfilter在与用户空间的通 读,也在最新版本中改变为netlink,无疑,它将是Linux用户态与内核态交流的主要方法之一。它的通信依据是一个对应于进程的标识,一般定为该进 程的 ID。当通信的一端处于中断过程时,该标识为 0。当使用 netlink 套接字进行通信,通信的双方都是用户态进程,则使用方法类似于消息队列。但通信双方有一端是中断过程,使用方法则不同。netlink 套接字的最大特点是对中断过程的支持,它在内核空间接收用户空间数据时不再需要用户自行启动一个内核线程,而是通过另一个软中断调用用户事先指定的接收函 数。
《UNIX Network Programming Volume 1 - 3rd Edition》第18章
讲到BSD UNIX系统中routing socket的应用,这种套接字是按下面方式生成的:
rt_socket = socket(AF_ROUTE, SOCK_RAW, 0);
然 后就可以用它跟内核交互,进行网络环境管理的操作,如读取/设置/删除路由表信息,更改网关等等,但书中所列代码只在4.3BSD及以后版本的原始 UNIX系统下可用,Linux虽然实现了AF_ROUTE族套接字,但用法却完全不同。由于网上这方面知识的资料想对匮乏,现对Linux下 routing socket的使用做一介绍。
由于我现在在Magic Linux1.0下工作,所以以下的讲解全部基于2.4.10内核。Linux从v2.2开始引入这一机制,因此可以肯定从v2.2到v2.4的内核都是适用的,更新的v2.6我没有试过。

Linux下虽然也有AF_ROUTE族套接字,但是这个定义只是个别名,请看
/usr/include/linux/socket.h, line 145:
#define AF_ROUTE AF_NETLINK /* Alias to emulate 4.4BSD */
可 见在Linux内核当中真正实现routing socket的是AF_NETLINK族套接字。AF_NETLINK族套接字像一个连接用户空间和内核的双工管道,通过它,用户进程可以修改内核运行参 数、读取和设置路由信息、控制特定网卡的up/down状态等等,可以说是一个管理网络资源的绝佳途径。

1 生成所需套接字,并绑定一个sockaddr结构

先来看如何生成一个AF_NETLINK族套接字:
sockfd = socket(AF_NETLINK, socket_type, netlink_faimly);
这里socket_type可选SOCK_DGRAM或SOCK_RAW;因为AF_NETLINK族是面向数据报的套接字,所以不能使用SOCK_STREAM。
netlink_family指定要和内核中的哪个子系统进行交互,目前支持:
NETLINK_ROUTE 与路由信息相关,包括查询、设置和删除路由表中的条目等。待会儿我们将以这类family举个实际的例子;
NETLINK_FIREWALL 接收由IPv4防火墙代码发送的包;
NETLINK_ARPD 可以在用户空间进行arp缓存的管理;
NETLINK_ROUTE6 在用户空间发送和接收路由表信息更新;
还有几种虽然没有实现,但已经有了定义,为以后扩展做好了准备。

接下来要给该套接字绑定一个sockaddr结构,实际上是一个sockaddr_nl结构:
struct sockaddr_nl {
sa_family_t nl_family; /*AF_NETLINK*/
unsigned short nl_pad; /* 0 */
pid_t nl_pid; /* 进程pid */
u_32 nl_groups; /* 多播组掩码*/
}nl;
这个结构一般按照注释填好就可以了,nl_groups我也不知道怎么用,一般填零了,表示没有多播。绑定:
bind(sockfd, (struct sockaddr*) &nl, sizeof(nl));

2 填充所需数据结构,并通过sendmsg()/send()等函数写到套接字里去

到 此为止,与内核通信的准备工作就完成了,下面要做的工作是,选取适当的数据结构进行填充,并作为sendmsg()的参数发送出去,并recv()收到的 消息。这个数据结构就是nlmsghdr,它只是一个信息头,后面可以接任意长的数据,这些数据实际上又是针对某一需求所采用的特定数据结构。先来看 nlmsghdr:
struct nlmsghdr {
_u32 nlmsg_len; /* Length of msg including header */
_u32 nlmsg_type; /* 操作命令 */
_u16 nlmsg_flags; /* various flags */
_u32 nlmsg_seq; /* Sequence number */
_u32 nlmsg_pid; /* 进程PID */
};
/* 紧跟着是实际要发送的数据,长度可以任意 */

nlmsg_type 决定这次要执行的操作,如查询当前路由表信息,所使用的就是RTM_GETROUTE。标准nlmsg_type包括:NLMSG_NOOP, NLMSG_DONE, NLMSG_ERROR等。根据采用的nlmsg_type不同,还要选取不同的数据结构来填充到nlmsghdr后面:
操作 数据结构
RTM_NEWLINK ifinfomsg
RTM_DELLINK
RTM_GETLINK
RTM_NEWADDR ifaddrmsg
RTM_DELADDR
RTM_GETADDR
RTM_NEWROUTE rtmsg
RTM_DELROUTE
RTM_GETROUTE
RTM_NEWNEIGH ndmsg/nda_chcheinfo
RTM_DELNEIGH
RTM_GETNEIGH
RTM_NEWRULE rtmsg
RTM_DELRULE
RTM_GETRULE
RTM_NEWQDISC tcmsg
RTM_DELQDISC
RTM_GETQDISC
RTM_NEWTCLASS tcmsg
RTM_DELTCLASS
RTM_GETTCLASS
RTM_NEWTFILTER tcmsg
RTM_DELTFILTER
RTM_GETTFILTER
由于情形众多,我从现在开始将用一个特定的例子来说明问题。我们的目的是从内核读取IPV4路由表信息。从上面表看,nlmsg_type一定使用RTM_xxxROUTE操作,对应的数据结构是rtmsg。既然是读取,那么应该是RTM_GETROUTE了。
struct rtmsg {
unsigned char rtm_family; /* 路由表地址族 */
unsigned char rtm_dst_len; /* 目的长度 */
unsigned char rtm_src_len; /* 源长度 */ (2.4.10头文件的注释标反了?)
unsigned char rtm_tos; /* TOS */

unsigned char rtm_table; /* 路由表选取 */
unsigned char rtm_protocol; /* 路由协议 */
unsigned char rtm_scope;
unsigned char rtm_type;

unsigned int rtm_flags;
};
对于RTM_GETROUTE操作来说,我们只需指定两个成员:rtm_family:AF_INET, rtm_table: RT_TABLE_MAIN。其他成员都初始化为0即可。将这个结构体跟nlmsghdr结合起来,得到我们自己的新结构体:
struct {
struct nlmsghdr nl;
struct rtmsg rt;
}req;
填充好rt结构之后,还要调整nl结构相应成员的值。Linux定义了多个宏来处理nlmsghdr成员的值,我们这里用到的是NLMSG_LENGTH(size_t len);
req.nl.nlmsg_len = NLMSG_LENGTH(sizeof(struct rtmsg));
这将计算nlmsghdr长度与rtmsg长度的和(其中包括了将rtmsg进行4字节边界对齐的调整),并存储到nlmsghdr的nlmsg_len成员中。接下来要做的就是将这个新结构体req放到sendmsg()函数的msghdr.iov处,并调用函数。
sendmsg(sockfd, &msg, 0);

3 接收数据,并进行分析

接下来的操作是recv()操作,从该套接字读取内核返回的数据,并进行分析处理。
recv(sockfd, p, sizeof(buf) - nll, 0);
其中p是指向一个缓冲区buf的指针,nll是已接收到的nlmsghdr数据的长度。
由于内核返回信息是一个字节流,需要调用者检查消息结尾。这是通过检查返回的nlmsghdr的nlmsg_type是否等于NLMSG_DONE来完成的。返回的数据格式如下:
-----------------------------------------------------------
| nlmsghdr+route entry | nlmsghdr+route entry | .........
-----------------------------------------------------------
| 解出route entry
V
-----------------------------------------------------------
| dst_addr | gateway | Output interface| ...............
-----------------------------------------------------------
可 以看出,返回消息由多个(nlmsghdr + route entry)组成,当某个nlmsghdr的nlmsg_type == NLMSG_DONE时就表示信息输出已经完毕。而每一个route entry由多个rtattr结构体组成,每个结构体表示该路由项的某个属性,如目的地址,网关等等。根据这个示意图我们就能够轻松解析需要的数据了。

原文:
http://www.douban.com/note/29453304/
在该C语言代码“#define _GNU_SOURCE //用于启用GNU C 库,需要在任何头文件之前定义 #include <linux/if_arp.h> //arp 地址解析协议 //#include <linux/if_packet.h> //原始数据包的数据结构定义 //#include <net/ethernet.h> //包括几个以太网的数据结构 //#include <net/if.h> //socket头文件,定义了网卡的接口信息的宏,和linux/if.h可能存在重复定义问题,先加载linux/if.h #include <netinet/ether.h> //以太帧的网络字节和ascii字节的转换 #include <arpa/inet.h> #include <errno.h> //#include <pthread.h> //多线程 #include <stdio.h> #include <stdlib.h> #include <string.h> #include <sys/ioctl.h> //IO控制操作相关的函数声明 #include <sys/socket.h> #include <sys/types.h> //类型重定义 #include <unistd.h> //read/write/close函数 /**************************************************************************************************/ /* DEFINES */ /**************************************************************************************************/ /*定义/宏*/ // 对外可见的最小接口(由 arpsd.c 调用) // 接口信息 包含系统索引、mac地址和IP地址 typedef struct { int ifindex; //网络接口的本地索引 uint8_t src_mac[6]; //mac地址 uint32_t src_ip; //主机字节序 } arpsd_ctx_t; // 描述ARP报文帧的紧凑内存布局 // __attribute__((packed)) 无填充字节,确保字段紧密排列 typedef struct __attribute__((packed)) { struct ether_header eth; //以太网帧头部 struct { uint16_t htype; // 硬件类型 uint16_t ptype; // 协议类型 uint8_t hlen; // 硬件地址长度 uint8_t plen; // 协议地址长度 uint16_t oper; // 操作码 uint8_t sha[6]; // 发送方MAC uint32_t spa; // 发送方IP uint8_t tha[6]; // 目标MAC uint32_t tpa; // 目标IP } arp; } arpsd_frame_t; /**************************************************************************************************/ /* TYPES */ /**************************************************************************************************/ /*类型*/ /**************************************************************************************************/ /* EXTERN_PROTOTYPES */ /**************************************************************************************************/ /*外部原型*/ /**************************************************************************************************/ /* LOCAL_PROTOTYPERS */ /**************************************************************************************************/ int arpsd_ctx_init(arpsd_ctx_t *ctx, uint32_t probe_ip_host); static int get_iface_for_ip(uint32_t ip_host, char *ifname, size_t len); int arpsd_send_who_has(const arpsd_ctx_t *ctx, uint32_t target_ip_host); int arpsd_recv_is_at(uint8_t *out_mac6, uint32_t *out_ip_host, int timeout_ms); /**************************************************************************************************/ /* VARIABLES */ /**************************************************************************************************/ /*本文件中的变量声明*/ /**************************************************************************************************/ /* PUBLIC_FUNCTIONS */ /**************************************************************************************************/ /*全局函数*/ /* * fn arpsd_ctx_init * brief 创建原始套接字并绑定到用于发包的接口;自动选路由接口 * detailes 根据目标IP动态获取绑定的网络接口,获取接口的系统索引、MAC地址和IP地址。 * * param[in] uint32_t probe_ip_host 主机字节序的IP地址 * param[out] arpsd_ctx_t *ctx 网络接口的系统索引、MAC地址和IP地址 * * return 返回函数运行状态 * retval 0表示成功,-1表示失败 */ int arpsd_ctx_init(arpsd_ctx_t *ctx, uint32_t probe_ip_host) { char ifname[IFNAMSIZ] = {0}; //IFNAMSIZ网络接口名称的最大长度 16 //根据目标IP地址动态获取对应的网络接口 if (get_iface_for_ip(probe_ip_host, ifname, sizeof(ifname)) != 0) { // 兜底:默认 eth0 snprintf(ifname, sizeof(ifname), "eth0"); } int fd = socket(AF_INET, SOCK_DGRAM, 0); //AF_INET-IPv4通信,SOCK_DGRAM-UDP连接 if (fd < 0) return -1; struct ifreq ifr; //网络接口名称 memset(&ifr, 0, sizeof(ifr)); strncpy(ifr.ifr_name, ifname, IFNAMSIZ-1); // index int ifindex = if_nametoindex(ifname); //将网络接口的名称转换为本地索引 if (!ifindex) { close(fd); return -1; } // MAC if (ioctl(fd, SIOCGIFHWADDR, &ifr) < 0) { close(fd); return -1; } //获取MAC地址 memcpy(ctx->src_mac, ifr.ifr_hwaddr.sa_data, 6); // IP if (ioctl(fd, SIOCGIFADDR, &ifr) < 0) { close(fd); return -1; } struct sockaddr_in *sa = (struct sockaddr_in *)&ifr.ifr_addr; ctx->src_ip = ntohl(sa->sin_addr.s_addr); //网络字节序->主机字节序 ctx->ifindex = ifindex; close(fd); return 0; } /* * fn arpsd_send_who_has * brief 发送ARP请求报文 * detailes 通过原始套接字构造并向局域网广播一个ARP请求帧,以查询目标地址对应的MAC地址 * * param[in] const arpsd_ctx_t *ctx 发包网络接口 * param[in] uint32_t target_ip_host 目标IP地址 * * return 返回函数运行状态 * retval 0-成功,-1失败 */ int arpsd_send_who_has(const arpsd_ctx_t *ctx, uint32_t target_ip_host) { // 创建链路层原始套接字,协议为ARP协议 int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP)); if (fd < 0) return -1; // 设置目标地址结构 struct sockaddr_ll addr = {0}; addr.sll_family = AF_PACKET; //链路层地址族 addr.sll_ifindex = ctx->ifindex; //发包网络接口 addr.sll_halen = ETH_ALEN; //MAC地址长度 memset(addr.sll_addr, 0xff, ETH_ALEN); // 广播地址 FF:FF:FF:FF:FF:FF,确保ARP请求广播到局域网 // 构造ARP请求帧 arpsd_frame_t frm; memset(&frm, 0, sizeof(frm)); // eth以太网帧头部 memset(frm.eth.ether_dhost, 0xff, 6); // 广播地址全ff memcpy(frm.eth.ether_shost, ctx->src_mac, 6); //发包MAC地址 frm.eth.ether_type = htons(ETH_P_ARP); //ARP协议 // arp frm.arp.htype = htons(ARPHRD_ETHER); //硬件类型为以太网(1) frm.arp.ptype = htons(ETH_P_IP); //协议类型为IPv4(0x0800) frm.arp.hlen = 6; //MAC地址长度 frm.arp.plen = 4; //IP地址长度 frm.arp.oper = htons(ARPOP_REQUEST); //操作码为ARP请求(1) memcpy(frm.arp.sha, ctx->src_mac, 6); frm.arp.spa = htonl(ctx->src_ip); // 发送方MAC和IP地址 memset(frm.arp.tha, 0x00, 6); //目标MAC未知 frm.arp.tpa = htonl(target_ip_host); //目标IP // 发送ARP请求 int n = sendto(fd, &frm, sizeof(frm), 0, (struct sockaddr *)&addr, sizeof(addr)); int ret = (n == sizeof(frm)) ? 0 : -1; close(fd); return ret; } /* * fn arpsd_recv_is_at * brief ARP响应接受器 * detailes 用于监听并解析局域网内的ARP回复报文,从中提取发送方的MAC地址和IP地址 * * param[in] int timeout_ms 超时时间,避免程序长时间挂起 * param[out] uint8_t *out_mac6 发送方MAC地址 * param[out] uint32_t *out_ip_host 发送方IP地址 * * return 返回函数运行状态 * retval 0 - 成功并填充输出 * -1 - 套接字创建失败 * -2 - 接收数据超时或出错 * -3 - 接收的数据包长度小于 arpsd_frame_t 结构体大小 * -4 - 以太网帧类型非 ARP * -5 - ARP 操作码非回复 * * note 简易实现,并未实现tha=本机MAC或广播 */ int arpsd_recv_is_at(uint8_t *out_mac6, uint32_t *out_ip_host, int timeout_ms) { //链路层原始套接字,ARP协议 int fd = socket(AF_PACKET, SOCK_RAW, htons(ETH_P_ARP)); if (fd < 0) return -1; // 设置超时时间 struct timeval tv = { .tv_sec = timeout_ms/1000, .tv_usec = (timeout_ms%1000)*1000 }; setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); uint8_t buf[1500]; ssize_t n = recv(fd, buf, sizeof(buf), 0); if (n < 0) { close(fd); return -2; } // 长度需满足arp报文要求 if ((size_t)n < sizeof(arpsd_frame_t)) { close(fd); return -3; } arpsd_frame_t *f = (arpsd_frame_t*)buf; //检验报文有效性 if (ntohs(f->eth.ether_type) != ETH_P_ARP) { close(fd); return -4; } //以太网帧类型为ARP协议 if (ntohs(f->arp.oper) != ARPOP_REPLY) { close(fd); return -5; } //ARP操作码为2 if (out_mac6) memcpy(out_mac6, f->arp.sha, 6); //发送方MAC地址 if (out_ip_host) *out_ip_host = ntohl(f->arp.spa); //发送方IP地址,主机字节序 close(fd); return 0; } /**************************************************************************************************/ /* LOCAL_FUNCTIONS */ /**************************************************************************************************/ /* * fn get_iface_for_ip * brief 根据目标IP地址查找匹配的网络接口名称 * detailes 解析内核的路由表文件/proc/net/route * * param[in] uint32_t ip_host 目标IP * param[in] size_t len 复制长度 * param[out] char *ifname 最佳网络接口 * * return 返回函数运行状态 * retval 0表示成功,-1表示失败 */ static int get_iface_for_ip(uint32_t ip_host, char *ifname, size_t len) { // 读取 /proc/net/route,取第一条 UP 的默认或最匹配路由(简单实现) // /proc/net/route-linux内核提供的路由表信息 // 每条路由包含接口名iface, 目标网络地址dest,子网掩码mask,标志位flags FILE *f = fopen("/proc/net/route", "r"); if (!f) return -1; char line[512]; // skip header fgets(line, sizeof(line), f); uint32_t best_mask = 0, best_dest = 0; char best_if[IFNAMSIZ] = {0}; while (fgets(line, sizeof(line), f)) { char iface[IFNAMSIZ]; unsigned int dest, gateway, flags, mask; int refcnt, use, metric, mtu, win, irtt; if (sscanf(line, "%s\t%X\t%X\t%X\t%d\t%d\t%d\t%X\t%d\t%d\t%d", iface, &dest, &gateway, &flags, &refcnt, &use, &metric, &mask, &mtu, &win, &irtt) != 11) continue; // ip_host/ mask 与目标匹配度 // 将目标ip与路由条目中的mask进行按位与运算,判断是否属于该路由的网络范围 uint32_t d = dest; uint32_t m = mask; if ((htonl(ip_host) & m) == (d & m)) { //选择最长前缀匹配作为最佳匹配接口,若存在多条匹配路由,优先选择掩码最大的条目 if (m >= best_mask) { best_mask = m; best_dest = d; strncpy(best_if, iface, IFNAMSIZ-1); } } // 记录默认路由兜底 if (best_if[0] == '\0' && d==0) { strncpy(best_if, iface, IFNAMSIZ-1); } } fclose(f); if (best_if[0]=='\0') return -1; snprintf(ifname, len, "%s", best_if); return 0; }”中,编译时会报错“In file included from /usr/include/netinet/if_ether.h:61, from arpsd_sock.c:26: /usr/include/net/if_arp.h:53:8: error: redefinition of ‘struct arphdr’ 53 | struct arphdr | ^~~~~~ In file included from arpsd_sock.c:21: /usr/include/linux/if_arp.h:144:8: note: originally defined here 144 | struct arphdr { | ^~~~~~ In file included from /usr/include/netinet/if_ether.h:61, from arpsd_sock.c:26: /usr/include/net/if_arp.h:138:8: error: redefinition of ‘struct arpreq’ 138 | struct arpreq | ^~~~~~ In file included from arpsd_sock.c:21: /usr/include/linux/if_arp.h:116:8: note: originally defined here 116 | struct arpreq { | ^~~~~~ In file included from /usr/include/netinet/if_ether.h:61, from arpsd_sock.c:26: /usr/include/net/if_arp.h:147:8: error: redefinition of ‘struct arpreq_old’ 147 | struct arpreq_old | ^~~~~~~~~~ In file included from arpsd_sock.c:21: /usr/include/linux/if_arp.h:124:8: note: originally defined here 124 | struct arpreq_old { | ^~~~~~~~~~ arpsd_sock.c: In function ‘arpsd_ctx_init’: arpsd_sock.c:130:19: warning: implicit declaration of function ‘if_nametoindex’ [-Wimplicit-function-declaration] 130 | int ifindex = if_nametoindex(ifname); //将网络接口的名称转换为本地索引 | ^~~~~~~~~~~~~~ ”请问如何修改?
08-20
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值