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 端口轉發是一項非常實用的技術,靈活的使用它不僅可以解決工程項目中繁雜的網絡問題,還能夠給我們的生活添加樂趣!