Tun/Tap接口使用指導


Tun/Tap接口指導

概述

對tun接口的了解需求主要來自於openshift的網絡,在openshift3和openshift4的OVS網絡中使用到了tun0接口,作為容器egresss訪問路徑上的接口之一。

工作機制

下面用到了tunctl和openvpn命令來創建tun/tap接口,但目前推薦使用ip tuntap命令:

# ip tuntap help
Usage: ip tuntap { add | del | show | list | lst | help } [ dev PHYS_DEV ]
       [ mode { tun | tap } ] [ user USER ] [ group GROUP ]
       [ one_queue ] [ pi ] [ vnet_hdr ] [ multi_queue ] [ name NAME ]

Where: USER  := { STRING | NUMBER }
    GROUP := { STRING | NUMBER }

tap/tun 是Linux內核 2.4.x 版本之后使用軟件實現的虛擬網絡設備,這類接口僅能工作在內核中。不同於普通的網絡接口,沒有物理硬件(因此也沒有物理線路連接到這類接口)。可以將tun/tap接口認為是一個普通的網絡接口,當內核決定發送數據時,會將數據發送到連接到該接口上的用戶空間的應用(而不是"線路"上)。當一個程序附加到tun/tap接口上時,該程序將獲得一個特定的文件描述符,從該描述符上可以獲得接口上發送過來的數據。類似地,程序也可以往該描述符上發送數據(需要保證數據格式的正確性),然后這些數據會輸入給tun/tap接口,內核中的tun/tap接口就像從線路上接收到數據一樣。

tap接口和tun接口的區別是,tap接口會會輸出完整的以太幀,而tun接口會輸出IP報文(不含以太頭)。可以在創建接口時指定該接口是tun接口還是tap接口。

這類接口可能是臨時的,意味着某些程序可以創建這類接口,並在使用后銷毀。當程序結束,即使沒有明確地刪除接口,也會被系統回收。另一種方式是通過專有工具(如tunctl或openvpn --mktun)將接口持久化,這樣其他程序就可以使用該接口,此時,使用該接口的程序必須使用與接口相同的類型(tun或tap)。

一旦創建了一個tun/tap接口,就可以像使用其他接口一樣使用該接口,既可以給該接口分配IP,分析流量,創建防火牆規則,創建指向該接口的路由等。

下面看下如何使用一個tun/tap接口。

創建接口

創建一個新接口的代碼與連接到一個持久接口的代碼基本是相同的,不同點是前者必須使用root權限執行(即使用CAP_NET_ADMIN capability權限的用戶),而后者可以被任意用戶執行。下面看下創建新接口的場景。

首先,/dev/net/tun必須以讀寫方式打開,由於該設備被用作創建任何tun/tap虛擬接口的起點,因此也被稱為克隆設備(clone device)。操作(open())后會返回一個文件描述符,但此時還無法與接口通信。

下一步會使用一個特殊的ioctl()系統調用,該函數的入參為上一步得到的文件描述符,以及一個TUNSETIFF常數和一個指向描述虛擬接口的結構體指針(基本上為接口名稱和操作模式--tun或tap)。作為一個可變的值,可以不指定虛擬接口名,此時內核將通過嘗試分配“下一個”設備來選擇一個名稱(例如,如果已經存在tap2,則內核會分配tap3,以此類推)。這些操作必須通過root用戶完成(或具有CAP_NET_ADMIN capability權限的用戶)。

如果ioctl()執行成功,則說明已經成功創建虛擬接口,且可以使用文件描述符通信。

此時,會有兩種情況:程序可以使用該接口(可能會在使用前分配IP),並在程序執行完后結束並銷毀該接口;另一種是通過兩個特殊的ioctl()調用來將接口持久化,在程序運行結束后會保留該接口,這樣其他程序就可以使用該接口(當使用tunctlopenvpn --mktun時會發生這種情況)。同時設置虛擬接口的所有者為一個非root的用戶或組,這樣當程序以非root用戶運行時也可以使用該接口(程序也需要有合適的權限)。

可以在內核源碼的Documentation/networking/tuntap.rst下找到基本的創建虛擬接口的示例代碼,下面對該代碼進行簡單修改:

#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_TUNIFF_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)用戶。tunctlopenvpn --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 opened
Fri 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.1
PING 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 ms
64 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/none
11: 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 route
default via 172.x.x.x.x dev eth0
1.1.1.0/24 dev tap0 proto kernel scope link src 1.1.1.1 dead linkdown
10.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.2
tcpdump: verbose output suppressed, use -v or -vv for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), capture size 262144 bytes
13:11:38.063274 IP iZuf6et6kto8eoc1kok0ydZ > 10.0.0.2: ICMP echo request, id 7030, seq 1, length 64
13: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 waldner
Fri Mar 26 10:48:12 2010 TUN/TAP device tun77 opened
Fri 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負載。

# ./tunclient
Read 84 bytes from device tun77
Read 84 bytes from device tun77
Read 84 bytes from device tun77
Read 84 bytes from device tun77
...

此時看下路由信息,由於連接了程序,tun77對應的路由是linkup的

# ip route
default via 172.20.98.253 dev eth0
10.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,為了正確構建響應幀,需要在代碼中實現ARP。User-Mode Linux也是做了類似的事情:將一個用戶空間運行的(修改過的)內核連接到主機上的一個tap接口,並通過該接口與主機進行通信。當然,一個完整的Linux內核會實現TCP/IP和以太網,新的虛擬化平台,如libvirt廣泛使用tap接口與支持qemu/kvm的客戶機進行通信,接口通常會被命名為vnet0,vnet1等。這些接口只有當它們連接的客戶還在運行的時候才會存在,因此沒有持久化,但可以在客戶機運行期間使用ip link showbrctl 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/24。simpletun默認會使用TCP端口55555進行連接。遠端主機以服務器模式運行simpletun程序,本機以客戶端模式運行(遠端服務器為10.86.43.52)。

[remote]# openvpn --mktun --dev tun3 --user waldner
Fri Mar 26 11:11:41 2010 TUN/TAP device tun3 opened
Fri 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 waldner
Fri Mar 26 11:17:37 2010 TUN/TAP device tun11 opened
Fri 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.2
PING 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 ms
64 bytes from 192.168.0.2: icmp_seq=2 ttl=241 time=41.3 ms
64 bytes from 192.168.0.2: icmp_seq=3 ttl=241 time=41.4 ms
64 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 2999ms
rtt 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.2
waldner@192.168.0.2's password:
Linux remote 2.6.22-14-xen #1 SMP Fri Feb 29 16:20:01 GMT 2008 x86_64

Welcome to remote!

[remote]$ 

上面例子中tun3和tun11之間的流量實際最終還是走的默認路由,通過eth0出去。

不要在k8s環境或容器環境中運行上述程序,可能會由於iptables導致連接失敗

# route -n
Kernel IP routing table
Destination     Gateway         Genmask         Flags Metric Ref    Use Iface
0.0.0.0         10.86.42.1      0.0.0.0         UG    100    0        0 eth0
10.86.42.0      0.0.0.0         255.255.254.0   U     100    0        0 eth0
169.254.169.254 10.86.43.39     255.255.255.255 UGH   100    0        0 eth0
192.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會導致性能降低。

參考


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM