UNIX网络编程卷一 学习笔记 第十八章 路由套接字

内核中的Unix路由表传统上一直使用ioctl函数访问,但没有ioctl函数请求能获取整个路由表,而netstat等程序通过读取内核的内存获取路由表内容。诸如gated等路由守护进程需要监视由内核收取的ICMP重定向消息(用于在IP网络中通知主机或路由器更优的路径,当路由器检测到数据包正在通过不是最佳路径的网关发送时,它可以发送ICMP重定向报文给源主机,以提供更好的路由选择),它们通常创建一个原始ICMP套接字,再在这个套接字上监听所有收到的ICMP消息。

4.3 BSD Reno对内核的路由子系统接口进行了整理,做法是创建了AF_ROUTE域访问内核路由子系统。路由域中唯一支持的套接字是原始套接字。路由套接字上支持三种操作:
1.进程可以写出到路由套接字而往内核发送消息。路径的增加和删除通过这种操作实现。

2.进程可通过从路由套接字读出而接收内核消息。内核采用这种操作通知进程已收到并处理一个ICMP重定向消息,或请求路由进程解析一个路径(当内核需要确定特定目标的最佳路由时,它可以向路由进程发送请求以获取解析)。

以上两种操作可使用同一个路由套接字,如进程通过写一个路由套接字往内核发送一个消息,请求内核提供关于某个给定路径的信息,又通过读这个路由套接字接收内核的应答。

3.进程可以使用sysctl函数获取所有路由表或已配置接口。

前两种操作需要超级用户权限,最后一种操作任何进程都可执行。

一些较新操作系统版本取消了打开路由套接字的超级用户权限要求,而只限制改动路由表的消息需要超级用户权限,这样任何进程都可以使用RTM_GET之类的消息来查找路径。

技术上说,第三种操作并非使用路由套接字执行的,而是使用通用的sysctl函数,但sysctl函数的输入参数之一是地址族,对于第三种操作来说这个参数为AF_ROUTE,且sysctl函数返回的信息与内核通过路由套接字返回的信息有相同格式。事实上,在4.4 BSD内核中,sysctl函数对AF_ROUTE地址族的处理是路由套接字代码的一部分。

sysctl函数首次出现在4.4 BSD中,但并非所有支持路由套接字的实现都提供sysctl函数,如AIX 5.1和Solaris 9。

通过路由套接字可返回数据链路套接字地址结构sockaddr_dl,它定义在头文件net/if_dl.h中:
在这里插入图片描述
每个接口都有一个唯一的正值索引,可返回该索引的方法:if_nametoindex函数和if_nameindex函数、第二十一章中的IPv6多播套接字选项、第二十七章中的IPv4和IPv6的一些高级套接字选项。

sdl_data成员含有名字和链路层地址(如以太网接口的48位MAC地址),名字从sdl_data[0]开始,且不以空字符结尾;链路层地址从sdl_data[sdl_nlen]开始。定义本结构的头文件net/if_dl.h中还定义了以下宏返回指向链路层地址的指针:

#define LLADDR(s) ((caddr_t)((s)->sdl_data + (s)->sal_nlen))

数据链路套接字地址结构是长度可变的,如果链路层地址和名字总长超出12字节,结构将大于20字节,在32位系统上,这个大小通常向上舍入到下一个4字节的倍数。在第二十二章中我们会看到,由IP_RECVIF套接字选项返回的sockaddr_dl结构中,所有3个长度成员都为0,从而根本没有sda_data成员。

创建一个路由套接字后,进程可通过写到该套接字向内核发送命令,通过读该套接字从内核接收信息。下图是路由域套接字的路由消息类型,其中5个可由进程发出,这些消息定义在头文件net/route.h中:
在这里插入图片描述
在这里插入图片描述
上图中的所有结构类型定义如下:
在这里插入图片描述
以上所有结构有相同的前3个成员:本消息的长度、版本、消息类型。长度成员允许应用跳过不理解的消息类型。消息类型成员是图18-2第一列中的常值之一。

rtm_addrs、ifm_addrs、ifam_addrs这三个成员是数位掩码(bit mask),指明本消息后跟的套接字地址结构是下图中8个可能的地址选择中的哪几个,下图中的常值定义在头文件net/route.h中:
在这里插入图片描述
当路由消息中存在多个套接字地址结构时,它们总是按上图所示顺序排列。

以下给出一个使用路由套接字的例子,我们的getrt程序从命令行参数取得一个IPv4点分十进制地址,并以这个地址向内核发送一个RTM_GET消息,内核在它的IPv4路由表中查找这个目的地址,并作为一个RTM_GET消息返回相应路由表项信息。如在主机freebsd上执行以下命令:
在这里插入图片描述
可以看到目的地址使用默认路径(默认路径在路由表中的目的IP地址为0.0.0.0,掩码为0.0.0.0)。下一跳路由器是12.106.32.1,即主机freebsd接入因特网的网关。

如果指定主机freebsd的第二个以太网接口所在子网为目的地址执行以下命令:
在这里插入图片描述
则目的地址就是网络本身。网关是外出接口,它作为一个sockaddr_dl结构返回,接口索引为2。

在给出getrt程序的源码前,我们先展示写到路由套接字的信息以及由内核返回的信息:
在这里插入图片描述
上图中,克隆掩码套接字地址结构在英文原版中是genmask socket address structure,而genmask与netmask的语义很相近,但我没有找到有关克隆掩码(cloning mask)的相关信息,此处作者用克隆掩码套接字含义可能是:这个套接字是网络掩码的克隆。以后再遇到克隆掩码套接字和genmask的时候再研究吧。

如上图,我们构造一个缓冲区,以一个rt_msghdr结构开头,后跟一个含有要查找的目的地址的套接字地址结构。rtm_type成员值为RTM_GET,rtm_addrs成员值为RTA_DST(表示套接字地址结构中含有目的地址)。getrt程序可用于任何内核为之提供路由表的协议族,因为待查找地址和协议族包含在套接字地址结构中,而套接字地址结构可兼容任意支持的协议族和地址。

把消息发送给内核后,我们读回应答,应答格式如上图右侧所示:一个rt_msghdr结构后跟最多4个套接字地址结构,这4个套接字地址结构中哪些会返回取决于路由表项,我们通过检查返回的rt_msghdr结构中的rtm_addrs成员的值得悉返回了哪些套接字地址结构。每个套接字地址结构的协议族可通过sa_family成员获得,如上例中第一个网关是一个IPv4套接字地址结构,第二个网关是一个数据链路套接字地址结构。

以下是我们的getrt程序要用到的头文件unproute.h:

#include "unp.h"
#include <net/if.h>	    /* if_msghdr{} */
#include <net/if_dl.h>    /* sockaddr_sdl{} */
#include <net/route.h>    /* RTA_xxx constants */
#include <sys/param.h>

#ifdef HAVE_SYS_SYSCTL_H
#include <sys/sysctl.h>    /* sysctl() */
#endif

/* function prototypes */
void get_rtaddrs(int, struct sockaddr *, struct sockaddr **);
char *net_rt_iflist(int, int, size_t *);
char *net_rt_dump(int, int, size_t *);
char *sock_masktop(struct sockaddr *, socklen_t);

/* wrapper functions */
char *Net_rt_iflist(int, int, size_t *);
char *Net_rt_dump(int, int, size_t *);
#define	Sock_masktop(a,b) sock_masktop((a), (b))

以下是getrt程序的源码:

#include "unproute.h"

/* sizeof (struct sockaddr_in6) * 8 = 224 */
// 该缓冲区能存放一个rt_msghdr结构和可能多达8个的套接字地址结构
// 既然1个IPv6套接字地址结构的大小是28字节,512字节足以存放最多达到8个的套接字地址结构
#define BUFLEN (sizeof(struct rt_msghdr) + 512)
#define SEQ 9999

int main(int argc, char **argv) {
    int sockfd;
    char *buf;
    pid_t pid;
    ssize_t n;
    struct rt_msghdr *rtm;
    struct sockaddr *sa, *rti_info[RTAX_MAX];
    struct sockaddr_in *sin;

    if (argc != 2) {
        err_quit("usage: getrt <IPaddress>");
    }

    sockfd = Socket(AF_ROUTE, SOCK_RAW, 0);    /* need superuser privileges */

    buf = Calloc(1, BUFLEN);    /* and initialized to 0 */

    rtm = (struct rt_msghdr *)buf;
    rtm->rtm_msglen = sizeof(struct rt_msghdr) + sizeof(struct sockaddr_in);
    rtm->rtm_version = RTM_VERSION;
    rtm->rtm_type = RTM_GET;
    rtm->rtm_addrs = RTA_DST;
    // 存放我们的进程ID和自选的序列号,我们会在读取的应答中匹配这些值,以寻找正确的应答
    rtm->rtm_pid = pid = getpid();
    rtm->rtm_seq = SEQ;

    // 紧跟rt_msghdr结构,我们构造一个sockaddr_in结构
    // 其中只包含要查找的目的IPv4地址、地址结构长度、地址族
    sin = (struct sockaddr_in *)(rtm + 1);
    sin->sin_len = sizeof(struct sockaddr_in);
    sin->sin_family = AF_INET;
    Inet_pton(AF_INET, argv[1], &sin->sin_addr);

    // 如果在write调用前禁止SO_USELOOPBACK套接字选项
    // 则内核不会把应答发给发送进程,它的默认设置是开启
    Write(sockfd, rtm, rtm->rtm_msglen);

    // 其他进程也可能打开路由套接字,且内核给所有路由套接字都传送一个消息副本
    // 因此我们需要检查消息的类型、序列号、进程ID,确保收到的是我们所期待的应答
    do {
        n = Read(sockfd, rtm, BUFLEN);
    }  while (rtm->rtm_type != RTM_GET || rtm->rtm_seq != SEQ ||
              rtm->rtm_pid != pid);

    rtm = (struct rt_msghdr *)buf;
    sa = (struct sockaddr *)(rtm + 1);
    // rtm->rtm_addrs是数位掩码,指出在rt_msghdr结构后的是8个可能的套接字地址结构中的哪些
    // get_rtaddrs函数稍后给出,它在rti_info表示的数组中填入指向相应套接字地址结构的指针
    get_rtaddrs(rtm->rtm_addrs, sa, rti_info);
    // 目的地址使用sock_ntop_host函数显示
    if ((sa = rti_info[RTAX_DST]) != NULL) {
        printf("dest: %s\n", Sock_ntop_host(sa, sa->sa_len));
    }
 
    // 网关地址使用sock_ntop_host函数显示
    if ((sa = rti_info[RTAX_GATEWAY]) != NULL) {
        printf("gateway: %s\n", Sock_ntop_host(sa, sa->sa_len));
    }

    // 以下两个掩码用sock_masktop函数显示
    if ((sa = rti_info[RTAX_NETMASK]) != NULL) {
        printf("netmask: %s\n", Sock_masktop(sa, sa->sa_len));
    }

    if ((sa = rti_info[RTAX_GENMASK]) != NULL) {
        printf("genmask: %s\n", Sock_masktop(sa, sa->sa_len));
    }

    exit(0);
}

如果图18-5中所有4个套接字地址结构都被内核返回,结果rti_info数组将如下图所示:
在这里插入图片描述
get_rtaddrs函数如下:

#include "unproute.h"

/*
 * Round up 'a' to next multiple of 'size', 'size' must be a power of 2
 * 只有size为2的幂次方时,size - 1的结果才是全1,才能用此宏把a向上舍入到size的整倍数
 * (a) & ((size) - 1))在size为2的幂时相当于计算a % size,如果余数为0,则直接返回a
 * 如果余数不为0,如果size - 1表示n个1,则把a的最右边n个2进制位全改为1,然后再加1
 */
#define ROUNDUP(a, size) (((a) & ((size) - 1)) ? (1 + ((a) | ((size) - 1)) : (a))

/*
 * Step to next socket address structure;
 * if sa_len is 0, assume it is sizeof(u_long).
 */
// 套接字地址结构是可变长度的,此处假设每个套接字地址结构都有一个指明自身长度sa_len成员
// 问题在于,网络掩码和克隆掩码套接字地址结构也可当作ap参数传入宏,此时sa_len成员的值可能为0
// sa_len成员为0时,掩码套接字地址结构实际只占用一个unsigned long大小,表示掩码每位都是0
// 每个套接字地址结构可在末尾填充字节,使得下一个结构从特定的边界开始
// 对于本例就是下一个结构要在unsigned long的整倍数边界处开始(即如果在32位体系结构中,就是4字节边界)
// sockaddr_in结构大小为16字节不需填充,而掩码套接字地址结构往往会在末尾填充字节(见下面的sock_masktop函数)
#define NEXT_SA(ap) ap = (SA *) \
    ((caddr_t)ap + (ap->sa_len ? ROUNDUP(ap->sa_len, sizeof(u_long)) : \
                                     sizeof(u_long)))
// caddr_t是一个已经过时的类型,在较早的C语言标准中使用
// 它是一个用于表示字符指针的类型,可以用于指向任意类型的字符数据
// 现代的C标准中不再定义或推荐使用caddr_t类型
// 应该使用更明确的指针类型,如char *或void *,来表示字符数据的指针
// u_long类型通常是通过typedef定义的

void get_rtaddrs(int addrs, SA *sa, SA **rti_info) {
    int i;

    // RTAX_MAX是内核在单个路由消息中能返回的套接字地址结构的最大数目
    // 本循环查看图18-4中8个RTA_xxx数位掩码常值中的每一个
    for (i = 0; i < RTAX_MAX; ++i) {
        // 如果某位被置,则rti_info数组中对应的元素就被设置为指向相应套接字地址结构的指针
        if (addrs & (1 << i)) {
		    rti_info[i] = sa;
		    NEXT_SA(sa);
		// 否则rti_info数组中对应元素被设置为空指针
		} else {
		    rti_info[i] = NULL;
		}
    }
}

sock_masktop函数返回通过路由套接字获取那两种掩码的表达字符串,掩码存放在套接字地址结构中,掩码的套接字地址结构的sa_family成员没有定义,但对于32位的IPv4掩码其sa_len成员值可能取0、5、6、7、8,当这个长度大于0时,真正的掩码离起点的偏移和IPv4地址在在sockaddr_in结构中离起点的偏移一样,都是4字节,即通用套接字地址结构的sa_data[2]成员(通用套接字地址结构大小为16字节,由3个成员,第一个成员是1字节的sa_len,表示套接字地址结构的长度,第二个成员是1字节的sa_family,表示地址族,第三个是sa_data[14],存放具体每个类型的套接字地址中的内容):

#include "unproute.h"

const char *sock_masktop(SA *sa, socklen_t salen) {
    static char str[INET6_ADDRSTRLEN];
    unsigned char *ptr = &sa->sa_data[2];

    // 如果长度为0,隐含着掩码的值为0.0.0.0
    if (sa->sa_len == 0) {
        // 返回指向静态存储区的字符串的指针
        return "0.0.0.0";
    // 如果长度为5,由于真正的掩码离起点的偏移为4字节,因此只有1字节表示掩码,隐含着剩下3字节为0
    } else if (sa->sa_len == 5) {
        snprintf(str, sizeof(str), "%d.0.0.0", *ptr);
    } else if (sa->sa_len == 6) {
        snprintf(str, sizeof(str), "%d.%d.0.0", *ptr, *(ptr + 1));
    } else if (sa->sa_len == 7) {
        snprintf(str, sizeof(str), "%d.%d.%d.0", *ptr, *(ptr + 1), *(ptr + 2));
    } else if (sa->sa_len == 8) {
        snprintf(str, sizeof(str), "%d.%d.%d.%d", *ptr, *(ptr + 1),
	          *(ptr + 2), *(ptr + 3));
    } else {
        snprintf(str, sizeof(str), "(unknown mask, len = %d, family = %d)",
	          sa->sa_len, sa->sa_family);
    }

    return str;
}

通常write函数发送到路由套接字的返回值会告诉我们发送给内核的命令是否执行成功,如果我们只需要知道命令是否执行成功,可在打开路由套接字后先立即以SHUT_RD为第二个参数调用shutdown,防止内核发送应答。如果我们是在删除一个路由表项,write函数返回0表示删除成功,返回ESRCH错误说明内核找不到这个路径。当增加一个路由表项时,write函数返回EEXIST错误说明该表项已存在。在上例中,如果我们要查找的目的地址的路由表项不存在,write函数将返回ESRCH错误。

创建路由套接字(AF_ROUTE域的原始套接字)需要超级用户权限,但使用sysctl函数检查路由表和接口列表却不限用户权限:
在这里插入图片描述
sysctl函数使用看起来像SNMP(Simple Network Management Protocol,简单网络管理协议)中的管理信息库(MIB)的名字,这些名字是分层结构的。

name参数是指定名字的一个整数数组,namelen参数指定该数组中的元素数目,该数组中第一个元素指定本请求定向到内核的哪个子系统,第二个及其后元素逐次细化指定该子系统的某个部分。下图展示前3级使用的常值:
在这里插入图片描述
为了获取某个值,oldp参数指向一个供内核存放该值的缓冲区。oldlenp参数是一个值-结果参数:函数被调用时,oldlenp参数是oldp参数指向的缓冲区的大小;函数返回时,该值给出内核存放在该缓冲区中的数据量。如果缓冲区太小,sysctl函数返回ENOMEM错误。oldp参数可以是空指针且oldlenp参数为非空指针,内核会确定这样的调用应返回的数据量,并通过oldlenp参数返回这个大小。

为了设置某个值,newp参数指向一个大小为newlen参数值的缓冲区。如果不准备设置值,则newp参数应为空指针,newlen参数应为0。

sysctl的手册页面描述了该函数可获取的系统信息,有文件系统、虚拟内存、内核限制、硬件等各方面信息,我们感兴趣的是网络子系统,通过把name参数数组的第一个元素设为CTL_NET来指定网络子系统。CTL_xxx常值定义在sys/sysctl.h头文件中。name数组的第一个元素为CTL_NET时,第二个元素可以是以下几种:
1.AF_INET:获取或设置影响网际网协议的变量,下一级使用IPPROTO_xxx常值指定具体协议。FreeBSD 5.0在AF_INET级下提供了大约75个变量,用于控制诸如内核是否应产生ICMP重定向、TCP是否应使用RFC 1323选项(RFC 1323主要介绍了一些TCP协议的扩展机制,旨在提高TCP协议在高速、高延迟网络环境下的性能和效率,如TCP窗口扩展、RTT测量、时间戳选项、SACK选项(SACK通过允许接收方向发送方提供有关已接收和未接收数据块的详细确认信息,这样发送方就可以准确知道哪些数据块需要重传,而不是重传已经成功接收的数据块,从而提高传输效率))、UDP校验和是否应发送等特性。

2.AF_LINK:获取或设置链路层信息,如PPP接口数目。

3.AF_ROUTE:返回路由表和接口列表信息。

4.AF_UNSPEC:获取或设置一些套接字层变量,如套接字发送或接收缓冲区的最大大小。

当name数组的第二个元素为AF_ROUTE时,第三个元素是协议号,协议号总是为0,因为AF_ROUTE族不像AF_INET族那样其中有协议;第四个元素是一个地址族,第五和第六个元素指定做什么:
在这里插入图片描述
sysctl函数的路由域指定3种操作,由name[4]指定(NET_RT_xxx常值定义在头文件sys/socket.h中),这三种操作返回的信息通过sysctl函数的oldp参数指针返回。oldp参数指向的缓冲区中会有可变数目的RTM_xxx消息。这3种操作如下:
1.NET_RT_DUMP:返回由name[3]指定的地址族的路由表,如果指定的地址族为0,则返回所有地址族的路由表。

路由表作为可变数目的RTM_GET消息返回,每个消息后面最多4个套接字地址结构:本路由表项的目的地址、网关、网络掩码、克隆掩码。相比直接读写路由套接字只能获取一个RTM_GET消息,sysctl函数可返回一个或多个RTM_GET消息。

2.NET_RT_FLAGS:返回由name[3]指定的地址族的路由表,但仅限于所带标志(若干个RTF_xxx常值的逻辑或)与name[5]指定的标志相匹配的路由表项。路由表中所有ARP相关表项都设置了RTF_LLINFO标志位,RTF_LLINFO标志位表示该路由表条目具有链路层信息,该条目通常用于存储与ARP相关的信息,以便在需要时进行地址解析。

本操作返回的信息格式与1中的相同。

3.NET_RT_IFLIST:返回所有已配置接口的信息,如果name[5]不为0,则它就是接口的索引号,于是仅仅返回该接口的信息。赋予接口的所有地址都要返回,但如果name[3]不为0,则仅返回指定地址族的地址。

获取接口信息时,每个接口的返回信息包括一个RTM_IFINFO消息和后跟的零个或多个RTM_NEWADDR消息,其中每个RTM_NEWADDR消息对应一个赋予该接口的地址。跟在RTM_IFINFO消息首部之后的是一个数据链路套接字地址结构,接在每个RTM_NEWADDR消息首部之后的是最多3个套接字地址结构:接口地址、网络掩码、广播地址:
在这里插入图片描述
上图中下面的那个if_msghdr结构应该是ifa_msghdr结构。

以下是使用sysctl函数判断UDP校验和是否开启,有些UDP应用(如BIND(Berkeley Internet Name Domain),它是一个开源的、常用的域名系统(DNS)软件套件)在启动时检查UDP校验和是否开启,若没有则尝试开启(开启需要超级用户权限),但本例中只检查该特性是否开启:

#include "unproute.h"
#include <netinet/udp.h>
#include <netinet/ip_var.h>
#include <netinet/udp_var.h>    /* fror 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;
    mid[3] = UDPCTL_CHECKSUM;
    len = sizeof(val);
    Sysctl(mib, 4, &val, &len, NULL, 0);
    // 获取的val为0(未开启)或1(开启)
    printf("udp checksum flag: %d\n", val);
    exit(0);
}

第十七章中我们编写了使用ioctl函数的SIOCGIFCONF请求实现的get_ifi_info函数,该函数返回所有up状态的接口,下面给出使用sysctl函数实现的get_ifi_info函数。

首先给出函数net_rt_iflist,该函数以NET_RT_IFLIST命令调用sysctl获取指定地址族的接口列表:

#include "unproute.h"

char *net_rt_iflist(int family, int flags, size_t *lenp) {
    int mib[6];
    char *buf;

    mib[0] = CTL_NET;
    mib[1] = AF_ROUTE;
    mib[2] = 0;
    mib[3] = family;    /* only addresses of this family */
    mib[4] = NET_RT_IFLIST;
    mib[5] = flags;    /* interface index or 0 */
    // 第一次调用sysctl,目的是获取存放所有接口信息所需缓冲区大小
    if (sysctl(mib, 6, NULL, lenp, NULL, 0) < 0) {
        return NULL;
    }

    if ((buf = malloc(*lenp)) == NULL) {
        return NULL;
    }
    // 第二次调用sysctl,由于路由表的大小和数目可能在两次sysctl
    // 调用之间发生变化,第一次调用返回的缓冲区大小实际含有10%的余量因子
    if (sysctl(mib, 6, buf, lenp, NULL, 0) < 0) {
        free(buf);
		return NULL;
    }

    return buf;
}

以下是使用sysctl函数实现的get_ifi_info函数:

#include "unpifi.h"
#include "unproute.h"

struct ifi_info *get_ifi_info(int family, int doaliases) {
    int flags;
    char *buf, *next, *lim;
    size_t len;
    struct if_msghdr *ifm;
    struct ifa_msghdr *ifam;;
    struct sockaddr *sa, *rti_info[RTAX_MAX];
    struct sockaddr_dl *sdl;
    struct ifi_info *ifi, *ifisave, *ifihead, **ifipnext;

    buf = Net_rt_iflist(family, 0, &len);

    ifihead = NULL;
    ifipnext = &ifihead;

    lim = buf + len;
    // 遍历由sysctl函数填写到缓冲区中的每个路由消息
    // ifm_msglen的长度或者是一个RTM_IFINFO的长度或者是一个RTM_NEWADDR的长度,见图18-13
    // ifm_msghdr和ifma_msghdr结构的前几个成员是相同的(见图18-3)
    // 因此我们此处访问ifm_msglen成员是没问题的,这是两个结构的相同成员
    // 下面即将访问ifm_type成员同理也没问题
    for (next = buf; next < lim; next += ifm->ifm_msglen) {
        ifm = (struct if_msghdr *)next;
        // sysctl函数为每个接口返回一个RTM_IFINFO结构
	    if (ifm->ifm_type == RTM_IFINFO) {
			if (((flags = ifm->ifm_flags) & IFF_UP) == 0) {
			    continue;    /* ignore if interface not up */
			}
		
		    // sa指向if_msghdr结构后的第一个套接字地址结构(数据链路套接字地址结构)
			sa = (struct sockaddr *)(ifm + 1);
			// 初始化rti_info数组,该数组结构如图18-8
			get_rtaddrs(ifm->ifm_addrs, sa, rti_info);
			// RTAX_IFP下标对应的套接字地址结构中包含接口名信息
			if ((sa = rti_info[RTAX_IFP]) != NULL) {
			    ifi = Calloc(1, sizeof(struct ifi_info));
			    *ifipnext = ifi;    /* prev points to this new one */
		        ifipnext = &ifi->ifi_next;    /* ptr to next one goes here */
		
		        ifi->ifi_flags = flags;
		        // 我们期望套接字地址结构的地址族为AF_LINK
				if (sa->sa_family == AF_LINK) {
				    sdl = (struct sockaddr_dl *)sa;
				    // 把接口索引保存到ifi_index成员
				    ifi->ifi_index = sdl->sdl_index;
				    // 如果有接口名,把接口名复制到ifi_name成员
		            if (sdl->sdl_nlen > 0) {
		                // IFI_NAME值为16,与头文件<net/if.h>中的IFNAMSIZ相同
		                // 头文件<net/if.h>中的IFNAMSIZ表示网络接口的名称的最大长度
		                snprintf(ifi->ifi_name, IFI_NAME, "%*s", sdl->sdl_nlen, 
		                         &sdl->sdl_data[0]);
		            // 否则将接口索引字符串作为接口名
		            } else {
		                snprintf(ifi->ifi_name, IFI_NAME, "index %d", sdl->sdl_index);
		            }   
		    
		            // 如果有硬件地址,则将其复制到ifi_haddr成员
		            if ((ifi->ifi_hlen = sdl->sdl_alen) > 0) {
		                memcpy(ifi->ifi_haddr, LLADDR(sdl), min(IFI_HADDR, sdl->sdl_alen));
		            }   
				}
		    }
		// sysctl函数为当前接口的每个已配置地址返回一个RTM_NEWADDR消息
		// 该消息中包含主地址和所有别名地址
		} else if (ifm->ifm_type == RTM_NEWADDR) {
		    // 如果当前接口在其ifi_info结构中的IP地址已经填写(即ifi_addr成员已填写)
		    // 我们就知道当前处理的是别名地址
		    if (ifi->ifi_addr) {    /* already have an IP addr for i/f */
		        if (doaliases == 0) {
				    continue;
				}
	
				/* we have a new IP addr for existing interface */
				// 到此调用者想要得到别名地址
				ifisave = ifi;
				// 为别名地址分配一个ifi_info结构
				ifi = Calloc(1, sizeof(struct ifi_info));
				*ifipnext = ifi;    /* prev points to this new one */
				ifipnext = &ifi->ifi_next;    /* ptr to next one goes here */
				// 复制已经填写的字段
				ifi->ifi_flags = ifisave->ifi_flags;
				fif->ifi_index = ifisave->ifi_index;
				ifi->ifi_hlen = ifisave->ifi_hlen;
				memcpy(ifi->ifi_name, ifisave->ifi_name, IFI_NAME);
				// IFI_HADDR值为8,ifi_haddr成员存放物理地址,实际以太网地址为6字节
				// 此处为8是为了兼容未来的64-bit EUI-64物理地址
				memcpy(ifi->ifi_haddr, ifisave->ifi_haddr, IFI_HADDR);
		    }
	
	        // 此处的next原本被用作if_msghdr结构,现用作ifa_msghdr结构
	        // 到此处我们确定next指向的实际是ifa_msghdr结构
		    ifam = (struct ifa_msghdr *)next;
		    sa = (struct sockaddr *)(ifam + 1);
		    get_rtaddrs(ifam->ifam_addrs, sa, rti_info);
	
	        // 填入当前处理的接口的主地址
		    if ((sa = rti_info[RTAX_IFA]) != NULL) {
		        ifi->ifi_addr = Calloc(1, sa->sa_len,);
				memcpy(ifi->ifi_addr, sa, sa->sa_len);
		    }
	
	        // 如果接口支持广播,就返回其广播地址
		    if ((flags & IFF_BROADCAST) && (sa = rti_info[RTAX_BRD]) != NULL) {
		        ifi->ifi_braddr = Calloc(1, sa->sa_len);
				memcpy(ifi->ifi_brdaddr, sa, sa->sa_len);
		    }
	
	        // 如果当前接口是点对点接口,就返回其目的地址
	        // 下标RTAX_BRD与上面获取广播地址时的一样,它同时也表示点对点接口的目的地址
		    if ((flags & IFF_POINTOPOINT) && (sa = rti_info[RTAX_BRD]) != NULL) {
		        ifi->ifi_dstaddr = Calloc(1, sa->sa_len);
				memcpy(ifi->ifi_dstaddr, sa, sa->sa_len);
	        }
		} else {
		    err_quit("unexpected message type %d", ifm->ifm_type);
		}
    }

    /* "ifihead" points to the first structure in the linked list */
    return ifihead;    /* ptr to first structure in linked list */
}

RFC 3493中定义了4个处理接口名和接口索引的函数,它们是为IPv6 API引入的,但也适用于IPv4 API。每个接口都有一个唯一的名字和一个唯一的正值索引。以下是这4个函数:
在这里插入图片描述
if_nametoindex函数返回名字为ifname参数的接口的索引。if_indextoname函数返回索引为参数ifindex的接口的名字,ifname参数指向一个大小为IFNAMSIZ(该常值定义在net/if.h头文件中)的缓冲区,调用者必须分配这个缓冲区以保存结果,调用成功时该缓冲区指针也是函数的返回值。

if_nameindex函数返回一个指向if_nameindex结构数组的指针,该结构定义如下:
在这里插入图片描述
该数组最后一个元素的if_index成员为0,if_name成员为空指针。该数组本身及数组中各个元素指向的名字所用内存由该函数动态获取,然后由if_freenameindex函数归还给系统。

下面使用路由套接字给出这4个函数的一个实现。首先是if_nametoindex函数:

#include "unpihi.h"
#include "unproute.h"

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;

    // 获取接口列表
    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) {
        ifm = (struct if_msghdr *)next;
        // 仅查找RTM_IFINFO信息,每个接口都会返回一个RTM_IFINFO信息
        // 其中包含一个数据链路套接字地址结构,可获取接口名、接口索引、硬件地址
		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;
}

以下是if_indextoname函数:

#include "unpifi.h"
#include "unproute.h"

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;

    // net_rt_iflist函数的第二个参数为期望的索引,因此buf中应该只含有期望的接口的信息
    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函数:

#include "unpifi.h"
#include "unproute.h"

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 *nameptr;

    if ((buf = net_rt_iflist(0, 0, &len)) == NULL) {
        return NULL;
    }

    // result用于存放将返回给调用者的if_nameindex结构数组
    // 此处分配的内存大小是一个过高的估计,如前所述,net_rt_iflist函数返回的len参数约有10%的余量
    // 这样比遍历两次接口列表简单:一次统计接口数和名字总大小,另一次为了填写信息
    // 我们在该缓冲区的开头正向构建if_nameindex数组,从末尾反向存放接口名
    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) {
        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函数:

void if_freenameindex(struct if_nameindex *ptr) {
    // 由于我们在if_nameindex函数中把结构数组和名字都存在同一缓冲区,只需一次free调用
    // 如果我们在if_nameindex函数中对每个名字都调用malloc
    // 那么为了释放空间,我们需要遍历整个数组,先释放每个名字的内存,再释放数组本身内存
    free(ptr);
}

sockaddr_dl结构(数据链路套接字地址结构)是可变长度的。

对于一个名为eth10且链路层地址是一个IEEE EUI-64地址的接口而言,它的数据链路套接字地址结构中的sdl_nlen成员(表示接口名)将是5,sdl_alen成员(表示物理地址)将是8,整个sockaddr_dl结构需要21字节(sdl_data成员中含有接口名和链路层地址,共13字节,见图18-1),在32位体系结构上会向上舍入成24字节。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
UNIX网络编程1 套接字联网API》是一本由W. Richard Stevens所著的经典图书。该书系统地介绍了UNIX操作系统上的套接字编程技术。 套接字UNIX网络编程中的核心概念之一,它提供了一种通信机制,使得不同主机间的进程可以进行数据的传输和交换。本书的主要内容包括网络编程基础知识、套接字编程的基本操作、传输层协议(TCP、UDP)的使用以及网络编程的高级主题,如进程间通信、多线程编程等。 本书共分为24个章节,每个章节都深入浅出地解释了UNIX套接字编程的各个方面。作者通过丰富的示例代码、清晰的图解和详细的解释帮助读者理解并掌握套接字编程的技巧和实践。 《UNIX网络编程1 套接字联网API》具有以下特点: 1. 详尽全面:书中对UNIX网络编程的各个方面进行了详细的介绍,从基础知识到高级主题,都有所涉及,对读者来说是一本全面系统的参考书。 2. 实用性强:书中的示例代码贴近实际应用场景,读者可以通过实践演练快速掌握套接字编程的技能,并了解如何解决实际网络编程中的常见问题。 3. 经典权威:作者W. Richard Stevens是UNIX网络编程领域的权威专家,他在书中融入了自己多年的经验和理论研究成果,使得本书成为了套接字编程领域的经典之作。 《UNIX网络编程1 套接字联网API》是一本经典可贵的学习资料,它对UNIX套接字编程提供了系统而丰富的介绍,既适用于初学者入门学习,也适合有经验的开发人员作为参考手册使用。无论是从事网络编程开发的工程师,还是对UNIX网络编程感兴趣的技术爱好者,都会从该书中获得丰厚的知识收益。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值