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_usercopy_from_user 而有所下降。其實我很想知道,在現在強力的 CPU 下,這真的還是很重大的問題嗎?

程式範例:

下面是一個自己撰寫的程式範例,因為很偷懶所以只做了單向的。

#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;
}
程式不算難懂,主要就是開一個 TUN 的介面,並將經過的封包給印出來而已。實驗如下:首先執行程式,並輸入這虛擬介面的 IP 以及 Netmask。

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/

留言

這個網誌中的熱門文章

我弟家的新居感恩禮拜分享:善頌善禱

如何將Linux打造成OpenFlow Switch:Openvswitch

Openssl 範例程式:建立SSL連線