⛄ Sync 2026-11-01 12:58:10
Merge-upstream / merge (push) Waiting to run

This commit is contained in:
github-actions[bot] committed 2026-11-01 12:58:10 +08:00
1 parent d1e51134a0
commit afd9decbc5
26 files changed
+269 -97

No files matched your search

+2 -1
View File
@@ -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
+2 -2
View File
@@ -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
+1 -1
View File
@@ -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:= \
@@ -207,11 +207,11 @@ domain_list.validate = function(self, value)
return value
end
domain_list.description = "<br /><ul>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. translate("Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. translate("Pre-defined domain list: Beginning with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn.") .. "</li>"
.. "<li>"
.. translate("Sing-Box is compatible with Geo rules and rule-set. rule-set begin with 'rule-set:remote:' or 'rule-set:local:'.")
.. "<ul>"
@@ -219,7 +219,7 @@ domain_list.description = "<br /><ul>"
.. "<li>" .. translate("Such as:") .. "'rule-set:local:/usr/share/sing-box/geosite-cn.srs'" .. "</li>"
.. "</ul>"
.. "</li>"
.. "<li>" .. translate("Annotation: Begining with #") .. "</li>"
.. "<li>" .. translate("Annotation: Beginning with #") .. "</li>"
.. "</ul>"
ip_list = s:option(TextValue, "ip_list", "IP")
ip_list.rows = 10
@@ -256,7 +256,7 @@ ip_list.description = "<br /><ul>"
.. "<li>" .. translate("Such as:") .. "'rule-set:local:/usr/share/sing-box/geoip-cn.srs'" .. "</li>"
.. "</ul>"
.. "</li>"
.. "<li>" .. translate("Annotation: Begining with #") .. "</li>"
.. "<li>" .. translate("Annotation: Beginning with #") .. "</li>"
.. "</ul>"
o = s:option(Flag, "invert", "Invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
+6 -6
View File
@@ -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."
@@ -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"
+1 -1
View File
@@ -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:= \
@@ -210,19 +210,19 @@ domain_list.validate = function(self, value)
return value
end
domain_list.description = "<br /><ul>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. translate("Pre-defined domain list: Begining with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. 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'.") .. "</li>"
.. "<li>" .. translate("Pre-defined domain list: Beginning with 'geosite:' and the rest is a name, such as geosite:google or geosite:cn.") .. "</li>"
.. "<li>"
.. 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:'")
.. "<ul>"
.. "<li>" .. translate("Such as:") .. "'rule-set:remote:https://raw.githubusercontent.com/SagerNet/sing-geosite/rule-set/geosite-cn.srs'" .. "</li>"
.. "<li>" .. translate("Such as:") .. "'rule-set:local:/usr/share/sing-box/geosite-cn.srs'" .. "</li>"
.. "</ul>"
.. "</li>"
.. "<li>" .. translate("Annotation: Begining with #") .. "</li>"
.. "<li>" .. translate("Annotation: Beginning with #") .. "</li>"
.. "</ul>"
ip_list = s:option(TextValue, "ip_list", "IP")
ip_list.rows = 10
@@ -253,13 +253,13 @@ ip_list.description = "<br /><ul>"
.. "<li>" .. translate("CIDR: such as '127.0.0.0/8'.") .. "</li>"
.. "<li>" .. translate("GeoIP: such as 'geoip:cn'. It begins with geoip: (lower case) and followed by two letter of country code.") .. "</li>"
.. "<li>"
.. 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:'")
.. "<ul>"
.. "<li>" .. translate("Such as:") .. "'rule-set:remote:https://raw.githubusercontent.com/SagerNet/sing-geoip/rule-set/geoip-cn.srs'" .. "</li>"
.. "<li>" .. translate("Such as:") .. "'rule-set:local:/usr/share/sing-box/geoip-cn.srs'" .. "</li>"
.. "</ul>"
.. "</li>"
.. "<li>" .. translate("Annotation: Begining with #") .. "</li>"
.. "<li>" .. translate("Annotation: Beginning with #") .. "</li>"
.. "</ul>"
o = s:option(Flag, "invert", "invert", translate("Invert match result.") .. " " .. translate("Only support Sing-Box."))
+8 -8
View File
@@ -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."
+7 -7
View File
@@ -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."
+7 -7
View File
@@ -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."
+7 -7
View File
@@ -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."
@@ -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
+2 -2
View File
@@ -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 <yusdyr@gmail.com>
+20 -1
View File
@@ -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.
@@ -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\"}"
@@ -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
@@ -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
+1 -1
View File
@@ -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
@@ -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'))
])
]);
+1 -1
View File
@@ -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 <jo@mein.io>
PKG_LICENSE:=Apache-2.0
+53 -2
View File
@@ -12,6 +12,8 @@
#include <fcntl.h>
#include <sys/stat.h>
#include <netinet/ether.h>
#include <ifaddrs.h>
#include <linux/netfilter/nf_conntrack_tcp.h>
#include <sys/socket.h>
#include <linux/netlink.h>
#include <linux/netfilter/nfnetlink.h>
@@ -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;
}
+7
View File
@@ -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.
+33 -1
View File
@@ -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);
+11 -11
View File
@@ -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 <kosnt.shell@gmail.com>
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
+54 -10
View File
@@ -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).