一.IP地址
一个IP地址是被指派给一个系统中的网络接口的而不是系统本身,就如在《TCP/IP实现(二) 接口层数据结构》中描述的那样,每一个用于存储IP协议地址信息的in_ifaddr结构都和一个描述接口信息的ifnet(或者是他的专用化)相关联。
IPv4地址分为5类:
二.地址指派
三.ioctl系统调用
ioctl函数可以用于获取接口信息,访问路由表,访问ARP高速缓存等,一般将那些不适合归于其它类别的特性接口放入该函数中。在本节中我们只讨论与接口信息相关的使用与实现。
1.ioctl函数的使用
1)获取所有接口的所有地址信息和接口名SIOCGIFCONF
通过给ioctl函数传递参数SIOCGIFCONF可以获取接口的名称和地址信息,此外还需要一个套接字描述符和一个ifconf结构体,该结构体的定义如下:
// 该结构体其实是对一个缓冲区的封装,该缓冲区用于存放ifreq结构
struct ifconf {
int ifc_len; // 初始化时设置为缓冲区buf的长度,当调用完ioctl函数后为缓冲区中的数据长度
union {
char* buf; // 在开辟空间时使用
struct ifreq *ifcu_req; // 在遍历缓冲区时使用
}ifc_ifcu;
#define ifc_buf ifc_ifcu.ifcu_buf
#define ifc_req ifc_ifcu.ifcu_req
};
struct ifreq {
#define IFHWADDRLEN 6
#define IFNAMSIZ IF_NAMESIZE
union {
char ifrn_name[IFNAMSIZ]; //接口名称,比如:le0
} ifr_ifrn;
union { // 注意以下都是共用体,因此一个ifreq只能保存以下信息中的一个
struct sockaddr ifru_addr; //接口地址信息
struct sockaddr ifru_dstaddr;
struct sockaddr ifru_broadaddr;
struct sockaddr ifru_netmask;
struct sockaddr ifru_hwaddr;
short int ifru_flags;
int ifru_ivalue;
int ifru_mtu;
struct ifmap ifru_map;
char ifru_slave[IFNAMSIZ]; /* Just fits the size */
char ifru_newname[IFNAMSIZ];
__caddr_t ifru_data;
} ifr_ifru;
};
使用方法如下:
void get_ifi_info(){
int sockfd;
int lastlen = 0, len;
char *buf;
ifconf ifc;
ifreq* ifrp;
sockfd = socket(AF_INET, SOCK_DGRAM, 0);
len = 100 * sizeof(struct ifreq);
// 获取接口列表
// - 由于起初并不知道要为接收接口信息的缓存ifconf 开辟多大的空间,因此只能采用探测的方法
// - 先调用ioctl,记录下缓冲区中的数据长度,再次用更大的缓冲区去调用ioctl,只有两次数据长度相等才说明缓冲区够大
while(1) {
buf = new char[len];
ifc.ifc_len = len;
ifc.ifc_buf = buf;
if(ioctl(sockfd, SIOCGIFCONF, &ifc) < 0) { // 调用ioctl将接口信息存入ifconf指示的缓存
if(errno != EINVAL || lastlen != 0) {
exit(errno);
}
}
else {
if(ifc.ifc_len == lastlen)
break;
lastlen = ifc.ifc_len;
}
len += 10 * sizeof(struct ifreq);
delete buf;
}
// 遍历存有接口信息的缓冲区
int addrlen;
sockaddr_in *addr4;
sockaddr_in6 *addr6;
char addrBuf[30];
for(ifrp = ifc.ifc_req; (char*)ifrp < buf + ifc.ifc_len; ifrp++){
switch(ifrp->ifr_addr.sa_family ) { // 判断地址簇类型
case AF_INET6:
addr6 = reinterpret_cast<sockaddr_in6 *>(&ifrp->ifr_addr);
inet_ntop(AF_INET6, &addr6->sin6_addr, addrBuf,30);
addrlen = sizeof(struct sockaddr_in6);
break;
case AF_INET:
addr4 = reinterpret_cast<sockaddr_in *>(&ifrp->ifr_addr);
inet_ntop(AF_INET, &addr4->sin_addr, addrBuf,30);
default:
addrlen = sizeof(struct sockaddr_in);
break;
}
std::cout<<ifrp->ifr_name << " " << addrBuf <<std::endl; // 打印接口名和接口地址
}
}
2)获取及设置接口标志(SIOCGIFFLAGS,SIOCSIFFLAGS)
接口标志ifnet中的if_flag用于表明接口的操作状态和属性,一个进程可以查看所有标志,但下图中标为内核专用的不可改变
查看接口标志的命令是:SIOCGIFFLAGS
设置接口标志的命令是:SIOCSIFFLAGS
对于这两个命令需要通过ifreq参数传入接口名来指定接口,从而获取指定接口的标志,若接口不存在则将errno设置为ENXIO。
使用方式如下:
void get_if_flags()
{
struct ifreq ifq;
strcpy(ifq.ifr_name, "ens33"); // 设置要查询的接口名,如“lo”
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
if(ioctl(sockfd, SIOCGIFFLAGS, &ifq) < 0) { // 将设置了接口名的ifreq 作为参数传入ioctl
exit(errno);
}
int flags = ifq.ifr_flags;
if( (flags & IFF_UP) == 0) { // 该接口是否打开
std::cout<<"IFF DOWN";
return;
}
if(flags & IFF_BROADCAST) { // 该接口是否用于广播网
std::cout<<"ifnet be used to broadcast"<<std::endl;;
}
if(flags & IFF_LOOPBACK) { // 该接口是否用于环回网络
std::cout<<"ifnet be used to LOOPBACK"<<std::endl;;
}
}
3)获取和设置接口的IP地址,网络掩码等(SIOCGIFADDR,SIOCSIFADDR,...)
意该命令所指定的socket必须支持传入地址的地址簇类,即你不能通过一个UDP套接字配置一个OSI地址。
设置接口IP(SIOCSIFADDR):
若该接口还未设置IP地址,则为其分配一个新的IP地址,若已有IP地址则将其第一个IP地址改变为用户设置的IP。若该过程中IP设置失败,则将接口回滚回原先的IP。
获取接口IP(SIOCGIFADDR):
该命令将IP地址存入用户指定的ifreq中,以便返回给用户。
这里只对设置接口IP进行举例说明:
void set_ifi_addr()
{
struct ifreq ifq;
strcpy(ifq.ifr_name,"ens33"); // 通过接口名指定接口
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
std::string ip = "10.1.16.9";
sockaddr_in *addr = (sockaddr_in*)&(ifq.ifr_addr);
addr->sin_family = AF_INET;
::inet_pton(AF_INET, ip.c_str(), &addr->sin_addr);
if(ioctl(sockfd, SIOCSIFADDR, &ifq) < 0) { // 设置指定接口的IP
std::cout<<"errno"<<errno<<std::endl;
}
}
2.ioctl系统调用的实现
1)SIOCGIFCONF(获取各个接口的名称和全部地址)命令的实现
ioctl函数转而调用ifconf函数来获取接口名和为其配置的地址信息。该函数首先遍历接口链表ifnet,将接口的名称复制到一个局部的ifreq中,接着遍历该接口ifnet的地址链表,将地址信息也保存到局部ifreq中,若无地址信息则将ifreq的ifru_addr成员清0,最后将该局部ifreq复制到用户提供的缓存中。其简要伪代码如下:
ifreq ifr; // 局部ifreq
// 遍历接口链表,并判断用户提供的剩余空间是否足够大
for(ifnet *ifp = ifnet; ifp && space > sizeof(ifreq); ifp = ifp->if_next) {
拷贝接口名到ifr ->ifr_name
if(ifp->if_addrlist == 0) {// 若接口的地址链表为空
bzero(ifr->ifru_adr);//清空局部ifreq的地址信息
将ifr拷贝至用户缓存;
}
else {
for(遍历接口的地址链表) {
拷贝地址信息到局部ifreq;
将ifr拷贝至用户缓存;
}
}
}
参照图:
2)SIOCGIFFLAGS(获取接口标志)和SIOCSIFFLAGS(设置接口标志)命令的实现
对于获取接口标志的命令SIOCGIFFLAGS,ioctl函数根据提供的接口名查找到相应接口,并将接口中的标志信息ifnet的if_flags复制到用户提供的ifreq中。
而对于设置接口标志的命令SIOCSIFFLAGS,ioctl函数还会调用suser函数检查进程的权限,只有进程拥有超级用户权限才可以修改接口标志,之后将用户传入的flags和~IFF_CANTCHANCE进行与操作,其意图在于去除那些不能被进程改变的标志,最后调用相应函数设置接口标志。
3) SIOCSIFADDR(设置接口IP)等命令的实现
在调用ioctl函数后回进行如下伪代码操作:
for(in_ifaddr* ia = in_ifaddr; ia; ia = ia->ia->next){ //遍历整个IP地址链表(in_ifaddr结构)
// 若该IP地址关联的接口是要找的接口
if(ia->ia_ifp == ifp) // ia_ifp 为协议地址结构对接口信息结构ifnet的回指
break;
}
switch(cmd) // 命令分类
{
case SIOCSIFADDR:
case SIOCSIFNETMASK:
case SIOCSIFDSTADDR:
if( (so->so_state & SS_PRIV) == 0) // 若不是超级用户进程创建的套接字
return EPERM; // 进程能够创建一个套接字,并放弃它的超级用户权限,仍可发送有特权的ioctl命令。
if(ia == (struct in_ifaddr *)0) {
分配一个新的in_ifaddr结构,并进行初始化,但不填入地址信息
}
};
switch(cmd)
{
case SIOCSIFADDR:
调用in_ifinit函数将地址信息填入找到的第一个IP地址结构或是上面新创建的地址结构中
若失败则回滚到旧值
break;
case SIOCSIFNETMASK:
...
break;
...
}
注意设置IP地址时,最后会执行ifp->if_flags |= IFF_UP。即将接口设为运行状态。