Tun/Tap介面使用指導

charlieroro發表於2020-08-15

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會導致效能降低。

參考

相關文章