虚拟网卡
Tun/Tap
较高版本的linux大都自带了tun/tap 驱动程序用以实现虚拟网卡的功能,tun表示虚拟的是点对点设备,tap表示虚拟的是以太网设备,这两种设备针对网络包实施不同的封装。利用tun/tap 驱动,可以将tcp/ip协议栈处理好的网络分包传给任何一个使用tun/tap驱动的进程,由进程重新处理后再发到物理链路中。
Tun/Tap驱动程序工作原理
做为虚拟网卡驱动,Tun/Tap驱动程序的数据接收和发送并不直接和真实网卡打交道,他在Linux内核中添加了一个TUN/TAP虚拟网络设备的驱动程序和一个与之相关连的字符设备 /dev/net/tun,字符设备tun作为用户空间和内核空间交换数据的接口。当内核将数据包发送到虚拟网络设备时,数据包被保存在设备相关的一个队列中,直到用户空间程序通过打开的字符设备tun的描述符读取时,它才会被拷贝到用户空间的缓冲区中,其效果就相当于,数据包直接发送到了用户空间。通过系统调用write发送数据包时其原理与此类似。
在linux下,要实现内核空间和用户空间数据的交互,有多种方式:
(1)可以通用socket创建特殊套接字,利用套接字实现数据交互;
(2)通过proc文件系统创建文件来进行数据交互;
(3)还可以使用设备文件的方式,访问设备文件会调用设备驱动相应的例程,设备驱动本身就是内核空间和用户空间的一个接口,Tun/tap驱动就是利用设备文件实现用户空间和内核空间的数据交互。 从结构上来说,Tun/tap驱动并不单纯是实现网卡驱动,同时它还实现了字符设备驱动部分。以字符设备的方式连接用户空间和内核空间。
Tun/tap 驱动程序中包含两个部分,一部分是字符设备驱动,还有一部分是网卡驱动部分。利用网卡驱动部分接收来自TCP/IP协议栈的网络分包并发送或者反过来将接收到的网络分包传给协议栈处理, 而字符驱动部分则将网络分包在用户空间和内核空间之间传送,模拟物理链路的数据接收和发送。Tun/tap驱动很好的实现了两种驱动的结合。
创建
创建虚拟网卡分两步,先生成一个这样tun设备:
int tun_open(const char * dev)
{
struct ifreq ifr;
int fd;
assert(dev != NULL);
fd = open("/dev/net/tun", O_RDWR);
if (fd < 0) {
perror("open");
return -1;
}
memset(&ifr, 0, sizeof(struct ifreq));
if (*dev)
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
//ifr.ifr_flags |= IFF_TAP; /* 以太网设备 */
ifr.ifr_flags |= IFF_TUN; /* 点对点设备 */
if (ioctl(fd, TUNSETIFF, (void *)&ifr) < 0)
{
close(fd);
return -2;
}
/* 进程退出依旧保留网卡 1 */
if(ioctl(fd, TUNSETPERSIST, 1) < 0) {
printf("remain tun fail\n");
}
if (strcmp(ifr.ifr_name, dev) != 0)
printf("tun name: %s\n", ifr.ifr_name);
return fd;
}
然后还要可以设置需要的参数,如ip/mask、监听模式等:
int tun_setup(const char * dev, struct sockaddr_in * addr)
{
struct ifreq ifr;
int fd;
if (NULL == dev || NULL == addr)
return -1;
fd = socket(AF_INET, SOCK_DGRAM, 0);
if (fd < 0) {
perror("socket");
goto err;
}
memset(&ifr, 0, sizeof(struct ifreq));
strncpy(ifr.ifr_name, dev, IFNAMSIZ);
/* get flag */
if (ioctl(fd, SIOCGIFFLAGS, &ifr) < 0) {
printf("get flag fail\n");
goto err;
}
/* set the interface up */
ifr.ifr_flags |= IFF_UP;
if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) {
printf("set interface up fail\n");
goto err;
}
/* 混杂模式 */
ifr.ifr_flags |= IFF_PROMISC;
if (ioctl(fd, SIOCSIFFLAGS, &ifr) < 0) {
printf("set promisc\n");
goto err;
}
/* set ip */
memcpy(&ifr.ifr_addr, addr, sizeof(struct sockaddr));
if (ioctl(fd, SIOCSIFADDR, &ifr) < 0) {
printf("set ip fail\n");
goto err;
}
/* set mask */
inet_pton(AF_INET, "255.255.255.255", &addr->sin_addr.s_addr);
memcpy(&ifr.ifr_netmask, addr, sizeof(struct sockaddr));
if (ioctl(fd, SIOCSIFNETMASK, &ifr) < 0) {
printf("set mask fail\n");
goto err;
}
close(fd);
return 0;
err:
close(fd);
return -1;
}
关闭的话直接调用close()就好了,毕竟一切皆文件。最后来简单的测试一下。我们从tun网卡read一下,并打印接收到的数据包协议编号:
int main()
{
int tun_fd = -1;
char buf[4096] = {0};
const char * dev = "tun";
struct sockaddr_in addr;
tun_fd = tun_open(dev);
if (tun_fd < 0)
{
printf("create tun fail\n");
return -1;
}
memset(&addr, 0, sizeof(struct sockaddr));
addr.sin_family = AF_INET;
inet_pton(AF_INET, "13.254.254.131", &addr.sin_addr.s_addr);
if (tun_setup(dev, &addr) < 0)
return -2;
while (1)
{
int ret;
struct tun_pi * pi = NULL;
ret = read(tun_fd, buf, sizeof(buf)-1);
if (ret < 0)
{
perror("read");
break;
}
buf[ret] = 0;
pi = (struct tun_pi *)buf;
if (pi->flags == TUN_PKT_STRIP)
{
printf("pkt is broken\n");
printf("proto:%d\n", pi->proto);
continue;
}
printf("proto:%02x\n", ntohs(pi->proto));
}
tun_close(tun_fd);
return 0;
}
运行一下,可以用ifconfig查看到刚刚创建的网卡:
还收到了一些包,看样子是icmpv6报文: