diff --git a/luci-app-adguardhome/Makefile b/luci-app-adguardhome/Makefile
index d02c9b03f..59c1bf77a 100644
--- a/luci-app-adguardhome/Makefile
+++ b/luci-app-adguardhome/Makefile
@@ -1,5 +1,6 @@
-# SPDX-License-Identifier: GPL-2.0-only
+# SPDX-License-Identifier: GPL-3.0-or-later
+PKG_LICENSE:=GPL-3.0-or-later
include $(TOPDIR)/rules.mk
LUCI_NAME:=luci-app-adguardhome
diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile
index 96b679006..8ed6c2bef 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.383
-PKG_RELEASE:=55
+PKG_VERSION:=0.1.385
+PKG_RELEASE:=56
PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later
diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile
index d04f2d4b6..2dc42472f 100644
--- a/luci-app-passwall/Makefile
+++ b/luci-app-passwall/Makefile
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.29
-PKG_RELEASE:=360
+PKG_RELEASE:=361
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/shunt_rules.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/shunt_rules.lua
index 92aed02ec..964634f0e 100644
--- a/luci-app-passwall/luasrc/model/cbi/passwall/client/shunt_rules.lua
+++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/shunt_rules.lua
@@ -207,11 +207,11 @@ domain_list.validate = function(self, value)
return value
end
domain_list.description = "
"
-.. "- " .. translate("Plaintext: If this string matches any part of the targeting domain, this rule takes effet. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'.") .. "
"
-.. "- " .. translate("Regular expression: Begining with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'.") .. "
"
-.. "- " .. translate("Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'.") .. "
"
-.. "- " .. translate("Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'.") .. "
"
-.. "- " .. translate("Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn.") .. "
"
+.. "- " .. translate("Plaintext: If this string matches any part of the targeting domain, this rule takes effect. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'.") .. "
"
+.. "- " .. translate("Regular expression: Beginning with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'.") .. "
"
+.. "- " .. translate("Subdomain (recommended): Beginning with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'.") .. "
"
+.. "- " .. translate("Full domain: Beginning with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'full:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'.") .. "
"
+.. "- " .. translate("Pre-defined domain list: Beginning with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn.") .. "
"
.. "- "
.. translate("Sing-Box is compatible with Geo rules and rule-set. rule-set begin with 'rule-set:remote:' or 'rule-set:local:'.")
.. "
"
@@ -219,7 +219,7 @@ domain_list.description = "
"
.. "- " .. translate("Such as:") .. "'rule-set:local:/usr/share/sing-box/geosite-cn.srs'" .. "
"
.. "
"
.. "
"
-.. "- " .. translate("Annotation: Begining with #") .. "
"
+.. "- " .. translate("Annotation: Beginning with #") .. "
"
.. "
"
ip_list = s:option(TextValue, "ip_list", "IP")
ip_list.rows = 10
@@ -256,7 +256,7 @@ ip_list.description = "
"
.. "- " .. translate("Such as:") .. "'rule-set:local:/usr/share/sing-box/geoip-cn.srs'" .. "
"
.. "
"
.. ""
-.. "" .. translate("Annotation: Begining with #") .. ""
+.. "" .. translate("Annotation: Beginning with #") .. ""
.. ""
o = s:option(Flag, "invert", "Invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
diff --git a/luci-app-passwall/po/zh-cn/passwall.po b/luci-app-passwall/po/zh-cn/passwall.po
index a456bb725..f2b86ccaf 100644
--- a/luci-app-passwall/po/zh-cn/passwall.po
+++ b/luci-app-passwall/po/zh-cn/passwall.po
@@ -1223,19 +1223,19 @@ msgstr "不是有效 IPv6 格式,请重新输入!"
msgid "Not true format, please re-enter!"
msgstr "不是正确的格式,请重新输入!"
-msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effet. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'."
+msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effect. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'."
msgstr "纯字符串: 当此字符串匹配目标域名中任意部分,该规则生效。比如'sina.com'可以匹配'sina.com'、'sina.com.cn'和'www.sina.com',但不匹配'sina.cn'。"
-msgid "Regular expression: Begining with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'."
+msgid "Regular expression: Beginning with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'."
msgstr "正则表达式: 由'regexp:'开始,余下部分是一个正则表达式。当此正则表达式匹配目标域名时,该规则生效。例如'regexp:\\.goo.*\\.com$'匹配'www.google.com'、'fonts.googleapis.com',但不匹配'google.com'。"
-msgid "Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'."
+msgid "Subdomain (recommended): Beginning with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'."
msgstr "子域名 (推荐): 由'domain:'开始,余下部分是一个域名。当此域名是目标域名或其子域名时,该规则生效。例如'domain:v2ray.com'匹配'www.v2ray.com'、'v2ray.com',但不匹配'xv2ray.com'。"
-msgid "Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'."
+msgid "Full domain: Beginning with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'full:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'."
msgstr "完整匹配: 由'full:'开始,余下部分是一个域名。当此域名完整匹配目标域名时,该规则生效。例如'full:v2ray.com'匹配'v2ray.com'但不匹配'www.v2ray.com'。"
-msgid "Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn."
+msgid "Pre-defined domain list: Beginning with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn."
msgstr "预定义域名列表:由'geosite:'开头,余下部分是一个名称,如geosite:google或者geosite:cn。"
msgid "Sing-Box is compatible with Geo rules and rule-set. rule-set begin with 'rule-set:remote:' or 'rule-set:local:'."
@@ -1244,7 +1244,7 @@ msgstr "Sing-Box 可兼容 Geo 规则和规则集,规则集由 'rule-set:remot
msgid "Such as:"
msgstr "例如:"
-msgid "Annotation: Begining with #"
+msgid "Annotation: Beginning with #"
msgstr "注释: 由 # 开头"
msgid "Invert match result."
diff --git a/luci-app-passwall/root/usr/share/passwall/nftables.sh b/luci-app-passwall/root/usr/share/passwall/nftables.sh
index 395f79af2..ef0e9d3fb 100755
--- a/luci-app-passwall/root/usr/share/passwall/nftables.sh
+++ b/luci-app-passwall/root/usr/share/passwall/nftables.sh
@@ -216,7 +216,7 @@ insert_nftset() {
gen_nftset() {
local nftset_name="${1}"; shift
local ip_type="${1}"; shift
- # 0 - don't set defalut timeout
+ # 0 - don't set default timeout
local timeout_argument_set="${1}"; shift
local gc_interval_time="1h"
diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile
index c3c0b8a78..d58cd5c9a 100644
--- a/luci-app-passwall2/Makefile
+++ b/luci-app-passwall2/Makefile
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.10.29
-PKG_RELEASE:=161
+PKG_RELEASE:=162
PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \
diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua
index e7bbfec6d..365b6a8e4 100644
--- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua
+++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/shunt_rules.lua
@@ -210,19 +210,19 @@ domain_list.validate = function(self, value)
return value
end
domain_list.description = "
"
-.. "- " .. translate("Plaintext: If this string matches any part of the targeting domain, this rule takes effet. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'.") .. "
"
-.. "- " .. translate("Regular expression: Begining with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'.") .. "
"
-.. "- " .. translate("Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'.") .. "
"
-.. "- " .. translate("Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'.") .. "
"
-.. "- " .. translate("Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn.") .. "
"
+.. "- " .. translate("Plaintext: If this string matches any part of the targeting domain, this rule takes effect. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'.") .. "
"
+.. "- " .. translate("Regular expression: Beginning with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'.") .. "
"
+.. "- " .. translate("Subdomain (recommended): Beginning with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'.") .. "
"
+.. "- " .. translate("Full domain: Beginning with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'full:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'.") .. "
"
+.. "- " .. translate("Pre-defined domain list: Beginning with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn.") .. "
"
.. "- "
- .. translate("Sing-Box rule-set: Begining with 'rule-set:remote:' or 'rule-set:local:'")
+ .. translate("Sing-Box rule-set: Beginning with 'rule-set:remote:' or 'rule-set:local:'")
.. "
"
.. "- " .. translate("Such as:") .. "'rule-set:remote:https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs'" .. "
"
.. "- " .. translate("Such as:") .. "'rule-set:local:/usr/share/sing-box/geosite-cn.srs'" .. "
"
.. "
"
.. " "
-.. "- " .. translate("Annotation: Begining with #") .. "
"
+.. "- " .. translate("Annotation: Beginning with #") .. "
"
.. "
"
ip_list = s:option(TextValue, "ip_list", "IP")
ip_list.rows = 10
@@ -253,13 +253,13 @@ ip_list.description = "
"
.. "- " .. translate("CIDR: such as '127.0.0.0/8'.") .. "
"
.. "- " .. translate("GeoIP: such as 'geoip:cn'. It begins with geoip: (lower case) and followed by two letter of country code.") .. "
"
.. "- "
- .. translate("Sing-Box rule-set: Begining with 'rule-set:remote:' or 'rule-set:local:'")
+ .. translate("Sing-Box rule-set: Beginning with 'rule-set:remote:' or 'rule-set:local:'")
.. "
"
.. "- " .. translate("Such as:") .. "'rule-set:remote:https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs'" .. "
"
.. "- " .. translate("Such as:") .. "'rule-set:local:/usr/share/sing-box/geoip-cn.srs'" .. "
"
.. "
"
.. " "
-.. "- " .. translate("Annotation: Begining with #") .. "
"
+.. "- " .. translate("Annotation: Beginning with #") .. "
"
.. "
"
o = s:option(Flag, "invert", "invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
diff --git a/luci-app-passwall2/po/fa/passwall2.po b/luci-app-passwall2/po/fa/passwall2.po
index 06bb1f226..b5923cea5 100644
--- a/luci-app-passwall2/po/fa/passwall2.po
+++ b/luci-app-passwall2/po/fa/passwall2.po
@@ -1216,28 +1216,28 @@ msgstr "فرمت IPv6 نامعتبر است، لطفاً دوباره وارد
msgid "Not true format, please re-enter!"
msgstr "فرمت صحیح نیست، لطفاً دوباره وارد کنید!"
-msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effet. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'."
+msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effect. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'."
msgstr "متن ساده (Plaintext): اگر این رشته با هر بخشی از دامنه هدف مطابقت داشته باشد، این قانون اعمال میشود. مثال: قانون 'sina.com' با دامنه هدف 'sina.com'، 'sina.com.cn' و 'www.sina.com' مطابقت دارد، اما با 'sina.cn' خیر."
-msgid "Regular expression: Begining with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'."
+msgid "Regular expression: Beginning with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'."
msgstr "عبارت باقاعده (Regular expression): با 'regexp:' شروع میشود و بقیه یک عبارت باقاعده است. زمانی که عبارت باقاعده با دامنه هدف مطابقت داشته باشد، این قانون اعمال میشود. مثال: قانون 'regexp:\\.goo.*\\.com$' با 'www.google.com' و 'fonts.googleapis.com' مطابقت دارد، اما با 'google.com' خیر."
-msgid "Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'."
+msgid "Subdomain (recommended): Beginning with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'."
msgstr "زیردامنه (توصیه شده): با 'domain:' شروع میشود و بقیه یک دامنه است. زمانی که دامنه هدف دقیقاً مقدار باشد، یا زیردامنهای از مقدار باشد، این قانون اعمال میشود. مثال: قانون 'domain:v2ray.com' با 'www.v2ray.com' و 'v2ray.com' مطابقت دارد، اما با 'xv2ray.com' خیر."
-msgid "Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'."
-msgstr "دامنه کامل: با 'full:' شروع میشود و بقیه یک دامنه است. زمانی که دامنه هدف دقیقاً مقدار باشد، قانون اعمال میشود. مثال: قانون 'domain:v2ray.com' با 'v2ray.com' مطابقت دارد، اما با 'www.v2ray.com' خیر."
+msgid "Full domain: Beginning with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'full:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'."
+msgstr "دامنه کامل: با 'full:' شروع میشود و بقیه یک دامنه است. زمانی که دامنه هدف دقیقاً مقدار باشد، قانون اعمال میشود. مثال: قانون 'full:v2ray.com' با 'v2ray.com' مطابقت دارد، اما با 'www.v2ray.com' خیر."
-msgid "Sing-Box rule-set: Begining with 'rule-set:remote:' or 'rule-set:local:'"
+msgid "Sing-Box rule-set: Beginning with 'rule-set:remote:' or 'rule-set:local:'"
msgstr "مجموعه قوانین Sing-Box: با 'rule-set:remote:' یا 'rule-set:local:' شروع میشود"
msgid "Such as:"
msgstr "مانند:"
-msgid "Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn."
+msgid "Pre-defined domain list: Beginning with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn."
msgstr "لیست دامنه از پیش تعریف شده: با 'geosite:' شروع میشود و بقیه یک نام است، مانند geosite:google یا geosite:cn."
-msgid "Annotation: Begining with #"
+msgid "Annotation: Beginning with #"
msgstr "توضیحات: با # شروع میشود"
msgid "Invert match result."
diff --git a/luci-app-passwall2/po/ru/passwall2.po b/luci-app-passwall2/po/ru/passwall2.po
index e5d8985b4..63fb7a088 100644
--- a/luci-app-passwall2/po/ru/passwall2.po
+++ b/luci-app-passwall2/po/ru/passwall2.po
@@ -1217,28 +1217,28 @@ msgstr "Неверный формат IPv6, пожалуйста, введите
msgid "Not true format, please re-enter!"
msgstr "Неверный формат, пожалуйста, введите заново!"
-msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effet. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'."
+msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effect. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'."
msgstr "Текст: правило срабатывает, если строка встречается в любом месте домена. Например, 'sina.com' совпадёт с 'sina.com', 'sina.com.cn', 'www.sina.com', но не с 'sina.cn'."
-msgid "Regular expression: Begining with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'."
+msgid "Regular expression: Beginning with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'."
msgstr "Регулярное выражение: начинается с 'regexp:', далее — шаблон. Правило срабатывает при совпадении. Например, 'regexp:\\.goo.*\\.com$' совпадёт с 'www.google.com' и 'fonts.googleapis.com', но не с 'google.com'."
-msgid "Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'."
+msgid "Subdomain (recommended): Beginning with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'."
msgstr "Поддомен (рекомендуется): начинается с 'domain:', далее — домен. Правило срабатывает для точного совпадения или любого поддомена. Например, 'domain:v2ray.com' совпадёт с 'www.v2ray.com' и 'v2ray.com', но не с 'xv2ray.com'."
-msgid "Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'."
+msgid "Full domain: Beginning with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'full:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'."
msgstr "Полное совпадение: начинается с 'full:', далее — домен. Правило срабатывает только при точном совпадении. Например, 'full:v2ray.com' совпадёт только с 'v2ray.com', но не с 'www.v2ray.com'."
-msgid "Sing-Box rule-set: Begining with 'rule-set:remote:' or 'rule-set:local:'"
+msgid "Sing-Box rule-set: Beginning with 'rule-set:remote:' or 'rule-set:local:'"
msgstr "Набор правил Sing-Box: начинается с 'rule-set:remote:' или 'rule-set:local:'"
msgid "Such as:"
msgstr "Например:"
-msgid "Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn."
+msgid "Pre-defined domain list: Beginning with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn."
msgstr "Предопределённый список: начинается с 'geosite:', далее — имя набора, например geosite:google или geosite:cn."
-msgid "Annotation: Begining with #"
+msgid "Annotation: Beginning with #"
msgstr "Комментарий начинается с #"
msgid "Invert match result."
diff --git a/luci-app-passwall2/po/zh-cn/passwall2.po b/luci-app-passwall2/po/zh-cn/passwall2.po
index da2002425..2982c732b 100644
--- a/luci-app-passwall2/po/zh-cn/passwall2.po
+++ b/luci-app-passwall2/po/zh-cn/passwall2.po
@@ -1208,28 +1208,28 @@ msgstr "不是有效IPv6格式,请重新输入!"
msgid "Not true format, please re-enter!"
msgstr "不是正确的格式,请重新输入!"
-msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effet. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'."
+msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effect. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'."
msgstr "纯字符串: 当此字符串匹配目标域名中任意部分,该规则生效。比如'sina.com'可以匹配'sina.com'、'sina.com.cn'和'www.sina.com',但不匹配'sina.cn'。"
-msgid "Regular expression: Begining with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'."
+msgid "Regular expression: Beginning with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'."
msgstr "正则表达式: 由'regexp:'开始,余下部分是一个正则表达式。当此正则表达式匹配目标域名时,该规则生效。例如'regexp:\\.goo.*\\.com$'匹配'www.google.com'、'fonts.googleapis.com',但不匹配'google.com'。"
-msgid "Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'."
+msgid "Subdomain (recommended): Beginning with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'."
msgstr "子域名 (推荐): 由'domain:'开始,余下部分是一个域名。当此域名是目标域名或其子域名时,该规则生效。例如'domain:v2ray.com'匹配'www.v2ray.com'、'v2ray.com',但不匹配'xv2ray.com'。"
-msgid "Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'."
+msgid "Full domain: Beginning with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'full:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'."
msgstr "完整匹配: 由'full:'开始,余下部分是一个域名。当此域名完整匹配目标域名时,该规则生效。例如'full:v2ray.com'匹配'v2ray.com'但不匹配'www.v2ray.com'。"
-msgid "Sing-Box rule-set: Begining with 'rule-set:remote:' or 'rule-set:local:'"
+msgid "Sing-Box rule-set: Beginning with 'rule-set:remote:' or 'rule-set:local:'"
msgstr "Sing-Box 规则集: 由 'rule-set:remote:' 或 'rule-set:local:' 开始"
msgid "Such as:"
msgstr "例如:"
-msgid "Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn."
+msgid "Pre-defined domain list: Beginning with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn."
msgstr "预定义域名列表:由'geosite:'开头,余下部分是一个名称,如geosite:google或者geosite:cn。"
-msgid "Annotation: Begining with #"
+msgid "Annotation: Beginning with #"
msgstr "注释: 由 # 开头"
msgid "Invert match result."
diff --git a/luci-app-passwall2/po/zh-tw/passwall2.po b/luci-app-passwall2/po/zh-tw/passwall2.po
index 9ec22971e..0d38f8191 100644
--- a/luci-app-passwall2/po/zh-tw/passwall2.po
+++ b/luci-app-passwall2/po/zh-tw/passwall2.po
@@ -1208,28 +1208,28 @@ msgstr "不是有效IPv6格式,請重新輸入!"
msgid "Not true format, please re-enter!"
msgstr "不是正確的格式,請重新輸入!"
-msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effet. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'."
+msgid "Plaintext: If this string matches any part of the targeting domain, this rule takes effect. Example: rule 'sina.com' matches targeting domain 'sina.com', 'sina.com.cn' and 'www.sina.com', but not 'sina.cn'."
msgstr "純字符串: 當此字符串匹配目標域名中任意部分,该規則生效。比如'sina.com'可以匹配'sina.com'、'sina.com.cn'和'www.sina.com',但不匹配'sina.cn'。"
-msgid "Regular expression: Begining with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'."
+msgid "Regular expression: Beginning with 'regexp:', the rest is a regular expression. When the regexp matches targeting domain, this rule takes effect. Example: rule 'regexp:\\.goo.*\\.com$' matches 'www.google.com' and 'fonts.googleapis.com', but not 'google.com'."
msgstr "正則表达式: 由'regexp:'開始,余下部分是一個正則表达式。當此正則表达式匹配目標域名時,该規則生效。例如'regexp:\\.goo.*\\.com$'匹配'www.google.com'、'fonts.googleapis.com',但不匹配'google.com'。"
-msgid "Subdomain (recommended): Begining with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'."
+msgid "Subdomain (recommended): Beginning with 'domain:' and the rest is a domain. When the targeting domain is exactly the value, or is a subdomain of the value, this rule takes effect. Example: rule 'domain:v2ray.com' matches 'www.v2ray.com', 'v2ray.com', but not 'xv2ray.com'."
msgstr "子域名 (推荐): 由'domain:'開始,余下部分是一個域名。當此域名是目標域名或其子域名時,该規則生效。例如'domain:v2ray.com'匹配'www.v2ray.com'、'v2ray.com',但不匹配'xv2ray.com'。"
-msgid "Full domain: Begining with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'domain:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'."
+msgid "Full domain: Beginning with 'full:' and the rest is a domain. When the targeting domain is exactly the value, the rule takes effect. Example: rule 'full:v2ray.com' matches 'v2ray.com', but not 'www.v2ray.com'."
msgstr "完整匹配: 由'full:'開始,余下部分是一個域名。當此域名完整匹配目標域名時,该規則生效。例如'full:v2ray.com'匹配'v2ray.com'但不匹配'www.v2ray.com'。"
-msgid "Sing-Box rule-set: Begining with 'rule-set:remote:' or 'rule-set:local:'"
+msgid "Sing-Box rule-set: Beginning with 'rule-set:remote:' or 'rule-set:local:'"
msgstr "Sing-Box 規則集: 由 'rule-set:remote:' 或 'rule-set:local:' 開始"
msgid "Such as:"
msgstr "例如:"
-msgid "Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn."
+msgid "Pre-defined domain list: Beginning with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn."
msgstr "预定义域名列表:由'geosite:'開头,余下部分是一個名称,如geosite:google或者geosite:cn。"
-msgid "Annotation: Begining with #"
+msgid "Annotation: Beginning with #"
msgstr "注释: 由 # 開头"
msgid "Invert match result."
diff --git a/luci-app-passwall2/root/usr/share/passwall2/nftables.sh b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh
index 37ce93a7f..b0f6a7152 100755
--- a/luci-app-passwall2/root/usr/share/passwall2/nftables.sh
+++ b/luci-app-passwall2/root/usr/share/passwall2/nftables.sh
@@ -176,7 +176,7 @@ insert_nftset() {
gen_nftset() {
local nftset_name="${1}"; shift
local ip_type="${1}"; shift
- # 0 - don't set defalut timeout
+ # 0 - don't set default timeout
local timeout_argument_set="${1}"; shift
# 0 - don't let element timeout(365 days) when set's timeout parameters be seted
# -1 - follow the set's timeout parameters
diff --git a/luci-app-trafficctl/Makefile b/luci-app-trafficctl/Makefile
index 1ab4912e3..4fc03dc71 100644
--- a/luci-app-trafficctl/Makefile
+++ b/luci-app-trafficctl/Makefile
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-trafficctl
-PKG_VERSION:=1.25.0
-PKG_RELEASE:=16
+PKG_VERSION:=1.25.1
+PKG_RELEASE:=17
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Denis Iusupov
diff --git a/luci-app-trafficctl/root/etc/config/trafficctl b/luci-app-trafficctl/root/etc/config/trafficctl
index b08199d03..c28ab02d9 100644
--- a/luci-app-trafficctl/root/etc/config/trafficctl
+++ b/luci-app-trafficctl/root/etc/config/trafficctl
@@ -1,7 +1,26 @@
config trafficctl 'main'
option enabled '1'
option default_mode 'limiter'
- option persist_rules '0'
+ # Write per-device blocks and rate limits to /etc/trafficctl/rules.json so
+ # the ifup-lan hook can restore them after a reboot.
+ #
+ # On by default since #105. It used to ship off, which made the same
+ # intention behave two ways: a traffic SHAPE persists unconditionally (the
+ # shaper has to record which HTB class belongs to which device, so
+ # shapes.json is always written and always restored), while a block or a
+ # limit vanished on reboot — and the default mode is the limiter, so the
+ # out-of-the-box path was the one that forgot. That was an unexamined
+ # default sitting next to a deliberate one, not a design.
+ #
+ # The cost is one small write to /etc per rule CHANGE. Not periodic, so
+ # unlike the byte counters — which live in tmpfs precisely because they are
+ # rewritten every poll — this is not a flash-wear concern.
+ #
+ # NOT the same switch as the global cut's "keep after reboot": that one is
+ # its own opt-in because an all-devices cut can lock out whoever
+ # administers the router through a LAN host, so a reboot must be able to
+ # undo it. See the cut section below.
+ option persist_rules '1'
# Extra subnets to monitor (space-separated CIDRs), for downstream-router
# clients that have no kernel route on this router. Subnets with a
# "via" route over a LAN interface are picked up automatically.
diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-block.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-block.sh
index 7ee1b7415..084ad4083 100755
--- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-block.sh
+++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-block.sh
@@ -61,12 +61,13 @@ if tctl_block_add "$IP" "$COMMENT"; then
else
NOTE=" — IPv4 only: no MAC known for $IP (no DHCP lease, no neighbour entry), so IPv6 is not covered"
fi
- NOTE="$NOTE$(tctl_nss_note)"
+ NOTE="$NOTE$(tctl_nss_note)$(tctl_persist_note)"
NSS="$(tctl_nss_status)"
+ PERSIST=$(tctl_persist_enabled && echo true || echo false)
if [ "$SELF_BLOCK" = "true" ]; then
- echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"internet blocked for $IP (your device — LuCI access preserved)$NOTE\"}"
+ echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"internet blocked for $IP (your device — LuCI access preserved)$NOTE\"}"
else
- echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"internet blocked for $IP$NOTE\"}"
+ echo "{\"ok\":true,\"ipv6\":$([ "$TCTL_BLOCK_IPV6" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"internet blocked for $IP$NOTE\"}"
fi
else
echo "{\"ok\":false,\"msg\":\"failed to block $IP\"}"
diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh
index ade64af2f..269774178 100755
--- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh
+++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-fw.sh
@@ -1208,6 +1208,18 @@ tctl_nss_note() {
"$TCTL_ECM_DEBUGFS"
}
+# The suffix a caller appends when the rule it just installed will not be
+# written down, and so will not come back after a reboot (#105).
+#
+# Same principle as the IPv6 and NSS qualifiers: this package says what a
+# success does NOT cover rather than letting the operator discover it later.
+# Here "later" meant the next reboot, which is the worst possible moment to
+# find out that a device you blocked is online again.
+tctl_persist_note() {
+ tctl_persist_enabled && return 0
+ printf ' [NOT PERSISTENT: this will be lost on reboot — persistence is off. Enable it with `uci set trafficctl.main.persist_rules=1 && uci commit trafficctl`, or in Settings → Logging & Persistence, then re-apply]'
+}
+
# Defunct the flows ECM has already accelerated.
#
# This is the NSS counterpart of the `conntrack -D` every rule change already
diff --git a/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh b/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh
index 7a5e63e91..8e55d4337 100755
--- a/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh
+++ b/luci-app-trafficctl/root/usr/local/bin/trafficctl-ratelimit.sh
@@ -104,13 +104,14 @@ else
# packets never reach the Linux stack. Reporting an unqualified success
# there is the defect in #81, so every message below carries the verdict
# and the result carries the status as a field.
- NSSNOTE="$(tctl_nss_note)"
+ NSSNOTE="$(tctl_nss_note)$(tctl_persist_note)"
NSS="$(tctl_nss_status)"
+ PERSIST=$(tctl_persist_enabled && echo true || echo false)
if [ "$TCTL_RL_DOWNLOAD_FAILED" = "1" ]; then
- echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"rate limit ${RATE_UP} kbit/s applied to $IP UPLOAD ONLY — WAN device not resolvable$V6NOTE$NSSNOTE\"}"
+ echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"rate limit ${RATE_UP} kbit/s applied to $IP UPLOAD ONLY — WAN device not resolvable$V6NOTE$NSSNOTE\"}"
elif [ "$TCTL_RL_UPLOAD_FAILED" = "1" ]; then
- echo "{\"ok\":true,\"ipv6_upload\":false,\"nss\":\"$NSS\",\"msg\":\"rate limit ${RATE} kbit/s applied to $IP DOWNLOAD ONLY — no LAN ingress device [IPv4 only]$NSSNOTE\"}"
+ echo "{\"ok\":true,\"ipv6_upload\":false,\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"rate limit ${RATE} kbit/s applied to $IP DOWNLOAD ONLY — no LAN ingress device [IPv4 only]$NSSNOTE\"}"
else
- echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"msg\":\"rate limit $(rate_text) for $IP (both directions, $MODE)$V6NOTE$NSSNOTE\"}"
+ echo "{\"ok\":true,\"ipv6_upload\":$([ "$TCTL_RL_UPLOAD6_OK" = "1" ] && echo true || echo false),\"nss\":\"$NSS\",\"persist\":$PERSIST,\"msg\":\"rate limit $(rate_text) for $IP (both directions, $MODE)$V6NOTE$NSSNOTE\"}"
fi
fi
diff --git a/luci-mod-status/Makefile b/luci-mod-status/Makefile
index 2b97a8b15..5e9432c90 100644
--- a/luci-mod-status/Makefile
+++ b/luci-mod-status/Makefile
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI Status Pages
LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-mod-dashboard +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket
-PKG_RELEASE:=40
+PKG_RELEASE:=41
PKG_BUILD_DEPENDS:=iwinfo
PKG_LICENSE:=Apache-2.0
diff --git a/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js b/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js
index 9a7d4412a..7f94375dc 100644
--- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js
+++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/40_dhcp.js
@@ -227,11 +227,13 @@ return baseclass.extend({
L.resolveDefault(uci.load('dhcp')),
L.resolveDefault(callClientWeb(), {}),
L.resolveDefault(fs.exec_direct('/usr/libexec/luci-arp'), ''),
- L.resolveDefault(fs.read('/tmp/luci-client-history.json').then(JSON.parse), {})
+ L.resolveDefault(fs.read('/tmp/luci-client-history.json').then(JSON.parse), {}),
+ L.resolveDefault(rpc.list('hqos').then(methods => methods && methods.hqos && methods.hqos.set ? L.require('hqos') : null), null)
]);
},
- render([dhcp_leases, host_hints, ufp_list, dhcp_config, web, arp, history]) {
+ render([dhcp_leases, host_hints, ufp_list, dhcp_config, web, arp, history, hqos]) {
+ this.hqos = hqos;
if (!this.ratePoll) {
this.ratePoll = L.bind(this.refreshRates, this);
poll.add(this.ratePoll, 2);
@@ -366,6 +368,8 @@ return baseclass.extend({
},
leaseActions(client) {
+ if (this.hqos)
+ return this.hqos.renderAction(client);
const actions = [];
for (const [family, leases] of [[4, client.leases], [6, client.leases6]]) {
for (const lease of leases) {
@@ -431,7 +435,7 @@ return baseclass.extend({
E('th', { 'class': 'th' }, _('Download')),
E('th', { 'class': 'th', 'data-total-traffic': '1' }, _('Total traffic') + ' (↑ / ↓)'),
E('th', { 'class': 'th' }, _('Connection count')),
- isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions center' }, _('Static Lease'))
+ isReadonlyView ? E([]) : E('th', { 'class': 'th cbi-section-actions center' }, this.hqos ? _('Client rate limit') : _('Static Lease'))
])
]);
diff --git a/rpcd-mod-luci/Makefile b/rpcd-mod-luci/Makefile
index 1b040f744..0f933b00b 100644
--- a/rpcd-mod-luci/Makefile
+++ b/rpcd-mod-luci/Makefile
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=rpcd-mod-luci
PKG_VERSION:=20240305
-PKG_RELEASE:=3
+PKG_RELEASE:=4
PKG_MAINTAINER:=Jo-Philipp Wich
PKG_LICENSE:=Apache-2.0
diff --git a/rpcd-mod-luci/src/traffic.c b/rpcd-mod-luci/src/traffic.c
index 5e2e9a001..6f7e7ed86 100644
--- a/rpcd-mod-luci/src/traffic.c
+++ b/rpcd-mod-luci/src/traffic.c
@@ -12,6 +12,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -43,6 +45,7 @@ struct host {
unsigned char mac[6];
uint64_t total[2];
uint32_t connections, pending_connections;
+ uint32_t routed_connections, pending_routed_connections;
bool available;
};
struct flow {
@@ -63,6 +66,7 @@ struct client {
bool warm, ready, missing;
};
static struct avl_tree clients, flows, hosts;
+static struct ifaddrs *router_addresses;
static struct uloop_fd ct_fd = { .fd = -1 };
static struct uloop_timeout timer;
static uint32_t sequence, generation;
@@ -413,6 +417,30 @@ static void add_bytes(struct client *c, const unsigned char *owner, uint64_t up,
}
}
+/* The reply source is the destination after DNAT, including hairpin NAT.
+ * Exclude traffic terminating on the router from the HQoS forward-hook count. */
+static bool router_destination(const struct address *address)
+{
+ if (address->family == AF_INET) {
+ uint32_t ip;
+ memcpy(&ip, address->bytes, sizeof(ip));
+ if (IN_MULTICAST(ntohl(ip)) || ip == INADDR_BROADCAST)
+ return true;
+ } else if (address->bytes[0] == 0xff) {
+ return true;
+ }
+ for (struct ifaddrs *ifa = router_addresses; ifa; ifa = ifa->ifa_next) {
+ if (!ifa->ifa_addr || ifa->ifa_addr->sa_family != address->family)
+ continue;
+ const void *bytes = address->family == AF_INET ?
+ (void *)&((struct sockaddr_in *)ifa->ifa_addr)->sin_addr :
+ (void *)&((struct sockaddr_in6 *)ifa->ifa_addr)->sin6_addr;
+ if (!memcmp(address->bytes, bytes, address->family == AF_INET ? 4 : 16))
+ return true;
+ }
+ return false;
+}
+
/* Count dump records independently of byte accounting and flow tombstones. */
static void count_connection(struct nlmsghdr *nlh)
{
@@ -433,6 +461,12 @@ static void count_connection(struct nlmsghdr *nlh)
if (a) a->pending_connections++;
/* Hairpin and dual-address connections count once for the same MAC. */
if (b && b != a) b->pending_connections++;
+ struct nlattr *info = nested(attr_find(data, len, CTA_PROTOINFO), CTA_PROTOINFO_TCP);
+ uint8_t state = 0;
+ attr_copy(nested(info, CTA_PROTOINFO_TCP_STATE), &state, sizeof(state));
+ if (a && state != TCP_CONNTRACK_TIME_WAIT && state != TCP_CONNTRACK_CLOSE &&
+ !router_destination(&reply_src))
+ a->pending_routed_connections++;
}
static bool process_flow(struct nlmsghdr *nlh)
@@ -533,8 +567,10 @@ static void update_rate(struct client *c, uint64_t elapsed)
static void finish_dump(void)
{
struct host *h;
- avl_for_each_element(&hosts, h, avl)
+ avl_for_each_element(&hosts, h, avl) {
h->connections = h->pending_connections;
+ h->routed_connections = h->pending_routed_connections;
+ }
uint64_t now = now_ms(), elapsed = now - sampled;
struct client *c;
struct flow *f, *tmp;
@@ -650,6 +686,9 @@ static void sample_timer(struct uloop_timeout *t)
return;
}
discover_clients();
+ freeifaddrs(router_addresses);
+ router_addresses = NULL;
+ getifaddrs(&router_addresses);
FILE *acct = fopen("/proc/sys/net/netfilter/nf_conntrack_acct", "r");
int enabled = 0;
if (acct) { enabled = fgetc(acct) == '1'; fclose(acct); }
@@ -677,8 +716,10 @@ static void sample_timer(struct uloop_timeout *t)
return;
}
struct host *h;
- avl_for_each_element(&hosts, h, avl)
+ avl_for_each_element(&hosts, h, avl) {
h->pending_connections = 0;
+ h->pending_routed_connections = 0;
+ }
generation++;
dumping = true;
dump_started = now;
@@ -770,6 +811,16 @@ static int get_rates(struct ubus_context *ctx, struct ubus_object *obj,
}
}
blobmsg_close_table(&result, connections);
+ void *routed = blobmsg_open_table(&result, "routed_connections");
+ if (router_addresses && sampled && !failure && now - sampled < 3 * SAMPLE_MS) {
+ avl_for_each_element(&hosts, host, avl) {
+ char mac[18];
+ snprintf(mac, sizeof(mac), "%02X:%02X:%02X:%02X:%02X:%02X",
+ host->mac[0], host->mac[1], host->mac[2], host->mac[3], host->mac[4], host->mac[5]);
+ blobmsg_add_u32(&result, mac, host->routed_connections);
+ }
+ }
+ blobmsg_close_table(&result, routed);
ubus_send_reply(ctx, req, result.head);
return 0;
}
diff --git a/rpcd-mod-luci/tests/README.md b/rpcd-mod-luci/tests/README.md
index 417c7c8e2..3a7cbb859 100644
--- a/rpcd-mod-luci/tests/README.md
+++ b/rpcd-mod-luci/tests/README.md
@@ -75,3 +75,10 @@ MAC. It includes idle tracked entries and counts records even without byte
counters. It is a two-second snapshot, not a TCP ESTABLISHED-only count.
Failed or stale snapshots return no counts, rendered as `-`; a valid empty
snapshot returns zero. Counts are not restored from historical traffic state.
+
+The `routed_connections` table exposes original-source connections per MAC for
+HQoS admission monitoring. It combines IPv4 and IPv6, excludes destinations
+that terminate on the router after DNAT, multicast, TCP TIME_WAIT and CLOSE.
+Inbound connections are not charged to the destination client. The existing
+`connections` field keeps the homepage total count, including router sessions.
+Tests cover router-local IPv4/IPv6, redirect, hairpin and closed TCP states.
diff --git a/rpcd-mod-luci/tests/traffic.c b/rpcd-mod-luci/tests/traffic.c
index ef7d248b6..4bc3ba7fd 100644
--- a/rpcd-mod-luci/tests/traffic.c
+++ b/rpcd-mod-luci/tests/traffic.c
@@ -73,7 +73,7 @@ static void feed(int family, const char *src, const char *dst, const char *reply
assert(process_flow(msg));
}
-static void count_record(int family, const char *src, const char *dst, const char *reply_src)
+static void count_record_state(int family, const char *src, const char *dst, const char *reply_src, int state)
{
char buffer[2048] = {0};
struct nlmsghdr *msg = (void *)buffer;
@@ -82,10 +82,21 @@ static void count_record(int family, const char *src, const char *dst, const cha
nf->nfgen_family = family;
tuple(msg, CTA_TUPLE_ORIG, src, dst);
tuple(msg, CTA_TUPLE_REPLY, reply_src, src);
+ if (state >= 0) {
+ struct nlattr *info = begin(msg, CTA_PROTOINFO), *tcp = begin(msg, CTA_PROTOINFO_TCP);
+ uint8_t value = state;
+ put(msg, CTA_PROTOINFO_TCP_STATE, &value, sizeof(value));
+ end(msg, tcp); end(msg, info);
+ }
/* Missing byte counters must not prevent connection counting. */
count_connection(msg);
}
+static void count_record(int family, const char *src, const char *dst, const char *reply_src)
+{
+ count_record_state(family, src, dst, reply_src, -1);
+}
+
static struct client *client(const char *ip)
{
struct client *c = calloc(1, sizeof(*c));
@@ -115,6 +126,27 @@ int main(void)
assert(get_host(a->mac, false)->pending_connections == 3);
assert(get_host(b->mac, false)->pending_connections == 1);
assert(get_host(a->mac, false)->connections == 0);
+ assert(get_host(a->mac, false)->pending_routed_connections == 3);
+ assert(get_host(b->mac, false)->pending_routed_connections == 0);
+ struct sockaddr_in local4 = { .sin_family = AF_INET };
+ struct sockaddr_in6 local6 = { .sin6_family = AF_INET6 };
+ inet_pton(AF_INET, "192.168.0.1", &local4.sin_addr);
+ inet_pton(AF_INET6, "fd00::1", &local6.sin6_addr);
+ struct ifaddrs if6 = { .ifa_addr = (void *)&local6 };
+ struct ifaddrs if4 = { .ifa_addr = (void *)&local4, .ifa_next = &if6 };
+ router_addresses = &if4;
+ count_record(AF_INET, "192.168.0.100", "192.168.0.1", "192.168.0.1");
+ count_record(AF_INET6, "fd00::2", "fd00::1", "fd00::1");
+ count_record(AF_INET, "192.168.0.100", "8.8.8.8", "192.168.0.1"); /* Redirect to router. */
+ count_record(AF_INET6, "fd00::2", "ff02::fb", "ff02::fb");
+ count_record_state(AF_INET, "192.168.0.100", "8.8.8.8", "8.8.8.8", TCP_CONNTRACK_TIME_WAIT);
+ count_record_state(AF_INET, "192.168.0.100", "8.8.8.8", "8.8.8.8", TCP_CONNTRACK_CLOSE);
+ assert(get_host(a->mac, false)->pending_routed_connections == 3);
+ count_record(AF_INET, "192.168.0.100", "203.0.113.1", "192.168.0.101"); /* Hairpin forwards. */
+ count_record_state(AF_INET, "192.168.0.100", "8.8.8.8", "8.8.8.8", TCP_CONNTRACK_ESTABLISHED);
+ assert(get_host(a->mac, false)->pending_routed_connections == 5);
+ router_addresses = NULL;
+ get_host(a->mac, false)->pending_routed_connections = 0;
get_host(a->mac, false)->pending_connections = 0;
get_host(b->mac, false)->pending_connections = 0;
memcpy(v6->mac, v6mac, 6);
diff --git a/vless-core-cli/Makefile b/vless-core-cli/Makefile
index 7f68c44e6..e6dcf7f9d 100644
--- a/vless-core-cli/Makefile
+++ b/vless-core-cli/Makefile
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=vless-core-cli
-PKG_VERSION:=1.1.4
-PKG_RELEASE:=1
+PKG_VERSION:=2.0.0
+PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/notfence/vless-core-cli/tar.gz/v$(PKG_VERSION)?
@@ -11,7 +11,7 @@ PKG_HASH:=skip
PKG_MAINTAINER:=Konstantine Shevlakov
PKG_LICENSE_FILES:=THIRD_PARTY_LICENSES.txt
-PKG_BUILD_PARALLEL:=1
+PKG_BUILD_FLAGS:=gc-sections
include $(INCLUDE_DIR)/package.mk
@@ -19,14 +19,15 @@ define Package/vless-core-cli
SECTION:=net
CATEGORY:=Network
SUBMENU:=Web Servers/Proxies
- TITLE:=Standalone C VLESS client with local SOCKS5
+ TITLE:=Standalone C proxy client (VLESS/Trojan/VMess/SS) with local SOCKS5
URL:=https://github.com/notfence/vless-core-cli
DEPENDS:=+libopenssl
endef
define Package/vless-core-cli/description
- Standalone C client for VLESS (TCP/XHTTP/gRPC/WS, Reality/TLS) and SOCKS5
- upstreams. Listens on a local SOCKS5 port on 127.0.0.1.
+ Standalone C client for VLESS, Trojan, VMess, Shadowsocks and SOCKS5
+ upstreams (TCP/WS/gRPC/XHTTP/HTTPUpgrade, TLS/Reality).
+ Listens on a local SOCKS5 port on 127.0.0.1.
endef
define Package/vless-core-cli/conffiles
@@ -34,11 +35,10 @@ define Package/vless-core-cli/conffiles
endef
define Build/Compile
- $(MAKE) -C $(PKG_BUILD_DIR) linux \
- CC="$(TARGET_CC)" \
- COMMON_CFLAGS="$(TARGET_CFLAGS) $(TARGET_CPPFLAGS) -std=c11 -Wall" \
- LDFLAGS="$(TARGET_LDFLAGS)" \
- BIN_LINUX=vless-core
+ $(TARGET_CC) $(TARGET_CFLAGS) $(TARGET_CPPFLAGS) -std=c11 -Wall \
+ -I$(PKG_BUILD_DIR)/include $(PKG_BUILD_DIR)/src/*.c \
+ -o $(PKG_BUILD_DIR)/vless-core \
+ $(TARGET_LDFLAGS) -lssl -lcrypto -lpthread
endef
define Package/vless-core-cli/install
diff --git a/vless-core-cli/README.md b/vless-core-cli/README.md
index 60c5f0088..420c2276c 100644
--- a/vless-core-cli/README.md
+++ b/vless-core-cli/README.md
@@ -1,13 +1,18 @@
# vless-core-cli
OpenWrt package for [notfence/vless-core-cli](https://github.com/notfence/vless-core-cli) —
-a standalone C client for VLESS that exposes a local SOCKS5 proxy.
+a standalone C client that connects to a VLESS, Trojan, VMess, Shadowsocks or SOCKS5 server
+and exposes a local SOCKS5 proxy.
-Compared to Go-based cores it is tiny (the ipk is about ~70 KB) and depends only on `libopenssl`,
+Compared to Go-based cores it is very small and depends only on `libopenssl`,
which makes it suitable for devices with small flash and RAM.
-Supported upstreams: VLESS over TCP / XHTTP / gRPC / WebSocket with Reality, TLS or no security
-(`xtls-rprx-vision` on TCP), and plain SOCKS5.
+Supported upstreams:
+
+- VLESS (with `xtls-rprx-vision` and VLESS Encryption), Trojan, VMess AEAD (`alterId=0`),
+ Shadowsocks (AEAD and 2022) over TCP / WebSocket / gRPC / XHTTP / HTTPUpgrade with
+ no security, TLS or Reality (Reality is not available for WebSocket and HTTPUpgrade).
+- Plain SOCKS5, optionally with username/password.
## Files
@@ -44,13 +49,15 @@ Section `config vless-core 'main'`:
| Option | Default | Description |
|--------|---------|-------------|
| `enabled` | `0` | Start the service (`1`/`0`) |
-| `uri` | – | `vless://...` or `socks5://host:port` upstream URI (required) |
+| `uri` | – | `vless://`, `vmess://`, `trojan://`, `ss://` or `socks5://` upstream URI (required) |
| `listen_port` | `1080` | Local SOCKS5 port on `127.0.0.1` |
| `routing` | – | Optional routing rules, see below |
| `tls_mode` | `strict` | Certificate verification: `strict`, `insecure` or `tofu` |
Always quote the URI in `uci set`: it contains `&`, `#` and `?`.
+The client serves exactly one upstream URI, so the init script runs a single instance.
+
## URI examples
Reality + Vision:
@@ -77,13 +84,38 @@ gRPC + Reality:
vless://UUID@example.com:443?type=grpc&security=reality&serviceName=svc&sni=www.example.org&fp=chrome&pbk=PUBLIC_KEY&sid=SHORT_ID#grpc
```
+HTTPUpgrade + TLS:
+
+```
+vless://UUID@example.com:443?type=httpupgrade&security=tls&sni=example.com&host=example.com&path=%2Fup#httpupgrade
+```
+
+Trojan (TLS by default, the whole userinfo is the password):
+
+```
+trojan://PASSWORD@example.com:443?sni=example.com#trojan
+```
+
+VMess (`encryption` is the body mode, `scy` is an alias; the base64 JSON form is accepted too):
+
+```
+vmess://UUID@example.com:443?type=ws&security=tls&sni=example.com&host=example.com&path=%2Fws&encryption=auto#vmess
+```
+
+Shadowsocks (SIP002, userinfo is base64url of `method:password`):
+
+```
+ss://BASE64URL_METHOD_PASSWORD@example.com:8388#ss
+```
+
Upstream SOCKS5:
```
socks5://192.0.2.1:1080
```
-Supported `fp` values: `chrome`, `firefox`, `edge`, `random`, `randomized`, `qq`.
+Supported `fp` values: `chrome`, `firefox`, `edge`, `random`, `randomized`, `qq`
+(there are no `safari` or `ios` fingerprints).
## Routing
@@ -130,12 +162,24 @@ Notes:
- `insecure` — never verify certificates.
- `tofu` — pin the certificate on first connection, later connections must match.
+## Reality client version
+
+For Reality the client writes an Xray version (`x.y.z`, default `26.3.27`) into the ClientHello
+session ID. Override it with the `XRAY_VER` environment variable or `--xray-version x.y.z`,
+for example if the server limits client versions with `minClientVer` / `maxClientVer`.
+This is not exposed in UCI.
+
## Build notes
-Upstream hardcodes the output name `vless-core-linux-amd64` regardless of the target
-architecture, so the package passes `BIN_LINUX=vless-core` to upstream `make` and builds
-with the SDK `CC`/`CFLAGS`/`LDFLAGS`.
+Upstream's Makefile runs its test suite (clang sanitizers, a separate OpenSSL 3.5, Xray) and
+an x86-64-only binary check, and hardcodes the output name `vless-core-linux-amd64`.
+The package therefore does not use it: it compiles `src/*.c` directly with the SDK toolchain
+and installs the result as `/usr/bin/vless-core`.
+
+ML-KEM based features (VLESS Encryption, the X25519MLKEM768 key share) need OpenSSL 3.5 or newer.
+
+Tested: OpenWrt SDK, musl x86-64, OpenSSL 3.5.9.
## ToDo
-- `luci-app-vless-core` — LuCI web interface.
+- `luci-app-vless-core` — LuCI web interface (someday).