diff --git a/luci-app-eqosplus b/luci-app-eqosplus deleted file mode 160000 index c7ea61c9..00000000 --- a/luci-app-eqosplus +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c7ea61c971f123b59282131ea5b3438e39118dae diff --git a/luci-app-eqosplus/.github/workflows/main.yml b/luci-app-eqosplus/.github/workflows/main.yml new file mode 100644 index 00000000..3258ec29 --- /dev/null +++ b/luci-app-eqosplus/.github/workflows/main.yml @@ -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" diff --git a/luci-app-eqosplus/LICENSE b/luci-app-eqosplus/LICENSE new file mode 100644 index 00000000..574c7ac3 --- /dev/null +++ b/luci-app-eqosplus/LICENSE @@ -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. diff --git a/luci-app-eqosplus/Makefile b/luci-app-eqosplus/Makefile new file mode 100644 index 00000000..91e501b7 --- /dev/null +++ b/luci-app-eqosplus/Makefile @@ -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 + +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 + diff --git a/luci-app-eqosplus/README.md b/luci-app-eqosplus/README.md new file mode 100644 index 00000000..d2707f57 --- /dev/null +++ b/luci-app-eqosplus/README.md @@ -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) + +

+
luci-app-eqosplus
+

+ +

+OpenWrt +Chrome +Safari +Firefox + GitHub release +Downloads +

+ +[中文] | [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 + +| 图飞了 | 图飞了 | +| :-----------------: | :-------------: | +|![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) | + + + no + + +![hello](https://visitor-badge-deno.deno.dev/sirpdboy.sirpdboy.svg) [![](https://img.shields.io/badge/TGGroup-ClickJoin-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ) diff --git a/luci-app-eqosplus/README_CN.md b/luci-app-eqosplus/README_CN.md new file mode 100644 index 00000000..8da16627 --- /dev/null +++ b/luci-app-eqosplus/README_CN.md @@ -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) + +

+
luci-app-eqosplus
+

+ +

+ + + + +

+ +[中文] | [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) + +| 图飞了 | 图飞了 | +| :-----------------: | :-------------: | +|![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/支付宝.png) | ![xm1](https://raw.githubusercontent.com/sirpdboy/openwrt/master/doc/微信.png) | + + + 图飞了 + + +![](https://visitor-badge-deno.deno.dev/sirpdboy.sirpdboy.svg) [![](https://img.shields.io/badge/TG群-点击加入-FFFFFF.svg)](https://t.me/joinchat/AAAAAEpRF88NfOK5vBXGBQ) + diff --git a/luci-app-eqosplus/doc/eqosplus0.png b/luci-app-eqosplus/doc/eqosplus0.png new file mode 100644 index 00000000..978f4abd Binary files /dev/null and b/luci-app-eqosplus/doc/eqosplus0.png differ diff --git a/luci-app-eqosplus/doc/eqosplus1.png b/luci-app-eqosplus/doc/eqosplus1.png new file mode 100644 index 00000000..c603fcda Binary files /dev/null and b/luci-app-eqosplus/doc/eqosplus1.png differ diff --git a/luci-app-eqosplus/luasrc/controller/eqosplus.lua b/luci-app-eqosplus/luasrc/controller/eqosplus.lua new file mode 100644 index 00000000..cbe01b70 --- /dev/null +++ b/luci-app-eqosplus/luasrc/controller/eqosplus.lua @@ -0,0 +1,19 @@ +module("luci.controller.eqosplus", package.seeall) +-- Copyright 2022-2023 sirpdboy +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 diff --git a/luci-app-eqosplus/luasrc/model/cbi/eqosplus.lua b/luci-app-eqosplus/luasrc/model/cbi/eqosplus.lua new file mode 100644 index 00000000..32c3433c --- /dev/null +++ b/luci-app-eqosplus/luasrc/model/cbi/eqosplus.lua @@ -0,0 +1,190 @@ +-- Copyright 2022-2023 sirpdboy +-- 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 diff --git a/luci-app-eqosplus/luasrc/view/eqosplus/eqosplus.htm b/luci-app-eqosplus/luasrc/view/eqosplus/eqosplus.htm new file mode 100644 index 00000000..4445143f --- /dev/null +++ b/luci-app-eqosplus/luasrc/view/eqosplus/eqosplus.htm @@ -0,0 +1,3 @@ +<%+cbi/valueheader%> +<%=pcdata(self:cfgvalue(section) or self.default or "")%> +<%+cbi/valuefooter%> \ No newline at end of file diff --git a/luci-app-eqosplus/luasrc/view/eqosplus/index.htm b/luci-app-eqosplus/luasrc/view/eqosplus/index.htm new file mode 100644 index 00000000..c4b8a072 --- /dev/null +++ b/luci-app-eqosplus/luasrc/view/eqosplus/index.htm @@ -0,0 +1,12 @@ +<% include("cbi/map") %> + \ No newline at end of file diff --git a/luci-app-eqosplus/po/templates/eqosplus.pot b/luci-app-eqosplus/po/templates/eqosplus.pot new file mode 100644 index 00000000..151cee8d --- /dev/null +++ b/luci-app-eqosplus/po/templates/eqosplus.pot @@ -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 "" + diff --git a/luci-app-eqosplus/po/zh-cn/eqosplus.po b/luci-app-eqosplus/po/zh-cn/eqosplus.po new file mode 100644 index 00000000..0fa56393 --- /dev/null +++ b/luci-app-eqosplus/po/zh-cn/eqosplus.po @@ -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限制用户上传/下载的网速。速度单位为 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 "自动设置" + diff --git a/luci-app-eqosplus/po/zh_Hans/eqosplus.po b/luci-app-eqosplus/po/zh_Hans/eqosplus.po new file mode 100644 index 00000000..840559fc --- /dev/null +++ b/luci-app-eqosplus/po/zh_Hans/eqosplus.po @@ -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 "自动设置" + diff --git a/luci-app-eqosplus/root/etc/config/eqosplus b/luci-app-eqosplus/root/etc/config/eqosplus new file mode 100644 index 00000000..722434ae --- /dev/null +++ b/luci-app-eqosplus/root/etc/config/eqosplus @@ -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' + diff --git a/luci-app-eqosplus/root/etc/hotplug.d/iface/10-eqosplus b/luci-app-eqosplus/root/etc/hotplug.d/iface/10-eqosplus new file mode 100644 index 00000000..91059ec8 --- /dev/null +++ b/luci-app-eqosplus/root/etc/hotplug.d/iface/10-eqosplus @@ -0,0 +1,5 @@ +#!/bin/sh +[ "$ACTION" = "ifup" ] && [ "$INTERFACE" = "wan" ] && { + sleep 5 + /etc/init.d/eqosplus restart +} \ No newline at end of file diff --git a/luci-app-eqosplus/root/etc/init.d/eqosplus b/luci-app-eqosplus/root/etc/init.d/eqosplus new file mode 100644 index 00000000..f74b5fdd --- /dev/null +++ b/luci-app-eqosplus/root/etc/init.d/eqosplus @@ -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 +} + diff --git a/luci-app-eqosplus/root/etc/uci-defaults/luci-eqosplus b/luci-app-eqosplus/root/etc/uci-defaults/luci-eqosplus new file mode 100644 index 00000000..f22cd0c6 --- /dev/null +++ b/luci-app-eqosplus/root/etc/uci-defaults/luci-eqosplus @@ -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 diff --git a/luci-app-eqosplus/root/usr/bin/eqosplus b/luci-app-eqosplus/root/usr/bin/eqosplus new file mode 100644 index 00000000..721fafa0 --- /dev/null +++ b/luci-app-eqosplus/root/usr/bin/eqosplus @@ -0,0 +1,379 @@ +#!/bin/bash + +# Copyright (C) 2006 OpenWrt.org +# Copyright 2022-2025 sirpdboy + +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 \ No newline at end of file diff --git a/luci-app-eqosplus/root/usr/bin/eqosplusctrl b/luci-app-eqosplus/root/usr/bin/eqosplusctrl new file mode 100644 index 00000000..9b5721d6 --- /dev/null +++ b/luci-app-eqosplus/root/usr/bin/eqosplusctrl @@ -0,0 +1,63 @@ +#!/bin/sh + +# Copyright (C) 2006 OpenWrt.org +# Copyright 2022-2025 sirpdboy +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 \ No newline at end of file diff --git a/luci-app-eqosplus/root/usr/share/rpcd/acl.d/luci-app-eqosplus.json b/luci-app-eqosplus/root/usr/share/rpcd/acl.d/luci-app-eqosplus.json new file mode 100644 index 00000000..b879f4de --- /dev/null +++ b/luci-app-eqosplus/root/usr/share/rpcd/acl.d/luci-app-eqosplus.json @@ -0,0 +1,11 @@ +{ + "luci-app-eqosplus": { + "description": "Grant UCI access for luci-app-eqosplus", + "read": { + "uci": [ "eqosplus" ] + }, + "write": { + "uci": [ "eqosplus" ] + } + } +} diff --git a/luci-app-eqosplus/界面.png b/luci-app-eqosplus/界面.png new file mode 100644 index 00000000..d346c698 Binary files /dev/null and b/luci-app-eqosplus/界面.png differ diff --git a/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua b/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua index 5ccf8999..a3563791 100644 --- a/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua +++ b/luci-app-passwall/root/usr/share/passwall/clash_subconverter.lua @@ -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) diff --git a/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/luci-app-passwall/root/usr/share/passwall/subscribe.lua index be5eee6f..769202a6 100755 --- a/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -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]) diff --git a/v2ray-geodata/Makefile b/v2ray-geodata/Makefile index 432d091f..cc2d6ff6 100644 --- a/v2ray-geodata/Makefile +++ b/v2ray-geodata/Makefile @@ -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 diff --git a/vmlinux-btf/.github/workflows/build.yml b/vmlinux-btf/.github/workflows/build.yml index 8898becd..2953fa54 100644 --- a/vmlinux-btf/.github/workflows/build.yml +++ b/vmlinux-btf/.github/workflows/build.yml @@ -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 '\'' diff --git a/vmlinux-btf/Makefile b/vmlinux-btf/Makefile index 77bbc74b..268288ab 100644 --- a/vmlinux-btf/Makefile +++ b/vmlinux-btf/Makefile @@ -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