SSH 埠轉發功能能夠將其他 TCP 埠的網路資料透過 SSH 連結來轉發,並且自動提供了相應的加密及解密服務。其實這一技術就是我們常常聽說的隧道(tunnel)技術,原因是 SSH 為其他 TCP 連結提供了一個安全的通道來進行傳輸。
我們知道,FTP 協議是以明文來傳遞資料的。但是我們可以讓 FTP 客戶端和伺服器透過 SSH 隧道傳輸資料,從而實現安全的 FTP 資料傳輸。
更常見的情況是我們的應用經常被各種防火牆限制。常見的有禁止訪問某些網站、禁用某類軟體,同時你的所有網路行為都被監控並分析!同樣的透過 SSH 隧道技術我們完全可以規避這些限制。
如上圖所示,透過 SSH 的埠轉發, 應用程式的客戶端和應用程式的伺服器端不再直接通訊,而是轉發到了 SSH 客戶端及 SSH 服務端來通訊。這樣就可以同時實現兩個目的:資料的加密傳輸和穿透防火牆!
在具體的使用場景中,埠轉發又被細分為本地埠轉發、遠端埠轉發、動態埠轉發等。本文將詳細的介紹其技術原理及使用方法。
本地埠轉發
假設我們有一臺主機 B,上面執行著 smtp 伺服器,監聽的埠號為 25,但是隻監聽了 localhost 網路介面。也就是說只有執行在主機 B 上的郵件客戶端才能與 smtp 伺服器建立連線。此時另外一臺主機 A 上的郵件客戶端如果想要透過主機 B 上的 smtp 伺服器收發郵件該怎麼設定呢?透過 SSH 的本地埠轉發功能可以輕鬆的搞定這樣的場景!
假設兩臺主機上都安裝了 SSH,我們可以使用主機 A 上的 SSH 客戶端向主機 B 上的 SSH 伺服器發起請求,建立一條執行埠轉發的隧道:
$ ssh -L 10025:localhost:25 HostB
此命令的執行原理如下圖所示(此圖來自網際網路):
執行上面的命令後,SSH 客戶端程式在主機 A 上監聽了 localhost:10025(你可以用 1024 - 65535 之間的任意埠代替 10025,只要不與已有埠衝突就行)。所有在主機 A 上發往 10025 埠的訊息都會透過 SSH 隧道轉發到主機 B 上的 25 埠。接下來需要配置主機 A 上的郵件客戶端程式,讓它把訊息傳送到 localhost:10025。完成之後主機 A 上的郵件客戶端就可以透過主機 B 上的 smtp 伺服器收發郵件了。具體的資料包的流向為:
1 郵件客戶端把資料包傳送到 localhost(主機 A) 的 10025 埠
2 SSH 客戶端把資料包加密並從主機 A 傳送到主機 B 的 SSH 伺服器
3 SSH 伺服器把資料包解密併傳送到 localhost(主機 B) 的 25 埠
從 smtp 伺服器返回的資料包則是沿著原路返回以完成資料的雙向傳遞。
至此我們已經完成了一個最基本的本地埠轉發 demo 的介紹。接下來讓我們來聊一下究竟什麼叫本地埠轉發?
在上面的 demo 中我們注意到一共有兩對的客戶端與伺服器程式,分別是 smtp 應用的客戶端和伺服器與 SSH 的客戶端和伺服器。如果應用程式的客戶端和 SSH 的客戶端位於 SSH 隧道的同一側,而應用程式的伺服器和 SSH 伺服器位於 SSH 隧道的另一側,那麼這種埠轉發型別就是本地埠轉發。需要使用 -L 選項來建立。
前面的 demo 中應用程式的客戶端和 SSH 客戶端位於同一臺主機上,應用程式的伺服器端和 SSH 的伺服器端也位於同一臺主機上,真實的情況往往不是這樣的:
上圖中的場景可能更符合真實情況(此圖來自網際網路)。應用程式的客戶端和 SSH 客戶端分別位於 SSH 隧道同一側的兩臺不同的主機上,而應用的伺服器端和 SSH 伺服器分別位於 SSH 隧道另一側的兩臺不同的主機上。此時我們需要使用下面的命令:
$ ssh -g -L P:HostS:W HostB
應用 -g 選項後主機 A 不僅會監聽 localhost 的 P 埠,還能夠監聽所有網路介面的 P 埠,所以主機 C 上的應用客戶端就可以把訊息傳送到主機 A 的 P 埠。
接下來我們必須要介紹本地埠轉發的命令格式了:
ssh -L <local port>:<remote host>:<remote port> <SSH server host>
SSH server host 是 SSH 伺服器所在的主機, remote host 和 remote port 則分別指應用程式伺服器所在主機和監聽埠。如果 remote host 指定為 localhost 則認為應用程式伺服器和 SSH 伺服器在同一臺主機上。
在結束本地埠轉發之前還需要介紹另外兩個選項,它們是 f 和 N。上面的命令在建立隧道的同時登入到遠端主機,一般情況下我們不需要這個登入。況且一旦這個登入退出,隧道也會隨之關閉。我們更期望的是能夠建立在後臺執行的隧道,這時就需要新增 f 和 N 選項。
遠端埠轉發
我們必須區別遠端埠轉發和本地埠轉發,因為它們對應了不同的應用場景,當然使用的命令列選項也是不一樣的。如果應用程式的客戶端和 SSH 的伺服器位於 SSH 隧道的同一側,而應用程式的伺服器和 SSH 的客戶端位於 SSH 隧道的另一側,那麼這種埠轉發型別就是遠端埠轉發。遠端埠轉發的結構如下圖所示(此圖來自網際網路):
所以,區分本地埠轉發和遠端埠轉發主要是看 SSH 客戶端與應用程式的哪一部分在 SSH 隧道的同一側!遠端埠轉發的命令格式為:
ssh -R <local port>:<remote host>:<remote port> <SSH server host>
其它的細節兩者基本也是一樣的。但是遠端埠轉發不支援 -g 引數,這讓我們很難實現類似下面的用例:
內網中主機 A 上執行 Jenkins 伺服器監聽本機 8080 埠,並執行 SSH 客戶端。
外網中的主機 B 上執行 SSH 伺服器。
希望可以透過遠端埠轉發的方式在主機 A 和 B 之間建立隧道,
然後外網的 Bitbucket 等程式碼管理服務可以透過 Webhook 的方式訪問主機 B 從而觸發 Jenkins 伺服器中的 Build。
這個問題的根源在於我們執行下面的遠端埠轉發命令後:
$ ssh -R 18080:localhost:8080 HostB
主機 B 只能監聽 localhost 的 18080 埠:
如何讓 HostB 監聽本機所有網路介面的 18080 埠呢? 需要透過修改 SSH 伺服器的配置來實現這個功能!在 SSH 伺服器的配置檔案 /etc/ssh/sshd_config 中新增一行:
GatewayPorts yes
儲存後重啟 SSH 伺服器,然後重新建立隧道:
此時主機 B 已經可以接受外部 webhook 的呼叫了。
動態埠轉發
相對於動態埠轉發,前面介紹的埠轉發型別都叫靜態埠轉發。所謂的 "靜態" 是指應用程式伺服器端的 IP 地址和監聽的埠是固定的。試想另外一類應用場景:設定瀏覽器透過埠轉發訪問不同網路中的網站(比如在家裡連線公司內網中的站點,哈哈)。這類應用的特點是目標伺服器的 IP 和埠是未知的並且總是在變化,建立埠轉發時不可能知道這些資訊。只有在傳送 HTTP 請求時才能確定目標伺服器的 IP 和埠。在這種場景下靜態埠轉發的方式是搞不定的,因而需要一種專門的埠轉發方式支援即 "動態埠轉發"。SSH 動態埠轉發是透過 Socks 協議實現的,建立動態埠轉發時 SSH 伺服器就類似一個 Socks 代理伺服器,所以這種轉發方式也叫 Socks 轉發。
動態埠轉發的命令格式為:
$ ssh -D <local port> <SSH Server Host>
例如:
$ ssh -D 11080 nick@xxx.xxx.xxx.xxx
注意,命令中不需要指定目標伺服器和埠號。執行上面的命令後 SSH 客戶端就開始監聽本機 localhost 的 11080 埠。你可以把本機上瀏覽器網路配置中的 Socks 伺服器指定為 localhost:11080。然後瀏覽器中的請求會被轉發到 SSH 伺服器端,並從SSH 伺服器端與目標站點建立連線進行通訊。
總結
SSH 埠轉發是一項非常實用的技術,靈活的使用它不僅可以解決工程專案中繁雜的網路問題,還能夠給我們的生活新增樂趣!