UNP_18_路由套接字
18.1 概述
内核中的 Unix 路由表传统上一直使用 ioctl 命令进行访问,但是没有 ioctl 命令可以获得所有路由表,相反,类似 netstat 等程序通过读取内核的内存获取路由表的内容。
- 进程可以通过写出到路由套接字而往内核发送消息。路径的增加和删除采用这种操作实现
- 进程可以通过从路由套接字读入而自内核接收消息。内核采用这种操作通知进程已收到并处理一个 ICMP 重定向消息,或者请求外部路由进程解析一个消息
- 进程可以使用 sysctl 函数获取所有路由表或所有已配置的接口
前两种操作需要超级用户权限,最后一种只需要普通用户权限
18.2 数据链路套接字地址结构
通过路由套接字返回一些消息中含有作为返回值给出的数据链路套接字地址结构,结构如下:
struct sockaddr_dl {
uint8_t sdl_len;
sa_family_t sdl_family; /* AF_LINK */
uint16_t sdl_index; /* system assigned index, if > 0 */
uint8_t sdl_type; /* IFT_ETHER, etc. from */
uint8_t sdl_nlen; /* name length, starting in sdl_data[0] */
uint8_t sdl_alen; /* link-layer address length */
uint8_t sdl_slen; /* link-layer selector length */
char sdl_data[12]; /* minimum work area, can be larger;
contains i/f name and link-layer address */
};
- 每个接口都有一个唯一的正值索引,返回索引的方法有:if_nametoindex 和 if_nameindex 函数,还有不同协议对应的套接字选项
- sdl_data 成员含有名字和链路地址。名字从 sdl_data[0] 开始,而且不以空字符结尾。链路层地址从 sdl_data[sdl_nlen] 开始
- 数据链路套接字地址结构的长度是可变的。如果链路层地址和名字超过 12 字节,结构将大于 20 字节
18.3 读和写
创建一个路由套接字后,进程可以通过写到该套接字向核和发送命令,通过读自该套接字的内核接受信息。路由域套接字共有 12 个路有消息,其中 5 个可以由进程发出,具体消息和数据结构参照 UNP 图18-2,图18-3
每个结构有相同的前 3 个成员:本消息的长度,版本和类型。
18.4 sysct 操作
我们对路由套接字的主要兴趣在于使用 sysctl 函数检查路由表和接口列表。创建路由套接字(一个 AF_ROUTE 域的原始套接字)需要超级用户权限,然而使用 sysctl 检查路由表和接口列表的进程普通用户也可以操作
#include<sys/param.h>
#include<sys/sysctl.h>
int sysctl(int* name, u_int namelen, void* oldp, size_t* oldlenp, void * newp, size_t newlen);
// 返回:若成功返回 0,出错返回 -1
- name 参数指定名字的整数数组
- namelen 参数指定数组中的元素数,该数组中的第一个元素指定本请求定向到内核的拿一个子系统,后面的值逐渐细化该子系统的某个部分
- oldp 参数指向一个宫内和存放分层的层级值的缓冲区
- oldlenp 参数指定的值代表该缓冲区的大小
sysctl 函数可以获取许多系统信息:文件系统,虚拟内存,内核限制,硬件等等。我们在这里感兴趣的是网络子系统,通过把 name 数组的第一个元素设置为 CTL_NET 来指定。第二个参数可以是下面几种:
- AF_INET:获取或设置影响网际网协议的变量。下一级为使用某个 IPPROTO_xx 常值指定的具体协议
- AF_LINK:获取或设置链路层信息,譬如 PPP 接口的数目
- AF_ROUTE:返回路由表或接口列表的信息
- AF_UNSPEC:获取或设置一些套接字层变量,譬如套接字发送或接收缓冲区的最大大小。
UNP 小例子:判断 UDP 检验和是否开启
#include "unproute.h"
#include <netinet/udp.h>
#include <netinet/ip_var.h>
#include <netinet/udp_var.h> /* for UDPCTL_xxx constants */
int main(int argc, char **argv)
{
int mib[4], val;
size_t len;
mib[0] = CTL_NET;
mib[1] = AF_INET;
mib[2] = IPPROTO_UDP;
mib[3] = UDPCTL_CHECKSUM;
len = sizeof(val);
Sysctl(mib, 4, &val, &len, NULL, 0);
printf("udp checksum flag: %d\n", val);
exit(0);
}
18.5 get_ifi_info 函数
使用路由套接字又把 get_ifi_info 实现了一遍,具体代码见 UNP-18.6
18.6 接口名字和索引函数
有四个函数用于处理接口名字和索引。这四个函数用于需要描述一个接口的地方,适用于 IPv4 和 IPv6。需要注意一个基本概念:每个接口都有唯一的名字和唯一的正值索引(0 从不用做索引)
#include<net/if.h>
unsigned int if_nametoindex(const char * ifname);
// 返回:成功返回为正值的接口索引,出错返回 0
char* if_indextoname(unisgned int ifindex, char* ifname);
// 返回:成功返回指向接口名字的指针,出错返回 NULL
struct if_nameindex* if_nameindex(void);
// 返回:成功为非空指针,出错返回 NULL
void if_freenameindex(struct if_nameindex* ptr)
- if_nametoindex 返回名字为 ifname 的接口的索引
- if_indextoname 返回索引为 ifindex 的接口名字
- if_nameindex 返回一个指向 if_nameindex 结构数组的指针
下面使用路由套接字实现这四个函数:
if_nametoindex 函数
unsigned int if_nametoindex(const char *name)
{
unsigned int idx, namelen;
char *buf, *next, *lim;
size_t len;
struct if_msghdr *ifm;
struct sockaddr *sa, *rti_info[RTAX_MAX];
struct sockaddr_dl *sdl;
// net_rt_iflist 函数用来返回接口列表
if ( (buf = net_rt_iflist(0, 0, &len)) == NULL)
return(0);
namelen = strlen(name);
lim = buf + len;
for (next = buf; next < lim; next += ifm->ifm_msglen) {
// 处理缓冲区中的消息,仅仅查找 RTM_IFINFO 消息
// 找到后调用 get_rtaddrs 函数设置指向各个套接字地址结构的指针
// 如果存在一个接口名字,那就比较其中的接口名字(rti_info[RTAX_IFP])和调用者参数中要查找的接口名字
ifm = (struct if_msghdr *) next;
if (ifm->ifm_type == RTM_IFINFO) {
sa = (struct sockaddr *) (ifm + 1);
get_rtaddrs(ifm->ifm_addrs, sa, rti_info);
if ( (sa = rti_info[RTAX_IFP]) != NULL) {
if (sa->sa_family == AF_LINK) {
sdl = (struct sockaddr_dl *) sa;
if (sdl->sdl_nlen == namelen && strncmp(&sdl->sdl_data[0], name, sdl->sdl_nlen) == 0) {
idx = sdl->sdl_index; /* save before free() */
free(buf);
return(idx);
}
}
}
}
}
free(buf);
return(0); /* no match for name */
}
if_indextoname 函数
// 该函数和上一个函数类似,仅仅是比较的类型不一样而已
char * if_indextoname(unsigned int idx, char *name)
{
char *buf, *next, *lim;
size_t len;
struct if_msghdr *ifm;
struct sockaddr *sa, *rti_info[RTAX_MAX];
struct sockaddr_dl *sdl;
if ( (buf = net_rt_iflist(0, idx, &len)) == NULL)
return(NULL);
lim = buf + len;
for (next = buf; next < lim; next += ifm->ifm_msglen) {
ifm = (struct if_msghdr *) next;
if (ifm->ifm_type == RTM_IFINFO) {
sa = (struct sockaddr *) (ifm + 1);
get_rtaddrs(ifm->ifm_addrs, sa, rti_info);
if ( (sa = rti_info[RTAX_IFP]) != NULL) {
if (sa->sa_family == AF_LINK) {
sdl = (struct sockaddr_dl *) sa;
if (sdl->sdl_index == idx) {
int slen = min(IFNAMSIZ - 1, sdl->sdl_nlen);
strncpy(name, sdl->sdl_data, slen);
name[slen] = 0; /* null terminate */
free(buf);
return(name);
}
}
}
}
}
free(buf);
return(NULL); /* no match for index */
}
if_nameindex 函数
// 返回一个 if_nameindex 结构数组,包含所有接口名字和索引对
struct if_nameindex * if_nameindex(void)
{
char *buf, *next, *lim;
size_t len;
struct if_msghdr *ifm;
struct sockaddr *sa, *rti_info[RTAX_MAX];
struct sockaddr_dl *sdl;
struct if_nameindex *result, *ifptr;
char *namptr;
// 构建返回数组,确定大小
if ( (buf = net_rt_iflist(0, 0, &len)) == NULL)
return(NULL);
if ( (result = malloc(len)) == NULL) /* overestimate */
return(NULL);
ifptr = result;
namptr = (char *) result + len; /* names start at end of buffer */
lim = buf + len;
for (next = buf; next < lim; next += ifm->ifm_msglen) {
// 遍历所有消息,从中查找 RTM_IFINFO 消息以及后面的数据链路套接字地址结构。把接口名字和索引放到返回数组中
ifm = (struct if_msghdr *) next;
if (ifm->ifm_type == RTM_IFINFO) {
sa = (struct sockaddr *) (ifm + 1);
get_rtaddrs(ifm->ifm_addrs, sa, rti_info);
if ( (sa = rti_info[RTAX_IFP]) != NULL) {
if (sa->sa_family == AF_LINK) {
sdl = (struct sockaddr_dl *) sa;
namptr -= sdl->sdl_nlen + 1;
strncpy(namptr, &sdl->sdl_data[0], sdl->sdl_nlen);
namptr[sdl->sdl_nlen] = 0; /* null terminate */
ifptr->if_name = namptr;
ifptr->if_index = sdl->sdl_index;
ifptr++;
}
}
}
}
// 把返回数组的最后一个元素置空
ifptr->if_name = NULL; /* mark end of array of structs */
ifptr->if_index = 0;
free(buf);
return(result); /* caller must free() this when done */
}
if_freenameindex 函数
// 释放 if_nameindex 结构内存即可,因为是一整块内存,不需要遍历
void if_freenameindex(struct if_nameindex *ptr)
{
free(ptr);
}
小结
本节所说的 sockaddr_dl 结构,是一种可变长度的数据链路套接字地址结构。以便返回接口索引、名字和硬件地址等不同信息。
进程可以写到路由套接字的消息有 5 个类型,内核通过路由套接字异步返回的消息有 15 个类型。
sysctl 函数是获取和设置操作系统参数的通用方法。我们所关注的 sysctl 操作包括:
- 获取所有接口列表
- 获取到路由表
- 获取 ARP 高速缓存
IPv6 要求套接字 API 实施的相关改动包括在接口名字和接口索引之间进行映射的 4 个函数。每个接口都有一个正值索引。系统已经把接口名字和索引值关联起来,我们可以很容易的使用 sysctl 实现接口信息相关函数