在Linux中用C语言在tap0->app->tap1路径上发送数据包

记录一下自己探索tap虚拟网络设备所做的实验。
概述 需要实现的效果如图所示
在Linux中用C语言在tap0->app->tap1路径上发送数据包
文章图片

创建两个和同一个程序绑定的tap。数据发到tap0,tap0将数据转发到程序中,程序再将数据转发给tap1。此场景验证两个tap之间的通信,IP地址配置如图。
编码思路 程序应当实现以下部分:

  1. 创建两个tap
  2. 会用到socket发送数据包
  3. 阻塞状态接收数据包,接收到数据时转发
    代码
// tap.c #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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的IPsockfd = 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

【在Linux中用C语言在tap0->app->tap1路径上发送数据包】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: 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: 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 DestinationGatewayGenmaskFlags Metric RefUse Iface 0.0.0.0192.168.1.2540.0.0.0UG10000 ens192 192.168.1.00.0.0.0255.255.255.0U10000 ens192 192.168.3.00.0.0.0255.255.255.0U000 tap0 192.168.4.00.0.0.0255.255.255.0U000 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. 编程发送以太网帧

    推荐阅读