记录一下自己探索tap虚拟网络设备所做的实验。
概述
需要实现的效果如图所示
创建两个和同一个程序绑定的tap。数据发到tap0,tap0将数据转发到程序中,程序再将数据转发给tap1。此场景验证两个tap之间的通信,IP地址配置如图。
编码思路
程序应当实现以下部分:
- 创建两个tap
- 会用到socket发送数据包
阻塞状态接收数据包,接收到数据时转发
代码
// 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;
}
- 程序首先打开/dev/net/tun文件,调用ioctl并指定TUNSETIFF,将flags = IFF_TAP | IFF_NO_PI传入,这一步表示创建出来的虚拟设备是tap而不是tun。
- 接着预先创建一个socket,并用sockaddr_in结构体绑定。用于后面收到数据包时转发给tap1。这里sleep 10秒是因为配置的源IP地址在环境中不存在,10秒是给刚创建出来的tap0和tap1设置IP地址预留的时间空隙。
不断循环调用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
程序收到的包的src address,为tap0的MAC地址,即发出数据包的源是tap0。 - 窗口2、3
tap0发出了arp请求询问192.168.3.11的MAC地址在哪。tap1发出了arp请求询问192.168.4.2的MAC地址在哪.两者均未收到回复。 窗口4
数据包最终被丢弃,ping不同参考
- Linux虚拟网络设备之tun/tap
- Tun/Tap interface tutorial
- C语言中利用AF_PACKET 原始套接字发送一个任意以太网帧 (一)
- 编程发送以太网帧
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。