Unix网络编程(11):路由套接字
注:以下内容可能不适用于Linux系统
内核中的Unix
路由表传统上一直使用 $ioctl$ 访问,但是 $ioctl$ 不能输出整个路由表,而诸如 $netstat$ 等程序通过读取内核的内存获取路由表内容。诸如 $gated$ 等路由守护进程需要监视由内核收取的ICMP
重定向消息,它们通常创建一个原始ICMP
套接字,再在这个套接字上监听所有收到的ICMP
消息。在路由域中支持的唯一一种套接字是原始套接字。路由套接字上支持 $3$ 种类型的操作:
- 进程可以通过写出到路由套接字而往内核发送消息。路径的增加和删除采用这种操作实现;
- 进程可以通过从路由套接字读入而从内核接收消息。内核采用这种操作通知进程已收到并处理一个
ICMP
重定向消息,或者请求外部路由进程解析一个路径; - 进程可以使用 $sysctl$ 函数输出路由表或者列出所有已配置的接口。
前两种操作可以复合使用,需要超级用户权限,最后一种任何进程都可以执行。
1. 数据链路套接字地址结构
#include <net/if_dl.h>
struct sockaddr_dl {
uint8_t sdl_len;
sa_family_t sdl_family; // AF_LINK
uint16_t sdl_index; // 大于0的系统分配的索引
uint8_t sdl_type; // IFT_ETHER等在<net/if_types.h>中定义的常值
uint8_t sdl_nlen; // 名字长度,从sdl_data[0]开始
uint8_t sdl_alen; // 链路层地址长度
uint8_t sdl_slen; // 链路层选择器长度
char sdl_data[12]; // 最小工作区域,可以扩大,包含i/f名称和链路层地址
};
#define LLADDR(s) ((caddr_t) ((s)->sdl_data + (s)->sdl_nlen))
每个接口都有一个唯一的索引。$sdl_-data$ 成员含有名字和链路层地址,名字从 $sdl_-data[0]$ 开始,并且不以空字符结尾。链路层地址从 $sdl_-data[sdl_-nlen]$ 开始,$LLADDR$ 宏返回指向链路层地址的指针。
2. 套接字读写
创建一个路由套接字以后,进程可以通过写到该套接字向内核发送命令,通过从套接字读来从内核接收信息。路由套接字共有 $12$ 个路由消息,定义在 <$net/route.h$> 头文件中。
消息类型 | 写到内核 | 从内核读 | 说明 | 结构类型 |
---|---|---|---|---|
$RTM_-ADD$ | $\checkmark$ | $\checkmark$ | 增加路径 | $rt_-msghdr$ |
$RTM_-CHANGE$ | $\checkmark$ | $\checkmark$ | 改动网关、测度或标志 | $rt_-msghdr$ |
$RTM_-DELADDR$ | $\checkmark$ | 地址从接口中删除 | $ifa_-msghdr$ | |
$RTM_-DELETE$ | $\checkmark$ | $\checkmark$ | 删除路径 | $rt_-msghdr$ |
$RTM_-DELMADDR$ | $\checkmark$ | 多播地址从接口中删除 | $ifma_-msghdr$ | |
$RTM_-GET$ | $\checkmark$ | $\checkmark$ | 报告测度及其他路径信息 | $rt_-msghdr$ |
$RTM_-IFINFO$ | $\checkmark$ | 接口可用、不可用等 | $if_-msghdr$ | |
$RTM_-LOCK$ | $\checkmark$ | $\checkmark$ | 锁住给定的测度 | $rt_-msghdr$ |
$RTM_-LOSING$ | $\checkmark$ | 内核怀疑路径即将失效 | $rt_-msghdr$ | |
$RTM_-MISS$ | $\checkmark$ | 地址查找失败 | $rt_-msghdr$ | |
$RTM_-NEWADDR$ | $\checkmark$ | 地址被添加至接口 | $ifa_-msghdr$ | |
$RTM_-NEWMADDR$ | $\checkmark$ | 多播地址被添加至接口 | $ifa_-msghdr$ | |
$RTM_-REDIRECT$ | $\checkmark$ | 内核被告知使用另外的路径 | $rt_-msghdr$ | |
$RTM_-RESOLVE$ | $\checkmark$ | 请求把目的地址解析成链路层地址 | $rt_-msghdr$ |
通过路由套接字交换的结构有 $5$ 个类型。
struct rt_msghdr { // defined in <net/route.h>
u_short rtm_msglen; // to skip over non-understood messages
u_char rtm_version; // future binary compatibility
u_char rtm_type; // message type
u_short trm_index; // index for associated ifp
int rtm_flags; // flags, incl . kern &message, e.g., DONE
int rtm_addrs; // bitmask identifying sockaddrs in msg
pid_t rtm_pid; // identify sender
int rtm_seq; // for sender to identify action
int rtm_errno; // why failed
int rtm_use; // from rtentry
u_long rtm_inits; // which metrics we are initializing
struct rt_metricsrtm_rmx; // metrics themselves
};
struct if_msghdr { // defined in <net/if.h>
u_short ifm_msglen; // to skip over non-understood messages
u_char ifm_version; // future binary compatibility
u_char ifm_type; // message type
int ifm_addrs; // like rtm_addrs
int ifm_flags; // value of if_flags
u_short ifm_index; // index for associated ifp
struct if_data ifm_data; // statistics and other data about if
};
struct ifa_msghdr { // defined in <net/if.h>
u_short ifam_msglen; // to skip over non-understood messages
u_char ifam_version; // future binary compatibility
u_char ifam_type; // message type
int ifam_addrs; // like rtm_addrs
int ifam_flags; // value of ifa_flags
u_short ifam_index; // index for associated ifp
int ifam_metric; // value of ifa_metric
};
struct ifma_msghdr { // defined in <net/if.h>
u_short ifmam_msglen; // to skip over non-understood messages
u_char ifmam_version; // future binary compatibility
u_char ifmam_type; // message type
int ifmam_addrs; // like rtm_addrs
int ifmam_flags; // value of ifa_flags
u_short ifmam_index; // index for associated ifp
};
struct if_announcemsghdr { // defined in <net/if.h>
u_short ifan_msglen; // to skip over non-understood messages
u_char ifan_version; // future binary compatibility
u_char ifan_type; // message type
u_short ifan_index; // index for associated ifp
char ifan_name[IFNAMSIZ]; // if name, e.g. "en0"
u_short ifan_what; // what type of announcement
};
每个结构都有相同的前 $3$ 个成员:本消息的长度、版本和类型,类型是上表中第一列的常值之一,长度则允许应用程序跳过无法理解的消息类型。$rtm_-addr$ 、$ifm_-addrs$ 和 $ifam_-addrs$ 这 $3$ 个成员是数位掩码 ( $bit$ $mask$ ),指明本消息后跟的套接字地址结构是 $8$ 个可能选择中的哪几个。
数位掩码 | 数组下标 | 套接字地址结构包含 | ||
---|---|---|---|---|
常值 | 数值 | 常值 | 数值 | |
$RTA_-DST$ | $0x01$ | $RTAX_-DST$ | $0$ | 目的地址 |
$RTA_-GATEWAY$ | $0x02$ | $RTAX_-GATEWAY$ | $1$ | 网关地址 |
$RTA_-NETMASK$ | $0x04$ | $RTAX_-NETMASK$ | $2$ | 网络掩码 |
$RTA_-GENMASK$ | $0x08$ | $RTAX_-GENMASK$ | $3$ | 克隆掩码 |
$RTA_-IFP$ | $0x10$ | $RTAX_-IFP$ | $4$ | 接口名字 |
$RTA_-IFA$ | $0x20$ | $RTAX_-IFA$ | $5$ | 接口地址 |
$RTA_-AUTHOR$ | $0x40$ | $RTAX_-AUTHOR$ | $6$ | 重定向原创者 |
$RTA_-BRD$ | $0x80$ | $RTAX_-BRD$ | $7$ | 广播或点到点目的地址 |
$RTAX_-MAX$ | $8$ | 最大元素数目 |
上述常值定义在 <$net/route.h$> 头文件中,当存在多个套接字地址结构时,它们总是按照表中所示的顺序排列。
3. sysctl
使用 $sysctl$ 检查路由表和接口列表的进程不需要超级用户权限。
#include <sys/param.h>
#include <sys/sysctl.h>
// 成功返回0,出错返回-1
int sysctl(int *name, u_int namelen, void *oldp, size_t *oldlenp,
void *newp, size_t newlen);
这个函数使用类似简单网络管理协议 ( $Simple$ $Network$ $Management$ $Protocol$ , $SNMP$ ) 中管理信息库 ( $management$ $information$ $base$ , $MIB$ ) 的名字。$name$ 参数是一个整数数组,$namelen$ 指定数组长度。数组的第一个元素指定本请求定向到内核的哪个子系统,第二个及之后元素依次细化指定该子系统的某个部分。为了获取某个值,$oldp$ 参数指向一个供内核存放该值的缓冲区。$oldlenp$ 是一个值-结果参数,函数被调用时,该值表示缓冲区中的数据量,返回时表示内核向该缓冲区存放的数据量。如果缓冲区不够大,就会返回 $ENOMEM$ 错误。特别地,$oldp$ 可以是一个空指针而 $oldlenp$ 是非空指针,这样内核可以确定返回的数据量,并通过 $oldlenp$ 返回。$newp$ 指向一个大小为 $newlen$ 的缓冲区,代表设置新值。如果不想设置新值,那么 $newp$ 应该为空指针,$newlen$ 为 $0$ 。
通过 $sysctl$ ,我们可以获取各种系统信息,有文件系统、虚拟内存、内核限制等。我们感兴趣的是网络子系统,通过把 $name$ 数组的第一个元素设置为 $CTL_-NET$ 来指定。这些常值定义在 <$sys/sysctl.h$> 头文件中。第二个元素可以是:
- $AF_-INET$ :获取或设置影响网际网协议的变量,下一级为使用 $IPPROTO_-xxx$ 指定的具体协议;
- $AF_-LINK$ :获取或设置链路层信息,譬如
PPP
接口数量; - $AF_-ROUTE$ :返回路由表或接口列表的信息;
- $AF_-UNSPEC$ :获取或设置一些套接字层变量,譬如套接字发送或接收缓冲区的最大大小。
当 $name$ 数组的第二个元素为 $AF_-ROUTE$ 时,第三个元素 ( 协议号 ) 总是为 $0$ ,第四个元素是一个地址族,第五和第六指定做什么。
$name[\ ]$ | 返回IPv4 路由表 |
返回IPv4 ARP 高速缓存 |
返回IPv6 路由表 |
返回接口清单 |
---|---|---|---|---|
$0$ | $CTL_-NET$ | $CTL_-NET$ | $CTL_-NET$ | $CTL_-NET$ |
$1$ | $AF_-ROUTE$ | $AF_-ROUTE$ | $AF_-ROUTE$ | $AF_-ROUTE$ |
$2$ | $0$ | $0$ | $0$ | $0$ |
$3$ | $AF_-INET$ | $AF_-INET$ | $AF_-INET6$ | $0$ |
$4$ | $NET_-RT_-DUMP$ | $NET_-RT_-FLAGS$ | $NET_-RT_-DUMP$ | $NET_-RT_-IFLIST$ |
$5$ | $0$ | $RTF_-LLINFO$ | $0$ | $0$ |
路由域支持 $3$ 种操作,由 $name[4]$ 指定,这些常值定义在 <$sys/socket.h$> 头文件中。这 $3$ 种操作返回的信息通过 $oldp$ 指针返回,$oldp$ 指向的缓冲区中含有可变数目的 $RTM_-xxx$ 消息。
- $NET_-RT_-DUMP$ 返回由 $name[3]$ 指定地址族的路由表,如果指定地址族为 $0$ ,则返回所有地址族的路由表。
- 路由表作为可变数目的 $RTM_-GET$ 消息返回,每个消息后跟最多 $4$ 个套接字地址结构:路由表项的目的地址、网关、网络掩码和克隆掩码。相比于直接读写路由套接字的操作,$sysctl$ 操作所有改动仅仅体现在内核通过后者返回一个或多个 $RTM_-GET$ 信息;
- $NET_-RT_-FLAGS$ 返回由 $name[3]$ 指定的地址族的路由表,但是仅限于那些所带标志与由 $name[5]$ 指定的标志相匹配的路由表项。路由表中所有
ARP
高速缓存均设置了 $RTF_-LLINFO$ 标志;
- 这种操作返回的信息与上一种一致;
- $NET_-RT_-IFLIST$ 返回所有已配置接口的信息。如果 $name[5]$ 不为 $0$ ,它就是某个接口的索引号,仅仅返回该接口的信息。已赋予每个接口的所有地址也同时返回,不过如果 $name[3]$ 不为 $0$ ,那么仅限于返回指定地址族的地址;
- 每个接口的返回信息包括一个 $RTM_-IFINFO$ 消息和后跟零个或多个 $RTM_-NEWADDR$ 消息,其中每个 $RTM_-NEWADDR$ 消息对应已赋予该接口的一个地址。接在 $RTM_-IFINFO$ 消息首部之后的是一个数据链路套接字地址结构,接在每个 $RTM_-NEWADDR$ 消息首部之后的则是最多 $3$ 个套接字地址结构:接口地址、网络掩码和广播地址。
4. 接口名字和索引函数
#include <net/if.h>
// 成功返回接口索引,出错返回0
unsigned int if_nametoindex(const char *ifname);
// 成功返回接口名指针,出错返回NULL
char *if_indextoname(unsigned int ifindex, char *ifname);
// 成功返回非空指针,出错返回NULL
struct if_nameindex *if_nameindex(void);
void if_freenameindex(struct if_nameindex *ptr);
struct if_nameindex {
unsigned int if_index;
char *if_name; // null-terminated name
};
上面 $4$ 个函数用于需要描述一个接口的场合,并且是为IPv6
API
引入的,不过也适用于IPv4
API
。这里存在一个基本概念,即每个接口都有一个唯一的名字和一个唯一的索引 ( 从 $1$ 开始 )。$if_-nametoindex$ 返回名字为 $ifname$ 的接口的索引。$if_-indextoname$ 返回索引为 $ifindex$ 的接口的名字。$ifname$ 参数指向一个大小为 $IFNAMSIZ$ 的缓冲区,调用者必须分配这个缓冲区以保存结果,调用成功时这个指针也是函数的返回值。$if_-nameindex$ 返回一个 $if_-namindex$ 结构数组,数组最后一个元素的 $if_-index$ 为 $0$ 且 $if_-name$ 为 $NULL$ 。该数组本身以及数组中各个元素指向的名字所用的内存空间由该函数动态获取,然后由 $if_-freenameindex$ 函数释放。