diff --git a/.github/diy/packages/luci-app-meow/Makefile b/.github/diy/packages/luci-app-meow/Makefile.k similarity index 100% rename from .github/diy/packages/luci-app-meow/Makefile rename to .github/diy/packages/luci-app-meow/Makefile.k diff --git a/.github/diy/packages/meow/Makefile b/.github/diy/packages/meow/Makefile.k similarity index 100% rename from .github/diy/packages/meow/Makefile rename to .github/diy/packages/meow/Makefile.k diff --git a/cloudflared-next/Makefile b/cloudflared-next/Makefile index 7a990ad6b..6cdd430f4 100644 --- a/cloudflared-next/Makefile +++ b/cloudflared-next/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=cloudflared-next -PKG_VERSION:=2026.10.0 -PKG_RELEASE:=3 +PKG_VERSION:=2026.10.1 +PKG_RELEASE:=4 # Architecture mapping for pre-built binaries ifeq ($(ARCH),x86_64) diff --git a/luci-app-meow/.gitignore b/luci-app-meow/.gitignore new file mode 100644 index 000000000..c2658d7d1 --- /dev/null +++ b/luci-app-meow/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/luci-app-meow/Makefile b/luci-app-meow/Makefile index fe42da240..39d02c90a 100644 --- a/luci-app-meow/Makefile +++ b/luci-app-meow/Makefile @@ -1,50 +1,18 @@ +# +# Copyright (C) 2026 OpenWrt.org +# +# This is free software, licensed under the GNU General Public License v3. +# See /LICENSE for more information. +# + include $(TOPDIR)/rules.mk -PKG_NAME:=luci-app-meow +LUCI_TITLE:=meow package - LuCI interface +LUCI_DEPENDS:=+luci-base +meow -# Keep in sync with meow/Makefile. -PKG_BASE_VERSION?=0.22.0 +PKG_MAINTAINER:=meow-rs +PKG_LICENSE:=GPL-3.0-or-later -# Rewritten automatically by .github/workflows/sync-upstream.yml; the LuCI -# package always carries the same version as the core it drives. -PKG_VERSION?=$(PKG_BASE_VERSION).202610130311 -PKG_RELEASE?=1 +include $(TOPDIR)/feeds/luci/luci.mk -include $(INCLUDE_DIR)/package.mk - -PKG_MAINTAINER:=Independent meow OpenWrt package - -define Package/luci-app-meow - SECTION:=luci - CATEGORY:=LuCI - SUBMENU:=3. Applications - TITLE:=LuCI support for meow - URL:=https://github.com/meow-rs/meow-rs - LICENSE:=MIT - DEPENDS:=+luci-base +meow +curl - PKGARCH:=all -endef - -define Package/luci-app-meow/description - LuCI interface for the meow OpenWrt package. - - Adds Services -> meow (Overview, Panel, Configuration, Clients, Settings, - Log). Runtime data comes from the meow REST API through authenticated LuCI - RPC; curl is only used against the router's loopback API. -endef - -define Build/Prepare -endef - -define Build/Compile -endef - -define Package/luci-app-meow/install - $(INSTALL_DIR) $(1) - cp -a ./root/. $(1)/ - $(INSTALL_DIR) $(1)/www/luci-static - cp -a ./htdocs/luci-static/. $(1)/www/luci-static/ - find $(1)/usr/libexec -type f -exec chmod 0755 {} + 2>/dev/null || true -endef - -$(eval $(call BuildPackage,luci-app-meow)) +# call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-meow/package-lock.json b/luci-app-meow/package-lock.json new file mode 100644 index 000000000..a74fafdc4 --- /dev/null +++ b/luci-app-meow/package-lock.json @@ -0,0 +1,514 @@ +{ + "name": "luci-app-meow-build", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "luci-app-meow-build", + "devDependencies": { + "esbuild": "0.28.2", + "yaml": "2.9.1" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + } + } +} diff --git a/luci-app-meow/package.json b/luci-app-meow/package.json new file mode 100644 index 000000000..69abcb9d5 --- /dev/null +++ b/luci-app-meow/package.json @@ -0,0 +1,6 @@ +{ + "name": "luci-app-meow-build", + "private": true, + "scripts": { "build": "node vendor/build.cjs" }, + "devDependencies": { "esbuild": "0.28.2", "yaml": "2.9.1" } +} diff --git a/luci-app-meow/vendor/README.md b/luci-app-meow/vendor/README.md new file mode 100644 index 000000000..f4d74a10e --- /dev/null +++ b/luci-app-meow/vendor/README.md @@ -0,0 +1,20 @@ +# YAML document parser + +`tools/meow_yaml.js` is a browser-only bundle of the ISC-licensed +[yaml](https://eemeli.org/yaml/) document API. It retains comments, aliases and +unrelated configuration while LuCI edits its managed keys. The bundled file +and `meow_yaml.LICENSE` ship in the LuCI package; npm is not needed on OpenWrt. + +Regenerate from pinned dependencies: + +```sh +cd openwrt/luci-app-meow +npm ci +npm run build +``` + +The Settings map's save callback runs after LuCI parses form values and before +UCI is saved. YAML validation/upload failures reject that save. If UCI saving +subsequently fails, the callback's rollback restores the previous YAML unless +another writer has changed it. The separate Configuration editor applies the +same transformation before validating imported YAML. diff --git a/luci-app-meow/vendor/build.cjs b/luci-app-meow/vendor/build.cjs new file mode 100644 index 000000000..aa6fb08c6 --- /dev/null +++ b/luci-app-meow/vendor/build.cjs @@ -0,0 +1,14 @@ +const { buildSync } = require('esbuild'); +const { copyFileSync } = require('node:fs'); +const path = require('node:path'); +const root = path.join(__dirname, '..'); +buildSync({ + entryPoints: [path.join(__dirname, 'yaml.js')], + outfile: path.join(root, 'htdocs/luci-static/resources/tools/meow_yaml.js'), + bundle: true, minify: true, platform: 'browser', format: 'iife', + globalName: 'MeowYaml', target: 'es2020', + banner: { js: "'use strict';\n'require baseclass';\n// Generated by npm ci && npm run build in openwrt/luci-app-meow; yaml 2.9.1 (ISC)." }, + footer: { js: 'return baseclass.extend(MeowYaml);' } +}); +copyFileSync(path.join(root, 'node_modules/yaml/LICENSE'), + path.join(root, 'htdocs/luci-static/resources/tools/meow_yaml.LICENSE')); diff --git a/luci-app-meow/vendor/yaml.js b/luci-app-meow/vendor/yaml.js new file mode 100644 index 000000000..a33ae442c --- /dev/null +++ b/luci-app-meow/vendor/yaml.js @@ -0,0 +1 @@ +export { parseDocument, isMap, isSeq } from 'yaml'; diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile index 370181b1d..7c07dba34 100644 --- a/luci-app-open-box/Makefile +++ b/luci-app-open-box/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-open-box -PKG_VERSION:=0.1.333 -PKG_RELEASE:=30 +PKG_VERSION:=0.1.334 +PKG_RELEASE:=31 PKG_MAINTAINER:=liandu2024 PKG_LICENSE:=GPL-3.0-or-later diff --git a/luci-app-openvpn-plus/Makefile b/luci-app-openvpn-plus/Makefile index cd11ffcf1..543230e32 100755 --- a/luci-app-openvpn-plus/Makefile +++ b/luci-app-openvpn-plus/Makefile @@ -10,7 +10,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-openvpn-plus PKG_VERSION:=1.0.0 -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Manfred Jaider diff --git a/luci-app-openvpn-plus/root/etc/openvpn/luci/default.nft b/luci-app-openvpn-plus/root/etc/openvpn/luci/default.nft index 460aa2575..4655a8593 100755 --- a/luci-app-openvpn-plus/root/etc/openvpn/luci/default.nft +++ b/luci-app-openvpn-plus/root/etc/openvpn/luci/default.nft @@ -1,4 +1,4 @@ -#!/bin/sh +#!/usr/sbin/nft -f # Copyright (C) 2008-2026 The OpenWrt Project # Copyright (C) 2026 Manfred Jaider @@ -6,7 +6,7 @@ # This is free software, licensed under the Apache License, Version 2.0. # See /LICENSE for more information. # -# luci-app-openvpn : specific openvpn flood protection protocol filters +# luci-app-openvpn : OpenVPN Flood-Guard - security firewall rules for OpenVPN packet validation # USEFUL COMMANDS FOR FIREWALL MANAGEMENT & LIVE MONITORING # @@ -19,65 +19,50 @@ # - View entire dynamic fw4 table: nft -a list table inet fw4 # - View main input chain with handles: nft -a list chain inet fw4 input # - View main forward chain with handles: nft -a list chain inet fw4 forward -# - View your custom OpenVPN sub-chain: nft -a list chain inet fw4 ovpn_filter_instance0 -# - Quick view of custom sub-chain: nft -a list chain inet fw4 ovpn_filter_instance0 | sed -E -n 's/.*(openvpn[^"]+).*counter (packets [0-9]+ bytes [0-9]+).*(# handle [0-9]+).*/\3 -> "\1" counter \2/p' +# - View your custom OpenVPN sub-chain: nft -a list chain inet fw4 openvpn_floodguard_instance0 +# - Quick view of custom sub-chain: nft -a list chain inet fw4 openvpn_floodguard_instance0 | sed -E -n 's/.*(openvpn[^"]+).*counter (packets [0-9]+ bytes [0-9]+).*(# handle [0-9]+).*/\3 -> "\1" counter \2/p' # # * Live Security & Attacker Tracking (The most useful commands) -# - View active IPv4 blocked IPs & timers: nft list set inet fw4 ovpn_block_ipv4_instance0 -# - Flush/Clear a specific ban list now: nft flush set inet fw4 ovpn_block_ipv4_instance0 +# - View active IPv4 blocked IPs & timers: nft list set inet fw4 openvpn_block_ipv4_instance0 +# - Flush/Clear a specific ban list now: nft flush set inet fw4 openvpn_block_ipv4_instance0 -fwPort=443 -fwProto="tcp" -SUB_CHAIN="ovpn_filter_instance0" +# Define named sets for blocked IPs with automatic timeout +set openvpn_block_ipv4_instance0 { + type ipv4_addr; + flags timeout; +}; -# STEP 1: CLEAN UP OLD RULES AND SETS (Prevents duplicates and memory leaks) +set openvpn_block_ipv6_instance0 { + type ipv6_addr; + flags timeout; +}; -# Find and delete the old jump rule handle inside the main input chain to unlock the sub-chain -oldJumpHandle=$(nft -a list chain inet fw4 input | grep "jump $SUB_CHAIN" | grep -oE 'handle [0-9]+' | tail -n 1 | awk '{print $2}') -if [ -n "$oldJumpHandle" ]; then - nft "delete rule inet fw4 input handle $oldJumpHandle" -fi - -# Now the sub-chain is unlocked and can be safely deleted without resource busy errors -nft "flush chain inet fw4 $SUB_CHAIN" 2>/dev/null -nft "delete chain inet fw4 $SUB_CHAIN" 2>/dev/null -nft "flush set inet fw4 ovpn_block_ipv4_instance0" 2>/dev/null -nft "delete set inet fw4 ovpn_block_ipv4_instance0" 2>/dev/null -nft "flush set inet fw4 ovpn_block_ipv6_instance0" 2>/dev/null -nft "delete set inet fw4 ovpn_block_ipv6_instance0" 2>/dev/null - -# STEP 2: BUILD FRESH NAMED SETS WITH AUTOMATIC TIMEOUT -nft "add set inet fw4 ovpn_block_ipv4_instance0 { type ipv4_addr ; flags timeout ; }" -nft "add set inet fw4 ovpn_block_ipv6_instance0 { type ipv6_addr ; flags timeout ; }" - -# STEP 3: CREATE THE CLEAN SUB-CHAIN CONTAINER -nft "add chain inet fw4 $SUB_CHAIN" - -# STEP 4: BUILD THE CASCADE RULES (based on protocol) -if [ "$fwProto" = "tcp" ]; then +# Define the flood guard filter chain table +chain openvpn_floodguard_instance0 { + # === START OF TCP BLOCK === # TCP-S1: Instantly rejects traffic from IPv4/IPv6 addresses listed in the block sets. - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 ip saddr @ovpn_block_ipv4_instance0 log prefix \"openvpn(instance0)[firewall]{ban}: \" counter reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 ip6 saddr @ovpn_block_ipv6_instance0 log prefix \"openvpn(instance0)[firewall]{ban}: \" counter reject with tcp reset" + tcp dport $ovpn_port ct packets < 12 ip saddr @openvpn_block_ipv4_instance0 log prefix "openvpn(instance0)[firewall]{ban}: " counter reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 ip6 saddr @openvpn_block_ipv6_instance0 log prefix "openvpn(instance0)[firewall]{ban}: " counter reject with tcp reset; # TCP-S2: Blocks fragmented TCP packets before they can cause buffer overflows. - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 ip frag-off & 0x3fff != 0 log prefix \"openvpn(instance0)[firewall]{fragment}: \" counter reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 exthdr frag exists log prefix \"openvpn(instance0)[firewall]{fragment}: \" counter reject with tcp reset" + tcp dport $ovpn_port ct packets < 12 ip frag-off & 0x3fff != 0x0 log prefix "openvpn(instance0)[firewall]{fragment}: " counter reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 exthdr frag exists log prefix "openvpn(instance0)[firewall]{fragment}: " counter reject with tcp reset; # TCP-S3: Validates strict TCP headers and forces standard handshake behaviors. - # - TCP Timestamps is required (This forces the TCP header size to be exactly 32 bytes for every data packets) + # - TCP Timestamps is required (this forces the TCP header size to be exactly 32 bytes for every data packets) # - SYN packets must have maxseg, window and sack-perm option. # - Data packets are forbidden to have the maxseg option. - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp option timestamp missing log prefix \"openvpn(instance0)[firewall]{timestamp}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == syn tcp option maxseg missing log prefix \"openvpn(instance0)[firewall]{maxseg}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == 0 tcp option maxseg exists log prefix \"openvpn(instance0)[firewall]{maxseg}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == syn tcp option window missing log prefix \"openvpn(instance0)[firewall]{wscale}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == syn tcp option sack-perm missing log prefix \"openvpn(instance0)[firewall]{sack}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp option timestamp missing log prefix \"openvpn(instance0)[firewall]{timestamp}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == syn tcp option maxseg missing log prefix \"openvpn(instance0)[firewall]{maxseg}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == 0 tcp option maxseg exists log prefix \"openvpn(instance0)[firewall]{maxseg}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == syn tcp option window missing log prefix \"openvpn(instance0)[firewall]{wscale}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == syn tcp option sack-perm missing log prefix \"openvpn(instance0)[firewall]{sack}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset" + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv4 tcp option timestamp missing log prefix "openvpn(instance0)[firewall]{timestamp}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv4 tcp flags & syn == syn tcp option maxseg missing log prefix "openvpn(instance0)[firewall]{maxseg}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv4 tcp flags ! syn tcp option maxseg exists log prefix "openvpn(instance0)[firewall]{maxseg}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv4 tcp flags & syn == syn tcp option window missing log prefix "openvpn(instance0)[firewall]{wscale}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv4 tcp flags & syn == syn tcp option sack-perm missing log prefix "openvpn(instance0)[firewall]{sack}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv6 tcp option timestamp missing log prefix "openvpn(instance0)[firewall]{timestamp}: " counter update @openvpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv6 tcp flags & syn == syn tcp option maxseg missing log prefix "openvpn(instance0)[firewall]{maxseg}: " counter update @openvpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv6 tcp flags ! syn tcp option maxseg exists log prefix "openvpn(instance0)[firewall]{maxseg}: " counter update @openvpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv6 tcp flags & syn == syn tcp option window missing log prefix "openvpn(instance0)[firewall]{wscale}: " counter update @openvpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv6 tcp flags & syn == syn tcp option sack-perm missing log prefix "openvpn(instance0)[firewall]{sack}: " counter update @openvpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset; # TCP-S4: Catches smart bots that delay data transmission to bypass early scanners. # * CRITICAL MINIMUM DATA SIZES (Worst-Case Real OpenVPN Client): @@ -90,69 +75,58 @@ if [ "$fwProto" = "tcp" ]; then # - IPv4 limit is set to < 700 Bytes. Bots with no data are smaller and get killed. # - IPv6 limit is set to < 900 Bytes. Bots with no data are smaller and get killed. # - Real worst-case OpenVPN users always stay safely above this byte radar. - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 ct packets > 9 meta nfproto ipv4 ct bytes < 700 log prefix \"openvpn(instance0)[firewall]{stealth}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 ct packets > 9 meta nfproto ipv6 ct bytes < 900 log prefix \"openvpn(instance0)[firewall]{stealth}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset" + tcp dport $ovpn_port ct packets < 12 ct packets > 9 meta nfproto ipv4 ct bytes < 700 log prefix "openvpn(instance0)[firewall]{stealth}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 ct packets > 9 meta nfproto ipv6 ct bytes < 900 log prefix "openvpn(instance0)[firewall]{stealth}: " counter update @openvpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset; # TCP-S5: Blocks payload data hidden inside the very first SYN packet (80 bytes for IPv4 packets and max 100 bytes for IPv6) - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == syn tcp option maxseg exists meta length > 80 log prefix \"openvpn(instance0)[firewall]{fastopen}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == syn tcp option maxseg exists meta length > 100 log prefix \"openvpn(instance0)[firewall]{fastopen}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset" + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv4 tcp flags & syn == syn tcp option maxseg exists meta length > 80 log prefix "openvpn(instance0)[firewall]{fastopen}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv6 tcp flags & syn == syn tcp option maxseg exists meta length > 100 log prefix "openvpn(instance0)[firewall]{fastopen}: " counter update @openvpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset; # TCP-S6: Deep Packet Inspection for bad protocol lengths and fake small sizes. # - Starts only when a data packet is bigger than 53 bytes (IPv4) or 73 bytes (IPv6). # - Reads the 16-bit OpenVPN packet length field at the exact bit offset pointer 256. # - Catches bad opcodes, web text (like GET/POST), and fake sizes (payload < 155, payload > 1499). # - Real users with tls-crypt-v2 are safe because encrypted traffic never matches this bad matrix. - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == 0 meta length > 53 @th,256,16 < 0x009b log prefix \"openvpn(instance0)[firewall]{payload}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv4 tcp flags & (syn) == 0 meta length > 53 @th,256,16 > 0x05db log prefix \"openvpn(instance0)[firewall]{payload}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == 0 meta length > 73 @th,256,16 < 0x009b log prefix \"openvpn(instance0)[firewall]{payload}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset" - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort ct packets < 12 meta nfproto ipv6 tcp flags & (syn) == 0 meta length > 73 @th,256,16 > 0x05db log prefix \"openvpn(instance0)[firewall]{payload}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset" + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv4 tcp flags ! syn meta length > 53 @th,256,16 < 0x9b log prefix "openvpn(instance0)[firewall]{payload}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv4 tcp flags ! syn meta length > 53 @th,256,16 > 0x5db log prefix "openvpn(instance0)[firewall]{payload}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv6 tcp flags ! syn meta length > 73 @th,256,16 < 0x9b log prefix "openvpn(instance0)[firewall]{payload}: " counter update @openvpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset; + tcp dport $ovpn_port ct packets < 12 meta nfproto ipv6 tcp flags ! syn meta length > 73 @th,256,16 > 0x5db log prefix "openvpn(instance0)[firewall]{payload}: " counter update @openvpn_block_ipv6_instance0 { ip6 saddr timeout 5m } reject with tcp reset; # TCP-S7: Packets that survived all previous checks are genuine. - nft "add rule inet fw4 $SUB_CHAIN tcp dport $fwPort accept" + tcp dport $ovpn_port accept; -else + # === END OF TCP BLOCK === + # === START OF UDP BLOCK === # UDP-S1: Instantly drops traffic from IPv4/IPv6 addresses listed in the block sets. - # (Bleibt bei ct state new, damit bestehender established Tunnel-Traffic niemals blockiert wird!) - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new ip saddr @ovpn_block_ipv4_instance0 log prefix \"openvpn(instance0)[firewall]{ban}: \" counter drop" - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new ip6 saddr @ovpn_block_ipv6_instance0 log prefix \"openvpn(instance0)[firewall]{ban}: \" counter drop" + udp dport $ovpn_port ct state new ip saddr @openvpn_block_ipv4_instance0 log prefix "openvpn(instance0)[firewall]{ban}: " counter drop; + udp dport $ovpn_port ct state new ip6 saddr @openvpn_block_ipv6_instance0 log prefix "openvpn(instance0)[firewall]{ban}: " counter drop; # UDP-S2: Blocks fragmented UDP packets before they can cause buffer overflows. - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 ip frag-off & 0x3fff != 0 log prefix \"openvpn(instance0)[firewall]{fragment}: \" counter drop" - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv6 exthdr frag exists log prefix \"openvpn(instance0)[firewall]{fragment}: \" counter drop" + udp dport $ovpn_port ct state new ip frag-off & 0x3fff != 0x0 log prefix "openvpn(instance0)[firewall]{fragment}: " counter drop; + udp dport $ovpn_port ct state new exthdr frag exists log prefix "openvpn(instance0)[firewall]{fragment}: " counter drop; # UDP-S3: Smallest valid OpenVPN packet is 48 bytes. Anything smaller is dropped. - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new udp length < 48 log prefix \"openvpn(instance0)[firewall]{short}: \" counter drop" + udp dport $ovpn_port ct state new udp length < 48 log prefix "openvpn(instance0)[firewall]{short}: " counter drop; # UDP-S4: Drops and bans new IPv4 bots that scan with a zero checksum to save time. - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 udp checksum 0 log prefix \"openvpn(instance0)[firewall]{chksum}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } drop" + udp dport $ovpn_port ct state new meta nfproto ipv4 udp checksum 0 log prefix "openvpn(instance0)[firewall]{chksum}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } drop; # UDP-S5: Whitelist valid initial OpenVPN opcodes P_CONTROL_HARD_RESET_CLIENT_V1,2,3 (skipping the 64-bit UDP header) - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 @th,64,8 & 0xf8 == 0x08 udp length > 250 accept" - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 @th,64,8 & 0xf8 == 0x38 udp length > 250 accept" - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 @th,64,8 & 0xf8 == 0x50 udp length > 250 accept" - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv6 @th,64,8 & 0xf8 == 0x08 udp length > 250 accept" - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv6 @th,64,8 & 0xf8 == 0x38 udp length > 250 accept" - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv6 @th,64,8 & 0xf8 == 0x50 udp length > 250 accept" + udp dport $ovpn_port ct state new meta nfproto ipv4 @th,64,8 & 0xf8 == 0x8 udp length > 250 accept; + udp dport $ovpn_port ct state new meta nfproto ipv4 @th,64,8 & 0xf8 == 0x38 udp length > 250 accept; + udp dport $ovpn_port ct state new meta nfproto ipv4 @th,64,8 & 0xf8 == 0x50 udp length > 250 accept; + udp dport $ovpn_port ct state new meta nfproto ipv6 @th,64,8 & 0xf8 == 0x8 udp length > 250 accept; + udp dport $ovpn_port ct state new meta nfproto ipv6 @th,64,8 & 0xf8 == 0x38 udp length > 250 accept; + udp dport $ovpn_port ct state new meta nfproto ipv6 @th,64,8 & 0xf8 == 0x50 udp length > 250 accept; # UDP-S6: Instantly ban and drop any other packet that tries to start a new connection - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv4 log prefix \"openvpn(instance0)[firewall]{opcode}: \" counter update @ovpn_block_ipv4_instance0 { ip saddr timeout 5m } drop" - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort ct state new meta nfproto ipv6 log prefix \"openvpn(instance0)[firewall]{opcode}: \" counter update @ovpn_block_ipv6_instance0 { ip6 saddr timeout 5m } drop" + udp dport $ovpn_port ct state new meta nfproto ipv4 log prefix "openvpn(instance0)[firewall]{opcode}: " counter update @openvpn_block_ipv4_instance0 { ip saddr timeout 5m } drop; + udp dport $ovpn_port ct state new meta nfproto ipv6 log prefix "openvpn(instance0)[firewall]{opcode}: " counter update @openvpn_block_ipv6_instance0 { ip6 saddr timeout 5m } drop; # UDP-S7: Packets that survived all previous checks are genuine. - nft "add rule inet fw4 $SUB_CHAIN udp dport $fwPort accept" - -fi - -# STEP 4: LINK THE SUB-CHAIN TO THE MAIN INPUT (single jump rule) - -# Find the starting anchor in the main input chain (Loopback handle) -mainAnchorHandle=$(nft -a list chain inet fw4 input | grep -E 'iif ["'\'']?lo["'\'']? accept' | grep -oE 'handle [0-9]+' | tail -n 1 | awk '{print $2}') - -if [ -z "$mainAnchorHandle" ]; then - mainAnchorHandle=35 -fi - -# Inject the dynamic jump trigger right below the loopback anchor rule -nft "add rule inet fw4 input handle $mainAnchorHandle $fwProto dport $fwPort jump $SUB_CHAIN" + udp dport $ovpn_port accept; + # === END OF UDP BLOCK === +}; +; diff --git a/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus b/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus index 99851053a..e22b0b138 100755 --- a/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus +++ b/luci-app-openvpn-plus/root/usr/libexec/luci-app-openvpn-plus @@ -442,9 +442,10 @@ firewallservice () { fi # 2. Call the standard OpenWrt system firewall controller safely - if [ "$action_cmd" = "stop" ] || [ "$action_cmd" = "start" ] || [ "$action_cmd" = "restart" ]; then + if [ "$action_cmd" = "stop" ] || [ "$action_cmd" = "start" ] || [ "$action_cmd" = "restart" ] || [ "$action_cmd" = "reload" ]; then /etc/init.d/firewall "$action_cmd" >/dev/null 2>&1 fi + return 0 } @@ -1371,6 +1372,7 @@ cleanup() { if ! uci -q get openvpn."$instname" >/dev/null; then rm -f "$conf_file" rm -f "$conf_dir/${instname}.nft" + rm -f "$conf_dir/${instname}.jmp.nft" rm -f "$conf_dir/${instname}.s2s.nft" rm -f "$key_dir/ca_${instname}.crt" rm -f "$key_dir/dh_${instname}.pem" @@ -1909,15 +1911,15 @@ ovpnservice_async() { rm -f "$pid_file" # 2. Start the target command via worker wrapper to handle absolute lifecycle traps - start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- ovpnservice_worker "$pid_file" "$action_cmd" "$inst" + start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- ovpnservice_worker "$action_cmd" "$inst" fi return 0 } ovpnservice_worker() { shift # Remove "ovpnservice_worker" from the arguments list - local pid_file="$1" - local cmd="$2" - local inst="$3" + local pid_file="/var/run/luci-app-openvpn-plus-ovpn-service.pid" + local cmd="$1" + local inst="$2" trap 'rm -f "$pid_file"' EXIT INT TERM if [ "$cmd" = "start" ]; then ovpnstart "ovpnstart" "$inst" 3 @@ -1941,14 +1943,14 @@ firewallservice_async() { # 2. Start the worker directly via the main script frame context # Inside the worker, we use a trap to cleanly remove the pid_file upon exit - start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- firewallservice_worker "$pid_file" "$action_cmd" + start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- firewallservice_worker "$action_cmd" fi return 0 } firewallservice_worker() { shift # Remove "firewallservice_worker" from the arguments list - local pid_file="$1" - local cmd="$2" + local cmd="$1" + local pid_file="/var/run/luci-app-openvpn-plus-firewall-service.pid" trap 'rm -f "$pid_file"' EXIT INT TERM firewallservice "firewallservice" "$cmd" 3 rm -f "$pid_file" @@ -1957,17 +1959,18 @@ firewallservice_worker() { initkeys_async() { shift # Remove "initkeys_async" from the arguments list + local pid_file="/var/run/openvpn.initkeys.pid" # 1. Check if the key generator is running - if start-stop-daemon -K -q -p "/var/run/openvpn.initkeys.pid" -t 2>/dev/null; then + if start-stop-daemon -K -q -p "$pid_file" -t 2>/dev/null; then exit 0 fi # 2. Start the background worker and create the PID file automatically - start-stop-daemon -S -b -m -p "/var/run/openvpn.initkeys.pid" -x "$0" -- "initkeys_worker" "/var/run/openvpn.initkeys.pid" + start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- "initkeys_worker" return 0 } initkeys_worker() { shift # Remove "initkeys_worker" from the arguments list - local pid_file="$1" + local pid_file="/var/run/openvpn.initkeys.pid" # 3. Secure the PID file lifecycle instantly with a trap framework trap 'rm -f "$pid_file"' EXIT INT TERM # 4. Run the key creation function quietly in the background @@ -1980,14 +1983,23 @@ keygen_async() { shift # Remove "keygen" from the arguments list local type="$1" local instance="$2" + if [ -z "$instance" ] || ! echo "$instance" | grep -Eq '^[A-Za-z0-9_-]+$'; then + echo "ERROR: Invalid instance name." >&2 + return 1 + fi + local pid_file="/var/run/openvpn.keygen-${instance}.pid" # 1. Start the background keygen worker and create the PID file automatically - start-stop-daemon -S -b -m -p "/var/run/openvpn.keygen-${instance}.pid" -x "$0" -- "keygen_worker" "/var/run/openvpn.keygen-${instance}.pid" "$@" + start-stop-daemon -S -b -m -p "$pid_file" -x "$0" -- "keygen_worker" "$instance" "$@" return 0 } keygen_worker() { shift # Remove "keygen_worker" from the arguments list - local pid_file="$1" - shift # Remove "pid_file" from the arguments list + local instance="$1" + if [ -z "$instance" ] || ! echo "$instance" | grep -Eq '^[A-Za-z0-9_-]+$'; then + return 1 + fi + local pid_file="/var/run/openvpn.keygen-${instance}.pid" + shift # Remove "instance" from the arguments list # 3. Secure the dynamic PID node tracking with a strict trap guard trap 'rm -f "$pid_file"' EXIT INT TERM # 4. Run the certificate engine pipeline with corrected original parameters ($@: type="$1", instance="$2") diff --git a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-keygen.js b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-keygen.js index 1c6ed2bb8..ffb5f48aa 100755 --- a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-keygen.js +++ b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-keygen.js @@ -189,6 +189,82 @@ const sanitizeInputText = function (value) { */ +/** + * Cryptographic Asset Identity Enum + */ +const CRYPTO_TYPE = Object.freeze({ + CA_TRUE: 'CA_TRUE', + STANDARD_CERTIFICATE: 'STANDARD_CERTIFICATE', + PUBLIC_KEY: 'PUBLIC_KEY', + PRIVATE_KEY: 'PRIVATE-KEY', + TLS2_SERVER_KEY: 'TLS2_SERVER_KEY', + TLS2_CLIENT_KEY: 'TLS2_CLIENT_KEY', + DH_PARAMETERS: 'DH_PARAMETERS', + INVALID_KEY: 'INVALID_KEY', +}); + +/** + * Reads, validates, and categorizes cryptographic files using a secure Enum mapper + */ +const getKeyMeta = async function (keyfile, luci_app_openvpn_plus_callback) { + const result = { + valid: false, + type: CRYPTO_TYPE.INVALID_KEY, + metadata: { type: _('Unknown Structure'), subject: '', expires: '', algo: '', curve: '' }, + raw: '' + }; + + // 1. Guard check: Ensure keyfile is given and callback is a callable execution pipe + if (!keyfile || typeof luci_app_openvpn_plus_callback !== 'function') { + console.error("getKeyMeta execution aborted: keyfile is missing or callback parameter is not a valid function."); + return result; + } + + // 2. Safely trigger the verified callback mechanism + const res = await luci_app_openvpn_plus_callback([CFG.LIBEXEC.keymeta, keyfile]); + + // Early escape if the backend fails or outputs errors + if (!res || res.length === 0 || res.startsWith('ERROR') || res.toUpperCase().indexOf('ERROR') !== -1) { + return result; + } + + result.raw = res; + result.valid = true; + const rawUpper = res.toUpperCase(); + + // DETERMINISTIC ENUM MAPPER (Maps backend strings directly to your explicit Enum keys) + if (rawUpper.indexOf('AUTHORITY') !== -1 || rawUpper.indexOf('CA:TRUE') !== -1) { + result.type = CRYPTO_TYPE.CA_TRUE; + } else if (rawUpper.indexOf('STANDARD CERTIFICATE') !== -1) { + result.type = CRYPTO_TYPE.STANDARD_CERTIFICATE; + } else if (rawUpper.indexOf('PUBLIC-KEY') !== -1) { + result.type = CRYPTO_TYPE.PUBLIC_KEY; + } else if (rawUpper.indexOf('PRIVATE-KEY') !== -1) { + result.type = CRYPTO_TYPE.PRIVATE_KEY; + } else if (rawUpper.indexOf('TLSV2-SERVER-KEY') !== -1) { + result.type = CRYPTO_TYPE.TLS2_SERVER_KEY; + } else if (rawUpper.indexOf('TLSV2-CLIENT-KEY') !== -1) { + result.type = CRYPTO_TYPE.TLS2_CLIENT_KEY; + } else if (rawUpper.indexOf('DH PARAMETERS') !== -1) { + result.type = CRYPTO_TYPE.DH_PARAMETERS; + } + + // Parse human-readable lines for UI panels + const lines = res.split('\n'); + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line === '') continue; + + if (line.startsWith('TYPE:')) result.metadata.type = line.replace('TYPE:', '').trim(); + else if (line.startsWith('Subject:')) result.metadata.subject = line.replace('Subject:', '').trim(); + else if (line.startsWith('Not After :') || line.startsWith('Not After:')) result.metadata.expires = line.replace(/Not After\s*:\s*/, '').trim(); + else if (line.startsWith('Public-Key:') || line.startsWith('Private-Key:')) result.metadata.algo = line.trim(); + else if (line.startsWith('ASN1 OID:') || line.startsWith('NIST Curve:') || line.startsWith('Curve:')) result.metadata.curve = line.trim(); + } + + return result; +}; + /** * Show simple key editor that verifies and displays key metadata. */ @@ -208,70 +284,86 @@ const openKeyEditorModal = function (filename, instance_id, displayId, role, app }); // Update the quick info box content - const quickInfoBoxUpdate = function (rawMeta) { + const quickInfoBoxUpdate = function (keyInfo) { let keyTypeInfo = TXT.KEY.unknown_asset; let expirationInfo = TXT.KEY.not_applicable; let commonNameInfo = ''; let isInvalid = false; - const cleanMeta = String(rawMeta || '').trim(); - // Parse expiration date safely - const expiryMatch = cleanMeta.match(/Not After\s*:\s*([^\n\r]+)/i); - if (expiryMatch && expiryMatch[1]) { - expirationInfo = expiryMatch[1].trim(); - } + // 1. Map directly from our clean metadata object if the key is valid + if (keyInfo && keyInfo.valid === true) { + const meta = keyInfo.metadata; + const rawUpper = String(keyInfo.raw || '').toUpperCase(); - // Extract Subject Common Name perfectly supporting optional spaces - const cnMatch = cleanMeta.match(/Subject\s*:\s*CN\s*=\s*([^,\n\r]+)/i); - if (cnMatch && cnMatch[1]) { - commonNameInfo = cnMatch[1].trim(); - } + if (meta.expires) { + expirationInfo = meta.expires; + } - // Identify key type and strength using clean text patterns - if ((cleanMeta.indexOf('Public-Key: (4096 bit)') !== -1) || (cleanMeta.indexOf('Private-Key: (4096 bit)') !== -1)) { - keyTypeInfo = 'RSA 4096 Bit'; - } else if (cleanMeta.indexOf('Public-Key: (256 bit)') !== -1 || cleanMeta.indexOf('prime256v1') !== -1) { - keyTypeInfo = 'ECC Prime256v1'; - } else if ((cleanMeta.indexOf('Public-Key: (2048 bit)') !== -1) || (cleanMeta.indexOf('Private-Key: (2048 bit)') !== -1 || cleanMeta.indexOf('PRIVATE-KEY') !== -1)) { - keyTypeInfo = 'RSA 2048 Bit'; - } else if (cleanMeta.indexOf('DH PARAMETERS') !== -1 || cleanMeta.indexOf('bit') !== -1) { - const dhBits = cleanMeta ? cleanMeta.match(/([0-9]+)\s*bit/i) : null; - const dbits_info = dhBits ? '(' + dhBits[1] + ' Bit)' : ''; - keyTypeInfo = 'Diffie-Hellman ' + dbits_info; - } else if (cleanMeta.indexOf('TLSv2-Server-Key') !== -1) { - keyTypeInfo = 'TLS Crypt v2'; - } else if (cleanMeta.indexOf('TLSv2-Client-Key') !== -1) { - keyTypeInfo = 'TLS Crypt v2'; - } else if (cleanMeta.indexOf('Unsupported') !== -1) { - keyTypeInfo = '' + ICON.WARNING + TXT.WARNING.key_decrepted + ''; - isInvalid = true; - } else if (cleanMeta.indexOf('ERROR') !== -1) { + // Extract Subject Common Name perfectly supporting optional spaces + const cnMatch = meta.subject.match(/CN\s*=\s*([^,\n\r]+)/i); + if (cnMatch && cnMatch[1]) { + commonNameInfo = cnMatch[1].trim(); + } + + // Identify key type and strength using clean Enum keys + switch (keyInfo.type) { + case CRYPTO_TYPE.CA_TRUE: + case CRYPTO_TYPE.STANDARD_CERTIFICATE: + case CRYPTO_TYPE.PUBLIC_KEY: + case CRYPTO_TYPE.PRIVATE_KEY: + if (rawUpper.indexOf('4096 BIT') !== -1) { + keyTypeInfo = 'RSA 4096 Bit'; + } else if (rawUpper.indexOf('256 BIT') !== -1 || rawUpper.indexOf('PRIME256V1') !== -1) { + keyTypeInfo = 'ECC Prime256v1'; + } else { + keyTypeInfo = 'RSA 2048 Bit'; + } + break; + case CRYPTO_TYPE.DH_PARAMETERS: { + const dhBits = rawUpper.match(/([0-9]+)\s*BIT/i); + const dbits_info = dhBits ? '(' + dhBits[1] + ' Bit)' : ''; + keyTypeInfo = 'Diffie-Hellman ' + dbits_info; + break; + } + case CRYPTO_TYPE.TLS2_SERVER_KEY: + case CRYPTO_TYPE.TLS2_CLIENT_KEY: + keyTypeInfo = 'TLS Crypt v2'; + break; + default: + if (rawUpper.indexOf('UNSUPPORTED') !== -1) { + keyTypeInfo = '' + ICON.WARNING + TXT.WARNING.key_decrepted + ''; + isInvalid = true; + } + break; + } + + // Add dynamic Role identity decorations to the type string + if (!isInvalid) { + const strRole = (role === OPENVPN.ROLE.SERVER) ? TXT.INFO.server : TXT.INFO.client; + + if (keyInfo.type === CRYPTO_TYPE.CA_TRUE) { + if (rawUpper.indexOf('SECRET-KEY-CA') !== -1) { + keyTypeInfo += ' - [' + TXT.KEY.master_ca + ' ' + TXT.KEY.certificate + ' ' + TXT.KEY.secret_ca + ']'; + } else { + keyTypeInfo += ' - [' + TXT.KEY.master_ca + ' ' + TXT.KEY.certificate + ']'; + } + } else if (keyInfo.type === CRYPTO_TYPE.STANDARD_CERTIFICATE) { + keyTypeInfo += ' - [' + strRole + ' ' + TXT.KEY.certificate + ']'; + } else if (keyInfo.type === CRYPTO_TYPE.PRIVATE_KEY) { + keyTypeInfo += ' - [' + strRole + ' ' + TXT.KEY.private_key + ']'; + } else if (keyInfo.type === CRYPTO_TYPE.TLS2_SERVER_KEY) { + keyTypeInfo += ' - [' + TXT.KEY.tls_key_server + ']'; + } else if (keyInfo.type === CRYPTO_TYPE.TLS2_CLIENT_KEY) { + keyTypeInfo += ' - [' + TXT.KEY.tls_key_client + ']'; + } + } + } else { + // Fallback state if getKeyMeta marked the asset file as invalid keyTypeInfo = '' + ICON.ERROR + TXT.ERROR.format_corruption + ''; isInvalid = true; } - // Role identities to make the type text easy to read for the admin - if (!isInvalid) { - const strRole = (role === OPENVPN.ROLE.SERVER) ? TXT.INFO.server : TXT.INFO.client; - - if (cleanMeta.indexOf('AUTHORITY') !== -1 || cleanMeta.indexOf('CA:TRUE') !== -1) { - if (cleanMeta.indexOf('SECRET-KEY-CA') !== -1) { - keyTypeInfo += ' - [' + TXT.KEY.master_ca + ' ' + TXT.KEY.certificate + ' ' + TXT.KEY.secret_ca + ']'; - } else { - keyTypeInfo += ' - [' + TXT.KEY.master_ca + ' ' + TXT.KEY.certificate + ']'; - } - } else if (cleanMeta.indexOf('STANDARD CERTIFICATE') !== -1) { - keyTypeInfo += ' - [' + strRole + ' ' + TXT.KEY.certificate + ']'; - } else if (cleanMeta.indexOf('Private-Key') !== -1 || cleanMeta.indexOf('PRIVATE-KEY') !== -1) { - keyTypeInfo += ' - [' + strRole + ' ' + TXT.KEY.private_key + ']'; - } else if (cleanMeta.indexOf('TLSv2-Server-Key') !== -1) { - keyTypeInfo += ' - [' + TXT.KEY.tls_key_server + ']'; - } else if (cleanMeta.indexOf('TLSv2-Client-Key') !== -1) { - keyTypeInfo += ' - [' + TXT.KEY.tls_key_client + ']'; - } - } - - // Apply styles and render HTML nodes dynamically + // 3. Apply styles and render HTML nodes dynamically if (isInvalid) { quickInfoBox.style.borderLeftColor = 'var(--success-color-high, #00ac59)'; quickInfoBox.innerHTML = keyTypeInfo; @@ -288,17 +380,22 @@ const openKeyEditorModal = function (filename, instance_id, displayId, role, app quickInfoBox.style.display = 'block'; }; - // Read file and parse metadata - secureFileCallbacks.secureReadFile(absolutePath).then(function (content) { + // Read file and parse metadata asynchronously + secureFileCallbacks.secureReadFile(absolutePath).then(async function (content) { + // 1. Fill the textarea workspace panel instantly modalKeyTextArea.value = content ? content.trim() + '\n' : '--- EMPTY OR BLANK KEY FILE ---'; - if (!content || content.trim() === '') return; + // 2. Early escape if the file is completely empty to prevent unneeded backend queries + if (!content || content.trim() === '') { + quickInfoBoxUpdate({ valid: false }); + return; + } - secureFileCallbacks.luci_app_openvpn_plus([CFG.LIBEXEC.keymeta, filename]).then(function (res) { - if (res.length > 0) { - quickInfoBoxUpdate(res); - } - }); + // 3. FIX: 'async function' allows using 'await' here flawlessly! + const keyInfo = await getKeyMeta(filename, secureFileCallbacks.luci_app_openvpn_plus); + + // 4. Update the visual UI panel metrics + quickInfoBoxUpdate(keyInfo); }); const modalSaveApplyBtn = E('button', { @@ -306,45 +403,45 @@ const openKeyEditorModal = function (filename, instance_id, displayId, role, app 'style': 'margin-right:10px;' }, [TXT.BTN.save_apply]); - modalSaveApplyBtn.addEventListener('click', function () { + modalSaveApplyBtn.addEventListener('click', async function () { modalSaveApplyBtn.disabled = true; modalSaveApplyBtn.textContent = TXT.BTN.saving; // Standardize all copy-pasted line endings instantly (Supports UNIX \n, Windows \r\n, and Mac \r) const sanitizedKeyContent = sanitizeInputText(modalKeyTextArea.value) + '\n'; - // Write file and re-validate key metadata (Using our clean sanitized text string) - secureFileCallbacks.secureWriteFile(absolutePath, sanitizedKeyContent) - .then(function () { - return secureFileCallbacks.luci_app_openvpn_plus([CFG.LIBEXEC.keymeta, filename]); - }) - .then(function (keymeta) { + try { + // 1. Write the clean sanitized text string to disk + await secureFileCallbacks.secureWriteFile(absolutePath, sanitizedKeyContent); - if (keymeta.indexOf('ERROR') !== -1) { - quickInfoBoxUpdate(keymeta); - modalSaveApplyBtn.disabled = false; - modalSaveApplyBtn.textContent = TXT.BTN.save_apply; - return Promise.reject(new Error('Editor validation failed')); - } + // 2. Re-validate key metadata using our smart callback engine + const keyInfo = await getKeyMeta(filename, secureFileCallbacks.luci_app_openvpn_plus); - hasSaved = true; - modalSaveApplyBtn.textContent = TXT.BTN.saved; + // 3. Evaluate the cryptographic validation status + if (keyInfo.valid === false) { + quickInfoBoxUpdate(keyInfo); // Renders the error state inside the UI box + modalSaveApplyBtn.disabled = false; + modalSaveApplyBtn.textContent = TXT.BTN.save_apply; + return; // Stop execution instantly + } - setTimeout(function () { - modalSaveApplyBtn.disabled = false; - modalSaveApplyBtn.textContent = TXT.BTN.save_apply; - if (keymeta !== '') { - quickInfoBoxUpdate(keymeta); - } - }, 1200); - }) - .catch(function (err) { - if (err.message !== 'Editor validation failed') { - console.error('Editor save chain caught unhandled error context:', err); - modalSaveApplyBtn.disabled = false; - modalSaveApplyBtn.textContent = TXT.BTN.save_apply; - } - }); + // 4. Success path: Key is good! + hasSaved = true; + modalSaveApplyBtn.textContent = TXT.BTN.saved; + + // Quick temporary visual feedback loop for the administrator + setTimeout(function () { + modalSaveApplyBtn.disabled = false; + modalSaveApplyBtn.textContent = TXT.BTN.save_apply; + quickInfoBoxUpdate(keyInfo); + }, 1200); + + } catch (err) { + console.error('Editor save chain caught unhandled error context:', err); + modalSaveApplyBtn.disabled = false; + modalSaveApplyBtn.textContent = TXT.BTN.save_apply; + quickInfoBoxUpdate({ valid: false }); + } }); const modalDownloadKeyBtn = E('button', { @@ -1244,6 +1341,8 @@ const openKeyGenModal = function (instance_id, displayId, role, appData, uciSave return L.Class.extend( { KEYTYPE: KEYTYPE, + CRYPTO_TYPE: CRYPTO_TYPE, + getKeyMeta: getKeyMeta, executeAsynchronousKeyGen: executeAsynchronousKeyGen, openKeyEditorModal: openKeyEditorModal, openAutomatedPostKeyGenModal: openAutomatedPostKeyGenModal, diff --git a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-status.js b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-status.js index 54641bc60..4bf3fca4a 100755 --- a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-status.js +++ b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-status.js @@ -82,8 +82,8 @@ const OPENVPN = Object.freeze({ CLIENT: 'client' }), PROTO: Object.freeze({ - TCP: 'tcp', - UDP: 'udp' + tcp: 'tcp', + udp: 'udp' }), PORT: Object.freeze({ s1194: '1194' @@ -173,9 +173,15 @@ const nativeFileExecRpc = L.rpc.declare({ const luci_app_openvpn_plus = async function (args, skipTrim) { const extraArgs = (typeof args === 'string') ? [args] : (Array.isArray(args) ? args : []); const res = await L.resolveDefault(nativeFileExecRpc(CFG.LIBEXEC.luci_app_openvpn_plus, extraArgs), {}); + + // CHECK EXIT CODE: If the backend returned anything else than 0, it failed! + if (res && typeof res === 'object' && res.code !== undefined && res.code !== 0) { + return 'ERROR: CODE_' + res.code; + } if (res && typeof res === 'object' && typeof res.stdout === 'string') { if (skipTrim === true) { - return res.stdout.trim(); + // Remove ONLY the very last trailing newline character cleanly + return res.stdout.replace(/\r?\n$/, ''); } else { return res.stdout.trim(); } @@ -210,10 +216,10 @@ const rpcGetServiceStatus = async function () { const secureReadStatusFile = async function (filepath) { // PASS true AS SECOND PARAMETER TO KEEP ALL NEWLINES AND SPACES! const res = await luci_app_openvpn_plus([CFG.LIBEXEC.readstatus, filepath], true); - if (res && res.length > 0) { - return res; + if (!res || res.length === 0 || res.startsWith('ERROR')) { + return null; } - return null; + return res; }; /** @@ -221,15 +227,11 @@ const secureReadStatusFile = async function (filepath) { */ const secureStatFile = async function (filepath) { const res = await luci_app_openvpn_plus([CFG.LIBEXEC.statfile, filepath]); - try { - if (res && res.startsWith("{")) { - const statObj = JSON.parse(res); - if (statObj && typeof statObj === 'object') { - return statObj; - } + if (res && res.startsWith("{")) { + const statObj = JSON.parse(res); + if (statObj && typeof statObj === 'object') { + return statObj; } - } catch (err) { - console.error("Failed to parse file statistics: ", CFG.LIBEXEC.luci_app_openvpn_plus + ' ' + CFG.LIBEXEC.statfile + ' ' + filepath + ' -> ' + err); } return null; }; @@ -277,14 +279,14 @@ const openVpnInfo = { } /** - * Checks if at least one OpenVPN instance is enabled in the configuration array + * Checks if at least one OpenVPN instance is enabled in the configuration array and collects system capabilities securely. */ const getOpenVpnInfo = async function () { - // get mac address + // 1. Get MAC address safely let rawMac = await luci_app_openvpn_plus([CFG.LIBEXEC.getmac]); - if (rawMac.length > 0 && rawMac !== '00:00:00:00:00:00') { + if (rawMac && rawMac.length > 0 && !rawMac.startsWith('ERROR') && rawMac !== '00:00:00:00:00:00') { const cleanMac = rawMac.replace(/[^a-fA-F0-9]/g, ''); // Isolate the last 3 hex characters (12-bit entropy seed) @@ -297,31 +299,34 @@ const getOpenVpnInfo = async function () { } } - // get ovpn version + // 2. Get OpenVPN version and capabilities safely const rawVersion = await luci_app_openvpn_plus([CFG.LIBEXEC.ovpnver]); - if (rawVersion.length > 0) { - // Looks for "OpenVPN" followed by spaces, and stops at the first space or bracket. - // This safely ignores git tags like [git:DSM7-...] or OS tags. - const ovpnMatch = rawVersion.match(/OpenVPN\s+([0-9.]+)/); - if (ovpnMatch && ovpnMatch[1]) { - openVpnInfo.version = ovpnMatch[1]; - } - // This searches for keywords like OpenSSL, mbedTLS, or wolfSSL followed by their numbers. - const sslMatch = rawVersion.match(/(OpenSSL|mbedTLS|wolfSSL)\s+([0-9a-zA-Z.]+)/i); - if (sslMatch) { - // sslMatch[1] is the name (e.g. "OpenSSL"), sslMatch[2] is the version (e.g. "3.5.8") - openVpnInfo.openssl = sslMatch[2]; - openVpnInfo.openssl_type = sslMatch[1]; - } - - // Looks for "DCO version:". If it finds "N/A", we keep the default 'Disabled' state. - const dcoMatch = rawVersion.match(/DCO version:\s+([0-9.]+)/); - if (dcoMatch && dcoMatch[1]) { - openVpnInfo.dco = dcoMatch[1]; - } + // Abort if the version query returned a backend error or empty response + if (!rawVersion || rawVersion.length === 0 || rawVersion.startsWith('ERROR')) { + return; } -} + + // Looks for "OpenVPN" followed by spaces, and stops at the first space or bracket. + const ovpnMatch = rawVersion.match(/OpenVPN\s+([0-9.]+)/); + if (ovpnMatch && ovpnMatch[1]) { + openVpnInfo.version = ovpnMatch[1]; + } + + // This searches for keywords like OpenSSL, mbedTLS, or wolfSSL followed by their numbers. + const sslMatch = rawVersion.match(/(OpenSSL|mbedTLS|wolfSSL)\s+([0-9a-zA-Z.]+)/i); + if (sslMatch) { + openVpnInfo.openssl = sslMatch[2]; + openVpnInfo.openssl_type = sslMatch[1]; + } + + // Looks for "DCO version:". If it finds "N/A", we keep the default 'Disabled' state. + const dcoMatch = rawVersion.match(/DCO version:\s+([0-9.]+)/); + if (dcoMatch && dcoMatch[1]) { + openVpnInfo.dco = dcoMatch[1]; + } +}; + /** * Reads system uptime from the proc filesystem asynchronously @@ -338,99 +343,99 @@ const getSystemUptime = async function () { * Reads network statistics using parallel RPC requests and updates each instance object */ const refreshNetStat = async function (instances) { - try { - // Check if the input is a valid array with data - if (!Array.isArray(instances) || instances.length === 0) { - return; + // Check if the input is a valid array with data + if (!Array.isArray(instances) || instances.length === 0) { + return; + } + + // Reset all netstat fields to a default empty structure first + for (let i = 0; i < instances.length; i++) { + instances[i].netstat = { + rxBytes: 0, + txBytes: 0, + rxPkts: 0, + txPkts: 0, + hasData: false + }; + } + + const promises = []; + + // Step 1: Start RPC requests for running instances at the same time + for (let j = 0; j < instances.length; j++) { + const inst = instances[j]; + if (inst && inst.isRunning === true && inst.devName) { + promises.push(luci_app_openvpn_plus([CFG.LIBEXEC.getnetstat, inst.devName])); + } + } + + // Step 2: Wait for all parallel backend calls to finish + const results = await Promise.all(promises); + + // Step 3: Parse the results and map them directly into the correct instance + for (let k = 0; k < results.length; k++) { + const res_getnetstat = results[k] ? String(results[k]).trim() : ''; + + // Catch backend execution errors immediately and skip this result row + if (res_getnetstat === '' || res_getnetstat.startsWith('ERROR')) { + continue; } - // Reset all netstat fields to a default empty structure first - for (let i = 0; i < instances.length; i++) { - instances[i].netstat = { - rxBytes: 0, - txBytes: 0, - rxPkts: 0, - txPkts: 0, - hasData: false - }; - } + const partsMetrics = res_getnetstat.split('|'); - const promises = []; + // Check if the backend returned all 6 fields (rxb|txb|rxp|txp|status|iface) + if (partsMetrics && partsMetrics.length === 6) { + const ifaceName = partsMetrics[5]; + const isUp = (partsMetrics[4] === 'true'); - // Step 1: Start RPC requests for running instances at the same time - for (let j = 0; j < instances.length; j++) { - const inst = instances[j]; - if (inst && inst.isRunning === true && inst.devName) { - promises.push(luci_app_openvpn_plus([CFG.LIBEXEC.getnetstat, inst.devName])); - } - } + // Find the matching instance by comparing the device name + for (let m = 0; m < instances.length; m++) { + const targetInst = instances[m]; + const expectedDev = targetInst.devName; - // Step 2: Wait for all parallel backend calls to finish - const results = await Promise.all(promises); - - // Step 3: Parse the results and map them directly into the correct instance - for (let k = 0; k < results.length; k++) { - const res_getnetstat = results[k] ? String(results[k]).trim() : ''; - const partsMetrics = res_getnetstat.split('|'); - - // Check if the backend returned all 6 fields (rxb|txb|rxp|txp|status|iface) - if (partsMetrics && partsMetrics.length === 6) { - const ifaceName = partsMetrics[5]; - const isUp = (partsMetrics[4] === 'true'); - - // Find the matching instance by comparing the device name - for (let m = 0; m < instances.length; m++) { - const targetInst = instances[m]; - const expectedDev = targetInst.devName; - - if (expectedDev === ifaceName) { - targetInst.netstat.rxBytes = parseInt(partsMetrics[0], 10) || 0; - targetInst.netstat.txBytes = parseInt(partsMetrics[1], 10) || 0; - targetInst.netstat.rxPkts = parseInt(partsMetrics[2], 10) || 0; - targetInst.netstat.txPkts = parseInt(partsMetrics[3], 10) || 0; - targetInst.netstat.hasData = isUp; - break; - } + if (expectedDev === ifaceName) { + targetInst.netstat.rxBytes = parseInt(partsMetrics[0], 10) || 0; + targetInst.netstat.txBytes = parseInt(partsMetrics[1], 10) || 0; + targetInst.netstat.rxPkts = parseInt(partsMetrics[2], 10) || 0; + targetInst.netstat.txPkts = parseInt(partsMetrics[3], 10) || 0; + targetInst.netstat.hasData = isUp; + break; } } } + } - // Step 4: Sanity check to fix DCO kernel sync delays against active session bytes - for (let n = 0; n < instances.length; n++) { - const activeInst = instances[n]; + // Step 4: Sanity check to fix DCO kernel sync delays against active session bytes + for (let n = 0; n < instances.length; n++) { + const activeInst = instances[n]; - // Only compare if the instance is running and has connected clients - if (activeInst && activeInst.isRunning === true && Array.isArray(activeInst.connectedClients)) { - let totalClientRx = 0; - let totalClientTx = 0; + // Only compare if the instance is running and has connected clients + if (activeInst && activeInst.isRunning === true && Array.isArray(activeInst.connectedClients)) { + let totalClientRx = 0; + let totalClientTx = 0; - // Sum up the bytes from all currently connected clients - for (let c = 0; c < activeInst.connectedClients.length; c++) { - const client = activeInst.connectedClients[c]; - if (client) { - totalClientRx += client.bytesReceived || 0; - totalClientTx += client.bytesSent || 0; - } - } - - // If kernel statistics are lower than session bytes, force the session values - if (activeInst.netstat.rxBytes < totalClientRx) { - activeInst.netstat.rxBytes = totalClientRx; - activeInst.netstat.hasData = true; - } - if (activeInst.netstat.txBytes < totalClientTx) { - activeInst.netstat.txBytes = totalClientTx; - activeInst.netstat.hasData = true; + // Sum up the bytes from all currently connected clients + for (let c = 0; c < activeInst.connectedClients.length; c++) { + const client = activeInst.connectedClients[c]; + if (client) { + totalClientRx += client.bytesReceived || 0; + totalClientTx += client.bytesSent || 0; } } - } - } catch (err) { - console.error('Error refreshing network data:', err); + // If kernel statistics are lower than session bytes, force the session values + if (activeInst.netstat.rxBytes < totalClientRx) { + activeInst.netstat.rxBytes = totalClientRx; + activeInst.netstat.hasData = true; + } + if (activeInst.netstat.txBytes < totalClientTx) { + activeInst.netstat.txBytes = totalClientTx; + activeInst.netstat.hasData = true; + } + } } }; - /** * Parses client IP addresses from the OpenVPN status log text file */ @@ -676,7 +681,7 @@ const parsePortFromConfig = function (role, content) { * Extracts the OpenVPN protocol string (udp/tcp) from the configuration text safely */ const parseProtoFromConfig = function (content) { - if (!content) return OPENVPN.PROTO.UDP; + if (!content) return OPENVPN.PROTO.udp; let lines if (Array.isArray(content)) { @@ -702,12 +707,12 @@ const parseProtoFromConfig = function (content) { if (tokens[0].toLowerCase() === 'proto') { const protoVal = tokens[1].toLowerCase(); // Handle standard openvpn proto values (udp, tcp, udp4, tcp4, udp6, tcp6) - if (protoVal.indexOf(OPENVPN.PROTO.TCP) !== -1) return OPENVPN.PROTO.TCP; - if (protoVal.indexOf(OPENVPN.PROTO.UDP) !== -1) return OPENVPN.PROTO.UDP; + if (protoVal.indexOf(OPENVPN.PROTO.tcp) !== -1) return OPENVPN.PROTO.tcp; + if (protoVal.indexOf(OPENVPN.PROTO.udp) !== -1) return OPENVPN.PROTO.udp; return protoVal; } } - return OPENVPN.PROTO.UDP; // Default fallback matching standard OpenVPN defaults + return OPENVPN.PROTO.udp; // Default fallback matching standard OpenVPN defaults }; /** @@ -1445,7 +1450,7 @@ const renderRemoteNode = function (role, isRunning, connectedClients, protoStr, // Get the first part and convert to lowercase for the check const firstPart = parts[0].toLowerCase(); // If first part is a protocol prefix, process and clean it - if (firstPart.indexOf(OPENVPN.PROTO.UDP) === 0 || firstPart.indexOf(OPENVPN.PROTO.TCP) === 0) { + if (firstPart.indexOf(OPENVPN.PROTO.udp) === 0 || firstPart.indexOf(OPENVPN.PROTO.tcp) === 0) { // Remove everything after the dash (e.g. "tcp4-server" becomes "tcp4") currentProto = firstPart.split('-')[0]; displayAddress = parts.slice(1).join(':'); // Keep only IP and port diff --git a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-wizard.js b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-wizard.js index 359e1dd49..1248ba174 100755 --- a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-wizard.js +++ b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn-wizard.js @@ -225,8 +225,8 @@ const OPENVPN = Object.freeze({ SITETOSITE: 'sitetosite' }), PROTO: Object.freeze({ - TCP: 'tcp', - UDP: 'udp' + tcp: 'tcp', + udp: 'udp' }), PORT: Object.freeze({ s1194: '1194', @@ -271,7 +271,7 @@ const WIZARD_PARAMS_TEMPLATE = Object.freeze({ role: OPENVPN.ROLE.SERVER, // Target configuration profile role mapping (server or client) port: OPENVPN.PORT.n1194, // Internal numeric system port assignment for the local process listener portExtern: OPENVPN.PORT.n1194, // External port offset mapping used for firewall rule deployments - proto: OPENVPN.PROTO.UDP, // Transport layer connection protocol string selection (udp or tcp) + proto: OPENVPN.PROTO.udp, // Transport layer connection protocol string selection (udp or tcp) displayName: '', // Localized custom alias name assigned to distinguish the specific tunnel strategy: OPENVPN.STRATEGY.STANDARD, // Network traffic management profile (standard, redirect, or sitetosite) remoteServer: '', // Client role parameter: Target remote server public IP or domain string @@ -1147,10 +1147,7 @@ const importClientInstance = async function (elements, wizardData, nextBtn, prev } try { - const extractedParams = await wizardData.importOvpnClientProfileCallback( - activeSubParams.importedRawOvpn, - targetInstanceId - ); + const extractedParams = await wizardData.importOvpnClientProfileCallback(activeSubParams.importedRawOvpn, targetInstanceId, wizardData.appData); // Path A: Update security keys only if (extractedParams.isCryptoUpdateOnly === true) { @@ -1647,7 +1644,7 @@ const renderOptionSiteClient = function (buttons, messageBox) { E('span', {}, [ ICON.ARROW + ' ' + TXT.INFO.server_address + ': ', E('code', { 'style': 'font-weight:bold; color:var(--primary-color-high, #1976d2);' }, - remoteServerMatch[1] + ':' + remoteServerMatch[2] + ' (' + (protoMatch ? protoMatch[1] : OPENVPN.PROTO.UDP) + ')' + remoteServerMatch[1] + ':' + remoteServerMatch[2] + ' (' + (protoMatch ? protoMatch[1] : OPENVPN.PROTO.udp) + ')' ) ]) ]); @@ -1975,8 +1972,8 @@ const createWizardInputElements = function (buttons, wizardData, selectedScenari radioSiteClient.disabled = true; } - const radioUdp = E('input', { 'type': 'radio', 'name': 'wizard_proto_group', 'value': OPENVPN.PROTO.UDP, 'style': 'width:18px; height:18px; margin:0; cursor:pointer;' }); - const radioTcp = E('input', { 'type': 'radio', 'name': 'wizard_proto_group', 'value': OPENVPN.PROTO.TCP, 'style': 'width:18px; height:18px; margin:0; cursor:pointer;' }); + const radioUdp = E('input', { 'type': 'radio', 'name': 'wizard_proto_group', 'value': OPENVPN.PROTO.udp, 'style': 'width:18px; height:18px; margin:0; cursor:pointer;' }); + const radioTcp = E('input', { 'type': 'radio', 'name': 'wizard_proto_group', 'value': OPENVPN.PROTO.tcp, 'style': 'width:18px; height:18px; margin:0; cursor:pointer;' }); const displayNameInput = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'placeholder': TXT.MSG.placeholder_myhomevpn, 'style': 'width:100%;' }); const ddnsInput = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'placeholder': TXT.MSG.placeholder_mydomain_publicip, 'style': 'width:100%; font-weight:bold; color:var(--primary-color-high, #1976d2);' }); const portInput = E('input', { 'type': 'text', 'class': 'cbi-input-text', 'style': 'font-family:var(--font-monospace, monospace)' }); @@ -2060,7 +2057,7 @@ const createWizardNavigationButtons = function () { const updateDefaultWizardPorts = async function (protoValue, portForwardingAlert, portInput, externPortInput, scenario, wizardData) { let defaultPort; let defaultPortExtern; - const isTcp = (protoValue === OPENVPN.PROTO.TCP); + const isTcp = (protoValue === OPENVPN.PROTO.tcp); if (isTcp) { const ports = getPortsTcp(wizardData); defaultPort = ports.port @@ -2142,7 +2139,7 @@ const syncScenarioSubNodeVisibility = function (elements) { * Find out if UDP or TCP is selected */ const getActiveProtoValue = function (elements) { - const protoValue = elements.radios.tcp.checked ? OPENVPN.PROTO.TCP : OPENVPN.PROTO.UDP; + const protoValue = elements.radios.tcp.checked ? OPENVPN.PROTO.tcp : OPENVPN.PROTO.udp; return protoValue; }; @@ -2299,7 +2296,7 @@ const getPortsTcp = function (wizardData) { // 1. Just count how many TCP instances are currently registered in total for (let i = 0; i < instances.length; i++) { - if (instances[i] && instances[i].proto === OPENVPN.PROTO.TCP) { + if (instances[i] && instances[i].proto === OPENVPN.PROTO.tcp) { tcpInstanceCount++; } } diff --git a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn.js b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn.js index 9cfea7daf..2744d9d94 100755 --- a/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn.js +++ b/luci-app-openvpn-plus/root/www/luci-static/resources/view/vpn/openvpn.js @@ -240,15 +240,15 @@ const CFG = Object.freeze({ instance: 'instance', openvpn_logread: 'openvpn', openvpn_log_stamp: 'openvpn_log_stamp', - fwPort: 'fwPort', - fwProto: 'fwProto', fw_openvpn_rule: 'openvpn_rule_', fw_openvpn_inc: 'openvpn_inc_', + fw_openvpn_jmp_inc: 'openvpn_jmp_inc_', fw_openvpn_s2s_inc: 'openvpn_s2s_inc_', fw_openvpn_zone: 'openvpn_zone_', fw_openvpn_fwd_lan: 'openvpn_fwd_lan_', fw_openvpn_fwd_vpn: 'openvpn_fwd_vpn_', fw_openvpn_server: 'openvpn_server_', + fw_openvpn_floodguard: 'openvpn_floodguard_' }), CONF: Object.freeze({ modern_vpn_client: '# Modern OpenVPN Client Configuration Instance', @@ -314,8 +314,8 @@ const OPENVPN = Object.freeze({ SITETOSITE: 'sitetosite' }), PROTO: Object.freeze({ - UDP: 'udp', - TCP: 'tcp' + udp: 'udp', + tcp: 'tcp' }), PORT: Object.freeze({ s1194: '1194', @@ -400,9 +400,15 @@ const nativeFileExecRpc = L.rpc.declare({ const luci_app_openvpn_plus = async function (args, skipTrim) { const extraArgs = (typeof args === 'string') ? [args] : (Array.isArray(args) ? args : []); const res = await L.resolveDefault(nativeFileExecRpc(CFG.LIBEXEC.luci_app_openvpn_plus, extraArgs), {}); + + // CHECK EXIT CODE: If the backend returned anything else than 0, it failed! + if (res && typeof res === 'object' && res.code !== undefined && res.code !== 0) { + return 'ERROR: CODE_' + res.code; + } if (res && typeof res === 'object' && typeof res.stdout === 'string') { if (skipTrim === true) { - return res.stdout.trim(); + // Remove ONLY the very last trailing newline character cleanly + return res.stdout.replace(/\r?\n$/, ''); } else { return res.stdout.trim(); } @@ -474,15 +480,11 @@ const secureRemoveFile = async function (filepath) { */ const secureStatFile = async function (filepath) { const res = await luci_app_openvpn_plus([CFG.LIBEXEC.statfile, filepath]); - try { - if (res && res.startsWith("{")) { - const statObj = JSON.parse(res); - if (statObj && typeof statObj === 'object') { - return statObj; - } + if (res && res.startsWith("{")) { + const statObj = JSON.parse(res); + if (statObj && typeof statObj === 'object') { + return statObj; } - } catch (err) { - console.error("Failed to parse file statistics: ", CFG.LIBEXEC.luci_app_openvpn_plus + ' ' + CFG.LIBEXEC.statfile + ' ' + filepath + ' -> ' + err); } return null; }; @@ -517,12 +519,23 @@ const isAnyInstanceEnabled = function (ovpnUciSections) { */ const sha256sum = async function (textToHash) { const res = await luci_app_openvpn_plus([CFG.LIBEXEC.openssldgst, '-sha256', String(textToHash || '')]); - if (res.length > 0) { - const parts = res.trim().split(/\s+/); - return (parts && parts[0]) ? parts[0] : ''; - } else { - return ''; + + // 1. Check if the response is empty or contains a backend error code + if (!res || res.length === 0 || res.startsWith('ERROR:')) { + console.error("SHA256 calculation failed because the backend returned an error."); + return 'ERROR'; } + + // 2. Parse the verified output from openSSL + const parts = res.trim().split(/\s+/); + const potentialHash = (parts && parts[0]) ? parts[0] : ''; + + // 3. Extra security check: A real SHA256 string must be exactly 64 characters long + if (potentialHash.length === 64) { + return potentialHash; + } + + return 'ERROR'; }; /** @@ -570,73 +583,102 @@ const netCache = { }; /** - * Get LAN IP with local cache + * Get LAN IP with local cache and secure error handling */ const getLanIp = async function (force) { const now = new Date().getTime(); + + // 1. Return cached value if it is still fresh and valid if (force !== true && netCache.LanIp !== null && Math.abs(now - netCache.LanIpTime) < netCache.TIMEOUT) { return netCache.LanIp; } + + // 2. Query the backend for the current LAN IP const lanIp = await luci_app_openvpn_plus([CFG.LIBEXEC.lanip]); - if (lanIp.length > 0) { - netCache.LanIp = lanIp; - netCache.LanIpTime = new Date().getTime(); - return lanIp; + + // 3. Catch errors or empty strings from the backend immediately + if (!lanIp || lanIp.length === 0 || lanIp.startsWith('ERROR')) { + return getHostnameFallback(false); } - return getHostnameFallback(false); + + // 4. If the IP is valid, save it to the cache and return it + netCache.LanIp = lanIp; + netCache.LanIpTime = new Date().getTime(); + return lanIp; }; + /** - * Get LAN IP and Mask with local cache + * Get LAN IP and Mask with local cache and secure error handling */ const getLanIpMask = async function (force) { const now = new Date().getTime(); + + // 1. Return cached values if they are still fresh and valid if (force !== true && netCache.LanIpMask !== null && Math.abs(now - netCache.LanIpMaskTime) < netCache.TIMEOUT) { return netCache.LanIpMask; } + + // 2. Query the backend for the current IP and CIDR mask const res = await luci_app_openvpn_plus([CFG.LIBEXEC.lanip, CFG.CMD.cidr]); - if (res.length > 0) { - const parts = res.split('/'); - if (parts.length === 2) { - const inputIp = parts[0]; - let inputMask = parts[1]; - // Only run the bit-shifting calculation if it is a standard IPv4 address - if (inputIp.indexOf('.') !== -1) { - const inlinePrefix = parseInt(parts[1], 10) || 24; - const maskArray = [0, 0, 0, 0]; - - // Rebuild explicit netmask format string from the prefix bits - for (let bit4 = 0; bit4 < 32; bit4++) { - if (bit4 < inlinePrefix) { - maskArray[Math.floor(bit4 / 8)] |= (1 << (7 - (bit4 % 8))); - } - } - inputMask = maskArray.join('.'); - } - netCache.LanIpMask = { ip: inputIp, mask: inputMask }; - netCache.LanIpMaskTime = new Date().getTime(); - return netCache.LanIpMask; - } + // 3. Catch errors or empty strings from the backend immediately + if (!res || res.length === 0 || res.startsWith('ERROR')) { + // Secure fallback without breaking the calculation + return getHostnameFallback(true); } + + // 4. Process the verified backend output + const parts = res.split('/'); + if (parts.length === 2) { + const inputIp = parts[0]; + let inputMask = parts[1]; + + // Only run the bit-shifting calculation if it is a standard IPv4 address + if (inputIp.indexOf('.') !== -1) { + const inlinePrefix = parseInt(parts[1], 10) || 24; + const maskArray = [0, 0, 0, 0]; + + // Rebuild explicit netmask format string from the prefix bits + for (let bit4 = 0; bit4 < 32; bit4++) { + if (bit4 < inlinePrefix) { + maskArray[Math.floor(bit4 / 8)] |= (1 << (7 - (bit4 % 8))); + } + } + inputMask = maskArray.join('.'); + } + netCache.LanIpMask = { ip: inputIp, mask: inputMask }; + netCache.LanIpMaskTime = new Date().getTime(); + return netCache.LanIpMask; + } + return getHostnameFallback(true); }; /** - * Get public WAN IP with local cache. + * Get public WAN IP with local cache and secure error handling */ const queryPublicIp = async function (force) { const currentHost = window.location.hostname; const currentTime = Date.now(); + + // 1. Return cached value if it is still fresh and valid if (force !== true && netCache.PublicIp !== null && Math.abs(currentTime - netCache.PublicIpTime) < netCache.TIMEOUT) { return netCache.PublicIp; } + + // 2. Query the backend for the public IP const cleanIp = await luci_app_openvpn_plus([CFG.LIBEXEC.publicip]); - if (cleanIp.length > 0) { - netCache.PublicIp = cleanIp; - netCache.PublicIpTime = currentTime; - return cleanIp; + + // 3. Catch errors or empty strings from the backend immediately + if (!cleanIp || cleanIp.length === 0 || cleanIp.startsWith('ERROR')) { + // Secure fallback without corrupting the global cache + return currentHost; } - return currentHost; + + // 4. If the IP is valid, save it to the cache and return it + netCache.PublicIp = cleanIp; + netCache.PublicIpTime = currentTime; + return cleanIp; }; /** @@ -675,12 +717,14 @@ const getDdnsOrPublicIp = async function (instObj) { // 2. Resolve the domain we found or run the public IP fallback if (foundDomain) { const resolvedIp = await luci_app_openvpn_plus([CFG.LIBEXEC.checkddns, foundDomain]); - if (resolvedIp.length > 0) { + + // Catch errors or empty strings from the DDNS backend resolver immediately + if (resolvedIp && resolvedIp.length > 0 && !resolvedIp.startsWith('ERROR')) { return resolvedIp; } } - // 3. fallback if domain fails or is completely empty + // 3. Fallback if domain fails, is completely empty, or returned a backend error return await queryPublicIp(false); }; @@ -692,10 +736,12 @@ const cleanIpOrDomain = async function (rawString) { return ''; } const cleaned = await luci_app_openvpn_plus([CFG.LIBEXEC.cleanipdns, rawString]); - if (cleaned.length > 0) { - return cleaned; + + // Catch backend errors immediately and return the uncleaned rawString as fallback + if (!cleaned || cleaned.length === 0 || cleaned.startsWith('ERROR')) { + return rawString; } - return rawString; + return cleaned; }; /** @@ -705,17 +751,22 @@ const checkDdns = async function (targetHost) { if (!targetHost) { return { success: false, response: '' }; } - try { - const res = await luci_app_openvpn_plus([CFG.LIBEXEC.checkddns, targetHost]); - if (res.length > 0) { - return { - success: true, - response: res - }; - } - } catch { - return { success: false, response: '' }; + + const res = await luci_app_openvpn_plus([CFG.LIBEXEC.checkddns, targetHost]); + + // 1. Catch errors or empty strings from the backend immediately + if (!res || res.length === 0 || res.startsWith('ERROR')) { + return { + success: false, + response: res || 'ERROR' + }; } + + // 2. If the response is valid and online, return success + return { + success: true, + response: res + }; }; /** @@ -725,11 +776,18 @@ const checkPort = async function (targetHost, externalPort, internalPort, protoc if (!targetHost || !externalPort || !internalPort || !protocol) { return false; } + const protoStr = protocol.toLowerCase(); const res = await luci_app_openvpn_plus([CFG.LIBEXEC.checkport, targetHost, externalPort, internalPort, protoStr]); + if (res && res.startsWith('ERROR')) { + return false; + } + + // Verify if the backend tool officially confirmed the port status if (res === 'PORT_OPEN') { return true; } + return false; }; @@ -740,10 +798,17 @@ const checkStaticRoute = async function (wanIp, remoteNet) { if (!wanIp || !remoteNet) { return false; } + const res = await luci_app_openvpn_plus([CFG.LIBEXEC.checksroute, wanIp, remoteNet]); + if (res && res.startsWith('ERROR')) { + return false; + } + + // Verify if the backend tool officially confirmed the route status if (res === 'ROUTE_OPEN') { return true; } + return false; }; @@ -755,18 +820,28 @@ const updateDdnsProvider = async function (updateUrl, domain) { return null; } const responseText = await luci_app_openvpn_plus([CFG.LIBEXEC.wgetddns, updateUrl, domain]); - if ((responseText.length > 0) && (responseText.toLowerCase().indexOf('success') !== -1)) { + if (!responseText || responseText.length === 0 || responseText.startsWith('ERROR')) { + return { + raw: responseText || 'ERROR', + isError: true + }; + } + + // Check if the response from the provider explicitly contains the 'success' keyword + if (responseText.toLowerCase().indexOf('success') !== -1) { return { raw: responseText, isError: false }; } + return { raw: responseText, isError: true }; }; + /** * checkNetworkStructure cached data storage */ @@ -1008,18 +1083,20 @@ const getServerIpv6SubnetFromInstNum = function (instNum) { * Executes a hardware cpu check to determine and return if the optimal cipher is AES */ const checkOptimalDataCipherAES = async function () { - const res = await luci_app_openvpn_plus([CFG.LIBEXEC.bestcrypto]); - if (res.length > 0) { - if (res === 'AES') { - return true - } else { - return false; - } + if (!res || res.length === 0 || res.startsWith('ERROR')) { + console.warn("Hardware crypto check failed. Backend returned an error. Falling back to default AES cipher."); + return true; + } + + if (res === 'AES') { + return true; + } else { + return false; } - return true; }; + /** * Compresses and formats the configuration text. Optionally removes dynamic blocks defined by start and end identifiers. */ @@ -1100,7 +1177,7 @@ const generateServerConfig = async function (appData, newInstanceItem, wizardPar const instNum = newInstanceItem.instNum; const chosenPort = (wizardParams && wizardParams.port) ? wizardParams.port : newInstanceItem.port; - const chosenProto = (wizardParams && wizardParams.proto) ? wizardParams.proto : OPENVPN.PROTO.UDP; + const chosenProto = (wizardParams && wizardParams.proto) ? wizardParams.proto : OPENVPN.PROTO.udp; const displayName = (wizardParams && wizardParams.displayName) ? wizardParams.displayName.trim() : ''; // Calculate the external client port target safely @@ -1142,7 +1219,7 @@ const generateServerConfig = async function (appData, newInstanceItem, wizardPar } let blocksToRemove = []; - if (chosenProto === OPENVPN.PROTO.UDP) { + if (chosenProto === OPENVPN.PROTO.udp) { // remove tls_timeout_5 if runing on UDP config = config.replace(CFG.CONF.tls_timeout_5, ''); } else { @@ -1288,7 +1365,7 @@ const generateServerConfig = async function (appData, newInstanceItem, wizardPar * Finds the last active server ID, its configuration port number, and its protocol (udp/tcp) */ const getLastServerIdAndPort = function (currentId, defaultPort, appData) { - const result = { id: currentId, port: defaultPort, proto: OPENVPN.PROTO.UDP }; + const result = { id: currentId, port: defaultPort, proto: OPENVPN.PROTO.udp }; if (!appData || !Array.isArray(appData.instances)) { return result; @@ -1369,7 +1446,7 @@ const generateClientConfig = async function (appData, newInstanceItem, wizardPar config = config.replace(CFG.CONF.data_ciphers_aes, CFG.CONF.data_ciphers_comment + "\n" + CFG.CONF.data_ciphers_chacha); } - if (chosenProto === OPENVPN.PROTO.TCP) { + if (chosenProto === OPENVPN.PROTO.tcp) { // remove explicit-exit-notify 1, mssfix 1360 and tun-mtu 1420 if running on TCP config = config.replace(CFG.CONF.explicit_exit_notify, ''); config = config.replace(CFG.CONF.mssfix_1360, ''); @@ -1422,14 +1499,18 @@ const compileOvpnProfileText = async function (cname, targetHost, targetPort, pr ovpn = ovpn.replace(CFG.CONF.data_ciphers_aes, CFG.CONF.data_ciphers_comment + '\n' + CFG.CONF.data_ciphers_chacha); } - if (proto === OPENVPN.PROTO.TCP) { + if (proto === OPENVPN.PROTO.tcp) { ovpn = ovpn.replace(CFG.CONF.mssfix_1360, ''); ovpn = ovpn.replace(CFG.CONF.tun_mtu_1420, ''); } // Collapse three or more consecutive newlines down to exactly one empty line ovpn = ovpn.replace(/\n{3,}/g, '\n\n'); - ovpn += 'setenv client-cname ' + cname + '\n\n'; + if (!ovpn.endsWith('\n')) { + ovpn += '\n'; + } + + ovpn += '\n' + 'setenv client-cname ' + cname + '\n\n'; if (cryptoAssets.ca && typeof cryptoAssets.ca.trim === 'function') { const rawCa = cryptoAssets.ca.trim(); @@ -1495,9 +1576,6 @@ const syncInstanceFiles = async function (newInstanceItem, appData, wizardParams filePromises.push( initFile(CFG.FILE.dir_keys + 'dh_' + id + '.pem', CFG.FILE.dir_keys + CFG.FILE.dh_def_pem, newInstanceItem, appData, wizardParams) ); - filePromises.push( - initFile(CFG.FILE.dir_cfg + id + '.nft', CFG.FILE.dir_cfg + CFG.FILE.default_nft, newInstanceItem, appData, wizardParams) - ); } // We wait for all parallel file transmissions @@ -1647,21 +1725,17 @@ const ovpnServiceStartStop = { const targetAction = this.Action; const targetInstance = this.Instance; this.Instance = ''; + + // 1. Trigger the background service action safely without blocking the UI thread setTimeout(async function () { - try { - await luci_app_openvpn_plus([CFG.LIBEXEC.ovpnservice, targetAction, targetInstance]); - } catch { - console.error('Asynchronous background OpenVPN service call failed: ' + CFG.LIBEXEC.luci_app_openvpn_plus + ' ' + CFG.LIBEXEC.ovpnservice + ' ' + targetAction + ' ' + targetInstance); - } + await luci_app_openvpn_plus([CFG.LIBEXEC.ovpnservice, targetAction, targetInstance]); }, 0); + + // 2. Trigger the UCI sorting routine if requested if (this.uciSort === true) { this.uciSort = false; setTimeout(async function () { - try { - await luci_app_openvpn_plus([CFG.LIBEXEC.ucisortovpn]); - } catch { - console.error('Asynchronous background call "uci reorder openvpn.instanceX=Y" failed: ' + CFG.LIBEXEC.luci_app_openvpn_plus + ' ' + CFG.LIBEXEC.ucisortovpn); - } + await luci_app_openvpn_plus([CFG.LIBEXEC.ucisortovpn]); }, 3000); } } @@ -1806,7 +1880,7 @@ const uciSaveApplyRestart = async function (instance_id) { /** * Reads a profile file, tests keys with keymeta, and saves them to the router. */ -const importOvpnClientProfile = async function (ovpnContent, instanceId) { +const importOvpnClientProfile = async function (ovpnContent, instanceId, appData) { if (!ovpnContent || !ovpnContent.trim()) { throw new Error(ICON.ERROR + TXT.ERROR.upload_key_empty); } @@ -1871,13 +1945,12 @@ const importOvpnClientProfile = async function (ovpnContent, instanceId) { await secureWriteFile(CFG.FILE.dir_keys + tmpFile, currentBlock + '\n'); // Run keymeta to check if this key is good or bad - const res = await luci_app_openvpn_plus([CFG.LIBEXEC.keymeta, tmpFile]); + const keyInfo = await appData.keygenClass.getKeyMeta(tmpFile, luci_app_openvpn_plus); await secureRemoveFile(CFG.FILE.dir_keys + tmpFile); - const metaReport = res.toUpperCase(); - // Stop instantly if keymeta finds an error or trash text inside the file - if (metaReport.length == 0 || metaReport.indexOf('ERROR') !== -1) { - throw new Error(ICON.ERROR + TXT.ERROR.key_check_failed); + // Catch backend errors or empty responses immediately before parsing + if (keyInfo.valid === false) { + throw new Error(ICON.ERROR + ' ' + TXT.ERROR.key_check_failed); } // SORTING: Place keys into correct variables using their real crypto type from keymeta @@ -1888,18 +1961,24 @@ const importOvpnClientProfile = async function (ovpnContent, instanceId) { else if (currentItem.type === 'key') { keyContent = currentBlock; } else if (currentItem.type === 'tlscrypt-v2') { tlsCryptContent = currentBlock; } } else { - // Sorting path for pure keys.crt files based on actual crypto traits - if (metaReport.indexOf('AUTHORITY') !== -1 || metaReport.indexOf('CA:TRUE') !== -1) { - caContent = currentBlock; - } - else if (metaReport.indexOf('STANDARD CERTIFICATE') !== -1 || metaReport.indexOf('PUBLIC-KEY') !== -1) { - certContent = currentBlock; - } - else if ((metaReport.indexOf('PRIVATE-KEY') !== -1) || metaReport.indexOf('Private-Key') !== -1) { - keyContent = currentBlock; - } - else if (metaReport.indexOf('TLSv2-Client-Key') !== -1) { - tlsCryptContent = currentBlock; + + let activeType = keyInfo.type; + + // 3. CLEAN INTELLIGENT SORTING (Using our type Enum constants) + switch (activeType) { + case appData.keygenClass.CRYPTO_TYPE.CA_TRUE: + caContent = currentBlock; + break; + case appData.keygenClass.CRYPTO_TYPE.STANDARD_CERTIFICATE: + case appData.keygenClass.CRYPTO_TYPE.PUBLIC_KEY: + certContent = currentBlock; + break; + case appData.keygenClass.CRYPTO_TYPE.PRIVATE_KEY: + keyContent = currentBlock; + break; + case appData.keygenClass.CRYPTO_TYPE.TLS2_CLIENT_KEY: + tlsCryptContent = currentBlock; + break; } } } @@ -1951,7 +2030,7 @@ const importOvpnClientProfile = async function (ovpnContent, instanceId) { remoteServer: remoteServerMatch[1].trim(), remoteServerLan: remoteServerLanMatch ? remoteServerLanMatch[1].trim() : '', port: parseInt(remoteServerMatch[2], 10) || OPENVPN.PORT.n1194, - proto: protoMatch ? protoMatch[1].trim().toLowerCase() : OPENVPN.PROTO.UDP, + proto: protoMatch ? protoMatch[1].trim().toLowerCase() : OPENVPN.PROTO.udp, cname: cnameMatch ? cnameMatch[1].trim() : '' }; }; @@ -2090,8 +2169,17 @@ const setupQrBoxEvents = function (elements, ovpnParams, cryptoAssets, appData, renderClientOvpnProfileQr(elements.nodes.qr, downloadUrl); } else { console.error('Backend failed to return a valid download link:', backendResponse); + elements.nodes.qr.innerHTML = ''; + elements.nodes.qr.appendChild( + E('div', { 'style': 'color:var(--error-color-high, #f62b12); font-size:12px; font-weight:bold; padding:20px;' }, [ + ICON.ERROR + ' ' + _('Failed to generate secure download profile. Please check system logs.') + ]) + ); + elements.nodes.url.href = '#'; + elements.nodes.url.textContent = _('Profile Generation Error'); } + } catch (e) { console.error('Failed to process and link profile export assets:', e); } @@ -2879,7 +2967,7 @@ const downloadClientOvpnProfile = async function (instance_id, instObj, customUc const isSiteToSite = appData.statusClass.parseSiteToSite(serverAssets.conf); // Read connection parameters instantly from your clean instObj RAM cache! - const serverProto = instObj.proto || OPENVPN.PROTO.UDP; + const serverProto = instObj.proto || OPENVPN.PROTO.udp; const internalPort = instObj.port || OPENVPN.PORT.s1194; // Read the custom client port range directly from the template schema @@ -3542,21 +3630,6 @@ const renderKeyButtons = function (label, filename, instance_id, displayId, defa ]); }; -/** - * Update port and proto in the flood guard rules file 'instance*.nft' - */ -const updatefloodGuardFile = async function (instance_id, port, proto) { - const nftFilePath = CFG.FILE.dir_cfg + instance_id + '.nft'; - const nftFileRaw = await secureReadFile(nftFilePath); - let nftFile = String(nftFileRaw || '').trim(); - if (nftFile.length > 0) { - nftFile = nftFile.replace(new RegExp(CFG.ID.instance + '0', 'g'), instance_id); - nftFile = nftFile.replace(new RegExp(CFG.ID.fwPort + '\\s*=\\s*\\d+', 'g'), CFG.ID.fwPort + '=' + port); - nftFile = nftFile.replace(new RegExp(CFG.ID.fwProto + '\\s*=\\s*"[^"]*"', 'g'), CFG.ID.fwProto + '="' + proto + '"'); - } - await secureWriteFile(nftFilePath, nftFile); -} - /** * Saves the modified configuration text and handles the instance restart logic */ @@ -3589,9 +3662,9 @@ const handleInstanceSave = async function (instance_id, instObj, role, txtArea, currentPort = appData.statusClass.calcPortFromId(instance_id); } const newPort = appData.statusClass.parsePortFromConfig(role, newConfigContent); - let newProto = appData.statusClass.parseProtoFromConfig(newConfigContent); - const detectedProto = appData.statusClass.parseProtoFromConfig(newConfigContent); + const currentProto = appData.statusClass.parseProtoFromConfig(originalConfContent); + const newProto = appData.statusClass.parseProtoFromConfig(newConfigContent); if (role === OPENVPN.ROLE.SERVER) { @@ -3621,15 +3694,15 @@ const handleInstanceSave = async function (instance_id, instObj, role, txtArea, if (instance && instance.id === instance_id) { instance.confContent = newConfigContent; instance.port = newPort || currentPort; + instance.proto = newProto || currentProto; + instance.serverSiteToSite = serverSiteToSite; + instance.floodGuard = floodGuardEnabled; break; } } } - if (role === OPENVPN.ROLE.SERVER) { - await updatefloodGuardFile(instance_id, newPort, newProto); - } - await syncInstanceFirewallRule(instance_id, instObj, role, newPort, detectedProto, serverSiteToSite, floodGuardEnabled, appData, newConfigContent); + await syncInstanceFirewallRule(instance_id, instObj, role, newPort, newProto, serverSiteToSite, floodGuardEnabled, appData, newConfigContent); // Trigger changes system banner and restart workflow if needed if (isCurrentlyEnabled) { @@ -3946,7 +4019,7 @@ const renderPortAlertBox = function (instObj, role, appData) { runBackgroundPortCheckExtern.callback = showAlertBoxCallback; initialRefreshQueue.push(runBackgroundPortCheckExtern); await portForwardingAlert.check( - instObj.proto || OPENVPN.PROTO.UDP, + instObj.proto || OPENVPN.PROTO.udp, role, instObj.port || OPENVPN.PORT.s1194, instObj.portExtern || OPENVPN.PORT.s1194, @@ -4170,8 +4243,6 @@ const createInstance = async function (newInstanceItem, appData, wizardParams) { // check if 'setenv floodguard on' floodGuardEnabled = appData.statusClass.parseFloodGuard(newInstanceItem.confContent); - await updatefloodGuardFile(newInstanceItem.id, newInstanceItem.port, newInstanceItem.proto); - } // create firewall rules of new isntance @@ -4345,6 +4416,115 @@ const reloadFirewall = { } }; +/** + * Update port, proto and clean unused rules block from 'instance*.nft' + */ +const generate_nft_file = async function (proto, port, instance_id, nftScriptPath) { + const nftDefaultPath = CFG.FILE.dir_cfg + CFG.FILE.default_nft; + const nftDefaultRaw = await secureReadFile(nftDefaultPath); + let nftNew = nftDefaultRaw ? String(nftDefaultRaw) : ''; + + // If the template file is empty or does not exist, it is not valid + if (nftNew.length === 0) { + return { changed: false, valid: false }; + } + + // 1. Create the new file content from the template (remove TCP or UDP block) + if (proto === OPENVPN.PROTO.tcp) { + nftNew = nftNew.replace(/# === START OF UDP BLOCK ===[\s\S]*?# === END OF UDP BLOCK ===/g, ''); + } else if (proto === OPENVPN.PROTO.udp) { + nftNew = nftNew.replace(/# === START OF TCP BLOCK ===[\s\S]*?# === END OF TCP BLOCK ===/g, ''); + } + + // Change the instance ID (e.g: change instance0 to instance1) + nftNew = nftNew.replace(new RegExp(CFG.ID.instance + '0', 'g'), instance_id); + + // Change '$ovpn_port' to the real port number (e.g: 444) + nftNew = nftNew.replace(/\$ovpn_port/g, port); + + // 2. Read the existing file + const nftFilePath = CFG.FILE.dir_cfg + instance_id + '.nft'; + const nftFileRaw = await secureReadFile(nftFilePath); + const nftFile = String(nftFileRaw) || ''; + + // 3. Only save the file if the new content is different from the old content + if (nftFile !== nftNew) { + await secureWriteFile(nftFilePath, nftNew); + return { changed: true, valid: true }; + } + + // If the file is already correct and no changes are needed + return { changed: false, valid: true }; +} + + +/** + * Generate Flood Guard jump rule file for input chain + */ +const generate_jmp_nft_file = async function (proto, port, instance_id, jmpScriptPath) { + let jmpNftContent = '#!/usr/sbin/nft -f\n'; + jmpNftContent += '# Automatically generated by luci-app-openvpn-plus - OpenVPN Flood Guard firewall input rule\n'; + + if (proto === OPENVPN.PROTO.tcp) { + jmpNftContent += 'tcp dport ' + String(port) + ' jump ' + CFG.ID.fw_openvpn_floodguard + instance_id + ' comment "luci-app-openvpn-plus: flood guard filter rules for ' + instance_id + '";\n;\n'; + } else { + jmpNftContent += 'udp dport ' + String(port) + ' jump ' + CFG.ID.fw_openvpn_floodguard + instance_id + ' comment "luci-app-openvpn-plus: flood guard filter rules for ' + instance_id + '";\n;\n'; + } + + // Try to read the old file content to see if the script actually changed + let oldScriptContent = ''; + try { + oldScriptContent = await secureReadFile(jmpScriptPath); + } catch { + oldScriptContent = ''; + } + + // Only write the file to the flash storage if the content is different + if (String(oldScriptContent).trim() !== jmpNftContent.trim()) { + await secureWriteFile(jmpScriptPath, jmpNftContent); + // if changed return true + return true; + } else { + return false; + } +} + +/** + * Generate static rules file for Site-To-Site outbound and inbound forward + */ +const generate_s2s_nft_file = async function (deviceName, localSubnet, remoteSubnets, instance_id, s2sScriptPath) { + let s2sNftContent = '#!/usr/sbin/nft -f\n'; + s2sNftContent += '# Automatically generated by luci-app-openvpn-plus - OpenVPN Site-to-Site Firewall Rules\n'; + + s2sNftContent += 'iifname "' + deviceName + '" ip daddr ' + localSubnet + ' accept comment "luci-app-openvpn-plus: static Site-To-Site outbound forward for ' + instance_id + '";\n'; + + for (let i = 0; i < remoteSubnets.length; i++) { + const remoteSubnet = remoteSubnets[i]; + if (!remoteSubnet) { + continue; + } + s2sNftContent += 'iifname "br-lan" ip daddr ' + remoteSubnet + ' accept comment "luci-app-openvpn-plus: Static Site-To-Site inbound forward for ' + instance_id + '";\n'; + } + s2sNftContent += ';\n'; + + // Try to read the old file content to see if the script actually changed + let oldScriptContent = ''; + try { + oldScriptContent = await secureReadFile(s2sScriptPath); + } catch { + oldScriptContent = ''; + } + + // Only write the file to the flash storage if the content is different + if (String(oldScriptContent).trim() !== s2sNftContent.trim()) { + await secureWriteFile(s2sScriptPath, s2sNftContent); + // if changed return true + return true; + } else { + return false; + } +} + /** * Configures firewall zones, forwardings, and static site-to-site forwarding loops without wiping sections */ @@ -4353,6 +4533,7 @@ const syncInstanceFirewallRule = async function (instance_id, instObj, role, cus // Inbound WAN rule blocks (Server only) const fwRuleSection = CFG.ID.fw_openvpn_rule + instance_id; const fwIncludeSection = CFG.ID.fw_openvpn_inc + instance_id; + const fwJumpSection = CFG.ID.fw_openvpn_jmp_inc + instance_id; // Dedicated block for the custom static site-to-site script injection const fwS2SIncludeSection = CFG.ID.fw_openvpn_s2s_inc + instance_id; @@ -4395,6 +4576,10 @@ const syncInstanceFirewallRule = async function (instance_id, instObj, role, cus try { const deviceName = instObj.devName; const zoneName = 'vpn_zone_' + instance_id; + const nftScriptPath = CFG.FILE.dir_cfg + instance_id + '.nft'; + const jmpScriptPath = CFG.FILE.dir_cfg + instance_id + '.jmp.nft'; + const s2sScriptPath = CFG.FILE.dir_cfg + instance_id + '.s2s.nft'; + let floodGuardValid = false; // --- SECTION 1: SERVER SPECIFIC INBOUND WAN ROLES --- if (role === OPENVPN.ROLE.SERVER) { @@ -4404,8 +4589,8 @@ const syncInstanceFirewallRule = async function (instance_id, instObj, role, cus } let targetProto = customProto; - if (!targetProto || (targetProto !== OPENVPN.PROTO.UDP && targetProto !== OPENVPN.PROTO.TCP)) { - targetProto = OPENVPN.PROTO.UDP; + if (!targetProto || (targetProto !== OPENVPN.PROTO.udp && targetProto !== OPENVPN.PROTO.tcp)) { + targetProto = OPENVPN.PROTO.udp; } // Ensure the inbound WAN port rule exists and matches target metrics @@ -4415,21 +4600,52 @@ const syncInstanceFirewallRule = async function (instance_id, instObj, role, cus safeUciSet(CFG.CMD.firewall, fwRuleSection, 'dest_port', String(targetPort)); safeUciSet(CFG.CMD.firewall, fwRuleSection, 'proto', targetProto); safeUciSet(CFG.CMD.firewall, fwRuleSection, 'target', 'ACCEPT'); + safeUciSet(CFG.CMD.firewall, fwRuleSection, 'enabled', '1'); // Manage the custom flood guard script include block if (floodGuardEnabled === true) { - safeUciAdd(CFG.CMD.firewall, 'include', fwIncludeSection); - safeUciSet(CFG.CMD.firewall, fwIncludeSection, 'type', 'script'); - safeUciSet(CFG.CMD.firewall, fwIncludeSection, 'fw4_compatible', '1'); - safeUciSet(CFG.CMD.firewall, fwIncludeSection, 'enabled', '1'); - safeUciSet(CFG.CMD.firewall, fwIncludeSection, 'path', CFG.FILE.dir_cfg + instance_id + '.nft'); - } else { - safeUciRemove(CFG.CMD.firewall, fwIncludeSection); + + // create/check instance*.nft file + const result = await generate_nft_file(targetProto, targetPort, instance_id, nftScriptPath); + if (result.valid) { + floodGuardValid = true; + if (result.changed) { + hasChanges = true; + } + + // Set instance*.nft include (floodGuardEnabled rules file) + safeUciAdd(CFG.CMD.firewall, 'include', fwIncludeSection); + safeUciSet(CFG.CMD.firewall, fwIncludeSection, 'type', 'nftables'); + safeUciSet(CFG.CMD.firewall, fwIncludeSection, 'path', nftScriptPath); + safeUciSet(CFG.CMD.firewall, fwIncludeSection, 'position', 'table-pre'); + safeUciSet(CFG.CMD.firewall, fwIncludeSection, 'enabled', '1'); + + // create/check instance*.jmp.nft file + const new_jmp_file = await generate_jmp_nft_file(targetProto, targetPort, instance_id, jmpScriptPath); + if (new_jmp_file) { + hasChanges = true; + } + + // Set instance*.jmp.nft include (jump to floodGuardEnabled rules) + safeUciAdd(CFG.CMD.firewall, 'include', fwJumpSection); + safeUciSet(CFG.CMD.firewall, fwJumpSection, 'type', 'nftables'); + safeUciSet(CFG.CMD.firewall, fwJumpSection, 'path', jmpScriptPath); + safeUciSet(CFG.CMD.firewall, fwJumpSection, 'position', 'chain-pre'); + safeUciSet(CFG.CMD.firewall, fwJumpSection, 'chain', 'input'); + safeUciSet(CFG.CMD.firewall, fwJumpSection, 'enabled', '1'); + } } } else { // Remove server specific rules if the instance is now a client safeUciRemove(CFG.CMD.firewall, fwRuleSection); + } + + // Remove floodguard rules and files if not valid + if (floodGuardValid === false) { safeUciRemove(CFG.CMD.firewall, fwIncludeSection); + safeUciRemove(CFG.CMD.firewall, fwJumpSection); + secureRemoveFile(nftScriptPath); + secureRemoveFile(jmpScriptPath); } // --- SECTION 2: STATIC SITE-TO-SITE ROUTING --- @@ -4441,43 +4657,18 @@ const syncInstanceFirewallRule = async function (instance_id, instObj, role, cus const localLan = await getLanIpMask(true); const localSubnet = appData.statusClass.netIPCIDR(localLan.ip, localLan.mask); - let s2sNftContent = '#!/bin/sh\n\n'; - s2sNftContent += '# Automatically generated by luci-app-openvpn-plus - OpenVPN Site-to-Site Firewall Rules\n\n'; - - for (let i = 0; i < remoteSubnets.length; i++) { - const remoteSubnet = remoteSubnets[i]; - if (!remoteSubnet) { - continue; - } - s2sNftContent += 'nft "insert rule inet fw4 forward index 0 iifname br-lan ip daddr ' + remoteSubnet + ' counter accept comment \\"luci-app-openvpn-plus: static s2s outbound forward for ' + instance_id + '\\""\n'; - } - - s2sNftContent += '\n'; - s2sNftContent += 'nft "insert rule inet fw4 forward index 0 iifname ' + deviceName + ' ip daddr ' + localSubnet + ' counter accept comment \\"luci-app-openvpn-plus: static s2s inbound forward for ' + instance_id + '\\""\n'; - s2sNftContent += '\n'; - - const s2sScriptPath = CFG.FILE.dir_cfg + instance_id + '.s2s.nft'; - - // Try to read the old file content to see if the script actually changed - let oldScriptContent = ''; - try { - oldScriptContent = await secureReadFile(s2sScriptPath); - } catch { - oldScriptContent = ''; - } - - // Only write the file to the flash storage if the content is different - if (String(oldScriptContent).trim() !== s2sNftContent.trim()) { - await secureWriteFile(s2sScriptPath, s2sNftContent); + const newfile = await generate_s2s_nft_file(deviceName, localSubnet, remoteSubnets, instance_id, s2sScriptPath); + if (newfile) { hasChanges = true; } // Register the script file inside the firewall configuration tree safely safeUciAdd(CFG.CMD.firewall, 'include', fwS2SIncludeSection); - safeUciSet(CFG.CMD.firewall, fwS2SIncludeSection, 'type', 'script'); - safeUciSet(CFG.CMD.firewall, fwS2SIncludeSection, 'fw4_compatible', '1'); - safeUciSet(CFG.CMD.firewall, fwS2SIncludeSection, 'enabled', '1'); + safeUciSet(CFG.CMD.firewall, fwS2SIncludeSection, 'type', 'nftables'); safeUciSet(CFG.CMD.firewall, fwS2SIncludeSection, 'path', s2sScriptPath); + safeUciSet(CFG.CMD.firewall, fwS2SIncludeSection, 'position', 'chain-pre'); + safeUciSet(CFG.CMD.firewall, fwS2SIncludeSection, 'chain', 'forward'); + safeUciSet(CFG.CMD.firewall, fwS2SIncludeSection, 'enabled', '1'); s2sScript = true; } } @@ -4485,6 +4676,7 @@ const syncInstanceFirewallRule = async function (instance_id, instObj, role, cus // Remove the site-to-site block if it was not triggered or has no valid subnets if (s2sScript === false) { safeUciRemove(CFG.CMD.firewall, fwS2SIncludeSection); + secureRemoveFile(s2sScriptPath); } // --- SECTION 3: SYMMETRICAL VPN ZONE & LAN FORWARDING --- @@ -4498,6 +4690,7 @@ const syncInstanceFirewallRule = async function (instance_id, instObj, role, cus safeUciSet(CFG.CMD.firewall, fwZoneSection, 'output', 'ACCEPT'); safeUciSet(CFG.CMD.firewall, fwZoneSection, 'forward', 'ACCEPT'); safeUciSet(CFG.CMD.firewall, fwZoneSection, 'mtu_fix', '1'); + safeUciSet(CFG.CMD.firewall, fwZoneSection, 'enabled', '1'); // Resolve the main local LAN zone safely const fwSections = L.uci.sections(CFG.CMD.firewall, 'zone') || []; @@ -4522,11 +4715,13 @@ const syncInstanceFirewallRule = async function (instance_id, instObj, role, cus safeUciAdd(CFG.CMD.firewall, 'forwarding', fwForwardLanSection); safeUciSet(CFG.CMD.firewall, fwForwardLanSection, 'src', 'lan'); safeUciSet(CFG.CMD.firewall, fwForwardLanSection, 'dest', zoneName); + safeUciSet(CFG.CMD.firewall, fwForwardLanSection, 'enabled', '1'); // Configure the forwarding from VPN zone back to LAN safely safeUciAdd(CFG.CMD.firewall, 'forwarding', fwForwardVpnSection); safeUciSet(CFG.CMD.firewall, fwForwardVpnSection, 'src', zoneName); safeUciSet(CFG.CMD.firewall, fwForwardVpnSection, 'dest', 'lan'); + safeUciSet(CFG.CMD.firewall, fwForwardVpnSection, 'enabled', '1'); // Only save and request a firewall reload if changes were actually detected if (hasChanges === true) { @@ -4564,6 +4759,7 @@ const removeInstanceFirewallRule = function (instance_id) { // Delete the inbound WAN rule blocks if they exist safeUciRemove(CFG.CMD.firewall, CFG.ID.fw_openvpn_rule + instance_id); safeUciRemove(CFG.CMD.firewall, CFG.ID.fw_openvpn_inc + instance_id); + safeUciRemove(CFG.CMD.firewall, CFG.ID.fw_openvpn_jmp_inc + instance_id); safeUciRemove(CFG.CMD.firewall, CFG.ID.fw_openvpn_s2s_inc + instance_id); // Delete the site-to-site zone and forwarding blocks completely if they exist @@ -4571,6 +4767,11 @@ const removeInstanceFirewallRule = function (instance_id) { safeUciRemove(CFG.CMD.firewall, CFG.ID.fw_openvpn_fwd_lan + instance_id); safeUciRemove(CFG.CMD.firewall, CFG.ID.fw_openvpn_fwd_vpn + instance_id); + // Delete nft files if they exist + secureRemoveFile(CFG.FILE.dir_cfg + instance_id + '.nft'); + secureRemoveFile(CFG.FILE.dir_cfg + instance_id + '.jmp.nft'); + secureRemoveFile(CFG.FILE.dir_cfg + instance_id + '.s2s.nft'); + // Only commit to storage and request a reload if sections were actually deleted if (hasChanges === true) { L.uci.save(); @@ -4579,35 +4780,37 @@ const removeInstanceFirewallRule = function (instance_id) { }; /** - * Reads and aggregates live packet and byte counters from the custom nftables sub-chain - */ +* Reads and aggregates live packet and byte counters from the custom nftables sub-chain +*/ const getSubChainMetrics = async function (subChainName) { // Runs your specialized firewall nft list backend utility safely const output = await luci_app_openvpn_plus([CFG.LIBEXEC.firewallnft, subChainName]); - if (output.length > 0) { - // Matches any counter line ending with either 'reject' or 'drop' (supports both TCP & UDP) - const counterLines = output.match(/counter packets \d+ bytes \d+ (update|reject|drop)/g) || []; - - let totalPackets = 0; - let totalBytes = 0; - - counterLines.forEach(function (line) { - // Captures both numeric groups cleanly using digits (\d+) - const metrics = line.match(/packets (\d+) bytes (\d+)/); - if (metrics && metrics[1] && metrics[2]) { - totalPackets += parseInt(metrics[1], 10); - totalBytes += parseInt(metrics[2], 10); - } - }); - - return { - active: true, - packets: totalPackets, - bytes: totalBytes - }; + // Catch backend errors or inactive firewall chains immediately + if (!output || output.length === 0 || output.startsWith('ERROR')) { + return { active: false, packets: 0, bytes: 0 }; } - return { active: false, packets: 0, bytes: 0 }; + + // Matches any counter line ending with either 'reject' or 'drop' (supports both TCP & UDP) + const counterLines = output.match(/counter packets \d+ bytes \d+ (update|reject|drop)/g) || []; + + let totalPackets = 0; + let totalBytes = 0; + + counterLines.forEach(function (line) { + // Captures both numeric groups cleanly using digits (\d+) + const metrics = line.match(/packets (\d+) bytes (\d+)/); + if (metrics && metrics[1] && metrics[2]) { + totalPackets += parseInt(metrics[1], 10); + totalBytes += parseInt(metrics[2], 10); + } + }); + + return { + active: true, + packets: totalPackets, + bytes: totalBytes + }; }; /** @@ -4665,7 +4868,7 @@ const refreshFirewall = async function (appData) { if (inst && inst.isRunning === true && inst.floodGuard === true) { isfloodGuardInUse = true; // Push active query promise into parallel execution queue - metricPromises.push(getSubChainMetrics('ovpn_filter_' + inst.id)); + metricPromises.push(getSubChainMetrics(CFG.ID.fw_openvpn_floodguard + inst.id)); } }); @@ -4737,7 +4940,7 @@ const renderFirewallInfoBox = function (appData) { const sectionName = r['.name'] || ''; if (sectionName.indexOf('openvpn_') === 0 || sectionName.indexOf('openvpn_rule_') === 0) { const pVal = L.uci.get(CFG.CMD.firewall, sectionName, 'dest_port'); - let protoVal = L.uci.get(CFG.CMD.firewall, sectionName, 'proto') || OPENVPN.PROTO.UDP; + let protoVal = L.uci.get(CFG.CMD.firewall, sectionName, 'proto') || OPENVPN.PROTO.udp; if (pVal) { const pNum = parseInt(pVal, 10); diff --git a/luci-app-wwand/Makefile b/luci-app-wwand/Makefile index 0b1a4b221..a47e4a85b 100644 --- a/luci-app-wwand/Makefile +++ b/luci-app-wwand/Makefile @@ -17,12 +17,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-wwand -PKG_VERSION:=1.7.0_pre8 -PKG_RELEASE:=44 +PKG_VERSION:=1.7.0_pre9 +PKG_RELEASE:=45 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/luci-app-wwand.git -PKG_SOURCE_VERSION:=45ededed69f719e84145839fc0075d8222d18198 +PKG_SOURCE_VERSION:=ab79811095c81817d55ec9bf82a9fc978f34856d PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only diff --git a/meow/Makefile b/meow/Makefile index f8147ca29..66630f29e 100644 --- a/meow/Makefile +++ b/meow/Makefile @@ -1,72 +1,73 @@ +# SPDX-License-Identifier: MIT +# +# Copyright (C) 2026 ImmortalWrt.org +# + include $(TOPDIR)/rules.mk PKG_NAME:=meow - -# Upstream release baseline. Bump by hand when meow-rs tags a new release. PKG_BASE_VERSION?=0.22.0 - -# PKG_VERSION is rewritten automatically by -# .github/workflows/sync-upstream.yml from the pinned upstream commit's -# committer time (UTC, %Y%m%d%H%M). Do not edit this line by hand. -# All-numeric dotted segments keep the version valid for both package -# managers: apk -> meow-0.21.2.202610010430-r1.apk -# ipk -> meow_0.21.2.202610010430-1_.ipk +PKG_SOURCE_VERSION?=5d7156438bf68636a5b1d00869524726fff03633 PKG_VERSION?=$(PKG_BASE_VERSION).202610130311 PKG_RELEASE?=1 -include $(INCLUDE_DIR)/package.mk +PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz +PKG_SOURCE_URL:=https://codeload.github.com/meow-rs/meow-rs/tar.gz/$(PKG_SOURCE_VERSION)? +PKG_HASH:=skip +PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-$(PKG_VERSION) -PKG_MAINTAINER:=Independent meow OpenWrt package +PKG_MAINTAINER:=Max Lv +PKG_LICENSE:=MIT +PKG_LICENSE_FILES:=LICENSE + +PKG_BUILD_DEPENDS:=rust/host cmake/host +PKG_BUILD_PARALLEL:=1 + +include $(INCLUDE_DIR)/package.mk +include $(TOPDIR)/feeds/packages/lang/rust/rust-package.mk + +TAR_OPTIONS += --strip-components 1 +TAR_CMD=$(HOST_TAR) -C $(1) $(TAR_OPTIONS) define Package/meow SECTION:=net CATEGORY:=Network + SUBMENU:=Web Servers/Proxies TITLE:=meow high-performance rule-based proxy core URL:=https://github.com/meow-rs/meow-rs - LICENSE:=MIT - # Required by the default transparent-proxy mode implemented in - # files/gateway.sh (nftables "socket transparent" + tproxy for UDP). - # Both packages exist for OpenWrt 25.12 (openwrt-25.12 netfilter.mk); - # they are published per target under targets///kmods//. - # If you need the package to install on a target whose kmods feed is - # stale or mismatched, drop this line - gateway.sh already probes for - # the modules at runtime and logs a hint instead of leaking traffic. - DEPENDS:=+kmod-nft-socket +kmod-nft-tproxy + DEPENDS:=@(aarch64||arm||x86_64||riscv64) +kmod-nft-socket +kmod-nft-tproxy +libstdcpp endef define Package/meow/description - meow Rust proxy core built from the upstream commit recorded in UPSTREAM.json. + A high-performance, rule-based tunneling proxy kernel in Rust, + compatible with mihomo (Clash Meta). Static binary; configuration lives in + /etc/meow/config.yaml, service settings in /etc/config/meow. - /usr/bin/meow, the procd init script, UCI settings and the default config - at /etc/meow/config.yaml (mixed HTTP/SOCKS5 proxy on :7890, REST API and - web panel on :9090, rule mode with MATCH,DIRECT). - - Transparent-proxy / gateway mode additionally needs kmod-nft-socket and - kmod-nft-tproxy (pulled in as dependencies). + Transparent-proxy / gateway mode additionally uses kmod-nft-socket and + kmod-nft-tproxy. endef -define Build/Prepare -endef +define Package/meow/config + if PACKAGE_meow + config MEOW_MINIMAL + bool "Build with minimal feature set (--no-default-features --features minimal)" + default n + help + Build a smaller binary using the minimal feature bundle (< 8 MiB budget on aarch64) + instead of the default full feature set. -define Build/Compile -endef + config MEOW_EXTERNAL_UI_DOWNLOAD + bool "Enable external-ui-download feature" + default y + help + Allow meow to auto-download a third-party web dashboard from external-ui-url. -define Package/meow/install - @test -s ./prebuilt-meow || { echo "ERROR: meow/prebuilt-meow is not populated yet; run build-core.yml first."; exit 1; } - $(INSTALL_DIR) $(1)/usr/bin - $(INSTALL_DIR) $(1)/etc/init.d - $(INSTALL_DIR) $(1)/etc/config - $(INSTALL_DIR) $(1)/etc/meow - $(INSTALL_DIR) $(1)/etc/uci-defaults - $(INSTALL_DIR) $(1)/usr/share/meow - $(INSTALL_BIN) ./prebuilt-meow $(1)/usr/bin/meow - $(INSTALL_BIN) ./files/meow.init $(1)/etc/init.d/meow - $(INSTALL_DATA) ./files/meow.config $(1)/etc/config/meow - $(INSTALL_DATA) ./files/config.yaml $(1)/etc/meow/config.yaml - $(INSTALL_BIN) ./files/gateway.sh $(1)/usr/share/meow/gateway.sh - $(INSTALL_BIN) ./files/arp-hijack.sh $(1)/usr/share/meow/arp-hijack.sh - $(INSTALL_BIN) ./files/meow-arp.init $(1)/etc/init.d/meow-arp - $(INSTALL_BIN) ./files/meow.uci-defaults $(1)/etc/uci-defaults/80_meow + config MEOW_VERSION_SUFFIX + string "Optional version suffix (MEOW_VERSION_SUFFIX)" + default "" + help + Optional suffix appended to the version reported by `meow -v` (e.g. -alpha+5d71564). + endif endef define Package/meow/conffiles @@ -74,4 +75,59 @@ define Package/meow/conffiles /etc/meow/config.yaml endef +define Select/Features +$(strip \ + $(if $(CONFIG_MEOW_MINIMAL),--no-default-features --features minimal) \ + $(if $(CONFIG_MEOW_EXTERNAL_UI_DOWNLOAD),--features external-ui-download) \ +) +endef + +define Build/Compile + ( \ + pushd $(PKG_BUILD_DIR) ; \ + $(CARGO_PKG_CONFIG_VARS) \ + MEOW_VERSION_SUFFIX="$(call qstrip,$(CONFIG_MEOW_VERSION_SUFFIX))" \ + cargo build --release \ + --locked \ + -p meow-app \ + --bin meow \ + --target $(RUSTC_TARGET_ARCH) \ + $(call Select/Features) ; \ + popd ; \ + ) +endef + +define Package/meow/install + $(INSTALL_DIR) $(1)/usr/bin + $(INSTALL_BIN) $(PKG_BUILD_DIR)/target/$(RUSTC_TARGET_ARCH)/release/meow $(1)/usr/bin/meow + + $(INSTALL_DIR) $(1)/etc/init.d + $(INSTALL_BIN) ./files/meow.init $(1)/etc/init.d/meow + $(INSTALL_BIN) ./files/meow-arp.init $(1)/etc/init.d/meow-arp + + $(INSTALL_DIR) $(1)/etc/config + $(INSTALL_DATA) ./files/meow.config $(1)/etc/config/meow + + $(INSTALL_DIR) $(1)/etc/meow + $(INSTALL_DATA) ./files/config.yaml $(1)/etc/meow/config.yaml + + $(INSTALL_DIR) $(1)/usr/share/meow + $(INSTALL_BIN) ./files/gateway.sh $(1)/usr/share/meow/gateway.sh + $(INSTALL_BIN) ./files/arp-hijack.sh $(1)/usr/share/meow/arp-hijack.sh + + $(INSTALL_DIR) $(1)/etc/uci-defaults + $(INSTALL_BIN) ./files/meow.uci-defaults $(1)/etc/uci-defaults/80_meow +endef + +define Package/meow/prerm +#!/bin/sh +[ -n "$${IPKG_INSTROOT}" ] && exit 0 +/etc/init.d/meow-arp stop 2>/dev/null +/etc/init.d/meow-arp disable 2>/dev/null +/etc/init.d/meow stop 2>/dev/null +/etc/init.d/meow disable || true +exit 0 +endef + +$(eval $(call RustBinPackage,meow)) $(eval $(call BuildPackage,meow)) diff --git a/meow/prebuilt-meow b/meow/prebuilt-meow deleted file mode 100755 index 95de8aa90..000000000 Binary files a/meow/prebuilt-meow and /dev/null differ diff --git a/wwand/Makefile b/wwand/Makefile index 6bb0ab5e2..04e326da9 100644 --- a/wwand/Makefile +++ b/wwand/Makefile @@ -50,12 +50,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=wwand -PKG_VERSION:=1.7.0_pre28 -PKG_RELEASE:=78 +PKG_VERSION:=1.7.0_pre33 +PKG_RELEASE:=79 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git -PKG_SOURCE_VERSION:=2a09643571ddfe2c4b538c4123d3d5985b410fcd +PKG_SOURCE_VERSION:=5281731de7f5465552a9bba1b909b082b3050fae PKG_MIRROR_HASH:=skip PKG_LICENSE:=GPL-2.0-only