概觀

systemcontroller 是 Town OS 的核心後端服務。它建構於 Echo v5 之上,正式環境中會監聽 5309 連接埠(TCP) 或一個 Unix 網域 socket。所有請求與回應主體都使用 JSON。錯誤遵循 RFC 9457application/problem+json)。

開發時會啟用 CORS。正式環境中,API 與介面部署在同一個來源之下。

身分驗證

呼叫 POST /account/authenticate 並附上使用者名稱與密碼即可驗證。 回應中會含有一個 Bearer 權杖。請在後續請求中帶上它:

Authorization: Bearer <token>

工作階段閒置 7 天後過期。共有五種驗證層級:

層級說明
公開不需要權杖。
已驗證任何有效的工作階段權杖。
管理者屬於管理者帳號的工作階段權杖。
授權帳號上的某項特定授權可以放行非管理者。物件儲存端點使用物件儲存授權;對等裝置註冊使用 wireguard 授權,而依網路的範圍與每個對等裝置的歸屬則由處理常式本身檢查。
Localhost來自 loopback 的請求不需驗證即可通過——能連到 loopback 本身就代表已經在這台機器上——其他來源則需要徽章旁標示的層級。systemd unit 與記錄端點使用它,因為控制器自己的工具會讀取這些端點。

即使分割區內部的端點並不限於管理者,建立物件儲存分割區仍然只保留給管理者: 分割區是一棵權限樹的根,並且會配置一個帶配額的 btrfs 子磁碟區, 因此授權允許你操作分割區內部的使用者,而不是允許你決定這個分割區該不該存在。

分頁

所有清單端點都接受下列查詢參數,並回傳統一的信封結構:

參數型別說明
sort_bystring用來排序的欄位名稱。
sort_orderstringascdesc
limitint每頁筆數(預設 20)。
offsetint分頁位移。
searchstring對所有字串欄位做不分大小寫的子字串比對。

回應信封

{
  "entries":     [...],
  "has_more":    true,
  "total_pages": 5,
  "total_count": 97
}

狀態

GET /status/ping 公開

健康檢查與系統概覽。未驗證的呼叫者會取得精簡回應,只含 statusneeds_setup。已驗證的呼叫者則取得完整的儀表板資料, 包含檔案系統數量、套件計數、unit 狀態摘要、磁碟使用量、外部/內部 IP,以及是否有可用升級。

帳號

POST /account/authenticate 公開

以使用者名稱與密碼驗證。回傳一個工作階段權杖與該帳號物件。

欄位型別說明
usernamestring必填。帳號使用者名稱。
passwordstring必填。帳號密碼。
POST /account/create 公開 / 管理者

建立新帳號。在初始啟動模式下(尚無任何已啟用的管理者帳號),此端點是公開的; 否則需要管理者驗證。建立的第一個帳號會成為管理者。密碼至少 8 個字元。 電子郵件、電話與真實姓名為必填。

欄位型別說明
usernamestring必填。
passwordstring必填。至少 8 個字元。
emailstring必填。
phonestring必填。
real_namestring必填。
adminboolean此帳號是否具有管理者權限。
POST /account 已驗證

依使用者名稱取得單一帳號。請求主體:{"username": "alice"}

GET /account 已驗證

列出所有帳號。支援分頁參數。

POST /account/update 已驗證

更新帳號欄位。送出 username 以指明帳號,並附上一個 fields 物件,其中可任意組合 passwordemailphonereal_nameadmin。 只有提供的欄位會被變更。

GET /account/me 已驗證

回傳與 Authorization 標頭中權杖關聯的使用者名稱。

GET /account/sessions 已驗證

列出目前已驗證使用者的所有作用中工作階段。每個工作階段含其 ID、使用者名稱、建立時間與最後使用時間。

POST /account/session/revoke 已驗證

依 ID 撤銷一個工作階段。請求主體:{"session_id": "..."}

POST /account/disable 管理者

停用一個帳號。請求主體:{"username": "bob"}

POST /account/enable 管理者

重新啟用一個已停用的帳號。請求主體:{"username": "bob"}

儲存

POST /storage 已驗證

列出檔案系統。除分頁參數外,請求主體中還可傳入選用的 name (前綴篩選)與 stateuserinstalleduninstalled)。

POST /storage/create 已驗證

建立一個新的 btrfs 子磁碟區。送出 name 與選用的 quota (位元組)。若配額為 0 或省略,則採用系統預設值(50 GB)。保留名稱 (installeduninstalledarchives)會被拒絕。

POST /storage/modify 已驗證

修改既有的檔案系統。送出 name 以指明對象,並附上一個 filesystem 物件,其中含更新後的 name 與/或 quota

POST /storage/remove 已驗證

移除一個檔案系統。請求主體:{"name": "mydata"}

POST /storage/upload-archive 管理者

上傳封存檔並解開到目標子磁碟區。接受 multipart/form-data, 含一個 subvolume 欄位與一個 archive 檔案。支援 .tar.gz.tgz.tar.bz2.tbz2.tar.xz.txz.tar.zip.7z

欄位型別說明
subvolumestring必填。目標子磁碟區路徑。
archivefile必填。要上傳的封存檔。
subpathstring選用。磁碟區內用來解開的相對路徑;會視需要建立。
stop_servicestring選用。解開前停止、完成後重新啟動的 systemd unit 名稱。
設定預設值說明
max_archive_size1 GB上傳大小上限。
archive_unpack_timeout600 秒解開封存檔的最長時間。
POST /storage/download-archive 管理者

下載子磁碟區內容的封存檔。會以所要求的格式回傳串流封存。

欄位型別說明
subvolumestring必填。來源子磁碟區路徑。
pathsstring[]選用。子磁碟區內要納入的特定路徑陣列。
stop_servicestring選用。封存期間停止、之後重新啟動的 systemd unit 名稱。
formatstring選用。壓縮格式:tar.gz(預設)、tar.bz2tar.xz
filenamestring選用。下載檔案的自訂主檔名,伺服器會補上對應副檔名。預設為 download
POST /storage/package-volumes 已驗證

依套件分組列出套件磁碟區,並可選擇是否納入已解除安裝的磁碟區。

POST /storage/remove-package-volume 管理者

依內部名稱刪除某個特定的套件磁碟區。

POST /storage/remove-package-volume-group 管理者

一次呼叫即刪除屬於某個套件的所有磁碟區,不必逐一依內部名稱移除。

套件庫

GET /repository 已驗證

列出所有已設定的套件庫,含名稱、網址與任何錯誤狀態。支援分頁參數。

POST /repository/add 已驗證

新增一個套件庫。會立即觸發一次重新整理。

欄位型別說明
namestring必填。套件庫的顯示名稱。
urlstring必填。套件庫的 Git 網址。
usernamestring選用。私人套件庫的驗證使用者名稱。
passwordstring選用。私人套件庫的驗證密碼。
POST /repository/remove 已驗證

依名稱移除一個套件庫。會立即觸發一次重新整理。 請求主體:{"name": "my-repo"}

POST /repository/move 管理者

把某個套件庫移到新的位置(從 0 起算)。當套件名稱衝突時,排在後面的套件庫會覆蓋前面的。 請求主體:{"name": "my-repo", "position": 0}

POST /repository/refresh 已驗證

強制立即重新整理所有套件庫的中繼資料。成功時回傳空的主體;若有套件庫失敗, 則回傳一個將套件庫名稱對應到錯誤字串的 JSON 物件。

套件

GET /packages 已驗證

列出所有套件庫中全部可用的套件。每筆項目含套件庫、名稱、版本、描述、supplies 標籤、 安裝狀態,以及是否有可用升級。支援分頁參數。

GET /packages/by-repo 已驗證

依套件庫分組列出套件。接受選用的 search 查詢參數。 回傳一個由 {"repo": "...", "packages": [...]} 組成的陣列。

GET /packages/installed 已驗證

列出已安裝套件的識別碼。支援分頁參數。

POST /packages/installed/info 已驗證

取得某個已安裝套件的詳細資訊。送出 reponameversion。回傳問題、使用者回答、註記與註記型別。

POST /packages/responses 已驗證

取得某個已安裝套件所保存的問題回答。送出 reponameversion。回傳一份鍵值對應。

POST /packages/versions 已驗證

列出某個套件的可用版本。請求主體:{"name": "nginx"}。 回傳一個版本識別碼的字串陣列。

POST /packages/children 已驗證

列出子套件。送出 reponame。回傳一個字串陣列。

POST /packages/questions 管理者

取得某個套件的安裝問題。請求主體:{"name": "nginx"}。 回傳一份從問題鍵到 {"query": "...", "type": "..."} 的對應。

POST /packages/questions/identity 管理者

取得特定套件版本的問題。送出 reponameversion

POST /packages/oauth/start 管理者

為某個 oauth 問題啟動 OAuth 裝置流程。送出 reponameversionquestion。系統控制器 會向供應商執行該流程的起始步驟,並回傳 flow_idapprove_url(請在使用者的瀏覽器中開啟)、 選用的 user_code,以及 interval_ms——輪詢的頻率。

供應商的各個網址來自套件而非 Town OS,因此呼叫前會先檢查: 只允許 https,而且絕不能是主機自身網路上的位址。

POST /packages/oauth/poll 管理者

輪詢上面啟動的流程。送出 flow_id。回傳 status: 使用者尚未核准時為 pending,核准完成時為 approved 並附上 token,而流程逾時或權杖已被取走後則為 expired—— 每個流程只能使用一次。取得的權杖接著會作為該問題的答案送到 /packages/install,與手動輸入的回答完全相同。

POST /packages/install-preview 管理者

在真正安裝之前先預覽這次安裝會做什麼。送出 reponameversion。回傳磁碟區細節、連接埠對應、 磁碟使用量、配額資訊、升級的來源版本,以及一段易讀的摘要。

POST /packages/install 管理者

安裝一個套件。

欄位型別說明
repostring必填。套件庫名稱。
namestring必填。套件名稱。
versionstring必填。要安裝的版本。
responsesobject必填。對安裝問題的鍵值回答。
reuse_volumesboolean重用先前安裝留下的資料磁碟區。
import_from_versionstring升級時要從哪個版本匯入磁碟區。
POST /packages/uninstall 管理者

解除安裝一個套件。送出 reponameversion, 以及選用的 purge_volumes(布林值)以刪除相關資料。

POST /packages/disable 管理者

停用一個已安裝的套件(停止其服務)。送出 reponame

POST /packages/enable 管理者

重新啟用一個已停用的套件(啟動其服務)。送出 reponame

POST /packages/purge-volumes 管理者

刪除某個已安裝套件的所有資料磁碟區。送出 reponame

POST /packages/uninstalled-volumes 管理者

檢查某個套件是否有先前安裝留下的磁碟區。送出 reponame。回傳 has_uninstalled_volumesuninstalled_versionsinstalled_versions

POST /packages/purge-uninstalled-volumes 管理者

刪除先前已解除安裝版本留下的磁碟區。送出 reponame

GET /packages/upgrades 已驗證

列出已安裝套件的可用升級。每筆項目含 installed_versionlatest_version,以及套件定義是否 changed

POST /packages/upgrades/dismiss 管理者

忽略目前的升級通知。送出一個空的 JSON 物件。

POST /packages/manifest 已驗證

回傳原始的 YAML 套件定義。送出 reponameversion。以 Content-Type: text/x-yaml 回傳檔案內容。 若該套件檔案不存在則回傳 404。

GET /packages/featured 已驗證

列出所有套件庫中的精選套件。

POST /packages/last-responses 已驗證

取回某個套件快取的 last 回答。送出 reponame。回傳上一次解除安裝時保存的回答,供重新安裝時重用。

POST /packages/clear-last-responses 管理者

刪除某個套件快取的 last 回答檔案。送出 reponame

POST /packages/rebuild-git 管理者

為某個已安裝套件中由 git 填入的磁碟區拉取最新變更,並重新啟動相依的服務。 送出 reponameversion。 重新建置之前會依已保存的回答重新求值範本變數。

Systemd

GET /systemd/units 已驗證 / Localhost

列出由 Town OS 管理的 systemd unit。每筆項目含 unit 名稱、描述、 load/active/sub 狀態、關聯的套件識別碼與描述,以及一個失敗旗標。支援分頁參數。

GET /systemd/units-tree 已驗證 / Localhost

與扁平清單相同的 unit,但依相依關係組成樹狀:根套件在最上層,相依項逐層巢狀於其父項之下—— 與 /storage/package-volumes 採用的結構相同。每一列都帶有扁平端點回傳的相同狀態資料, 因此用戶端不需要再送一次請求來補齊資訊。

POST /systemd/status 管理者

控制某個 systemd unit。送出 name(unit 名稱)與 actionstartstoprestartenabledisable)。

POST /systemd/status/tree 管理者

一次呼叫即依相依順序,對某個套件及其整棵相依樹套用同一個動作。 這裡同樣拒絕 enabledisable,理由與 /systemd/status 相同: 連鎖執行 enable 會把那些已透過父項連結的相依項重複啟用一次。

GET /systemd/logs 管理者 / Localhost

透過 Server-Sent Events 即時串流某個 unit 的 journal 項目。傳入 unit 查詢參數;留空或使用 __system__ 會回傳全系統記錄。 每個 SSE 事件都含一筆 JSON 編碼的 journal 項目,欄位如 MessagePriorityRealtimeTimestampSystemdUnit

GET /systemd/logs/tail 管理者 / Localhost

取得一頁 journal 項目,支援以游標分頁與篩選。

參數型別說明
unitstringsystemd unit 名稱。留空或使用 __system__ 表示全系統記錄。
linesint要回傳的項目數(預設 100)。
beforestring游標——回傳該位置之前的項目。
afterstring游標——回傳該位置之後的項目。
grepstring對訊息文字做不分大小寫的子字串篩選。
sinceintUnix 時間戳記——回傳該時間之後的項目。
untilintUnix 時間戳記——蒐集到該時間為止。
priorityintsyslog 嚴重程度篩選(0 表示不篩選)。

回傳 entriescursor(第一筆)與 end_cursor(最後一筆),供後續分頁使用。

GET /systemd/logs/tree 管理者 / Localhost

/systemd/logs 的樹狀對應端點:以一條 Server-Sent Events 串流承載某個套件 及其之下所有 unit 的記錄,並依時間順序合併。即使是沒有安裝紀錄的未知根, 也會得到一條開啟但沒有項目的串流,而不是 404,因此記錄檢視器對單一 unit 與整棵樹的處理方式完全一致。

GET /systemd/logs/tree/tail 管理者 / Localhost

合併後樹狀記錄的分頁形式,接受與 /systemd/logs/tail 相同的游標、篩選與時間範圍參數。

設定

GET /settings 管理者

以鍵值物件取得所有設定。

POST /settings/get 管理者

取得單一設定項。請求主體:{"key": "default_quota"}。 回傳 keyvalue

POST /settings/set 管理者

設定某個項目的值。請求主體:{"key": "default_quota", "value": "107374182400"}

預設設定

預設值說明
default_quota53687091200(50 GB)新檔案系統的預設配額。
max_archive_size1073741824(1 GB)封存檔上傳大小上限。
archive_unpack_timeout600(秒)解開封存檔的最長時間。
localeen-US用於國際化的全系統語言地區。
proton_imagequay.io/town/proton:latestProton/Wine 執行器容器映像檔。
dns_tldhome本機 DNS 解析所用的頂層網域。

稽核記錄

POST /audit/log 管理者

列出稽核記錄項目。請求主體中的所有欄位皆為選填。

欄位型別說明
before_idint鍵集分頁——回傳 ID 小於此值的項目。
accountstring依帳號使用者名稱篩選。
sort_bystring用來排序的欄位。
sort_orderstringascdesc
limitint每頁筆數。
offsetint分頁位移。
searchstring搜尋篩選條件。

每筆稽核項目含 idaccountactionpathdetailsuccesserrorcreated_at。被稽核的動作包括:身分驗證,建立/更新/停用帳號, 撤銷工作階段,安裝/解除安裝/停用/啟用套件,建立/修改/移除檔案系統, 新增/移除/移動/重新整理套件庫,上傳/下載封存檔,更新設定,忽略升級,以及清除磁碟區。

頁面

靜態網站代管,支援三種內容來源型別:上傳封存檔、容器映像檔與 git 儲存庫。 使用者為其指定一個網域,系統則透過 Caddy 容器提供內容。 所有會造成變更的端點都需要管理者驗證;清單端點只需一般驗證。

GET /pages 已驗證

列出所有頁面,支援排序、搜尋與分頁。可依名稱、儲存庫網址、 分支、網域、來源型別、狀態與時間戳記排序。

POST /pages/create 管理者

建立一個新頁面。接受名稱、來源型別(archivecontainer_imagegit)、儲存庫網址、分支、網域、 容器映像檔與映像檔內目錄。來源型別預設為 archive。 git 與容器映像檔類型的頁面會以非同步方式佈建。

POST /pages/upload 管理者

為封存類型的頁面上傳內容的 tar 封存檔。接受含 namearchive 檔案的 multipart 表單。僅對來源型別為 archive 的頁面有效;其他來源型別會回傳 400。

POST /pages/update 管理者

對頁面的儲存庫網址、分支、網域、來源型別、容器映像檔或映像檔內目錄做部分更新。 只有提供的欄位會被變更。

POST /pages/remove 管理者

從資料庫刪除一個頁面,移除 webroot 符號連結,並刪除對應的 btrfs 子磁碟區。

POST /pages/rebuild 管理者

從來源重新建置頁面內容。git 頁面會拉取最新變更;容器映像檔頁面會從映像檔重新擷取。 封存頁面會回傳 400(請改用 /pages/upload 重新上傳)。

網路

網路是一個具名的 WireGuard 疊加網路,並與一個 DNS TLD 配對。 套件安裝到某個網路中,對等裝置加入這個網路,而 TLD 決定誰能解析什麼。 網路名稱必須是合法的 DNS 標籤,且不超過 32 個字元,因為它們同時會被當作 WireGuard 介面字尾與 systemd unit 名稱使用。

home 網路始終存在——它隨資料庫一併植入,而不是在開機時建立——並且在三個方面很特別: 它無法刪除,也不能被第二次建立;它只提供 DNS (沒有 WireGuard 介面、沒有子網路、沒有對等裝置);而且在它上面註冊對等裝置會被拒絕, 回傳 400。每個帳號都屬於 home 網路,因此若在此接受註冊,單憑成員身分就等於取得了進入通道的途徑, 而且儲存下來的對等裝置所描述的通道根本不存在。

停用網路只會關掉傳輸層:WireGuard 介面不會被拉起,遠端存取因此中斷, 而本地 DNS 解析與容器本身照常執行。

GET /networks 已驗證

列出網路。每筆項目含名稱、TLD、子網路、本機在疊加網路中的位址、公鑰、監聽連接埠與啟用旗標。 私鑰永遠不會被序列化。

POST /networks/create 管理者

建立網路。子網路由本機識別種子與網路名稱以確定性方式推導而來,取自 10.64.0.0/10,以避開家用路由器常用的位址範圍。 以本機識別為依據,代表兩台都在提供對等接入的 Town OS 主機會選出不同的子網路, 因此同時加入兩者的裝置永遠不會遇到衝突。建立 home 會因 TLD 衝突檢查而回傳 409。

POST /networks/remove 管理者

刪除網路。會拒絕 home 網路。

POST /networks/enable 管理者

拉起該網路的 WireGuard 傳輸層。

POST /networks/disable 管理者

關掉傳輸層,同時讓 DNS 與容器繼續執行。

對等裝置

GET /networks/peers 已驗證

列出已在某個網路中註冊的對等裝置。

GET /networks/peers/connected 管理者

列出目前確實處於連線狀態的對等裝置,而不只是已註冊的。

POST /networks/peers/add 授權

註冊一個對等裝置。wireguard 授權是放行非管理者的依據;依網路的範圍與每個對等裝置的歸屬由處理常式檢查。 對 home 網路會回傳 400,因為它只提供 DNS。

POST /networks/peers/refresh 授權

在對等裝置的 TTL 到期之前續期其註冊。註冊是有時效的,回收程序會清掉已過期的項目。

POST /networks/peers/remove 管理者

把某個對等裝置從網路中移除。

本地 CA

GET /tls/ca.crt 公開

以 PEM 格式下載本機的本地憑證授權單位。Town OS 會為套件名稱簽發自己的憑證, 因此信任這張憑證正是讓那些名稱在瀏覽器中不再出現警告的關鍵。它刻意設為公開: CA 憑證本來就是拿來散布的那一部分,而且用戶端在持有任何可用於驗證的憑據之前就需要它。

物件儲存

Town OS 透過 gfeh 提供物件儲存。一個分割區包含一個 btrfs 子磁碟區、 一個 gfehd 行程、一個管理 socket,以及它自己的一套使用者。 每個 Town OS 網路恰好對應一個分割區,因此物件儲存的命名空間與 DNS、WireGuard 沿著同一條邊界劃分: office 分割區中的主體、授權或公開曝光,在 home 中毫無意義。

每個分割區在固定的容器連接埠上提供四種 HTTP 檢視——S3 在 9000、HTTP 在 9001、雲端硬碟在 9002、IPFS 在 9003—— 並且完全不發布主機連接埠。這正是固定連接埠之所以安全的原因: 每個分割區都有自己的網路命名空間,ingress 透過容器名稱存取它,與存取套件的方式完全一樣, 所以兩個都在 9000 上提供 S3 的分割區不可能衝突。

分割區

這四條路由之所以獨立於 /storage/*,是因為 /storage/create 會無條件把送進來的名稱改寫成 user/<名稱>,因此無法產生 gfeh/ 前綴底下的磁碟區。它們的封包形態是 gfeh 用戶端會解析的公開契約,而非內部細節。

有兩處細節至關重要。前綴是不對稱的:請求帶的是裸名稱, 回應帶的是 gfeh/<名稱>,因為該前綴是 Town OS 的命名空間產物, 並不屬於分割區本身的身分。以及清單回傳的是裸 JSON 陣列,而不是分頁信封, 這與本 API 中其他所有清單端點都不同:gfeh 用戶端會直接反序列化一個普通清單, 而分頁包裝會導致解碼失敗。

路由權限請求回應
POST /gfeh/partitions/create管理者name(不帶前綴)、quotaFilesystem,名稱為 gfeh/<n>
POST /gfeh/partitions/modify管理者namequotaFilesystem
POST /gfeh/partitions/remove管理者name200,空回應
POST /gfeh/partitions已驗證無請求主體Filesystem 的普通陣列

用戶端應據以分支的狀態碼:409 已存在(gfeh 的佈建是「建立或調整大小」, 正是靠這個狀態區分兩者)、404 不存在、400 名稱不合法、 403 非管理者。含有路徑分隔符號的名稱在這裡就會被拒絕, 因為 gfehd 在它自己的邊界上同樣會拒絕——雙方對「什麼是合法分割區名稱」若不一致, 就會讓 ../user/something 這樣的名稱指向物件儲存根目錄之外的磁碟區。

建立分割區僅限管理者,且無法透過授權取得: 它是一棵權限樹的根,並且會配置一個帶配額的 btrfs 子磁碟區, 因此持有授權的帳號會在任何處理常式執行之前就被拒絕。

瀏覽

GET /gfeh 已驗證

物件儲存總覽:存在哪些分割區,以及它們各自處於什麼狀態。

主體

分割區中的使用者。建立一個主體需要名稱、父項與權限上限——但不需要密碼, 這正是介面從不索取密碼的原因。上限遵循 gfeh 的投影規則: Town OS 管理者為 all,其餘為讀/寫。

GET /gfeh/principals 已驗證

列出某個分割區中的主體。

POST /gfeh/principals/add 授權

在某個父項底下建立主體,並設定其權限上限。

POST /gfeh/principals/remove 授權

刪除一個主體。

授權

也就是存取控制清單。gfehd把授權收窄到主體的權限上限, 因此用戶端應顯示回傳回來的權限,而不是自己送出去的那些: 管理者必須能夠看出某項授權被收窄了。

GET /gfeh/grants 已驗證

列出授權,也可以只列出某一個主體的。

POST /gfeh/grants/add 授權

授予某個主體存取權限。回應中帶有實際儲存下來的權限。

POST /gfeh/grants/revoke 授權

依 id 撤銷一項授權。

公開曝光

一條已發布的檔案連結,服務於 /f/<token>

GET /gfeh/exposures 已驗證

列出某個分割區中已發布的連結。

POST /gfeh/exposures/withdraw 授權

依 token 撤回一條已發布的連結,使該網址不再可用。

DNS

rolodex-dns 容器驅動的整合式本機 DNS 解析器。它為已安裝的套件 管理區域檔與記錄,並透過 gRPC Unix socket 介面提供本機名稱解析。

GET /dns/status 已驗證

回傳 DNS 狀態,包含啟用旗標、執行狀態、TLD 與記錄數量。

GET /dns/records 已驗證

列出所有 DNS 記錄。

POST /dns/records/add 管理者

新增一筆 DNS 記錄。接受名稱、記錄型別、值與 TTL。

POST /dns/records/remove 管理者

依名稱與型別移除一筆 DNS 記錄。

GET /dns/tld 已驗證

取得目前的頂層網域設定。

POST /dns/tld 管理者

設定 TLD。會變更既有的 TLD 並重新註冊所有已安裝的套件。

POST /dns/setup 管理者

初始化或重新啟動 DNS 伺服器,並註冊所有已安裝的套件。

封鎖清單

這是兩份彼此獨立的清單。DNSBL 以訂閱為基礎——由 rolodex 取回並套用的上游封鎖清單—— 並搭配一份放行清單,用來豁免那些無論上游清單怎麼說都希望能解析的名稱。 本地封鎖清單(RBL)則是本機自己的清單,逐筆編輯。

GET /dns/dnsbl 已驗證

取得 DNSBL 設定:訂閱了哪些上游封鎖清單,以及如何套用它們。

POST /dns/dnsbl 管理者

取代 DNSBL 設定。

GET /dns/dnsbl/allowlist 已驗證

列出已從訂閱封鎖清單中豁免的名稱。

POST /dns/dnsbl/allowlist/add 管理者

把某個名稱從訂閱的封鎖清單中豁免出來。

POST /dns/dnsbl/allowlist/remove 管理者

刪除一筆放行紀錄,讓訂閱的封鎖清單重新對該名稱生效。

GET /dns/rbl/local 已驗證

列出本機自有封鎖清單中的項目。

POST /dns/rbl/local/add 管理者

在本地封鎖清單中加入一個名稱。

POST /dns/rbl/local/remove 管理者

從本地封鎖清單移除一個名稱。

依服務發布 DNS

GET /dns/services 已驗證

列出已安裝的服務,以及每個服務是否發布了 DNS 名稱。

POST /dns/services/set 管理者

為某個服務開啟或關閉 DNS 發布,讓套件可以在不佔用網路名稱的情況下執行。

監控

整合的 Prometheus、Node Exporter 與 Grafana 堆疊,用於系統監控。 整套元件以受 systemd 監管、帶 Restart=always 的 podman 容器執行。

GET /monitoring/status 已驗證

回傳每個監控服務的容器狀態(名稱、映像檔、執行狀態、連接埠)。 未設定監控時會回傳 {"status": "disabled"}

如何取得儀表板資料

系統控制器上並不存在任何反向代理。監控資料由專屬的 5308 連接埠提供,瀏覽器直接與該連接埠通訊;控制器自己的連接埠(5309)只承載 /monitoring/status。5308 上由誰監聽,取決於所設定的後端:

  • uPlot 模式(預設)——由一個 socat 轉送器把 Prometheus 的 HTTP API 開在 5308 上, 介面直接查詢 /api/v1/query_range,並自行繪製圖表。
  • Grafana 模式——Grafana 透過 podman 的連接埠對應直接監聽 5308,介面則以 iframe 內嵌。

TOWN_OS_MONITORING_PORT 可以更動儀表板連接埠; TOWN_OS_PROMETHEUS_PORTTOWN_OS_NODE_EXPORTER_PORT 對兩個 loopback 連接埠有同樣作用。

系統服務

系統服務是由 systemd 管理的基礎架構容器(與使用者安裝的套件服務不同)。 它們使用 town-os-system-- 作為 unit 名稱前綴。

GET /system-services 公開 / 已驗證

列出系統服務與其即時 unit 狀態。從本機存取時不需驗證。 每筆項目含鍵、顯示名稱、映像檔、連接埠與 systemd unit 狀態欄位。

POST /system-services/status 管理者

控制某個系統服務。接受 keyactionstartstoprestart)。

POST /system-services/refresh 管理者

重新整理系統服務的 unit 檔案與狀態。

語言地區

系統的國際化語言地區資訊。

GET /locales 已驗證

回傳目前的語言地區、已填入的語言地區清單、常見語言(含其母語字體名稱) 以及延伸語言地區。使用 BCP 47 語言代碼。

虛擬機器映像檔

管理虛擬機器套件所使用、已快取的虛擬機器磁碟映像檔。遠端映像檔會被下載, 並透過 qemu-img convert 轉換成 raw 格式;轉換後的映像檔會快取在 vm-images 子磁碟區中。

GET /vm-images 已驗證

列出已快取的虛擬機器磁碟映像檔。回傳每個映像檔的名稱與檔案大小。

POST /vm-images/upload 管理者

從某個網址下載虛擬機器映像檔並轉換成 raw 格式。接受一個網址與選用的名稱。 名稱預設取自網址中的檔名,並加上 .raw 副檔名。 下載的逾時時間為 30 分鐘。

POST /vm-images/delete 管理者

依名稱移除一個已快取的虛擬機器映像檔。

物件儲存管理 API(gfeh)

以上都是 Town OS 的 API,通常應用程式應該使用它。在它之下, 每個 gfehd 分割區還有自己的管理介面: 以 JSON over HTTP 提供,而且只在它的 Unix socket 上,絕不監聽連接埠。

這個介面上既沒有權杖,也沒有身分驗證。socket 的檔案系統權限本身就是存取控制, 因此能夠連到它,就已經代表是這台機器上的 root。socket 位於 btrfs 磁碟區上, 因為那是 gfehd 容器與系統控制器容器都看得到的唯一一個檔案系統。

呼叫方法與路徑用途
HealthGET /v1/health存活探測,同時也用作就緒探測。
NamesGET /v1/names該分割區希望發布的名稱。
ListPrincipalsGET /v1/principals該分割區的使用者樹林。
CreatePrincipalPOST /v1/principals接受 nameparentceiling——不含密碼。
DeletePrincipalDELETE /v1/principals/<name>移除一個主體。
ListGrantsGET /v1/grants?principal=存取控制清單,可只取某一個主體的。
CreateGrantPOST /v1/grants授予存取權限;會被收窄到主體的上限。
RevokeGrantDELETE /v1/grants/<id>撤銷一項授權。
ListExposuresGET /v1/exposures已發布的 /f/<token> 連結。
WithdrawExposureDELETE /v1/exposures/<token>停止提供某條已發布的連結。

gfehd 會把內部錯誤對應成 HTTP 狀態碼——404、409、400—— 而 Go 用戶端再把它們對應回哨兵錯誤,因此 errors.Is 跨過 socket 邊界之後依然可用。

對於名為 <network> 的網路,其分割區的檔案位置如下:

內容位置
分割區資料<btrfsBase>/gfeh/<network>,掛載於 /data/<network>
設定<btrfsBase>/gfeh-control/<network>/gfehd.yaml
管理 socket<btrfsBase>/gfeh-control/<network>/run/admin.sock
unittown-os-system--gfeh-<network>.service

DNS gRPC API(rolodex)

上面的 /dns/* 端點是 Town OS 視角下的 DNS。rolodex 本身透過 gRPC 管理,開在一個 Unix socket 上(預設為 /var/run/rolodex-dns.sock),也可以選擇開在 TCP 上。 預設情況下,socket 是唯一的管理通道——grpc.tcp_bind 是空的。

只有一個服務 rolodex_dns.RolodexDnsService,包含 74 個方法。 所有路徑均為 /rolodex_dns.RolodexDnsService/<方法名稱>。 完整的訊息定義位於 rolodex-dns 儲存庫的 proto/rolodex_dns.proto; 下面的分組說明這些方法各自涵蓋什麼。

記錄與解析

方法用途
AddRecord在本地資料庫加入一筆 DNS 記錄。
RemoveRecord從本地資料庫移除記錄。
ListRecords以可選的篩選條件查詢本地資料庫。
SetForwarders設定上游轉送器。
SetResolutionMode / GetResolutionMode在執行時變更並讀取解析模式。
GetSearchDomains某個用戶端 IP 對應的搜尋網域。
FlushCache清空 DNS 與封鎖清單快取。

權威區域

方法用途
AddAuthoritativeZone把某個區域宣告為權威區域。
RemoveAuthoritativeZone把某個區域從權威清單中移除。
ListAuthoritativeZones列出所有權威區域。

網路範圍

範圍是 rolodex 劃分「誰能解析什麼」的方式,也是 Town OS 網路所對應到的對象。 IP 與範圍的關聯帶有 TTL,需要定期更新。

方法用途
CreateNetworkScope / DeleteNetworkScope / ListNetworkScopes管理範圍。刪除一個範圍會連同它的記錄與關聯一起刪除。
JoinNetwork / LeaveNetwork把用戶端 IP 關聯到某個範圍,或解除該關聯。
GetNetworkAssociations讀取 IP 與範圍的關聯。
AddScopedRecord / RemoveScopedRecord / ListScopedRecords只在某個範圍內存在的記錄。

範圍 TLD

依網路歸屬的區域,在各網路之間劃分。

方法用途
AddScopeTld / RemoveScopeTld / ListScopeTlds把一個全域唯一的 TLD 註冊為某個範圍所有。
SetScopeTldForwarders / ListScopeTldForwarders某個範圍的 TLD 所對應的對端轉送器。
ListScopeTldListeners繫結到某個範圍各 TLD 上的 ingress DNS 監聽器。

封鎖清單

方法用途
SetDnsblConfig / GetDnsblConfig以訂閱為基礎的網域封鎖清單設定。
AddDnsblAllowlistEntry把某個名稱及其子網域從依名稱的封鎖檢查中豁免。
RemoveDnsblAllowlistEntry / ListDnsblAllowlistEntries管理放行清單。
AddLocalBlocklistEntry / RemoveLocalBlocklistEntry / ListLocalBlocklistEntries本機自有的封鎖清單。

加密傳輸

每種傳輸都有對應的設定與讀取方法。DoH 提供 HTTP/2,並在 enable_h3 開啟時, 於同一位址、同一連接埠、同一憑證上提供 HTTP/3。

方法用途
SetDotConfig / GetDotConfigDNS over TLS。
SetDohConfig / GetDohConfigDNS over HTTPS,含 HTTP/3。
SetDoqConfig / GetDoqConfigDNS over QUIC。
SetProxyConfig / GetProxyConfigHTTP 代理設定。

DNSSEC、DANE 與 ACME

方法用途
GenerateDnssecKey / ListDnssecKeys / DeleteDnssecKey依區域管理 DNSSEC 金鑰材料。
GetDsRecords某個區域的 DS 記錄。
SignZone以區域自己的 DNSSEC 金鑰為其簽章。
GenerateTlsaRecord / ListTlsaRecords由憑證產生的 TLSA 記錄。
GenerateDaneRootCa產生用於 DANE 的根 CA 憑證。
EnsureZoneCa確保某個區域擁有 CA。
RequestAcmeCert / GetAcmeStatus透過 ACME DNS-01 申請憑證,並查詢其狀態。
CreateEabCredential / RemoveEabCredential簽發限定於某個區域的外部帳戶繫結(kid 加 HMAC),供 ACME 用戶端的 newAccount 使用。
ListAcmeAccounts / ListAcmeCertificates已註冊的 ACME 帳戶與已簽發的憑證。

DHCP

方法用途
AddDhcpPool / RemoveDhcpPool / ListDhcpPools在某個範圍內用於配發的位址池。
ListDhcpLeases / DeleteDhcpLease位址租約,依 MAC 位址刪除。
SetDhcpCertOption / RemoveDhcpCertOption / ListDhcpCertOptions為某個範圍透過 DHCP 下發給用戶端的憑證。

診斷與調校

方法用途
GetCacheStats / FlushDnsCache快取統計,以及清空回應快取。
GetQueryLatencyStats上游查詢延遲。
SetTtlDriftConfig / GetTtlDriftConfigTTL 漂移設定。
SetTrackedTlds / ListTrackedTlds依 TLD 指標背後所追蹤的 TLD 清單,包括已儲存的與實際生效的。
SetDns64Config / GetDns64ConfigDNS64 設定。

用戶端函式庫

Town OS 內附 Go 與 JavaScript 用戶端函式庫,涵蓋完整的 API。 兩個用戶端遇到非 200 回應時,都會依據 RFC 9457 的問題細節拋出帶型別的錯誤。

Go 用戶端

Go 用戶端位於 src/svc/systemcontroller/client.go,實作了 Client 介面。它同時支援 Unix socket 與 HTTP 連線。

// 透過 Unix 網域 socket 連線(正式環境)
client := systemcontroller.InitClient("/run/town-os/systemcontroller.sock")

// 透過 HTTP 連線(開發 / 測試)
client := systemcontroller.FromClient(http.DefaultClient, "http://localhost:5309")

驗證完成後請設定 client.Token。所有方法的第一個參數都是 context.Context

儲存

方法說明
CreateFilesystem(ctx, fs)建立一個新的 btrfs 子磁碟區。
ModifyFilesystem(ctx, name, fs)重新命名檔案系統或調整其大小。
RemoveFilesystem(ctx, name)依名稱刪除檔案系統。
ListFilesystems(ctx, prefix, state, params)依名稱前綴與狀態("user""installed""uninstalled")篩選的分頁清單。

套件庫

方法說明
AddRepository(ctx, name, rawURL, username, password)註冊一個套件庫,憑證選填。
RemoveRepository(ctx, name)依名稱移除套件庫。
MoveRepository(ctx, name, position)調整優先權(0 = 最高)。
RefreshRepositories(ctx)重新整理所有中繼資料。回傳錯誤對應。
ListRepositories(ctx, params)套件庫的分頁清單。

套件

方法說明
ListPackages(ctx, params)可用套件的分頁清單。
ListPackagesByRepo(ctx, params)依套件庫分組的套件。
ListPackageVersions(ctx, name)某個套件的可用版本。
GetPackageQuestions(ctx, name)依名稱取得設定問題。
GetPackageQuestionsByIdentity(ctx, repo, name, version)特定版本的問題。
ListChildren(ctx, repo, name)子套件的名稱。
InstallPreview(ctx, repo, name, version)不實際安裝,預覽磁碟區與連接埠。
InstallPackage(ctx, name, version, responses, reuseVolumes, importFromVersion, skipResponseReuse)安裝一個套件。name 採 "repo/package" 格式。
UninstallPackage(ctx, repo, name, version, purgeVolumes)移除一個已安裝的套件。
DisablePackage(ctx, repo, name)停止服務但不解除安裝。
EnablePackage(ctx, repo, name)重新啟用一個已停用的套件。
PurgeVolumes(ctx, repo, name)刪除某個套件的所有資料磁碟區。
ListUninstalledVolumes(ctx, repo, name)檢查是否有殘留的磁碟區。
PurgeUninstalledVolumes(ctx, repo, name)刪除殘留的磁碟區。
ListInstalled(ctx, params)"repo/name@version" 形式列出已安裝的套件。
GetResponses(ctx, repo, name, version)已儲存的設定回答。
GetInstalledInfo(ctx, repo, name, version)詳細資訊,含問題、回答與註記。

Systemd

方法說明
ListUnits(ctx, params)systemd unit 的分頁清單。
SetUnitStatus(ctx, name, action)執行 "start""stop""restart"
LogReplay(ctx, name)透過 SSE 串流 journal 項目。回傳一個 channel。
LogTail(ctx, params)一頁 journal 項目,支援游標分頁、grep、時間範圍與優先層級篩選。

帳號

方法說明
Authenticate(ctx, username, password)回傳工作階段權杖與帳號。
CreateAccount(ctx, username, password, email, phone, realName, admin)建立一個使用者。密碼至少 8 個字元。
GetAccount(ctx, username)依使用者名稱取得帳號。
UpdateAccount(ctx, username, fields)修改帳號欄位(password、email、phone、real_name、admin)。
ListAccounts(ctx, params)帳號的分頁清單。
DisableAccount(ctx, username)阻止其進行身分驗證。
EnableAccount(ctx, username)重新啟用一個已停用的帳號。
ListSessions(ctx, token)該權杖所屬使用者的作用中工作階段。
SessionUsername(ctx, token)工作階段權杖對應的使用者名稱。
RevokeSession(ctx, sessionID)使某個工作階段失效。

稽核、設定與升級

方法說明
ListAuditLog(ctx, opts, token)帶篩選條件的分頁稽核記錄。
GetSettings(ctx)以鍵值對應回傳所有設定。
GetSetting(ctx, key)依鍵取得單一設定。
SetSetting(ctx, key, value)更新某個設定。
ListUpgrades(ctx)有較新版本可用的套件。
DismissUpgrades(ctx)將待處理的升級標為已忽略。

封存檔

方法說明
UploadArchive(ctx, subvolume, archiveReader, filename, subpath, stopService)上傳封存檔並解開到某個子磁碟區。格式:tar.gz、tar.bz2、tar.xz。
DownloadArchive(ctx, subvolume, paths, stopService, format)為子磁碟區內容建立封存檔。回傳一個 io.ReadCloser

健康檢查

方法說明
Ping(ctx)服務健康狀況與各項計數摘要。

JavaScript 用戶端

JavaScript 用戶端位於 ui/src/api/,由 Town OS 儀表板介面使用。 它以一組模組化的 mixin 建構在 SystemControllerClient 類別之上。 非 200 回應會拋出 ApiError,其中帶有解析後的 RFC 9457 問題細節。

import SystemControllerClient from './api/client.js';

const client = new SystemControllerClient('http://localhost:5309');

// 驗證完成後
const result = await client.authenticate('admin', 'password');
client.setToken(result.token);

儲存

方法說明
createFilesystem(fs)建立一個新的 btrfs 子磁碟區。
modifyFilesystem(name, fs)重新命名檔案系統或調整其大小。
removeFilesystem(name)依名稱刪除檔案系統。
listFilesystems(prefix, sortBy, sortOrder, state, limit, offset, search)帶篩選條件的分頁清單。

套件庫

方法說明
addRepository(name, url, username?, password?)註冊一個套件庫,憑證選填。
removeRepository(name)依名稱移除套件庫。
moveRepository(name, position)調整優先權(0 = 最高)。
refreshRepositories()重新整理所有中繼資料。回傳錯誤對應或 null。
listRepositories(sortBy, sortOrder, limit, offset, search)分頁清單。

套件

方法說明
listPackages(sortBy, sortOrder, limit, offset, search)可用套件的分頁清單。
listPackagesByRepo(search)依套件庫分組的套件。
listPackageVersions(name)某個套件的可用版本。
getPackageQuestions(name)依名稱取得設定問題。
getPackageQuestionsByIdentity(repo, name, version)特定版本的問題。
installPreview(repo, name, version)不實際安裝,預覽磁碟區與連接埠。
installPackage(repo, name, version, responses, reuseVolumes?, importFromVersion?)帶設定回答安裝一個套件。
uninstallPackage(repo, name, version, purgeVolumes?)移除一個已安裝的套件。
disablePackage(repo, name)停止服務但不解除安裝。
enablePackage(repo, name)重新啟用一個已停用的套件。
purgeVolumes(repo, name)刪除某個套件的所有資料磁碟區。
listUninstalledVolumes(repo, name)檢查是否有殘留的磁碟區。
purgeUninstalledVolumes(repo, name)刪除殘留的磁碟區。
listInstalled(sortBy, sortOrder, limit, offset, search)"repo/name@version" 形式列出已安裝的套件。
getResponses(repo, name, version)已儲存的設定回答。
getInstalledInfo(repo, name, version)詳細資訊,含問題、回答與註記。

Systemd

方法說明
listUnits(sortBy, sortOrder, limit, offset, search)systemd unit 的分頁清單。
setUnitStatus(name, action)執行 "start""stop""restart"
logReplay(unit)透過 SSE 串流 journal 項目。回傳一個 AsyncGenerator
logTail(unit, lines?, before?, after?, grep?, since?, until?, priority?)一頁 journal 項目,支援游標分頁、grep、時間範圍與優先層級篩選。

帳號

方法說明
authenticate(username, password)回傳工作階段權杖與帳號。
createAccount(username, password, email, phone, realName, admin)建立一個使用者。密碼至少 8 個字元。
getAccount(username)依使用者名稱取得帳號。
updateAccount(username, fields)修改帳號欄位。
listAccounts(sortBy, sortOrder, limit, offset, search)帳號的分頁清單。
disableAccount(username)阻止其進行身分驗證。
enableAccount(username)重新啟用一個已停用的帳號。
listSessions(token)該權杖所屬使用者的作用中工作階段。
sessionUsername(token)工作階段權杖對應的使用者名稱。
revokeSession(sessionID)使某個工作階段失效。

稽核、設定與升級

方法說明
listAuditLog(opts)帶篩選條件的分頁稽核記錄。
getSettings()以鍵值物件回傳所有設定。
getSetting(key)依鍵取得單一設定。
setSetting(key, value)更新某個設定。
listUpgrades()有較新版本可用的套件。
dismissUpgrades()將待處理的升級標為已忽略。

封存檔

方法說明
uploadArchive(subvolume, file, subpath?, stopService?)透過 FormData 上傳並解開封存檔。回傳 {needs_restart, message}
downloadArchive(subvolume, paths?, stopService?, format?)下載子磁碟區封存檔。回傳原始的 Response 以便串流處理。

健康檢查

方法說明
ping()服務健康狀況與各項計數摘要。

開發參考

Town OS 後端執行於 5309 連接埠,Vite 開發伺服器則在 5173 連接埠。 請用 make dev 啟動完整的開發環境。

核心目標

目標說明
make dev啟動完整的開發環境(後端 + Vite 開發伺服器)。
make dev-stop停止並移除開發用的後端容器。
make dev-logs在執行中的開發容器內追蹤 journalctl。
make dev-clean停止容器並拆掉開發用的 btrfs 磁碟區。

測試目標

目標說明
make test執行靜態檢查、Go 單元測試與 JS 單元測試。
make test-integration在特權 Podman 容器中執行 Go 整合測試。
make test-ui-integration針對後端容器執行 Bun 介面整合測試。
make test-full依序執行所有測試套組。
make auto-test監看檔案變更並自動重跑測試。

建置目標

目標說明
make production-image建置正式環境的容器映像檔。
make test-image建置測試用的容器映像檔。
make pull-images從 Docker Hub 拉取基礎容器映像檔。

先決條件

  • Go 1.25+
  • Bun——JavaScript 執行環境
  • Podman——rootful 模式,需要 sudo
  • btrfs-progs——mkfs.btrfs
  • golangci-lint

建立一個 .env 檔案,填入儲存庫憑證:

TOWN_OS_REPO_USERNAME=<username>
TOWN_OS_REPO_PASSWORD=<password>

安裝好先決條件之後,請先執行 make pull-images,再執行其他任何目標。