等风起

如果当誓时

All at vows.


luci-app-tailscale插件编写的爬坑记录


背景

一直以来笔者都用OpenWrt的ZeroTier服务进行组网,最近网络环境不太稳定,经常出现内网无法连接的情况。于是笔者打算使用Tailscale作为替代方案。现有的LuCI插件不能完全满足如意,作为又菜又爱的人,笔者参考 openwrt-tailscaleluci-app-zerotier 自己动手编写一个适用于新版LuCI的 luci-app-tailscale 插件。本文记录一个没有任何LuCI插件编写经验菜鸟的爬坑过程。

关于LuCI2

早期的LuCI是主要使用Lua编写,通过ubus、luci.model.uci、nixio.fs等扩展来访问系统信息和设置。而新版Luci采用了不同架构,且称之为LuCI2。它不再使用Lua,转而通过JavaScript XHR方法来渲染页面。

基础构建

  1. 文件结构 - 参照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
    
  2. 编写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配置授权(具体方法后文再说)。

  1. 编写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” 文件路径

  1. 预编译 - 使用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进行配置授权。

授权方法

  1. 创建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" ]
             }
         }
     }
    
  2. 重启RPC服务

    /etc/init.d/rpcd reload
    

创建语言包模板

最初笔者手敲.pot语言包模版以实现多语言支持,后来发现OpenWrt官方提供了perl脚本i18n-scan.pl来自动生成.pot文件。

使用i18n-scan.pl自动生成语言包

  1. 安装依赖

    sudo apt-get install perl gettext
    
  2. 生成.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









除另有声明外,本博客文章均采用 知识共享许可协议 - 署名标示 4.0(CC BY 4.0)进行授权许可。 Made with by AsVow

最近的文章

获取Cloudflare Tunnel下访客真实IP

背景介绍 假定局域网环境中,我们在 80 端口部署一个 PHP 网站,并且使用 Cloudflare Tunnel 将内网地址 http://127.0.0.1 转发至外网。然而,这种设置会导致所有访 ……

笔记 继续阅读
更早的文章

必应每日壁纸获取脚本编写实例

今日壁纸 背景 必应主页上每天都会更新一张来自世界各地的精美图片,作为网站背景或桌面壁纸都十分美轮美奂。笔者通过反代官方API解决跨域问题,在setBackGround.js里面处理API返回 ……

js笔记 继续阅读