title: 為什麼我為 macOS 做了一個 SSH 設定與通道管理器
published: true
description:
tags: ssh, macOS, swift, tunneling

我需要的所有內部工具都在 SSH 後面。Grafana、Prometheus、staging 叢集、內部 AI 工具——沒有任何一個是對外公開位址,唯一的入口是我有金鑰的 bastion 主機。對任何有真實資料在後面的系統來說,這才是正確的架構,我也不會想改變它。真正改變的是,我每天要在三台不同的機器上,靠記憶四次輸入 ssh -N -L 3000:localhost:3000 -J bastion prod-1

Image description

所以某個週末,我開始寫 SSH Config Manager。這是一個原生 macOS App,可以在不破壞格式的情況下編輯 ~/.ssh/config,把隧道儲存成預設,並且直接在程式內開啟這些隧道,而不是呼叫 shell 去執行 ssh。我一開始是為了自己的工作流程而寫的。後來把它上架到 App Store,是因為它真的對我有幫助,而我也意識到其他人很可能正面臨同樣的摩擦。

VPN 的問題

大家第一個會問的是:為什麼不直接跑 VPN 就好了? 這問題很合理,但老實說,我已經很熟悉 SSH 這個工具到骨子裡了。

我設定過 sshd 的次數夠多,所以我知道 PermitRootLogin noPasswordAuthentication no 到底改變了什麼。當連線壞掉時,我通常能直接指出是哪一行設定造成的。VPN 則是在底下再加上一整層網路,帶著它自己的憑證、要持續修補的背景常駐程式,以及在凌晨 2 點 production 掛掉時要除錯的獨特失敗模式。SSH 已經存在於我接觸的每一台 Linux 伺服器和我擁有的每一台開發機上——不用再導入新東西,也不用再多做一層安全維護。

這個取捨是真實存在的,我寧可一開始就講清楚。沒有 VPN 的代價,是沒有透明的網路路由:我想連到的每個內部服務,都必須事先明確轉發到本機的某個埠,而且沒有我的設定檔,團隊成員就完全連不到它們。即便如此,我還是寧可維護一份乾淨的埠轉發清單,也不想再維護另一個背景常駐程式。

Shell 別名撐不過三台機器

長指令最直觀的解法就是 shell 函式。實際上,對我來說這條路行不通。

我平常大多在 MacBook Pro 上工作,但 ScyllaDB 的工作是在 Linux 上進行,因為團隊每個人都用 Linux,而且工具鏈也是以 Linux 為前提。那些環境很難彼此一致——shell 不同、金鑰路徑不同、inventory 主機清單也不同。我從來沒有把 dotfile 同步調到一個能讓 SSH 別名乾淨共用、而不是雜亂合併的狀態。失敗模式很可預期:在筆電上寫好的別名,到了我真正需要它的遠端機器上卻不存在;更糟的是,它還指向幾個月前就已經改掉的埠。

設定檔本身才是已經具備可攜性、而且全球都標準化的那個部分。所有工具開箱即用都會讀它:sshscprsync -e ssh,以及我編輯器的遠端開發外掛。圍繞 ~/.ssh/config 而不是 shell 腳本來做工具,是所有其他設計都從這裡延伸出去的核心決策。

沒有人會告訴你這些設定鍵是什麼意思

Image description

另一個摩擦點是,一般文字編輯器會把 ~/.ssh/config 當成任意文字區塊來處理。拼錯的指令不會自動補全,也不會被標示出來——你要等到連線時才會發現,原本以為有設定的東西其實被默默忽略了。某個關鍵字真正代表什麼,得去看另一個終端機視窗裡的 ssh_config(5) man page,偏偏那正是你在編輯時最不想來回切換的地方。

為了解決這件事,App 內建了一個完整的關鍵字目錄。KeywordRegistry.swift 目前包含 95 個專案,每個專案都定義了標準拼法、預期值型別(字串、整數、布林值、固定列舉、路徑或清單)、分類區段,以及直接從 ssh_config(5) 擷取的簡短說明:

.init(
    canonical: "IdentityFile", field: .path, category: .identity,
    help: "用於公鑰驗證的私鑰檔案。可重複設定。"),
.init(
    canonical: "ProxyJump", field: .string, category: .connection,
    help: "透過一個或多個跳板主機連線,例如 user@bastion:22。"),

這個目錄支援即時自動完成、可搜尋的「新增設定」選擇器,以及內嵌欄位說明——不再需要猜到底是 IdentityFile 還是 IdentityKey,或者 IdentitiesOnly 到底能不能接受檔案路徑。

編輯器引擎是完全無損的。註解、空白行和自訂縮排格式都會原封不動保留。儲存時只會重寫被修改的指令。這點非常重要:我的 SSH 設定檔裡有多年來累積的行內註解,解釋各種晦澀的主機設定,而那種會在儲存時重新排版或刪掉註解的工具,我只會用一次。

通道引擎從來不執行 ssh

Image description

最有趣的架構限制來自 Apple 的 App Store 規範。被 sandbox 限制的 macOS App 不能任意啟動系統的 /usr/bin/ssh 二進位檔。因此,所有 SSH 通道都必須在程式內透過 swift-nio-ssh 開啟,實作在 NIOTunnelEngine.swift 裡——執行流程中完全沒有任何 ssh 子程序。

這件事比整個 App 的其他部分加起來還更費工。三種轉發模式都建立在同一個連線模型上:

  • -L 會啟動一個本機 listener,將流量固定轉送到 direct-tcpip 通道的目標。
  • -D 則透過同一種通道型別,驅動一個動態 SOCKS5 proxy listener。
  • -R 則是反向運作:引擎向伺服器請求 tcpip-forward,把每個回傳的 forwarded-tcpip 通道對應到本機埠。

ProxyJump 指令在連線前會被遞迴解析成有順序的跳板鏈。每個跳板主機都會完整繼承其明確設定的內容(UserIdentityFile、巢狀 ProxyJump),行為與 OpenSSH 的評估方式一致。主機金鑰會對照 known_hosts 驗證,對於先前沒見過的主機則採用首次使用信任(TOFU)。

為了達到完全一致,還必須替 swift-nio-ssh 補上兩個缺少的能力:

  1. RSA 支援: swift-nio-ssh 預設不提供 RSA 金鑰支援。NIOSSHRSA 會在啟動時以自訂金鑰處理器註冊,因此來自檔案或 SSH agent 的 ssh-rsa 身分可以在握手時被提供。
  2. 後量子 KEX: 在 macOS 15 之前的系統上,CryptoKit 缺少原生的 ML-KEM 支援。啟動時會安裝一個備援的 ML-KEM-768 後端,以確保所有支援的 macOS 版本都能使用現代的後量子金鑰交換。

重新連線邏輯依賴 TunnelBackoff.swift 裡的確定性指數退避實作(1 秒、2 秒、5 秒、15 秒,並加上 ±20% 抖動上限)。把這段邏輯做成獨立的純型別,就能在不需要實際網路伺服器的情況下,完整進行單元測試。

有幾個明確的取捨值得一提:

  • ProxyCommand 會被直接拒絕,因為在 app sandbox 裡執行任意子程序是受限制的。
  • ControlMaster 多工指令會被解析並驗證,但不會執行,因為沒有本機子程序可供多工。
  • 關閉 App 時,正在運作的通道會結束。

對於某些邊緣情況,如果真的需要執行原生 binary,App 會產生並複製完整的命令字串到你的剪貼簿:

ssh -N -T -o ControlPath=none -L 3000:localhost:3000 prod-1

因為傳入的是主機別名而不是解析後的 IP 位址,ssh 會自行進行解析,保證行為和你在 shell 裡手動執行命令時完全一致。

看得見連線狀態就是最大的價值之一

視覺化介面有很明顯的營運優勢。能有一個清楚的儀表板,顯示正在運作的通道、反覆斷線的連線、已解析的身分金鑰,以及精確的錯誤診斷,和逐一查詢終端機指令相比,這些螢幕空間很值得。

最後證明最實用的功能之一,是內建的 known_hosts 稽核器。它透過靜態分析運作,不會造成網路副作用,並會自動標出以下問題:

  • 格式錯誤的專案: 無法解析的語法或損毀的行。
  • 重複專案: 主機定義重複,且金鑰彼此衝突或冗餘。
  • 孤兒專案:~/.ssh/config 中任何主機專案都對不上的主機金鑰。

雜湊過的專案在孤兒檢查時會被忽略,因為主機名稱無法反推;而已撤銷的金鑰專案則會刻意保留。這些發現單看都不戲劇化,但合在一起,會把 known_hosts 從一個越堆越亂的雜物抽屜,變成你真正看得懂、也願意維護的檔案。

App Store 的代價

這是我第一次把作品提交到 Mac App Store。整個流程既有成就感,也相當瑣碎。sandbox 限制造成了必須打造自訂的 in-process 通道引擎——這反而是個例子:平台限制最後導向了一個比呼叫 binary 更乾淨、更穩健的架構。

Apple Developer 的一次性費用不高,而我也偏好把 App 定價得單純一些,讓它能自己負擔維護成本,而不需要訂閱制。這是出於個人需要而做出來的工具,不管有多少人使用,它都仍然有價值。

如果你也在處理受限的伺服器架構,以及脆弱的 SSH 設定檔,最容易立刻採用的部分就是 known_hosts 稽核模式。找出格式錯誤、重複或孤兒行都很容易實作,而且能立刻清理掉很多開發者常常忽略的技術債。


原文出處:https://dev.to/malusev998/why-i-built-an-ssh-config-and-tunnel-manager-for-macos-58n8


精選技術文章翻譯,幫助開發者持續吸收新知。

共有 0 則留言


精選技術文章翻譯,幫助開發者持續吸收新知。
🏆 本月排行榜
🥇
站長阿川
📝11   💬4  
178
🥈
我愛JS
💬1  
6
評分標準:發文×10 + 留言×3 + 獲讚×5 + 點讚×1 + 瀏覽數÷10
本數據每小時更新一次
📢 贊助商廣告 · 我要刊登