From de7e69dbe3865f0d70238203d453f0f84f059ddf Mon Sep 17 00:00:00 2001
From: bgme <i@bgme.me>
Date: Mon, 21 Apr 2025 13:54:00 +0800
Subject: [PATCH] Squashed 'luci-app-einat/' content from commit 82838e7

git-subtree-dir: luci-app-einat
git-subtree-split: 82838e76d43b3b947c4bd5ddac26d5be02d5fb94
---
 .prepare.sh                                   |  19 ++
 LICENSE                                       |  21 ++
 Makefile                                      |  23 +++
 README.md                                     |  40 ++++
 htdocs/luci-static/resources/view/einat.js    | 183 ++++++++++++++++++
 po/templates/einat.pot                        | 133 +++++++++++++
 po/zh_Hans/einat.po                           | 142 ++++++++++++++
 po/zh_Hant/einat.po                           | 142 ++++++++++++++
 .../usr/share/luci/menu.d/luci-app-einat.json |  13 ++
 root/usr/share/rpcd/acl.d/luci-app-einat.json |  21 ++
 root/usr/share/rpcd/ucode/luci.einat          |  38 ++++
 11 files changed, 775 insertions(+)
 create mode 100755 .prepare.sh
 create mode 100644 LICENSE
 create mode 100644 Makefile
 create mode 100644 README.md
 create mode 100644 htdocs/luci-static/resources/view/einat.js
 create mode 100644 po/templates/einat.pot
 create mode 100644 po/zh_Hans/einat.po
 create mode 100644 po/zh_Hant/einat.po
 create mode 100644 root/usr/share/luci/menu.d/luci-app-einat.json
 create mode 100644 root/usr/share/rpcd/acl.d/luci-app-einat.json
 create mode 100644 root/usr/share/rpcd/ucode/luci.einat

diff --git a/.prepare.sh b/.prepare.sh
new file mode 100755
index 0000000..ee81496
--- /dev/null
+++ b/.prepare.sh
@@ -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
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..8c55001
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..957fbbd
--- /dev/null
+++ b/Makefile
@@ -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
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..97ca837
--- /dev/null
+++ b/README.md
@@ -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
diff --git a/htdocs/luci-static/resources/view/einat.js b/htdocs/luci-static/resources/view/einat.js
new file mode 100644
index 0000000..ff92063
--- /dev/null
+++ b/htdocs/luci-static/resources/view/einat.js
@@ -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));
+	}
+});
diff --git a/po/templates/einat.pot b/po/templates/einat.pot
new file mode 100644
index 0000000..3677805
--- /dev/null
+++ b/po/templates/einat.pot
@@ -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 ""
diff --git a/po/zh_Hans/einat.po b/po/zh_Hans/einat.po
new file mode 100644
index 0000000..caa52f5
--- /dev/null
+++ b/po/zh_Hans/einat.po
@@ -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 ""
diff --git a/po/zh_Hant/einat.po b/po/zh_Hant/einat.po
new file mode 100644
index 0000000..f972830
--- /dev/null
+++ b/po/zh_Hant/einat.po
@@ -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 ""
diff --git a/root/usr/share/luci/menu.d/luci-app-einat.json b/root/usr/share/luci/menu.d/luci-app-einat.json
new file mode 100644
index 0000000..a0ea134
--- /dev/null
+++ b/root/usr/share/luci/menu.d/luci-app-einat.json
@@ -0,0 +1,13 @@
+{
+	"admin/network/einat": {
+		"title": "einat-ebpf",
+		"order": 60,
+		"action": {
+			"type": "view",
+			"path": "einat"
+		},
+		"depends": {
+			"acl": [ "luci-app-einat" ]
+		}
+	}
+}
diff --git a/root/usr/share/rpcd/acl.d/luci-app-einat.json b/root/usr/share/rpcd/acl.d/luci-app-einat.json
new file mode 100644
index 0000000..a3cab4a
--- /dev/null
+++ b/root/usr/share/rpcd/acl.d/luci-app-einat.json
@@ -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"]
+		}
+	}
+}
diff --git a/root/usr/share/rpcd/ucode/luci.einat b/root/usr/share/rpcd/ucode/luci.einat
new file mode 100644
index 0000000..7128563
--- /dev/null
+++ b/root/usr/share/rpcd/ucode/luci.einat
@@ -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 };