OpenWRT Tailscale Luci-App Package 網頁操作插件開發筆記(持續更新)

小U爲大家提供預裝 Tailscale 的機器已經達到商業VPN在香港的伺服器數目了,但因爲 Tailscale 現時並未有 OpenWRT 的網頁操作界面(luci-app),所以小U覺得要爲各位用戶整一個。小U是第一次搞 OpenWRT 插件開發,第一次總是最寶貴的,所以我決定將這次的過程儘可能詳細的記錄下來。

(我很有信心地說,本文有很多地方都是不對的,我是一個沒有經過「正規教學」的IT門外漢,歡迎大家留言指正)

避免從零開始:尋找巨人的肩膀

懶人+非科班出生的業餘愛好者小U要做插件開發是很大的挑戰,所以第一想法是看看有沒有前人經驗可以參考。A lucky start,在 GitHub 上看到已有前人做了一個 叫 Tailscaler的插件:Carseason/openwrt-tailscale (github.com)

但是,這個插件的說明只有幾行文字,而且只有源碼沒有IPK打包?看來要順利開始沒那麼簡單:

OpenWRT插件編譯的三種方法

只有源碼的第一反映就是要編譯,誰不知網路上關於IPK編譯的資料很少,YouTube資料更少,但幸好還是有,小U找到的第一種方法是和從零開始編譯一個固件相似,但就是在 /package 的資料夾中,放入 插件的源碼,詳情可以參考這個 Youtube:

【视频教程】手把手教你用OpenWRT编译ipk插件 (youtube.com)

編譯過 OpenWRT 固件的朋友都有感覺,隨便都要幾個小時。於是小U又看到了另一個視頻,提供了一種省時間的方法,利用已經適配target 和host架構的SDK來進行MAKE,速度十分快:

软路由 | 单独编译第三方openwrt软件包插件 – YouTube

不過又遇到了問題,Carseason/openwrt-tailscale (github.com) 中的文件結構和一個常見的OpenWRT package不同,首先他有兩個主要目錄:

  • luci-app-tailscaler
  • web

MAKEFILE 是存放在 /luci-app-tailscaler 裏面,而web似乎沒有被MAKE碰到。細看 MAKEFILE 裏面也缺少了一些主要的元素。雖然 用上文第二個方法(SDK)能夠把ipk編譯出來,而且能夠在 Ups-AX6000 上安裝運行,但是 網頁面是一片空白,console 顯示 js 和 css 文件都缺失,應該就是和 /web 沒有編譯有關。

順藤摸瓜找到新天地

沒有辦法,再自己讀 Github repo 的README,發現第一句 github action build IPK 似乎有玄機。我一開始以爲這是 template之類默認語句,交給 ChatGPT 才發現可以直接讓 Github 幫忙編譯IPK!!!完全不用自己搞 Ubuntu 搞開發環境。而關鍵就在 /.github/workflows/openwrt_ipk.yaml 這裏,我一開始以爲 /.github 都是些 github內部系統處理的文件(如同.gitignore)。 這個 openwrt_ipk.yaml 是定義了交給 Github Action 編譯的設定,我的理解是 雲端用的 .makeconfig 文件。只要把這個repo fork到自己賬戶,然後就能從 Action 中提交編譯請求(請留意選擇系統架構)。

很快就能下載到一個ipk文件了,裝上 Ups-AX6000 可以正常顯示 Tailscale狀態:

Tailscaler 原版設計邏輯的研究

能在 我的Ups-AX6000 上安裝並運行了第一個編譯插件(還要是雲編譯),十分開心。但很快又被更多的問題衝擊: Tailscaler 的執行邏輯沒有想象中的簡單。

編譯前後的工程文件對比

上文已經說道,編譯前的源碼分爲 luci-app 的部分和 web 的部分。而仔細看 openwrt_ipk.yaml:

      # 打包 web 项目
      - run: |
          cd web
          pnpm install
          pnpm run build

      # 复制 web至 luci
      - run: |
          mkdir ${IPK_NAME}/htdocs
          cp -r web/dist/luci-static ${IPK_NAME}/htdocs

這裡把用 pnpm (Node.js 的 package management,大約)打包編譯好web裡面的文件後,會存放到 /luci-static 文件夾,這正正就是上文提到 ”網頁空白,找不到源碼“ 的指向處。

也就是說,luci-app-tailscaler 的前端是需要用 pnpm 編譯打包後,才能正常被瀏覽器讀取的。

那問題來了,爲什麼一個網頁操作界面,html + javascript,要打包編譯才行,不能直接把這些網頁文件做好嗎?

WEB文件夾的內容研究

從來沒有做過移動開發的小U,看來看去只知道這些文件和 vue 以及 typescript 有關,但這兩個名字小U只知道和移動APP/新型網頁開發有關,實際操作完全零接觸。無奈之下未有把文件扔給 ChatGPT,不料他竟然解開了我90%的疑惑!

經過和ChatGPT的捉膝詳談,我終於搞明白了Tailscaler 原版設計邏輯。大約是這樣子的:

  • Tailscaler 是一個前端用 Vue + TypeScript 編寫, 後端用 Lua 編寫的程式。
  • 前端的 Vue + TypeScript 不能夠直接在瀏覽器中運行,要經過 Node.js 的 serve,又或者是透過 pnpm 利用 Vite 打包成爲 HTML + Javascript (並放在/luci-static)。

後端 Lua 的職能

  • Lua在後端搭建了幾個 url path (是不是叫API entry?),例如訪問 /cgi-bin/luci/admin/services/tailscaler/status (以下簡稱 $baseurl/status)時候會執行 tailscale status --json 並把json輸出給http。訪問 $baseurl/logout 會執行 tailscale logout
  • 其中一個API ($baseurl/config)負責監聽來自前端的 “POST REQUEST”(例如按鈕點擊),並根據不同的REQUEST內容做出不同的處理。

前端 Vue + Typescript 的職能

  • Typescript 負責定義了好幾套變量(response.d.ts),並通過 ref() 在 App.vue 中引用
  • Typescript 定義了幾個重要 function:
    • 第一個是 getStatus 可以透過 我理解不了但經常用到的 async await組合來獲取 Lua 提供的 json 狀態檔。
    • 第二個是 obSubmit 可以把選項扔回給 $baseurl/config
  • Vue的html部分透過 v-if,v-model等的功能,把動態數據聯動到html。

提出對 Tailscaler 原版設計的修改

經過仔細研究,小U認爲原版的 Tailscaler 有兩個比較不合適的設計,以及我的改善提案:

設定應該使用 Tailscale set 而不是 up

Tailscaler 中,所有對 Tailscale 運行的設置,都是通過 tailscale up + flags 來設定。這是不好設置的,因爲 這種方法每次都要添加 --reset 才能通過。導致下面這種情況:

你設定了10個變量,現在只需要修改1個,但是由於要添加 –reset ,所以每次修改都必須把10個變量都加進去CLI命令。

如果改用 tailscale set ,則只需要修改目標的1個變量。

延伸到不必要的config文件和UCI

因爲上面所說,所有設定都用了“牽一髮動全身”的tailscale up + flags 方法,所以作者配了一個config檔,而且透過 UCI 來設定 tailscaler 的 config組合 ,再把 tailscaler 的 config組合透過 tailscale up + flags 的方法傳給 tailscale,這是沒有必要的。

改善方法提案

小U會把每個設置都獨立開,調用 tailscale set 來單獨設定每一個設置。而總體的運行狀態、綁定,可以保留原版的設定。ChatGPT 教我這樣修改的,我覺得邏輯合適:

function tailscale_config()
    local http = require "luci.http"
    local jsonc = require "luci.jsonc"
    http.prepare_content("application/json")
    local method = http.getenv("REQUEST_METHOD")
    
    if method == "POST" then
        local content = http.content()
        local req = jsonc.parse(content)
        if req then
            -- 檢查 req 對象中是否有 advertiseRoutes 屬性
            if req.advertiseRoutes then
                -- 構造並執行 tailscale 命令
                local command = "/usr/sbin/tailscale set --advertise-route=" .. req.advertiseRoutes
                local output = luci.sys.exec(command)
                -- 返回命令執行結果
                http.write_json({ result = "Command executed", output = output })
            else
                http.write_json({ error = "advertiseRoutes not provided" })
            end
        else
            http.write_json({ error = "Invalid request" })
        end
    else
        http.status(405, "Method Not Allowed")
    end
end

我會設計一個三個columns的表格,左中右分別是:設定項目 – 現時狀態 – 更改

心目中主界面會是這樣:

截 20240203

歡迎你的留言討論:

你可以一針見血

by Upsangel
Logo