#include <linux /if.h>#include <linux /if_tun.h>int tun_alloc(char dev, int flags) { struct ifreq ifr; int fd, err; char clonedev = "/dev/net/tun"; / Arguments taken by the function: char dev: the name of an interface (or '\0'). MUST have enough space to hold the interface name if '\0' is passed int flags: interface flags (eg, IFF_TUN etc.) / / open the clone device / if( (fd = open(clonedev, O_RDWR)) < 0 ) { / 使用读写方式打开 / return fd; } / preparation of the struct ifr, of type "struct ifreq" / memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = flags; / IFF_TUN or IFF_TAP, plus maybe IFF_NO_PI / if (dev) { / if a device name was specified, put it in the structure; otherwise, the kernel will try to allocate the "next" device of the specified type / strncpy(ifr.ifr_name, dev, IFNAMSIZ); / 设置设备名称 / } / try to create the device / if( (err = ioctl(fd, TUNSETIFF, (void ) &ifr)) < 0 ) { close(fd); return err; } / if the operation was successful, write back the name of the interface to the variable "dev", so the caller can know it. Note that the caller MUST reserve space in dev (see calling code below) / strcpy(dev, ifr.ifr_name); / this is the special file descriptor that the caller will use to talk with the virtual interface / return fd;}
tun_alloc() 函数具有两个参数:char dev:包含接口的名称(例如,tap0,tun2等)虽然可以使用任意名称,但建议最好使用能够代表该接口类型的名称实际中通常会用到类似tunX或tapX这样的名称如果dev为'\0',则内核会尝试使用第一个对应类型的可用的接口(如tap0,但如果已经存在该接口,则使用tap1,以此类推)int flags:包含接口的类型(tun或tap)通常会使用IFF_TUN来指定一个TUN设备(报文不包括以太头),或使用IFF_TAP来指定一个TAP设备(报文包含以太头)此外,还有一个IFF_NO_PI标志,可以与IFF_TUN或IFF_TAP执行OR配合使用IFF_NO_PI 会告诉内核不需要提供报文信息,即告诉内核仅需要提供"纯"IP报文,不需要其他字节否则(不设置IFF_NO_PI),会在报文开始处添加4个额外的字节(2字节的标识和2字节的协议)IFF_NO_PI不需要再创建和连接之间进行匹配(即当创建时指定了该标志,可以在连接时不指定),需要注意的是,当使用wireshark在该接口上抓取流量时,不会显示这4个字节因此可以使用如下代码创建一个设备: char tun_name[IFNAMSIZ]; char tap_name[IFNAMSIZ]; char a_name; ... strcpy(tun_name, "tun1"); tunfd = tun_alloc(tun_name, IFF_TUN); / tun interface / strcpy(tap_name, "tap44"); tapfd = tun_alloc(tap_name, IFF_TAP); / tap interface / a_name = malloc(IFNAMSIZ); a_name[0]='\0'; tapfd = tun_alloc(a_name, IFF_TAP); / let the kernel pick a name /
到此为止,程序可以使用该接口进行通信,或将接口持久化(或将接口分配给特定的用户/组)还有两个ioctl()调用,通常是一起使用的第一个调用用于设置(或移除)接口的持久化状态,第二个用于将接口分配给一个普通的(非root)用户tunctl和openvpn --mktun这两个程序都实现了该特性下面看下tunctl的代码:... / "delete" is set if the user wants to delete (ie, make nonpersistent) an existing interface; otherwise, the user is creating a new interface / if(delete) { / remove persistent status / if(ioctl(tap_fd, TUNSETPERSIST, 0) < 0){ perror("disabling TUNSETPERSIST"); exit(1); } printf("Set '%s' nonpersistent\n", ifr.ifr_name); } else { / emulate behaviour prior to TUNSETGROUP / if(owner == -1 && group == -1) { owner = geteuid(); / 如果没有设置用户或组,则使用本uid / } if(owner != -1) { if(ioctl(tap_fd, TUNSETOWNER, owner) < 0){ / 设置接口用户所属者 / perror("TUNSETOWNER"); exit(1); } } if(group != -1) { if(ioctl(tap_fd, TUNSETGROUP, group) < 0){ / 设置接口组所属者 / perror("TUNSETGROUP"); exit(1); } } if(ioctl(tap_fd, TUNSETPERSIST, 1) < 0){ / 设置接口持久化 / perror("enabling TUNSETPERSIST"); exit(1); } if(brief) printf("%s\n", ifr.ifr_name); else { printf("Set '%s' persistent and owned by", ifr.ifr_name); if(owner != -1) printf(" uid %d", owner); if(group != -1) printf(" gid %d", group); printf("\n"); } } ...
上述的ioctl()调用必须以root执行但如果该接口已经是一个属于特定用户的持久化接口,那么该用户就可以使用该接口如上所述,连接到一个已有的tun/tap接口的代码与创建一个tun/tap接口的代码相同,即,可以多次使用tun_alloc()为了执行成功,需要注意如下三点:接口必须已经存在,且所有者与连接该接口的用户相同用户必须有 /dev/net/tun的读写权限必须提供创建接口时使用的相同的标志(即,如果接口使用IFF_TUN创建,则在连接时也必须使用该标志)当用户指定一个已经存在的接口执行 TUNSETIFF ioctl() (且该用户是该接口的所有者)时会返回成功,但这种情况下不会创建新的接口,因此一个普通用户可以成功执行该操作因此这样也可以尝试解释当调用ioctl(TUNSETIFF) 会发生什么,以及内核如何区分请求分配一个新接口和请求连接到一个现有的接口如果没有现有的接口或没有指定接口名称,意味着用户需要请求申请一个新的接口,这样内核会使用给定的名称创建一个接口(如果没有给定接口名称,则会挑选下一个可用的名称)仅能在root用户下执行如果指定了一个存在的接口名称,意味着用户期望连接到前面分配好的接口上可以使用普通用户完成该操作用户需要拥有克隆设备的合适(读写)权限,且为接口的所有者,且指定的模式(tun或tap)可以匹配创建时的模式可以在内核源码drivers/net/tun.c中查看上述代码的实现,实现函数为tun_attach(), tun_net_init(), tun_set_iff(), tun_chr_ioctl(),其中最后一个函数各种ioctl(),包括TUNSETIFF, TUNSETPERSIST, TUNSETOWNER, TUNSETGROUP等任何一种场景下,非root用户都可以配置接口(如分配IP地址,并up该接口),但这些操作同样可以作用于任何一个接口如果一个非root用户需要执行一些root特权才能执行的操作,而可以使用一些方法实现这种需求,如使用suid,sudo等下面是一般的使用场景:创建一个虚拟接口,将其持久化,分配给一个用户,并使用root权限进行配置(如,使用tunctl或其他命令实现启动初始化脚本);然后普通用户就可以连接(或取消连接)到他们期望的虚拟接口上;使用root权限销毁虚拟接口,如在系统shutdown时使用脚本(如使用tunctl -d或其他命令)进行清理举例使用tun/tap接口与使用其他接口并没有什么不同,在创建或连接到已有的接口时必须知道接口的类型,以及期望读取或写入的数据下面创建一个持久化接口,并给该接口分配IP地址# openvpn --mktun --dev tun2 #当然也可以使用 ip tuntap add tun3 mode tun创建tun接口Fri Mar 26 10:29:29 2010 TUN/TAP device tun2 openedFri Mar 26 10:29:29 2010 Persist state set to: ON# ip link set tun2 up# ip addr add 10.0.0.1/24 dev tun2
下面启动一个网络分析器来查看流量:# tshark -i tun2 #使用 tcpdump -i tun2 即可Running as user "root" and group "root". This could be dangerous.Capturing on tun2# On another console# ping 10.0.0.1PING 10.0.0.1 (10.0.0.1) 56(84) bytes of data.64 bytes from 10.0.0.1: icmp_seq=1 ttl=64 time=0.115 ms64 bytes from 10.0.0.1: icmp_seq=2 ttl=64 time=0.105 ms
执行ping操作后发现tshark并没有任何打印信息,即没有任何流量经过该接口这种现象是符合预期的,因为当ping该接口的IP地址时,操作系统会认为报文不需要在"线路"上进行传输,由内核负责回应ping请求(当ping其他接口的IP地址时的现象也是一样的)tshark抓包是在网络协议栈外进行的,ping本地IP地址时的报文会在协议层面处理,因此无法抓到报文当给一个接口分配了一个24位的IP地址时,系统会为接口对应的整个IP段分配一个可连接的路由如果路由可达,当使用tun接口时,内核会发送IP报文(无以太头),而使用tap接口时,内核首先会发送ARP请求报文下面是创建的一个tun,一个tap接口,可以看到tap0上是有mac地址的(可以使用 SIOCSIFHWADDR ioctl() 对mac地址进行修改,参考drivers/net/tun.c中的函数tun_chr_ioctl()),而tun3则没有10: tun3: <NO-CARRIER,POINTOPOINT,MULTICAST,NOARP,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 500 link/none11: tap0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc pfifo_fast state DOWN mode DEFAULT group default qlen 1000 link/ether d6:64:12:d9:19:44 brd ff:ff:ff:ff:ff:ff
在原文中,可能是因为10.0.0.2是一个可达的地址,因此能够ping通在实际测试时配置的网段10.0.0.0/24是个虚拟的地址,因此可以看到该路由是linkdown的(下面可以看到,如果有程序连接到这些接口,则对应的link是up的),因此ping 10.0.0.2时无法抓到报文# ip routedefault via 172.x.x.x.x dev eth01.1.1.0/24 dev tap0 proto kernel scope link src 1.1.1.1 dead linkdown10.0.0.0/24 dev tun2 proto kernel scope link src 10.0.0.1 dead linkdown可以使用如下命令进行修改,这样当该路由可用时,会走默认路由# echo 1 > /proc/sys/net/ipv4/conf/tun2/ignore_routes_with_linkdown这样就可以在默认路由接口eth0上抓到该报文# tcpdump -i eth0 host 10.0.0.2tcpdump: verbose output suppressed, use -v or -vv for full protocol decodelistening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes13:11:38.063274 IP iZuf6et6kto8eoc1kok0ydZ > 10.0.0.2: ICMP echo request, id 7030, seq 1, length 6413:11:39.074138 IP iZuf6et6kto8eoc1kok0ydZ > 10.0.0.2: ICMP echo request, id 7030, seq 2, length 64...上面已经创建了接口,但没有程序连接这些接口,下面编写一个简单的程序来在接口上读取内核发送的数据简单的程序下面的程序会连接到一个tun接口,并读取内核发送到该接口的数据如果该接口已经被持久化,那么就可以i使用一个普通用户(可以读写克隆设备/dev/net/tun,并且为接口的所有者)来运行这个程序下面程序只是个框架,展示了如何从设备获取数据,并对这些数据进行简单的处理下面程序使用了上面定义的tun_alloc()函数,完整代码如下:#include <net/if.h>#include <linux/if_tun.h>#include <fcntl.h>#include <sys/ioctl.h>int tun_alloc(char dev, int flags) { struct ifreq ifr; int fd, err; char clonedev = "/dev/net/tun"; / Arguments taken by the function: char dev: the name of an interface (or '\0'). MUST have enough space to hold the interface name if '\0' is passed int flags: interface flags (eg, IFF_TUN etc.) / / open the clone device / if( (fd = open(clonedev, O_RDWR)) < 0 ) { return fd; } / preparation of the struct ifr, of type "struct ifreq" / memset(&ifr, 0, sizeof(ifr)); ifr.ifr_flags = flags; / IFF_TUN or IFF_TAP, plus maybe IFF_NO_PI / if (dev) { / if a device name was specified, put it in the structure; otherwise, the kernel will try to allocate the "next" device of the specified type / strncpy(ifr.ifr_name, dev, IFNAMSIZ); } / try to create the device / if( (err = ioctl(fd, TUNSETIFF, (void ) &ifr)) < 0 ) { close(fd); return err; } / if the operation was successful, write back the name of the interface to the variable "dev", so the caller can know it. Note that the caller MUST reserve space in dev (see calling code below) / strcpy(dev, ifr.ifr_name); / this is the special file descriptor that the caller will use to talk with the virtual interface / return fd;}int main(){ int tun_fd,nread; unsigned char buffer[2000]; char tun_name[IFNAMSIZ]; / Connect to the device / strcpy(tun_name, "tun77"); tun_fd = tun_alloc(tun_name, IFF_TUN | IFF_NO_PI); / tun interface / if(tun_fd < 0){ perror("Allocating interface"); exit(1); } / Now read data coming from the kernel / while(1) { / Note that "buffer" should be at least the MTU size of the interface, eg 1500 bytes / nread = read(tun_fd,buffer,sizeof(buffer)); if(nread < 0) { perror("Reading from interface"); close(tun_fd); exit(1); } / Do whatever with the data / printf("Read %d bytes from device %s\n", nread, tun_name); }}
在一个终端中执行如下命令,创建一个与程序中使用的名称相同的tun接口tun77,并执行ping操作:# openvpn --mktun --dev tun77 --user waldnerFri Mar 26 10:48:12 2010 TUN/TAP device tun77 openedFri Mar 26 10:48:12 2010 Persist state set to: ON# ip link set tun77 up# ip addr add 10.0.0.1/24 dev tun77# ping 10.0.0.2
在另一个终端中启用编译好的程序,可以得到如下结果84字节中,20个字节为IP首部,8字节为ICMP首部,其余56字节为ICMP的echo负载# ./tunclientRead 84 bytes from device tun77Read 84 bytes from device tun77Read 84 bytes from device tun77Read 84 bytes from device tun77...
此时看下路由信息,由于连接了程序,tun77对应的路由是linkup的# ip routedefault via 172.20.98.253 dev eth010.0.0.0/24 dev tun77 proto kernel scope link src 10.0.0.1
可以使用上述程序将多种类型的流量发送到创建的tun接口,并校验从接口上读取的数据的大小每次read()操作都会返回一个完整的报文类似地,如果需要往该接口写入数据,则需要写入完整的IP报文那么如何使用这些数据呢?例如可以模拟读取的目标流量行为,为了方便解释,以上面的ping为例可以解析报文,并从IP首部,ICMP首部和负载中抽取信息,用于构造一个包含ICMP响应的IP报文,并发送出去(即,写入tun/tap设备对应的描述符),这样发送ping的源头将会接收到该响应当然,上述程序的使用场景并没有限制为ping,因此可以实现各种网络协议通常需要解析接收到的报文,并作出相应动作如果使用tap,为了正确构建响应帧,需要在代码中实现ARPUser-Mode Linux也是做了类似的事情:将一个用户空间运行的(修改过的)内核连接到主机上的一个tap接口,并通过该接口与主机进行通信当然,一个完整的Linux内核会实现TCP/IP和以太网,新的虚拟化平台,如libvirt广泛使用tap接口与支持qemu/kvm的客户机进行通信,接口通常会被命名为vnet0,vnet1等这些接口只有当它们连接的客户还在运行的时候才会存在,因此没有持久化,但可以在客户机运行期间使用ip link show和brctl show进行查看类似地,也可以将自己的代码连接到接口上,并尝试网络编程以及实现以太网和TCP/IP栈可以通过查看 drivers/net/tun.c中的函数 tun_get_user() 和tun_put_user()来了解tun驱动在内核侧做的事情隧道此外,还可以使用tun/tap接口来实现隧道功能此时不需要重新实现TCP/IP,只需要编写一个程序,在运行相同程序的主机之间进行原始数据的传递即可(通过反射方式)假设上面的程序中,除了连接到了tun/tap接口,还与一个远端主机建立了网络连接(该远端主机以服务器模式运行了一个类型的程序)(实际上两个程序都是相同的,谁是客户端,谁是服务端取决于命令行参数)一旦运行了两个程序,就可以在两个方向上传递数据网络连接使用了TCP,但也可以使用给其他协议(如UDP,甚至ICMP)可以在simpletun下载完整的代码下面是程序的主要循环,主要的工作是在tun/tap接口和网络隧道之间传数据下面简化了debug语句:... / net_fd is the network file descriptor (to the peer), tap_fd is the descriptor connected to the tun/tap interface / / use select() to handle two descriptors at once / maxfd = (tap_fd > net_fd)?tap_fd:net_fd; while(1) { int ret; fd_set rd_set; FD_ZERO(&rd_set); FD_SET(tap_fd, &rd_set); FD_SET(net_fd, &rd_set); ret = select(maxfd + 1, &rd_set, NULL, NULL, NULL); if (ret < 0 && errno == EINTR) { continue; } if (ret < 0) { perror("select()"); exit(1); } if(FD_ISSET(tap_fd, &rd_set)) { / data from tun/tap: just read it and write it to the network / nread = cread(tap_fd, buffer, BUFSIZE); / write length + packet / plength = htons(nread); nwrite = cwrite(net_fd, (char )&plength, sizeof(plength)); nwrite = cwrite(net_fd, buffer, nread); } if(FD_ISSET(net_fd, &rd_set)) { / data from the network: read it, and write it to the tun/tap interface. We need to read the length first, and then the packet / / Read length / nread = read_n(net_fd, (char )&plength, sizeof(plength)); / read packet / nread = read_n(net_fd, buffer, ntohs(plength)); / now buffer[] contains a full packet or frame, write it into the tun/tap interface / nwrite = cwrite(tap_fd, buffer, nread); } }...
上述代码的主要逻辑为:程序使用select()多路复用来同时操作两个描述符,当任何一个描述符接收到数据后,就会发送到另一个描述符中由于程序使用了TCP,接收者会会看到一条数据流,比较难以分辨报文边界因此当向网络写入一个报文或一个帧时,会在实际数据包的前面加上它的长度(2个字节)当数据来自于tap_fd 描述符时,会一次性读取一个完整的报文或帧,这样就可以将读取的数据直接写入网络,并在报文前面加上长度由于长度字段为一个short int类型的值,大于1个字节,且使用了二进制格式,因此可以使用ntohs()/htons()来兼容不同机器的字节序当数据来自于网络时,使用前面提到的技巧,可以通过报文前面的两个字节了解到后面要读取字节流中的报文的长度当读取报文后,会将其写入tun/tap接口描述符,后续会被内核接收使用上述代码可以创建一个隧道首先在隧道两端的主机上配置必要的tun/tap接口,并分配IP地址在本例中使用了两个tun接口:本机的tun11接口,IP为192.168.0.1/24;远端主机的tun3接口,IP为192.168.0.2/24simpletun默认会使用TCP端口55555进行连接远端主机以服务器模式运行simpletun程序,本机以客户端模式运行(远端服务器为10.86.43.52)[remote]# openvpn --mktun --dev tun3 --user waldnerFri Mar 26 11:11:41 2010 TUN/TAP device tun3 openedFri Mar 26 11:11:41 2010 Persist state set to: ON[remote]# ip link set tun3 up[remote]# ip addr add 192.168.0.2/24 dev tun3[remote]$ ./simpletun -i tun3 -s# server blocks waiting for the client to connect[local]# openvpn --mktun --dev tun11 --user waldnerFri Mar 26 11:17:37 2010 TUN/TAP device tun11 openedFri Mar 26 11:17:37 2010 Persist state set to: ON[local]# ip link set tun11 up[local]# ip addr add 192.168.0.1/24 dev tun11[local]$ ./simpletun -i tun11 -c 10.86.43.52# nothing happens, but the peers are now connected[local]$ ping 192.168.0.2PING 192.168.0.2 (192.168.0.2) 56(84) bytes of data.64 bytes from 192.168.0.2: icmp_seq=1 ttl=241 time=42.5 ms64 bytes from 192.168.0.2: icmp_seq=2 ttl=241 time=41.3 ms64 bytes from 192.168.0.2: icmp_seq=3 ttl=241 time=41.4 ms64 bytes from 192.168.0.2: icmp_seq=4 ttl=241 time=41.0 ms--- 192.168.0.2 ping statistics ---4 packets transmitted, 4 received, 0% packet loss, time 2999msrtt min/avg/max/mdev = 41.047/41.599/42.588/0.621 ms# let's try something more exciting now[local]$ ssh waldner@192.168.0.2waldner@192.168.0.2's password:Linux remote 2.6.22-14-xen #1 SMP Fri Feb 29 16:20:01 GMT 2008 x86_64Welcome to remote![remote]$
上面例子中tun3和tun11之间的流量实际最终还是走的默认路由,通过eth0出去# route -nKernel IP routing tableDestination Gateway Genmask Flags Metric Ref Use Iface0.0.0.0 10.86.42.1 0.0.0.0 UG 100 0 0 eth010.86.42.0 0.0.0.0 255.255.254.0 U 100 0 0 eth0169.254.169.254 10.86.43.39 255.255.255.255 UGH 100 0 0 eth0192.168.0.0 0.0.0.0 255.255.255.0 U 0 0 0 tun11
当上述隧道up之后,就可以看到simpletun两端的TCP连接"真实的"数据(即,上层应用传输的数据,ping或ssh)不会在线路上传输如果在运行simpletun的主机上启用了IP转发,并在其他主机上创建了必要的路由,那么就可以通过隧道连接到远端网络当使用的虚拟接口类型为tap时,可以透明地桥接两个地理位置遥远的以太网LAN,这样设备会认为它们位于相同的二层网络为了到这种效果,需要将本地LAN接口和虚拟tap接口一起桥接到网关(即,运行simpletun的主机或使用tap接口的另外一个隧道软件)上这样,从LAN接收到的帧也会发送到tap接口上(因为使用了桥接),隧道应用会读取数据并发送到远端另一个网桥将确保将接收到的帧转发到远程LAN另外一端也会发生相同的情况由于在两个LAN之间使用了以太帧,因此可以将两个局域网有效地连接在一起意味着可以在伦敦有10台机器,而在柏林有50台机器,且可以使用192.168.1.0/24 子网创建一个60台计算机的以太网络(或使用其他子网地址)拓展simpletun 是一个非常简单的程序,可以通过多种方式进行扩展首先,可以增加新的连接方式,例如,可以实现使用UDP的连接再者,目前的数据是以明文方式传输的,但当数据位于程序的buffer中时,可以在传输前进行变更,例如进行加密虽然simpletun是一个简单的程序,但很多热门的程序也是通过这种方式使用tun/tap网络的,如 OpenVPN, vtun或Openssh的 VPN 特性最后要说明的是,在TCP之上运行隧道并没有任何意义,上述使用场景被称为"tcp之上的tcp",更多参见"Why tcp over tcp is a bad idea"OpenVPN等应用程序默认使用UDP正是出于这个原因,使用TCP会导致性能降低(图片来源网络,侵删)
0 评论