From afd9decbc5683535ea3826b037f794f3c8cb2990 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 1 Nov 2026 12:58:10 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9B=84=20Sync=202026-11-01=2012:58:10?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- luci-app-adguardhome/Makefile | 3 +- luci-app-open-box/Makefile | 4 +- luci-app-passwall/Makefile | 2 +- .../model/cbi/passwall/client/shunt_rules.lua | 14 ++-- luci-app-passwall/po/zh-cn/passwall.po | 12 ++-- .../root/usr/share/passwall/nftables.sh | 2 +- luci-app-passwall2/Makefile | 2 +- .../cbi/passwall2/client/shunt_rules.lua | 18 +++--- luci-app-passwall2/po/fa/passwall2.po | 16 ++--- luci-app-passwall2/po/ru/passwall2.po | 14 ++-- luci-app-passwall2/po/zh-cn/passwall2.po | 14 ++-- luci-app-passwall2/po/zh-tw/passwall2.po | 14 ++-- .../root/usr/share/passwall2/nftables.sh | 2 +- luci-app-trafficctl/Makefile | 4 +- .../root/etc/config/trafficctl | 21 +++++- .../root/usr/local/bin/trafficctl-block.sh | 7 +- .../root/usr/local/bin/trafficctl-fw.sh | 12 ++++ .../usr/local/bin/trafficctl-ratelimit.sh | 9 +-- luci-mod-status/Makefile | 2 +- .../resources/view/status/include/40_dhcp.js | 10 ++- rpcd-mod-luci/Makefile | 2 +- rpcd-mod-luci/src/traffic.c | 55 +++++++++++++++- rpcd-mod-luci/tests/README.md | 7 ++ rpcd-mod-luci/tests/traffic.c | 34 +++++++++- vless-core-cli/Makefile | 22 +++---- vless-core-cli/README.md | 64 ++++++++++++++++--- 26 files changed, 269 insertions(+), 97 deletions(-) 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 = "
" 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 = "
" ip_list = s:option(TextValue, "ip_list", "IP") ip_list.rows = 10 @@ -253,13 +253,13 @@ ip_list.description = "
" 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).