背景
一直以来笔者都用OpenWrt的ZeroTier服务进行组网,最近网络环境不太稳定,经常出现内网无法连接的情况。于是笔者打算使用Tailscale作为替代方案。现有的LuCI插件不能完全满足如意,作为又菜又爱的人,笔者参考 openwrt-tailscale 和 luci-app-zerotier 自己动手编写一个适用于新版LuCI的 luci-app-tailscale 插件。本文记录一个没有任何LuCI插件编写经验菜鸟的爬坑过程。
关于LuCI2
早期的LuCI是主要使用Lua编写,通过ubus、luci.model.uci、nixio.fs等扩展来访问系统信息和设置。而新版Luci采用了不同架构,且称之为LuCI2。它不再使用Lua,转而通过JavaScript XHR方法来渲染页面。
基础构建
-
文件结构 - 参照luci-app-zerotier源码,创建以下文件
luci-app-tailscale ├── Makefile ├── htdocs │ └── luci-static │ └── resources │ └── view │ └── tailscale │ ├── base.js │ └── interface.js ├── po │ ├── templates │ │ └── tailscale.pot │ ├── zh_Hans │ │ └── tailscale.po │ └── zh_Hant │ └── tailscale.po └── root ├── etc │ ├── config │ │ └── tailscale │ ├── hotplug.d │ │ └── iface │ │ └── 40-tailscale │ ├── init.d │ │ └── tailscale │ └── uci-defaults │ └── 40_luci-tailscale └── usr └── share ├── luci │ └── menu.d │ └── luci-app-tailscale.json └── rpcd └── acl.d └── luci-app-tailscale.json
-
编写base.js - 通过
tailscale status —-json
获取登录状态,未登录则执行tailscale login
进行登录function getLoginStatus() { return fs.exec("/usr/sbin/tailscale", ["status", "—json"]).then(function(res) { var status = JSON.parse(res.stdout); if (!status.AuthURL && status.BackendState == "NeedsLogin") { fs.exec("/usr/sbin/tailscale", ["login"]); } var displayName = status.BackendState == "Running" ? status.User[status.Self.UserID].DisplayName : undefined; return { backendState: status.BackendState, authURL: status.AuthURL, displayName: displayName }; }).catch(function(error) { return { backendState: undefined, authURL: undefined, displayName: undefined }; }); }
坑1: fs.exec
报错提示权限不足。OpenWrt LuCI2前端调用后端命令需要rpcd配置授权(具体方法后文再说)。
-
编写base.js - 将Tailscale登录状态显示到页面中,并增加注销功能。
创建
renderLogin
函数以渲染页面function renderLogin(loginStatus, authURL, displayName) { var spanTemp = '<span style="color:%s">%s</span>'; var renderHTML; if (loginStatus == "NeedsLogin") { renderHTML = String.format('<a href="%s" target="_blank">%s</a>', authURL, _('Needs Login')); } else if (loginStatus == "Running") { renderHTML = String.format('<a href="%s" target="_blank">%s</a>', 'https://login.tailscale.com/admin/machines', displayName); renderHTML += String.format('<br><a style="color:green" id="logout_button">%s</a>', _('Logout and Unbind')); } else { renderHTML = String.format(spanTemp, 'orange', _('NOT RUNNING')); } return renderHTML; }
定义界面表单元素
o = s.option(form.DummyValue, 'login_status', _('Login Status')); o.renderWidget = function(section_id, option_id) { poll.add(function() { return Promise.resolve(getLoginStatus()).then(function(res) { document.getElementById('login_status_div').innerHTML = renderLogin(res.backendState, res.authURL, res.displayName); var logoutButton = document.getElementById('logout_button'); if (logoutButton) { logoutButton.onclick = function() { if (confirm(_('Are you sure you want to logout and unbind the current device?'))) { fs.exec("/usr/sbin/tailscale", ["logout"]); } } } }); }); return E('div', { 'id': 'login_status_div' }, _('Collecting data ...')); };
注:在LuCI中,form 和 datatype 分别用来定义配置接口的组件、数据验证规则。
form 取值:
form.Value 单行文本框,用于输入文本
form.TextValue 多行文本框,可以输入更长的文本
form.ListValue 下拉列表,可以选择多个预定义的选项之一
form.Flag 复选框,通常用于布尔值(开/关)
form.MultiValue 多选框,可以选择多个值
form.DynamicList 动态列表,用户可以添加多个任意值
form.SectionValue 嵌套的配置段,用于复杂的配置结构
form.Button 按钮,用于执行一个动作,而不是保存一个值
form.FileUpload 文件上传,允许用户上传一个文件
form.DummyValue 虚拟值,用于显示信息而非输入
datatype 取值:
o.datatype=“uinteger” 无符号整数
o.datatype=“integer” 有符号整数
o.datatype=“float” 浮点数
o.datatype=“boolean” 布尔值,通常为0(假)或1(真)
o.datatype=“ipaddr” IP地址
o.datatype=“ip4addr” IPv4地址
o.datatype=“ip6addr” IPv6地址
o.datatype=“netmask” 网络掩码
o.datatype=“macaddr” MAC地址
o.datatype=“hostname” 主机名
o.datatype=“network” 网络接口名称
o.datatype=“port” 端口号
o.datatype=“string” 任意字符串
o.datatype=“wwan_apn” 无线广域网接入点名称
o.datatype=“file” 文件路径
-
预编译 - 使用OpenWrt SDK编译插件
make package/luci-app-tailscale/compile V=s
坑2: openwrt/packages/net/tailscale 占用了 /etc/config/tailscale 和 /etc/init.d/tailscale 导致编译和安装时报错。
* check_data_file_clashes: Package luci-app-tailscale wants to install file /etc/config/tailscale
But that file is already provided by package * tailscaled
* check_data_file_clashes: Package luci-app-tailscale wants to install file /etc/init.d/tailscale
But that file is already provided by package * tailscaled
本插件中的这两个文件均参考了tailscale官方标准编写,可以直接替换掉。
笔者尝试在Makefile中加入以下preinst
定义,无果…
define Package/luci-app-tailscale/preinst
#!/bin/sh
rm /etc/config/tailscale /etc/init.d/tailscale
exit 0
endef
那就…直接从根源上解决提出问题的人
sed -i '/\/etc\/init\.d\/tailscale/d;/\/etc\/config\/tailscale/d;' feeds/packages/net/tailscale/Makefile
RPC配置方式
在测试过程中,笔者遇到了一个问题:OpenWrt前端的fs.exec
权限不足,无法执行/usr/sbin/tailscale命令。为了解决这个问题,需要对rpcd进行配置授权。
授权方法
-
创建rpcd配置文件 - 在
/usr/share/rpcd/acl.d/
路径下创建luci-app-tailscale.json
。{ "luci-app-tailscale": { "description": "Grant access to Tailscale configuration", "read": { "file": { "/usr/sbin/tailscale": [ "exec" ] }, "ubus": { "service": [ "list" ] }, "uci": [ "tailscale" ] }, "write": { "uci": [ "tailscale" ] } } }
-
重启RPC服务
/etc/init.d/rpcd reload
创建语言包模板
最初笔者手敲.pot
语言包模版以实现多语言支持,后来发现OpenWrt官方提供了perl
脚本i18n-scan.pl来自动生成.pot
文件。
使用i18n-scan.pl自动生成语言包
-
安装依赖
sudo apt-get install perl gettext
-
生成.pot文件
wget https://raw.githubusercontent.com/openwrt/luci/master/build/i18n-scan.pl chmod +x i18n-scan.pl ./i18n-scan.pl luci-app-tailscale > luci-app-tailscale/po/templates/tailscale.pot
至此折腾告一段落,最终笔者成功地编写了luci-app-tailscale插件,并且在OpenWrt上顺利运行。GitHub项目地址:asvow/luci-app-tailscale。