一位朋友想校對你正在撰寫的部落格文章,但其預覽功能僅在 localhost:8080 上運行。有幾種工具可以提供協助。有些是商業服務,例如 ngrok 或 Cloudflare Quick Tunnels。有些可以自架,但需要特定的客戶端,例如 frp 或 localtunnel。有些僅需要一般的 SSH 客戶端,但依賴特定的 SSH 伺服器,例如 sish。讓我們用 OpenSSH 和 nginx 來實作一個自架的解決方案!
首先,我們將遠端伺服器上的埠連線轉發到你的本地服務:
當你在遠端埠指定 0 時,伺服器會分配一個可用的埠。接著,我們設定 nginx 將來自 https://p41535.ssh.luffy.cx 的請求代理到 http://127.0.0.1:41535:
我們還需要為 *.ssh.luffy.cx 添加 DNS 記錄,並透過 Let's Encrypt 獲取萬用字元憑證:
acme.luffy.cx 是託管在 Route 53 上的區域。我用它來進行 ACME DNS-01 挑戰,用於萬用字元憑證以及由多個網頁伺服器提供的網域。在我的情況下,NixOS 會自動獲取憑證。
埠是唯一「秘密」1,用於保持內容的機密性。其他轉發解決方案會在網域名稱中添加隨機字串,以防止入侵者枚舉可能的數值。
由於核心會從本地埠範圍分配它,因此其熵很低。此外,由於核心在選擇隨機可用埠時偏好奇數埠,我們又失去了一個位元。
藉助 ngx_http_secure_link_module,我們可以稍微加強這個設定。該模組會根據一組值(包括一個秘密)計算雜湊值 2,並將其與請求中的雜湊值進行比較。雜湊值是 base64 編碼的,因此我們無法將其放在網域名稱中,因為網域名稱不區分大小寫。相反,我們將其作為使用者名稱放在 URL 中,並附帶其過期時間戳:3
該模組依賴 MD5。其安全性較弱,但足以應付此用途。❦
我們需要一個過期日期,因為後續的連線可能會重複使用該埠,而我們無法偵測到這種情況。❦
客戶端透過 HTTP 基本驗證將使用者名稱傳送給伺服器。這適用於大多數 HTTP 客戶端,包括 curl。Nginx 在 $remote_user 變數中公開使用者名稱。該模組期望雜湊值和過期時間戳以逗號分隔。我們使用 map 指令從 $remote_user 中提取兩部分,並用逗號將它們連接起來。4 我們還為該模組提供要雜湊的字串。它包含過期時間戳、埠和一個秘密:
使用者名稱可以直接使用逗號而不是雙破折號。但有些應用程式無法正確識別 URL,使其更難分享。❦
該模組在 $secure_link 變數中返回檢查狀態:
如果雜湊值不正確或遺失,我們將返回 401 錯誤並帶有 WWW-Authenticate 標頭以要求憑證。如果連結已過期,我們將返回 410 錯誤。我們會在轉發請求之前移除 Authorization 標頭,並添加一些指令來代理 WebSocket 連線。這是完整的設定:5
此設定將 127.0.0.1 或 0.0.0.0 上的任何 TCP 埠暴露給任何擁有秘密的人,繞過了大多數防火牆規則。你可以透過將 server_name 正則表達式限制在臨時埠範圍來加強安全性。❦
我想你現在一定在問一個顯而易見的問題:「我該如何產生雜湊值?」很簡單!
好吧,我想你現在一定在說:「Vincent,這樣很不方便!如果你不介意的話,我還是會繼續使用 ngrok。」好的,我聽到了。讓我們寫一個輔助腳本。
主要困難在於找到 OpenSSH 分配的臨時埠,因為它不會出現在任何環境變數中。6 為了解決這個障礙,我們尋找祖先的 sshd-session 進程:7
一個 SSH 連線可以包含多個隧道,客戶端可以隨時添加或移除它們。這與 tun 裝置轉發 (ssh -w) 不同,後者有自己的 SSH_TUNNEL 環境變數。❦
自 OpenSSH 9.8 起,sshd-session 是處理連線的輔助程式名稱。在舊版本中,請尋找 sshd。❦
然後,我們取得與這些 sshd-session 進程相關聯的監聽埠:8
我們需要 sudo,因為 sshd-session 進程已經放棄了其權限。核心隨後將其標記為不可傾印,並且其 /proc/PID/fd 目錄屬於 root。如果沒有存取此目錄的權限,ss 將無法找到哪個進程擁有套接字。❦
最後,我們顯示 URL 並保持連線開啟:
我將此腳本安裝為 http-over-ssh 在伺服器上,並將此條目添加到我的 ~/.ssh/config:
透過此解決方案,我僅依賴 OpenSSH 和 nginx,這兩者都是我伺服器上已有的軟體。一個簡單的命令就能提供一個自架的隧道和一個可分享的 URL。若要嘗試,請取得完整的輔助腳本,其中包含一些小改進。如果你使用 NixOS,就像任何有品味的人一樣,請看看我的 http-over-ssh.nix。