非常簡單的IP 隧道,基於DTLS。
如果沒有強加密?需求,可以考慮使用更為輕量的utun?
go install github.com/taoso/dtun/cmd/dtun
# 服务端
dtun -key foo
# 客户端
dtun -connect addr:port -key foo
簡單列表下幾個考慮的因素。
要實現穩定靠的傳輸層資料加密並不容易,所以我一直都用TLS 作為底層協定。
TLS 使用TCP,加密的資料很大程度上也是TCP 資料。這樣傳輸一個上層的資料包就需要內外兩層TCP 連線確認。這種TCP over TCP 的實作問題還不小,請參考: 參考http://sites.inka.de/~bigred/devel/tcp-tcp.html
所以說,最好還是用UDP 傳輸。加密自然要用DTLS 了。目前DTLS 還不支援1.3,而且go 語言官方還不支持,只能用這個三方實作https://github.com/pion/dtls
不論是TLS 還是DTLS,一般都需要建立憑證。這個過程現在可以不用花錢了,但配置起來還是很複雜。另外,證書只解決了加密問題,並沒有解決鑑權問題。通常只有客戶端校驗服務端憑證。也可以讓服務端校驗客戶端證書,但這樣太麻煩了。
除此之外,DTLS 是無連線的,不能像TCP 連線一樣只在創建連線的時候做鑑權就可以了。
所以,最好能在DTLS 握手的時候同時完成兩端的鑑權。所以我選用Pre-Shared Key(PSK) 模式。我們只要在兩端使用-key
指定主金鑰,就可以完成雙端鑑權。如果客戶端不知道PSK就無法建立DTLS會話。
另外,PSK還需要指定一個hint 參數。大家可以簡單看作是PSK的名字。 DTLS沒有連接, 不好判斷客戶端是不是已經下線。我決定讓每個客戶端都使用唯一的hint 參數。服務端針對hint 分配tun 設備。如果客戶端斷線重連,也不會建立多個tun 設備。但副作用就是同一個hinit 的客戶端不能同時登入。
為了支援macos,tun 設備只能設定成點對點模式。如果我們想做透明路由轉發,看下圖
pc <-----------> router <====== dtun ======> pc2 <---------> www
10.0.0.2/16 10.0.0.1/16 10.1.0.1/16 10.1.0.2/16
我們希望pc 發出的包經路由器router 轉送pc2 再轉送到外部網路。一般我們會在router 上做一次nat,再在pc2 上做一次nat。這樣做的好處是pc2 不需要感知pc 到router 網路配置。但壞處也很明顯,有兩次nat。路由器的效能一般也不強,nat 還是要盡量避免的。
所以我的方案是直接將pc 所在的網段10.0.0.0/16 推給pc2,並在pc2 上加入路由
ip route add 10.0.0.0/16 via 10.1.0.1
這樣router 可以把來自pc 的包原樣轉發給pc2,只需在pc2 上做一次nat 就可以了。
有時候我們需要指定路由白名單。在白名單裡的網段走預設路由,其他的透過隧道轉送。
我們可以在router 先加入白名單路由,下一跳設成router 預設路由。 然後指定pc2 的公網IP走router 的預設路由(關鍵!)。 最後添加
ip route add 0.0.0.0/1 via 10.1.0.2
ip route add 128.0.0.0/1 via 10.1.0.2
這裡的0.0.0.0/1
和128.0.0.1/1
正好涵蓋整個網段,效果等同於default,但又不會覆蓋預設路由。如果隧道異常關閉,所有相關路由會自動刪除,非常穩定。
你可以寫成一個腳本,使用-up
參數指定運行。我的腳本如下:
#! /bin/sh
# curl -S https://cdn.jsdelivr.net/gh/misakaio/chnroutes2@master/chnroutes.txt|grep -v '#'|xargs -I % ip route add % via $DEFAULT_GW 2>/dev/null
VPN_IP= $( ping your-server-name -c 1 | grep from | cut -d ' ' -f4 | cut -d: -f1 )
DEFAULT_GW= $( ip route | grep default | cut -d ' ' -f3 )
ip route add $VPN_IP /32 via $DEFAULT_GW
ip route add 0.0.0.0/1 via $PEER_IP
ip route add 128.0.0.0/1 via $PEER_IP