若要實現在 Linux 下的代理程序,方法有很多,比如看着 RFC 1928 來實現一個 socks5 代理並自行設置程序經過 socks5 代理等方式,下文是使用 Linux 提供的 tun/tap
設備來實現 UDP 代理隧道的大體思路和過程講解。
TUN 設備
tun / tap 是由 Linux (可能還有其他 *NIX 系統提供支持)提供的,可以用來實現用戶態的網絡路由等處理的虛擬網絡接口。也就是說,它們允許用戶態的程序直接管理這個網絡接口,而不是讓內核協議棧來處理網絡包。
那么很明顯,如果我們要實現一個代理隧道程序,那么我們第一步就要解決包從哪里來的問題,這很好解決,我們知道我們可以通過路由表來指定包到底應該流向哪個網絡設備,等數據包進入我們能夠控制的網絡接口后,我們自行處理網絡包的轉發就好了。而這里,tun / tap 就是我們所需要的,能夠為所欲為的自行處理包狀態的虛擬網絡接口。
TUN 和 TAP 分別是虛擬的三層和二層網絡設備,也就是說,我們可以從 TUN 拿到的就是 IP 層的網絡數據包了,而 TAP 則是二層網絡包,比如以太網包。因為我只打算對 IP 層的包進行處理(其實只打算處理 TCP 和 UDP),故接下來就只討論 TUN 設備了。
要想使用 臀 TUN 設備,首先需要啟用這個內核模塊,在我的系統( Arch linux )上,只需 insmod
一下就行了。
find /lib/modules/ -iname 'tun.ko.gz' # 找到在哪兒
sudo insmod /lib/modules/4.12.12-1-ARCH/kernel/drivers/net/tun.ko.gz # 插入該模塊
modprobe tun # 加載該內核模塊,當然你也可以選擇 the Windows way: 重啟一下
modinfo tun # 可以檢查一下了
lsmod | grep tun # 也可以這樣檢查
為了方便,我選擇重啟...
TUN 設備可以由程序創建和銷毀(這種情況下即便程序沒有主動的銷毀創建的 TUN 設備,程序退出時 TUN 設備也會自行銷毀),也可以使用 cli 工具創建和銷毀,比如 ip tuntap
,tunctl
,或是 openvpn --mktun
。在我們用程序實現之前,我們先使用 ip tuntap
來創建一個 TUN 設備來進行簡易的測試。
簡易測試
建立一個 tun 設備(網絡接口),然后設置路由表把數據包路由到 tun 設備里。
sudo ip tuntap add dev dummytun mode tun # 添加一個叫 dummytun 的 tun 設備
sudo ip link set dummytun up # 把設備 dummytun 開起來
sudo route add 123.123.123.123 dummytun # 把 123.123.123.123 路由到 dummytun
ip route get 123.123.123.123 # 試試看是否搞成了
如上並沒有給這個 tun 設備 IP (但 ioctl()
打開這個 tun 設備后就會有一個 IPv6 地址了),當然也可以給它一個 IP :
sudo ip addr add 192.168.61.0/24 dev dummytun
由於 tun 設備需要我們編寫用戶態的程序來操作數據包,所以需要寫個東西來處理包數據的 IO 。tun 是三層設備,故能拿到的都是三層( IP 層)的包了。下面是一個非常簡單的代碼片段,僅僅簡單的把東西從 dummytun
中讀出來而已。
// 此處省略了 tun_open() 的原型。
// 作用僅僅是 `open()` 該設備,`ioctl()` 連接到該設備最終返回文件描述符
int fd = tun_open("dummytun");
// 我們假設是按照剛剛的步驟,在運行程序之前就已經創建並 `up` 了設備 dummytun
// 如果你希望通過代碼創建 tun 設備,別忘了把創建的設備 `up` 起來
printf("Device dummytun opened\n");
while(1) {
int nbytes = read(fd, buf, sizeof(buf)); // FYI: char buf[1600];
printf("Read %d bytes from dummytun\n", nbytes);
}
另外額外需要注意的事是,在寫過路由表規則以及給設備綁 IP 之后,最好還是刷一下緩存比較好,以免出現測了半天才發現壓根沒經過自己的 tun 設備的情況。
sudo ip route flush cache
當開始處理包時,我們就可以在 wireshark 或其他類似軟件中看到流經該 tun 的網絡包了。值得一提的是,即便事先沒有給 tun 設備分配任何 IP 地址的情況下,在使用 ioctl()
打開 tun 設備后, tun 設備也將自動分得一個 IPv6 地址,所以在 wireshark 中是可以看到 ICMPv6 包存在的。
TUN 設備的使用
根據上面的簡單測試,我們可以發現,實際上我們的程序本身就是完全接管所創建的虛擬網絡設備的,我們程序所處於的職責就是,不斷的 read
看看哪些網絡包需要處理,然后程序進行處理並 write
就是了。
作為最簡單的實踐,我們可以寫一個無腦的程序去偽裝遠程端響應我們網絡設備中出現的 ICMP 包,首先本地攔截 123.123.123.123
到我們的 TUN 網絡設備,然后我們在 ping 的時候,就可以從 TUN 中 read
到 ICMP 包了,那么接下來,我們只需要互換 IP 包的源地址和目的地址,並修改 ICMP 包中的標志位為 ECHO_REPLY
,(別忘了重算包的checksum)然后寫回 TUN 設備,就可以讓 ping 程序認為遠程端服務器正確的做了響應了。
我在編寫時使用了一個叫 libtins
的第三方庫來做包的拼裝和解析,下面則是對上面描述的步驟的一個簡單的例子。
while(1) {
nbytes = read(fd, buf, sizeof(buf)); // 假設 fd 為所創建的 tun 設備的文件描述符
printf("Read %d bytes from dummytun\n", nbytes);
RawPDU p((uint8_t *)buf, nbytes);
try {
IP ip(p.to<IP>());
cout << "IP Packet: " << ip.src_addr() << " -> " << ip.dst_addr() << std::endl;
Tins::IPv4Address srcaddr = ip.src_addr();
ip.src_addr(ip.dst_addr());
ip.dst_addr(srcaddr);
ICMP &icmp = ip.rfind_pdu<ICMP>();
icmp.type(ICMP::ECHO_REPLY);
write(fd, ip.serialize().data(),ip.serialize().size()); // 其實不建議 serialize 調用兩次,這里無所謂了
} catch (...) {
continue;
}
}
現在就可以看出我們的程序到底是干嘛的了吧?所以,當我們要實現隧道程序時,我們實際只是需要從我們創建的網絡接口上讀取數據包,並把數據包通過我們自己的方式發給代理隧道服務端,並等待代理隧道服務端的回復並寫回我們創建的的網絡接口就好了。也就是說,我們的客戶端程序做的事情就是:
- 從 TUN 設備讀取數據包
- 將數據包發送給代理隧道服務端(本篇所用的是 UDP 發送未做任何加密處理的數據包)
- 讀取代理隧道服務端發回的數據包
- 把發回的數據包寫回 TUN 設備
於是接下來我們可以試着把上面的程序改成兩部分,客戶端僅發送和接收,服務端則僅僅把源地址和目的地址交換並修改 ICMP 頭的標志為 ECHO_REPLY
。示例代碼這里就不貼了,有興趣的讀者可以自己試着寫一寫,很簡單的內容。
所以,服務端呢?
實際上,客戶端的工作就是簡單的監聽 UDP socket 和 TUN 設備的文件描述符,然后讀寫就是了,那么服務端怎么把客戶端發來的包寫到網卡中,又怎么把遠程端服務器返回的數據包捕獲到程序中呢?
為了讓我們的數據包發出去后遠程端返回的數據包依然能回到我們的代理隧道服務端程序所處的服務器上,我們自然要對數據包進行一次 sNAT。而假如我們把源地址改成了服務器的 IP,返回的數據包就會進入服務器的默認網絡接口。一旦數據包進入由內核控制的網絡接口,內核協議棧就會處理 SYN 包並自動做出響應,如果我們隧道中的 TCP 數據包發出去,結果遠程端服務器與我們的服務器建立的連接,這就很糟糕了,於是我們需要讓我們的數據包不經過協議棧處理而由我們控制。
我們能控制什么?當然是 TUN 設備啦!還記得我們可以給 TUN 設備指定網絡地址嗎?我們可以在服務端也建立一個 TUN 設備,並指定一個內網網絡地址(我假設指定的是192.168.61.123
),我們把要發的數據包寫入該 TUN ,數據包就發出去了。接下來呢?我們當然是寫一條 iptables 規則來把數據包導到我們的 TUN 設備了。大概是這樣的:
iptables -t nat -A POSTROUTING -s 192.168.61.0/24 -o eth0 -j MASQUERADE # 這樣寫
iptables -t nat -A POSTROUTING -s 192.168.61.0/24 -o eth0 -j SNAT --to-source xxx.xxx.xxx.xxx # 或這樣,xx是服務器 IP
哦對了。別忘了打開服務器的路由轉發功能:
echo "1" > /proc/sys/net/ipv4/ip_forward
這樣的話,我們做 sNAT 的時候把源地址改為 tun 設備的地址就是了,而數據包回來時,網絡數據包就會進入我們的能為所欲為控制的 TUN 設備啦!此時我們只需要做一次 dNAT 然后把數據包發回客戶端,就大功告成了。
整個過程並不復雜,我的一份簡單實現可以見 GitHub: BLumia/udptun 。上述內容可能有遺漏和錯誤,如果你發現了任何錯誤,都歡迎在下面評論指正,感激不盡!