美文网首页
TUN/TAP设备浅析(二) -- TUN/TAP的编程

TUN/TAP设备浅析(二) -- TUN/TAP的编程

作者: Yihulee | 来源:发表于2017-06-12 21:36 被阅读872次

    这篇文章想详细阐述一下有关于 TUN/TAP 设备的编程。

    其实关于这两种设备的编程,基本上属于八股文,大家一般都这么干。

    启动设备之前

    有的linux 并没有将tun 模块编译到内核之中,所以,我们要做的第一件事情就是检查我们的系统是否支持 TUN/TAP 。具体如何检查和解决,请查看这里http://blog.csdn.net/lishuhuakai/article/details/70305543,这篇文章就不再赘述。

    光有tun 模块还不够,我们还要创建上篇文章中所提到的文件,运行命令:

    % sudo mknod /dev/net/tun c 10 200 # c表示为字符设备,10和200分别是主设备号和次设备号
    

    这样,你到 /dev/net/ 目录下就可以看到一个名称为 tun 的文件了。当然这里的 tun 可以改成任意的你喜欢的名称。

    启动设备

    对于TUN设备,我们一般这样来初始化:

    int 
    tun_alloc(char dev[IFNAMSIZ]) // dev数组用于存储设备的名称
    {
      struct ifreq ifr;
      int fd, err;
    
      if ((fd = open("/dev/net/tun", O_RDWR)) < 0) { // 打开文件
        perror("open");
        return -1;
      }
    
      bzero(&ifr, sizeof(ifr));
      
      /* Flags : IFF_TUN   - TUN设备
       *         IFF_TAP   - TAP设备
       *         IFF_NO_PI - 不需要提供包的信息
       */
      
      ifr.ifr_flags = IFF_TUN | IFF_NO_PI; // tun设备不包含以太网头部,而tap包含,仅此而已
    
      if (*dev) {
        strncpy(ifr.ifr_name, dev, IFNAMSIZ); 
      }
    
      if ((err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0) { // 打开设备
        perror("ioctl TUNSETIFF");
        close(fd);
        return err;
      }
      // 一旦设备开启成功,系统会给设备分配一个名称对于tun设备,一般为tunX,X为从0开始的编号,对于tap设备
      // 一般为tapX,X为从0开始的编号
      strcpy(dev, ifr.ifr_name); // 拷贝设备的名称至dev中
      return fd;
    }
    

    如果我们想启动一个TAP 设备的话,很简单,将上面的ifr.ifr_flags = IFF_TUN | IFF_NO_PI;改为ifr.ifr_flags = IFF_TAP | IFF_NO_PI;即可,那么我们就启动了一个 TAP 设备。

    设定网络地址

    上面的代码打开了文件,并且返回了文件的描述符,但是还不够,对于一张网卡来说,我们还要给其配置网络地址,有时候甚至是路由信息,网卡才能够正常地工作。

    一旦虚拟的 TUN/TAP 设备启动成功,我们便可以通过命令来给其设定地址。

    我来举个例子,以一个 TAP 设备为例:

    % sudo ip link set dev tap0 up  # 启动tap0网卡,虽然网卡已经启动,但是此时使用ipconfig命令并不能看到tap0这个设备,因为我们还没有给其配置ip地址
    
    % sudo ip address add dev tap0 10.0.1.5/24 # 给tap0设置ip地址
    
    % ifconfig  # 此时在ifconfig命令下已经可以看到tap0设备了
    tap0: flags=4163<UP,BROADCAST,RUNNING,MULTICAST>  mtu 1500
            inet 10.0.1.5  netmask 255.255.255.0  broadcast 0.0.0.0
            inet6 fe80::1872:80ff:fe20:46e2  prefixlen 64  scopeid 0x20<link>
            ether 1a:72:80:20:46:e2  txqueuelen 1000  (Ethernet)
            RX packets 0  bytes 0 (0.0 B)
            RX errors 0  dropped 0  overruns 0  frame 0
            TX packets 0  bytes 0 (0.0 B)
            TX errors 0  dropped 0 overruns 0  carrier 0  collisions 0
    
            
    % ip route show # 显示所有的路由信息
    default via 192.168.140.2 dev ens33 proto static metric 100
    10.0.0.0/24 dev ens39 proto kernel scope link src 10.0.0.130 metric 100
    10.0.1.0/24 dev tap0 proto kernel scope link src 10.0.1.5 # 给网卡设定ip后,系统自动添加了路由
    192.168.140.0/24 dev ens33 proto kernel scope link src 192.168.140.133 metric 100
    

    通过手动敲命令的方式来配置 tap0 设备,略显麻烦,其实我们可以直接在程序中调用 system 函数:

    int 
    run_cmd(char *cmd, ...)
    {
        va_list ap;
        char buf[CMDBUFLEN];
        va_start(ap, cmd);
        vsnprintf(buf, CMDBUFLEN, cmd, ap);
        va_end(ap);
        if (debug) { // DEBUG模式下输出信息
            printf("EXEC: %s\n", buf);
        }
        return system(buf);
    }
    

    将上面的命令直接传递给 run_cmd 函数即可.

    当然,如果你不喜欢这种方式,我们自然还可以有其他的方法,比如说使用下面的函数:

    int
    set_stack_attribute(char *dev)
    {
        struct ifreq ifr;
        struct sockaddr_in addr;
        int sockfd, err = -1;
    
        bzero(&addr, sizeof(addr));
        addr.sin_family = AF_INET;
        inet_pton(AF_INET, tapaddr, &addr.sin_addr);
    
        bzero(&ifr, sizeof(ifr));
        strcpy(ifr.ifr_name, dev);
        bcopy(&addr, &ifr.ifr_addr, sizeof(addr));
        sockfd = socket(AF_INET, SOCK_DGRAM, 0);
        if (sockfd < 0) {
            perror("socket");
            return -1;
        }
    
        // ifconfig tap0 10.0.1.5 #设定ip地址
        if ((err = ioctl(sockfd, SIOCSIFADDR, (void *)&ifr)) < 0) {
            perror("ioctl SIOSIFADDR");
            goto done;
        }
    
        /* 获得接口的标志 */
        if ((err = ioctl(sockfd, SIOCGIFFLAGS, (void *)&ifr)) < 0) {
            perror("ioctl SIOCGIFADDR");
            goto done;
        }
    
        /* 设置接口的标志 */
        ifr.ifr_flags |= IFF_UP;
        // ifup tap0 #启动设备
        if ((err = ioctl(sockfd, SIOCSIFFLAGS, (void *)&ifr)) < 0) {
            perror("ioctl SIOCSIFFLAGS");
            goto done;
        }
    
        inet_pton(AF_INET, "255.255.255.0", &addr.sin_addr);
        bcopy(&addr, &ifr.ifr_netmask, sizeof(addr));
        // ifconfig tap0 10.0.1.5/24 #设定子网掩码
        if ((err = ioctl(sockfd, SIOCSIFNETMASK, (void *) &ifr)) < 0) {
            perror("ioctl SIOCSIFNETMASK");
            goto done;
        }
    
    
    done:
        close(sockfd);
        return err;
    }
    

    上面的函数主要干的事情和上面的命令大致相同。

    收发数据

    收发数据非常简单,每次读取返回的文件描述符即可接收数据,没有数据到来时,会一直阻塞在哪里,当然,你也可以玩一下非阻塞 IO,然后想要发送数据的话,只需要将数据写入到该文件描述符对应的文件中即可。

    相关文章

      网友评论

          本文标题:TUN/TAP设备浅析(二) -- TUN/TAP的编程

          本文链接:https://www.haomeiwen.com/subject/izrrqxtx.html