记录一下自己探索tap虚拟网络设备所做的实验。

概述

需要实现的效果如图所示
image.png
创建两个和同一个程序绑定的tap。数据发到tap0,tap0将数据转发到程序中,程序再将数据转发给tap1。此场景验证两个tap之间的通信,IP地址配置如图。

编码思路

程序应当实现以下部分:

  1. 创建两个tap
  2. 会用到socket发送数据包
  3. 阻塞状态接收数据包,接收到数据时转发

    代码

// tap.c
#include <net/if.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <fcntl.h>
#include <string.h>
#include <strings.h>
#include <linux/if_tun.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <dirent.h>

#define PORT 80 /* 使用的port */

int tun_alloc(int flags)
{

    struct ifreq ifr;
    int fd, err;
    char *clonedev = "/dev/net/tun";

    if ((fd = open(clonedev, O_RDWR)) < 0) {
        return fd;
    }

    memset(&ifr, 0, sizeof(ifr));
    ifr.ifr_flags = flags;

    if ((err = ioctl(fd, TUNSETIFF, (void *) &ifr)) < 0) {
        close(fd);
        return err;
    }

    printf("Open tun/tap device: %s for reading...\n", ifr.ifr_name);

    return fd;
}

void PrintBuffer(char *buffer, int nread)
{
    int i = 0;
    printf("Read %d bytes from tun/tap device\nRead info:\ndst address: ",
     nread);
    for (i = 0; i < 6; i++) {
        printf("%x ", buffer[i]);
    }
    printf("\nsrc address: ");
    for (; i < 12; i++) {
        printf("%x ", buffer[i]);
    }
    printf("\nframe type: ");
    for (; i < 14; i++) {
        printf("%x ", buffer[i]);
    }
    printf("\ndata(to idx 99): ");
    for (; i < 100; i++) {
        printf("%x ", buffer[i]);
    }
    printf("\n");
}

int main()
{
    struct sockaddr_in saddr, caddr;
    int tun_fd, nread, i, sockfd, ret;
    char buffer[1500];
    int tun_fd1;
    int count = 0;

    /* Flags: IFF_TUN   - TUN device (no Ethernet headers)
     *        IFF_TAP   - TAP device
     *        IFF_NO_PI - Do not provide packet information
     */
    tun_fd = tun_alloc(IFF_TAP | IFF_NO_PI);
    if (tun_fd < 0) {
        perror("Allocating interface");
        exit(1);
    }

    tun_fd1 = tun_alloc(IFF_TAP | IFF_NO_PI);
    if (tun_fd1 < 0) {
        perror("Allocating interface 1");
        exit(1);
    }

    sleep(10); // 由于103行写死数据包的源IP地址为tap0,此处预留10秒时间空隙,用于配置tap0的IP

    sockfd = socket(AF_INET, SOCK_DGRAM, 0);
    if(sockfd < 0)
    {
        perror ("Socket failed:");
        exit(1);
    }

    bzero(&caddr, sizeof(caddr));
    caddr.sin_family = AF_INET;
    caddr.sin_port = htons(PORT);
    caddr.sin_addr.s_addr = inet_addr("192.168.3.1"); // 这个是发送端的IP
    if(bind(sockfd, (struct sockaddr*)&caddr, sizeof(caddr)) < 0)
    {
        perror("Bind failed:");
        exit(1);
    }

    while (1) {
        count++;
        bzero(buffer, sizeof(buffer));
        // memset(buffer, 0, sizeof(buffer));
        nread = read(tun_fd, buffer, sizeof(buffer));
        printf("idx:%d----------READ--------------\n", count, nread);
        if (nread < 0) {
            perror("Reading from interface");
            close(tun_fd);
            exit(1);
        }
        PrintBuffer(buffer, nread);

        // read数据包后,把buffer发送到另一个tap中,先固定发到192.168.4.11(通过此程序作为中介)
        // TODO: 目的地址为MAC地址(MAC->index)应该怎么发?;
        saddr.sin_family = AF_INET;
        saddr.sin_port = htons(PORT);
        saddr.sin_addr.s_addr = inet_addr("192.168.4.2"); // 这个是接收端的IP
        printf("---------------SEND--------------\n", nread);
        ret = sendto(sockfd, buffer, sizeof(buffer), 0, (struct sockaddr*)&saddr, sizeof(saddr));
        if (ret < 0) {
            printf("Send ret failed: %d\n", ret);
            continue;
            // exit(1);
        }
        printf("Send ret: %d\n", ret);
        printf("Send buffer: ");
        for (i = 0; i < 100; i++) {
            printf("%x ", buffer[i]);
        }
        printf("\nSend to: %s\n", inet_ntoa(saddr.sin_addr));
    }

    return 0;
}
  1. 程序首先打开/dev/net/tun文件,调用ioctl并指定TUNSETIFF,将flags = IFF_TAP | IFF_NO_PI传入,这一步表示创建出来的虚拟设备是tap而不是tun。
  2. 接着预先创建一个socket,并用sockaddr_in结构体绑定。用于后面收到数据包时转发给tap1。这里sleep 10秒是因为配置的源IP地址在环境中不存在,10秒是给刚创建出来的tap0和tap1设置IP地址预留的时间空隙。
  3. 不断循环调用read函数,一旦tap0接收到数据包,就可以触发read操作,将数据内容读出到缓存buffer中。接着通过sendto函数将buffer发到tap1上。(这里的tap1地址是写死的,后续可优化为解析程序运行时带的参数args)

    验证

    这里创建4个shell窗口,第一个用于运行程序,第二、三个使用tcpdump抓tap0,tap1和lo设备的包,第四个用于命令下发。
    首先将程序放在linux设备中,在窗口1中执行

# 窗口1
[root@localhost ~]# vi tap.c # 拷贝代码
[root@localhost ~]# gcc tap.c -o tap
[root@localhost ~]# ./tap
Open tun/tap device: tap0 for reading...
Open tun/tap device: tap1 for reading...

然后在窗口5下发命令配置两个tap的IP地址和up设备。

# 窗口4
[root@localhost JerCode]# sudo ip addr add 192.168.3.1/24 dev tun0
[root@localhost JerCode]# sudo ip addr add 192.168.4.1/24 dev tun1
[root@localhost JerCode]# ip link set tun0 up
[root@localhost JerCode]# ip link set tun1 up

up操作后,在窗口2-4中下发tcpdump命令

# 窗口2
[root@localhost ~]# tcpdump -ni tap0
# 窗口3
[root@localhost ~]# tcpdump -ni tap1

配置结束,观察一下环境上的设备信息。以下得知:

  • tap0的IP地址为192.168.3.1,192.168.3.1-254网段的数据包会发给tap0,MAC地址为fe:a3:e4:8c:47
  • tap1的IP地址为192.168.4.1,192.168.4.1-254网段的数据包会发给tap1,MAC地址为4a:dd:f7:bc:dd

    [root@localhost JerCode]# ip addr
    1: lo:......
    2: ens192: ......
    110: tap0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 1000
      link/ether fe:a3:e4:8c:47:50 brd ff:ff:ff:ff:ff:ff
      inet 192.168.3.1/24 scope global tap0
         valid_lft forever preferred_lft forever
      inet6 fe80::fca3:e4ff:fe8c:4750/64 scope link
         valid_lft forever preferred_lft forever
    111: tap1: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc pfifo_fast state UNKNOWN group default qlen 1000
      link/ether 4a:dd:f7:bc:dd:8d brd ff:ff:ff:ff:ff:ff
      inet 192.168.4.1/24 scope global tap1
         valid_lft forever preferred_lft forever
      inet6 fe80::48dd:f7ff:febc:dd8d/64 scope link
         valid_lft forever preferred_lft forever
    [root@localhost JerCode]# route -n
    Kernel IP routing table
    Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
    0.0.0.0         192.168.1.254   0.0.0.0         UG    100    0        0 ens192
    192.168.1.0     0.0.0.0         255.255.255.0   U     100    0        0 ens192
    192.168.3.0     0.0.0.0         255.255.255.0   U     0      0        0 tap0
    192.168.4.0     0.0.0.0         255.255.255.0   U     0      0        0 tap1

    开始验证,在窗口4中下发ping 192.168.3.11,观察各个窗口的回显

    # 窗口1
    idx:1----------READ--------------
    Read 42 bytes from tun/tap device
    Read info:
    dst address: ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff
    src address: fffffffe ffffffa3 ffffffe4 ffffff8c 47 50
    frame type: 8 6
    data(to idx 99): 0 1 8 0 6 4 0 1 fffffffe ffffffa3 ffffffe4 ffffff8c 47 50 ffffffc0 ffffffa8 3 1 0 0 0 0 0 0 ffffffc0 ffffffa8 3 b 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    ---------------SEND--------------
    Send ret: 1500
    Send buffer: ffffffff ffffffff ffffffff ffffffff ffffffff ffffffff fffffffe ffffffa3 ffffffe4 ffffff8c 47 50 8 6 0 1 8 0 6 4 0 1 fffffffe ffffffa3 ffffffe4 ffffff8c 47 50ffffffc0 ffffffa8 3 1 0 0 0 0 0 0 ffffffc0 ffffffa8 3 b 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 00 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
    Send to: 192.168.4.2
    idx:2----------READ--------------
    ......
    # 窗口2
    [root@localhost ~]# tcpdump -ni tap0
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on tap0, link-type EN10MB (Ethernet), capture size 262144 bytes
    17:45:34.075924 ARP, Request who-has 192.168.3.11 tell 192.168.3.1, length 28
    17:45:35.078049 ARP, Request who-has 192.168.3.11 tell 192.168.3.1, length 28
    17:45:36.080049 ARP, Request who-has 192.168.3.11 tell 192.168.3.1, length 28
    # 窗口3
    [root@localhost ~]# tcpdump -ni tap1
    tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
    listening on tap1, link-type EN10MB (Ethernet), capture size 262144 bytes
    17:45:34.076107 ARP, Request who-has 192.168.4.2tell 192.168.3.1, length 28
    17:45:35.078060 ARP, Request who-has 192.168.4.2tell 192.168.3.1, length 28
    17:45:36.080058 ARP, Request who-has 192.168.4.2tell 192.168.3.1, length 28
    # 窗口4
    [root@localhost ~]# ping 192.168.3.11
    PING 192.168.3.11 (192.168.3.11) 56(84) bytes of data.
    # 敲ctrl+c
    --- 192.168.3.11 ping statistics ---
    3packets transmitted, 0 received, 100% packet loss, time 1999ms
    [root@localhost ~]#

    分析

    ping 192.168.3.11构造了数据包发到内核协议栈,内核协议栈查询路由表认为这个包应该发给tap0。tap0接收到包后,发出了arp请求,查询谁是192.168.3.11,但是没有收到回复。tap0将数据包转给了和他绑定的程序。
    程序read到数据后,将数据存入buffer中,调用sendto函数发送到192.168.4.2中。
    内核协议栈收到数据后,认为192.168.4.2要发送到tap1,tap1收到包后,也发了arp请求,询问源地址为192.168.3.1的包中,目的地址192.168.4.2在哪,但是没有回复。tap1和程序绑定,但是程序没有处理tap1的操作,包被丢弃,所以ping不同。
    以上实现了数据从tap0到app再到tap1的过程。

  1. 窗口1
    程序收到的包的src address,为tap0的MAC地址,即发出数据包的源是tap0。
  2. 窗口2、3
    tap0发出了arp请求询问192.168.3.11的MAC地址在哪。tap1发出了arp请求询问192.168.4.2的MAC地址在哪.两者均未收到回复。
  3. 窗口4
    数据包最终被丢弃,ping不同

    参考

  4. Linux虚拟网络设备之tun/tap
  5. Tun/Tap interface tutorial
  6. C语言中利用AF_PACKET 原始套接字发送一个任意以太网帧 (一)
  7. 编程发送以太网帧

你来操作一下呢
1 声望0 粉丝