Merge commit 'de7e69dbe3865f0d70238203d453f0f84f059ddf' as 'luci-app-einat'

This commit is contained in:
bgme 2025-04-21 13:54:00 +08:00
commit bb6c79b701
11 changed files with 775 additions and 0 deletions
luci-app-einat
.prepare.shLICENSEMakefileREADME.md
htdocs/luci-static/resources/view
po
templates
zh_Hans
zh_Hant
root/usr/share

19
luci-app-einat/.prepare.sh Executable file
View file

@ -0,0 +1,19 @@
PKG_NAME="$1"
CURDIR="$2"
PKG_BUILD_DIR="$3"
if [ -d "$CURDIR/.git" ]; then
config="$CURDIR/.git/config"
else
config="$(sed "s|^gitdir:\s*|$CURDIR/|;s|$|/config|" "$CURDIR/.git")"
fi
[ -n "$(sed -En '/^\[remote /{h;:top;n;/^\[/b;s,(https?://gitcode\.(com|net)),\1,;T top;H;x;s|\n\s*|: |;p;}' "$config")" ] && {
for d in luasrc ucode htdocs root src; do
rm -rf "$PKG_BUILD_DIR"/$d
done
mkdir -p "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view
touch "$PKG_BUILD_DIR"/htdocs/luci-static/resources/view/$PKG_NAME.js
mkdir -p "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d
touch "$PKG_BUILD_DIR"/root/usr/share/luci/menu.d/$PKG_NAME.json
}
exit 0

21
luci-app-einat/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Anya Lin
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

23
luci-app-einat/Makefile Normal file
View file

@ -0,0 +1,23 @@
# SPDX-License-Identifier: MIT
#
# Copyright (C) 2024-2025 Anya Lin <https://github.com/muink>
include $(TOPDIR)/rules.mk
LUCI_NAME:=luci-app-einat
LUCI_TITLE:=LuCI Support for einat
LUCI_PKGARCH:=all
LUCI_DEPENDS:=+einat-ebpf
LUCI_DESCRIPTION:=eBPF-based Endpoint-Independent NAT
PKG_MAINTAINER:=Anya Lin <hukk1996@gmail.com>
PKG_LICENSE:=MIT
PKG_LICENSE_FILES:=LICENSE
PKG_UNPACK=$(CURDIR)/.prepare.sh $(PKG_NAME) $(CURDIR) $(PKG_BUILD_DIR)
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature

40
luci-app-einat/README.md Normal file
View file

@ -0,0 +1,40 @@
# luci-app-einat
> [einat-ebpf][] project is an eBPF application implements an "Endpoint-Independent Mapping" and "Endpoint-Independent Filtering" NAT(network address translation) on TC egress and ingress hooks.
## Depends
1. [openwrt-einat-ebpf][]
## Releases
You can find the prebuilt-ipks [here](https://fantastic-packages.github.io/packages/)
## Build
```shell
# Take the x86_64 platform as an example
tar xjf openwrt-sdk-23.05.3-x86-64_gcc-12.3.0_musl.Linux-x86_64.tar.xz
# Go to the SDK root dir
cd OpenWrt-sdk-*-x86_64_*
# First run to generate a .config file
make menuconfig
./scripts/feeds update -a
./scripts/feeds install -a
# Get Makefile
git clone --depth 1 --branch master --single-branch --no-checkout https://github.com/muink/luci-app-einat.git package/luci-app-einat
pushd package/luci-app-einat
umask 022
git checkout
popd
# Select the package LuCI -> Applications -> luci-app-einat
make menuconfig
# Start compiling
make package/luci-app-einat/compile V=99
```
[einat-ebpf]: https://github.com/EHfive/einat-ebpf
[openwrt-einat-ebpf]: https://github.com/muink/openwrt-einat-ebpf
## License
This project is licensed under the MIT license

View file

@ -0,0 +1,183 @@
'use strict';
'require form';
'require fs';
'require uci';
'require ui';
'require rpc';
'require poll';
'require view';
'require network';
'require tools.widgets as widgets';
const conf = 'einat';
const instance = 'einat';
const callServiceList = rpc.declare({
object: 'service',
method: 'list',
params: ['name'],
expect: { '': {} }
});
const callRcInit = rpc.declare({
object: 'rc',
method: 'init',
params: ['name', 'action']
});
const callGetFeatures = rpc.declare({
object: 'luci.einat',
method: 'get_features',
expect: { '': {} }
});
function getServiceStatus() {
return L.resolveDefault(callServiceList(conf), {})
.then((res) => {
let isrunning = false;
try {
isrunning = res[conf]['instances'][instance]['running'];
} catch (e) { }
return isrunning;
});
}
function handleAction(action, ev) {
return callRcInit("einat", action).then((ret) => {
if (ret)
throw _('Command failed');
return true;
}).catch((e) => {
ui.addNotification(null, E('p', _('Failed to execute "/etc/init.d/%s %s" action: %s').format("einat", action, e)));
});
}
return view.extend({
load() {
return Promise.all([
getServiceStatus(),
L.resolveDefault(fs.stat('/usr/bin/einat'), null),
callGetFeatures(),
uci.load('einat')
]);
},
poll_status(nodes, stat) {
const isRunning = stat[0];
let view = nodes.querySelector('#service_status');
if (isRunning) {
view.innerHTML = "<span style=\"color:green;font-weight:bold\">" + instance + " - " + _("SERVER RUNNING") + "</span>";
} else {
view.innerHTML = "<span style=\"color:red;font-weight:bold\">" + instance + " - " + _("SERVER NOT RUNNING") + "</span>";
}
return;
},
render(res) {
const isRunning = res[0];
const has_einat = res[1] ? res[1].path : null;
const features = res[2];
let m, s, o;
m = new form.Map('einat', _('einat-ebpf'), _('eBPF-based Endpoint-Independent NAT'));
s = m.section(form.NamedSection, '_status');
s.anonymous = true;
s.render = function(section_id) {
return E('div', { class: 'cbi-section' }, [
E('div', { id: 'service_status' }, _('Collecting data ...'))
]);
};
s = m.section(form.NamedSection, 'config', instance);
s.anonymous = true;
o = s.option(form.Button, '_reload', _('Reload'));
o.inputtitle = _('Reload');
o.inputstyle = 'apply';
o.onclick = function() {
return handleAction('reload');
};
o = s.option(form.Flag, 'enabled', _('Enable'));
o.default = o.disabled;
o.rmempty = false;
if (! has_einat) {
o.description = _('To enable you need install <b>einat-ebpf</b> first');
o.readonly = true;
}
o = s.option(form.ListValue, 'bpf_log_level', _('BPF tracing log level'));
o.default = '0';
o.value('0', 'disable - ' + _('Disable'));
o.value('1', 'error - ' + _('Error'));
o.value('2', 'warn - ' + _('Warn'));
o.value('3', 'info - ' + _('Info'));
o.value('4', 'debug - ' + _('Debug'));
o.value('5', 'trace - ' + _('Trace'));
o = s.option(form.ListValue, 'bpf_loader', _('BPF loading backend'));
o.value('', _('Default'));
if (features.features.includes('aya'))
o.value('aya', _('aya'));
if (features.features.includes('libbpf'))
o.value('libbpf', _('libbpf'));
o = s.option(form.Flag, 'nat44', _('NAT44'));
o.default = o.disabled;
o.rmempty = false;
//o = s.option(form.Flag, 'nat66', _('NAT66'));
//o.default = o.disabled;
//o.rmempty = false;
o = s.option(widgets.DeviceSelect, 'ifname', _('External interface'));
o.multiple = false;
o.noaliases = true;
o.nobridges = true;
o.nocreate = true;
o = s.option(form.Value, 'ports', _('External TCP/UDP port ranges'),
_('Please avoid conflicts with external ports used by other applications'));
o.datatype = 'portrange';
o.placeholder = '20000-29999';
o.rmempty = true;
o = s.option(widgets.NetworkSelect, 'internal_ifaces', _('Internal interfaces'),
_('Perform source NAT for these internal networks only.'));
o.multiple = true;
o.nocreate = true;
o = s.option(form.DynamicList, 'internal_subnets', _('Internal subnets'),
_('Perform source NAT for these internal networks only.'));
o.datatype = 'cidr';
o.placeholder = '192.168.0.0/16';
o = s.option(form.Flag, 'hairpin_enabled', _('Enable hairpin'),
_('May conflict with other policy routing-based applications'));
o.default = o.disabled;
o.rmempty = false;
o = s.option(widgets.DeviceSelect, 'hairpinif', _('Hairpin internal interfaces'));
o.multiple = true;
o.noaliases = true;
o.nobridges = false;
o.nocreate = true;
o.depends('hairpin_enabled', '1');
o.rmempty = true;
o.retain = true;
return m.render()
.then(L.bind(function(m, nodes) {
poll.add(L.bind(function() {
return Promise.all([
getServiceStatus()
]).then(L.bind(this.poll_status, this, nodes));
}, this), 3);
return nodes;
}, this, m));
}
});

View file

@ -0,0 +1,133 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: htdocs/luci-static/resources/view/einat.js:114
msgid "BPF loading backend"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:105
msgid "BPF tracing log level"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:83
msgid "Collecting data ..."
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:42
msgid "Command failed"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:111
msgid "Debug"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:115
msgid "Default"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:107
msgid "Disable"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:97
msgid "Enable"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:149
msgid "Enable hairpin"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:108
msgid "Error"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:133
msgid "External TCP/UDP port ranges"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:127
msgid "External interface"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:46
msgid "Failed to execute \"/etc/init.d/%s %s\" action: %s"
msgstr ""
#: root/usr/share/rpcd/acl.d/luci-app-einat.json:3
msgid "Grant access to LuCI app einat"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:154
msgid "Hairpin internal interfaces"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:110
msgid "Info"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:139
msgid "Internal interfaces"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:144
msgid "Internal subnets"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:150
msgid "May conflict with other policy routing-based applications"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:119
msgid "NAT44"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:140
#: htdocs/luci-static/resources/view/einat.js:145
msgid "Perform source NAT for these internal networks only."
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:134
msgid "Please avoid conflicts with external ports used by other applications"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:90
#: htdocs/luci-static/resources/view/einat.js:91
msgid "Reload"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:66
msgid "SERVER NOT RUNNING"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:64
msgid "SERVER RUNNING"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:101
msgid "To enable you need install <b>einat-ebpf</b> first"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:112
msgid "Trace"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:109
msgid "Warn"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:116
msgid "aya"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:77
msgid "eBPF-based Endpoint-Independent NAT"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:77
#: root/usr/share/luci/menu.d/luci-app-einat.json:3
msgid "einat-ebpf"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:117
msgid "libbpf"
msgstr ""

View file

@ -0,0 +1,142 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2024-05-10 12:23+0000\n"
"Last-Translator: Anya Lin <muink@users.noreply.github.com>\n"
"Language-Team: Chinese (Simplified) <https://hosted.weblate.org/projects/"
"openwrt/luciapplicationseinat/zh_Hans/>\n"
"Language: zh_Hans\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.18-dev\n"
#: htdocs/luci-static/resources/view/einat.js:114
msgid "BPF loading backend"
msgstr "BPF 加载后端"
#: htdocs/luci-static/resources/view/einat.js:105
msgid "BPF tracing log level"
msgstr "BPF 追踪日志等级"
#: htdocs/luci-static/resources/view/einat.js:83
msgid "Collecting data ..."
msgstr "正在收集数据..."
#: htdocs/luci-static/resources/view/einat.js:42
msgid "Command failed"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:111
msgid "Debug"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:115
msgid "Default"
msgstr "默认"
#: htdocs/luci-static/resources/view/einat.js:107
msgid "Disable"
msgstr "禁用"
#: htdocs/luci-static/resources/view/einat.js:97
msgid "Enable"
msgstr "启用"
#: htdocs/luci-static/resources/view/einat.js:149
msgid "Enable hairpin"
msgstr "启用 Hairpin"
#: htdocs/luci-static/resources/view/einat.js:108
msgid "Error"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:133
msgid "External TCP/UDP port ranges"
msgstr "外部 TCP/UDP 端口范围"
#: htdocs/luci-static/resources/view/einat.js:127
msgid "External interface"
msgstr "外部接口"
#: htdocs/luci-static/resources/view/einat.js:46
msgid "Failed to execute \"/etc/init.d/%s %s\" action: %s"
msgstr ""
#: root/usr/share/rpcd/acl.d/luci-app-einat.json:3
msgid "Grant access to LuCI app einat"
msgstr "授予访问 LuCI 应用 einat 的权限"
#: htdocs/luci-static/resources/view/einat.js:154
msgid "Hairpin internal interfaces"
msgstr "Hairpin 内部接口"
#: htdocs/luci-static/resources/view/einat.js:110
msgid "Info"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:139
msgid "Internal interfaces"
msgstr "内部接口"
#: htdocs/luci-static/resources/view/einat.js:144
msgid "Internal subnets"
msgstr "内部子网"
#: htdocs/luci-static/resources/view/einat.js:150
msgid "May conflict with other policy routing-based applications"
msgstr "可能与其他基于策略路由的应用程序发生冲突"
#: htdocs/luci-static/resources/view/einat.js:119
msgid "NAT44"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:140
#: htdocs/luci-static/resources/view/einat.js:145
msgid "Perform source NAT for these internal networks only."
msgstr "仅对这些内部网络执行 SNAT。"
#: htdocs/luci-static/resources/view/einat.js:134
msgid "Please avoid conflicts with external ports used by other applications"
msgstr "请避免与其他应用使用的外部端口冲突"
#: htdocs/luci-static/resources/view/einat.js:90
#: htdocs/luci-static/resources/view/einat.js:91
msgid "Reload"
msgstr "重新载入"
#: htdocs/luci-static/resources/view/einat.js:66
msgid "SERVER NOT RUNNING"
msgstr "服务器未运行"
#: htdocs/luci-static/resources/view/einat.js:64
msgid "SERVER RUNNING"
msgstr "服务器运行中"
#: htdocs/luci-static/resources/view/einat.js:101
msgid "To enable you need install <b>einat-ebpf</b> first"
msgstr "要启用您需要先安装 <b>einat-ebpf</b>"
#: htdocs/luci-static/resources/view/einat.js:112
msgid "Trace"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:109
msgid "Warn"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:116
msgid "aya"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:77
msgid "eBPF-based Endpoint-Independent NAT"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:77
#: root/usr/share/luci/menu.d/luci-app-einat.json:3
msgid "einat-ebpf"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:117
msgid "libbpf"
msgstr ""

View file

@ -0,0 +1,142 @@
msgid ""
msgstr ""
"PO-Revision-Date: 2024-05-10 12:23+0000\n"
"Last-Translator: Anya Lin <muink@users.noreply.github.com>\n"
"Language-Team: Chinese (Traditional) <https://hosted.weblate.org/projects/"
"openwrt/luciapplicationseinat/zh_Hant/>\n"
"Language: zh_Hant\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Weblate 4.18-dev\n"
#: htdocs/luci-static/resources/view/einat.js:114
msgid "BPF loading backend"
msgstr "BPF 載入後端"
#: htdocs/luci-static/resources/view/einat.js:105
msgid "BPF tracing log level"
msgstr "BPF 追蹤日誌級別"
#: htdocs/luci-static/resources/view/einat.js:83
msgid "Collecting data ..."
msgstr "正在收集數據..."
#: htdocs/luci-static/resources/view/einat.js:42
msgid "Command failed"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:111
msgid "Debug"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:115
msgid "Default"
msgstr "預設"
#: htdocs/luci-static/resources/view/einat.js:107
msgid "Disable"
msgstr "禁用"
#: htdocs/luci-static/resources/view/einat.js:97
msgid "Enable"
msgstr "啟用"
#: htdocs/luci-static/resources/view/einat.js:149
msgid "Enable hairpin"
msgstr "啟用 Hairpin"
#: htdocs/luci-static/resources/view/einat.js:108
msgid "Error"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:133
msgid "External TCP/UDP port ranges"
msgstr "外部 TCP/UDP 埠範圍"
#: htdocs/luci-static/resources/view/einat.js:127
msgid "External interface"
msgstr "外部介面"
#: htdocs/luci-static/resources/view/einat.js:46
msgid "Failed to execute \"/etc/init.d/%s %s\" action: %s"
msgstr ""
#: root/usr/share/rpcd/acl.d/luci-app-einat.json:3
msgid "Grant access to LuCI app einat"
msgstr "授予訪問 LuCI 應用 einat 的權限"
#: htdocs/luci-static/resources/view/einat.js:154
msgid "Hairpin internal interfaces"
msgstr "Hairpin 內部介面"
#: htdocs/luci-static/resources/view/einat.js:110
msgid "Info"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:139
msgid "Internal interfaces"
msgstr "內部介面"
#: htdocs/luci-static/resources/view/einat.js:144
msgid "Internal subnets"
msgstr "內部子網"
#: htdocs/luci-static/resources/view/einat.js:150
msgid "May conflict with other policy routing-based applications"
msgstr "可能與其他基於策略路由的應用程式發生衝突"
#: htdocs/luci-static/resources/view/einat.js:119
msgid "NAT44"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:140
#: htdocs/luci-static/resources/view/einat.js:145
msgid "Perform source NAT for these internal networks only."
msgstr "僅對這些內部網路執行 SNAT。"
#: htdocs/luci-static/resources/view/einat.js:134
msgid "Please avoid conflicts with external ports used by other applications"
msgstr "請避免與其他應用程式使用的外部連接埠衝突"
#: htdocs/luci-static/resources/view/einat.js:90
#: htdocs/luci-static/resources/view/einat.js:91
msgid "Reload"
msgstr "重新載入"
#: htdocs/luci-static/resources/view/einat.js:66
msgid "SERVER NOT RUNNING"
msgstr "伺服器未運行"
#: htdocs/luci-static/resources/view/einat.js:64
msgid "SERVER RUNNING"
msgstr "伺服器運行中"
#: htdocs/luci-static/resources/view/einat.js:101
msgid "To enable you need install <b>einat-ebpf</b> first"
msgstr "要啟用您需要先安裝 <b>einat-ebpf</b>"
#: htdocs/luci-static/resources/view/einat.js:112
msgid "Trace"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:109
msgid "Warn"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:116
msgid "aya"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:77
msgid "eBPF-based Endpoint-Independent NAT"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:77
#: root/usr/share/luci/menu.d/luci-app-einat.json:3
msgid "einat-ebpf"
msgstr ""
#: htdocs/luci-static/resources/view/einat.js:117
msgid "libbpf"
msgstr ""

View file

@ -0,0 +1,13 @@
{
"admin/network/einat": {
"title": "einat-ebpf",
"order": 60,
"action": {
"type": "view",
"path": "einat"
},
"depends": {
"acl": [ "luci-app-einat" ]
}
}
}

View file

@ -0,0 +1,21 @@
{
"luci-app-einat": {
"description": "Grant access to LuCI app einat",
"read": {
"file": {
"/etc/init.d/einat reload": [ "exec" ]
},
"ubus": {
"service": [ "list" ],
"luci.einat": [ "*" ]
},
"uci": ["einat"]
},
"write": {
"ubus": {
"rc": [ "init" ]
},
"uci": ["einat"]
}
}
}

View file

@ -0,0 +1,38 @@
#!/usr/bin/ucode
'use strict';
import { access, popen } from 'fs';
const methods = {
get_features: {
call: function() {
let features = {
version: null,
features: [],
build_features: []
};
const fd = popen('/usr/bin/einat -v');
if (fd) {
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
let ver = match(trim(line), /version: (\S+)/);
if (ver)
features.version = ver[1];
let feats = match(trim(line), /features: (\S+)/);
if (feats)
features.features = split(feats[1], ',');
let build_feats = match(trim(line), /build_features: (\S+)/);
if (build_feats)
features.build_features = split(build_feats[1], ',');
}
fd.close();
}
return features;
}
}
};
return { 'luci.einat': methods };