原理簡介
TUN/TAP 虛擬網絡設備的原理比較簡單,他在Linux內核中添加了一個TUN/TAP虛擬網絡設備的驅動程序和一個與之相關連的字符設備 /dev/net/tun,字符設備tun作爲用戶空間和內核空間交換數據的接口。當內核將數據包發送到虛擬網絡設備時,數據包被保存在設備相關的一個隊 列中,直到用戶空間程序通過打開的字符設備tun的描述符讀取時,它纔會被拷貝到用戶空間的緩衝區中,其效果就相當於,數據包直接發送到了用戶空間。通過 系統調用write發送數據包時其原理與此類似。
值得注意的是:一次read系統調用,有且只有一個數據包被傳送到用戶空間,並且當用戶空間的緩衝區比較小時,數據包將被截斷,剩餘部分將永久地消失,write系統調用與read類似,每次只發送一個數據包。所以在編寫此類程序的時候,請用足夠大的緩衝區,直接調用系統調用read/write,避免採用C語言的帶緩存的IO函數。
準備工作
首先你需要一個能工作的Linux操作系統,並且內核支持TUN/TAP虛擬網絡設備,如果沒有,請在內核中選中:
Device Drivers => Network device support => Universal TUN/TAP device driver support
你可以選擇編譯進內核或者是編譯成模塊,然後重新編譯內核並用新內核啓動。如果你編譯的是模塊,那麼在下步開始之前,你需要手工加載它。
root@gentux ~ # modprobe tun
開始編程
從代碼開始,
12 #include <linux/if_tun.h>
13
14 int tun_create(char *dev, int flags)
15 {
16 struct ifreq ifr;
17 int fd, err;
18
19 assert(dev != NULL);
20
21 if ((fd = open("/dev/net/tun", O_RDWR)) < 0)
22 return fd;
23
24 memset(&ifr, 0, sizeof(ifr));
25 ifr.ifr_flags |= flags;
26 if (*dev != '\0')
27 strncpy(ifr.ifr_name, dev, IFNAMSIZ);
28 if ((err = ioctl(fd, TUNSETIFF, (void *)&ifr)) < 0) {
29 close(fd);
30 return err;
31 }
32 strcpy(dev, ifr.ifr_name);
33
34 return fd;
35 }
爲了使用TUN/TAP設備,我們必須包含特定的頭 文件linux/if_tun.h,如12行所示。在21行,我們打開了字符設備/dev/net/tun。接下來我們需要爲ioctl的 TUNSETIFF命令初始化一個結構體ifr,一般的時候我們只需要關心其中的兩個成員ifr_name, ifr_flags。ifr_name定義了要創建或者是打開的虛擬網絡設備的名字,如果它爲空或者是此網絡設備不存在,內核將新建一個虛擬網絡設備,並 返回新建的虛擬網絡設備的名字,同時文件描述符fd也將和此網絡設備建立起關聯。如果並沒有指定網絡設備的名字,內核將根據其類型自動選擇tunXX和 tapXX作爲其名字。ifr_flags用來描述網絡設備的一些屬性,比如說是點對點設備還是以太網設備。詳細的選項解釋如下:
- IFF_TUN: 創建一個點對點設備
- IFF_TAP: 創建一個以太網設備
- IFF_NO_PI: 不包含包信息,默認的每個數據包當傳到用戶空間時,都將包含一個附加的包頭來保存包信息
- IFF_ONE_QUEUE: 採用單一隊列模式,即當數據包隊列滿的時候,由虛擬網絡設備自已丟棄以後的數據包直到數據包隊列再有空閒。
struct tun_pi {
unsigned short flags;
unsigned short proto;
};
目前,flags只在收取數據包的時候有效,當它的TUN_PKT_STRIP標誌被置時,表示當前的用戶空間緩衝區太小,以致數據包被截斷。proto成員表示發送/接收的數據包的協議。
上面代碼中的文件描述符fd除了支持TUN_SETIFF和其他的常規ioctl命令外,還支持以下命令:
- TUNSETNOCSUM: 不做校驗和校驗。參數爲int型的bool值。
- TUNSETPERSIST: 把對應網絡設備設置成持續模式,默認的虛擬網絡設備,當其相關的文件符被關閉時,也將會伴隨着與之相關的路由等信息同時消失。如果設置成持續模式,那麼它將會被保留供以後使用。參數爲int型的bool值。
- TUNSETOWNER: 設置網絡設備的屬主。參數類型爲uid_t。
- TUNSETLINK: 設置網絡設備的鏈路類型,此命令只有在虛擬網絡設備關閉的情況下有效。參數爲int型。
int main(int argc, char *argv[])
{
int tun, ret;
char tun_name[IFNAMSIZ];
unsigned char buf[4096];
tun_name[0] = '\0';
tun = tun_create(tun_name, IFF_TUN | IFF_NO_PI);
if (tun < 0) {
perror("tun_create");
return 1;
}
printf("TUN name is %s\n", tun_name);
while (1) {
unsigned char ip[4];
ret = read(tun, buf, sizeof(buf));
if (ret < 0)
break;
memcpy(ip, &buf[12], 4);
memcpy(&buf[12], &buf[16], 4);
memcpy(&buf[16], ip, 4);
buf[20] = 0;
*((unsigned short*)&buf[22]) += 8;
printf("read %d bytes\n", ret);
ret = write(tun, buf, ret);
printf("write %d bytes\n", ret);
}
return 0;
}
以上代碼簡答地處理了ICMP的ECHO包,並回應以ECHO REPLY。
首先運行這個程序:
root@gentux test # ./a.out
TUN name is tun0
接着在另外一個終端運行如下命令:
root@gentux linux-2.6.15-gentoo # ifconfig tun0 0.0.0.0 up
root@gentux linux-2.6.15-gentoo # route add 10.10.10.1 dev tun0
root@gentux linux-2.6.15-gentoo # ping 10.10.10.1
PING 10.10.10.1 (10.10.10.1) 56(84) bytes of data.
64 bytes from 10.10.10.1: icmp_seq=1 ttl=64 time=1.09 ms
64 bytes from 10.10.10.1: icmp_seq=2 ttl=64 time=5.18 ms
64 bytes from 10.10.10.1: icmp_seq=3 ttl=64 time=3.37 ms
--- 10.10.10.1 ping statistics ---
3 packets transmitted, 3 received, 0% packet loss, time 2011ms
rtt min/avg/max/mdev = 1.097/3.218/5.181/1.671 ms
可見,我們順利地接受到了迴應包,這時,切回到前一個終端下:
read 84 bytes
write 84 bytes
read 84 bytes
write 84 bytes
read 84 bytes
write 84 bytes
一切正如我們所預想的那樣。
TUN/TAP能做什麼?
hoho,問這個問題似乎有些傻,你說一個網卡能做什麼?我可以告訴你兩個基於此的開源項目:vtun和openvpn,至於其他的應用,請自由發揮你的想像力吧!