Linux下的虚拟网卡TUN TAP
通常的socket编程,面对的都是物理网卡,Linux下其实很容易创建虚拟网卡;本文简单介绍一下Linux虚拟网卡的概念,并以tun设备为例在客户端和服务器端分别建立一个实际的虚拟网卡,最终实现一个从客户端到服务器的简单的IP隧道,希望本文能对理解虚拟网卡和IP隧道有所帮助,本文将提供完整的源程序;
1 1. Linux下的虚拟网卡TUN/TAP
- TUN和TAP是Linuxn内核的虚拟网络设备,不同于普通靠硬件网络适配器实现的设备,这些虚拟的网络设备全部用软件实现,并可以向运行于Linux上的应用软件提供与硬件的网络设备完全相同的功能;
- TAP等同于一个以太网设备,它操作OSI模型的第二层(数据链路层)数据包,通常我们所使用的网络就是以太网数据帧,所以要使用TAP设备,就需要自己构建以太网报头、IP报头、TCP/UDP报头;
- TUN模拟了网络层设备,操作第三层(网络层)数据包,通常我们使用的TCP/UDP报文在网络层使用的IP协议,所以使用TUN设备,需要自己构建IP报头和TCP/UDP报头,比TAP设备少构建一个以太网报头;
- Linux通过TUN/TAP设备向绑定该设备的用户空间的应用程序发送数据;同样,用户空间的应用程序也可以像操作硬件网络设备那样,通过TUN/TAP设备发送数据;在后面这种情况下,TUN/TAP设备向Linux的网络协议栈提交数据包,从而模拟从外部接收数据的过程;
2 2. 构建一个TUN设备
-
上一节的描述显然过于枯燥,可能会对初次接触虚拟网卡的读者感到困惑,不知所云,本节将实际建立一个tun设备,帮助你走出困惑;
-
构建一个基本的tun设备,只需要两个步骤
-
编写一个程序,至少完成三个任务
- 以可读写模式打开设备文件
/dev/net/tun
int fd; fd = open("/dev/net/tun", O_RDWR));
-
向Linux内核注册一个tun设备名称,本例中为tun0
(struct ifreq)定义在头文件<linux/if.h>中,
struct ifreq ifr; memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = IFF_TUN | IFF_NO_PI; strcpy(ifr.ifr_name, "tun0"); ioctl(fd, TUNSETIFF, (void *)&ifr);
-
编写处理tun0接收/发送数据的程序
char buffer[BUFSIZE]; while (1) { read(fd, buffer, BUFSIZE); // todo }
- 以可读写模式打开设备文件
-
为设备分配IP地址(本例中为tun0分配的IP为10.0.0.1) `bash
sudo ifconfig tun0 10.0.0.1 netmask 255.255.255.0 up```
-
-
把上面的代码片段组合在一起,就可以完成一个tun设备的建立,
-
这段程序在进入循环前增加了
system("ifconfig tun0 10.0.0.1/24 up")
,为tun0分配了IP地址10.0.0.1,所以运行完后就不需要再为这个设备分配IP了; -
编译:
gcc -Wall tun-01.c -o tun-01
-
该程序需要root权限运行,主要是因为其中使用了ioctl,运行:
sudo ./tun-01
-
运行该程序,会构建一个tun设备,打开一个新的终端,使用
ifconfig
将可以看到系统中多了一个虚拟网络接口tun0,使用route -n
查看路由也会看到增加了一条关于tun0设备的路由图1:构建一个tun设备后
-
尽管建立起了虚拟网卡tun0,但因为程序过于简单,所以这样建立的设备什么事情都做不了,必须完善程序,才能让这个设备真正地发挥作用;
-
tun设备是一个第三层(网络层)的设备,在这个设备上只能收到IP报头,收不到以太网报头,所以Linux索性没有为tun设备分配MAC地址;
-
后面将以本节的程序为基础,不断改进,最终写出一个简单的IP隧道的程序。
/*
* File: tun-01.c
* To compile: $ gcc -Wall tun-01.c -o tun-01
* Usage: $ sudo ./tun-01
*
* Example source code for article 《使用tun虚拟网络接口建立IP隧道的实例》
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#define BUFSIZE 2048
#define TUN_DEV_NAME "tun0"
#define TUN_DEV_FILE "/dev/net/tun"
int tun_fd = 0;
unsigned long int tun_count = 0;
void sigint(int signum);
int main(int argc, char *argv[]) {
uint16_t nread;
char buffer[BUFSIZE];
struct ifreq ifr;
// Open device file /dev/net/tun
tun_fd = open(TUN_DEV_FILE , O_RDWR);
// Register device name into Linux kernel
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
strcpy(ifr.ifr_name, TUN_DEV_NAME);
ioctl(tun_fd, TUNSETIFF, (void *)&ifr);
printf("Successfully connected to interface %s\n", TUN_DEV_NAME);
signal(SIGINT, sigint);
// set up IP address for tun0
system("ifconfig tun0 10.0.0.1/24 up");
// Process the packet received from tun0
while (1) {
nread = read(tun_fd, buffer, BUFSIZE);
printf("TUN %lu: Read %d bytes from the tun interface\n", ++tun_count, nread);
}
if (tun_fd) close(tun_fd);
return 0;
}
void sigint(int signum) {
// Clean up ......
if (tun_fd > 0) close(tun_fd);
printf("Terminating....\n");
printf("Totally packets from tun: %ld packets\n", tun_count);
exit(EXIT_SUCCESS);
}
3 3. 使用tun设备的基本数据流向
-
设备建立起来以后,程序员关心的是我们如何从这个设备上收发报文,如何处理这些报文;
-
对于一个物理网络接口而言,接口一端连接着网络协议栈,另一端连接着物理网络;而对于一个虚拟网络接口而言,接口的一端仍然连接着网络协议栈,但是另一端连接着一个应用程序,也就是我们前面的那个程序,我们把这个程序称为 application-tun;
-
可以和一个物理网络接口比较来说明虚拟网络接口的数据流向,在物理接口上要发送到物理网络上去的报文,相对于虚拟接口将被发送到应用程序 application-tun 上;
-
当我们使用socket发送报文时,报文被提交给Linux的网络协议栈,协议栈为报文封装各个协议层的报头,并根据路由表将报文交给相应设备的驱动程序,比如enp0s3的驱动程序,然后由驱动程序将报文发送到物理网络上(物理设备),或者发送给应用程序 application-tun(虚拟设备);
-
在上一节中,我们使用
route -n
已经看到了关于tun0设备的路由:
内核 IP 路由表
目标 网关 子网掩码 标志 跃点 引用 使用 接口
0.0.0.0 192.168.2.3 0.0.0.0 UG 100 0 0 enp0s3
10.0.0.0 0.0.0.0 255.255.255.0 U 0 0 0 tun0
169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 enp0s3
192.168.2.0 0.0.0.0 255.255.255.0 U 100 0 0 enp0s3
-
路由表明,当目的IP地址为10.0.0.x时,报文将被送到虚拟设备tun0的驱动程序上去,该设备绑定的IP为10.0.0.1;
-
还有一条路由,当目的IP地址为192.168.2.x时,报文将被送到物理设备enp0s3的驱动程序上去,该设备绑定的IP为192.168.2.114;
-
这两条路由比较相似,区别是一个是物理设备enp0s3,另一个是虚拟设备tun0,我们拿这两条路由进行对比说明数据流向;
-
发送报文到物理/虚拟接口绑定的IP地址上
- 当我们发送一个UDP报文到 192.168.2.114:5678(也就是本机物理设备enp0s3的IP)时,根据路由,报文被送给enp0s3的驱动程序,驱动程序并不会把这个报文发送到物理网络上,因为enp0s3的驱动程序已经是这个报文最终的目的地,所以enp0s3的驱动程序会将这个报文发到一个正在监听192.168.2.114:5678的用户程序上,如果我们没有编写这个程序,报文将被丢弃,这样我们就收不到这个报文;
- 当我们发送一个UDP报文到 10.0.0.1:5678(也就是本机虚拟设备tun0的IP)时,根据路由,报文被送给tun0的驱动程序,驱动程序并不会把这个报文发送到 application-tun 上,因为tun0的驱动程序已经是这个报文的最终目的地,所以tun0的驱动程序会将这个报文发到一个正在监听10.0.0.1:5678的用户程序上,和物理设备一样,如果我们没有编写这个程序,报文将被丢弃,我们收不到这个报文;
图2:发送报文到tun0的IP上
-
发送报文到符合路由的其他IP地址上
- 当我们发送一个UDP报文到 192.168.2.112:5678 时,根据路由报文会被送给enp0s3的驱动程序,驱动程序会把这个报文发送到物理网络上;
- 当我们发送一个UDP报文到 10.0.0.2:5678 时,根据路由报文会被送给报文被送给tun0的驱动程序,驱动程序会把这个报文发送到应用程序 application-tun 上;
图3:发送报文到符合tun0路由的其他IP上
-
对上述说明可以做一个简单的测试
-
打开终端,运行前面的程序:tun-01
sudo ./tun-01
-
打开另一个终端,使用下面命令分别向 10.0.0.1:5678 发送数据,在运行 tun-01 的终端上并不会显示收到数据;
echo "hello" > /dev/udp/10.0.0.1/5678
-
使用下面命令分别向 10.0.0.2:5678 发送数据,在运行 tun-01 的终端上会显示收到数据;
echo "hello" > /dev/udp/10.0.0.2/5678
-
-
源IP地址的选择
-
当我们在电脑系统上运行
sudo ./tun-01
时,我们的系统就有了两个IP地址,一个是物理网卡的,IP为192.168.2.114,另一个是虚拟网卡的,IP为10.0.0.1; -
当我们在做上面的测试时,我们用
echo ......
命令向 10.0.0.1 和 10.0.0.2 发送了UDP消息,发送时我们并没有指定源IP地址,那么发出的消息的源IP地址是什么呢?192.168.2.114 还是 10.0.0.1? -
我们把前面那个程序 tun_01.c 改一下,一是增加一些错误判断,使这个程序更加完善一些,另外我们增加一个显示IPv4报头的功能,这样我们就可以看到IP头中的源IP地址了;
-
改好的程序在后面
-
编译:
gcc -Wall tun-02.c -o tun-02
-
下面我们做个测试,向 10.0.0.2:5678 发送一条UDP消息,我们看看源IP地址是什么?
-
打开一个终端,运行tun-02
sudo ./tun-02
-
打开另一个终端,向10.0.0.2发送消息
echo "hello" > /dev/udp/10.0.0.2/5678
-
在运行tun-02的终端上显示出源IP地址为10.0.0.1
图4:Linux在多网卡环境下选择源IP
-
-
当使用sendmsg()发送数据时,是可以显式地指定源IP地址的;
-
路由表中有一个src字段,当没有指定源IP地址时,将使用选定路由的src字段作为源IP地址,使用
ip route
可以看到src字段图5:ip route命令显示路由表中src字段
- 如果选定的路由没有src字段,Linux会搜寻选定路由的网络接口上所有绑定的IP,对IPv6将选择第一个搜寻到的地址,对IPv4则尽量选择与目标IP在同一网段的IP地址;
-
/*
* File: tun-02.c
* To compile: $ gcc -Wall tun-02.c -o tun-02
* Usage: $ sudo ./tun-02
*
* Example source code for article 《使用tun虚拟网络接口建立IP隧道的实例》
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <linux/ip.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <arpa/inet.h>
#define BUFSIZE 2048
#define TUN_DEV_NAME "tun0"
#define TUN_DEV_FILE "/dev/net/tun"
int tun_fd = 0;
unsigned long int tun_count = 0;
void sigint(int signum);
void print_iphdr(struct iphdr *iph) {
uint8_t *p1, *p2;
printf("===============================================\n");
if (iph->version == 4) {
printf("Version: %d\tInternet Header Length: %d bytes\n", iph->version, iph->ihl * 4);
printf("TOS: %d\tTotal Length: %d bytes\n", iph->tos, ntohs(iph->tot_len));
printf("ID: %d\tFragment Offset: %d\n", ntohs(iph->id), ntohs(iph->frag_off));
printf("TTL: %d\tProtocol: %d\tChecksum: %d\n", iph->ttl, iph->protocol, ntohs(iph->check));
p1 = (uint8_t *)&iph->saddr;
p2 = (uint8_t *)&iph->daddr;
printf("Source IP: %d.%d.%d.%d\tDestination IP: %d.%d.%d.%d\n",
p1[0], p1[1], p1[2], p1[3], p2[0], p2[1], p2[2], p2[3]);
} else {
printf("The packet is not IPv4.\n");
}
printf("\n");
}
int main(int argc, char *argv[]) {
uint16_t nread;
char buffer[BUFSIZE];
struct ifreq ifr;
int err, ret_value = EXIT_SUCCESS;
// Open device file /dev/net/tun
if( (tun_fd = open(TUN_DEV_FILE , O_RDWR)) < 0 ) {
perror("Opening /dev/net/tun");
exit(EXIT_FAILURE);
}
// Register device name into Linux kernel
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
strcpy(ifr.ifr_name, TUN_DEV_NAME);
if( (err = ioctl(tun_fd, TUNSETIFF, (void *)&ifr)) < 0 ) {
perror("ioctl(TUNSETIFF)");
ret_value = EXIT_FAILURE;
goto quit;
}
printf("Successfully connected to interface %s\n", TUN_DEV_NAME);
signal(SIGINT, sigint);
// set up IP address for tun0
system("ifconfig tun0 10.0.0.1/24 up");
// Process the packet received from tun0
while (1) {
if ((nread = read(tun_fd, buffer, BUFSIZE)) < 0) {
perror("Read from tun");
ret_value = EXIT_FAILURE;
goto quit;
} else if (nread == 0) {
// Do nothing.
} else {
printf("TUN %lu: Read %d bytes from the tun interface\n", ++tun_count, nread);
print_iphdr((struct iphdr *)buffer);
}
}
quit:
if (tun_fd) close(tun_fd);
return ret_value;
}
void sigint(int signum) {
// Clean up ......
if (tun_fd > 0) close(tun_fd);
printf("Terminating....\n");
printf("Totally packets from tun: %ld packets\n", tun_count);
exit(EXIT_SUCCESS);
}
4 4. 使用tun设备搭建一个简单的IP隧道
-
tun实际上是tunnel的前面三个字母,tun设备注定和隧道是有关系的,tun设备也的确常用来构建一个IP隧道;
-
IP报文其实是指:IP报头 + TCP/UDP报头 + 数据
-
所谓IP隧道是指把一个IP报文作为数据再封装一个TCP头和IP头,所以整个报文变成:IP报头 + TCP报头 + (IP报头 + TCP/UDP报头 + 数据)
-
至于IP隧道的意义、应用场景之类的,本文不予讨论,可以自己去百个度或者谷个歌查一下,本文将致力于做一个简单的IP隧道;
-
先看一张示意图
图6:简单的IP隧道示意图
-
有两台电脑,Computer A和Computer B
- Computer A:
- 物理网卡为enp0s3,绑定IP:192.168.2.112
- 虚拟网卡为tun0,绑定IP:10.0.0.2
- Computer B:
- 物理网卡为enp0s3,绑定IP:192.168.2.114
- 虚拟网卡为tun0,绑定IP:10.0.0.1
- Computer A:
-
Computer A和Computer B的路由表一样,如下:
内核 IP 路由表
目标 网关 子网掩码 标志 跃点 引用 使用 接口
0.0.0.0 192.168.2.3 0.0.0.0 UG 100 0 0 enp0s3
10.0.0.0 0.0.0.0 255.255.255.0 U 0 0 0 tun0
169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 enp0s3
192.168.2.0 0.0.0.0 255.255.255.0 U 100 0 0 enp0s3
-
Computer A的应用程序app A向10.0.0.1:1234发送报文,Computer B的应用程序app D侦听在10.0.0.1:1234上;
-
目标很简单,computer A的app A直接向10.0.0.1:1234发送报文,computer B的app D能够正常收到收到,就像在一个局域网上一样;
-
首先要明确的,物理局域网的网段是192.168.2.x,所以向10.0.0.1发送报文并不会被送到物理局域网上,按照路由,这条报文会被送到tun0的驱动程序上去,因为10.0.0.1并不是computer A的虚拟网卡tun0绑定的IP,所以驱动程序会把这个报文送到application-tun上,所以如果我们不做处理,这个报文根本无法到达目的地;
-
如何处理这个报文使其发送到computer B的app D上去呢?通常的方法就是在computer A和computer B的物理网卡之间建立一条IP隧道;
-
当computer A启动applition-tun时,主动发起向computer B的连接,端口号定为5678,computer B在启动applition-tun时,主动侦听在端口5678上,并等待computer A的连接请求,一旦连接建立,这个隧道就建好了;
-
computer A的application-tun收到发往10.0.0.1的报文时,要在整个IP报文上再包装上一个IP报头+TCP报头,TCP报头中指定目的端口号为5678,IP报头中指定目的IP为192.168.2.114,源IP为192.168.2.112,然后把这个新报文从建立的隧道中发出;
-
computer B上侦听在5678端口上的应用程序app C会收到这个报文,app C去掉IP报头和TCP报头,把数据部分作为一个完整的报文重新从socket发出,这个报文的内容正是computer A发出的原始报文,computer B的内核协议栈根据路由会将该报文发给tun0的驱动程序,驱动程序会将这个报文送到正在侦听1234端口的app D上;
-
在客户端(computer A)需要编写一个程序,程序文件名:app-client.c,这个程序应遵循以下处理流程:
- 打开 /dev/net/tun 文件,返回tun_fd,在内核注册虚拟设备 tun0;
- 创建socket,sock_fd,在这个 sock_fd 上连接服务器端(computer B)的5678端口,建立IP隧道;
- 使用select检查tun_fd和sock_fd,并分别处理在这两个 fd 上收到的数据;
- 在tun_fd上收到数据的处理流程
- 将收到的包括IP报头在内的报文作为数据从sock_fd上发出
- 在sock_fd上收到数据的处理流程
- 把收到的数据作为一个IP报文显示报头及内容
-
在服务器端(computer B)编写一个程序,文件名为:app-server.c,这个程序应遵循以下处理流程:
- 打开 /dev/net/tun 文件,返回tun_fd,在内核注册虚拟设备 tun0;
- 创建socket,fd为sock_fd,在这个 sock_fd 上侦听5678端口,等待客户端连接以建立IP隧道;
- 接受客户端的连接请求,为新连接创建socket,fd为net_fd
- 使用select检查tun_fd和net_fd,并分别处理在这两个 fd 上收到的数据;
- 在tun_fd上收到数据的处理流程
- 将收到的包括IP报头在内的报文作为数据从net_fd上发出
- 在net_fd上收到数据的处理流程
- 把收到的数据(不包括IP报头和TCP报头)作为带有IP报头的报文发到tun_fd上
-
客户端程序在后面
-
客户端程序编译:
gcc -Wall app-client.c -o app-client
-
服务器端程序在后面
-
服务器端程序编译:
gcc -Wall app-server.c -o app-server
-
为了运行方便,也可以将这两个程序写成守护进程,将程序中注释掉的
daemon(0, 0)
放开即可; -
请根据实际情况调整程序中的宏定义,SERVER_IP和TUN_IP;
-
在服务器端注意防火墙设置,打开5678端口或者关闭防火墙;
-
这两个程序的运行均需要root权限。
-
客户端程序测试
-
需要打开三个终端窗口;
-
首先将程序中的SERVER_IP改为本机的IP地址,然后重新编译;
-
打开第一个终端,运行
nc -l 5678
,这个命令将监听本机的5678端口; -
打开第二个终端,运行客户端程序:
sudo ./app-client
,应该显示"Connected to server …" -
打开第三个终端,运行
echo "hello" > /dev/udp/10.0.0.1/1234
,这个命令将向10.0.0.1的1234端口发送一个UDP报文,报文的数据部分为"hello" -
此时在第二个终端上应该显示"Received data from tun",在第一个终端上收到一些乱码,但其中有"hello"字符串,乱码是因为我们收到的数据包括IP报头和UDP报头,这两部分是二进制的数据;
-
如果你看到的和上面的描述一致,那么你的客户端程序基本没有问题;
-
下面是截屏
图7:测试客户端程序时的第一个终端
图8:测试客户端程序时的第二个终端
-
-
服务器端程序无需独立测试;
-
IP隧道测试
-
需要两台机器,一台做客户端,另一台做服务器端
-
再次强调,请根据实际情况调整程序中的宏定义,SERVER_IP和TUN_IP,并重新编译程序;
-
在服务器端注意防火墙设置,打开5678端口或者关闭防火墙;
-
在服务器端和客户端均需要打开两个终端,下面是测试方法示意图
图9:测试示意图
-
在服务器第一个终端上启动服务器端程序
sudo ./app-server
-
在服务器第二个终端上执行命令
nc -luk 1234
,这个命令将一直监听在UDP的1234端口上; -
在客户端第一个终端上启动客户端程序
sudo ./app-client
-
在客户端第二个终端上执行命令
echo "hello" > /dev/udp/10.0.0.1/1234
,这个命令将向10.0.0.1(服务器端的tun0绑定的IP)的UDP端口1234发送一条消息 -
客户端第二个终端上向10.0.0.1:1234发送了一个UDP消息,内容是:hello
-
最终在服务器端的第二个终端上收到了这个信息
-
下面是运行截图
图10:服务器端第一个终端
图11:客户端第一个终端
图12:服务器端第二个终端
图13:客户端第二个终端
-
/*
* File: app-client.c
* To compile: $ gcc -Wall app-client.c -o app-client
* Usage: $ sudo ./app-client
*
* Example source code for article 《使用tun虚拟网络接口建立IP隧道的实例》
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <linux/ip.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <arpa/inet.h>
#define BUFSIZE 2048
#define SERVER_IP "192.168.2.114"
#define PORT 5678
#define TUN_DEV_NAME "tun0"
#define TUN_DEV_FILE "/dev/net/tun"
#define TUN_IP "10.0.0.2"
int tun_fd = 0, sock_fd = 0;
unsigned long int tun_count = 0, nic_count = 0;
void sigint(int signum);
/******************************************************************
* Function: void print_iphdr(struct iphdr *iph)
* Description: Print IP header
******************************************************************/
void print_iphdr(struct iphdr *iph) {
uint8_t *p1, *p2;
if (iph->version == 4) {
printf("===============================================\n");
printf("Version: %d\tInternet Header Length: %d bytes\n", iph->version, iph->ihl * 4);
printf("TOS: %d\tTotal Length: %d bytes\n", iph->tos, ntohs(iph->tot_len));
printf("ID: %d\tFragment Offset: %d\n", ntohs(iph->id), ntohs(iph->frag_off));
printf("TTL: %d\tProtocol: %d\tChecksum: %d\n", iph->ttl, iph->protocol, ntohs(iph->check));
p1 = (uint8_t *)&iph->saddr;
p2 = (uint8_t *)&iph->daddr;
printf("Source IP: %d.%d.%d.%d\tDestination IP: %d.%d.%d.%d\n",
p1[0], p1[1], p1[2], p1[3], p2[0], p2[1], p2[2], p2[3]);
printf("\n");
}
}
int main(int argc, char *argv[]) {
uint16_t nread, nwrite;
char buffer[BUFSIZE];
struct sockaddr_in remote;
struct ifreq ifr;
int err, ret_value = EXIT_SUCCESS;
int maxfd = 0;
char cmd[128];
// Step 01: set up tun_fd and register tun0 into linux kernel
//============================================================
// Open device file /dev/net/tun
if( (tun_fd = open(TUN_DEV_FILE , O_RDWR)) < 0 ) {
perror("Opening /dev/net/tun");
exit(EXIT_FAILURE);
}
// Register device name into Linux kernel
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
strcpy(ifr.ifr_name, TUN_DEV_NAME);
if( (err = ioctl(tun_fd, TUNSETIFF, (void *)&ifr)) < 0 ) {
perror("ioctl(TUNSETIFF)");
ret_value = EXIT_FAILURE;
goto quit;
}
printf("Successfully connected to interface %s\n", TUN_DEV_NAME);
// Step 02: set up sock_fd and connect to server
//===============================================
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket()");
exit(EXIT_FAILURE);
}
// assign the destination address
memset(&remote, 0, sizeof(remote));
remote.sin_family = AF_INET;
remote.sin_addr.s_addr = inet_addr(SERVER_IP);
remote.sin_port = htons(PORT);
/* connection request */
if (connect(sock_fd, (struct sockaddr*)&remote, sizeof(remote)) < 0) {
perror("connect()");
ret_value = EXIT_FAILURE;
goto quit;
}
printf("CLIENT: Connected to server %s\n", inet_ntoa(remote.sin_addr));
// Step 03: set up ctrl+c handler and allocate an IP for tun0
//============================================================
signal(SIGINT, sigint);
// set up IP address for tun0
sprintf(cmd, "ifconfig %s %s/24 up", TUN_DEV_NAME, TUN_IP);
system(cmd);
//daemon(0, 0);
// Process the packet received from tun0
maxfd = (tun_fd > sock_fd) ? tun_fd : sock_fd;
while(1) {
int ret;
fd_set rd_set;
FD_ZERO(&rd_set);
FD_SET(sock_fd, &rd_set);
FD_SET(tun_fd, &rd_set);
ret = select(maxfd + 1, &rd_set, NULL, NULL, NULL);
if (ret < 0 && errno == EINTR){
continue;
}
if (ret < 0) {
perror("select()");
ret_value = EXIT_FAILURE;
goto quit;
}
// Step 04: Process data received from tun
//=========================================
if (FD_ISSET(tun_fd, &rd_set)) {
// data from tun: just read it and write it to the network
if ((nread = read(tun_fd, buffer, BUFSIZE)) < 0) {
perror("Read from tun");
ret_value = EXIT_FAILURE;
goto quit;
} else if (nread == 0) {
// Do nothing.
} else if (nread > sizeof(struct iphdr)) {
struct iphdr *p = (struct iphdr *)buffer;
if (p->version == 4 && p->protocol == IPPROTO_UDP && p->daddr == inet_addr("10.0.0.1")) {
// write the data to sock_fd
printf("Received data from tun.\n");
tun_count++;
nwrite = write(sock_fd, buffer, nread);
if (nwrite < 0) {
perror("write");
ret_value = EXIT_FAILURE;
goto quit;
}
}
}
}
// Step 05: Process data received from NIC
//=========================================
if (FD_ISSET(sock_fd, &rd_set)) {
// data from physical nic, read it and display the IP header
// read packet
if ((nread = read(sock_fd, buffer, BUFSIZE)) < 0) {
perror("Read from nic");
ret_value = EXIT_FAILURE;
goto quit;
} else if (nread == 0) {
// socket has been closed
printf("The socket has been closed by server end.\n");
ret_value = EXIT_FAILURE;
goto quit;
}
// now buffer[] contains a full packet or frame
buffer[nread] = 0;
nic_count++;
printf("Client received message from server.\n");
if (nread >= sizeof(struct iphdr)) print_iphdr((struct iphdr *)buffer);
}
}
quit:
if (sock_fd > 0) close(sock_fd);
if (tun_fd > 0) close(tun_fd);
return(ret_value);
}
/*********************************************
* Function: void sigint(int signum)
* Desxription: ctrl+c handler
*********************************************/
void sigint(int signum) {
// Clean up ......
if (tun_fd > 0) close(tun_fd);
if (sock_fd > 0) close(sock_fd);
printf("Terminating....\n");
printf("Totally packets from tun: %ld packets\n", tun_count);
printf("Totally packets from server: %ld packets\n", nic_count);
exit(EXIT_SUCCESS);
}
/*
* File: app-server.c
* To compile: $ gcc -Wall app-server.c -o app-server
* Usage: $ sudo ./app-server
*
* Example source code for article 《使用tun虚拟网络接口建立IP隧道的实例》
*
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <signal.h>
#include <linux/if.h>
#include <linux/if_tun.h>
#include <linux/ip.h>
#include <sys/socket.h>
#include <sys/ioctl.h>
#include <sys/select.h>
#include <arpa/inet.h>
#define BUFSIZE 2048
//#define SERVER_IP "192.168.2.114"
#define PORT 5678
#define TUN_DEV_NAME "tun0"
#define TUN_DEV_FILE "/dev/net/tun"
#define TUN_IP "10.0.0.1"
int tun_fd = 0, sock_fd = 0, net_fd = 0;
unsigned long int tun_count = 0, nic_count = 0;
void sigint(int signum);
/******************************************************************
* Function: void print_iphdr(struct iphdr *iph)
* Description: Print IP header
******************************************************************/
void print_iphdr(struct iphdr *iph) {
uint8_t *p1, *p2;
if (iph->version == 4) {
printf("===============================================\n");
printf("Version: %d\tInternet Header Length: %d bytes\n", iph->version, iph->ihl * 4);
printf("TOS: %d\tTotal Length: %d bytes\n", iph->tos, ntohs(iph->tot_len));
printf("ID: %d\tFragment Offset: %d\n", ntohs(iph->id), ntohs(iph->frag_off));
printf("TTL: %d\tProtocol: %d\tChecksum: %d\n", iph->ttl, iph->protocol, ntohs(iph->check));
p1 = (uint8_t *)&iph->saddr;
p2 = (uint8_t *)&iph->daddr;
printf("Source IP: %d.%d.%d.%d\tDestination IP: %d.%d.%d.%d\n",
p1[0], p1[1], p1[2], p1[3], p2[0], p2[1], p2[2], p2[3]);
printf("\n");
}
}
int main(int argc, char *argv[]) {
uint16_t nread, nwrite;
char buffer[BUFSIZE];
//struct sockaddr_in remote;
struct ifreq ifr;
int err, ret_value = EXIT_SUCCESS;
int maxfd = 0;
char cmd[128];
struct sockaddr_in server_addr;
// Step 01: set up tun_fd and register tun0 into linux kernel
//============================================================
// Open device file /dev/net/tun
if( (tun_fd = open(TUN_DEV_FILE , O_RDWR)) < 0 ) {
perror("Opening /dev/net/tun");
exit(EXIT_FAILURE);
}
// Register device name into Linux kernel
memset(&ifr, 0, sizeof(ifr));
ifr.ifr_flags = IFF_TUN | IFF_NO_PI;
strcpy(ifr.ifr_name, TUN_DEV_NAME);
if( (err = ioctl(tun_fd, TUNSETIFF, (void *)&ifr)) < 0 ) {
perror("ioctl(TUNSETIFF)");
ret_value = EXIT_FAILURE;
goto quit;
}
printf("Successfully connected to interface %s\n", TUN_DEV_NAME);
// Step 02: set up sock_fd and listen on a port
//==============================================
if ((sock_fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) {
perror("socket()");
exit(EXIT_FAILURE);
}
bzero(&server_addr, sizeof(server_addr));
// assign IP, PORT
server_addr.sin_family = AF_INET;
server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
server_addr.sin_port = htons(PORT);
// Binding newly created socket to given IP and verification
if ((bind(sock_fd, (const struct sockaddr *)&server_addr, sizeof(server_addr))) != 0) {
perror("bind()");
close(sock_fd);
exit(EXIT_FAILURE);
} else printf("Successfully bind an address on sock_fd.\n");
if ((listen(sock_fd, 5)) != 0) {
perror("listen()");
ret_value = EXIT_FAILURE;
goto quit;
} else printf("Server listening..\n\n");
// Step 03: Accept the connection request from the client
//========================================================
// Accept the data packet from client and verification
//len = sizeof(client_addr);
//connfd = accept(sockfd, (struct sockaddr *)&client_addr, &len);
net_fd = accept(sock_fd, NULL, NULL);
if (net_fd < 0) {
perror("accept()");
ret_value = EXIT_FAILURE;
goto quit;
} else printf("Accept the connection request from client.\n");
// Step 04: set up ctrl+c handler and allocate an IP for tun0
//============================================================
signal(SIGINT, sigint);
// set up IP address for tun0
sprintf(cmd, "ifconfig %s %s/24 up", TUN_DEV_NAME, TUN_IP);
system(cmd);
//daemon(0, 0);
// Process the packet received from tun0
maxfd = (tun_fd > net_fd) ? tun_fd : net_fd;
while(1) {
int ret;
fd_set rd_set;
FD_ZERO(&rd_set);
FD_SET(net_fd, &rd_set);
FD_SET(tun_fd, &rd_set);
ret = select(maxfd + 1, &rd_set, NULL, NULL, NULL);
if (ret < 0 && errno == EINTR){
continue;
}
if (ret < 0) {
perror("select()");
ret_value = EXIT_FAILURE;
goto quit;
}
// Step 05: Process data received from tun
//=========================================
if (FD_ISSET(tun_fd, &rd_set)) {
// data from tun: just read it and write it to the network
if ((nread = read(tun_fd, buffer, BUFSIZE)) < 0) {
perror("Read from tun");
ret_value = EXIT_FAILURE;
goto quit;
} else if (nread == 0) {
// Do nothing.
} else {
struct iphdr *p = (struct iphdr *)buffer;
if (p->version == 4 && p->protocol == IPPROTO_UDP) {
// write the data to sock_fd
printf("Received data from tun.\n");
tun_count++;
/*
nwrite = write(net_fd, buffer, nread);
if (nwrite < 0) {
perror("write");
ret_value = EXIT_FAILURE;
goto quit;
}
*/
}
}
}
// Step 06: Process data received from NIC
//=========================================
if (FD_ISSET(net_fd, &rd_set)) {
// data from physical nic, read it and send to tun
// read packet
if ((nread = read(net_fd, buffer, BUFSIZE)) < 0) {
perror("Read from nic");
ret_value = EXIT_FAILURE;
goto quit;
} else if (nread == 0) {
// socket has been closed
printf("The socket has been closed by client end.\n");
ret_value = EXIT_FAILURE;
goto quit;
}
// now buffer[] contains a full packet or frame
buffer[nread] = 0;
nic_count++;
printf("Server received message from client.\n");
//if (nread >= sizeof(struct iphdr)) print_iphdr((struct iphdr *)buffer);
nwrite = write(tun_fd, buffer, nread);
if (nwrite < 0) {
perror("write to tun");
ret_value = EXIT_FAILURE;
goto quit;
}
}
}
quit:
if (sock_fd > 0) close(sock_fd);
if (net_fd > 0) close(net_fd);
if (tun_fd > 0) close(tun_fd);
return(ret_value);
}
/*********************************************
* Function: void sigint(int signum)
* Desxription: ctrl+c handler
*********************************************/
void sigint(int signum) {
// Clean up ......
if (tun_fd > 0) close(tun_fd);
if (sock_fd > 0) close(sock_fd);
if (net_fd > 0) close(net_fd);
printf("Terminating....\n");
printf("Totally packets from tun: %ld packets\n", tun_count);
printf("Totally packets from server: %ld packets\n", nic_count);
exit(EXIT_SUCCESS);
}
5 5. 后记
- 我们实现了一个简单的IP隧道,在这个IP隧道,我们传送一个UDP报文,我们传了一个UDP报文而不是一个TCP报文是为了省去connect()的麻烦;
- 这样一个IP隧道并不局限在局域网中,通过互联网一样可以建立一个IP隧道;
- 我们的这个服务器端的程序仅处理了一个客户端的连接,如果我们允许多个客户端接入并建立多条IP隧道,如果连接的多个客户端的tun都绑定在同一个网段上,那么通过服务器显然是可以像局域网一样相互通信的,好像多个终端在一个局域网里一样,是不是有点像VPN,实际上很多vpn就是使用IP隧道实现的;
- IP隧道还可以用于很多场合,如果你的防火墙不允许某些协议通过,那么你可以通过一个防火墙允许的端口与服务器建立一个IP隧道,然后在这个IP隧道里跑那个不被防火墙允许的协议,就像我们在IP隧道里跑UDP协议一样;
- 建立隧道也不一定非得使用TCP/IP协议,比如可以使用ICMP协议建立一个ICMP隧道,当你的电脑只能ping通你的服务器,其它的所有协议都无法通过防火墙的情况下,使用ICMP协议建立一个ICMP隧道,然后可以在这个隧道里跑其它协议;
- 虚拟网络接口的用途很多,现在的虚拟机、容器等大多使用了虚拟网络接口,希望这篇文章可以让你对虚拟网络接口有个初步的认识。
6 6. REF.
https://blog.whowin.net/post/blog/network/0018-tun-example-for-setting-up-ip-tunnel/