Linux Virtual Interface: TUN/TAP
今天要介紹的,不是什麼新技術,只是之前沒碰過,而現在工作有用到,所以做個私人紀錄。
問題描述以及過去的解決方案
工作上面遇到的問題,如何建立一張虛擬的網路介面,並且讓封包經過該介面後額外封裝一個新的 Header。建立虛擬網路介面不難,但要如何去撈封包(使封包經過該介面)呢?並且之後封包的處理又是如何?之前在公司曾經用過幾種招數:
第一個作法是透過 Bridge 來將網卡綁在一起,然後去修改 Bridge 的程式碼做自己想做的事情。第二個作法是透過 Netfilter 來攔截封包,做完事情以後再硬導到正確的網卡。這兩個作法很亂來,我的亂來指的是不理會 Linux 內部封包處理流程,隨意的處理並安插封包,但我可沒說這樣做不到啊。第3種方法很好,完全符合 Linux 核心的封包處理程序,代表的例子就是 GRE,問題是現在的封裝技術亂七八糟(這個詞純粹是為了抱怨用),並不一定是單純的 IP-in-IP ,所以這個架構不一定適用。正當決定來亂搞的時候,有人提供了一個新的方向(應該是說自己孤陋寡聞):TUN/TAP。
TUN/TAP: Virtual Network Kernel Device
我們先抄一下 Wiki 上的說明:
In computer networking, TUN and TAP are virtual-network kernel devices. Being network devices supported entirely in software, they differ from ordinary network devices which are backed up by hardware network adapters.
TUN (namely network TUNnel) simulates a network layer device and it operates with layer 3 packets like IP packets. TAP (namely network tap) simulates a link layer device and it operates with layer 2 packets like Ethernet frames. TUN is used with routing, while TAP is used for creating a network bridge.
簡單來說,TUN 和 TAP 是在 Linux 核心模擬了一張虛擬網卡,而 TUN 處理的是 IP 封包,而 TAP 處理的是 Ethernet 封包。那這張網卡是在做什麼事情呢?我們用下面這張圖來說明:
在參考聯結那邊有更詳細的圖檔,包含對應的 Linux Kernel API,但在這裡就用簡化版的圖來理解。對 TUN/TAP 這張網卡來說,當收到 xmit 的時候(例如圖上粉紅色框框的 APP,如 Firefox 等),它會將封包導入某個虛擬出來的 character device,然後 User Space 的 process 就可以透過 read 來收到封包。反過來,當 process 對該 character device 進行 write 時,就好像是有外部的封包送到 TUN/TAP 的虛擬網卡,之後會進入 TCP/IP Stack 處理(例如之後會送給上層開好 socket 的某個 APP,如 Firefox 等)。這個架構最大的好處是封包的處理是在 User Space,因此可用的資源比較多,像是各式各樣的函式庫,如 Openssl 等,這也是為什麼 Openvpn 在 Linux 上就是採用 TUN/TAP 這樣的虛擬介面。第二,在這種架構下,要進行封包的再封裝也很容易,參考上圖應該就知道要怎麼做了。缺點也很明顯,效能會因為 copy_to_user 和 copy_from_user 而有所下降。其實我很想知道,在現在強力的 CPU 下,這真的還是很重大的問題嗎?
程式範例:
下面是一個自己撰寫的程式範例,因為很偷懶所以只做了單向的。
neokent@Banner ~/ $ sudo ./tun01 tun01 192.168.200.20 255.255.255.0
IF Name: tun01
IP Address: 192.168.200.20
Netmask: 255.255.255.0
這時候透過 ifconfig 來看,可以看到一張新的網路介面:
tun01 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
inet addr:192.168.200.20 P-t-P:192.168.200.20 Mask:255.255.255.0
UP POINTOPOINT RUNNING NOARP MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:500
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
然後,我們看看 Routing Table:
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.8.1 0.0.0.0 UG 0 0 0 eth0
169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 eth0
192.168.8.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0
192.168.200.0 0.0.0.0 255.255.255.0 U 0 0 0 tun01
從 Routing Table 可以發現,192.168.200.* 的封包會從 tun01 這個虛擬介面出去。然後我們用 ping 192.168.200.21 來做作看實驗,結果我們的程式印出來下面的內容:
=========================================
IP Header:
45 00 00 54
00 00 40 00
40 01 29 2E
C0 A8 C8 14
C0 A8 C8 15
Data Payload:
08 00 37 93 29 0B 00 01
92 42 88 53 00 00 00 00
B1 F7 0C 00 00 00 00 00
10 11 12 13 14 15 16 17
18 19 1A 1B 1C 1D 1E 1F
20 21 22 23 24 25 26 27
28 29 2A 2B 2C 2D 2E 2F
30 31 32 33 34 35 36 37
=========================================
這是一個 ICMP 的封包,01代表 ICMP,紅色的部份是 src 和 dst 的 IP Address,08 代表 Echo Request。同樣的實驗我們用 TCP 和 UDP 在做一次。在 UDP 上,我們寫了一個簡單的小程式來傳送封包到 192.168.200.30 的 3000 port 上,tun01 上會收到下面的資料:
=========================================
IP Header:
45 00 00 22
00 00 40 00
40 11 29 47
C0 A8 C8 14
C0 A8 C8 1E
UDP Header:
D9 77 0B B8
00 0E 6F AD
Data Payload:
31 32 33 34 35 0A
=========================================
其中 0BB8 就是 3000 的意思。在 TCP 上面,我們用 Firefox 來做實驗,打開 Firefox,網址輸入 192.168.200.40,輸出如下:
=========================================
IP Header:
45 00 00 3C
07 90 40 00
40 06 21 9E
C0 A8 C8 14
C0 A8 C8 28
TCP Header:
BE 99 00 50
BA 96 B0 88
00 00 00 00
A0 02 39 08
15 26 00 00
Data Payload:
02 04 05 B4 04 02 08 0A
00 5C BD DE 00 00 00 00
01 03 03 07
=========================================
可以看到 TCP 的 port 是 80 ,而且還看到這是 sync 的封包(藍色部份)。想當然,這些封包完全沒有回應,因為根本沒有接的人。但我們可以想一下,如果是 tunnel 到對方設備,解開 tunnel 後把這些資料拿出來,不就可以直接打到 TCP/IP 的 Stack 然後繼續處理了嗎?這就是 TUN/TAP 最基本的用法。
其實我範例應該在寫另一段,但 ... 興致已盡,交給我同事繼續後面的開發吧 :p
參考聯結:
http://backreference.org/2010/03/26/tuntap-interface-tutorial/
http://www.ibm.com/developerworks/cn/linux/l-tuntap/
問題描述以及過去的解決方案
工作上面遇到的問題,如何建立一張虛擬的網路介面,並且讓封包經過該介面後額外封裝一個新的 Header。建立虛擬網路介面不難,但要如何去撈封包(使封包經過該介面)呢?並且之後封包的處理又是如何?之前在公司曾經用過幾種招數:
第一個作法是透過 Bridge 來將網卡綁在一起,然後去修改 Bridge 的程式碼做自己想做的事情。第二個作法是透過 Netfilter 來攔截封包,做完事情以後再硬導到正確的網卡。這兩個作法很亂來,我的亂來指的是不理會 Linux 內部封包處理流程,隨意的處理並安插封包,但我可沒說這樣做不到啊。第3種方法很好,完全符合 Linux 核心的封包處理程序,代表的例子就是 GRE,問題是現在的封裝技術亂七八糟(這個詞純粹是為了抱怨用),並不一定是單純的 IP-in-IP ,所以這個架構不一定適用。正當決定來亂搞的時候,有人提供了一個新的方向(應該是說自己孤陋寡聞):TUN/TAP。
TUN/TAP: Virtual Network Kernel Device
我們先抄一下 Wiki 上的說明:
In computer networking, TUN and TAP are virtual-network kernel devices. Being network devices supported entirely in software, they differ from ordinary network devices which are backed up by hardware network adapters.
TUN (namely network TUNnel) simulates a network layer device and it operates with layer 3 packets like IP packets. TAP (namely network tap) simulates a link layer device and it operates with layer 2 packets like Ethernet frames. TUN is used with routing, while TAP is used for creating a network bridge.
簡單來說,TUN 和 TAP 是在 Linux 核心模擬了一張虛擬網卡,而 TUN 處理的是 IP 封包,而 TAP 處理的是 Ethernet 封包。那這張網卡是在做什麼事情呢?我們用下面這張圖來說明:
在參考聯結那邊有更詳細的圖檔,包含對應的 Linux Kernel API,但在這裡就用簡化版的圖來理解。對 TUN/TAP 這張網卡來說,當收到 xmit 的時候(例如圖上粉紅色框框的 APP,如 Firefox 等),它會將封包導入某個虛擬出來的 character device,然後 User Space 的 process 就可以透過 read 來收到封包。反過來,當 process 對該 character device 進行 write 時,就好像是有外部的封包送到 TUN/TAP 的虛擬網卡,之後會進入 TCP/IP Stack 處理(例如之後會送給上層開好 socket 的某個 APP,如 Firefox 等)。這個架構最大的好處是封包的處理是在 User Space,因此可用的資源比較多,像是各式各樣的函式庫,如 Openssl 等,這也是為什麼 Openvpn 在 Linux 上就是採用 TUN/TAP 這樣的虛擬介面。第二,在這種架構下,要進行封包的再封裝也很容易,參考上圖應該就知道要怎麼做了。缺點也很明顯,效能會因為 copy_to_user 和 copy_from_user 而有所下降。其實我很想知道,在現在強力的 CPU 下,這真的還是很重大的問題嗎?
程式範例:
下面是一個自己撰寫的程式範例,因為很偷懶所以只做了單向的。
程式不算難懂,主要就是開一個 TUN 的介面,並將經過的封包給印出來而已。實驗如下:首先執行程式,並輸入這虛擬介面的 IP 以及 Netmask。#include <stdio.h> #include <stdlib.h> #include <net/if.h> // struct ifreq #include <sys/types.h> // open #include <sys/stat.h> // open #include <fcntl.h> // open #include <arpa/inet.h> // inet #include <sys/ioctl.h> // ioctl #include <linux/if_tun.h> // tun/tap #include <errno.h> #include <unistd.h> // close #include <sys/epoll.h> #include <string.h> int tun_alloc( char *dev, int flags ) { struct ifreq ifr; int fd, err; char *clonedev = "/dev/net/tun"; if( ( fd = open( clonedev , O_RDWR ) ) < 0 ) { perror( "Opening /dev/net/tun" ); return fd; } memset( &ifr, 0, sizeof( ifr ) ); ifr.ifr_flags = flags; // Set the interface name. if ( strlen( dev ) > 0 ) { strncpy( ifr.ifr_name, dev, IFNAMSIZ ); } if( ( err = ioctl( fd, TUNSETIFF, (void *)&ifr ) ) < 0 ) { perror( "ioctl(TUNSETIFF)" ); close( fd ); return err; } strcpy( dev, ifr.ifr_name ); return fd; } int set_ip( char *dev, char *ipaddr, char *netmask ) { struct ifreq ifr; int err; // ioctl needs one fd as an input. // Request kernel to give me an unused fd. int fd = socket( PF_INET, SOCK_DGRAM, IPPROTO_IP ); // Set the interface name. if ( *dev ) { strncpy( ifr.ifr_name, dev, IFNAMSIZ ); } ifr.ifr_addr.sa_family = AF_INET; // Set IP address // The structure of ifr.ifr_addr.sa_data is "struct sockaddr" // struct sockaddr // { // unsigned short sa_family; // char sa_data[14]; // } // This is why +2 is used. if( ( err = inet_pton( AF_INET, ipaddr, ifr.ifr_addr.sa_data + 2 ) ) != 1 ) { perror( "Error IP address." ); close( fd ); return err; } if( ( err = ioctl( fd, SIOCSIFADDR, &ifr ) ) < 0 ) { perror( "IP: ioctl(SIOCSIFADDR)" ); close( fd ); return err; } // Set netmask if( ( err = inet_pton( AF_INET, netmask, ifr.ifr_addr.sa_data + 2 ) ) != 1 ) { perror( "Error IP address." ); close( fd ); return err; } if( ( err = ioctl( fd, SIOCSIFNETMASK, &ifr ) ) < 0 ) { perror( "Netmask: ioctl(SIOCSIFNETMASK)" ); close( fd ); return err; } // Enable the interface // Get the interface flag first and add IFF_UP | IFF_RUNNING. if( ( err = ioctl( fd, SIOCGIFFLAGS, &ifr ) ) < 0 ) { perror( "ioctl(SIOCGIFFLAGS)" ); close( fd ); return err; } ifr.ifr_flags |= ( IFF_UP | IFF_RUNNING ); if( ( err = ioctl( fd, SIOCSIFFLAGS, &ifr ) ) < 0 ) { perror( "ioctl(SIOCSIFFLAGS)" ); close( fd ); return err; } close( fd ); return 1; } void print_ip_packet( unsigned char *packet, int size ) { int ipheaderlen = 0; int protocol = 0; int i = 0, offset = 0; if( size < 20 ) { printf( "Size < IP Header!!\n" ); return; } ipheaderlen = ( packet[0] & 0x0F ) * 4; protocol = packet[9]; printf( "=========================================\n" ); printf( "IP Header:\n\t" ); for( i = 0 ; i < ipheaderlen ; i++ ) { printf( "%02X ", packet[i] ); if( i % 4 == 3 ) { printf( "\n\t" ); } } printf( "\n" ); offset = ipheaderlen; if( protocol == 6 ) { printf( "TCP Header:\n\t" ); for( i = 0 ; i < 20 ; i++ ) { printf( "%02X ", packet[offset + i] ); if( i % 4 == 3 ) { printf( "\n\t" ); } } offset += 20; printf( "\n" ); } else if( protocol == 17 ) { printf( "UDP Header:\n\t" ); for( i = 0 ; i < 8 ; i++ ) { printf( "%02X ", packet[offset + i] ); if( i % 4 == 3 ) { printf( "\n\t" ); } } offset += 8; printf( "\n" ); } printf( "Data Payload:\n\t" ); for( i = offset ; i < size ; i++ ) { printf( "%02X ", packet[i] ); if( i % 8 == ( ( offset - 1) % 8 ) ) { printf( "\n\t" ); } } printf( "\n" ); printf( "=========================================\n" ); } int main( int argc, char *argv[] ) { char ifname[IFNAMSIZ]; char ipaddr[16]; char netmask[16]; int tunfd = 0; int epfd; // EPOLL File Descriptor. struct epoll_event ev; // Used for EPOLL. struct epoll_event events[5]; // Used for EPOLL. int noEvents; // EPOLL event number. int i = 0, rcvlen = 0, running = 1; unsigned char buffer[1024]; // Receive packet buffer. memset( ifname, 0, IFNAMSIZ ); memset( ipaddr, 0, 16 ); memset( netmask, 0, 16 ); if( argc == 3 ) { strncpy( ipaddr, argv[1], 15 ); strncpy( netmask, argv[2], 15 ); } else if( argc == 4 ) { strncpy( ifname, argv[1], IFNAMSIZ - 1 ); strncpy( ipaddr, argv[2], 15 ); strncpy( netmask, argv[3], 15 ); } else { printf( "Usage:\n" ); printf( " %s\n", argv[0] ); printf( " %s \n", argv[0] ); return 0; } printf( "IF Name: %s\n", ifname ); printf( "IP Address: %s\n", ipaddr ); printf( "Netmask: %s\n", netmask ); // IFF_TUN is for TUN; IFF_TAP is for TAP // // TUN (namely network TUNnel) simulates a network layer device and it // operates with layer 3 packets like IP packets. // TAP (namely network tap) simulates a link layer device and it operates // with layer 2 packets like Ethernet frames. // // IFF_NO_PI tells the kernel to not provide packet information. if( ( tunfd = tun_alloc( ifname, IFF_TUN | IFF_NO_PI ) ) < 0 ) { printf( "Create TUN/TAP interface fail!!\n" ); } set_ip( ifname, ipaddr, netmask ); // Create epoll file descriptor. epfd = epoll_create( 5 ); // Add socket into the EPOLL set. ev.data.fd = tunfd; ev.events = EPOLLIN | EPOLLET; epoll_ctl( epfd, EPOLL_CTL_ADD, tunfd, &ev ); // Use Ctrl-C to interrupt the process. while( running ) { noEvents = epoll_wait( epfd, events, FD_SETSIZE , -1 ); for( i = 0 ; i < noEvents; i++ ) { if( events[i].events & EPOLLIN && tunfd == events[i].data.fd ) { memset( buffer, 0, 1024 ); if( ( rcvlen = read( tunfd, buffer, 1024 ) ) < 0 ) { perror( "Reading data" ); running = 0; } else { // Since this is a TUN interface, we parse this as an IP packet. // If TAP, buffer will be an ethernet frame. print_ip_packet( buffer, rcvlen ); } } } } close( tunfd ); return 0; }
neokent@Banner ~/ $ sudo ./tun01 tun01 192.168.200.20 255.255.255.0
IF Name: tun01
IP Address: 192.168.200.20
Netmask: 255.255.255.0
這時候透過 ifconfig 來看,可以看到一張新的網路介面:
tun01 Link encap:UNSPEC HWaddr 00-00-00-00-00-00-00-00-00-00-00-00-00-00-00-00
inet addr:192.168.200.20 P-t-P:192.168.200.20 Mask:255.255.255.0
UP POINTOPOINT RUNNING NOARP MULTICAST MTU:1500 Metric:1
RX packets:0 errors:0 dropped:0 overruns:0 frame:0
TX packets:0 errors:0 dropped:0 overruns:0 carrier:0
collisions:0 txqueuelen:500
RX bytes:0 (0.0 B) TX bytes:0 (0.0 B)
然後,我們看看 Routing Table:
Destination Gateway Genmask Flags Metric Ref Use Iface
0.0.0.0 192.168.8.1 0.0.0.0 UG 0 0 0 eth0
169.254.0.0 0.0.0.0 255.255.0.0 U 1000 0 0 eth0
192.168.8.0 0.0.0.0 255.255.255.0 U 1 0 0 eth0
192.168.200.0 0.0.0.0 255.255.255.0 U 0 0 0 tun01
從 Routing Table 可以發現,192.168.200.* 的封包會從 tun01 這個虛擬介面出去。然後我們用 ping 192.168.200.21 來做作看實驗,結果我們的程式印出來下面的內容:
=========================================
IP Header:
45 00 00 54
00 00 40 00
40 01 29 2E
C0 A8 C8 14
C0 A8 C8 15
Data Payload:
08 00 37 93 29 0B 00 01
92 42 88 53 00 00 00 00
B1 F7 0C 00 00 00 00 00
10 11 12 13 14 15 16 17
18 19 1A 1B 1C 1D 1E 1F
20 21 22 23 24 25 26 27
28 29 2A 2B 2C 2D 2E 2F
30 31 32 33 34 35 36 37
=========================================
這是一個 ICMP 的封包,01代表 ICMP,紅色的部份是 src 和 dst 的 IP Address,08 代表 Echo Request。同樣的實驗我們用 TCP 和 UDP 在做一次。在 UDP 上,我們寫了一個簡單的小程式來傳送封包到 192.168.200.30 的 3000 port 上,tun01 上會收到下面的資料:
=========================================
IP Header:
45 00 00 22
00 00 40 00
40 11 29 47
C0 A8 C8 14
C0 A8 C8 1E
UDP Header:
D9 77 0B B8
00 0E 6F AD
Data Payload:
31 32 33 34 35 0A
=========================================
其中 0BB8 就是 3000 的意思。在 TCP 上面,我們用 Firefox 來做實驗,打開 Firefox,網址輸入 192.168.200.40,輸出如下:
=========================================
IP Header:
45 00 00 3C
07 90 40 00
40 06 21 9E
C0 A8 C8 14
C0 A8 C8 28
TCP Header:
BE 99 00 50
BA 96 B0 88
00 00 00 00
A0 02 39 08
15 26 00 00
Data Payload:
02 04 05 B4 04 02 08 0A
00 5C BD DE 00 00 00 00
01 03 03 07
=========================================
可以看到 TCP 的 port 是 80 ,而且還看到這是 sync 的封包(藍色部份)。想當然,這些封包完全沒有回應,因為根本沒有接的人。但我們可以想一下,如果是 tunnel 到對方設備,解開 tunnel 後把這些資料拿出來,不就可以直接打到 TCP/IP 的 Stack 然後繼續處理了嗎?這就是 TUN/TAP 最基本的用法。
其實我範例應該在寫另一段,但 ... 興致已盡,交給我同事繼續後面的開發吧 :p
參考聯結:
http://backreference.org/2010/03/26/tuntap-interface-tutorial/
http://www.ibm.com/developerworks/cn/linux/l-tuntap/
留言
張貼留言