update 2026-10-20 02:12:23

This commit is contained in:
action committed 2026-10-20 02:12:23 +08:00
1 parent 313fe55d56
commit f322ead2a4
28 files changed
+1625 -241

No files matched your search

Submodule luci-app-eqosplus deleted from c7ea61c971.
+47
View File
@@ -0,0 +1,47 @@
name: Build
on:
push:
tags:
- v*
jobs:
build:
name: Build ${{ matrix.arch }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
arch:
- aarch64_generic
- i386_pentium-mmx
- loongarch64_generic
- mips_mips32
- mipsel_24kc
- mipsel_mips32
- x86_64
steps:
- uses: actions/checkout@main
with:
fetch-depth: 0
- name: Building packages
uses: sbwml/openwrt-gh-action-sdk@go1.24
env:
ARCH: ${{ matrix.arch }}-openwrt-24.10
FEEDNAME: packages_ci
PACKAGES: luci-app-eqosplus
NO_REFRESH_CHECK: true
- name: Upload artifacts
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.arch }}
path: bin/packages/${{ matrix.arch }}/packages_ci/*.ipk
- name: Upload packages
uses: ncipollo/release-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
allowUpdates: true
replacesArtifacts: true
artifacts: "bin/packages/${{ matrix.arch }}/packages_ci/*.ipk"
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 sirpdboy
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+39
View File
@@ -0,0 +1,39 @@
#
# Copyright (C) 2006-2017 OpenWrt.org#
# Copyright (C) 2022-2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-eqosplus
# This is free software, licensed under the GNU General Public License v2.
# See /LICENSE for more information.
#
include $(TOPDIR)/rules.mk
THEME_NAME:=eqosplus
PKG_NAME:=luci-app-$(THEME_NAME)
PKG_LICENSE:=Apache-2.0
LUCI_TITLE:=LuCI support for eqosplus.
LUCI_DESCRIPTION:=LuCI support for Easy eqosplus(Support speed limit based on IP address).
LUCI_DEPENDS:=+ip-full +tc +nftables +bc +kmod-ifb
LUCI_PKGARCH:=all
PKG_VERSION:=1.3.0
PKG_RELEASE:=20251128
PKG_MAINTAINER:=sirpdboy <herboy2008@gmail.com>
define Build/Compile
endef
define Package/$(PKG_NAME)/postinst
#!/bin/sh
rm -f /tmp/luci-*
endef
define Package/$(PKG_NAME)/conffiles
/etc/config/eqosplus
endef
include $(TOPDIR)/feeds/luci/luci.mk
# call BuildPackage - OpenWrt buildroot signature
+91
View File
@@ -0,0 +1,91 @@
![hello](https://views.whatilearened.today/views/github/sirpdboy/deplives.svg) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
<h1 align="center">
<br>luci-app-eqosplus<br>
</h1>
<p align="center">
<a href="https://openwrt.org"><img alt="OpenWrt" src="https://img.shields.io/badge/OpenWrt-%E2%89%A519.07-ff0000?logo=openwrt&logoColor=white"></a>
<a href="https://www.google.com/chrome/"><img alt="Chrome" src="https://img.shields.io/badge/Chrome-%E2%89%A5111-4285F3?logo=googlechrome&logoColor=white"></a>
<a href="https://www.apple.com/safari/"><img alt="Safari" src="https://img.shields.io/badge/Safari-%E2%89%A516.4-000000?logo=safari&logoColor=white"></a>
<a href="https://www.mozilla.org/firefox/"><img alt="Firefox" src="https://img.shields.io/badge/Firefox-%E2%89%A5128-FF7138?logo=firefoxbrowser&logoColor=white"></a>
<a target="_blank" href="https://github.com/sirpdboy/luci-app-eqosplus/releases"> <img alt="GitHub release" src="https://img.shields.io/github/v/release/sirpdboy/luci-app-eqosplus"></a>
<a href="https://github.com/sirpdboy/luci-app-eqosplus/releases"><img alt="Downloads" src="https://img.shields.io/github/downloads/sirpdboy/luci-app-eqosplus/total"></a>
</p>
[中文] | [English](README.md)
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg)
Please read this page carefully, which includes precautions and instructions on how to use it.
## Enhanced version of EQOS with timed speed limit and added features such as timed limit.
### Method for downloading source code:
```Brach
# downloading
git clone https://github.com/sirpdboy/luci-app-eqosplus package/luci-app-eqosplus
make menuconfig
```
### Configuration Menu
```Brach
make menuconfig
# find LuCI -> Applications, select luci-app-eqosplus, save and exit
```
### compile
```Brach
# compile
make package/luci-app-eqosplus/compile V=s
```
## describe
- luci-app-eqosplus : https://github.com/sirpdboy/luci-app-eqosplus
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明2.jpg)
## interface
![screenshots](./doc/eqosplus0.png)
![screenshots](./doc/eqosplus1.png)
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/eqosplus.png)
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/eqosplus2.png)
# My other project
- Watch Dog : https://github.com/sirpdboy/luci-app-watchdog
- Net Speedtest : https://github.com/sirpdboy/luci-app-netspeedtest
- Task Plan : https://github.com/sirpdboy/luci-app-taskplan
- Power Off Device : https://github.com/sirpdboy/luci-app-poweroffdevice
- OpentoPD Theme : https://github.com/sirpdboy/luci-theme-opentopd
- KuCat Theme : https://github.com/sirpdboy/luci-theme-kucat
- KuCat Theme Config : https://github.com/sirpdboy/luci-app-kucat-config
- NFT Time Control : https://github.com/sirpdboy/luci-app-timecontrol
- Parent Control: https://github.com/sirpdboy/luci-theme-parentcontrol
- Eqos Plus: https://github.com/sirpdboy/luci-app-eqosplus
- Advanced : https://github.com/sirpdboy/luci-app-advanced
- ddns-go : https://github.com/sirpdboy/luci-app-ddns-go
- Advanced Plus): https://github.com/sirpdboy/luci-app-advancedplus
- Net Wizard: https://github.com/sirpdboy/luci-app-netwizard
- Part Exp: https://github.com/sirpdboy/luci-app-partexp
- Lukcy: https://github.com/sirpdboy/luci-app-lukcy
## HELP
| <img src="https://img.shields.io/badge/-Alipay-F5F5F5.svg" href="#赞助支持本项目-" height="25" alt="图飞了"/> | <img src="https://img.shields.io/badge/-WeChat-F5F5F5.svg" height="25" alt="图飞了" href="#赞助支持本项目-"/> |
| :-----------------: | :-------------: |
|![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) |
<a href="#readme">
<img src="https://img.shields.io/badge/-TOP-orange.svg" alt="no" title="Return TOP" align="right"/>
</a>
![hello](https://visitor-badge-deno.deno.dev/sirpdboy.sirpdboy.svg) [![](https://img.shields.io/badge/TGGroup-ClickJoin-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
+109
View File
@@ -0,0 +1,109 @@
![hello](https://views.whatilearened.today/views/github/sirpdboy/deplives.svg) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
<h1 align="center">
<br>luci-app-eqosplus<br>
</h1>
<p align="center">
<a target="_blank" href="https://github.com/sirpdboy/luci-app-eqosplus/releases">
<img src="https://img.shields.io/github/release/sirpdboy/luci-app-eqosplus.svg?style=flat-square&label=luci-app-eqosplus&colorB=green">
</a>
</p>
[中文] | [English](README.md)
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明1.jpg)
请 **认真阅读完毕** 本页面,本页面包含注意事项和如何使用。
## 功能说明:
### 定时限速1.2.2版
#### 2023.7.19 定时限速1.2.2:增加更多日期:工作日和休息日,自定义日期1,2,3中间用逗号分隔;加入MAC地址限速,从此不用担心IPV6和IPV4的限速问题。
### 定时限速1.0版
#### 2022.12.24 定时限速在eqos的加强版,加入定时限制等功能。
## 编译使用方法 [![](https://img.shields.io/badge/-编译使用方法-F5F5F5.svg)](#编译使用方法-)
将luci-app-eqosplus添加至 LEDE/OpenWRT 源码的方法。
### 下载源码方法一:
编辑源码文件夹根目录feeds.conf.default并加入如下内容:
```Brach
# feeds获取源码:
src-git eqosplus https://github.com/sirpdboy/luci-app-eqosplus
```
```Brach
# 更新feeds,并安装主题:
scripts/feeds update eqosplus
scripts/feeds install luci-app-eqosplus
```
### 下载源码方法:
```Brach
# 下载源码
git clone https://github.com/sirpdboy/luci-app-eqosplus package/luci-app-eqosplus
make menuconfig
```
### 配置菜单
```Brach
make menuconfig
# 找到 LuCI -> Applications, 选择 luci-app-eqosplus, 保存后退出。
```
### 编译
```Brach
# 编译固件
make package/luci-app-eqosplus/compile V=s
```
## 说明 [![](https://img.shields.io/badge/-说明-F5F5F5.svg)](#说明-)
源码来源:https://github.com/sirpdboy/luci-app-eqosplus
- 你可以随意使用其中的源码,但请注明出处。
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明2.jpg)
## 界面
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/eqosplus.png)
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/eqosplus2.png)
# My other project
- 路由安全看门狗 :https://github.com/sirpdboy/luci-app-watchdog
- 网络速度测试 :https://github.com/sirpdboy/luci-app-netspeedtest
- 计划任务插件(原定时设置) : https://github.com/sirpdboy/luci-app-taskplan
- 关机功能插件 : https://github.com/sirpdboy/luci-app-poweroffdevice
- opentopd主题 : https://github.com/sirpdboy/luci-theme-opentopd
- kucat酷猫主题: https://github.com/sirpdboy/luci-theme-kucat
- kucat酷猫主题设置工具: https://github.com/sirpdboy/luci-app-kucat-config
- NFT版上网时间控制插件: https://github.com/sirpdboy/luci-app-timecontrol
- 家长控制: https://github.com/sirpdboy/luci-theme-parentcontrol
- 定时限速: https://github.com/sirpdboy/luci-app-eqosplus
- 系统高级设置 : https://github.com/sirpdboy/luci-app-advanced
- ddns-go动态域名: https://github.com/sirpdboy/luci-app-ddns-go
- 进阶设置(系统高级设置+主题设置kucat/agron/opentopd): https://github.com/sirpdboy/luci-app-advancedplus
- 网络设置向导: https://github.com/sirpdboy/luci-app-netwizard
- 一键分区扩容: https://github.com/sirpdboy/luci-app-partexp
- lukcy大吉: https://github.com/sirpdboy/luci-app-lukcy
## 捐助
![screenshots](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/说明3.jpg)
| <img src="https://img.shields.io/badge/-支付宝-F5F5F5.svg" href="#赞助支持本项目-" height="25" alt="图飞了"/> | <img src="https://img.shields.io/badge/-微信-F5F5F5.svg" height="25" alt="图飞了" href="#赞助支持本项目-"/> |
| :-----------------: | :-------------: |
|![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) |
<a href="#readme">
<img src="https://img.shields.io/badge/-返回顶部-orange.svg" alt="图飞了" title="返回顶部" align="right"/>
</a>
![](https://visitor-badge-deno.deno.dev/sirpdboy.sirpdboy.svg) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ)
Binary file not shown.

After

Width:  |  Height:  |  Size: 645 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 664 KiB

@@ -0,0 +1,19 @@
module("luci.controller.eqosplus", package.seeall)
-- Copyright 2022-2023 sirpdboy <herboy2008@gmail.com>
function index()
if not nixio.fs.access("/etc/config/eqosplus") then return end
entry({"admin", "control"}, firstchild(), "Control", 44).dependent = false
local e = entry({"admin", "control", "eqosplus"}, cbi("eqosplus"), _("Eqosplus"), 10)
e.dependent=false
e.acl_depends = { "luci-app-eqosplus" }
entry({"admin", "control", "eqosplus", "status"}, call("act_status")).leaf = true
end
function act_status()
local sys = require "luci.sys"
local e = {}
e.status = sys.call(" busybox ps -w | grep eqosplus | grep -v grep >/dev/null ") == 0
luci.http.prepare_content("application/json")
luci.http.write_json(e)
end
@@ -0,0 +1,190 @@
-- Copyright 2022-2023 sirpdboy <herboy2008@gmail.com>
-- Licensed to the public under the Apache License 2.0.
local sys = require "luci.sys"
local ifaces = sys.net:devices()
local WADM = require "luci.tools.webadmin"
local ipc = require "luci.ip"
local a, t, e
a = Map("eqosplus", translate("Network speed limit"))
a.description = translate("Users can limit the network speed for uploading/downloading through MAC, IP, and IP segments (192.168.110.00-192.168.10.200). The speed unit is MB/second.")
a.template = "eqosplus/index"
t = a:section(TypedSection, "eqosplus")
t.anonymous = true
e = t:option(DummyValue, "eqosplus_status", translate("Status"))
e.template = "eqosplus/eqosplus"
e.value = translate("Collecting data...")
ipi = t:option(ListValue, "ifname", translate("Interface"), translate("Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"))
ipi.default = "1"
ipi:value(1,translate("Automatic settings"))
ipi:value("br-lan", translate("br-lan (LAN Bridge)"))
local function get_wan_interfaces()
local result = {}
local ubus = require "ubus"
local conn = ubus.connect()
if not conn then
return result
end
local network_status = conn:call("network.interface", "dump", {})
for _, iface in ipairs(network_status.interface) do
if iface.interface:match("^wan") or iface.interface:match("^pppoe") or iface.proto == "pppoe" then
local dev = iface.l3_device or iface.device
if dev then
table.insert(result, {
name = dev,
proto = iface.proto,
logical_name = iface.interface
})
end
end
end
conn:close()
return result
end
local wan_ifaces = get_wan_interfaces()
for _, iface in ipairs(wan_ifaces) do
ipi:value(iface.name, translate(iface.name) .. (iface.proto == "pppoe" and " (PPPoE)" or " (WAN)"))
end
t = a:section(TypedSection, "device")
t.template = "cbi/tblsection"
t.anonymous = true
t.addremove = true
comment = t:option(Value, "comment", translate("Comment"))
comment.size = 8
e = t:option(Flag, "enable", translate("Enabled"))
e.rmempty = false
e.size = 4
ip = t:option(Value, "mac", translate("IP/MAC"))
ip.size = 8
local function get_devices()
local devices = {}
local seen_ips = {}
local ubus = require "ubus"
local conn = ubus.connect()
local function get_hostname(ip)
local f = io.popen("nslookup "..ip.." 2>/dev/null | grep 'name =' | cut -d'=' -f2 | sed 's/\\.$//'")
if f then
local name = f:read("*l")
f:close()
if name and name ~= "" then
return name:match("^%s*(.-)%s*$")
end
end
local leases_file = io.open("/tmp/dhcp.leases", "r")
if leases_file then
for line in leases_file:lines() do
local mac, ip_lease, _, hostname = line:match("^(%S+)%s+(%S+)%s+(%S+)%s+(%S+)")
if ip_lease == ip and hostname ~= "*" then
leases_file:close()
return hostname
end
end
leases_file:close()
end
return "unknown"
end
if conn then
local leases = conn:call("dhcp", "ipv4leases", {}) or {}
for _, lease in ipairs(leases) do
if lease.ipaddr and lease.mac then
local hostname = lease.hostname or get_hostname(lease.ipaddr)
devices[#devices+1] = {
ip = lease.ipaddr,
mac = lease.mac:upper(),
hostname = hostname,
display = string.format("%s (%s) - %s", lease.ipaddr, lease.mac:upper(), hostname)
}
seen_ips[lease.ipaddr] = true
end
end
conn:close()
end
local arp_cmd = io.popen("ip -4 neigh show dev br-lan 2>/dev/null")
if arp_cmd then
for line in arp_cmd:lines() do
local ip_addr, mac = line:match("^(%S+)%s+.+%s+(%S+)%s+")
if ip_addr and mac and mac ~= "00:00:00:00:00:00" and not seen_ips[ip_addr] then
mac = mac:upper()
local hostname = get_hostname(ip_addr)
devices[#devices+1] = {
ip = ip_addr,
mac = mac,
hostname = hostname,
display = string.format("%s (%s) - %s", ip_addr, mac, hostname)
}
seen_ips[ip_addr] = true
end
end
arp_cmd:close()
end
table.sort(devices, function(a, b) return a.ip < b.ip end)
return devices
end
local devices = get_devices()
for _, dev in ipairs(devices) do
ip:value(dev.ip, dev.display)
end
dl = t:option(Value, "download", translate("Downloads"))
dl.default = '0.1'
dl.size = 4
ul = t:option(Value, "upload", translate("Uploads"))
ul.default = '0.1'
ul.size = 4
function validate_time(self, value, section)
local hh, mm, ss
hh, mm, ss = string.match (value, "^(%d?%d):(%d%d)$")
hh = tonumber (hh)
mm = tonumber (mm)
if hh and mm and hh <= 23 and mm <= 59 then
return value
else
return nil, "Time HH:MM or space"
end
end
e = t:option(Value, "timestart", translate("Start control time"))
e.placeholder = '00:00'
e.default = '00:00'
e.validate = validate_time
e.rmempty = true
e.size = 4
e = t:option(Value, "timeend", translate("Stop control time"))
e.placeholder = '00:00'
e.default = '00:00'
e.validate = validate_time
e.rmempty = true
e.size = 4
week=t:option(Value,"week",translate("Week Day(1~7)"))
week.rmempty = true
week:value('0',translate("Everyday"))
week:value(1,translate("Monday"))
week:value(2,translate("Tuesday"))
week:value(3,translate("Wednesday"))
week:value(4,translate("Thursday"))
week:value(5,translate("Friday"))
week:value(6,translate("Saturday"))
week:value(7,translate("Sunday"))
week:value('1,2,3,4,5',translate("Workday"))
week:value('6,7',translate("Rest Day"))
week.default='0'
week.size = 6
return a
@@ -0,0 +1,3 @@
<%+cbi/valueheader%>
<font class="eqosplus_status"><%=pcdata(self:cfgvalue(section) or self.default or "")%></font>
<%+cbi/valuefooter%>
@@ -0,0 +1,12 @@
<% include("cbi/map") %>
<script type="text/javascript">//<![CDATA[
XHR.poll(2, '<%=luci.dispatcher.build_url("admin", "control", "eqosplus", "status" )%>', null,
function (x, result) {
var status = document.getElementsByClassName('eqosplus_status')[0];
status.setAttribute("style", "font-weight:bold;");
status.setAttribute("color", result.status ? "green" : "red");
status.innerHTML = result.status ? '<%=translate("RUNNING")%>' : '<%=translate("NOT RUNNING")%>';
}
)
//]]>
</script>
@@ -0,0 +1,61 @@
msgid "Eqosplus"
msgstr ""
msgid "Network speed limit"
msgstr ""
msgid "Running state"
msgstr ""
msgid "Not running"
msgstr ""
msgid "Running"
msgstr ""
msgid "Users can limit the network speed for uploading/downloading through MAC, IP, and IP segments (192.168.110.00-192.168.10.200). The speed unit is MB/second."
msgstr ""
msgid "MAC/IP"
msgstr ""
msgid "Downloads"
msgstr ""
msgid "Uploads"
msgstr ""
msgid "Comment"
msgstr ""
msgid "Upload bandwidth(Mbit/s)"
msgstr ""
msgid "Download bandwidth(Mbit/s)"
msgstr ""
msgid "Start control time"
msgstr ""
msgid "Stop control time"
msgstr ""
msgid "Week Day(1~7)"
msgstr ""
msgid "Suggested feedback:"
msgstr ""
msgid "Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"
msgstr ""
msgid "Rest Day"
msgstr ""
msgid "Workday"
msgstr ""
msgid "Automatic settings"
msgstr ""
+61
View File
@@ -0,0 +1,61 @@
msgid "Eqosplus"
msgstr "定时限速"
msgid "Network speed limit"
msgstr "定时网速限制"
msgid "Running state"
msgstr "运行状态"
msgid "Not running"
msgstr "未运行"
msgid "Running"
msgstr "已运行"
msgid "Users can limit the network speed for uploading/downloading through MAC, IP.The speed unit is MB/second."
msgstr "可以通过MAC,IP限制用户上传/下载的网速。速度单位为<font color=\"red\"><b> MB/秒 </b></font>。"
msgid "MAC/IP"
msgstr "MAC/IP"
msgid "Downloads"
msgstr "下载速度"
msgid "Uploads"
msgstr "上传速度"
msgid "Comment"
msgstr "备注"
msgid "Upload bandwidth(Mbit/s)"
msgstr "上传总带宽(Mbit/s)"
msgid "Download bandwidth(Mbit/s)"
msgstr "下载总带宽(Mbit/s)"
msgid "Start control time"
msgstr "起控时间"
msgid "Stop control time"
msgstr "停控时间"
msgid "Week Day(1~7)"
msgstr "星期(1~7)"
msgid "Suggested feedback:"
msgstr "建议反馈:"
msgid "Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"
msgstr "设定用来限制网络速度的接口,拨号使用pppoe-wan,DHCP模式使用WAN口的硬件接口(如:eth1),通用使用br-lan接口"
msgid "Rest Day"
msgstr "休息日"
msgid "Workday"
msgstr "工作日"
msgid "Automatic settings"
msgstr "自动设置"
+61
View File
@@ -0,0 +1,61 @@
msgid "Eqosplus"
msgstr "定时限速"
msgid "Network speed limit"
msgstr "定时网速限制"
msgid "Running state"
msgstr "运行状态"
msgid "Not running"
msgstr "未运行"
msgid "Running"
msgstr "已运行"
msgid "Users can limit the network speed for uploading/downloading through MAC, IP, and IP segments (192.168.110.00-192.168.10.200). The speed unit is MB/second."
msgstr "用户可以通过MAC、IP、IP段(192.168.10.100-192.168.10.200),限制上传/下载的网络速度。速度单位为MB/秒。。"
msgid "MAC/IP"
msgstr "MAC/IP"
msgid "Downloads"
msgstr "下载速度"
msgid "Uploads"
msgstr "上传速度"
msgid "Comment"
msgstr "备注"
msgid "Upload bandwidth(Mbit/s)"
msgstr "上传总带宽(Mbit/s)"
msgid "Download bandwidth(Mbit/s)"
msgstr "下载总带宽(Mbit/s)"
msgid "Start control time"
msgstr "起控时间"
msgid "Stop control time"
msgstr "停控时间"
msgid "Week Day(1~7)"
msgstr "星期(1~7)"
msgid "Suggested feedback:"
msgstr "建议反馈:"
msgid "Set the interface used for restriction, use pppoe-wan for dialing, use WAN hardware interface for DHCP mode (such as eth1), and use br-lan for bypass mode"
msgstr "设定用来限制网络速度的接口,拨号使用pppoe-wan,DHCP模式使用WAN口的硬件接口(如:eth1),通用使用br-lan接口"
msgid "Rest Day"
msgstr "休息日"
msgid "Workday"
msgstr "工作日"
msgid "Automatic settings"
msgstr "自动设置"
@@ -0,0 +1,25 @@
config eqosplus
option enabled '0'
option download '1000'
option upload '30'
option ifname '1'
config device
option timestart '00:00'
option week '0'
option download '1'
option timeend '23:55'
option upload '1'
option mac '192.168.10.10'
option enable '0'
config device
option mac '192.168.10.10-192.168.10.200'
option timestart '00:00'
option timeend '00:00'
option week '0'
option download '0.1'
option upload '0.1'
option enable '0'
@@ -0,0 +1,5 @@
#!/bin/sh
[ "$ACTION" = "ifup" ] && [ "$INTERFACE" = "wan" ] && {
sleep 5
/etc/init.d/eqosplus restart
}
@@ -0,0 +1,54 @@
#!/bin/sh /etc/rc.common
#
# Copyright (C) 2025 sirpdboy herboy2008@gmail.com https://github.com/sirpdboy/luci-app-eqosplus
#
START=99
USE_PROCD=1
NAME=eqosplus
LOCK="/var/lock/$NAME.lock"
CR=/etc/crontabs/root
start_instance() {
procd_open_instance
procd_set_param command /usr/bin/eqosplusctrl
procd_set_param respawn
procd_set_param stderr 1
procd_close_instance
}
_eqosplus_start() {
if [ "$(grep -c 'option enable .1.' /etc/config/$NAME 2>/dev/null)" -gt "0" ]; then
if [ x$(uci -q get turboacc.config.sw_flow) = 'x1' ] || [ x$(uci -q get turboacc.config.sfe_flow) = 'x1' ] ;then
uci -q set turboacc.config.sw_flow='0'
uci -q set turboacc.config.sfe_flow='0'
uci -q set turboacc.config.hw_flow='0'
uci commit turboacc
/etc/init.d/turboacc restart
fi
touch $LOCK
eqosplus start
start_instance
(crontab -l ; echo "00 1 * * * /etc/init.d/eqosplus start") | sort - | uniq - | crontab -
fi
}
start_service(){
[ -f $LOCK ] && exit
stop_service
_eqosplus_start
rm -f $LOCK
}
service_triggers() {
procd_add_reload_trigger 'eqosplus'
}
stop_service(){
kill -9 $(busybox ps -w | grep 'eqosplusctrl' | grep -v 'grep' | awk '{print $1}') >/dev/null 2>&1
sed -i '/eqosplus/d' $CR >/dev/null 2>&1
rm -f $LOCK 2>/dev/null
eqosplus stop
}
@@ -0,0 +1,13 @@
#!/bin/sh
chmod +x /etc/init.d/eqosplus /usr/bin/eqosplus*
uci -q batch <<-EOF >/dev/null
delete ucitrack.@eqosplus[-1]
add ucitrack eqosplus
set ucitrack.@eqosplus[-1].init=eqosplus
commit ucitrack
EOF
[ -s /etc/config/eqosplus ] || echo "config eqosplus" > /etc/config/eqosplus
rm -f /tmp/luci-indexcache
exit 0
+379
View File
@@ -0,0 +1,379 @@
#!/bin/bash
# Copyright (C) 2006 OpenWrt.org
# Copyright 2022-2025 sirpdboy <herboy2008@gmail.com>
crrun=$1
crid=$2
NAME=eqosplus
IDLIST="/var/$NAME.idlist"
LOCK="/var/lock/$NAME.lock"
TMPID="/var/$NAME.tmpid"
LOG="/var/$NAME_log.log"
# idlist=`uci show $NAME | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' `
if [ x$(uci get $NAME.@$NAME[0].ifname 2>/dev/null) = 'x1' ]; then
ifname=$(uci -q get network.lan.ifname)
[ "x$ifname" = "x" ] && ifname="device" || ifname="ifname"
dev=$(uci -q get network.wan.$ifname)
[ ! "$dev" ] && dev=br-lan
else
dev=$(uci -q get $NAME.@$NAME[0].ifname 2>/dev/null)
[ ! "$dev" ] && dev=br-lan
fi
bin_nft=$(which nft)
bin_tc=$(which tc)
bin_ip=$(which ip)
DEBUG=1
nft_type() {
if command -v nft >/dev/null; then
nftables_ver="true"
fi
}
# Debug functions
dbg_nft() {
[ "${DEBUG:-0}" -eq 0 ] || echo "DEBUG: nft $*"
$bin_nft "$@"
}
dbg_tc() {
[ "${DEBUG:-0}" -eq 0 ] || echo "DEBUG: tc $*"
$bin_tc "$@"
}
dbg_ip() {
[ "${DEBUG:-0}" -eq 0 ] || echo "DEBUG: ip $*"
$bin_ip "$@"
}
is_macaddr() {
ret=1
if echo "$1" | grep -qE '^([0-9A-Fa-f]{2}[-:]){5}[0-9A-Fa-f]{2}$'; then
ret=0
fi
return $ret
}
# 数值比较函数
is_gt_zero() {
local value=$1
# 使用bc进行浮点数比较
echo "$value > 0" | bc -l 2>/dev/null | grep -q 1
}
# 转换为kbit
to_kbit() {
local mb=$1
# MB/s 转 kbit/s: 1 MB/s = 8192 kbit/s
echo "scale=0; $mb * 8192 / 1" | bc 2>/dev/null || echo "0"
}
# Default commands
tc="dbg_tc"
ip="dbg_ip"
nft="dbg_nft"
nft_type
stop_qos() {
for face in $(tc qdisc show 2>/dev/null | grep htb | awk '{print $5}'); do
$tc qdisc del dev $face root 2>/dev/null
done
$tc qdisc del dev ${dev} root 2>/dev/null
$tc qdisc del dev ${dev}_ifb root 2>/dev/null
$tc qdisc del dev ${dev} ingress 2>/dev/null
$ip link del dev ${dev}_ifb 2>/dev/null 2>&1
if [ -n "$nftables_ver" ]; then
$nft delete table inet ${NAME} 2>/dev/null
$nft delete table bridge ${NAME} 2>/dev/null
fi
echo "" > "$IDLIST" 2>/dev/null
}
init_qosplus() {
insmod sch_htb 2>/dev/null
insmod act_mirred 2>/dev/null
insmod ifb 2>/dev/null
$ip link del dev ${dev}_ifb 2>/dev/null
$ip link add dev ${dev}_ifb name ${dev}_ifb type ifb
$ip link set dev ${dev}_ifb up
$tc qdisc del dev ${dev} root 2>/dev/null
$tc qdisc add dev ${dev} root handle 1:0 htb default 1
$tc class add dev ${dev} parent 1:0 classid 1:1 htb rate 1000mbit ceil 1000mbit
$tc qdisc del dev ${dev}_ifb root 2>/dev/null
$tc qdisc add dev ${dev}_ifb root handle 1:0 htb default 1
$tc class add dev ${dev}_ifb parent 1:0 classid 1:1 htb rate 1000mbit ceil 1000mbit
lanipaddr=$(uci -q get network.lan.ipaddr 2>/dev/null | awk -F '.' '{print $1"."$2"."$3".0/24"}')
if [ -n "$lanipaddr" ]; then
$tc filter add dev $dev parent 1:0 protocol ip prio 1 u32 match ip src "$lanipaddr" match ip dst "$lanipaddr" flowid 1:1
$tc filter add dev ${dev}_ifb parent 1:0 protocol ip prio 1 u32 match ip src "$lanipaddr" match ip dst "$lanipaddr" flowid 1:1
fi
$tc qdisc del dev ${dev} ingress 2>/dev/null
$tc qdisc add dev ${dev} ingress
$tc filter add dev ${dev} parent ffff: protocol all prio 2 u32 match u32 0 0 flowid 1:1 action mirred egress redirect dev ${dev}_ifb
if [ -n "$nftables_ver" ]; then
$nft delete table inet ${NAME} 2>/dev/null
$nft delete table bridge ${NAME} 2>/dev/null
$nft add table inet ${NAME}
$nft add chain inet ${NAME} mark_forward { type filter hook forward priority -200\; policy accept\; }
$nft add chain inet ${NAME} mark_output { type filter hook output priority -200\; policy accept\; }
$nft add chain inet ${NAME} mark_postrouting { type filter hook postrouting priority -200\; policy accept\; }
fi
return 0
}
del_id() {
local list_id=$1
id=$((list_id * 10 + 1000))
[ "${DEBUG:-0}" -eq 0 ] || echo "D: del_id $@ --$id --$mac"
$tc filter del dev ${dev} parent 1: handle 800::$(printf "%x" $id) 2>/dev/null
$tc class del dev ${dev} parent 1:1 classid 1:$id 2>/dev/null
$tc filter del dev ${dev}_ifb parent 1: handle 800::$(printf "%x" $id) 2>/dev/null
$tc class del dev ${dev}_ifb parent 1:1 classid 1:$id 2>/dev/null
mac=$(uci -q get $NAME.@device[$list_id].mac 2>/dev/null)
if [ -n "$nftables_ver" ] && [ -n "$mac" ]; then
if is_macaddr "$mac"; then
$nft delete rule inet ${NAME} mark_forward ether saddr $mac counter 2>/dev/null
$nft delete rule inet ${NAME} mark_output ether saddr $mac counter 2>/dev/null
$nft delete rule inet ${NAME} mark_postrouting ether saddr $mac counter 2>/dev/null
else
Z=$(echo $mac | awk -F '[/]' '{print $2}')
[ -n "$Z" ] && mac=$(echo $mac | awk -F '[/]' '{print $1}') || Z=32
$nft delete rule inet ${NAME} mark_forward ip saddr $mac/$Z counter 2>/dev/null
$nft delete rule inet ${NAME} mark_output ip saddr $mac/$Z counter 2>/dev/null
$nft delete rule inet ${NAME} mark_postrouting ip saddr $mac/$Z counter 2>/dev/null
fi
fi
}
add_mac() {
local list_id=$1
id=$((list_id * 10 + 1000))
M0=$(echo $mac | cut -d : -f 1)$(echo $mac | cut -d : -f 2)
M1=$(echo $mac | cut -d : -f 3)$(echo $mac | cut -d : -f 4)
M2=$(echo $mac | cut -d : -f 5)$(echo $mac | cut -d : -f 6)
[ "${DEBUG:-0}" -eq 0 ] || echo "D: add_mac $@ --id:$id --mac:$mac M012--$M0-$M1-$M2"
if is_gt_zero "$UL"; then
if [ -n "$nftables_ver" ]; then
$nft add rule inet ${NAME} mark_forward ether saddr $mac counter meta mark set $id
$nft add rule inet ${NAME} mark_output ether saddr $mac counter meta mark set $id
$nft add rule inet ${NAME} mark_postrouting ether saddr $mac counter meta mark set $id
fi
$tc class add dev ${dev}_ifb parent 1:1 classid 1:$id htb rate "${UL}kbit" ceil "${UL}kbit" prio $id burst 15k cburst 15k
$tc qdisc add dev ${dev}_ifb parent 1:$id handle ${id}: sfq perturb 10
$tc filter add dev ${dev}_ifb parent 1: protocol ip prio $id handle $id fw flowid 1:$id
$tc filter add dev ${dev}_ifb parent 1: protocol ip prio $((id + 100)) u32 \
match u16 0x0800 0xFFFF at -2 \
match u16 0x${M2} 0xFFFF at -4 \
match u32 0x${M0}${M1} 0xFFFFFFFF at -8 \
flowid 1:$id
fi
if is_gt_zero "$DL"; then
$tc class add dev ${dev} parent 1:1 classid 1:$id htb rate "${DL}kbit" ceil "${DL}kbit" prio $id burst 15k cburst 15k
$tc qdisc add dev ${dev} parent 1:$id handle ${id}: sfq perturb 10
$tc filter add dev ${dev} parent 1: protocol ip prio $id u32 \
match u16 0x0800 0xFFFF at -2 \
match u32 0x${M1}${M2} 0xFFFFFFFF at -12 \
match u16 0x${M0} 0xFFFF at -14 \
flowid 1:$id
fi
}
add_ip() {
local list_id=$1
id=$((list_id * 10 + 1000))
[ "${DEBUG:-0}" -eq 0 ] || echo "D: add_ip $@ --$id --$mac"
Z=$(echo $mac | awk -F '[/]' '{print $2}')
[ -n "$Z" ] && mac=$(echo $mac | awk -F '[/]' '{print $1}') || Z=32
if is_gt_zero "$UL"; then
if [ -n "$nftables_ver" ]; then
$nft add rule inet ${NAME} mark_forward ip saddr $mac/$Z counter meta mark set $id
$nft add rule inet ${NAME} mark_output ip saddr $mac/$Z counter meta mark set $id
$nft add rule inet ${NAME} mark_postrouting ip saddr $mac/$Z counter meta mark set $id
fi
$tc class add dev ${dev}_ifb parent 1:1 classid 1:$id htb rate "${UL}kbit" ceil "${UL}kbit" prio $id burst 15k cburst 15k
$tc qdisc add dev ${dev}_ifb parent 1:$id handle ${id}: sfq perturb 10
$tc filter add dev ${dev}_ifb parent 1: protocol ip prio $id handle $id fw flowid 1:$id
$tc filter add dev ${dev}_ifb parent 1:0 prio $((id + 100)) protocol ip u32 \
match ip src "$mac"/"$Z" \
flowid 1:$id
fi
if is_gt_zero "$DL"; then
$tc class add dev ${dev} parent 1:1 classid 1:$id htb rate "${DL}kbit" ceil "${DL}kbit" prio $id burst 15k cburst 15k
$tc qdisc add dev ${dev} parent 1:$id handle ${id}: sfq perturb 10
$tc filter add dev ${dev} parent 1:0 prio $id protocol ip u32 \
match ip dst "$mac"/"$Z" \
flowid 1:$id
fi
}
check_time() {
local start=$1
local end=$2
local current=$(date +%H%M)
local start_min=$((10#${start:0:2}*60 + 10#${start:3:2}))
local end_min=$((10#${end:0:2}*60 + 10#${end:3:2}))
local current_min=$((10#${current:0:2}*60 + 10#${current:2:2}))
if [[ $start_min -lt $end_min ]]; then
[[ $current_min -ge $start_min && $current_min -lt $end_min ]]
else
[[ $current_min -ge $start_min || $current_min -lt $end_min ]]
fi
}
check_list() {
local i=$1
local start_time=$(uci -q get $NAME.@device[$i].timestart 2>/dev/null)
local end_time=$(uci -q get $NAME.@device[$i].timeend 2>/dev/null)
local wweek=$(uci -q get $NAME.@device[$i].week 2>/dev/null)
local current_weekday=$(date +%u)
[ -z "$start_time" ] && [ -z "$end_time" ] && [ -z "$wweek" ] && return 0
if [ -n "$wweek" ] && [ "$wweek" != "0" ]; then
local day_match=0
for day in $(echo $wweek | tr ',' ' '); do
[ "$day" -eq "$current_weekday" ] && day_match=1 && break
done
[ "$day_match" -eq 0 ] && return 1
fi
if [ -n "$start_time" ] && [ -n "$end_time" ]; then
check_time "$start_time" "$end_time" || return 1
fi
return 0
}
case "$crrun" in
"stop")
stop_qos
;;
"start")
stop_qos > /dev/null 2>&1
sleep 2
init_qosplus
idlist=$(uci show $NAME 2>/dev/null | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' | sort -nu | sed -e 's/^/!/g' -e 's/$/!/g' > $IDLIST; cat $IDLIST 2>/dev/null | sed -e 's/!//g')
if [ ! -s "$IDLIST" ]; then
return 1
fi
device_count=0
for list in $(echo $idlist | sed -e 's/!//g'); do
if check_list $list; then
mac=$(uci -q get $NAME.@device[$list].mac 2>/dev/null)
download_mb=$(uci -q get $NAME.@device[$list].download 2>/dev/null)
upload_mb=$(uci -q get $NAME.@device[$list].upload 2>/dev/null)
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
# 转换为kbit
DL=$(to_kbit "$download_mb")
UL=$(to_kbit "$upload_mb")
if [ -n "$mac" ]; then
if is_macaddr "$mac"; then
add_mac $list >>$LOG
echo "✅ MAC限速: $comment ($mac) - 下载:${download_mb}MB/s(${DL}kbit/s) 上传:${upload_mb}MB/s(${UL}kbit/s)" >>$LOG
else
add_ip $list >>$LOG
echo "✅ IP限速: $comment ($mac) - 下载:${download_mb}MB/s(${DL}kbit/s) 上传:${upload_mb}MB/s(${UL}kbit/s)" >>$LOG
fi
device_count=$((device_count + 1))
fi
else
[ -f "$IDLIST" ] && [ $(grep "!${list}!" "$IDLIST" 2>/dev/null | wc -l) -gt 0 ] && {
del_id $list >>$LOG
sed -i "/!$list!/d" "$IDLIST" >/dev/null 2>&1
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
}
fi
done
echo "EQOSPLUS限速服务启动完成,共配置 $device_count 个设备" >>$LOG
echo ""
echo "当前限速状态:" >>$LOG
tc -s class show dev $dev | grep "rate" | head -5 >>$LOG
tc -s class show dev ${dev}_ifb | grep "rate" | head -5 >>$LOG
;;
"add")
echo "➕ 添加限速规则..."
for list in $(echo $crid | sed -e 's/!//g' | sed 's/,/ /g'); do
mac=$(uci -q get $NAME.@device[$list].mac 2>/dev/null)
download_mb=$(uci -q get $NAME.@device[$list].download 2>/dev/null)
upload_mb=$(uci -q get $NAME.@device[$list].upload 2>/dev/null)
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
DL=$(to_kbit "$download_mb")
UL=$(to_kbit "$upload_mb")
if is_macaddr $mac; then
add_mac $list
echo "✅ 添加MAC限速: $comment ($mac)" >>$LOG
else
add_ip $list
echo "✅ 添加IP限速: $comment ($mac)" >>$LOG
fi
done
;;
"del")
for list in $(echo $crid | sed -e 's/!//g' | sed 's/,/ /g'); do
comment=$(uci -q get $NAME.@device[$list].comment 2>/dev/null)
mac=$(uci -q get $NAME.@device[$list].mac 2>/dev/null)
del_id $list
done
;;
"status")
echo "EQOSPLUS限速服务状态"
echo "### 网络接口: $dev ###"
echo "# 下载队列统计 #"
tc -s qdisc show dev $dev 2>/dev/null || echo "无下载队列"
echo "# 下载类统计 #"
tc -s class show dev $dev 2>/dev/null | grep -E "class htb|rate [0-9]" || echo "无下载类"
echo "# 下载过滤器 #"
tc -s filter show dev $dev parent 1: 2>/dev/null | head -10 || echo "无下载过滤器"
echo "### 虚拟接口: ${dev}_ifb ###"
echo "# 上传队列统计 #"
tc -s qdisc show dev ${dev}_ifb 2>/dev/null || echo "无上传队列"
echo "# 上传类统计 #"
tc -s class show dev ${dev}_ifb 2>/dev/null | grep -E "class htb|rate [0-9]" || echo "无上传类"
echo "# 上传过滤器 #"
tc -s filter show dev ${dev}_ifb parent 1: 2>/dev/null | head -10 || echo "无上传过滤器"
if [ -n "$nftables_ver" ]; then
echo "### NFTables规则 ###"
nft list table inet ${NAME} 2>/dev/null || echo "没有NFTables规则"
else
echo "NFTables不可用"
fi
# 显示流量统计
echo "### 流量统计 ###"
echo "下载流量:"
tc -s class show dev $dev 2>/dev/null | grep "Sent" | head -5
echo "上传流量:"
tc -s class show dev ${dev}_ifb 2>/dev/null | grep "Sent" | head -5
;;
esac
@@ -0,0 +1,63 @@
#!/bin/sh
# Copyright (C) 2006 OpenWrt.org
# Copyright 2022-2025 sirpdboy <herboy2008@gmail.com>
NAME=eqosplus
IDLIST="/var/$NAME.idlist"
TMPID="/var/$NAME.tmpid"
idlist=`uci show $NAME | grep "enable='1'" | grep "device" | grep -oE '\[.*?\]' | grep -o '[0-9]' `
check_list() {
i=$1
checki='0'
start_time=$(uci -q get $NAME.@device[$i].timestart 2>/dev/null)
end_time=$(uci -q get $NAME.@device[$i].timeend 2>/dev/null)
wweek=`uci -q get $NAME.@device[$i].week `
current_time=$(date +%H:%M)
current_weekday=$(date +%u)
for ww in `echo $wweek | sed 's/,/ /g' `; do
if [ $current_weekday = $ww ] || [ 'x0' = x$ww ] ;then
[ "$start_time" = "$end_time" ] && checki='1' || {
if [ "$start_time" \< "$end_time" ]; then
if [ "$current_time" \> "$start_time" ] && [ "$current_time" \< "$end_time" ] ; then
checki='1'
fi
else
if [ "$start_time" \> "$end_time" ] ; then
if [ "$current_time" \< "$start_time" ] && [ "$current_time" \> "$end_time" ] ; then
checki='1'
fi
fi
fi
}
fi
done
return
}
idlistusr(){
checki='0'
[ -s $IDLIST ] || touch $IDLIST
for list in $idlist ;do
check_list $list
if [ $checki == '1' ] ; then
[ `cat $IDLIST 2>/dev/null | grep "!${list}!" | wc -l ` -gt 0 ] || {
eqosplus add $list
echo "!${list}!" >> $IDLIST ; cat $IDLIST | sort | uniq > $TMPID ;cat $TMPID >$IDLIST ;rm -rf $TMPID
}
else
[ `cat $IDLIST 2>/dev/null | grep "!${list}!" | wc -l ` -gt 0 ] && {
eqosplus del $list
sed -i "/!$list!/d" $IDLIST >/dev/null 2>&1
}
fi
done
}
while :;do
sleep 30
idlistusr
sleep 30
done
@@ -0,0 +1,11 @@
{
"luci-app-eqosplus": {
"description": "Grant UCI access for luci-app-eqosplus",
"read": {
"uci": [ "eqosplus" ]
},
"write": {
"uci": [ "eqosplus" ]
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 585 KiB

@@ -3,12 +3,86 @@
-- Copyright(c): lwb1978 2025–2027
local api = require("luci.passwall.api")
local urlencode = api.UrlEncode
local base64 = api.base64Encode
local json = api.jsonc
local domain_redir = {}
local function urlencode(value)
return api.UrlEncode(value == nil and "" or tostring(value))
end
local function add_param(params, key, value)
if value ~= nil and value ~= "" then
if type(value) == "boolean" then value = value and "1" or "0" end
params[#params + 1] = key .. "=" .. urlencode(value)
end
end
local function build_ech(node)
local opts = node["ech-opts"]
if type(opts) ~= "table" or opts.enable ~= true then return nil end
if opts.config and opts.config ~= "" then return opts.config end
local name = opts["query-server-name"]
if not name or name == "" then name = node.servername or node.sni or node.server end
if name == "" then name = node.server end
return name .. "+https://223.5.5.5/dns-query"
end
local function bandwidth_mbps(value)
if value == nil then return nil end
local number = tonumber(value)
if number then return number end
local amount, prefix, unit = tostring(value):match("^%s*(%d+)%s*([KMGT]?)([Bb])ps%s*$")
local scales = { [""] = 1, K = 1000, M = 1000000, G = 1000000000, T = 1000000000000 }
if not amount then return nil end
return tonumber(amount) * scales[prefix] * (unit == "B" and 8 or 1) / 1000000
end
local function inherit(base, overrides)
local result = {}
for key, value in pairs(base or {}) do result[key] = value end
for key, value in pairs(overrides or {}) do result[key] = value end
return result
end
local function build_xhttp_extra(opts)
local extra = {}
local fields = {
headers = "headers", ["x-padding-bytes"] = "xPaddingBytes",
["no-grpc-header"] = "noGRPCHeader", ["x-padding-obfs-mode"] = "xPaddingObfsMode",
["x-padding-key"] = "xPaddingKey", ["x-padding-header"] = "xPaddingHeader",
["x-padding-placement"] = "xPaddingPlacement", ["x-padding-method"] = "xPaddingMethod",
["uplink-http-method"] = "uplinkHTTPMethod", ["session-placement"] = "sessionIDPlacement",
["session-key"] = "sessionIDKey", ["session-table"] = "sessionIDTable",
["session-length"] = "sessionIDLength", ["seq-placement"] = "seqPlacement", ["seq-key"] = "seqKey",
["uplink-data-placement"] = "uplinkDataPlacement", ["uplink-data-key"] = "uplinkDataKey",
["uplink-chunk-size"] = "uplinkChunkSize", ["sc-max-each-post-bytes"] = "scMaxEachPostBytes",
["sc-min-posts-interval-ms"] = "scMinPostsIntervalMs"
}
for source, target in pairs(fields) do extra[target] = opts[source] end
-- jsonc treats empty tables as arrays and repeated table references as null.
if type(extra.headers) == "table" then
extra.headers = next(extra.headers) and inherit(extra.headers) or nil
end
local reuse = opts["reuse-settings"]
if reuse then
local xmux = {}
local ranges = {
["max-concurrency"] = "maxConcurrency", ["max-connections"] = "maxConnections",
["c-max-reuse-times"] = "cMaxReuseTimes", ["h-max-request-times"] = "hMaxRequestTimes",
["h-max-reusable-secs"] = "hMaxReusableSecs"
}
for source, target in pairs(ranges) do
local value = reuse[source]
if value ~= nil then xmux[target] = tonumber(value) or value end
end
xmux.hKeepAlivePeriod = tonumber(reuse["h-keep-alive-period"])
if next(xmux) then extra.xmux = xmux end
end
return extra
end
local function host_format(host)
if not host then return "" end
local str = host:match("%[(.-)%]") or host
@@ -62,18 +136,12 @@ local function build_common(node)
o.tls.sni = node.servername or node.sni
o.tls.fp = node["client-fingerprint"]
o.tls.pcs = node.fingerprint
o.tls.vcn = node["name-cert-verify"]
o.tls.insecure = node["skip-cert-verify"] == true
o.tls.alpn = build_alpn(node.alpn)
local ech_opts = node["ech-opts"]
if ech_opts and ech_opts.enable == true then
if ech_opts["query-server-name"] then
o.tls.ech = ech_opts["query-server-name"] .. "+https://223.5.5.5/dns-query"
elseif ech_opts.config then
o.tls.ech = ech_opts.config
end
end
o.tls.ech = build_ech(node)
-- ===== transport =====
o.transport = {}
@@ -89,16 +157,15 @@ local function build_common(node)
local opts = node["ws-opts"]
if opts then
local path = opts.path or "/"
local ed = opts["max-early-data"]
local eh = opts["early-data-header-name"]
if ed then
path = path .. "?ed=" .. ed
if opts["v2ray-http-upgrade"] ~= true and opts["max-early-data"] then
path = path .. (path:find("?", 1, true) and "&ed=" or "?ed=") .. opts["max-early-data"]
end
if eh then
path = path .. (path:find("?", 1, true) and "&eh=" or "?eh=") .. eh
if opts["v2ray-http-upgrade"] ~= true and opts["early-data-header-name"] then
path = path .. (path:find("?", 1, true) and "&eh=" or "?eh=") .. opts["early-data-header-name"]
end
o.transport.path = path
o.transport.host = opts.headers and opts.headers.Host
if opts["v2ray-http-upgrade"] == true then o.transport.type = "httpupgrade" end
end
elseif net == "grpc" then
@@ -108,12 +175,17 @@ local function build_common(node)
end
elseif net == "http" then
-- Use TCP + HTTP camouflage so each core selects its matching transport.
o.transport.type = "tcp"
o.transport.headerType = "http"
o.transport.host = node.server
o.transport.path = "/"
local opts = node["http-opts"]
if opts then
-- Clash http-opts carries the camouflage Host under headers.Host
-- (a list), like ws-opts; opts.host is not standard for http.
o.transport.host = get_first(opts.host) or (opts.headers and get_first(opts.headers.Host))
o.transport.path = get_first(opts.path)
o.transport.host = get_first(opts.host) or (opts.headers and get_first(opts.headers.Host)) or o.transport.host
o.transport.path = get_first(opts.path) or o.transport.path
end
elseif net == "h2" then
@@ -130,85 +202,45 @@ local function build_common(node)
o.transport.path = opts.path
o.transport.mode = opts.mode
local extra = {}
-- headers
if opts.headers then
extra.headers = opts.headers
end
if opts["x-padding-bytes"] then
extra.xPaddingBytes = opts["x-padding-bytes"]
end
if opts["no-grpc-header"] ~= nil then
extra.noGRPCHeader = opts["no-grpc-header"]
end
if opts["sc-max-each-post-bytes"] then
extra.scMaxEachPostBytes = opts["sc-max-each-post-bytes"]
end
if opts["sc-min-posts-interval-ms"] then
extra.scMinPostsIntervalMs = opts["sc-min-posts-interval-ms"]
end
-- xmux
if opts["reuse-settings"] then
local r = opts["reuse-settings"]
local xmux = {}
if r["max-concurrency"] then xmux.maxConcurrency = r["max-concurrency"] end
if r["max-connections"] then xmux.maxConnections = tonumber(r["max-connections"]) end
if r["c-max-reuse-times"] then xmux.cMaxReuseTimes = tonumber(r["c-max-reuse-times"]) end
if r["h-max-request-times"] then xmux.hMaxRequestTimes = r["h-max-request-times"] end
if r["h-max-reusable-secs"] then xmux.hMaxReusableSecs = r["h-max-reusable-secs"] end
if r["h-keep-alive-period"] then xmux.hKeepAlivePeriod = tonumber(r["h-keep-alive-period"]) end
if next(xmux) then extra.xmux = xmux end
end
local extra = build_xhttp_extra(opts)
-- download-settings
if opts["download-settings"] then
local d = opts["download-settings"]
local ds = {}
if d.server then ds.address = domain_redir[d.server] or d.server end
if d.port then ds.port = d.port end
ds.network = "xhttp"
-- TLS
if d.tls then
ds.security = "tls"
ds.tlsSettings = {}
if d.servername then
ds.tlsSettings.serverName = d.servername
end
if d["skip-cert-verify"] == true then
ds.tlsSettings.allowInsecure = true
end
if d["client-fingerprint"] then
ds.tlsSettings.fingerprint = d["client-fingerprint"]
end
if d.fingerprint then
ds.tlsSettings.pinnedPeerCertSha256 = d.fingerprint
end
if d.alpn then
ds.tlsSettings.alpn = d.alpn
local effective = inherit(node, d)
local ds = {
address = domain_redir[effective.server] or effective.server,
port = effective.port,
network = "xhttp",
security = effective.tls and "tls" or "none"
}
local sni = effective.servername or effective.sni or effective.server
local alpn = build_alpn(effective.alpn)
if effective.tls or effective["reality-opts"] then
ds.tlsSettings = {
serverName = sni,
allowInsecure = effective["skip-cert-verify"] == true,
fingerprint = effective["client-fingerprint"],
pinnedPeerCertSha256 = effective.fingerprint and api.sha256_sb_xray(effective.fingerprint) or nil,
verifyPeerCertByName = effective["name-cert-verify"],
alpn = alpn and api.split(alpn, ",") or nil,
echConfigList = build_ech(effective)
}
local reality = effective["reality-opts"]
if reality then
ds.security = "reality"
ds.realitySettings = {
serverName = sni, fingerprint = effective["client-fingerprint"] or "chrome",
publicKey = reality["public-key"], shortId = reality["short-id"] or ""
}
ds.tlsSettings = nil
end
end
-- xhttpSettings
local xs = {}
if d.path then xs.path = d.path end
if d.host then xs.host = d.host end
if next(xs) then
ds.xhttpSettings = xs
end
local xs = build_xhttp_extra(inherit(opts, d))
xs.path = d.path or opts.path or "/"
xs.host = d.host or opts.host or sni
xs.mode = opts.mode or "auto"
ds.xhttpSettings = xs
extra.downloadSettings = ds
end
@@ -229,27 +261,29 @@ local function encode_vless(node)
local link = "vless://" .. node.uuid .. "@" .. o.server .. ":" .. o.port
local p = {}
if type(node.flow) == "string" and node.flow:sub(1, 5) == "xtls-" then table.insert(p, "flow=" .. urlencode(node.flow)) end
if node.encryption then table.insert(p, "encryption=" .. urlencode(node.encryption)) end
if type(node.flow) == "string" and node.flow:sub(1, 5) == "xtls-" then add_param(p, "flow", node.flow) end
add_param(p, "encryption", node.encryption)
-- TLS
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end
table.insert(p, "allowInsecure=" .. (o.tls.insecure and "1" or "0"))
add_param(p, "security", o.tls.security)
add_param(p, "pbk", o.tls.pbk)
add_param(p, "sid", o.tls.sid)
add_param(p, "sni", o.tls.sni)
add_param(p, "fp", o.tls.fp)
add_param(p, "alpn", o.tls.alpn)
add_param(p, "ech", o.tls.ech)
add_param(p, "pcs", o.tls.pcs)
add_param(p, "vcn", o.tls.vcn)
add_param(p, "allowInsecure", o.tls.insecure)
-- transport
table.insert(p, "type=" .. o.transport.type)
if o.transport.host then table.insert(p, "host=" .. urlencode(o.transport.host)) end
if o.transport.path then table.insert(p, "path=" .. urlencode(o.transport.path)) end
if o.transport.serviceName then table.insert(p, "serviceName=" .. urlencode(o.transport.serviceName)) end
if o.transport.mode then table.insert(p, "mode=" .. urlencode(o.transport.mode)) end
if o.transport.extra then table.insert(p, "extra=" .. urlencode(o.transport.extra)) end
add_param(p, "type", o.transport.type)
add_param(p, "headerType", o.transport.headerType)
add_param(p, "host", o.transport.host)
add_param(p, "path", o.transport.path)
add_param(p, "serviceName", o.transport.serviceName)
add_param(p, "mode", o.transport.mode)
add_param(p, "extra", o.transport.extra)
if #p > 0 then
link = link .. "?" .. table.concat(p, "&")
@@ -262,23 +296,24 @@ end
local function encode_trojan(node)
local o = build_common(node)
local link = "trojan://" .. node.password .. "@" .. o.server .. ":" .. o.port
local link = "trojan://" .. urlencode(node.password) .. "@" .. o.server .. ":" .. o.port
local p = {}
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end
table.insert(p, "allowInsecure=" .. (o.tls.insecure and "1" or "0"))
table.insert(p, "type=" .. o.transport.type)
if o.transport.host then table.insert(p, "host=" .. urlencode(o.transport.host)) end
if o.transport.path then table.insert(p, "path=" .. urlencode(o.transport.path)) end
if o.transport.serviceName then table.insert(p, "serviceName=" .. urlencode(o.transport.serviceName)) end
add_param(p, "security", o.tls.security)
add_param(p, "pbk", o.tls.pbk)
add_param(p, "sid", o.tls.sid)
add_param(p, "sni", o.tls.sni)
add_param(p, "fp", o.tls.fp)
add_param(p, "alpn", o.tls.alpn)
add_param(p, "ech", o.tls.ech)
add_param(p, "pcs", o.tls.pcs)
add_param(p, "vcn", o.tls.vcn)
add_param(p, "allowInsecure", o.tls.insecure)
add_param(p, "type", o.transport.type)
add_param(p, "headerType", o.transport.headerType)
add_param(p, "host", o.transport.host)
add_param(p, "path", o.transport.path)
add_param(p, "serviceName", o.transport.serviceName)
if #p > 0 then
link = link .. "?" .. table.concat(p, "&")
@@ -301,21 +336,22 @@ local function encode_vmess(node)
net = o.transport.type,
security = node.cipher,
scy = node.cipher,
type = "none",
type = o.transport.headerType or "none",
host = o.transport.host or "",
path = o.transport.path or "",
path = urlencode(o.transport.path or ""),
tls = o.tls.security == "tls" and "tls" or "",
sni = o.tls.sni,
alpn = o.tls.alpn,
fp = o.tls.fp,
ech = o.tls.ech,
pcs = o.tls.pcs,
vcn = o.tls.vcn,
insecure = o.tls.insecure and "1" or "0",
tfo = node.tfo and "1" or "0"
}
if o.transport.type == "grpc" then
obj.path = o.transport.serviceName or ""
obj.path = urlencode(o.transport.serviceName or "")
end
return 0, "vmess://" .. base64(json.stringify(obj))
@@ -337,14 +373,14 @@ local function encode_ss(node)
local p = {}
if node.udp then table.insert(p, "udp=1") end
if node["udp-over-tcp"] then table.insert(p, "uot=1") end
if node.udp then add_param(p, "udp", true) end
if node["udp-over-tcp"] then add_param(p, "uot", true) end
if node.plugin then
local plugin = (node.plugin == "obfs") and "obfs-local" or node.plugin
if plugin == "shadow-tls" then
local shadow_tls = base64(json.stringify(node["plugin-opts"] or {}))
table.insert(p, "shadow-tls=" .. urlencode(shadow_tls))
add_param(p, "shadow-tls", shadow_tls)
elseif plugin == "obfs-local" or plugin == "v2ray-plugin" then
local opts = {}
for k, v in pairs(node["plugin-opts"] or {}) do
@@ -369,7 +405,7 @@ local function encode_ss(node)
end
end
if #opts > 0 then plugin = plugin .. ";" .. table.concat(opts, ";") end
table.insert(p, "plugin=" .. urlencode(plugin))
add_param(p, "plugin", plugin)
else
return 1, "订阅转换 → 丢弃 SS 节点:" .. (node.name or "") .. ",因 Core 不支持 " .. plugin .. " 插件"
end
@@ -383,22 +419,24 @@ local function encode_ss(node)
end
-- Hysteria
local function encode_hysteria2(node)
local function encode_hysteria(node)
if node.protocol and node.protocol ~= "udp" then
return 1, "订阅转换 → 丢弃 Hysteria 节点:" .. node.name .. ",当前 Core 不支持 " .. node.protocol .. " 传输"
end
local link = "hysteria://" .. host_format(node.server) .. ":" .. (node.port or "")
local p = {}
if node["auth-str"] then table.insert(p, "auth=" .. node["auth-str"]) end
if node["ports"] then table.insert(p, "mport=" .. node["ports"]) end
if node.obfs then table.insert(p, "obfsParam=" .. node.obfs) end
if node.sni then table.insert(p, "sni=" .. node.sni) end
if node.up then table.insert(p, "upmbps=" .. node.up) end
if node.down then table.insert(p, "downmbps=" .. node.down) end
if node["skip-cert-verify"] then table.insert(p, "insecure=1") end
if node["fingerprint"] then table.insert(p, "pinSHA256=" .. urlencode(node["fingerprint"])) end
add_param(p, "auth", node["auth-str"])
add_param(p, "mport", node.ports)
add_param(p, "obfsParam", node.obfs)
add_param(p, "sni", node.sni)
add_param(p, "upmbps", bandwidth_mbps(node.up))
add_param(p, "downmbps", bandwidth_mbps(node.down))
add_param(p, "insecure", node["skip-cert-verify"] == true)
add_param(p, "vcn", node["name-cert-verify"])
add_param(p, "pinSHA256", node.fingerprint)
if node.alpn then
table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn)))
end
add_param(p, "alpn", build_alpn(node.alpn))
if #p > 0 then
link = link .. "?" .. table.concat(p, "&")
@@ -409,20 +447,24 @@ end
-- Hysteria2
local function encode_hysteria2(node)
local link = "hysteria2://" .. (node.password or "") .. "@" .. host_format(node.server) .. ":" .. (node.port or "")
-- Hopping subscriptions may omit port; keep a valid fallback for all cores.
local port = node.port or tostring(node.ports or ""):match("^%s*(%d+)")
local link = "hysteria2://" .. urlencode(node.password or "") .. "@" .. host_format(node.server) .. ":" .. port
local p = {}
if node["ports"] then table.insert(p, "mport=" .. urlencode(node["ports"])) end
if node.obfs then table.insert(p, "obfs=" .. node.obfs) end
if node["obfs-password"] then table.insert(p, "obfs-password=" .. node["obfs-password"]) end
if node["obfs-min-packet-size"] then table.insert(p, "minPacketSize=" .. node["obfs-min-packet-size"]) end
if node["obfs-max-packet-size"] then table.insert(p, "maxPacketSize=" .. node["obfs-max-packet-size"]) end
if node.up then table.insert(p, "upmbps=" .. node.up) end
if node.down then table.insert(p, "downmbps=" .. node.down) end
if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end
if node["skip-cert-verify"] then table.insert(p, "insecure=1") end
if node["fingerprint"] then table.insert(p, "pinSHA256=" .. urlencode(node["fingerprint"])) end
add_param(p, "mport", node.ports)
add_param(p, "hopInterval", node["hop-interval"])
add_param(p, "obfs", node.obfs)
add_param(p, "obfs-password", node["obfs-password"])
add_param(p, "minPacketSize", node["obfs-min-packet-size"])
add_param(p, "maxPacketSize", node["obfs-max-packet-size"])
add_param(p, "upmbps", bandwidth_mbps(node.up))
add_param(p, "downmbps", bandwidth_mbps(node.down))
add_param(p, "ech", build_ech(node))
add_param(p, "vcn", node["name-cert-verify"])
add_param(p, "sni", node.sni)
add_param(p, "insecure", node["skip-cert-verify"] == true)
add_param(p, "pinSHA256", node.fingerprint)
if #p > 0 then
link = link .. "?" .. table.concat(p, "&")
@@ -433,22 +475,23 @@ end
-- TUIC
local function encode_tuic(node)
local link = "tuic://" .. node.uuid .. ":" .. node.password .. "@" .. host_format(node.server) .. ":" .. node.port
if node.token then
return 1, "订阅转换 → 丢弃 TUIC 节点:" .. node.name .. ",当前 Core 不支持 TUIC v4"
end
if not node.uuid or node.password == nil then
return 1, "订阅转换 → 丢弃 TUIC 节点:" .. node.name .. ",认证信息不完整。"
end
local link = "tuic://" .. urlencode(node.uuid) .. ":" .. urlencode(node.password) .. "@" .. host_format(node.server) .. ":" .. node.port
local p = {}
if node["congestion-controller"] then
table.insert(p, "congestion_control=" .. node["congestion-controller"])
end
if node.alpn then
table.insert(p, "alpn=" .. urlencode(build_alpn(node.alpn)))
end
if node.sni then table.insert(p, "sni=" .. urlencode(node.sni)) end
if node["disable-sni"] then table.insert(p, "disable_sni=1") end
if node["skip-cert-verify"] then table.insert(p, "allowInsecure=1") end
if node["udp-relay-mode"] then table.insert(p, "udp_relay_mode=" .. node["udp-relay-mode"]) end
if node["fingerprint"] then table.insert(p, "pcs=" .. urlencode(node["fingerprint"])) end
add_param(p, "congestion_control", node["congestion-controller"])
add_param(p, "alpn", build_alpn(node.alpn))
add_param(p, "sni", node.sni)
if node["disable-sni"] then add_param(p, "disable_sni", true) end
add_param(p, "allowInsecure", node["skip-cert-verify"] == true)
add_param(p, "udp_relay_mode", node["udp-relay-mode"])
add_param(p, "pcs", node.fingerprint)
add_param(p, "vcn", node["name-cert-verify"])
if #p > 0 then
link = link .. "?" .. table.concat(p, "&")
@@ -474,18 +517,18 @@ local function encode_anytls(node)
local o = build_common(node)
local link = "anytls://" .. (node.password or "") .. "@" .. host_format(node.server) .. ":" .. node.port
local link = "anytls://" .. urlencode(node.password or "") .. "@" .. host_format(node.server) .. ":" .. node.port
local p = {}
if o.tls.security then table.insert(p, "security=" .. o.tls.security) end
if o.tls.pbk then table.insert(p, "pbk=" .. urlencode(o.tls.pbk)) end
if o.tls.sid then table.insert(p, "sid=" .. urlencode(o.tls.sid)) end
if o.tls.sni then table.insert(p, "sni=" .. urlencode(o.tls.sni)) end
if o.tls.alpn then table.insert(p, "alpn=" .. urlencode(o.tls.alpn)) end
if o.tls.fp then table.insert(p, "fp=" .. urlencode(o.tls.fp)) end
if o.tls.ech then table.insert(p, "ech=" .. urlencode(o.tls.ech)) end
if o.tls.pcs then table.insert(p, "pcs=" .. urlencode(o.tls.pcs)) end
table.insert(p, "insecure=" .. (o.tls.insecure and "1" or "0"))
add_param(p, "security", o.tls.security)
add_param(p, "pbk", o.tls.pbk)
add_param(p, "sid", o.tls.sid)
add_param(p, "sni", o.tls.sni)
add_param(p, "alpn", o.tls.alpn)
add_param(p, "fp", o.tls.fp)
add_param(p, "ech", o.tls.ech)
add_param(p, "pcs", o.tls.pcs)
add_param(p, "insecure", o.tls.insecure)
if #p > 0 then
link = link .. "?" .. table.concat(p, "&")
@@ -514,8 +557,8 @@ end
-- snell
local function encode_snell(node)
local err_msg
local obfs = node["obfs-opts"]
if obfs and obfs.mode ~= "http" and obfs.mode ~= "none" then
local obfs = node["obfs-opts"] or {}
if obfs.mode and obfs.mode ~= "http" and obfs.mode ~= "none" then
err_msg = obfs.mode
end
if err_msg then
@@ -532,13 +575,13 @@ local function encode_snell(node)
local link = "snell://" .. host_format(node.server) .. ":" .. node.port
local p = {}
if node.psk then table.insert(p, "psk=" .. urlencode(node.psk)) end
table.insert(p, "version=" .. version)
add_param(p, "psk", node.psk)
add_param(p, "version", version)
if obfs.mode == "http" then
table.insert(p, "obfs=http")
if obfs.host then table.insert(p, "obfs-host=" .. urlencode(obfs.host)) end
add_param(p, "obfs", "http")
add_param(p, "obfs-host", obfs.host)
end
table.insert(p, "reuse=" .. (node.reuse and "1" or "0"))
add_param(p, "reuse", node.reuse and "1" or "0")
if #p > 0 then
link = link .. "?" .. table.concat(p, "&")
@@ -548,9 +591,22 @@ local function encode_snell(node)
end
local function encode_node(node)
if (not node.type) or (not node.name) then return nil end
if type(node) ~= "table" or not node.type or not node.name then
return 1, "订阅转换 → 跳过格式不完整的节点。"
end
if not node.server or node.server == "" or not (node.port or (node.type == "hysteria2" and node.ports)) then
return 1, "订阅转换 → 丢弃节点:" .. tostring(node.name) .. ",节点信息不完整。"
end
node = inherit(node)
local t = node.type
if t == "hysteria" or t == "hysteria2" then
local up, down = bandwidth_mbps(node.up), bandwidth_mbps(node.down)
if (node.up ~= nil and up == nil) or (node.down ~= nil and down == nil) then
return 1, "订阅转换 → 丢弃节点:" .. tostring(node.name) .. ",带宽格式不正确。"
end
node.up, node.down = up, down
end
node.server = domain_redir[node.server] or node.server
@@ -570,17 +626,22 @@ end
function parseClashNode(raw, remark)
if not raw then return "" end
domain_redir = {}
local ok, lyaml = pcall(require, "lyaml")
if not ok then return raw end
local data = lyaml.load(raw)
local parsed, data = pcall(lyaml.load, raw)
if not parsed then
api.log('订阅转换 → YAML 解析失败:' .. tostring(remark or ""))
return ""
end
if not data or type(data) ~= "table" then return raw end
if not data.proxies then return "" end
if type(data.proxies) ~= "table" then return "" end
api.log('检测到 Clash 订阅,正在进行转换 ...')
-- Some airports use hosts domain redirect to disguise the real node domain name.
for k, v in pairs(data.hosts or {}) do
for k, v in pairs(type(data.hosts) == "table" and data.hosts or {}) do
if type(k) == "string" and type(v) == "string" then
if api.datatypes.hostname(k) and api.datatypes.hostname(v) then
domain_redir[k] = v
@@ -590,8 +651,11 @@ function parseClashNode(raw, remark)
local links = {}
for _, node in ipairs(data.proxies) do
local err, link = encode_node(node)
if err == 1 and link then
local converted, err, link = pcall(encode_node, node)
if not converted then
local name = type(node) == "table" and node.name or ""
api.log('订阅转换 → 丢弃节点:' .. tostring(name or "") .. ',转换失败,已跳过。')
elseif err == 1 and link then
api.log(link)
elseif link then
table.insert(links, link)
@@ -130,9 +130,13 @@ end
local nodeResult = {} -- update result
local isDebug = false
local log_buffer = nil
local log = function(...)
if isDebug == true then
if log_buffer then
local n = select("#", ...)
tinsert(log_buffer, n == 1 and (...) or table.concat({...}, " "))
elseif isDebug == true then
local result = os.date("%Y-%m-%d %H:%M:%S: ") .. table.concat({...}, " ")
print(result)
else
@@ -140,6 +144,14 @@ local log = function(...)
end
end
local function flush_log_buffer()
local buffered = log_buffer
log_buffer = nil
for _, msg in ipairs(buffered or {}) do
log(msg)
end
end
local nodes_table = {}
for k, e in ipairs(api.get_valid_nodes()) do
if e.node_type == "normal" then
@@ -885,6 +897,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.method = method
result.ss_method = method
result.password = password
result.uot = params.uot
result.tcp_fast_open = params.tfo
result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil
result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil
@@ -1039,7 +1052,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
end
local insecure = params.allowinsecure or params.allowInsecure or params.insecure
result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0")
result.uot = params.udp
result.uot = params.uot or params.udp
else
result.error_msg = "请更换 Xray 或 Sing-Box 来支持 SS 更多的传输方式。"
end
@@ -1420,7 +1433,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
for _, v in pairs(split(query[2], '&')) do
local s = v:find("=", 1, true)
if s and s > 1 then
params[v:sub(1, s - 1)] = v:sub(s + 1)
params[v:sub(1, s - 1)] = UrlDecode(v:sub(s + 1))
end
end
-- [2001:4860:4860::8888]:443
@@ -1447,6 +1460,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.hysteria_up_mbps = params.upmbps or sub_hy_up_mbps
result.hysteria_down_mbps = params.downmbps or sub_hy_down_mbps
result.hysteria_hop = params.mport
result.tls_CertByName = params.vcn
elseif szType == 'hysteria2' or szType == 'hy2' then
local alias = ""
@@ -1490,6 +1504,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
result.hysteria2_up_mbps = params.upmbps or (sub_cfg and sub_hy_up_mbps or nil)
result.hysteria2_down_mbps = params.downmbps or (sub_cfg and sub_hy_down_mbps or nil)
result.hysteria2_hop = params.mport
result.hysteria2_hop_interval = params.hopinterval or params["hop-interval"]
if params.ech and params.ech ~= "" then
result.ech = "1"
result.ech_config = params.ech
@@ -1569,6 +1584,7 @@ local function processData(szType, content, add_mode, group, sub_cfg)
end
result.tls_serverName = params.sni
result.tls_disable_sni = params.disable_sni
result.tls_CertByName = params.vcn
result.tls_pinSHA256 = params.pcs or params.pinsha256
result.tuic_alpn = params.alpn or "h3"
result.tuic_congestion_control = params.congestion_control or "cubic"
@@ -1994,10 +2010,29 @@ local function select_node(nodes, config, parentConfig)
end
end
-- 账户信息只在实际变化时写回,由调用方统一提交。
local function update_subscribe_info()
local changed = {}
for cfgid, info in pairs(subscribe_info) do
for key, value in pairs(info) do
value = value and value ~= "" and value or nil
if uci_get(cfgid, key) ~= value then
if value then
uci_set(cfgid, key, value)
else
uci_del(cfgid, key)
end
changed[cfgid] = true
end
end
end
return changed
end
local function update_node(manual)
if next(nodeResult) == nil then
log("没有可用的节点信息更新。")
return
return false
end
local group = {}
@@ -2087,17 +2122,6 @@ local function update_node(manual)
end
end
end
-- 更新机场信息
for cfgid, info in pairs(subscribe_info) do
for key, value in pairs(info) do
if value ~= "" then
uci_set(cfgid, key, value)
else
uci_del(cfgid, key)
end
end
end
if next(CONFIG) then
local nodes = {}
uci_foreach("nodes", function(node)
@@ -2119,9 +2143,7 @@ local function update_node(manual)
end
end
uci_save(true)
if manual ~= 1 then restart_required = true end
return true
end
local function parse_link(raw, add_mode, group, sub_cfg)
@@ -2187,7 +2209,8 @@ local function parse_link(raw, add_mode, group, sub_cfg)
log('丢弃节点:' .. result.remarks .. " ,原因:" .. result.error_msg)
elseif not result.type then
log('丢弃节点:' .. result.remarks .. " ,找不到可使用二进制。")
elseif (add_mode == "2" and is_filter_keyword(keyword_filter, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or
-- 过滤无效物理网络节点(回环、全零等非网络出站卡片)
elseif (add_mode == "2" and is_filter_keyword(keyword_filter, result.remarks)) or not result.address or result.remarks == "NULL" or result.address == "127.0.0.1" or result.address == "0.0.0.0" or
(not datatypes.hostname(result.address) and not (api.is_ip(result.address))) then
log('丢弃过滤节点:' .. result.type .. ' 节点,' .. result.remarks)
else
@@ -2205,17 +2228,21 @@ local function parse_link(raw, add_mode, group, sub_cfg)
end
end
if #node_list > 0 then
nodeResult[#nodeResult + 1] = {
remark = group,
list = node_list,
sub_cfg = sub_cfg
}
if add_mode ~= "2" then
nodeResult[#nodeResult + 1] = {
remark = group,
list = node_list,
sub_cfg = sub_cfg
}
end
end
log('成功解析【' .. group .. '】节点数量:' .. #node_list)
return node_list
else
if add_mode == "2" then
log('获取到的【' .. group .. '】订阅内容为空,可能是订阅地址无效,或是网络问题,请诊断!')
end
return {}
end
end
@@ -2266,15 +2293,32 @@ local execute = function()
local stdout = f:read("*all")
f:close()
local raw_data = api.trim(stdout)
raw_data = parseClashNode(raw_data)
-- 优先提取标准 Clash 订阅响应头中的账户用量与到期时间(不受节点变化与否影响)
if headers and headers ~= "" then
subscribe_info[cfgid] = parse_clash_sub_info(headers)
end
-- 开启日志事务缓冲:前置解析期间日志暂存,未变更时静默丢弃,避免定时任务刷屏
log_buffer = {}
local parsed_nodes = parse_link(raw_data, "2", remark, value)
local old_md5 = value.md5 or ""
local new_md5 = api.md5_file(tmp_file)
local new_md5 = #parsed_nodes > 0 and api.md5_string(jsonStringify(parsed_nodes)) or ""
-- 仅当解析出有效节点且节点集合与上次完全一致时,跳过节点重写与服务重启
if not manual_sub and new_md5 ~= "" and old_md5 == new_md5 then
log_buffer = nil -- 无物理变动,直接丢弃暂存的丢弃与计数明细
log('订阅:【' .. remark .. '】没有变化,无需更新。')
else
raw_data = parseClashNode(raw_data)
subscribe_info[cfgid] = parse_clash_sub_info(headers)
parse_link(raw_data, "2", remark, value)
uci_set(cfgid, "md5", new_md5)
-- 节点发生变动时,回放前置解析期间的节点统计与丢弃日志
flush_log_buffer()
-- 暂存有效节点及其 MD5,完成节点更新后再一起写入配置。
if #parsed_nodes > 0 then
nodeResult[#nodeResult + 1] = {
remark = remark,
list = parsed_nodes,
sub_cfg = value,
md5 = new_md5
}
end
end
else
fail_list[#fail_list + 1] = value
@@ -2284,19 +2328,33 @@ local execute = function()
end
end
end
if #fail_list > 0 then
for index, value in ipairs(fail_list) do
log(string.format('【%s】订阅失败,可能是订阅地址无效,或是网络问题,请诊断![%s]', (value.remark or ""), tostring(value.http_code or 0)))
end
end
update_node(0)
local nodes_changed = update_node(0)
local info_changed = update_subscribe_info()
-- 节点、引用关系和 MD5 必须在同一次提交中保存。
for _, result in ipairs(nodeResult) do
uci_set(result.sub_cfg[".name"], "md5", result.md5)
end
if nodes_changed or next(info_changed) then
uci_save(true)
restart_required = nodes_changed
for _, value in ipairs(subscribe_list) do
if info_changed[value[".name"]] then
log(string.format('刷新订阅【%s】账户用量与到期时间信息。', (value.remark or "")))
end
end
end
end
end
if arg[1] == "start" then
log('开始订阅...')
xpcall(execute, function(e)
flush_log_buffer()
log(e)
if type(debug) == "table" and type(debug.traceback) == "function" then
log(debug.traceback())
@@ -2309,7 +2367,7 @@ elseif arg[1] == "add" then
local raw = f:read('*all')
f:close()
parse_link(raw, "1", arg[2])
update_node(1)
if update_node(1) then uci_save(true) end
api.remove("/tmp/links.conf")
elseif arg[1] == "truncate" then
truncate_nodes(arg[2])
+2 -2
View File
@@ -21,13 +21,13 @@ define Download/geoip
HASH:=4ff4b2ec242a9e87b296ba27ddb85f8311e8d3e071eaebac5be9231c60e408dd
endef
GEOSITE_VER:=20261019032359
GEOSITE_VER:=20261019074502
GEOSITE_FILE:=dlc.dat.$(GEOSITE_VER)
define Download/geosite
URL:=https://github.com/v2fly/domain-list-community/releases/download/$(GEOSITE_VER)/
URL_FILE:=dlc.dat
FILE:=$(GEOSITE_FILE)
HASH:=1092fd1a58deccd964e92b5ff05ce01ecabaf67b8242ade8bdc6b6b35821d402
HASH:=37c030c086c57da4d0987c6be836f8ae4f8957cd904cfa0bc6bb6036bb61c816
endef
GEOSITE_IRAN_VER:=202610120242
+8 -1
View File
@@ -131,7 +131,7 @@ jobs:
rm -rf /etc/apt/sources.list.d/*
fi
apt-get update -o Acquire::Check-Valid-Until=false || apt-get update
apt-get install -y --no-install-recommends dwarves || apt-get install -y dwarves || apt-get install -y pahole
apt-get install -y --no-install-recommends dwarves llvm || apt-get install -y dwarves llvm || apt-get install -y pahole
rm -rf /var/lib/apt/lists/*
elif command -v apk >/dev/null 2>&1; then
apk add --no-cache dwarves
@@ -149,6 +149,13 @@ jobs:
mkdir -p /builder/staging_dir/host/bin
ln -sf "$pahole_bin" /builder/staging_dir/host/bin/pahole
# Ensure real llvm-objcopy is accessible within OpenWrt staging_dir host tools
llvm_objcopy_bin="$(command -v llvm-objcopy || find /usr/bin -name 'llvm-objcopy*' 2>/dev/null | head -n 1 || true)"
if [ -n "$llvm_objcopy_bin" ]; then
ln -sf "$llvm_objcopy_bin" /builder/staging_dir/host/bin/llvm-objcopy
echo "Using llvm-objcopy: $llvm_objcopy_bin"
fi
# Fix permissions and compile as buildbot user
chown -R 1000:1000 /builder /artifacts
su -s /bin/bash buildbot -c '\''
+7 -15
View File
@@ -35,14 +35,11 @@ define Package/vmlinux-btf/prerm
#!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || rm -f /usr/lib/debug/boot/vmlinux-$$(uname -r)
endef
# Toolchain and helpers
BTF_CC ?= $(if $(TARGET_CC_NOCACHE),$(TARGET_CC_NOCACHE),$(TARGET_CROSS)gcc)
BTF_PAHOLE ?= $(firstword \
$(wildcard $(STAGING_DIR_HOST)/bin/pahole /usr/bin/pahole /usr/local/bin/pahole) \
$(shell command -v pahole 2>/dev/null) \
pahole)
PAHOLE_JOBS := $(if $(filter -j%,$(PKG_JOBS)),$(patsubst -j%,%,$(filter -j%,$(PKG_JOBS))),1)
KERNEL_RELEASE := $(if $(wildcard $(LINUX_DIR)/include/config/kernel.release),$(shell cat $(LINUX_DIR)/include/config/kernel.release),$(LINUX_VERSION))
BTF_CFLAGS := \
@@ -81,21 +78,16 @@ define Build/Compile
exit 1; \
fi
# Compile BTF stub sources
$(BTF_CC) $(BTF_CFLAGS) -c $(PKG_BUILD_DIR)/shadow_btf_core.c -o $(PKG_BUILD_DIR)/shadow_btf_core.o
-$(BTF_CC) $(BTF_CFLAGS) -c $(PKG_BUILD_DIR)/shadow_btf_modules.c -o $(PKG_BUILD_DIR)/shadow_btf_modules.o 2>/dev/null
-$(BTF_CC) $(BTF_CFLAGS) -c $(PKG_BUILD_DIR)/shadow_btf_private.c -o $(PKG_BUILD_DIR)/shadow_btf_private.o 2>/dev/null
# Link compiled objects into a single relocatable object
objs="$(PKG_BUILD_DIR)/shadow_btf_core.o"; \
[ ! -f "$(PKG_BUILD_DIR)/shadow_btf_modules.o" ] || objs="$$objs $(PKG_BUILD_DIR)/shadow_btf_modules.o"; \
[ ! -f "$(PKG_BUILD_DIR)/shadow_btf_private.o" ] || objs="$$objs $(PKG_BUILD_DIR)/shadow_btf_private.o"; \
$(TARGET_CROSS)ld -r -o $(PKG_BUILD_DIR)/shadow_btf.o $$objs
# Extract detached BTF
$(BTF_PAHOLE) --jobs=$(PAHOLE_JOBS) \
--btf_encode_detached=$(PKG_BUILD_DIR)/vmlinux-btf \
$(PKG_BUILD_DIR)/shadow_btf.o
$(TARGET_CROSS)ld -r -o $(PKG_BUILD_DIR)/shadow_btf.o $(PKG_BUILD_DIR)/shadow_btf_*.o
if $(BTF_PAHOLE) --help 2>&1 | grep -q -- '--btf_encode_detached'; then \
$(BTF_PAHOLE) --btf_encode_detached=$(PKG_BUILD_DIR)/vmlinux-btf $(PKG_BUILD_DIR)/shadow_btf.o; \
else \
$(BTF_PAHOLE) -J $(PKG_BUILD_DIR)/shadow_btf.o && \
$(TARGET_CROSS)objcopy --dump-section .BTF=$(PKG_BUILD_DIR)/vmlinux-btf $(PKG_BUILD_DIR)/shadow_btf.o; \
fi
test -s $(PKG_BUILD_DIR)/vmlinux-btf
$(BTF_PAHOLE) -C sk_buff $(PKG_BUILD_DIR)/shadow_btf.o >/dev/null
endef