Linux TUN 本质上是一个虚拟网卡, 只不过与物理网卡不一样,它一端连接着内核协议栈,另一端连接着“应用程序”(准确的说是与该虚拟网络设备关联的字符设备 /dev/net/tun)。用户可以通过读取字符设备轻松的获取内核流过该虚拟网卡的数据并做相关处理后在放回协议栈。
下面写一个简单的应用程序来创建一个TUN设备并通过读写与之关联的字符设备来做数据处理(截取协议栈经过TUN的ICMP request报文手动将包改造为ICMP response报文后写会TUN进行回复)。
#include <stdio.h>
#include <string.h>
#include <linux/if_tun.h>
#include <sys/types.h>
#include <net/if.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/ioctl.h>
int tun_create(char *dev, int flags)
{
struct ifreq ifr;
int fd;
if ((fd = open("/dev/net/tun", O_RDWR)) < 0)
{
return fd;
}
memset(&ifr, 0, sizeof(ifr));
strcpy(ifr.ifr_name, dev);
ifr.ifr_flags |= flags;
if(fcntl(fd, F_SETFL, O_NONBLOCK) <0)
{
return fd;
}
ioctl(fd, TUNSETIFF, (void *)&ifr);
return(fd);
}
int main(int argc, char *argv[])
{
int tun, ret;
unsigned char buf[4096];
unsigned char ip[4];
unsigned char ihl;
tun = tun_create("tun0", IFF_TUN | IFF_NO_PI);
if (tun < 0)
{
perror("tun_create");
return 1;
}
printf("TUN name is %s\n", "tun0");
while (1)
{
ret = read(tun, buf, sizeof(buf));
if (ret < 0)
{
//break;
continue;
}
ihl = buf[0] & 0xf;
printf("The length of ip header is %d\n", ihl);
memcpy(ip, &buf[12], 4);//save src ip address
memcpy(&buf[12], &buf[16], 4);//copy dst ip to src ip field
memcpy(&buf[16], ip, 4); //copy src ip to dst ip field
buf[20] = 0;// icmp type field, 8 is icmp request, 0 is icmp response
*((unsigned short*)&buf[22]) += 8;//response icmp checksum field
printf("read %d bytes\n", ret);
ret = write(tun, buf, ret);
printf("write %d bytes\n", ret);
}
return 0;
}
上述文件为tun_test.c , gcc tun_test.c 编译后默认执行文件名为a.out。./a.out 运行应用程序,此时TUN设备已创建完成但是还没有up.
ip link set tun0 up //up 该interface
ip addr add 192.0.0.1 dev tun0//给tun0添加ip地址
ip route add 10.0.0.1 dev tun0 //添加route表,去10.0.0.1的packet出口均为tun0, 或者ip route add 10.0.0.0/24 dev tun0 去10.0.0.0/24网段的packet出口均为tun0
route -n //查看ipv4 route表
原pc上重新打开一个command窗口(应用程序死循环读取字符设备,若在后台运行可以不用重新打开新的命令窗), ping 10.0.0.1 可以ping通。ICMP request src ip address 为192.0.0.1 dst ip address 为10.0.0.1. tcpdump tun0抓包为RAW packet,没有mac 域(14bytes),正常ICMP包为98bytes. 因为tun设备是IP层,故没有14bytes mac header.
重新连接一台pc1,这台pc1与运行tun_test应用程序的pc2通过网卡直接相连,pc1网卡ip地址为192.168.148.132, pc2 网卡ip地址为192.168.148.129.
pc1:
route add -net 10.0.0.0 netmask 255.255.255.0 gw 192.168.148.129 //添加网关
pc2:
echo 1 > /proc/sys/net/ipv4/ip_forward //开启ipv4 forward, 让pc2 host模拟路由器
此时再从pc1上ping10.0.0.1可以ping通。pc2上dump tun0为84bytes IP层报文, icmp request src address 为192.168.148.132,dst ip address 为10.0.0.1,icmp response src ip为10.0.0.1,dst ip为192.168.148.132。 pc2上dump 物理网卡ens33,ICMP packet为98bytes,icmp request src mac地址为pc1物理网卡ens33的mac 地址,dst mac为pc2物理网卡ens33的mac地址,src ip 为192.168.148.132,dst ip 为10.0.0.1;icmp response src mac为pc2物理网卡ens33的mac地址,dst mac为pc1物理网卡ens33的mac地址,src ip为10.0.0.1,dst ip为192.168.148.132。
该过程大致如下:
pc1上ping 10.0.0.1发现该地址与自己interface的地址192.168.148.132不在同一个网段,故向其网关192.168.148.129 (直连的pc2网卡ip地址)请求ARP,并得到192.168.148.129的物理mac地址,同时网关也学习到192.168.148.132的mac地址。然后构造IP层(ip 层协议为ICMP, ICMP type为 request)报文,src ip为192.168.148.132,dst ip为10.0.0.1, src mac为192.168.148.132网卡的物理地址,dst mac为下一跳(网关)192.168.148.129 的mac地址。ICMP request报文到达目的192.168.148.129 接口接收后发现目的mac为自己不会drop继续处理,处理到IP层发现目的地址为10.0.0.1查找路由表发现要从tun0 interface出去,ip报文forward到tun0, 被tun0字符设备关联的应用程序读取,构造好response报文(IP报文)后放回协议栈,向tun设备write就相当于实际物理网卡接收数据,此时response报文进入协议栈, response报文的dst ip为192.168.148.132,host pc2上知道192.168.148.0网段的地址要从物理网卡interface ens33路由出去,IP报文从PC2 ens33 网卡出去时打上ens33 src mac 地址,查找arp表得到目的ip 192.168.148.132对应的mac地址为pc1网卡的mac 地址。
继续,PC2上网卡的IPv地址192.168.148.129,且route表项默认有以下项。现在的想法是把该条路由表项delete掉。ip route del 192.168.148.0/24 dev ens33.然后添加新的表项 ip route add 192.168.148.129 dev tun0.
delete it.
add it.
目的是想让dst ip为192.168.148.129的packet也路由到tun0被应用程序自定义处理。此时还要添加一条表项 ip route add 192.168.148.132 dev ens33. 否则去pc1的response packet找不到路由出口。
此时再pc1上ping 192.168.148.129能ping 通。 但是发现dump tun0并没有packet, 应用程序也没有打印收到包。ICMP request报文到达192.168.148.129后IP层并没有路由到tun0,猜测ip层处理的时候会先判断接口的ip地址若与packet的dst ip地址相同则deliver给上层,若不同才会去查找路由表,具体有时间的时候可以研究下ip_rcv函数。
做以上实验是因为我在想tun在实现隧道处理的时候接收到隧道报文(比如ip-in-ip)后如何处理,如何解封装。假设两台pc 物理网卡直接相连,物理网卡之上创建了TUN虚拟网卡,封装的时候很容易做,比如目的为10.0.0.1的报文转到tun0被应用程序读取后,应用程序可以在原始IP报文上再加封一层外部IP header,然后在写回tun字符设备,这时加封后的packet重新进入协议栈,按照outer dst ip 重新路由到物理网卡TX出去。此时若对端设备的物理网卡收到该加封后的报文,肯定不能在物理网卡interface上解封,因为该interface处理完IP层要看IP层协议继续往下处理,IP-IN-IP 外部ip协议为4,L4层并没有register 该proto处理函数(因为并没有注册kernel的ip tunnel模块)。因此加封的packet肯定还是要上应用程序处理,应用程序解封后留下inner ip包放回协议栈,若inner dst ip为自己虚拟网卡tun0的ip地址则被自己虚拟网卡tun终结并reply。既然直接改变接收物理网卡的ip地址的路由指向tun0不行,那么猜测RX的时候可能需要在应用程序里做一个bind在物理网卡上的RAW socket,让应用程序直接收到完整的IP-IN-IP封装报文。具体可能需要看下openvpn或者vtun的opensource源程序看是怎么实现的。