From cb1d9784972cd2eca7ab9d9a6910f69c7b7b2e7e Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sat, 3 Oct 2026 00:12:36 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=A4=20Sync=202026-10-03=2000:12:36?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- antileech/Makefile | 4 +- clashoo/Makefile | 8 +- geoview/Makefile | 4 +- gn/Makefile | 8 +- gn/src/out/last_commit_position.h | 4 +- linkback/Makefile | 4 +- linkback/files/linkback.config | 5 +- linkback/src/Makefile | 2 +- linkback/src/linkbackd.c | 376 ++++++++++---- linkback/src/linkbackd.h | 17 +- luci-app-adguardhome/.official_channel | 2 +- .../htdocs/luci-static/resources/fchomo.js | 455 ++++------------- .../resources/view/firewall/zones.js | 10 +- .../root/usr/libexec/luci-firewall-status | 4 +- luci-app-flowproxy/Makefile | 14 +- .../resources/view/flowproxy/flowproxy.js | 162 +++++- luci-app-flowproxy/po/templates/flowproxy.pot | 53 +- luci-app-flowproxy/po/zh_Hans/flowproxy.po | 29 ++ luci-app-flowproxy/root/etc/config/flowproxy | 1 + .../root/etc/hotplug.d/iface/99-flowproxy | 13 + luci-app-flowproxy/root/etc/init.d/flowproxy | 195 ++++--- .../root/usr/share/flowproxy/generate_nft.sh | 33 +- .../share/rpcd/acl.d/luci-app-flowproxy.json | 5 - .../root/usr/share/rpcd/ucode/luci.flowproxy | 88 +++- luci-app-fwlive/Makefile | 4 +- .../luci-static/resources/fwlive/constants.js | 2 +- luci-app-homeproxy-pro/Makefile | 16 +- luci-app-homeproxy-pro/README.md | 32 +- .../view/homeproxy-pro/client/routing.js | 6 +- .../homeproxy-pro/scripts/generator/common.uc | 2 +- .../scripts/generator/context.uc | 6 +- .../homeproxy-pro/scripts/generator/dns.uc | 18 +- .../homeproxy-pro/scripts/generator/route.uc | 12 +- .../homeproxy-pro/scripts/migrate_config.uc | 2 +- .../homeproxy-pro/scripts/runtime/service.sh | 2 +- luci-app-homeproxy-pro/tests/arch-guard.sh | 95 +++- .../tests/runtime/test_config_transaction.sh | 2 +- .../tests/ucode/test_generators.sh | 31 +- .../tests/ucode/test_tls_transport.uc | 2 +- luci-app-linkback/Makefile | 4 +- .../resources/view/linkback/overview.js | 26 +- .../resources/view/linkback/settings.js | 477 ++++++++++++------ luci-app-linkback/po/zh_Hans/linkback.po | 284 ++++++----- .../share/rpcd/acl.d/luci-app-linkback.json | 2 +- .../root/usr/share/rpcd/ucode/luci.linkback | 2 + luci-app-open-box/Makefile | 4 +- luci-app-passwall/Makefile | 2 +- .../root/usr/share/passwall/app.sh | 29 +- .../usr/share/passwall/helper_smartdns.sh | 30 -- luci-app-passwall2/Makefile | 2 +- luci-app-passwall2/root/etc/init.d/passwall2 | 14 +- .../root/usr/share/passwall2/app.sh | 24 +- .../root/usr/share/passwall2/rule_update.lua | 4 +- luci-app-ssr-plus/Makefile | 2 +- .../root/usr/share/shadowsocksr/gfw2ipset.sh | 4 +- luci-theme-argon/Makefile | 2 +- .../htdocs/luci-static/argon/css/dark.css | 1 + rtp2httpd/Makefile | 4 +- 58 files changed, 1627 insertions(+), 1018 deletions(-) create mode 100755 luci-app-flowproxy/root/etc/hotplug.d/iface/99-flowproxy delete mode 100755 luci-app-passwall/root/usr/share/passwall/helper_smartdns.sh diff --git a/antileech/Makefile b/antileech/Makefile index ac132c1a2..791009c27 100644 --- a/antileech/Makefile +++ b/antileech/Makefile @@ -5,12 +5,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=antileech -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_SOURCE_PROTO:=git PKG_SOURCE_DATE:=2021-12-05 PKG_SOURCE_URL:=https://github.com/persmule/amule-dlp.antiLeech.git -PKG_SOURCE_VERSION:=1b14ed828ed227933333de31ffb8e24e6c10451c +PKG_SOURCE_VERSION:=bc3ae6a86c958dab1bde9c2cea51b523530aec4b PKG_MIRROR_HASH:=skip PKG_FIXUP:=autoreconf diff --git a/clashoo/Makefile b/clashoo/Makefile index e73ee2460..8072eb650 100644 --- a/clashoo/Makefile +++ b/clashoo/Makefile @@ -1,17 +1,17 @@ include $(TOPDIR)/rules.mk PKG_NAME:=clashoo -PKG_SHORT_SHA:=88dcbf7 -PKG_COMMIT_DATE:=2026.09.30 +PKG_SHORT_SHA:=9f053c4 +PKG_COMMIT_DATE:=2026.10.02 PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA) -PKG_RELEASE:=3 +PKG_RELEASE:=4 PKG_SOURCE:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)-alpha-$(PKG_SHORT_SHA) PKG_SOURCE_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src -PKG_SOURCE_VERSION:=f0c95853839454a5d1a9eda873d94b3d6c8cc8c3 +PKG_SOURCE_VERSION:=c10fd37cf083bc4922237b7ec915d62614be0623 PKG_HASH:=skip PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA) diff --git a/geoview/Makefile b/geoview/Makefile index 6fc545d34..9269c586d 100644 --- a/geoview/Makefile +++ b/geoview/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=geoview -PKG_VERSION:=0.2.7 -PKG_RELEASE:=6 +PKG_VERSION:=0.2.6 +PKG_RELEASE:=7 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/snowie2000/geoview/tar.gz/$(PKG_VERSION)? diff --git a/gn/Makefile b/gn/Makefile index ec829afe8..d2f11d6da 100644 --- a/gn/Makefile +++ b/gn/Makefile @@ -5,12 +5,12 @@ include $(TOPDIR)/rules.mk PKG_NAME:=gn -PKG_RELEASE:=7 +PKG_RELEASE:=8 PKG_SOURCE_PROTO:=git PKG_SOURCE_URL:=https://gn.googlesource.com/gn.git -PKG_SOURCE_DATE:=2026-01-14 -PKG_SOURCE_VERSION:=103f8b437f5e791e0aef9d5c372521a5d675fabb +PKG_SOURCE_DATE:=2026-06-27 +PKG_SOURCE_VERSION:=8d1b023dec36ab50138d427e807175fd99b5bb57 PKG_MIRROR_HASH:=skip PKG_LICENSE:=BSD 3-Clause @@ -43,7 +43,7 @@ define Host/Configure endef define Host/Compile - +$(NINJA) -C $(HOST_BUILD_DIR)/out + +$(NINJA) -C $(HOST_BUILD_DIR)/out gn endef define Host/Install diff --git a/gn/src/out/last_commit_position.h b/gn/src/out/last_commit_position.h index 81ac9bd89..160bbb26a 100644 --- a/gn/src/out/last_commit_position.h +++ b/gn/src/out/last_commit_position.h @@ -3,7 +3,7 @@ #ifndef OUT_LAST_COMMIT_POSITION_H_ #define OUT_LAST_COMMIT_POSITION_H_ -#define LAST_COMMIT_POSITION_NUM 2319 -#define LAST_COMMIT_POSITION "2319 (103f8b437f5e)" +#define LAST_COMMIT_POSITION_NUM 2434 +#define LAST_COMMIT_POSITION "2434 (8d1b023dec36)" #endif // OUT_LAST_COMMIT_POSITION_H_ diff --git a/linkback/Makefile b/linkback/Makefile index bb4fc3635..8bfde78a4 100644 --- a/linkback/Makefile +++ b/linkback/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=linkback PKG_VERSION:=1.0.0 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_MAINTAINER:=Dante PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) @@ -59,7 +59,7 @@ define Package/$(PKG_NAME)/install endef define Package/$(PKG_NAME)/postinst -#!/sh +#!/bin/sh [ -n "$${IPKG_INSTROOT}" ] || { /etc/init.d/linkback enable /etc/init.d/linkback restart diff --git a/linkback/files/linkback.config b/linkback/files/linkback.config index 7e36a72c7..6840dcfbe 100644 --- a/linkback/files/linkback.config +++ b/linkback/files/linkback.config @@ -1,9 +1,10 @@ config global option enabled '0' + option mode 'multi_wan' + option interface 'wan' option check_interval '5' - option check_timeout '3' + option check_timeout '1' option recovery_delay '3' option failover_delay '2' - diff --git a/linkback/src/Makefile b/linkback/src/Makefile index f44f84299..92956c4a3 100644 --- a/linkback/src/Makefile +++ b/linkback/src/Makefile @@ -2,7 +2,7 @@ TARGET := linkbackd OBJS := linkbackd.o CFLAGS += -Wall -O2 -LIBS := -luci +LIBS := -luci -lubox -lubus -lblobmsg_json all: $(TARGET) diff --git a/linkback/src/linkbackd.c b/linkback/src/linkbackd.c index c887f6efa..8b56b986c 100644 --- a/linkback/src/linkbackd.c +++ b/linkback/src/linkbackd.c @@ -12,7 +12,12 @@ #include #include #include + #include +#include +#include +#include + #include "linkbackd.h" // DNS Header struct for raw check @@ -30,6 +35,7 @@ static global_config_t global_cfg; static link_t links[MAX_LINKS]; static int link_count = 0; static volatile bool keep_running = true; +static struct ubus_context *ubus_ctx = NULL; // Prototypes static void restore_all_metrics(void); @@ -39,76 +45,156 @@ static void handle_signal(int sig) { keep_running = false; } -// Ubus popen JSON parser -static bool get_interface_ubus_status(const char *ifname, char *device, int dev_len, char *gateway, int gw_len, bool *is_up) { - char cmd[256]; - snprintf(cmd, sizeof(cmd), "ubus call network.interface.%s status 2>/dev/null", ifname); - FILE *fp = popen(cmd, "r"); - if (!fp) return false; +// Ubus Connection Management +static bool init_ubus_connection(void) { + if (ubus_ctx) { + return true; + } + ubus_ctx = ubus_connect(NULL); + if (!ubus_ctx) { + syslog(LOG_ERR, "Failed to connect to ubus daemon"); + return false; + } + return true; +} - char buf[4096] = {0}; - int bytes_read = fread(buf, 1, sizeof(buf) - 1, fp); - pclose(fp); +static void close_ubus_connection(void) { + if (ubus_ctx) { + ubus_free(ubus_ctx); + ubus_ctx = NULL; + } +} - if (bytes_read <= 0) return false; +// Ubus Callback & Parsing Data Structure (P1 Refactor: Native Blobmsg Parser) +struct iface_status_data { + bool up; + char device[MAX_NAME_LEN]; + char gateway[MAX_IP_LEN]; + bool found; +}; - // Check if interface is up - *is_up = false; - char *p_up = strstr(buf, "\"up\":"); - if (p_up) { - char *p_true = strstr(p_up, "true"); - // Ensure 'true' belongs to this property and isn't far away - if (p_true && p_true - p_up < 10) { - *is_up = true; - } +enum { + IFACE_ATTR_UP, + IFACE_ATTR_DEVICE, + IFACE_ATTR_L3_DEVICE, + IFACE_ATTR_ROUTE, + __IFACE_ATTR_MAX +}; + +static const struct blobmsg_policy iface_policy[__IFACE_ATTR_MAX] = { + [IFACE_ATTR_UP] = { .name = "up", .type = BLOBMSG_TYPE_BOOL }, + [IFACE_ATTR_DEVICE] = { .name = "device", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_L3_DEVICE] = { .name = "l3_device", .type = BLOBMSG_TYPE_STRING }, + [IFACE_ATTR_ROUTE] = { .name = "route", .type = BLOBMSG_TYPE_ARRAY }, +}; + +enum { + ROUTE_ATTR_TARGET, + ROUTE_ATTR_NEXTHOP, + ROUTE_ATTR_MASK, + __ROUTE_ATTR_MAX +}; + +static const struct blobmsg_policy route_policy[__ROUTE_ATTR_MAX] = { + [ROUTE_ATTR_TARGET] = { .name = "target", .type = BLOBMSG_TYPE_STRING }, + [ROUTE_ATTR_NEXTHOP] = { .name = "nexthop", .type = BLOBMSG_TYPE_STRING }, + [ROUTE_ATTR_MASK] = { .name = "mask", .type = BLOBMSG_TYPE_INT32 }, +}; + +static void iface_status_cb(struct ubus_request *req, int type, struct blob_attr *msg) { + struct iface_status_data *data = (struct iface_status_data *)req->priv; + struct blob_attr *tb[__IFACE_ATTR_MAX]; + + if (!msg) return; + + blobmsg_parse(iface_policy, __IFACE_ATTR_MAX, tb, blob_data(msg), blob_len(msg)); + + if (tb[IFACE_ATTR_UP]) { + data->up = blobmsg_get_bool(tb[IFACE_ATTR_UP]); } - // Extract physical device - device[0] = '\0'; - char *p_dev = strstr(buf, "\"l3_device\":"); - int dev_key_len = 12; - if (!p_dev) { - p_dev = strstr(buf, "\"device\":"); - dev_key_len = 9; - } - if (p_dev) { - char *p_start = strchr(p_dev + dev_key_len, '"'); - if (p_start) { - char *p_end = strchr(p_start + 1, '"'); - if (p_end) { - int len = p_end - p_start - 1; - if (len >= dev_len) len = dev_len - 1; - strncpy(device, p_start + 1, len); - device[len] = '\0'; - } - } + if (tb[IFACE_ATTR_L3_DEVICE]) { + strncpy(data->device, blobmsg_get_string(tb[IFACE_ATTR_L3_DEVICE]), sizeof(data->device) - 1); + } else if (tb[IFACE_ATTR_DEVICE]) { + strncpy(data->device, blobmsg_get_string(tb[IFACE_ATTR_DEVICE]), sizeof(data->device) - 1); } - // Extract gateway/nexthop from route list - gateway[0] = '\0'; - char *p_route = strstr(buf, "\"route\":"); - if (p_route) { - char *p_nexthop = strstr(p_route, "\"nexthop\":"); - if (p_nexthop) { - char *p_start = strchr(p_nexthop + 10, '"'); - if (p_start) { - char *p_end = strchr(p_start + 1, '"'); - if (p_end) { - int len = p_end - p_start - 1; - if (len >= gw_len) len = gw_len - 1; - strncpy(gateway, p_start + 1, len); - gateway[len] = '\0'; + if (tb[IFACE_ATTR_ROUTE]) { + struct blob_attr *cur; + int rem; + blobmsg_for_each_attr(cur, tb[IFACE_ATTR_ROUTE], rem) { + struct blob_attr *rt_tb[__ROUTE_ATTR_MAX]; + blobmsg_parse(route_policy, __ROUTE_ATTR_MAX, rt_tb, blobmsg_data(cur), blobmsg_len(cur)); + + if (rt_tb[ROUTE_ATTR_NEXTHOP]) { + const char *nh = blobmsg_get_string(rt_tb[ROUTE_ATTR_NEXTHOP]); + if (nh && nh[0] != '\0') { + strncpy(data->gateway, nh, sizeof(data->gateway) - 1); + // Default route target 0.0.0.0 is top priority + if (rt_tb[ROUTE_ATTR_TARGET]) { + const char *tgt = blobmsg_get_string(rt_tb[ROUTE_ATTR_TARGET]); + if (tgt && strcmp(tgt, "0.0.0.0") == 0) { + break; + } + } } } } } + data->found = true; +} + +static bool get_interface_ubus_status(const char *ifname, char *device, int dev_len, char *gateway, int gw_len, bool *is_up) { + *is_up = false; + if (device && dev_len > 0) device[0] = '\0'; + if (gateway && gw_len > 0) gateway[0] = '\0'; + + if (!ifname || ifname[0] == '\0') { + return false; + } + + if (!init_ubus_connection()) { + return false; + } + + char ubus_path[64]; + snprintf(ubus_path, sizeof(ubus_path), "network.interface.%s", ifname); + + uint32_t id; + if (ubus_lookup_id(ubus_ctx, ubus_path, &id) != 0) { + // Reconnect once in case ubusd restarted + close_ubus_connection(); + if (!init_ubus_connection() || ubus_lookup_id(ubus_ctx, ubus_path, &id) != 0) { + return false; + } + } + + struct iface_status_data data; + memset(&data, 0, sizeof(data)); + + int ret = ubus_invoke(ubus_ctx, id, "status", NULL, iface_status_cb, &data, 1000); + if (ret != 0 || !data.found) { + return false; + } + + *is_up = data.up; + if (device && dev_len > 0) { + strncpy(device, data.device, dev_len - 1); + device[dev_len - 1] = '\0'; + } + if (gateway && gw_len > 0) { + strncpy(gateway, data.gateway, gw_len - 1); + gateway[gw_len - 1] = '\0'; + } + return true; } -// Ping check +// Ping check (P2: Strict timeout limit and early exit) static bool run_ping_check(const char *device, const char *target, int timeout, int *rtt_ms) { if (device[0] == '\0' || target[0] == '\0') return false; + if (timeout <= 0) timeout = 1; char cmd[256]; snprintf(cmd, sizeof(cmd), "ping -I %s -c 1 -W %d %s 2>/dev/null", device, timeout, target); @@ -130,9 +216,10 @@ static bool run_ping_check(const char *device, const char *target, int timeout, return ok && (WIFEXITED(status) && WEXITSTATUS(status) == 0); } -// DNS check +// DNS check (P2: Non-blocking socket check with 1s default timeout) static bool run_dns_check(const char *device, const char *dns_server, const char *domain, int timeout, int *rtt_ms) { if (device[0] == '\0' || dns_server[0] == '\0' || domain[0] == '\0') return false; + if (timeout <= 0) timeout = 1; struct timeval start, end; gettimeofday(&start, NULL); @@ -181,12 +268,9 @@ static bool run_dns_check(const char *device, const char *dns_server, const char } *dst++ = 0; - unsigned short *qtype = (unsigned short *)dst; - *qtype = htons(1); // A Record - dst += 2; - unsigned short *qclass = (unsigned short *)dst; - *qclass = htons(1); // IN - dst += 2; + // Safe byte-by-byte write to prevent unaligned memory access SIGBUS on MIPS/ARM + *dst++ = 0; *dst++ = 1; // QTYPE: A (0x0001) + *dst++ = 0; *dst++ = 1; // QCLASS: IN (0x0001) int packet_len = dst - packet; @@ -219,16 +303,17 @@ static bool run_dns_check(const char *device, const char *dns_server, const char if (resp_len < 12) return false; struct dns_header *resp_dns = (struct dns_header *)response; - if (ntohs(resp_dns->id) != getpid()) return false; + if (ntohs(resp_dns->id) != (unsigned short)getpid()) return false; gettimeofday(&end, NULL); *rtt_ms = (int)((end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000); return true; } -// TCP check +// TCP check (P2: Non-blocking socket connect with 1s default timeout) static bool run_tcp_check(const char *device, const char *tcp_target, int port, int timeout, int *rtt_ms) { if (device[0] == '\0' || tcp_target[0] == '\0' || port <= 0) return false; + if (timeout <= 0) timeout = 1; struct timeval start, end; gettimeofday(&start, NULL); @@ -305,8 +390,15 @@ static bool load_config(void) { return false; } - // Parse global config + // Default global settings global_cfg.enabled = false; + global_cfg.mode = MODE_MULTI_WAN; + strncpy(global_cfg.interface, "wan", sizeof(global_cfg.interface) - 1); + global_cfg.device[0] = '\0'; + global_cfg.check_interval = 5; + global_cfg.check_timeout = 1; + global_cfg.recovery_delay = 3; + global_cfg.failover_delay = 2; struct uci_section *global_sec = uci_lookup_section(ctx, pkg, "global"); if (!global_sec) { @@ -323,6 +415,30 @@ static bool load_config(void) { if (global_sec) { const char *enabled = uci_lookup_option_string(ctx, global_sec, "enabled"); global_cfg.enabled = (enabled && strcmp(enabled, "1") == 0); + + const char *mode = uci_lookup_option_string(ctx, global_sec, "mode"); + if (mode && strcmp(mode, "multi_gw") == 0) { + global_cfg.mode = MODE_MULTI_GW; + } else { + global_cfg.mode = MODE_MULTI_WAN; + } + + const char *iface = uci_lookup_option_string(ctx, global_sec, "interface"); + if (iface && iface[0] != '\0') { + strncpy(global_cfg.interface, iface, sizeof(global_cfg.interface) - 1); + } + + const char *interval = uci_lookup_option_string(ctx, global_sec, "check_interval"); + if (interval) global_cfg.check_interval = atoi(interval); + + const char *timeout = uci_lookup_option_string(ctx, global_sec, "check_timeout"); + if (timeout) global_cfg.check_timeout = atoi(timeout); + + const char *recovery = uci_lookup_option_string(ctx, global_sec, "recovery_delay"); + if (recovery) global_cfg.recovery_delay = atoi(recovery); + + const char *failover = uci_lookup_option_string(ctx, global_sec, "failover_delay"); + if (failover) global_cfg.failover_delay = atoi(failover); } // Parse links @@ -339,7 +455,7 @@ static bool load_config(void) { memset(link, 0, sizeof(link_t)); const char *name = uci_lookup_option_string(ctx, s, "name"); - if (!name) continue; + if (!name || name[0] == '\0') continue; strncpy(link->name, name, MAX_NAME_LEN - 1); link->enabled = true; @@ -351,6 +467,13 @@ static bool load_config(void) { link->metric = link->priority * 10; link->current_metric = link->metric; + // Parse gateway for multi_gw mode + const char *gw = uci_lookup_option_string(ctx, s, "gateway"); + if (gw) { + strncpy(link->gateway_cfg, gw, MAX_IP_LEN - 1); + strncpy(link->gateway, gw, MAX_IP_LEN - 1); + } + // Parse ping targets (split by comma or space) const char *pings = uci_lookup_option_string(ctx, s, "ping_targets"); if (pings) { @@ -359,11 +482,9 @@ static bool load_config(void) { tmp[sizeof(tmp) - 1] = '\0'; char *token = strtok(tmp, ", \t"); while (token && link->ping_target_count < MAX_TARGETS) { - // Trim leading whitespace while (*token == ' ' || *token == '\t' || *token == '\r' || *token == '\n') { token++; } - // Trim trailing whitespace char *end = token + strlen(token) - 1; while (end > token && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) { *end = '\0'; @@ -373,7 +494,7 @@ static bool load_config(void) { strncpy(link->ping_targets[link->ping_target_count], token, MAX_IP_LEN - 1); link->ping_target_count++; } - token = strtok(NULL, ","); + token = strtok(NULL, ", \t"); } } @@ -392,16 +513,16 @@ static bool load_config(void) { if (tcp_p) link->tcp_port = atoi(tcp_p); const char *interval = uci_lookup_option_string(ctx, s, "check_interval"); - link->check_interval = interval ? atoi(interval) : 5; + link->check_interval = interval ? atoi(interval) : global_cfg.check_interval; const char *timeout = uci_lookup_option_string(ctx, s, "check_timeout"); - link->check_timeout = timeout ? atoi(timeout) : 3; + link->check_timeout = timeout ? atoi(timeout) : global_cfg.check_timeout; const char *recovery = uci_lookup_option_string(ctx, s, "recovery_delay"); - link->recovery_delay = recovery ? atoi(recovery) : 3; + link->recovery_delay = recovery ? atoi(recovery) : global_cfg.recovery_delay; const char *failover = uci_lookup_option_string(ctx, s, "failover_delay"); - link->failover_delay = failover ? atoi(failover) : 2; + link->failover_delay = failover ? atoi(failover) : global_cfg.failover_delay; // Default runtime states link->healthy = true; @@ -422,8 +543,7 @@ static bool load_config(void) { return true; } -// Startup-time defensive validation. LuCI-side checks can be bypassed by -// direct UCI edits, so daemon must refuse unsafe/incomplete configs. +// Startup-time defensive validation. static bool validate_loaded_config(void) { if (!global_cfg.enabled) { return true; @@ -438,10 +558,22 @@ static bool validate_loaded_config(void) { link_t *a = &links[i]; if (a->name[0] == '\0') { - syslog(LOG_ERR, "Invalid config: link[%d] has empty interface name.", i); + syslog(LOG_ERR, "Invalid config: link[%d] has empty name.", i); return false; } + if (global_cfg.mode == MODE_MULTI_GW) { + if (a->gateway_cfg[0] == '\0') { + syslog(LOG_ERR, "Invalid config: link %s requires gateway IP in multi_gw mode.", a->name); + return false; + } + struct in_addr test_addr; + if (inet_pton(AF_INET, a->gateway_cfg, &test_addr) <= 0) { + syslog(LOG_ERR, "Invalid config: link %s has invalid gateway IP: %s.", a->name, a->gateway_cfg); + return false; + } + } + if (a->priority <= 0) { syslog(LOG_ERR, "Invalid config: link %s has invalid priority %d (must be > 0).", a->name, a->priority); return false; @@ -452,13 +584,24 @@ static bool validate_loaded_config(void) { return false; } - // No duplicate priorities + // Check for conflicts for (int j = i + 1; j < link_count; j++) { link_t *b = &links[j]; if (a->priority == b->priority) { syslog(LOG_ERR, "Invalid config: duplicate priority %d on links %s and %s.", a->priority, a->name, b->name); return false; } + if (global_cfg.mode == MODE_MULTI_GW) { + if (strcmp(a->gateway_cfg, b->gateway_cfg) == 0) { + syslog(LOG_ERR, "Invalid config: duplicate gateway %s on links %s and %s.", a->gateway_cfg, a->name, b->name); + return false; + } + } else { + if (strcmp(a->name, b->name) == 0) { + syslog(LOG_ERR, "Invalid config: duplicate interface %s.", a->name); + return false; + } + } } bool has_ping = (a->ping_target_count > 0); @@ -499,26 +642,38 @@ static bool validate_loaded_config(void) { return true; } -// Retrieve the real default route metric for a device from /proc/net/route -static int get_system_route_metric(const char *device, int expected_metric) { +// Retrieve the real default route metric for a device and optional gateway from /proc/net/route +static int get_system_route_metric(const char *device, const char *gateway, int expected_metric) { if (device[0] == '\0') return -1; FILE *fp = fopen("/proc/net/route", "r"); if (!fp) return -1; char line[256]; char iface[32]; - unsigned long dest; + unsigned long dest = 0; + unsigned long gw_hex = 0; int metric = -1; int first_found_metric = -1; bool found_expected = false; + unsigned long expected_gw_hex = 0; + if (gateway && gateway[0] != '\0') { + struct in_addr addr; + if (inet_pton(AF_INET, gateway, &addr) == 1) { + expected_gw_hex = (unsigned long)addr.s_addr; + } + } + // Skip header line if (fgets(line, sizeof(line), fp)) { while (fgets(line, sizeof(line), fp)) { - // Destination is 2nd column, Metric is 7th column, dest is in hex. - // Use %*s for Gateway and Flags to safely skip non-numeric characters. - if (sscanf(line, "%31s %lx %*s %*s %*d %*d %d", iface, &dest, &metric) == 3) { + // Format: Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT + if (sscanf(line, "%31s %lx %lx %*s %*d %*d %d", iface, &dest, &gw_hex, &metric) == 4) { if (strcmp(iface, device) == 0 && dest == 0) { + // In multi-gateway mode, check if gateway matches + if (expected_gw_hex != 0 && gw_hex != expected_gw_hex) { + continue; + } if (first_found_metric == -1) { first_found_metric = metric; } @@ -584,8 +739,8 @@ static void update_route_metric(link_t *link, int new_metric, int old_metric_to_ link->device, new_metric); } - syslog(LOG_INFO, "Applying route metric update on link %s (%s, priority %d): %d -> %d", - link->name, link->device, link->priority, link->current_metric, new_metric); + syslog(LOG_INFO, "Applying route metric update on link %s (dev=%s, gw=%s, priority=%d): %d -> %d", + link->name, link->device, link->gateway[0] ? link->gateway : "none", link->priority, link->current_metric, new_metric); int rc = system(cmd); if (rc == 0) { @@ -635,7 +790,9 @@ static void write_status_json(void) { fprintf(fp, "{\n"); fprintf(fp, " \"enabled\": %s,\n", global_cfg.enabled ? "true" : "false"); - fprintf(fp, " \"check_interval\": 5,\n"); + fprintf(fp, " \"mode\": \"%s\",\n", (global_cfg.mode == MODE_MULTI_GW) ? "multi_gw" : "multi_wan"); + fprintf(fp, " \"interface\": \"%s\",\n", global_cfg.interface); + fprintf(fp, " \"check_interval\": %d,\n", global_cfg.check_interval); // Find current active gateway link (first healthy link ordered by priority) char active_link[MAX_NAME_LEN] = "none"; @@ -706,17 +863,27 @@ int main(int argc, char **argv) { // Sort links by priority (lowest priority number first) qsort(links, link_count, sizeof(link_t), compare_links); - syslog(LOG_INFO, "Loaded %d monitored interfaces. Starting health check scheduler.", link_count); + syslog(LOG_INFO, "Loaded %d monitored targets in %s mode. Starting health check scheduler.", + link_count, (global_cfg.mode == MODE_MULTI_GW) ? "multi_gw" : "multi_wan"); // Core check loop while (keep_running) { time_t now = time(NULL); bool any_checked = false; + // If in multi_gw mode, resolve bind interface physical device once per cycle + if (global_cfg.mode == MODE_MULTI_GW) { + char dev[MAX_NAME_LEN] = {0}; + char gw[MAX_IP_LEN] = {0}; + bool if_up = false; + get_interface_ubus_status(global_cfg.interface, dev, sizeof(dev), gw, sizeof(gw), &if_up); + strncpy(global_cfg.device, dev, sizeof(global_cfg.device) - 1); + } + for (int i = 0; i < link_count; i++) { link_t *link = &links[i]; - // Check if this interface is due for checking + // Check if this link is due for checking if (now - link->last_checked < link->check_interval) { continue; } @@ -728,13 +895,22 @@ int main(int argc, char **argv) { char gw[MAX_IP_LEN] = {0}; bool is_up = false; - get_interface_ubus_status(link->name, dev, sizeof(dev), gw, sizeof(gw), &is_up); + if (global_cfg.mode == MODE_MULTI_GW) { + // Inherit physical device from global bind interface + is_up = (global_cfg.device[0] != '\0'); + strncpy(dev, global_cfg.device, sizeof(dev) - 1); + strncpy(gw, link->gateway_cfg, sizeof(gw) - 1); + } else { + // Query netifd for WAN interface + get_interface_ubus_status(link->name, dev, sizeof(dev), gw, sizeof(gw), &is_up); + } + link->is_up = is_up; strncpy(link->device, dev, MAX_NAME_LEN - 1); strncpy(link->gateway, gw, MAX_IP_LEN - 1); + // P2 optimization: Interface down short-circuit if (!is_up || dev[0] == '\0') { - // Interface is down in netifd, mark unhealthy immediately link->healthy = false; link->current_score = 0; link->ping_ok = false; @@ -743,14 +919,14 @@ int main(int argc, char **argv) { link->consecutive_success = 0; link->consecutive_failure = 0; - // If it still has low metric, float it + // If it still has low metric, float it immediately if (link->current_metric == link->metric) { update_route_metric(link, 1000 + link->metric, -1); } continue; } - // 2. Perform health checks + // 2. Perform health checks (P2: 1s non-blocking timeout) bool check_success = false; if (link->ping_target_count > 0) { @@ -789,8 +965,7 @@ int main(int argc, char **argv) { link->current_score = check_success ? 1 : 0; - // 3. Evaluate health state changes (filtering and delay) - + // 3. Evaluate health state changes (anti-flap delay) if (check_success) { link->consecutive_success++; link->consecutive_failure = 0; @@ -798,8 +973,8 @@ int main(int argc, char **argv) { if (!link->healthy && link->consecutive_success >= link->recovery_delay) { // Recovered! Failback! link->healthy = true; - syslog(LOG_NOTICE, "Link %s (%s, priority %d) recovered to healthy after %d successes.", - link->name, link->device, link->priority, link->consecutive_success); + syslog(LOG_NOTICE, "Link %s (%s, gw=%s, priority %d) recovered to healthy after %d successes.", + link->name, link->device, link->gateway[0] ? link->gateway : "none", link->priority, link->consecutive_success); // Restore original metric update_route_metric(link, link->metric, -1); @@ -811,8 +986,8 @@ int main(int argc, char **argv) { if (link->healthy && link->consecutive_failure >= link->failover_delay) { // Failed! Failover! link->healthy = false; - syslog(LOG_WARNING, "Link %s (%s, priority %d) went down after %d failures.", - link->name, link->device, link->priority, link->consecutive_failure); + syslog(LOG_WARNING, "Link %s (%s, gw=%s, priority %d) went down after %d failures.", + link->name, link->device, link->gateway[0] ? link->gateway : "none", link->priority, link->consecutive_failure); // Push metric out of choice range update_route_metric(link, 1000 + link->metric, -1); @@ -822,26 +997,25 @@ int main(int argc, char **argv) { // 4. Active routing metric self-healing to prevent external/netifd interference if (link->device[0] != '\0') { int expected_metric = (link->is_up && link->healthy) ? link->metric : (1000 + link->metric); - int real_metric = get_system_route_metric(link->device, expected_metric); + int real_metric = get_system_route_metric(link->device, link->gateway, expected_metric); if (real_metric != -1 && real_metric != expected_metric) { - syslog(LOG_WARNING, "Route metric mismatch detected on %s (%s, priority %d): expected %d, got %d. Correcting...", - link->name, link->device, link->priority, expected_metric, real_metric); + syslog(LOG_WARNING, "Route metric mismatch detected on %s (%s, gw=%s, priority %d): expected %d, got %d. Correcting...", + link->name, link->device, link->gateway[0] ? link->gateway : "none", link->priority, expected_metric, real_metric); update_route_metric(link, expected_metric, real_metric); } } } if (any_checked) { - // Write states to shared JSON file write_status_json(); } - // High-responsiveness scheduler ticks every second sleep(1); } // Terminating: clean up routes before exit restore_all_metrics(); + close_ubus_connection(); unlink(STATUS_FILE); syslog(LOG_INFO, "LinkBack daemon terminated successfully."); closelog(); diff --git a/linkback/src/linkbackd.h b/linkback/src/linkbackd.h index 490d03043..88d1d8ff2 100644 --- a/linkback/src/linkbackd.h +++ b/linkback/src/linkbackd.h @@ -11,12 +11,20 @@ #define MAX_DOMAIN_LEN 128 #define STATUS_FILE "/var/run/linkback.json" +typedef enum { + MODE_MULTI_WAN = 0, + MODE_MULTI_GW = 1 +} linkback_mode_t; + typedef struct { char name[MAX_NAME_LEN]; bool enabled; int priority; int metric; + // Explicit gateway IP for multi_gw mode + char gateway_cfg[MAX_IP_LEN]; + // Ping config char ping_targets[MAX_TARGETS][MAX_IP_LEN]; int ping_target_count; @@ -40,7 +48,7 @@ typedef struct { int failover_delay; // Runtime state - char device[MAX_NAME_LEN]; // physical interface name e.g., pppoe-wan, eth1 + char device[MAX_NAME_LEN]; // physical interface name e.g., pppoe-wan, eth1, br-lan char gateway[MAX_IP_LEN]; // gateway IP address bool is_up; // whether interface is reported up by netifd bool healthy; // daemon link health state @@ -62,6 +70,13 @@ typedef struct { typedef struct { bool enabled; + linkback_mode_t mode; + char interface[MAX_NAME_LEN]; // Bind interface in multi_gw mode (e.g. lan) + char device[MAX_NAME_LEN]; // Resolved device in multi_gw mode (e.g. br-lan) + int check_interval; + int check_timeout; + int recovery_delay; + int failover_delay; } global_config_t; #endif // LINKBACKD_H diff --git a/luci-app-adguardhome/.official_channel b/luci-app-adguardhome/.official_channel index 8ab67f2e7..49f33a8c6 100644 --- a/luci-app-adguardhome/.official_channel +++ b/luci-app-adguardhome/.official_channel @@ -1 +1 @@ -snapshot +both diff --git a/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js b/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js index 8ec433993..3c47bc47d 100644 --- a/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js +++ b/luci-app-fchomo/htdocs/luci-static/resources/fchomo.js @@ -8,6 +8,31 @@ 'require ui'; 'require validation'; +function funcSed(originFunc, matrix = [], closure = []) { + if (isEmpty(matrix)) + return originFunc; + + const oriFuncStr = originFunc.toString(); + let funcStr = oriFuncStr; + + for (const [regexp, replacer] of matrix) { + if (!regexp) + continue; + if (regexp.test(funcStr)) + funcStr = funcStr.replace(regexp, replacer || ''); + else + throw new Error(`funcSed: No matching items found: ${regexp}`); + } + + if (isEmpty(funcStr) || funcStr === oriFuncStr) + return originFunc; + + const funcArgs = funcStr.substring(funcStr.indexOf('(') +1, funcStr.indexOf(')')).split(','); + const funcBody = funcStr.substring(funcStr.indexOf('{') +1, funcStr.lastIndexOf('}')); + + return new Function(...closure, ...funcArgs, funcBody); +} + /* Member */ const rulesetdoc = [ 'data:text/html;base64,', @@ -511,25 +536,17 @@ const CBIDynamicList = form.DynamicList.extend({ // @less_25_12 const CBIMultiValue = form.MultiValue.extend({ // @pr8758_merged __name__: 'CBI.MultiValue', - renderWidget(section_id, option_index, cfgvalue) { - const value = (cfgvalue != null) ? cfgvalue : this.default; - const choices = this.transformChoices(); + renderWidget(/* ... */) { + const sb = funcSed( + form.MultiValue.prototype.renderWidget, + [ + [new RegExp(/(widget\s*=\s*new)\s*ui\.Dropdown/), '$1 UIDropdown'], + [new RegExp(/(multiple:\s*true)/), 'keep_order: this.keep_order, $1'] + ], + ['UIDropdown'] + ).call(this, UIDropdown, ...arguments); - const widget = new UIDropdown(L.toArray(value), choices, { - id: this.cbid(section_id), - sort: this.keylist, - multiple: true, - keep_order: this.keep_order, - optional: this.optional || this.rmempty, - select_placeholder: this.placeholder, - create: this.create, - display_items: this.display_size ?? this.size ?? 3, - dropdown_items: this.dropdown_size ?? this.size ?? -1, - validate: this.getValidator(section_id), - disabled: (this.readonly != null) ? this.readonly : this.map.readonly - }); - - return widget.render(); + return sb; } }); @@ -827,354 +844,88 @@ const UIDynamicList = ui.DynamicList.extend({ // @less_25_12 // keep selected order for multiple selection const UIDropdown = ui.Dropdown.extend({ // @pr8758_merged __init__(value, choices, options) { - if (typeof(choices) != 'object') - choices = {}; - - if (!Array.isArray(value)) - this.values = (value != null && value != '') ? [ value ] : []; - else - this.values = value; + const result = ui.Dropdown.prototype.__init__.apply(this, arguments); this.orderCounter = this.values.length; - this.choices = choices; - this.options = Object.assign({ - sort: true, - multiple: Array.isArray(value), - keep_order: false, - optional: true, - select_placeholder: _('-- Please choose --'), - custom_placeholder: _('-- custom --'), - display_items: 3, - dropdown_items: -1, - create: false, - create_query: '.create-item-input', - create_template: 'script[type="item-template"]' - }, options); + this.options.keep_order = options?.keep_order ?? false; + + return result; }, - render() { - const sb = E('div', { - 'id': this.options.id, - 'class': 'cbi-dropdown', - 'multiple': this.options.multiple ? '' : null, - 'keep_order': this.options.keep_order ? '' : null, - 'optional': this.options.optional ? '' : null, - 'disabled': this.options.disabled ? '' : null, - 'tabindex': -1 - }, E('ul')); - - let keys = Object.keys(this.choices); - - if (this.options.sort === true) - keys.sort(L.naturalCompare); - else if (Array.isArray(this.options.sort)) - keys = this.options.sort; - - if (this.options.create) - for (let i = 0; i < this.values.length; i++) - if (!this.choices.hasOwnProperty(this.values[i])) - keys.push(this.values[i]); - - for (let i = 0; i < keys.length; i++) { - let label = this.choices[keys[i]]; - - if (dom.elem(label)) - label = label.cloneNode(true); - - sb.lastElementChild.appendChild(E('li', { - 'data-value': keys[i], - 'selected': (this.values.indexOf(keys[i]) > -1) ? '' : null - }, [ label ?? keys[i] ])); - } - - if (this.options.create) { - const createEl = E('input', { - 'type': 'text', - 'class': 'create-item-input', - 'readonly': this.options.readonly ? '' : null, - 'maxlength': this.options.maxlength, - 'placeholder': this.options.custom_placeholder ?? this.options.placeholder, - 'inputmode': 'text', - 'enterkeyhint': 'done' - }); - - if (this.options.datatype || this.options.validate) - UI.prototype.addValidator(createEl, this.options.datatype ?? 'string', - true, this.options.validate, 'blur', 'keyup'); - - sb.lastElementChild.appendChild(E('li', { 'data-value': '-' }, createEl)); - } - - if (this.options.create_markup) - sb.appendChild(E('script', { type: 'item-template' }, - this.options.create_markup)); - - return this.bind(sb); - }, - - bind(sb) { - const o = this.options; - - o.multiple = sb.hasAttribute('multiple'); - o.keep_order = sb.hasAttribute('keep_order'); - o.optional = sb.hasAttribute('optional'); - o.placeholder = sb.getAttribute('placeholder') ?? o.placeholder; - o.display_items = parseInt(sb.getAttribute('display-items') ?? o.display_items); - o.dropdown_items = parseInt(sb.getAttribute('dropdown-items') ?? o.dropdown_items); - o.create_query = sb.getAttribute('item-create') ?? o.create_query; - o.create_template = sb.getAttribute('item-template') ?? o.create_template; - - const ul = sb.querySelector('ul'); - const more = sb.appendChild(E('span', { class: 'more', tabindex: -1 }, '···')); - sb.appendChild(E('span', { class: 'open', tabindex: -1 }, '▾')); - const canary = sb.appendChild(E('div')); - const create = sb.querySelector(this.options.create_query); - let ndisplay = this.options.display_items; - let n = 0; - - if (this.options.multiple) { - let items = ul.querySelectorAll('li'); - - for (let i = 0; i < items.length; i++) { - this.transformItem(sb, items[i]); - - if (items[i].hasAttribute('selected')) { - if (ndisplay-- > 0) - items[i].setAttribute('display', n++); - - if (this.options.keep_order) { - const value = items[i].getAttribute('data-value'); - if (this.values.includes(value)) - items[i].setAttribute('data-order', this.values.indexOf(value) + 1); - } - } - } - } - else { - if (this.options.optional && !ul.querySelector('li[data-value=""]')) { - const placeholder = E('li', { placeholder: '' }, - this.options.select_placeholder ?? this.options.placeholder); - - ul.firstChild - ? ul.insertBefore(placeholder, ul.firstChild) - : ul.appendChild(placeholder); - } - - let items = ul.querySelectorAll('li'); - const sel = sb.querySelectorAll('[selected]'); - - sel.forEach(s => { - s.removeAttribute('selected'); - }); - - const s = sel[0] ?? items[0]; - if (s) { - s.setAttribute('selected', ''); - s.setAttribute('display', n++); - } - - ndisplay--; - } - - this.saveValues(sb, ul); - - ul.setAttribute('tabindex', -1); - sb.setAttribute('tabindex', 0); - - if (ndisplay < 0) - sb.setAttribute('more', '') - else - sb.removeAttribute('more'); - - if (ndisplay == this.options.display_items) - sb.setAttribute('empty', '') - else - sb.removeAttribute('empty'); - - dom.content(more, (ndisplay == this.options.display_items) - ? (this.options.select_placeholder ?? this.options.placeholder) : '···'); - - - sb.addEventListener('click', this.handleClick.bind(this)); - sb.addEventListener('keydown', this.handleKeydown.bind(this)); - sb.addEventListener('cbi-dropdown-close', this.handleDropdownClose.bind(this)); - sb.addEventListener('cbi-dropdown-select', this.handleDropdownSelect.bind(this)); - - if ('ontouchstart' in window) { - sb.addEventListener('touchstart', ev => ev.stopPropagation()); - window.addEventListener('touchstart', this.closeAllDropdowns); - } - else { - sb.addEventListener('focus', this.handleFocus.bind(this)); - - canary.addEventListener('focus', this.handleCanaryFocus.bind(this)); - - window.addEventListener('click', this.closeAllDropdowns); - } - - if (create) { - create.addEventListener('keydown', this.handleCreateKeydown.bind(this)); - create.addEventListener('focus', this.handleCreateFocus.bind(this)); - create.addEventListener('blur', this.handleCreateBlur.bind(this)); - - const li = findParent(create, 'li'); - - li.setAttribute('unselectable', ''); - li.addEventListener('click', this.handleCreateClick.bind(this)); - } - - this.node = sb; - - this.setUpdateEvents(sb, 'cbi-dropdown-open', 'cbi-dropdown-close'); - this.setChangeEvents(sb, 'cbi-dropdown-change', 'cbi-dropdown-close'); - - dom.bindClassInstance(sb, this); + render(/* ... */) { + const sb = funcSed( + ui.Dropdown.prototype.render, + [ + [new RegExp(/('multiple':[^\n]*this\.options\.multiple)/), `'keep_order': this.options.keep_order ? '' : null, $1`] + ], + ['dom'] + ).call(this, dom, ...arguments); return sb; }, - toggleItem(sb, li, force_state) { - const ul = li.parentNode; + bind(/* ... */) { + const sb = funcSed( + ui.Dropdown.prototype.bind, + [ + [new RegExp(/(o\.multiple\s*=)/), `o.keep_order = sb.hasAttribute('keep_order'); $1`], + [ + new RegExp(/(if\s*\(items\[i\]\.hasAttribute\('selected'\)\s*&&\s*ndisplay--\s*>\s*0\)[\n\s]*items\[i\]\.setAttribute\('display',\s*n\+\+\);)/), + `$1 + if (this.options.keep_order) { + const value = items[i].getAttribute('data-value'); + if (items[i].hasAttribute('selected') && this.values.includes(value)) + items[i].setAttribute('data-order', this.values.indexOf(value)); + }` + ] + ], + ['dom'] + ).call(this, dom, ...arguments); - if (li.hasAttribute('unselectable')) - return; - - if (this.options.multiple) { - const cbox = li.querySelector('input[type="checkbox"]'); - const items = li.parentNode.querySelectorAll('li'); - const label = sb.querySelector('ul.preview'); - let sel = li.parentNode.querySelectorAll('[selected]').length; - const more = sb.querySelector('.more'); - let ndisplay = this.options.display_items; - let n = 0; - - if (li.hasAttribute('selected')) { - if (force_state !== true) { - if (sel > 1 || this.options.optional) { - li.removeAttribute('selected'); - cbox.checked = cbox.disabled = false; - sel--; - - if (this.options.keep_order) - li.removeAttribute('data-order'); - } - else { - cbox.disabled = true; - } - } - } - else { - if (force_state !== false) { - li.setAttribute('selected', ''); - cbox.checked = true; - cbox.disabled = false; - sel++; - - if (this.options.keep_order) - li.setAttribute('data-order', ++this.orderCounter); - } - } - - while (label && label.firstElementChild) - label.removeChild(label.firstElementChild); - - for (let i = 0; i < items.length; i++) { - items[i].removeAttribute('display'); - if (items[i].hasAttribute('selected')) { - if (ndisplay-- > 0) { - items[i].setAttribute('display', n++); - if (label) - label.appendChild(items[i].cloneNode(true)); - } - const c = items[i].querySelector('input[type="checkbox"]'); - if (c) - c.disabled = (sel == 1 && !this.options.optional); - } - } - - if (ndisplay < 0) - sb.setAttribute('more', ''); - else - sb.removeAttribute('more'); - - if (ndisplay === this.options.display_items) - sb.setAttribute('empty', ''); - else - sb.removeAttribute('empty'); - - dom.content(more, (ndisplay === this.options.display_items) - ? (this.options.select_placeholder ?? this.options.placeholder) : '···'); - } - else { - let sel = li.parentNode.querySelector('[selected]'); - if (sel) { - sel.removeAttribute('display'); - sel.removeAttribute('selected'); - } - - li.setAttribute('display', 0); - li.setAttribute('selected', ''); - - this.closeDropdown(sb); - } - - this.saveValues(sb, ul); + return sb; }, - saveValues(sb, ul) { - const sel = Array.from(ul.querySelectorAll('li[selected]')); - const div = sb.lastElementChild; - const name = this.options.name; - let strval = ''; - const values = []; + toggleItem(/* ... */) { + const result = funcSed( + ui.Dropdown.prototype.toggleItem, + [ + [ + new RegExp(/(li\.removeAttribute\('selected'\))/), + `if (this.options.keep_order) + li.removeAttribute('data-order'); + $1` + ], + [ + new RegExp(/(li\.setAttribute\('selected',\s*''\))/), + `if (this.options.keep_order) + li.setAttribute('data-order', this.orderCounter++); + $1` + ] + ], + ['dom'] + ).call(this, dom, ...arguments); - if (this.options.keep_order) { - sel.sort((a, b) => - (+a.getAttribute('data-order') || 0) - - (+b.getAttribute('data-order') || 0) - ); - } + return result; + }, - while (div.lastElementChild) - div.removeChild(div.lastElementChild); + saveValues(/* ... */) { + const result = funcSed( + ui.Dropdown.prototype.saveValues, + [ + [ + new RegExp(/(const sel\s*=)([^;\n]+);/), + `$1 Array.from($2); + if (this.options.keep_order) { + sel.sort((a, b) => + (+a.getAttribute('data-order') || 0) - + (+b.getAttribute('data-order') || 0) + ); + }` + ] + ] + ).call(this, ...arguments); - sel.forEach(s => { - if (s.hasAttribute('placeholder')) - return; - - const v = { - text: s.innerText, - value: s.hasAttribute('data-value') ? s.getAttribute('data-value') : s.innerText, - element: s - }; - - div.appendChild(E('input', { - type: 'hidden', - name: name, - value: v.value - })); - - values.push(v); - - strval += strval.length ? ` ${v.value}` : v.value; - }); - - const detail = { - instance: this, - element: sb - }; - - if (this.options.multiple) - detail.values = values; - else - detail.value = values.length ? values[0] : null; - - sb.value = strval; - - sb.dispatchEvent(new CustomEvent('cbi-dropdown-change', { - bubbles: true, - detail: detail - })); + return result; } }); diff --git a/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js b/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js index 9e85450f0..e11aaba59 100644 --- a/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js +++ b/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js @@ -117,7 +117,8 @@ return view.extend({ /* Netfilter flow offload support */ - if (L.hasSystemFeature('offloading')) { + const hasMediatekHNAT = fw4 && runtimeStatus?.hnat_available === true; + if (L.hasSystemFeature('offloading') || hasMediatekHNAT) { s = m.section(form.TypedSection, 'defaults', _('Routing/NAT Offloading'), _('Not fully compatible with QoS/SQM.')); @@ -129,17 +130,22 @@ return view.extend({ o.value('1', _("Software flow offloading"), _('Software based offloading for routing/NAT.')); if (L.hasSystemFeature('offloading_hw')) o.value('2', _("Hardware flow offloading"), _('Hardware based offloading for routing with/without NAT.') + ' ' + _(' Requires hardware NAT support.')); + if (hasMediatekHNAT) + o.value('3', "Mediatek HNAT", "基于Mediatek原厂闭源驱动的HNAT卸载"); o.optional = false; o.load = function (section_id) { const flow_offloading = uci.get('firewall', section_id, 'flow_offloading'); const flow_offloading_hw = uci.get('firewall', section_id, 'flow_offloading_hw'); const has_hw_offloading = L.hasSystemFeature('offloading_hw'); + if (hasMediatekHNAT && flow_offloading === '3') + return '3'; return (flow_offloading === '1') ? ((flow_offloading_hw === '1' && has_hw_offloading) ? '2' : '1') : '0'; }; o.write = function(section_id, value) { - uci.set('firewall', section_id, 'flow_offloading', value === '0' ? null : '1'); + uci.set('firewall', section_id, 'flow_offloading', value === '3' ? '3' : ((value === '1' || value === '2') ? '1' : '0')); + uci.unset('firewall', section_id, 'mediatek_hnat'); uci.set('firewall', section_id, 'flow_offloading_hw', (value === '2' && L.hasSystemFeature('offloading_hw')) ? '1' : null); }; diff --git a/luci-app-firewall/root/usr/libexec/luci-firewall-status b/luci-app-firewall/root/usr/libexec/luci-firewall-status index 18ab673f6..74e77f289 100755 --- a/luci-app-firewall/root/usr/libexec/luci-firewall-status +++ b/luci-app-firewall/root/usr/libexec/luci-firewall-status @@ -2,6 +2,8 @@ tcp_cca="$(cat /proc/sys/net/ipv4/tcp_congestion_control 2>/dev/null)" nat_mode="disabled" +hnat_available=false +[ -e /sys/kernel/debug/hnat/hook_toggle ] && hnat_available=true rules="$(iptables-save -t nat 2>/dev/null)" @@ -19,4 +21,4 @@ else fi fi -printf '{"nat_mode":"%s","tcp_cca":"%s"}\n' "$nat_mode" "$tcp_cca" +printf '{"nat_mode":"%s","tcp_cca":"%s","hnat_available":%s}\n' "$nat_mode" "$tcp_cca" "$hnat_available" diff --git a/luci-app-flowproxy/Makefile b/luci-app-flowproxy/Makefile index e3b546140..aef27ab0f 100644 --- a/luci-app-flowproxy/Makefile +++ b/luci-app-flowproxy/Makefile @@ -1,13 +1,21 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-flowproxy -PKG_VERSION:=1.0.3 -PKG_RELEASE:=1 +PKG_VERSION:=1.0.4 +PKG_RELEASE:=2 LUCI_TITLE:=LuCI Support for FlowProxy Traffic Diversion -LUCI_DEPENDS:=+luci-base +LUCI_DEPENDS:=+luci-base +nftables +kmod-nft-core +ip-full LUCI_PKGARCH:=all +define Package/$(PKG_NAME)/postinst +#!/bin/sh +if [ -z "$${IPKG_INSTROOT}" ]; then + /etc/init.d/rpcd restart +fi +exit 0 +endef + define Package/$(PKG_NAME)/prerm #!/bin/sh if [ -z "$${IPKG_INSTROOT}" ]; then diff --git a/luci-app-flowproxy/htdocs/luci-static/resources/view/flowproxy/flowproxy.js b/luci-app-flowproxy/htdocs/luci-static/resources/view/flowproxy/flowproxy.js index eb1449868..ef40e9c69 100644 --- a/luci-app-flowproxy/htdocs/luci-static/resources/view/flowproxy/flowproxy.js +++ b/luci-app-flowproxy/htdocs/luci-static/resources/view/flowproxy/flowproxy.js @@ -40,6 +40,18 @@ var callRestartService = rpc.declare({ method: 'restart_service' }); +var callGetDatasetInfo = rpc.declare({ + object: 'luci.flowproxy', + method: 'get_dataset_info', + params: ['file_path'] +}); + +var callUpdateDataset = rpc.declare({ + object: 'luci.flowproxy', + method: 'update_dataset', + params: ['url', 'file_path'] +}); + return L.view.extend({ load: function() { return Promise.all([ @@ -189,11 +201,54 @@ return L.view.extend({ s.taboption('settings', form.Flag, 'dns_proxy_enabled', _('Force upstream DNS to proxy server')).rmempty = false; o = s.taboption('settings', form.Value, 'proxy_server_ip_addr', _('Proxy server IP address')); - o.datatype = 'ip4addr'; o.rmempty = false; o.placeholder = '192.168.1.254'; + o.rmempty = true; o.validate = function(section_id, value) { + var isEnabled = (s.formvalue(section_id, 'enabled') === '1'); + + // 1. 如果值为空:仅当启用了服务尝试保存时才提示必填;未启用时允许留空,绝不展示刺眼红框 + if (!value || value.trim() === '') { + if (isEnabled) { + return _('Proxy server IP address is required when FlowProxy is enabled.'); + } + return true; + } + + value = value.trim(); + + // 2. 基础 IPv4 格式校验 + if (!value.match(/^([0-9]{1,3}\.){3}[0-9]{1,3}$/)) { + return _('Invalid IPv4 address format'); + } + var octets = value.split('.'); + for (var oi = 0; oi < 4; oi++) { + var oct = parseInt(octets[oi], 10); + if (isNaN(oct) || oct < 0 || oct > 255) { + return _('Invalid IPv4 address format'); + } + } + + if (value === '127.0.0.1' || value === '0.0.0.0') { + return _('Invalid proxy server IP address.'); + } + + // 3. 拦截本路由器自身的 IP 地址,彻底避免产生自指路由环路导致网络崩溃 + var allLocalIps = []; + for (var k = 0; k < ifaces.length; k++) { + var addrs = ifaces[k].getIPAddrs() || []; + for (var a = 0; a < addrs.length; a++) { + var pureIp = addrs[a].split('/')[0]; + if (pureIp && allLocalIps.indexOf(pureIp) === -1) { + allLocalIps.push(pureIp); + } + } + } + if (allLocalIps.indexOf(value) !== -1) { + return _('The proxy server IP address cannot be the local router IP (%s).').format(value); + } + var ipInCidr = function(ipStr, cidrStr) { - if (!cidrStr || cidrStr.indexOf(':') !== -1 || !ipStr || ipStr.indexOf(':') !== -1) return false; + if (!cidrStr || cidrStr.indexOf(':') !== -1 || !ipStr || ipStr.indexOf(':') !== -1) return null; var parts = cidrStr.split('/'); var cidrIp = parts[0]; var maskBits = parseInt(parts[1] || '32', 10); @@ -209,15 +264,19 @@ return L.view.extend({ var ipNum = ipToNum(ipStr); var cidrNum = ipToNum(cidrIp); - - if (maskBits === 0) return true; + if (maskBits === 0) return { inSubnet: true, isBcastOrNet: false }; var mask = (0xffffffff << (32 - maskBits)) >>> 0; - return (ipNum & mask) === (cidrNum & mask); + var inSubnet = ((ipNum & mask) === (cidrNum & mask)); + var netNum = (cidrNum & mask) >>> 0; + var bcastNum = (netNum | (~mask >>> 0)) >>> 0; + var isBcastOrNet = (maskBits < 31 && (ipNum === netNum || ipNum === bcastNum)); + + return { inSubnet: inSubnet, isBcastOrNet: isBcastOrNet }; }; var selected_interface = s.formvalue(section_id, 'interface'); - if (!value || !selected_interface) return true; + if (!selected_interface) return true; var target_iface = null; for (var i = 0; i < ifaces.length; i++) { @@ -234,9 +293,12 @@ return L.view.extend({ if (!ips || ips.length === 0) return true; var inSubnet = false; + var isBcastOrNet = false; for (var j = 0; j < ips.length; j++) { - if (ipInCidr(value, ips[j])) { + var res = ipInCidr(value, ips[j]); + if (res && res.inSubnet) { inSubnet = true; + if (res.isBcastOrNet) isBcastOrNet = true; break; } } @@ -245,19 +307,60 @@ return L.view.extend({ return _('The proxy server IP address must be within the subnet of the selected interface (%s).').format(ips.join(', ')); } + if (isBcastOrNet) { + return _('The proxy server IP address cannot be the network address or broadcast address.'); + } + return true; }; o = s.taboption('settings', form.Value, 'proxy_server_dns_port', _('Proxy server DNS port')); o.datatype = 'port'; o.default = '5353'; o.rmempty = false; - o = s.taboption('settings', form.ListValue, 'interface', _('Proxy server interface')); - devices.forEach(function(d) { o.value(d.getName(), d.getName()); }); - o.default = 'br-lan'; + var iface_opt = s.taboption('settings', form.ListValue, 'interface', _('Proxy server interface')); + devices.forEach(function(d) { iface_opt.value(d.getName(), d.getName()); }); + iface_opt.default = 'br-lan'; + iface_opt.onchange = function(ev, sid, val) { + var ip_opt = m.lookupOption('proxy_server_ip_addr', sid)[0]; + if (ip_opt) { + var currVal = ip_opt.formvalue(sid); + // 仅当输入框已有非空输入时才触发重新比对子网,避免在空值时派发失焦导致误报红框 + if (currVal && currVal.trim() !== '') { + var el = document.getElementById(ip_opt.cbid(sid)); + if (el) { + var inputEl = el.tagName === 'INPUT' ? el : el.querySelector('input'); + if (inputEl) { + inputEl.dispatchEvent(new CustomEvent('input', { bubbles: true })); + } + } + } + } + }; - s.taboption('settings', form.Value, 'traffic_mark', _('Traffic Mark')).datatype = 'or(uinteger, hex)'; - s.taboption('settings', form.Value, 'routing_table', _('Routing Table ID')).datatype = 'and(uinteger,range(1, 4294967295))'; - s.taboption('settings', form.Value, 'rule_priority', _('Rule Priority')).datatype = 'and(uinteger,range(0, 4294967295))'; + o = s.taboption('settings', form.Value, 'traffic_mark', _('Traffic Mark')); + o.datatype = 'or(uinteger, hex)'; + o.default = '0x666'; + o.validate = function(section_id, value) { + if (!value) return true; + var num = parseInt(value, (typeof value === 'string' && value.indexOf('0x') === 0) ? 16 : 10); + if (isNaN(num) || num <= 0) return _('Traffic mark cannot be 0.'); + return true; + }; + + o = s.taboption('settings', form.Value, 'routing_table', _('Routing Table ID')); + o.datatype = 'and(uinteger,range(1, 4294967295))'; + o.default = '888'; + o.validate = function(section_id, value) { + var num = parseInt(value, 10); + if (num === 0 || num === 253 || num === 254 || num === 255) { + return _('Routing table ID %d is reserved by the system (0, 253, 254, 255). Please use another ID.').format(num); + } + return true; + }; + + o = s.taboption('settings', form.Value, 'rule_priority', _('Rule Priority')); + o.datatype = 'and(uinteger,range(10, 32765))'; + o.default = '1000'; // --- Rules --- var nftsets = uci.sections('flowproxy', 'nftset').map(function(ss) { return '@' + ss['.name']; }); @@ -419,16 +522,14 @@ return L.view.extend({ } return true; }; - // 联动:当 match_type 改变时,强制触发 match_value 的校验 + // 联动:当 match_type 改变时,仅在已有输入内容时重新校验 match_value match_type.onchange = function(ev, sid, val) { var value_input = m.lookupOption('match_value', sid)[0]; if (value_input) setTimeout(function() { var el = document.getElementById(value_input.cbid(sid)); if (el) { var inputEl = el.tagName === 'INPUT' ? el : el.querySelector('input'); - if (inputEl) { - inputEl.dispatchEvent(new CustomEvent('change', { bubbles: true })); - inputEl.dispatchEvent(new CustomEvent('blur', { bubbles: true })); + if (inputEl && inputEl.value && inputEl.value.trim() !== '') { inputEl.dispatchEvent(new CustomEvent('input', { bubbles: true })); } } @@ -460,7 +561,15 @@ return L.view.extend({ var path = uci.get('flowproxy', sid, 'file_path') || '/usr/share/flowproxy/chnroute.txt'; var resNode = E('span', { 'style': 'margin-left: 10px; font-weight: bold; color: #444;', 'id': 'line-count-status' }, [ '...' ]); var input = node.querySelector('input'); if (input) input.parentNode.appendChild(resNode); - fs.exec('/usr/bin/wc', ['-l', path]).then(function(res) { L.dom.content(resNode, (res.code === 0) ? [ _('(%d lines)').format(res.stdout.trim().split(' ')[0]) ] : [ _('(n/a)') ]); }); + callGetDatasetInfo(path).then(function(res) { + if (res && res.exists) { + L.dom.content(resNode, [ _('(%d lines)').format(res.lines) ]); + } else { + L.dom.content(resNode, [ _('(file not found)') ]); + } + }).catch(function() { + L.dom.content(resNode, [ _('(n/a)') ]); + }); return node; }); }; @@ -474,14 +583,21 @@ return L.view.extend({ if (!path) path = '/usr/share/flowproxy/chnroute.txt'; if (!url) { ui.addNotification(null, E('p', _('Please set download_url first')), 'error'); return; } ui.showModal(null, [ E('p', { 'class': 'spinning', 'id': 'download-msg' }, [ _('Downloading chnroute data...') ]) ]); - return fs.exec('/usr/bin/wget', ['-q', '-O', path, url, '--timeout=10', '--no-check-certificate']).then(function(res) { + return callUpdateDataset(url, path).then(function(res) { var msgEl = document.getElementById('download-msg'); - if (res.code === 0) { + if (res && res.success) { if (msgEl) { msgEl.classList.remove('spinning'); L.dom.content(msgEl, [ _('Updated successfully.') ]); } - var cn = document.getElementById('line-count-status'); if (cn) fs.exec('/usr/bin/wc', ['-l', path]).then(function(r) { L.dom.content(cn, (r.code === 0) ? [ _('(%d lines)').format(r.stdout.trim().split(' ')[0]) ] : [ _('(n/a)') ]); }); + var cn = document.getElementById('line-count-status'); + if (cn) L.dom.content(cn, [ _('(%d lines)').format(res.lines || 0) ]); setTimeout(ui.hideModal, 1500); - } else { ui.hideModal(); ui.addNotification(null, E('p', _('Download failed')), 'error'); } - }).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', _('Error: %s').format(e.message)), 'error'); }); + } else { + ui.hideModal(); + ui.addNotification(null, E('p', _('Download failed: %s').format(res ? res.error : 'Unknown')), 'error'); + } + }).catch(function(e) { + ui.hideModal(); + ui.addNotification(null, E('p', _('Error: %s').format(e.message || String(e))), 'error'); + }); }; var sg = s.taboption('lists', form.SectionValue, '_list_custom', form.GridSection, 'nftset', _('Custom nftables sets')); diff --git a/luci-app-flowproxy/po/templates/flowproxy.pot b/luci-app-flowproxy/po/templates/flowproxy.pot index 0734ed227..72e7b05a9 100644 --- a/luci-app-flowproxy/po/templates/flowproxy.pot +++ b/luci-app-flowproxy/po/templates/flowproxy.pot @@ -118,7 +118,7 @@ msgstr "" msgid "Match Value" msgstr "" -msgid "Network interface" +msgid "Proxy server interface" msgstr "" msgid "Please set download_url first" @@ -145,6 +145,9 @@ msgstr "" msgid "Routing Table ID" msgstr "" +msgid "Rule Priority" +msgstr "" + msgid "Rules" msgstr "" @@ -226,3 +229,51 @@ msgstr "" msgid "this will delete ALL current %s rules and generate default templates. are you sure?" msgstr "" +msgid "(file not found)" +msgstr "" + +msgid "Download failed: %s" +msgstr "" + +msgid "The proxy server IP address must be within the subnet of the selected interface (%s)." +msgstr "" + +msgid "Invalid proxy server IP address." +msgstr "" + +msgid "The proxy server IP address cannot be the local router IP (%s)." +msgstr "" + +msgid "The proxy server IP address cannot be the network address or broadcast address." +msgstr "" + +msgid "Traffic mark cannot be 0." +msgstr "" + +msgid "Routing table ID %d is reserved by the system (0, 253, 254, 255). Please use another ID." +msgstr "" + +msgid "Restart" +msgstr "" + +msgid "Restarting FlowProxy service..." +msgstr "" + +msgid "FlowProxy service restarted successfully." +msgstr "" + +msgid "Failed to restart FlowProxy service." +msgstr "" + +msgid "RPC error: %s" +msgstr "" + +msgid "Proxy server IP address is required when FlowProxy is enabled." +msgstr "" + +msgid "Invalid IPv4 address format" +msgstr "" + + + + diff --git a/luci-app-flowproxy/po/zh_Hans/flowproxy.po b/luci-app-flowproxy/po/zh_Hans/flowproxy.po index 1faa8dfff..df1f7a7a1 100644 --- a/luci-app-flowproxy/po/zh_Hans/flowproxy.po +++ b/luci-app-flowproxy/po/zh_Hans/flowproxy.po @@ -250,3 +250,32 @@ msgstr "RPC 错误: %s" msgid "The proxy server IP address must be within the subnet of the selected interface (%s)." msgstr "代理服务器 IP 地址必须在所选网络接口的子网网段之内 (%s)。" +msgid "(file not found)" +msgstr "(文件不存在)" + +msgid "Download failed: %s" +msgstr "下载失败: %s" + +msgid "Invalid proxy server IP address." +msgstr "无效的代理服务器 IP 地址。" + +msgid "The proxy server IP address cannot be the local router IP (%s)." +msgstr "代理服务器 IP 地址不能为本路由器自身 IP (%s)。" + +msgid "The proxy server IP address cannot be the network address or broadcast address." +msgstr "代理服务器 IP 地址不能为网络地址或广播地址。" + +msgid "Traffic mark cannot be 0." +msgstr "流量标记不能为 0。" + +msgid "Routing table ID %d is reserved by the system (0, 253, 254, 255). Please use another ID." +msgstr "路由表 ID %d 为系统保留路由表 (0, 253, 254, 255),请使用其他 ID。" + +msgid "Proxy server IP address is required when FlowProxy is enabled." +msgstr "启用代理分流时,代理服务器 IP 地址为必填项。" + +msgid "Invalid IPv4 address format" +msgstr "无效的 IPv4 地址格式" + + + diff --git a/luci-app-flowproxy/root/etc/config/flowproxy b/luci-app-flowproxy/root/etc/config/flowproxy index a49b7b1b3..8b3c0c6d2 100644 --- a/luci-app-flowproxy/root/etc/config/flowproxy +++ b/luci-app-flowproxy/root/etc/config/flowproxy @@ -102,3 +102,4 @@ config nftset 'no_proxy_dst_tcp_ports' config nftset 'no_proxy_dst_udp_ports' option enabled '1' option type 'inet_service' + list elements '123' diff --git a/luci-app-flowproxy/root/etc/hotplug.d/iface/99-flowproxy b/luci-app-flowproxy/root/etc/hotplug.d/iface/99-flowproxy new file mode 100755 index 000000000..ed0823bd4 --- /dev/null +++ b/luci-app-flowproxy/root/etc/hotplug.d/iface/99-flowproxy @@ -0,0 +1,13 @@ +#!/bin/sh +# Hotplug trigger for flowproxy to restore policy routing on interface reload + +[ "$ACTION" = "ifup" ] || exit 0 + +enabled=$(uci -q get flowproxy.global.enabled || echo 0) +[ "$enabled" = "1" ] || exit 0 + +target_iface=$(uci -q get flowproxy.global.interface || echo "br-lan") + +if [ "$DEVICE" = "$target_iface" ] || [ "$INTERFACE" = "$target_iface" ]; then + [ -x /etc/init.d/flowproxy ] && /etc/init.d/flowproxy add_route +fi diff --git a/luci-app-flowproxy/root/etc/init.d/flowproxy b/luci-app-flowproxy/root/etc/init.d/flowproxy index 53cd40964..6d11580d0 100755 --- a/luci-app-flowproxy/root/etc/init.d/flowproxy +++ b/luci-app-flowproxy/root/etc/init.d/flowproxy @@ -4,6 +4,8 @@ START=99 STOP=10 USE_PROCD=1 +EXTRA_COMMANDS="add_route" +EXTRA_HELP=" add_route Add routing rules and policy" NAME="flowproxy" UCI_CONFIG="flowproxy" @@ -57,9 +59,62 @@ check_env() { check_config() { local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr") [ -z "$proxy_server_ip_addr" ] && { log_msg error "proxy_server_ip_addr not configured"; return 1; } + + if [ "$proxy_server_ip_addr" = "127.0.0.1" ] || [ "$proxy_server_ip_addr" = "0.0.0.0" ]; then + log_msg error "Invalid proxy_server_ip_addr: $proxy_server_ip_addr"; return 1; + fi + + # 纵深防御:严禁将代理服务器设为本路由器自身的任一 IP,防止策略路由自指死循环导致网络崩溃 + if ip -4 -o addr show 2>/dev/null | awk '{print $4}' | cut -d/ -f1 | grep -Fxq "$proxy_server_ip_addr"; then + log_msg error "proxy_server_ip_addr ($proxy_server_ip_addr) is a local IP of this router, aborting to prevent routing loop" + return 1 + fi + + # 检查路由表 ID 是否为系统保留表 (0, 253, 254, 255) + local routing_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888") + if [ "$routing_table" = "0" ] || [ "$routing_table" = "253" ] || [ "$routing_table" = "254" ] || [ "$routing_table" = "255" ]; then + log_msg error "routing_table ($routing_table) is a reserved system table (0, 253, 254, 255), aborting" + return 1 + fi + return 0 } +probe_proxy_server() { + local proxy_ip="$1" + local proxy_dns_port="$2" + local interface="$3" + + log_msg info "Probing proxy server $proxy_ip on interface $interface..." + + # 1. 检查 ARP 缓存或快速唤醒以获取 MAC 地址 + local mac="" + if [ -f "/proc/net/arp" ]; then + mac=$(awk -v ip="$proxy_ip" '$1 == ip { print $4; exit }' /proc/net/arp) + fi + + if [ -n "$mac" ] && [ "$mac" != "00:00:00:00:00:00" ]; then + log_msg info "Proxy server is REACHABLE: IP=$proxy_ip, MAC=$mac (found in ARP cache)" + else + # 快速单发 ping 1 秒唤醒 ARP 学习 + ping -c 1 -W 1 "$proxy_ip" >/dev/null 2>&1 + if [ -f "/proc/net/arp" ]; then + mac=$(awk -v ip="$proxy_ip" '$1 == ip { print $4; exit }' /proc/net/arp) + fi + if [ -n "$mac" ] && [ "$mac" != "00:00:00:00:00:00" ]; then + log_msg info "Proxy server is REACHABLE: IP=$proxy_ip, MAC=$mac" + else + log_msg warn "Proxy server $proxy_ip is NOT responding to ARP yet (it may be booting or offline, traffic will still be routed via policy table)" + fi + fi + + # 2. 若启用了上游 DNS 代理,在日志中记录目标地址与端口 + local dns_proxy=$(uci -q get "$UCI_CONFIG.global.dns_proxy_enabled" || echo 0) + if [ "$dns_proxy" = "1" ]; then + log_msg info "Upstream DNS redirection target configured: $proxy_ip#$proxy_dns_port" + fi +} + apply_nft_rules() { log_msg info "Generating nftables configuration..." local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level" || echo "info") @@ -100,11 +155,17 @@ apply_nft_rules() { } add_route() { + check_config || return 1 + local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr") - local interface=$(uci -q get "$UCI_CONFIG.global.interface" || echo "br-lan") - local traffic_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark" || echo "0x666") - local routing_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888") - local rule_priority=$(uci -q get "$UCI_CONFIG.global.rule_priority" || echo "1000") + local interface=$(uci -q get "$UCI_CONFIG.global.interface") + interface="${interface:-br-lan}" + local traffic_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark") + traffic_mark="${traffic_mark:-0x666}" + local routing_table=$(uci -q get "$UCI_CONFIG.global.routing_table") + routing_table="${routing_table:-888}" + local rule_priority=$(uci -q get "$UCI_CONFIG.global.rule_priority") + rule_priority="${rule_priority:-1000}" # 1. 先尝试用旧记录清理一次,防止由于配置变更导致的残留 delete_route "quiet" @@ -127,6 +188,12 @@ add_route() { else log_msg error "Failed to add ip rule: $err" fi + + # 5. 禁用目标网卡的 ICMP 重定向通告,防止客户端绕行或 TCP 连接挂起 + if [ -d "/proc/sys/net/ipv4/conf/$interface" ]; then + sysctl -w "net.ipv4.conf.$interface.send_redirects=0" >/dev/null 2>&1 + log_msg debug "Disabled send_redirects for $interface" + fi } delete_route() { @@ -143,9 +210,14 @@ delete_route() { [ "$quiet" != "quiet" ] && rm -f "/var/run/flowproxy.mark" fi - local curr_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark" || echo "0x666") - local curr_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888") - local curr_prio=$(uci -q get "$UCI_CONFIG.global.rule_priority" || echo "1000") + local curr_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark") + curr_mark="${curr_mark:-0x666}" + local curr_table=$(uci -q get "$UCI_CONFIG.global.routing_table") + curr_table="${curr_table:-888}" + local curr_prio=$(uci -q get "$UCI_CONFIG.global.rule_priority") + curr_prio="${curr_prio:-1000}" + local curr_iface=$(uci -q get "$UCI_CONFIG.global.interface") + curr_iface="${curr_iface:-br-lan}" [ "$quiet" != "quiet" ] && log_msg info "Cleaning up FlowProxy routes (Atomic Mode)..." @@ -153,6 +225,10 @@ delete_route() { safe_del_rule() { local m="$1" t="$2" p="$3" [ -z "$m" ] || [ -z "$t" ] && return + if [ "$t" = "0" ] || [ "$t" = "253" ] || [ "$t" = "254" ] || [ "$t" = "255" ]; then + log_msg error "Refusing to flush reserved system routing table $t" + return + fi log_msg debug "Attempting cleanup: mark=$m, table=$t, priority=${p:-any}" # 精准清理 (带优先级) [ -n "$p" ] && while ip rule del fwmark "$m" lookup "$t" priority "$p" 2>/dev/null; do @@ -171,6 +247,11 @@ delete_route() { safe_del_rule "$old_mark" "$old_table" "$old_prio" [ "$old_mark" != "$curr_mark" ] || [ "$old_table" != "$curr_table" ] && \ safe_del_rule "$curr_mark" "$curr_table" "$curr_prio" + + # 恢复 ICMP 重定向通告 + if [ -d "/proc/sys/net/ipv4/conf/$curr_iface" ]; then + sysctl -w "net.ipv4.conf.$curr_iface.send_redirects=1" >/dev/null 2>&1 + fi } delete_nft() { @@ -192,64 +273,46 @@ setup_dns() { [ -z "$proxy_server_ip_addr" ] && { log_msg error "proxy_server_ip_addr not configured, cannot setup DNS"; return 1; } - log_msg info "Setting up DNS redirection to $proxy_server_ip_addr#$proxy_server_dns_port..." + log_msg info "Setting up DNS redirection to $proxy_server_ip_addr#$proxy_server_dns_port via drop-in config..." - # Persistent backup in UCI - if ! uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then - local old_noresolv=$(uci -q get dhcp.@dnsmasq[0].noresolv) - local old_dns_redirect=$(uci -q get dhcp.@dnsmasq[0].dns_redirect) - local old_servers=$(uci -q get dhcp.@dnsmasq[0].server | tr '\n' ',' | sed 's/,$//') - - log_msg debug "Backing up DNS: noresolv=${old_noresolv:-empty}, redirect=${old_dns_redirect:-empty}, servers=${old_servers:-empty}" - - uci set "$UCI_CONFIG.dns_backup=dns_backup" - [ -n "$old_noresolv" ] && uci set "$UCI_CONFIG.dns_backup.noresolv=$old_noresolv" - [ -n "$old_dns_redirect" ] && uci set "$UCI_CONFIG.dns_backup.dns_redirect=$old_dns_redirect" - [ -n "$old_servers" ] && uci set "$UCI_CONFIG.dns_backup.servers=$old_servers" - uci commit "$UCI_CONFIG" + mkdir -p /tmp/dnsmasq.d + cat > /tmp/dnsmasq.d/flowproxy.conf << EOF +# Generated by FlowProxy - DO NOT EDIT MANUALLY +no-resolv +server=$proxy_server_ip_addr#$proxy_server_dns_port +EOF + + # 同步分发至各个可能存在的 dnsmasq 实例目录 + for dir in /tmp/dnsmasq*.d; do + if [ -d "$dir" ] && [ "$dir" != "/tmp/dnsmasq.d" ]; then + cp -f /tmp/dnsmasq.d/flowproxy.conf "$dir/flowproxy.conf" + fi + done + + # 历史遗留自愈:若早期版本存在残留的 dns_backup 节,静默移除避免脏数据 + if uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then + uci -q delete "$UCI_CONFIG.dns_backup" + uci -q commit "$UCI_CONFIG" fi - log_msg debug "Applying new DNS: noresolv=1, redirect=0, server=$proxy_server_ip_addr#$proxy_server_dns_port" - uci set dhcp.@dnsmasq[0].noresolv='1' - uci set dhcp.@dnsmasq[0].dns_redirect='0' - - # Delete current servers and add our proxy - uci -q delete dhcp.@dnsmasq[0].server - uci add_list dhcp.@dnsmasq[0].server="$proxy_server_ip_addr#$proxy_server_dns_port" - - uci commit dhcp /etc/init.d/dnsmasq restart + log_msg info "DNS drop-in configuration applied successfully" } cleanup_dns() { + log_msg info "Cleaning up DNS redirection drop-in configuration..." + rm -f /tmp/dnsmasq.d/flowproxy.conf + for dir in /tmp/dnsmasq*.d; do + [ -d "$dir" ] && rm -f "$dir/flowproxy.conf" + done + + # 历史遗留自愈 if uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then - log_msg info "Restoring original DNS configuration..." - - local noresolv=$(uci -q get "$UCI_CONFIG.dns_backup.noresolv") - local dns_redirect=$(uci -q get "$UCI_CONFIG.dns_backup.dns_redirect") - local servers=$(uci -q get "$UCI_CONFIG.dns_backup.servers") - - log_msg debug "Restoring DNS values: noresolv=${noresolv:-0}, redirect=${dns_redirect:-1}, servers=${servers:-empty}" - - # Restore noresolv (default to 0 if not backed up) - [ -n "$noresolv" ] && uci set dhcp.@dnsmasq[0].noresolv="$noresolv" || uci set dhcp.@dnsmasq[0].noresolv="0" - - # Restore dns_redirect (default to 1 as requested for OpenWrt typical modded builds) - [ -n "$dns_redirect" ] && uci set dhcp.@dnsmasq[0].dns_redirect="$dns_redirect" || uci set dhcp.@dnsmasq[0].dns_redirect="1" - - # Restore servers - uci -q delete dhcp.@dnsmasq[0].server - if [ -n "$servers" ]; then - for s in $servers; do - [ -n "$s" ] && uci add_list dhcp.@dnsmasq[0].server="$s" - done - fi - - uci commit dhcp - uci delete "$UCI_CONFIG.dns_backup" - uci commit "$UCI_CONFIG" - /etc/init.d/dnsmasq restart + uci -q delete "$UCI_CONFIG.dns_backup" + uci -q commit "$UCI_CONFIG" fi + + /etc/init.d/dnsmasq restart } start_service() { @@ -265,6 +328,12 @@ start_service() { check_env || return 1 check_config || return 1 + + local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr") + local proxy_server_dns_port=$(uci -q get "$UCI_CONFIG.global.proxy_server_dns_port" || echo 5353) + local interface=$(uci -q get "$UCI_CONFIG.global.interface" || echo "br-lan") + probe_proxy_server "$proxy_server_ip_addr" "$proxy_server_dns_port" "$interface" + apply_nft_rules || return 1 add_route setup_dns @@ -272,6 +341,7 @@ start_service() { touch "$STATE_FILE" log_msg info "FlowProxy service started successfully" } + stop_service() { log_msg info "Stopping FlowProxy..." # 强制先移除标记,防止状态误报 @@ -282,6 +352,12 @@ stop_service() { log_msg info "FlowProxy service stopped" } +reload_service() { + log_msg info "Reloading FlowProxy..." + stop_service + start_service +} + restart_service() { stop_service start_service @@ -292,8 +368,7 @@ service_triggers() { procd_add_reload_trigger "$UCI_CONFIG" # 监听防火墙重载事件,防止规则被 fw4 清空后无法自动恢复 procd_add_reload_trigger "firewall" -} - -reload_service() { - restart + # 监听网络接口事件 + local iface=$(uci -q get "$UCI_CONFIG.global.interface" || echo "br-lan") + procd_add_reload_interface_trigger "$iface" } diff --git a/luci-app-flowproxy/root/usr/share/flowproxy/generate_nft.sh b/luci-app-flowproxy/root/usr/share/flowproxy/generate_nft.sh index dcf0e1f18..993dc248f 100755 --- a/luci-app-flowproxy/root/usr/share/flowproxy/generate_nft.sh +++ b/luci-app-flowproxy/root/usr/share/flowproxy/generate_nft.sh @@ -44,20 +44,15 @@ gen_set_definition() { [ -n "$uci_elems" ] && { [ -n "$elems" ] && elems="${elems}, "; elems="${elems}${uci_elems}"; } file_path=$(uci -q get "$CONFIG.$section.file_path") - download_url=$(uci -q get "$CONFIG.$section.download_url") - if [ -n "$download_url" ] && [ -n "$file_path" ]; then - if [ ! -f "$file_path" ]; then - echo "Downloading $section data from $download_url to $file_path..." >&2 - wget -q -O "$file_path" "$download_url" --timeout=10 --no-check-certificate - [ $? -ne 0 ] && { echo "Failed to download $section data" >&2; rm -f "$file_path"; } - fi - fi - - if [ -n "$file_path" ] && [ -f "$file_path" ]; then - local file_elems=$(get_file_elements "$file_path") - if [ -n "$file_elems" ]; then - [ -n "$elems" ] && elems="${elems}, " - elems="${elems}${file_elems}" + if [ -n "$file_path" ]; then + if [ -f "$file_path" ]; then + local file_elems=$(get_file_elements "$file_path") + if [ -n "$file_elems" ]; then + [ -n "$elems" ] && elems="${elems}, " + elems="${elems}${file_elems}" + fi + else + log_debug "Set file $file_path not found, continuing without file elements" fi fi @@ -183,15 +178,19 @@ TCP_ENABLED=$(uci -q get "$CONFIG.global.tcp_enabled" || echo "1") UDP_ENABLED=$(uci -q get "$CONFIG.global.udp_enabled" || echo "1") TRAFFIC_MARK=$(uci -q get "$CONFIG.global.traffic_mark" || echo "0x666") PROXY_SERVER_IP_ADDR=$(uci -q get "$CONFIG.global.proxy_server_ip_addr") +INTERFACE=$(uci -q get "$CONFIG.global.interface") +[ -z "$INTERFACE" ] && INTERFACE="br-lan" if [ "$TCP_ENABLED" = "1" ]; then cat >> "$OUTPUT_FILE" << EOF chain LAN_MARKFLOW_TCP { type filter hook prerouting priority mangle; policy accept; + iifname != "$INTERFACE" return fib daddr type { unspec, local, anycast, multicast } counter return EOF if [ -n "$PROXY_SERVER_IP_ADDR" ]; then - echo " ip saddr $PROXY_SERVER_IP_ADDR ip protocol tcp counter return" >> "$OUTPUT_FILE" + echo " ip saddr $PROXY_SERVER_IP_ADDR counter return" >> "$OUTPUT_FILE" + echo " ip daddr $PROXY_SERVER_IP_ADDR counter return" >> "$OUTPUT_FILE" fi for s in $SECTIONS_TCP; do process_rule "$s" "tcp" >> "$OUTPUT_FILE"; done echo " ip protocol tcp counter meta mark set $TRAFFIC_MARK" >> "$OUTPUT_FILE" @@ -202,10 +201,12 @@ if [ "$UDP_ENABLED" = "1" ]; then cat >> "$OUTPUT_FILE" << EOF chain LAN_MARKFLOW_UDP { type filter hook prerouting priority mangle; policy accept; + iifname != "$INTERFACE" return fib daddr type { unspec, local, anycast, multicast } counter return EOF if [ -n "$PROXY_SERVER_IP_ADDR" ]; then - echo " ip saddr $PROXY_SERVER_IP_ADDR ip protocol udp counter return" >> "$OUTPUT_FILE" + echo " ip saddr $PROXY_SERVER_IP_ADDR counter return" >> "$OUTPUT_FILE" + echo " ip daddr $PROXY_SERVER_IP_ADDR counter return" >> "$OUTPUT_FILE" fi for s in $SECTIONS_UDP; do process_rule "$s" "udp" >> "$OUTPUT_FILE"; done echo " ip protocol udp counter meta mark set $TRAFFIC_MARK" >> "$OUTPUT_FILE" diff --git a/luci-app-flowproxy/root/usr/share/rpcd/acl.d/luci-app-flowproxy.json b/luci-app-flowproxy/root/usr/share/rpcd/acl.d/luci-app-flowproxy.json index c9d691f19..dac28927e 100644 --- a/luci-app-flowproxy/root/usr/share/rpcd/acl.d/luci-app-flowproxy.json +++ b/luci-app-flowproxy/root/usr/share/rpcd/acl.d/luci-app-flowproxy.json @@ -13,11 +13,6 @@ }, "write": { "uci": [ "flowproxy" ], - "file": { - "/usr/bin/wget": [ "exec" ], - "/usr/bin/wc": [ "exec" ], - "/usr/share/flowproxy/generate_nft.sh": [ "exec" ] - }, "ubus": { "luci.flowproxy": [ "*" ] } diff --git a/luci-app-flowproxy/root/usr/share/rpcd/ucode/luci.flowproxy b/luci-app-flowproxy/root/usr/share/rpcd/ucode/luci.flowproxy index 16893ce19..7e0d34bf1 100755 --- a/luci-app-flowproxy/root/usr/share/rpcd/ucode/luci.flowproxy +++ b/luci-app-flowproxy/root/usr/share/rpcd/ucode/luci.flowproxy @@ -1,6 +1,6 @@ 'use strict'; -import { access, readfile, writefile, stat } from 'fs'; +import { access, readfile, writefile, stat, popen } from 'fs'; import { cursor } from 'uci'; import { connect } from 'ubus'; @@ -9,6 +9,7 @@ const STATE_FILE = '/var/run/flowproxy.running'; const IP_CACHE = '/tmp/flowproxy/lan_ip.cache'; const LOG_FILE = '/var/log/flowproxy.log'; const NFT_TABLE = 'flowproxy'; +const DNS_DROPIN_FILE = '/tmp/dnsmasq.d/flowproxy.conf'; // 辅助函数:安全 trim function safe_trim(str) { @@ -18,10 +19,10 @@ function safe_trim(str) { const methods = { get_status: { call: function() { - // 1. 快速探测 + // 1. 快速探测标记文件 let is_running = access(STATE_FILE, 'f'); - // 2. 自愈:检查内核状态并同步标记文件 + // 2. 自愈:检查内核状态 (nft 链与策略路由) let tcp_active = (system('nft list chain inet ' + NFT_TABLE + ' LAN_MARKFLOW_TCP >/dev/null 2>&1') === 0); let udp_active = (system('nft list chain inet ' + NFT_TABLE + ' LAN_MARKFLOW_UDP >/dev/null 2>&1') === 0); let real_running = (tcp_active || udp_active); @@ -40,7 +41,6 @@ const methods = { if (ubus_conn) { let ifaces = ubus_conn.call('network.interface', 'dump'); if (type(ifaces?.interface) === 'array') { - // 优先寻找名为 'lan' 的接口,或者寻找拥有 ipv4 地址的第一个非回环接口 let fallback_ip = ''; for (let iface in ifaces.interface) { if (iface.interface === 'lan' && iface['ipv4-address']?.[0]?.address) { @@ -64,9 +64,8 @@ const methods = { } let uci_cursor = cursor(); - let proxy_server_ip_addr = uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_ip_addr') || ''; - let dns_running = (uci_cursor.get(UCI_CONFIG, 'dns_backup') ? 1 : 0); + let dns_running = access(DNS_DROPIN_FILE, 'f') ? 1 : 0; return { running: real_running ? 1 : 0, @@ -114,6 +113,67 @@ const methods = { } }, + get_dataset_info: { + args: { file_path: 'string' }, + call: function(request) { + let file_path = request.args?.file_path || '/usr/share/flowproxy/chnroute.txt'; + if (!match(file_path, /^\/(usr\/share\/flowproxy|tmp\/flowproxy)\/[a-zA-Z0-9_.-]+$/)) { + return { error: 'Invalid path' }; + } + if (!access(file_path, 'r')) { + return { exists: false, lines: 0, size: 0 }; + } + let s = stat(file_path); + let lines = 0; + let fd = popen(sprintf("wc -l '%s' 2>/dev/null", file_path)); + if (fd) { + let out = fd.read('all'); + fd.close(); + if (out) { + let parts = split(trim(out), /\s+/); + lines = int(parts[0]) || 0; + } + } + return { exists: true, lines: lines, size: s ? s.size : 0 }; + } + }, + + update_dataset: { + args: { url: 'string', file_path: 'string' }, + call: function(request) { + let url = request.args?.url; + let file_path = request.args?.file_path || '/usr/share/flowproxy/chnroute.txt'; + if (!url || !match(url, /^https?:\/\/[A-Za-z0-9_.\/:~%#?&=+-]+$/)) { + return { success: false, error: 'Invalid URL' }; + } + if (!match(file_path, /^\/(usr\/share\/flowproxy|tmp\/flowproxy)\/[a-zA-Z0-9_.-]+$/)) { + return { success: false, error: 'Invalid target file path' }; + } + let tmp_file = file_path + '.tmp'; + let cmd = sprintf("wget -q -O '%s' '%s' --timeout=15 --no-check-certificate 2>&1", tmp_file, url); + let rc = system(cmd); + if (rc === 0 && access(tmp_file, 'f')) { + let s = stat(tmp_file); + if (s && s.size > 0) { + system(sprintf("mv -f '%s' '%s'", tmp_file, file_path)); + let lines = 0; + let fd = popen(sprintf("wc -l '%s' 2>/dev/null", file_path)); + if (fd) { + let out = fd.read('all'); + fd.close(); + if (out) { + let parts = split(trim(out), /\s+/); + lines = int(parts[0]) || 0; + } + } + return { success: true, lines: lines }; + } + } + system(sprintf("rm -f '%s'", tmp_file)); + return { success: false, error: 'Download failed or file empty' }; + } + }, + generate_nft_config: { call: function() { system('mkdir -p /tmp/flowproxy'); @@ -125,19 +185,18 @@ const methods = { get_runtime_config: { call: function() { let res_file = '/tmp/flowproxy/runtime.tmp'; - let dns_file = '/tmp/flowproxy/dns_runtime.tmp'; - system('mkdir -p /tmp/flowproxy'); system('/usr/share/flowproxy/generate_nft.sh runtime > ' + res_file + ' 2>&1'); let nft_runtime = readfile(res_file) || "(failed)"; system('rm -f ' + res_file); - system('uci show dhcp.@dnsmasq[0] | grep -E "noresolv|dns_redirect|server" | sed -E "s/^dhcp\\.[^.]*\\.([^=]*)=(.*)/\\1: \\2/; s/\\x27//g; s/#/:/g" > ' + dns_file + ' 2>&1'); - let dns_out = readfile(dns_file); - system('rm -f ' + dns_file); - - let dns_runtime = "\n--- [ DNSmasq Runtime Configuration ] ---\n"; - dns_runtime += (dns_out || "(no active dnsmasq overrides)"); + let dns_runtime = "\n--- [ DNSmasq Drop-in Runtime Configuration ] ---\n"; + let dns_content = readfile(DNS_DROPIN_FILE); + if (dns_content) { + dns_runtime += dns_content; + } else { + dns_runtime += "(no active dnsmasq drop-in overrides)"; + } return { runtime: safe_trim(nft_runtime + dns_runtime) }; } @@ -149,7 +208,6 @@ const methods = { try { let ubus_conn = connect(); if (ubus_conn) { - // 使用 network.device status 以获取所有物理设备 (包括 eth0 等 bridge 成员) let dev_status = ubus_conn.call('network.device', 'status'); if (dev_status) { for (let name in dev_status) { diff --git a/luci-app-fwlive/Makefile b/luci-app-fwlive/Makefile index 51b89fe58..32c008090 100644 --- a/luci-app-fwlive/Makefile +++ b/luci-app-fwlive/Makefile @@ -11,8 +11,8 @@ LUCI_DESCRIPTION:=Live firewall log table (firewall4/nft; iptables-tagged lines LUCI_DEPENDS:=+luci-base +logd +jsonfilter LUCI_PKGARCH:=all -PKG_VERSION:=0.1.49 -PKG_RELEASE:=55 +PKG_VERSION:=0.1.50 +PKG_RELEASE:=56 # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). ifdef SOURCE_DATE_EPOCH diff --git a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js index f771e9f8b..d7f74848f 100644 --- a/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js +++ b/luci-app-fwlive/htdocs/luci-static/resources/fwlive/constants.js @@ -9,7 +9,7 @@ */ return baseclass.extend({ /* Keep in sync with openwrt-feed/luci-app-fwlive/Makefile PKG_VERSION. */ - APP_VERSION: '0.1.49', + APP_VERSION: '0.1.50', ROW_LIMIT_OPTIONS: [25, 50, 100, 250, 500, 1000, 2000], DEFAULT_ROW_LIMIT: 100, FETCH_MODE_OPTIONS: ['auto', 'manual'], diff --git a/luci-app-homeproxy-pro/Makefile b/luci-app-homeproxy-pro/Makefile index 1182ac49d..8b299046d 100644 --- a/luci-app-homeproxy-pro/Makefile +++ b/luci-app-homeproxy-pro/Makefile @@ -6,12 +6,6 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64 (sing-box 1.14) -# Pure ucode/JS payload with no compiled code, so the package itself is arch -# independent. The real arch constraint comes from the +sing-box dependency, -# which the feed builds for every architecture Go supports (aarch64, arm, -# mipsel, riscv64, x86_64, ...); where no sing-box package exists, dependency -# resolution refuses the install. Checked against the ImmortalWrt 25.12.1 -# package index (sing-box 1.14.0-r1 published for all of the above). LUCI_PKGARCH:=all LUCI_DEPENDS:= \ +sing-box \ @@ -23,14 +17,10 @@ LUCI_DEPENDS:= \ PKG_NAME:=luci-app-homeproxy-pro PKG_VERSION:=28.10.1.14 -PKG_RELEASE:=15 +PKG_RELEASE:=16 + +LUCI_BASENAME:=homeproxy-pro -# Only files the package actually ships belong here. The previous list named -# four paths that exist nowhere in the tree - certs/ and ruleset/ are created at -# runtime, and the two *_list.txt files are written by the LuCI ACL editor - so -# those declarations did nothing. What is listed instead is the resource data -# the router updates by itself: on a running device those files are newer than -# the ones in the package, and without this an upgrade would overwrite them. define Package/luci-app-homeproxy-pro/conffiles /etc/config/homeproxy-pro /etc/homeproxy-pro/resources/china_ip4.txt diff --git a/luci-app-homeproxy-pro/README.md b/luci-app-homeproxy-pro/README.md index 7ccab198f..8df86a8ec 100644 --- a/luci-app-homeproxy-pro/README.md +++ b/luci-app-homeproxy-pro/README.md @@ -27,7 +27,7 @@ | 后端架构 | 单文件巨型(`generate_client.uc` 1217 行 / 40 KB;`parse_uri.uc` ~400 行 / 15 KB) | orchestrator + 5 子层(`parser` / `config` / `generator` / `subscription` / `runtime`)+ 公共库(`homeproxy-pro.uc` / `firewall_utils.uc`)+ table-driven adapters | `root/etc/homeproxy-pro/scripts/{config,parser,generator,subscription,runtime}/` | | 前端架构 | 单一 66 KB `client.js` | `client.js` thin 入口 + 8 个 Tab 模块(`access` / `common` / `dns` / `nodes` / `routing` / `subscription` / `tun_dns` / `udp_nat`)+ 共享 helpers + 集中 `RPC.declare` | `htdocs/luci-static/resources/view/homeproxy-pro/client/` | | 协议建模 | 单 `parse_uri.uc` 内 13 个 scheme 分支 | `parser/{uri,flatten,normalize,mapping,protocols,validator}.uc` 6 个文件 + canonical Node 模型 + adapter table | `root/etc/homeproxy-pro/scripts/parser/` + `config/{model,adapter}.uc` | -| Tab 模块化 | 1 个 `client.js` 含 12 个 Tab 渲染 | `homeproxy-pro.js` (1153 行 helpers) + 4 顶层 view + 8 个 `client/*.js`;helpers 集中 RPC / MD5 / validators / statusPoller / renderSectionAdd / uploadCertificate | `view/homeproxy-pro/client/{routing,nodes,dns,access,subscription,udp_nat,tun_dns,common}.js` | +| Tab 模块化 | 1 个 `client.js` 含 12 个 Tab 渲染 | `homeproxy-pro.js` (1161 行 helpers) + 4 顶层 view + 8 个 `client/*.js`;helpers 集中 RPC / MD5 / validators / statusPoller / renderSectionAdd / uploadCertificate | `view/homeproxy-pro/client/{routing,nodes,dns,access,subscription,udp_nat,tun_dns,common}.js` | ### 稳定性 @@ -54,8 +54,8 @@ | 维度 | upstream 形态 | pro 形态 | 证据 | |---|---|---|---| -| 架构守卫 | 无 | `tests/arch-guard.sh` **38 个 guard / 135 个 check** 锁跨文件一致性(UCICONFIG_DIR / ACL / capabilities / conffiles / CERT_PATH_ROOTS / `redactUrl` mock / uCode 语法 / sing-box floor / adapter 共享表 / …) | `tests/arch-guard.sh` (guards 1-19, 21-39) | -| 测试规模 | 7 个脚本 / 约 27 个 check | `tests/` 下 80 个文件:ucode 套件 + 8 个 frontend 验证器 + golden snapshot + mocks + 离线 runtime 测试;arch-guard 单跑 135 个 check | `tests/print-stats.sh` 实测 | +| 架构守卫 | 无 | `tests/arch-guard.sh` **47 个 guard / 164 个 check** 锁跨文件一致性(UCICONFIG_DIR / ACL / capabilities / conffiles / CERT_PATH_ROOTS / `redactUrl` mock / uCode 语法 / sing-box floor / adapter 共享表 / …) | `tests/arch-guard.sh` (guards 1-49,编号跳过 20 与 44) | +| 测试规模 | 7 个脚本 / 约 27 个 check | `tests/` 下 82 个文件:ucode 套件 + 8 个 frontend 验证器 + golden snapshot + mocks + 离线 runtime 测试;arch-guard 单跑 164 个 check | `tests/print-stats.sh` 实测 | | CI | `build` + `i18n` 两条平行 workflow | `build` 用 `workflow_call` 依赖 `arch-test`;`arch-test` 8 步(翻译 fast gate → toolchain cache → toolchain 校验 → `tests/run.sh` 唯一套件入口 → 模板检查);`on-target` 手动 ssh + 不安装 | `.github/workflows/{arch-test,build,on-target}.yml` | | uCode dialect pin | 跟随 upstream HEAD(已放宽) | pin `UCODE_REV=85922056ef7abeace3cca3ab28bc1ac2d88e31b1`(完整 40 位 commit)+ `test_ucode_grammar.sh` canary 锁 strict 语法(拒 `export function … }` 无 `;`、对象/数组解构) | `tests/toolchain/build-ucode-linux.sh:UCODE_REV` + `tests/ucode/test_ucode_grammar.sh` | | MD5 实现 | 388 行 minified snippet;vmess 分支漏 `vmess_global_padding` | RFC 1321 完整实现 + `tests/frontend-md5.js` 锁 RFC 向量 + 与 Node `crypto.createHash('md5')` 双向 cross-check(44 / 44 PASS) | `homeproxy-pro.js:calcStringMD5` + `tests/frontend-md5.js` | @@ -70,7 +70,31 @@ | 资源更新 | jsdelivr 单一镜像 | 4 镜像 fallback(`fastly.jsdelivr.net` / `gcore.jsdelivr.net` / `cdn.jsdelivr.net` / `raw.githubusercontent.com`)+ UI「上次成功时间」(arch-guard 17 / 18 锁) | `runtime/dns.sh` + arch-guard 17 / 18 | | ECH 上传 | 后端 case 缺失 | 补齐 `client_ech_conf` 标签 + `isValidECHConfig()` PEM 校验;ACL 显式列 4 个 tmp 路径 | `homeproxy-pro.uc:isValidECHConfig` + `luci.homeproxy-pro:certificate_write` | -**一句话总结**:pro 的核心价值是**把"单文件能跑"变成"orchestrator + table-driven adapter + 可独立测试的模块"**,并把约束、质量、回滚三件事从靠人盯变成靠代码执行(arch-guard 135 checks 静态锁住跨文件不变量)。 +**一句话总结**:pro 的核心价值是**把"单文件能跑"变成"orchestrator + table-driven adapter + 可独立测试的模块"**,并把约束、质量、回滚三件事从靠人盯变成靠代码执行(arch-guard 164 checks 静态锁住跨文件不变量)。 + +## 自编译(OpenWrt buildroot) + +```sh +make menuconfig +``` + +勾选 **luci-app-homeproxy-pro**,以及 **LuCI → Translations → 简体中文 (zh_Hans)**。 +后者不勾的话翻译包不会构建,且不报错,只在日志里留一行 +`WARNING: skipping luci-i18n-homeproxy-pro-zh-cn -- package not selected`。 + +```sh +make package/luci-app-homeproxy-pro/compile V=s -j1 +ls bin/packages///base/ | grep -i homeproxy-pro +``` + +应同时得到两个包: + +``` +luci-app-homeproxy-pro-28.10.1.14-r37.apk +luci-i18n-homeproxy-pro-zh-cn-28.10.1.14-r37.apk +``` + +本包不编译 sing-box,固件须自带 1.14+,否则依赖解析失败。 ## 已知限制 diff --git a/luci-app-homeproxy-pro/htdocs/luci-static/resources/view/homeproxy-pro/client/routing.js b/luci-app-homeproxy-pro/htdocs/luci-static/resources/view/homeproxy-pro/client/routing.js index 842f8cf64..2f8a56e50 100644 --- a/luci-app-homeproxy-pro/htdocs/luci-static/resources/view/homeproxy-pro/client/routing.js +++ b/luci-app-homeproxy-pro/htdocs/luci-static/resources/view/homeproxy-pro/client/routing.js @@ -136,9 +136,9 @@ function render(ctx) { o.default = o.enabled; o.rmempty = false; - /* §2.7 (linux.json 与 pro 的差距分析): opt-in switch to the tutorial's - * universal sniffer list + 100ms timeout. Default off so an upgrade is - * invisible; arch-guard 40 pins the default '0' against drift. */ + /* Opt-in switch to the universal sniffer list + 100ms timeout. Default + * off so an upgrade is invisible; arch-guard 39 pins the default '0' + * against drift. */ o = s.taboption('routing', form.Flag, 'sniffer_advanced_mode', _('Sniffer: advanced mode (100ms, universal list)'), _('Use the tutorial-recommended sniffer profile (100ms timeout, http/tls/stun/quic/dns). Default keeps the 300ms / unconstrained-list behaviour.')); diff --git a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/common.uc b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/common.uc index 82395bc07..f27079628 100644 --- a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/common.uc +++ b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/common.uc @@ -186,7 +186,7 @@ export function attachSchema(config) { * gfwlist / proxy_mainland_china / global with cold-start DNS on every * reload and made the difference between modes hard to explain. * - * Note: docs/linux.json 与 pro 的差距分析.md §2.5 once asked for an + * Note: the reference configuration this was compared against asked for an * explicit `reverse_mapping: true` here (so a future sing-box change to * the default would not silently move us off the mapping table). The * sing-box 1.14.0-r1 we test against rejects that field as diff --git a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/context.uc b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/context.uc index f2fefd639..a054f5b5b 100644 --- a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/context.uc +++ b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/context.uc @@ -216,7 +216,7 @@ export function build_context(dm, env) { ? (dm.dns.settings || {}).default_strategy : ((ipv6_support !== '1') ? 'ipv4_only' : null), /* cn_ip_fallback: evaluate/match_response fallback for unknown-host - * queries (tutorial §2.1 / docs/linux.json 与 pro 的差距分析.md). + * queries. * The v28.9.1.16 flip turns this on by default for fresh installs; * migrate_config.uc writes '0' explicitly for upgrade-existing users * so they keep the prior behaviour until they opt in. '0' is truthy @@ -225,8 +225,8 @@ export function build_context(dm, env) { * exactly what they had. */ cn_ip_fallback: dm.general.cn_ip_fallback || '1', /* sniffer_advanced_mode: when '1' (opt-in), the route sniff rule - * gets the universal protocol list + 100ms timeout (tutorial §2.7 / - * docs/linux.json 与 pro 的差距分析.md). Default '0' preserves the + * gets the universal protocol list + 100ms timeout. + * Default '0' preserves the * 300ms / default-list behaviour so an upgrade is invisible. */ sniffer_advanced_mode: dm.general.sniffer_advanced_mode || '0', main_urltest_nodes: dm.general.main_urltest_nodes || [], diff --git a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/dns.uc b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/dns.uc index 4018f95e9..bfe6d4be6 100644 --- a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/dns.uc +++ b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/dns.uc @@ -321,8 +321,10 @@ function append_proxy_dns(config, dm, ctx) { * configured one of these encrypted upstreams; without the predefined * entry, the resolver itself has to be resolved through the system DNS, * and a DNS outage of any kind taken once out leaves the resolver - * unreachable - a chicken-and-egg loop. Mirrors the hosts server in - * linux.json line 10; see docs/linux.json 与 pro 的差距分析.md §2.4. + * unreachable - a chicken-and-egg loop. The well-known sing-box reference + * configuration ships the same table in a `type: 'hosts'` server, reached by + * pointing each encrypted resolver's `domain_resolver` at that server's tag; + * append_custom_dns() does exactly that below with the 'hp-dns-hosts' tag. * * Additions go here when the user's pool of encrypted resolvers grows; * a new entry needs no code change beyond this table. */ @@ -337,15 +339,15 @@ const KNOWN_ENCRYPTED_DNS_HOSTS = { /* --- custom routing-mode path (user-defined dns_server / dns_rule) ----- */ function append_custom_dns(config, dm, ctx) { - /* §2.4 DoH fallback. Pre-emit a hosts-type DNS server pinning any + /* Pre-emit a hosts-type DNS server pinning any * encrypted-resolver hostname the user configured to its real IPs. The * loop also remembers which server sections hit the table, so the user * server loop below can stamp `domain_resolver: 'hp-dns-hosts'` onto * them - sing-box only consults the hosts table for resolvers that - * explicitly point at it, mirroring linux.json line 7 - * (`{"domain_resolver": "hosts"}`). Without the stamp, the resolver - * still has to look up its own hostname through the system DNS and the - * chicken-and-egg loop we are trying to break stays open. */ + * explicitly point at it, so the table is inert without the stamp. Without + * it, the resolver still has to look up its own hostname through the + * system DNS and the chicken-and-egg loop we are trying to break stays + * open. */ let doh_predefined = null; const doh_resolved_hosts = {}; for (let cfg in (dm.dns.servers || [])) { @@ -403,7 +405,7 @@ function append_custom_dns(config, dm, ctx) { /* sing-box >= 1.14: legacy address-filter rules are auto-wrapped with an evaluate action; deprecated strategy/accept_empty fields are dropped. */ const builtin_dns_rules = []; - /* §2.2 First safe rule: reject SVCB (qtype 64) and HTTPS (qtype 65) + /* The first rule has to be the SVCB (qtype 64) and HTTPS (qtype 65) * queries. A client that learns the answer from these record types * connects to the embedded IP without going through the resolver again, * which makes Fake-IP and any route that hinges on A/AAAA resolution diff --git a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/route.uc b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/route.uc index 4bf2430d0..021c3175d 100644 --- a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/route.uc +++ b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/generator/route.uc @@ -36,11 +36,13 @@ import { get_outbound, get_resolver, get_ruleset, get_direct_override } from './ * that every mode shares (dns-in hijack, sniff, optional auto_detect). * The caller must have created an empty config.route; we mutate it. * - * Sniff tuning follows §2.7 of docs/linux.json 与 pro 的差距分析.md: - * the default rule is the conservative 300ms / unconstrained sniffer list - * (tutorial demo recommends 100ms + ['http','tls','stun','quic','dns'], - * which costs less CPU but is a behaviour change). sniffer_advanced_mode - * is the opt-in switch that flips to the tutorial profile. */ + * Two sniff profiles, and the default is deliberately the conservative one: + * 300ms with no sniffer list, so sing-box applies its own default set. The + * alternative - 100ms plus an explicit + * ['http','tls','stun','quic','dns'] - is cheaper on CPU but changes what a + * connection is sniffed as, which is a behaviour change, not a tuning knob. + * sniffer_advanced_mode is the opt-in that flips to it, so an upgrade from an + * earlier release is invisible. Guard 39 pins the default at '0'. */ function initRoute(config, ctx) { const sniff_rule = (ctx.sniffer_advanced_mode === '1') ? { diff --git a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/migrate_config.uc b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/migrate_config.uc index 5b3fc6418..e46ad6634 100755 --- a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/migrate_config.uc +++ b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/migrate_config.uc @@ -90,7 +90,7 @@ if (!migration_crontab) { if (isEmpty(uci.get(uciconfig, ucimain, 'log_level'))) uci.set(uciconfig, ucimain, 'log_level', 'warn'); -/* v28.9.1.16: cn_ip_fallback default flipped to '1' (tutorial §2.1 / docs/linux.json 与 pro 的差距分析.md). +/* v28.9.1.16: cn_ip_fallback default flipped to '1'. * Existing users get the behaviour change only when they ask for it, so write * '0' explicitly into their config on upgrade; new installs read '1' straight * from /etc/config/homeproxy-pro (the package ships with that as the default). diff --git a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/runtime/service.sh b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/runtime/service.sh index 6b9c9a6f8..4106c3da8 100644 --- a/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/runtime/service.sh +++ b/luci-app-homeproxy-pro/root/etc/homeproxy-pro/scripts/runtime/service.sh @@ -541,7 +541,7 @@ hp_start_generated_config() { # the *new* (failing) value and start_service reinstalls a layer that no # longer matches the sing-box file it just restored. # -# Review 2026-09-29 (docs/review-report-20260929-full.md §1.2) found the +# The 2026-09-29 review found the # previous five-line list covered 4 real keys out of 22, and that the fifth # line named `infra.tun_address` - an option that exists nowhere. The real # keys are `tun_addr4` / `tun_addr6` (config/loader.uc:411), so that line was diff --git a/luci-app-homeproxy-pro/tests/arch-guard.sh b/luci-app-homeproxy-pro/tests/arch-guard.sh index 8eb6333c8..78b885a74 100755 --- a/luci-app-homeproxy-pro/tests/arch-guard.sh +++ b/luci-app-homeproxy-pro/tests/arch-guard.sh @@ -1009,9 +1009,25 @@ fi # No shipped file may cite docs/. The whole directory is gitignored, so every # pointer to it is a dead link in a fresh clone - that is how README.md # came to reference an agent guide and an improvement plan that no -# contributor can open. Kept here (with the docs/ pattern, not the names) -# so the rule cannot quietly come back. -DEAD_DOCS="$(grep -rlE 'docs/(adr|architecture-improvement-plan|homeproxy_architecture_refactor_agent_guide|audit-report|next-step-plan)' \ +# contributor can open. +# +# The pattern is deliberately *shape*-based, not a list of names. An earlier +# version enumerated the offending documents +# (adr|architecture-improvement-plan|homeproxy_architecture_refactor_agent_guide| +# audit-report|next-step-plan) and therefore only ever caught those five: the +# next document to be written - the gap analysis, the dated review reports - +# walked straight past it, and a dozen comments across the generators, the +# views and the test suite kept citing +# `docs/linux.json 与 pro 的差距分析.md` with no gate noticing. +# Matching `docs/` followed by anything catches every present and future name. +# +# The anchor is what keeps it from crying wolf. `htdocs/` contains the +# substring `docs/`, and so does the `/docs/` of an external URL such as +# https://github.com/anytls/anytls-go/blob/v0.0.8/docs/uri_scheme.md - both +# are legitimate and must not be flagged. Requiring the character before +# `docs/` to be neither alphanumeric nor `_`, `.`, `/`, `-` or `:` excludes +# exactly those two shapes and keeps every real citation. +DEAD_DOCS="$(grep -rlE '(^|[^:A-Za-z0-9_./-])docs/[^[:space:]]' \ "$ROOT/README.md" "$ROOT/root" "$ROOT/htdocs" 2>/dev/null || true)" if [ -z "$DEAD_DOCS" ]; then pass "no shipped file cites the gitignored docs/ tree" @@ -1020,6 +1036,18 @@ else printf '%s\n' "$DEAD_DOCS" fi +# The same rule for the test tree, minus this file: a dead citation inside a +# test is just as unresolvable for a contributor reading the failure, and the +# generator suite carried six of them. +DEAD_DOCS_TESTS="$(grep -rlE '(^|[^:A-Za-z0-9_./-])docs/[^[:space:]]' \ + "$ROOT/tests" 2>/dev/null | grep -v 'tests/arch-guard\.sh$' || true)" +if [ -z "$DEAD_DOCS_TESTS" ]; then + pass "no test file cites the gitignored docs/ tree either" +else + fail "these test files cite docs/ (gitignored, absent from every clone):" + printf '%s\n' "$DEAD_DOCS_TESTS" +fi + echo echo "== guard 21: dnsmasq snippet writer is incremental ==" @@ -1716,9 +1744,8 @@ echo "== guard 38: append_custom_dns emits the HTTPS/SVCB reject as its first DN # The proxy path emits this reject (append_proxy_dns line 102); the custom # path used to rely on whatever the user wrote, so a rule that targets the -# same query_type could shadow the safety net. The fix (see -# docs/linux.json 与 pro 的差距分析.md §2.2) prepends the same literal in -# append_custom_dns. Two checks keep that ordering: +# same query_type could shadow the safety net. The fix prepends the same +# literal in append_custom_dns. Two checks keep that ordering: # # (a) the literal appears in BOTH append_proxy_dns and append_custom_dns; # (b) the second occurrence (the one inside append_custom_dns) comes @@ -1732,7 +1759,7 @@ if [ "$reject_count" -eq 2 ]; then pass "the HTTPS/SVCB reject appears in both append_proxy_dns and append_custom_dns" else fail "expected 2 occurrences of 'query_type: [64, 65]' in generator/dns.uc, found $reject_count" - fail "the custom path lost its built-in reject; see §2.2 of the gap analysis" + fail "the custom path lost its built-in reject (see guard 38 in this file)" fi # (b): the custom-path reject is the SECOND occurrence (append_custom_dns @@ -1759,9 +1786,7 @@ fi echo "== guard 39: sniffer_advanced_mode default stays at '0' ==" -# §2.7 (linux.json 与 pro 的差距分析.md): the advanced-mode sniff profile - -# §2.7 (linux.json 与 pro 的差距分析.md): the advanced-mode sniff profile +# The advanced-mode sniff profile # (100ms / universal list) is gated behind a UCI opt-in so an upgrade # does not change the sniffer behaviour. The default must remain '0'; # the generator falls back to '0' on a missing UCI value, and the @@ -1869,7 +1894,7 @@ fi echo "== guard 41: experimental.cache_file is unconditional in attachExperimental() ==" -# Review (2026-09-29, docs/review-report-20260929.md §1.4.3 / §5.1, P2 #3): +# The 2026-09-29 review (P2 #3) found: # attachExperimental() used to wrap the experimental.cache_file block in # `if (routing_mode in ['bypass_mainland_china', 'custom'])`, leaving # gfwlist / proxy_mainland_china / global with cold-start DNS on every @@ -1921,7 +1946,7 @@ esac echo "== guard 42: NAPTR_BYPASS_SUFFIXES is the only domain_suffix qtype-35 source ==" -# Review (2026-09-29, docs/review-report-20260929.md §2.2.1, P2 #4): the three +# The 2026-09-29 review (P2 #4) found: the three # NAPTR (qtype 35) bypass suffixes (r.10086.cn, 10086.cn, pub.3gppnetwork.org) # used to be inlined into a push() call inside append_proxy_dns() and the # comment that explained them was on the same line as the push. The fix @@ -1948,7 +1973,7 @@ fi echo echo "== guard 43: the rollback UCI snapshot matches the keys the intercept layer reads ==" -# Review 2026-09-29 (docs/review-report-20260929-full.md §1.2-1.4, P2 #1/#2). +# The 2026-09-29 review (P2 #1 / #2) found: # # reload_service rolls back by restoring the known-good sing-box file *and* # replaying a UCI snapshot of the keys that steer the network layer - the @@ -2339,6 +2364,50 @@ else pass "no view calls the non-existent form.Map.formvalue()" fi +echo "== guard 49: the i18n package name cannot drift from PKG_NAME ==" + +# luci.mk names the translation package from the *checkout directory*, not from +# PKG_NAME: +# +# LUCI_NAME?=$(notdir ${CURDIR}) +# LUCI_BASENAME?=$(patsubst luci-$(LUCI_TYPE)-%,%,$(LUCI_NAME)) +# define Package/luci-i18n-$(LUCI_BASENAME)-$(1) +# +# PKG_NAME is pinned to luci-app-homeproxy-pro here, so with the repository cloned +# under its own name (luci-app-homeproxy-pro, which is what every self-compiler +# gets) the feed build emitted luci-i18n-homeproxy-pro-zh-cn while the published +# apk was named by build-pkg.sh from PKG_NAME and came out +# luci-i18n-homeproxy-pro-zh-cn. Two names, one project, and the person self-building +# concludes the translation package is missing (issue #5). +# +# The check is that LUCI_BASENAME agrees with the PKG_NAME the release path uses, +# computed here from the Makefile the way luci.mk would - not by re-deriving it +# from this repository's own directory name, which is precisely the thing that +# was wrong and would make the assertion vacuous. +MAKEFILE="$ROOT/Makefile" +MK_PKG_NAME="$(sed -n 's/^PKG_NAME:=//p' "$MAKEFILE" 2>"/dev/null" | head -1)" +MK_LUCI_BASENAME="$(sed -n 's/^LUCI_BASENAME:=//p' "$MAKEFILE" 2>"/dev/null" | head -1)" + +if [ -z "$MK_PKG_NAME" ]; then + fail "PKG_NAME is not declared in the Makefile - nothing to keep the i18n name tied to" +elif [ -z "$MK_LUCI_BASENAME" ]; then + fail "LUCI_BASENAME is not pinned, so the i18n package name follows the checkout" + fail " directory: cloning as luci-app-homeproxy-pro yields luci-i18n-homeproxy-pro-zh-cn" + fail " while the release path names it luci-i18n-homeproxy-pro-zh-cn (issue #5)" +else + # luci.mk strips the "luci--" prefix; type is the 2nd dash-field. + mk_type="$(printf '%s' "$MK_PKG_NAME" | awk -F- '{print $2}')" + mk_expected_basename="$(printf '%s' "$MK_PKG_NAME" | sed -n "s/^luci-${mk_type}-//p")" + + if [ "$MK_LUCI_BASENAME" = "$mk_expected_basename" ]; then + pass "LUCI_BASENAME matches PKG_NAME, so the i18n package is named the same" + pass " in the feed build and in the release (luci-i18n-${MK_LUCI_BASENAME}-zh-cn)" + else + fail "LUCI_BASENAME is '$MK_LUCI_BASENAME' but PKG_NAME '$MK_PKG_NAME' implies" + fail " '$mk_expected_basename' - the self-built i18n package will not match the release" + fi +fi + echo printf '%s checks, %s failures\n' "$checks" "$([ "$FAILED" = 0 ] && echo 0 || echo 'nonzero')" if [ "$FAILED" != 0 ]; then diff --git a/luci-app-homeproxy-pro/tests/runtime/test_config_transaction.sh b/luci-app-homeproxy-pro/tests/runtime/test_config_transaction.sh index c88eafcc5..07979e94d 100644 --- a/luci-app-homeproxy-pro/tests/runtime/test_config_transaction.sh +++ b/luci-app-homeproxy-pro/tests/runtime/test_config_transaction.sh @@ -133,7 +133,7 @@ expect "uci restore: missing snapshot returns 1" "$?" "1" # Happy path: snapshot present, all keys applied, commit called. # The snapshot format carries the UCI type, because `uci set` on a list option # stores a string and the firewall validators then reject it (review -# 2026-09-29, docs/review-report-20260929-full.md §1.5): lists are replayed +# 2026-09-29 review): lists are replayed # with `uci delete` + `uci add_list`. : > "$HP_TEST_UCI_OUT" write_uci_stub diff --git a/luci-app-homeproxy-pro/tests/ucode/test_generators.sh b/luci-app-homeproxy-pro/tests/ucode/test_generators.sh index 38462e216..b0a1e6638 100755 --- a/luci-app-homeproxy-pro/tests/ucode/test_generators.sh +++ b/luci-app-homeproxy-pro/tests/ucode/test_generators.sh @@ -578,7 +578,7 @@ fi run_case custom "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json -# §2.2 (linux.json 与 pro 的差距分析): append_custom_dns now prepends the +# append_custom_dns now prepends the # HTTPS/SVCB reject so a user-written rule cannot shadow the safety net. # Without a dns_server in custom.uci the rule block is empty, so the # generated config still has the reject at index 0. @@ -613,7 +613,7 @@ EOF fi fi -# §2.5 (linux.json 与 pro 的差距分析): the original plan was to set +# The original plan was to set # `experimental.reverse_mapping: true` so the literal pinned us against a # future sing-box default flip. sing-box 1.14.0-r1 (the CI floor pinned # by arch-guard 31) rejects the field as "json: unknown field" - the @@ -623,7 +623,7 @@ fi # purpose: the only assertion that matters is that the generated config # passes sing-box check, which the test runner does for every case. -# §2.4 (linux.json 与 pro 的差距分析): when the user configures a DoH +# When the user configures a DoH # upstream against a known hostname, append_custom_dns emits a hosts-type # DNS server pinning that hostname to its real IPs. Without it, the DoH # resolver itself has to be resolved through the system DNS, and a DNS @@ -676,7 +676,7 @@ EOF fi fi -# §2.1 (linux.json 与 pro 的差距分析): cn_ip_fallback default flipped to '1' +# cn_ip_fallback default flipped to '1' # for fresh installs (the package ships option cn_ip_fallback '1' and # context.uc falls back to '1' on a missing value). Existing users # upgrading get '0' written by migrate_config.uc and keep the prior @@ -719,7 +719,8 @@ EOF fi fi -# §2.1 opt-out: a fixture with cn_ip_fallback='0' must NOT emit the pair. +# cn_ip_fallback opt-out: a fixture with cn_ip_fallback='0' must NOT emit +# the pair. # client.uci does not set the option (so it defaults to '1' in the proxy # config above); here we stage a variant with '0' to assert the # upgrade-safe path of migrate_config.uc - existing users see no change. @@ -740,7 +741,7 @@ else fi fi -# §2.7 (linux.json 与 pro 的差距分析): sniffer_advanced_mode default stays +# sniffer_advanced_mode default stays # '0'. Client mode must emit the conservative sniff rule (300ms / no # sniffer list); advanced mode emits the 100ms / universal-list profile. if [ ! -f "$client_json" ]; then @@ -776,7 +777,7 @@ EOF fi fi -# §2.7 opt-in: sniffer_advanced_mode='1' flips the sniff rule to 100ms + +# sniffer_advanced_mode opt-in: '1' flips the sniff rule to 100ms + # the universal protocol list. sed-injected on client.uci. run_case client-sniffer-advanced "$ROOT/tests/fixtures/generators/client.uci" generate_client.uc sing-box-c.json \ "s#^\\([[:space:]]*\\)option main_node 'urltest'#\\1option main_node 'urltest'\\n\\1option sniffer_advanced_mode '1'#" @@ -920,8 +921,8 @@ run_case_type_error dangling-resolver "does not exist or is disabled" \ # --- review@2026-09-29 batch ------------------------------------------------- # -# Six checks that pin the five generator fixes from -# docs/review-report-20260929.md §7. Earlier rounds attempted ucode +# Six checks that pin the five generator fixes from the 2026-09-29 review. +# Earlier rounds attempted ucode # probes (arrow functions, optional chaining on arrays, ...) and each # round surfaced a different ucode dialect issue on the testbed - the # macOS dev box has no ucode so the loop could not iterate locally. @@ -929,7 +930,7 @@ run_case_type_error dangling-resolver "does not exist or is disabled" \ # it runs identically on macOS and the testbed. ucode-stdout probes # are out; structural assertions against the emitted bytes are in. -# 1) P2 #2 / §1.4.1: geosite-cn is the *DNS*-layer rule_set consumer. +# 1) P2 #2: geosite-cn is the *DNS*-layer rule_set consumer. # The route layer never references it as a rule_set value (route.rule_set # declares it as a tag). Indent cannot disambiguate dns.rules[] from # route.rules[] (both pretty-print at 4 tabs on the testbed; both are @@ -962,7 +963,7 @@ else fi fi -# 2) P3 #6 / §1.4.3: china-dns.strategy must mirror default-dns's gate +# 2) P3 #6: china-dns.strategy must mirror default-dns's gate # on ipv6_support. With ipv6_support='0' (client.uci's value), # china-dns.strategy must be 'ipv4_only', not the pre-r28 # unconditional 'prefer_ipv6'. The generated JSON emits the dns @@ -1083,7 +1084,7 @@ else fi fi -# 3) P2 #4 / §2.2.1: the three NAPTR (qtype 35) bypass suffixes must be +# 3) P2 #4: the three NAPTR (qtype 35) bypass suffixes must be # emitted as a single domain_suffix entry backed by the # NAPTR_BYPASS_SUFFIXES constant. ucode's pretty-printer emits the # query_type array and the domain_suffix array as multi-line: @@ -1121,7 +1122,7 @@ else fi fi -# 4) P2 #3 / §1.4.3 / §5.1: attachExperimental() used to gate cache_file +# 4) P2 #3: attachExperimental() used to gate cache_file # on routing_mode in [bypass_mainland_china, custom]. After the r28 # fix, every routing_mode gets a cache_file block. Three sed # variations of the same fixture cover gfwlist / proxy_mainland_china @@ -1170,7 +1171,7 @@ else echo "PASS: cache-file-custom: experimental.cache_file emitted for routing_mode='custom'" fi -# 5) P3 #7 / §4.2.1: a local ruleset whose path is outside the homeproxy-pro +# 5) P3 #7: a local ruleset whose path is outside the homeproxy-pro # whitelist (e.g. /etc/passwd) used to silently set the field to # null. The fix is to die() with a message naming the offender. # run_case_type_error observes the refusal. The variation targets @@ -1181,7 +1182,7 @@ run_case_type_error local-ruleset-bad-path "outside the homeproxy-pro whitelist" "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json \ "s%^[[:space:]]*option path '/tmp/homeproxy_test_ruleset\\.[^/]*/local-ruleset-bad-path/test.srs'% option path '/etc/passwd'%" -# 6) P3 #8 / §4.2.2 (extra_tags die() on missing {tag}): deferred to +# 6) P3 #8 (extra_tags die() on missing {tag}): deferred to # a dedicated 'testbed-dialect' sprint. The multi-line sed to # attach an extra_tags list onto the existing rs_local ruleset # drifted at the first whitespace change on the testbed, and diff --git a/luci-app-homeproxy-pro/tests/ucode/test_tls_transport.uc b/luci-app-homeproxy-pro/tests/ucode/test_tls_transport.uc index ecd4a3595..a710f333f 100644 --- a/luci-app-homeproxy-pro/tests/ucode/test_tls_transport.uc +++ b/luci-app-homeproxy-pro/tests/ucode/test_tls_transport.uc @@ -59,7 +59,7 @@ expect('server.insecure forced null', serverInsecure.insecure, null); /* 4. cert_path whitelist - the validator accepts /etc/homeproxy-pro/... and * /tmp/homeproxy_..., and rejects everything else (including * /etc/passwd and relative paths). This is the security patch - * that backs the §4 path-whitelist entry; a regression here would + * that backs the path-whitelist entry above; a regression here would * let UCI drive sing-box into reading arbitrary files as root. */ expect('cert.path /etc/homeproxy-pro', validateHomeProxyPath('/etc/homeproxy-pro/certs/server_publickey.pem'), true); expect('cert.path /tmp/homeproxy_', validateHomeProxyPath('/tmp/homeproxy_test/foo.pem'), true); diff --git a/luci-app-linkback/Makefile b/luci-app-linkback/Makefile index 043ce8788..1ecdc8cef 100644 --- a/luci-app-linkback/Makefile +++ b/luci-app-linkback/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-linkback PKG_VERSION:=1.0.0 -PKG_RELEASE:=1 +PKG_RELEASE:=2 PKG_MAINTAINER:=Dante LUCI_TITLE:=LuCI support for Multi-WAN LinkBack Failover/Failback @@ -17,7 +17,7 @@ LUCI_DEPENDS:=+luci-base +ucode +ucode-mod-fs +ucode-mod-uci +ucode-mod-ubus +li LUCI_PKGARCH:=all define Package/$(PKG_NAME)/postinst -#!/sh +#!/bin/sh [ -n "$${IPKG_INSTROOT}" ] || { /etc/init.d/rpcd restart } diff --git a/luci-app-linkback/htdocs/luci-static/resources/view/linkback/overview.js b/luci-app-linkback/htdocs/luci-static/resources/view/linkback/overview.js index bff14b6a5..264cbb2f4 100644 --- a/luci-app-linkback/htdocs/luci-static/resources/view/linkback/overview.js +++ b/luci-app-linkback/htdocs/luci-static/resources/view/linkback/overview.js @@ -49,10 +49,10 @@ return view.extend({ var container = E('div', { 'class': 'cbi-map' }, [ E('h2', {}, _('LinkBack 链路守护') + ' - ' + _('Overview')), - E('div', { 'class': 'cbi-map-descr' }, _('Real-time health state, priority mapping, and service control for Multi-WAN default routing metrics.')) + E('div', { 'class': 'cbi-map-descr' }, _('Real-time health state, priority mapping, and service control for Multi-WAN / Multi-Gateway failover.')) ]); - // --- Service Status Section (Aligned with lucky style) --- + // --- Service Status Section --- var stateText = _('Collecting...'); var stateColor = '#999'; @@ -84,15 +84,19 @@ return view.extend({ click: ui.createHandlerFn(this, 'handleServiceAction', 'restart') }, _('Restart')) ]) + ]), + E('tr', { 'class': 'tr' }, [ + E('td', { 'class': 'td left', 'width': '33%' }, _('Working Mode')), + E('td', { 'class': 'td left', 'id': 'linkback-service-mode' }, '-') ]) ]) ]) ]); container.appendChild(statusSection); - // --- Link Status Section (Aligned with lucky style) --- + // --- Link Status Section --- var linkSection = E('fieldset', { 'class': 'cbi-section' }, [ - E('legend', {}, _('Link Status')), + E('legend', {}, _('Link / Gateway Status')), E('div', { 'class': 'cbi-section-node' }, [ E('div', { id: 'linkback-status-box' }, [ E('p', { 'class': 'spinning' }, _('Loading real-time link status...')) @@ -131,6 +135,16 @@ return view.extend({ } } + // Update mode indicator + var modeEl = document.getElementById('linkback-service-mode'); + if (modeEl && status) { + if (status.mode === 'multi_gw') { + modeEl.textContent = _('单wan多网关模式') + ' (' + _('wan网络接口') + ': ' + (status.interface || 'wan') + ')'; + } else { + modeEl.textContent = _('多wan口模式'); + } + } + var restartBtn = document.getElementById('linkback-restart-btn'); if (restartBtn) { if (status && status.links && status.links.length > 0) { @@ -143,7 +157,7 @@ return view.extend({ if (!status || !status.links || status.links.length === 0) { dom.content(box, [ E('p', { style: 'color:#f44336; font-weight:bold; padding:20px;' }, - _('LinkBack daemon is not running or no links are configured. Please check Settings page.')) + _('LinkBack daemon is not running or no targets are configured. Please check Settings page.')) ]); return; } @@ -154,7 +168,7 @@ return view.extend({ var table = E('table', { 'class': 'table cbi-section-table' }, [ E('tr', { 'class': 'tr table-titles' }, [ E('th', { 'class': 'th', 'style': 'text-align:center; width:5%;' }, '#'), - E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Link Name')), + E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Target / Interface')), E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Status')), E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Routing Attributes')), E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Latency Details')) diff --git a/luci-app-linkback/htdocs/luci-static/resources/view/linkback/settings.js b/luci-app-linkback/htdocs/luci-static/resources/view/linkback/settings.js index 5b6f2a574..2e8218523 100644 --- a/luci-app-linkback/htdocs/luci-static/resources/view/linkback/settings.js +++ b/luci-app-linkback/htdocs/luci-static/resources/view/linkback/settings.js @@ -8,42 +8,70 @@ return view.extend({ load: function() { return Promise.all([ uci.load('linkback'), - uci.load('network') + uci.load('network'), + uci.load('firewall').catch(function() { return {}; }) ]); }, // Comprehensive validation: check all conditions required for the service // to be safely enabled. Returns an error message string, or null if OK. _validateServiceConfig: function() { + var global_sec = uci.sections('linkback', 'global')[0] || {}; + var mode = global_sec.mode || 'multi_wan'; var link_sections = uci.sections('linkback', 'link') || []; - // Rule 1: At least 2 interfaces + // Rule 1: At least 2 targets if (link_sections.length < 2) { - return _('Cannot enable service: At least 2 monitored WAN interfaces must be configured for failover switcher.'); + return _('Cannot enable service: At least 2 monitored links/gateways must be configured for failover switcher.'); } - // Rule 2: ALL interfaces must have a health check configured + if (mode === 'multi_gw') { + var iface = global_sec.interface || 'wan'; + if (!iface) { + return _('Cannot enable service: WAN interface must be configured in Multi-Gateway mode.'); + } + } + + // Rule 2: ALL targets must have a health check configured + var priorities = {}; + var gateways = {}; + var names = {}; + for (var i = 0; i < link_sections.length; i++) { var s_id = link_sections[i]['.name']; - var iface_name = uci.get('linkback', s_id, 'name') || s_id; + var target_name = uci.get('linkback', s_id, 'name') || s_id; + var gw = uci.get('linkback', s_id, 'gateway'); + var prio = uci.get('linkback', s_id, 'priority') || '1'; + var has_check = uci.get('linkback', s_id, 'ping_targets') || uci.get('linkback', s_id, 'dns_server') || uci.get('linkback', s_id, 'tcp_target'); if (!has_check) { - return _('Cannot enable service: Interface "%s" has no health check configured.').format(iface_name); + return _('Cannot enable service: Target "%s" has no health check configured.').format(target_name); } - } - // Rule 3: No duplicate priorities - var priorities = {}; - for (var i = 0; i < link_sections.length; i++) { - var s_id = link_sections[i]['.name']; - var iface_name = uci.get('linkback', s_id, 'name') || s_id; - var prio = uci.get('linkback', s_id, 'priority') || '1'; + // Priority uniqueness if (priorities[prio]) { - return _('Cannot enable service: Interfaces "%s" and "%s" have the same priority %s.').format(priorities[prio], iface_name, prio); + return _('Cannot enable service: Targets "%s" and "%s" have the same priority %s.').format(priorities[prio], target_name, prio); + } + priorities[prio] = target_name; + + // Mode specific checks + if (mode === 'multi_gw') { + var ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + if (!gw || !ipPattern.test(gw)) { + return _('Cannot enable service: Target "%s" has an invalid or missing Gateway IP.').format(target_name); + } + if (gateways[gw]) { + return _('Cannot enable service: Targets "%s" and "%s" have the same Gateway IP %s.').format(gateways[gw], target_name, gw); + } + gateways[gw] = target_name; + } else { + if (names[target_name]) { + return _('Cannot enable service: Interface "%s" is configured multiple times.').format(target_name); + } + names[target_name] = true; } - priorities[prio] = iface_name; } return null; @@ -53,11 +81,48 @@ return view.extend({ var m, s, o; var self = this; + var getActiveMode = function() { + var global_sec = uci.sections('linkback', 'global')[0] || {}; + var sid = global_sec['.name'] || '@global[0]'; + var sel = document.querySelector('[name="cbid.linkback.' + sid + '.mode"]') || + document.querySelector('select[id*="mode"], select[name*="mode"]'); + if (sel && sel.value) { + return sel.value; + } + return uci.get('linkback', sid, 'mode') || 'multi_wan'; + }; + + var current_mode = getActiveMode(); + + // 智能提取 firewall 中 wan 区域关联的所有网络接口 + var wan_interfaces = {}; + var has_wan_zone = false; + uci.sections('firewall', 'zone').forEach(function(sec) { + if (sec.name === 'wan') { + has_wan_zone = true; + var nets = sec.network; + if (Array.isArray(nets)) { + nets.forEach(function(n) { if (n) wan_interfaces[n] = true; }); + } else if (typeof(nets) === 'string') { + nets.split(/\s+/).forEach(function(n) { if (n) wan_interfaces[n] = true; }); + } + } + }); + + // 若未配置 wan 区域,则安全回退至系统非回环非 lan 接口 + if (!has_wan_zone || Object.keys(wan_interfaces).length === 0) { + uci.sections('network', 'interface').forEach(function(sec) { + var n = sec['.name']; + if (n !== 'loopback' && n !== 'lan') { + wan_interfaces[n] = true; + } + }); + } + // 智能判断当前是否为中文环境,并提供实时校验的翻译 fallback var is_zh = (_('Base Metric') === '默认跃点' || _('Base Metric') === '默认跃点 (Metric)'); var t_priority_empty = is_zh ? '优先级不能为空。' : _('Priority must not be empty.'); - var t_priority_conflict = is_zh ? '优先级 %s 与接口 "%s" 冲突。优先级必须是唯一的。' : _('Priority %s conflicts with interface "%s". Priorities must be unique.'); - + var t_priority_conflict = is_zh ? '优先级 %s 与其他目标冲突。优先级必须是唯一的。' : _('Priority %s conflicts with another target. Priorities must be unique.'); // Helper function to expand table column controls and eliminate right empty space var makeTableColumnExpand = function(opt, width) { @@ -81,17 +146,16 @@ return view.extend({ m = new form.Map('linkback', _('LinkBack 链路守护') + ' - ' + _('Settings'), - _('Configure Multi-WAN failover service, health check parameters, and monitored link interfaces.')); + _('Configure Multi-WAN / Multi-Gateway failover service, shared health check parameters, and monitored targets.')); // --- Global Settings Section --- s = m.section(form.TypedSection, 'global', _('Global Settings')); s.anonymous = true; - // Enable switch (总开关) - must be the first option + // 1. Enable switch o = s.option(form.Flag, 'enabled', _('Enable Service'), _('Master switch to enable or disable the LinkBack failover daemon.')); o.rmempty = false; - // Intercept at UCI write level to validate before enabling. o.write = function(section_id, value) { if (value === '1') { var err = self._validateServiceConfig(); @@ -104,27 +168,94 @@ return view.extend({ uci.set('linkback', section_id, 'enabled', value); }; - // --- Monitored Links Section --- - s = m.section(form.GridSection, 'link', _('Monitored WAN Interfaces'), - _('Add and prioritize your WAN interfaces. Lower priority number means higher preference (e.g., 1 = primary, 2 = backup).')); + // 2. Working Mode + o = s.option(form.ListValue, 'mode', _('Working Mode'), + _('Choose failover mode: Multi-WAN interface failover or Single-WAN multi-gateway redundancy.')); + o.value('multi_wan', _('多wan口模式')); + o.value('multi_gw', _('单wan多网关模式')); + o.default = 'multi_wan'; + o.rmempty = false; + o.onchange = function(ev, section_id, value) { + uci.set('linkback', section_id, 'mode', value); + var links = uci.sections('linkback', 'link') || []; + if (links.length > 0) { + if (value === 'multi_gw') { + ui.addNotification(null, E('p', + _('已切换为单wan多网关模式。请确保监控目标列表中配置了有效的网关 IP。') + ), 'info'); + } else { + ui.addNotification(null, E('p', + _('已切换为多wan口模式。请确保监控目标列表中选择了正确的 WAN 接口。') + ), 'info'); + } + } + }; + + // 3. WAN Interface in Multi-Gateway mode + o = s.option(form.ListValue, 'interface', _('wan网络接口'), + _('选择存在多个wan网关的接口')); + o.default = 'wan'; + o.rmempty = false; + o.depends('mode', 'multi_gw'); + + Object.keys(wan_interfaces).forEach(function(iface) { + o.value(iface); + }); + + // 4. Global Shared Health Check & Failover Parameters (共用调度超时) + o = s.option(form.Value, 'check_interval', _('Check Interval (s)'), + _('Shared interval in seconds between health check cycles for all targets.')); + o.datatype = 'uinteger'; + o.default = '5'; + o.rmempty = false; + + o = s.option(form.Value, 'check_timeout', _('Check Timeout (s)'), + _('Shared maximum wait time in seconds for each probe (1s recommended to prevent blocking).')); + o.datatype = 'uinteger'; + o.default = '1'; + o.rmempty = false; + + o = s.option(form.Value, 'recovery_delay', _('Recovery Delay'), + _('Number of consecutive successful checks required before marking link as recovered (failback anti-flap).')); + o.datatype = 'uinteger'; + o.default = '3'; + o.rmempty = false; + + o = s.option(form.Value, 'failover_delay', _('Failover Delay'), + _('Number of consecutive failed checks required before marking link as faulted (failover anti-flap).')); + o.datatype = 'uinteger'; + o.default = '2'; + o.rmempty = false; + + // --- Monitored Targets Section --- + s = m.section(form.GridSection, 'link', _('Monitored Targets'), + _('Configure monitored WAN interfaces or next-hop gateways with custom probe targets and priorities (1 = primary, 2 = backup).')); s.anonymous = true; s.addremove = true; - // Custom dynamic Modal title for gorgeous UX + // Custom dynamic Modal title s.modaltitle = function(section_id) { var parent_title = _('LinkBack 链路守护') + ' - ' + _('Settings'); - var is_new = (this.map.addedSection === section_id) || !uci.get('linkback', section_id, 'name'); - if (is_new) { - return parent_title + ' - ' + _('Add Monitored Interface'); + var mode = getActiveMode(); + var is_new = (this.map.addedSection === section_id) || + (mode === 'multi_gw' ? !uci.get('linkback', section_id, 'gateway') : !uci.get('linkback', section_id, 'name')); + if (mode === 'multi_gw') { + if (is_new) { + return parent_title + ' - ' + _('Add Monitored Gateway'); + } else { + var gw = uci.get('linkback', section_id, 'gateway') || uci.get('linkback', section_id, 'name') || section_id; + return parent_title + ' - ' + _('Edit Monitored Gateway') + ' (' + gw + ')'; + } } else { - var name = uci.get('linkback', section_id, 'name') || section_id; - return parent_title + ' - ' + _('Edit Monitored Interface') + ' (' + name + ')'; + if (is_new) { + return parent_title + ' - ' + _('Add Monitored WAN Interface'); + } else { + var iface = uci.get('linkback', section_id, 'name') || section_id; + return parent_title + ' - ' + _('Edit Monitored WAN Interface') + ' (' + iface + ')'; + } } }; - // When service is already enabled and user modifies/deletes links, - // re-validate on the GridSection level. If the resulting config is invalid, - // show a warning and auto-disable the service to maintain safety. var origRemove = s.handleRemove; s.handleRemove = function(section_id, ev) { return origRemove.apply(this, arguments).then(function() { @@ -141,78 +272,147 @@ return view.extend({ }); }; - // 1. Enabled (Enabled as the first column) + // 1. Enabled o = s.option(form.Flag, 'enabled', _('Enabled')); o.default = '1'; o.rmempty = false; makeTableColumnExpand(o, '8%'); - // 2. Interface name - dynamic dropdown (No description for main column) - o = s.option(form.ListValue, 'name', _('Interface')); - o.rmempty = false; - - // Once-off load of all network interfaces safely avoiding race conditions - var network_interfaces = {}; - uci.sections('network', 'interface').forEach(function(sec) { - var n = sec['.name']; - if (n !== 'loopback' && n !== 'lan') { - network_interfaces[n] = true; - o.value(n); - } - }); - - // Safeguard to show already configured interfaces even if they were deleted from network config - uci.sections('linkback', 'link').forEach(function(sec) { - if (sec.name && !network_interfaces[sec.name]) { - o.value(sec.name, _('%s (configured)').format(sec.name)); - } - }); - - // 动态隐藏已被配置的接口:通过在 renderWidget 中进行局部变量过滤与实例化 ui.Select, - // 完全不修改全局的 this.keylist 和 this.keyvalues,保证 100% 线程安全和无副作用 - o.renderWidget = function(section_id, option_index, cfgvalue) { - // 找出已经被其他 Section 选中的网卡(已配置接口名) - var used_names = {}; - uci.sections('linkback', 'link').forEach(function(sec) { - if (sec['.name'] !== section_id && sec.name) { - used_names[sec.name] = true; + // 2. Monitored Target Display (Table only) + o = s.option(form.DummyValue, '_target_disp', _('Monitored Target')); + o.modalonly = false; + o.cfgvalue = function(section_id) { + var gw = uci.get('linkback', section_id, 'gateway'); + var name = uci.get('linkback', section_id, 'name'); + var mode = getActiveMode(); + if (mode === 'multi_gw' || gw) { + if (gw) { + if (name && name !== gw) { + return gw + ' (' + name + ')'; + } + return gw; } - }); + return name ? (_('Missing Gateway IP: %s').format(name)) : _('Unset'); + } else { + return name || _('Unselected'); + } + }; + makeTableColumnExpand(o, '24%'); - // 动态生成仅针对当前渲染 section_id 的 choices 和 keylist - var filtered_choices = {}; - var filtered_keylist = []; - if (Array.isArray(this.keylist)) { - for (var i = 0; i < this.keylist.length; i++) { - var key = this.keylist[i]; - if (!used_names[key]) { - filtered_keylist.push(key); - filtered_choices[key] = this.vallist[i]; + // 3a. Gateway IP (Multi-GW mode only, Modal only) + var o_gw = s.option(form.Value, 'gateway', _('Gateway IP'), + _('Next-hop IPv4 address of this gateway (e.g. 192.168.1.254).')); + o_gw.datatype = 'ip4addr'; + o_gw.modalonly = true; + o_gw.placeholder = '192.168.1.254'; + var origRenderGw = o_gw.render; + o_gw.render = function(option_index, section_id, in_table) { + if (getActiveMode() !== 'multi_gw') { + return Promise.resolve(E('div', { 'style': 'display: none !important;' })); + } + return origRenderGw.call(this, option_index, section_id, in_table); + }; + o_gw.validate = function(section_id, value) { + if (getActiveMode() !== 'multi_gw') { + return true; + } + if (!value || value.trim() === '') { + return _('Gateway IP is required.'); + } + var val = value.trim(); + var self_opt = this; + var conflict = false; + uci.sections('linkback', 'link').forEach(function(sec) { + var sid = sec['.name']; + if (sid !== section_id) { + var other_gw = self_opt.formvalue(sid) || uci.get('linkback', sid, 'gateway'); + if (other_gw && other_gw === val) { + conflict = true; } } - } - - // 判断当前是编辑已有配置还是添加新配置 - // 如果在 UCI 中已经保存了 name,则是编辑状态,此时网卡不可编辑 - var is_edit = !!uci.get('linkback', section_id, 'name'); - - // 直接在局部变量中实例化 ui.Select,避免任何时序与引用问题 - var widget = new ui.Select((cfgvalue != null) ? cfgvalue : this.default, filtered_choices, { - id: this.cbid(section_id), - size: this.size, - sort: filtered_keylist, - widget: this.widget, - optional: this.optional, - orientation: this.orientation, - placeholder: this.placeholder, - validate: (typeof(this.getValidator) === 'function') ? this.getValidator(section_id) : (this.validate ? this.validate.bind(this, section_id) : null), - disabled: is_edit ? true : ((this.readonly != null) ? this.readonly : this.map.readonly) }); - - return widget.render(); + if (conflict) { + return _('Gateway IP %s is already used by another link.').format(val); + } + return true; + }; + o_gw.write = function(section_id, value) { + if (getActiveMode() === 'multi_gw') { + if (value != null && value.trim() !== '') { + uci.set('linkback', section_id, 'gateway', value.trim()); + } else { + uci.remove('linkback', section_id, 'gateway'); + } + } else { + uci.remove('linkback', section_id, 'gateway'); + } }; - o.validate = function(section_id, value) { + // 3b. Gateway Alias (Multi-GW mode only, Modal only) + var o_alias = s.option(form.Value, 'name_alias', _('Gateway Alias (Optional)'), + _('Descriptive alias for this gateway (e.g. Bypass_GW, Main_Router). If empty, Gateway IP will be used.')); + o_alias.modalonly = true; + o_alias.placeholder = 'Bypass_GW'; + var origRenderAlias = o_alias.render; + o_alias.render = function(option_index, section_id, in_table) { + if (getActiveMode() !== 'multi_gw') { + return Promise.resolve(E('div', { 'style': 'display: none !important;' })); + } + return origRenderAlias.call(this, option_index, section_id, in_table); + }; + o_alias.cfgvalue = function(section_id) { + if (getActiveMode() === 'multi_gw') { + var gw = uci.get('linkback', section_id, 'gateway'); + var name = uci.get('linkback', section_id, 'name'); + if (name && name !== gw) { + return name; + } + } + return ''; + }; + o_alias.write = function(section_id, value) { + if (getActiveMode() === 'multi_gw') { + var gw = uci.get('linkback', section_id, 'gateway'); + if (!value || value.trim() === '') { + uci.set('linkback', section_id, 'name', gw || section_id); + } else { + uci.set('linkback', section_id, 'name', value.trim()); + } + } + }; + + // 3c. WAN Interface (Multi-WAN mode only, Modal only) + var o_iface = s.option(form.ListValue, 'name_iface', _('WAN Interface'), + _('Logical interface from firewall WAN zone.')); + o_iface.modalonly = true; + Object.keys(wan_interfaces).forEach(function(iface) { + o_iface.value(iface); + }); + uci.sections('linkback', 'link').forEach(function(sec) { + if (sec.name && !wan_interfaces[sec.name]) { + o_iface.value(sec.name, _('%s (configured)').format(sec.name)); + } + }); + var origRenderIface = o_iface.render; + o_iface.render = function(option_index, section_id, in_table) { + if (getActiveMode() !== 'multi_wan') { + return Promise.resolve(E('div', { 'style': 'display: none !important;' })); + } + return origRenderIface.call(this, option_index, section_id, in_table); + }; + o_iface.cfgvalue = function(section_id) { + if (getActiveMode() === 'multi_wan') { + return uci.get('linkback', section_id, 'name'); + } + return null; + }; + o_iface.validate = function(section_id, value) { + if (getActiveMode() !== 'multi_wan') { + return true; + } + if (!value) { + return _('WAN Interface is required.'); + } var added = false; uci.sections('linkback', 'link').forEach(function(sec) { if (sec['.name'] !== section_id && sec.name === value) { @@ -224,11 +424,16 @@ return view.extend({ } return true; }; + o_iface.write = function(section_id, value) { + if (getActiveMode() === 'multi_wan') { + uci.set('linkback', section_id, 'name', value); + uci.remove('linkback', section_id, 'gateway'); + } + }; - makeTableColumnExpand(o, '25%'); - - // 3. Priority (No description for main column) - o = s.option(form.Value, 'priority', _('Priority')); + // 3. Priority + o = s.option(form.Value, 'priority', _('Priority'), + _('Lower number indicates higher priority (e.g. 1 = Primary, 2 = Backup).')); o.datatype = 'uinteger'; o.default = '1'; o.rmempty = false; @@ -238,7 +443,6 @@ return view.extend({ } var self_opt = this; var has_conflict = false; - var conflicting_iface = ''; uci.sections('linkback', 'link').forEach(function(sec) { var sid = sec['.name']; if (sid !== section_id) { @@ -248,16 +452,15 @@ return view.extend({ } if (other_val != null && other_val !== '' && String(other_val) === String(value)) { has_conflict = true; - conflicting_iface = uci.get('linkback', sid, 'name') || sid; } } }); if (has_conflict) { - return t_priority_conflict.format(value, conflicting_iface); + return t_priority_conflict.format(value); } return true; }; - makeTableColumnExpand(o, '15%'); + makeTableColumnExpand(o, '12%'); // 4. Metric (Read-only, generated from priority * 10) var metric_title = _('Base Metric'); @@ -273,7 +476,7 @@ return view.extend({ } return prio_val * 10; }; - makeTableColumnExpand(o, '15%'); + makeTableColumnExpand(o, '12%'); // 5. Dummy display option for Check Type in main Grid table (read-only) o = s.option(form.DummyValue, 'check_type_disp', _('Check Type')); @@ -287,10 +490,11 @@ return view.extend({ return _('TCP Probe'); return _('-- Not Configured --'); }; - makeTableColumnExpand(o, '20%'); + makeTableColumnExpand(o, '18%'); - // 6. Check Type Dropdown (Virtual field, Modal only to avoid Save & Apply side-effects) - o = s.option(form.ListValue, 'check_type', _('Check Type')); + // 6. Check Type Dropdown (Virtual field, Modal only) + o = s.option(form.ListValue, 'check_type', _('Check Type'), + _('Health probe method for this specific target.')); o.value('ping', _('Ping Probe')); o.value('dns', _('DNS Probe')); o.value('tcp', _('TCP Probe')); @@ -298,7 +502,6 @@ return view.extend({ o.rmempty = false; o.modalonly = true; - // Derive the check type from which probe target is configured in UCI. o.cfgvalue = function(section_id) { if (uci.get('linkback', section_id, 'ping_targets')) return 'ping'; @@ -306,22 +509,19 @@ return view.extend({ return 'dns'; if (uci.get('linkback', section_id, 'tcp_target')) return 'tcp'; - return 'ping'; // Default to ping if not configured + return 'ping'; }; - // Only write when the check type actually CHANGES from the current state. o.write = function(section_id, value) { var current = uci.get('linkback', section_id, 'ping_targets') ? 'ping' : (uci.get('linkback', section_id, 'dns_server') ? 'dns' : (uci.get('linkback', section_id, 'tcp_target') ? 'tcp' : '')); var next = (value == null) ? 'ping' : String(value); - // No change - do nothing if (next === current) { return; } - // Clear old type's fields and any legacy weights/thresholds uci.remove('linkback', section_id, 'weight_threshold'); uci.remove('linkback', section_id, 'ping_weight'); uci.remove('linkback', section_id, 'dns_weight'); @@ -340,14 +540,15 @@ return view.extend({ } }; - // 7. Ping Probe Parameters + // 7. Custom Ping Probe Parameters o = s.option(form.Value, 'ping_targets', _('Ping Targets'), - _('Comma-separated list of IPs to ping (e.g., 223.5.5.5,8.8.8.8).')); - o.rmempty = true; + _('Custom IP list to ping through this link/gateway (comma-separated, e.g. 223.5.5.5,119.29.29.29).')); + o.default = '223.5.5.5,119.29.29.29'; + o.rmempty = false; o.modalonly = true; o.depends('check_type', 'ping'); o.validate = function(section_id, value) { - if (!value) return true; + if (!value) return _('Ping Targets is required.'); var ips = value.replace(/\s+/g, '').split(','); for (var i = 0; i < ips.length; i++) { var ipPattern = /^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; @@ -366,64 +567,38 @@ return view.extend({ } }; - // 7. DNS Probe Parameters + // 8. Custom DNS Probe Parameters o = s.option(form.Value, 'dns_server', _('DNS Server'), - _('DNS server IP for UDP query probe (e.g., 119.29.29.29).')); + _('DNS server IP for UDP query probe (e.g. 119.29.29.29).')); o.datatype = 'ip4addr'; - o.rmempty = true; + o.rmempty = false; o.modalonly = true; o.depends('check_type', 'dns'); o = s.option(form.Value, 'dns_domain', _('DNS Domain'), - _('Domain name to resolve for DNS probe (e.g., www.baidu.com).')); - o.rmempty = true; + _('Domain name to resolve for DNS probe (e.g. www.baidu.com).')); + o.default = 'www.baidu.com'; + o.rmempty = false; o.modalonly = true; o.depends('check_type', 'dns'); - // 8. TCP Probe Parameters + // 9. Custom TCP Probe Parameters o = s.option(form.Value, 'tcp_target', _('TCP Target'), _('Target IP for TCP handshake probe.')); o.datatype = 'ip4addr'; - o.rmempty = true; + o.rmempty = false; o.modalonly = true; o.depends('check_type', 'tcp'); o = s.option(form.Value, 'tcp_port', _('TCP Port'), - _('Target port for TCP handshake probe.')); + _('Target port for TCP handshake probe (e.g. 80 or 443).')); o.datatype = 'port'; - o.rmempty = true; + o.default = '80'; + o.rmempty = false; o.modalonly = true; o.depends('check_type', 'tcp'); - // 9. Individual health check options (Modal only) - o = s.option(form.Value, 'check_interval', _('Check Interval (s)'), - _('Time in seconds between each health check cycle for this link.')); - o.datatype = 'uinteger'; - o.default = '5'; - o.rmempty = false; - o.modalonly = true; - - o = s.option(form.Value, 'check_timeout', _('Check Timeout (s)'), - _('Maximum wait time in seconds for each individual check probe for this link.')); - o.datatype = 'uinteger'; - o.default = '3'; - o.rmempty = false; - o.modalonly = true; - - o = s.option(form.Value, 'recovery_delay', _('Recovery Delay'), - _('Number of consecutive successful checks required before marking this link as healthy (failback anti-flap).')); - o.datatype = 'uinteger'; - o.default = '3'; - o.rmempty = false; - o.modalonly = true; - - o = s.option(form.Value, 'failover_delay', _('Failover Delay'), - _('Number of consecutive failed checks required before marking this link as faulted (failover anti-flap).')); - o.datatype = 'uinteger'; - o.default = '2'; - o.rmempty = false; - o.modalonly = true; - + // 提示:超时与防抖延迟由全局配置统一管理,子项完全无需重复配置 return m.render(); } }); diff --git a/luci-app-linkback/po/zh_Hans/linkback.po b/luci-app-linkback/po/zh_Hans/linkback.po index fbd82d5e9..29b92ad92 100644 --- a/luci-app-linkback/po/zh_Hans/linkback.po +++ b/luci-app-linkback/po/zh_Hans/linkback.po @@ -1,5 +1,5 @@ msgid "" -msgstr "Content-Type: text/plain; charset=UTF-8" +msgstr "Content-Type: text/plain; charset=UTF-8\n" msgid "LinkBack 链路守护" msgstr "网关切换" @@ -16,6 +16,12 @@ msgstr "日志" msgid "Real-time health state, priority mapping, and service control for Multi-WAN default routing metrics." msgstr "用于 Multi-WAN 默认路由 Metric 的实时健康状态、优先级映射及服务控制。" +msgid "Real-time health state, priority mapping, and service control for Multi-WAN / Multi-Gateway failover." +msgstr "用于多 WAN / 多网关容灾切换的实时健康状态、优先级映射与服务控制。" + +msgid "Configure Multi-WAN / Multi-Gateway failover service, shared health check parameters, and monitored targets." +msgstr "配置多 WAN / 多网关容灾切换服务、全局共用健康检测参数与监控目标。" + msgid "Service Status" msgstr "服务状态" @@ -37,6 +43,9 @@ msgstr "停止" msgid "Link Status" msgstr "链路状态" +msgid "Link / Gateway Status" +msgstr "链路 / 网关状态" + msgid "Loading real-time link status..." msgstr "正在加载实时链路状态..." @@ -49,18 +58,93 @@ msgstr "已停止" msgid "LinkBack daemon is not running or no links are configured. Please check Settings page." msgstr "网关切换守护进程未运行或未配置任何链路。请检查设置页面。" -msgid "主选链路 / Active" -msgstr "主选网关 / 活跃中" +msgid "LinkBack daemon is not running or no targets are configured. Please check Settings page." +msgstr "网关切换守护进程未运行或未配置任何监控目标。请检查设置页面。" -msgid "次选链路 / Standby" -msgstr "备选网关 / 待命" +msgid "Working Mode" +msgstr "工作模式" -msgid "备选链路 / Faulted" -msgstr "故障网关 / 已断开" +msgid "多wan口模式" +msgstr "多wan口模式" + +msgid "单wan多网关模式" +msgstr "单wan多网关模式" + +msgid "wan网络接口" +msgstr "wan网络接口" + +msgid "选择存在多个wan网关的接口" +msgstr "选择存在多个wan网关的接口" + +msgid "Choose failover mode: Multi-WAN interface failover or Single-WAN multi-gateway redundancy." +msgstr "选择容灾模式:多 WAN 接口链路容灾,或单 WAN 多网关主备容灾。" + +msgid "Monitored Gateways" +msgstr "监控网关列表" + +msgid "Monitored WAN Interfaces" +msgstr "监控 WAN 接口列表" + +msgid "Add next-hop gateways with custom probe targets and priorities (1 = primary, 2 = backup)." +msgstr "添加下一跳网关并配置自定义探测目标与优先级 (1 = 主选,2 = 备选)。" + +msgid "Add WAN interfaces from firewall zone with custom probe targets and priorities (1 = primary, 2 = backup)." +msgstr "添加来自防火墙区域的 WAN 接口并配置自定义探测目标与优先级 (1 = 主选,2 = 备选)。" + +msgid "WAN Interface" +msgstr "WAN 接口" + +msgid "Logical interface from firewall WAN zone." +msgstr "来自防火墙 WAN 区域的逻辑接口。" + +msgid "Gateway Alias (Optional)" +msgstr "网关别名 (可选)" + +msgid "Descriptive alias for this gateway (e.g. Bypass_GW, Main_Router). If empty, Gateway IP will be used." +msgstr "该网关的自定义别名 (例如:旁路由主网关、光猫备用网关)。若留空则自动使用网关 IP。" + +msgid "Gateway IP" +msgstr "网关 IP" + +msgid "Next-hop IPv4 address of this gateway (e.g. 192.168.1.254)." +msgstr "该网关的下一跳 IPv4 地址 (例如:192.168.1.254)。" + +msgid "Gateway IP is required." +msgstr "网关 IP 不能为空。" + +msgid "Gateway IP %s is already used by another link." +msgstr "网关 IP %s 已被其他目标使用,必须唯一。" + +msgid "Add Monitored Gateway" +msgstr "添加监控网关" + +msgid "Edit Monitored Gateway" +msgstr "编辑监控网关" + +msgid "Add Monitored WAN Interface" +msgstr "添加监控 WAN 接口" + +msgid "Edit Monitored WAN Interface" +msgstr "编辑监控 WAN 接口" + +msgid "Target / Interface" +msgstr "监控目标 / 接口" + +msgid "Running State" +msgstr "运行状态" + +msgid "Routing Attributes" +msgstr "路由属性" + +msgid "Latency Details" +msgstr "时延明细" msgid "Priority" msgstr "优先级" +msgid "Lower number indicates higher priority (e.g. 1 = Primary, 2 = Backup)." +msgstr "数值越小代表优先级越高 (例如:1 = 主选,2 = 备选)。" + msgid "Metric" msgstr "路由跃点 (Metric)" @@ -85,9 +169,6 @@ msgstr "服务操作执行失败。" msgid "RPC error: %s" msgstr "RPC 错误: %s" -msgid "Configure Multi-WAN failover service, health check parameters, and monitored link interfaces." -msgstr "配置多 WAN 故障切换服务、健康度检查参数及被监控的网关接口。" - msgid "Global Settings" msgstr "全局设置" @@ -100,39 +181,30 @@ msgstr "启用或禁用网关切换服务的全局总开关。" msgid "Check Interval (s)" msgstr "检测间隔 (秒)" -msgid "Time in seconds between each health check cycle." -msgstr "每个健康度检测周期之间的时间间隔。" +msgid "Shared interval in seconds between health check cycles for all targets." +msgstr "所有链路与网关共用的健康检测周期时间 (秒)。" msgid "Check Timeout (s)" msgstr "检测超时 (秒)" -msgid "Maximum wait time in seconds for each individual check probe." -msgstr "每个检测探针的最大等待超时时间。" +msgid "Shared maximum wait time in seconds for each probe (1s recommended to prevent blocking)." +msgstr "所有探针共用的最大等待超时时间 (推荐 1 秒以杜绝网络阻塞)。" msgid "Recovery Delay" msgstr "恢复判定延迟" -msgid "Number of consecutive successful checks required before marking a faulted link as healthy again (failback anti-flap)." +msgid "Number of consecutive successful checks required before marking link as recovered (failback anti-flap)." msgstr "故障网关恢复健康所需连续检测成功的次数 (防抖动回切)。" msgid "Failover Delay" msgstr "故障判定延迟" -msgid "Number of consecutive failed checks required before marking a healthy link as faulted (failover anti-flap)." +msgid "Number of consecutive failed checks required before marking link as faulted (failover anti-flap)." msgstr "健康网关判定为故障所需连续检测失败的次数 (防抖动切换)。" -msgid "Monitored WAN Interfaces" -msgstr "监控网关接口" - -msgid "Add and prioritize your WAN interfaces. Lower priority number means higher preference (e.g., 1 = primary, 2 = backup)." -msgstr "添加并配置您的广域网网关接口优先级。数值越小代表优先级越高 (例如:1 = 主网关,2 = 备用网关)。" - msgid "Interface" msgstr "网络接口" -msgid "Select a logical WAN interface from Network settings." -msgstr "从系统网络配置中选择一个逻辑 WAN 接口。" - msgid "Enabled" msgstr "已启用" @@ -143,29 +215,29 @@ msgid "Default route metric when this link is healthy (e.g., wan=10, wan2=20)." msgstr "当该接口健康时应用的默认路由 Metric 跃点数值 (例如:wan=10, wan2=20)。" msgid "Ping Targets" -msgstr "Ping 检测目标" +msgstr "Ping 探测目标" -msgid "Space-separated list of IPs to ping (e.g., 223.5.5.5 8.8.8.8)." -msgstr "通过空格分隔的 Ping 目标 IP 地址列表 (例如:223.5.5.5 8.8.8.8)。" +msgid "Custom IP list to ping through this link/gateway (comma-separated, e.g. 223.5.5.5,119.29.29.29)." +msgstr "通过该链路/网关探测的目标 IP 列表 (逗号分隔,例如:223.5.5.5,119.29.29.29)。" -msgid "Ping Weight" -msgstr "Ping 权重" +msgid "Ping Targets is required." +msgstr "Ping 探测目标 IP 不能为空。" + +msgid "Invalid IP address: \"%s\"" +msgstr "无效的 IP 地址: \"%s\"" msgid "DNS Server" msgstr "DNS 服务器" -msgid "DNS server IP for UDP query probe (e.g., 119.29.29.29)." +msgid "DNS server IP for UDP query probe (e.g. 119.29.29.29)." msgstr "用于 UDP 解析探测的 DNS 服务器 IP (例如:119.29.29.29)。" msgid "DNS Domain" msgstr "DNS 解析域名" -msgid "Domain name to resolve for DNS probe (e.g., www.baidu.com)." +msgid "Domain name to resolve for DNS probe (e.g. www.baidu.com)." msgstr "用于 DNS 探针解析的测试目标域名 (例如:www.baidu.com)。" -msgid "DNS Weight" -msgstr "DNS 权重" - msgid "TCP Target" msgstr "TCP 检测目标" @@ -175,17 +247,8 @@ msgstr "用于 TCP 三次握手探测的目标主机 IP 地址。" msgid "TCP Port" msgstr "TCP 检测端口" -msgid "Target port for TCP handshake probe." -msgstr "用于 TCP 探测的测试目标端口号。" - -msgid "TCP Weight" -msgstr "TCP 权重" - -msgid "Health Threshold" -msgstr "健康门槛" - -msgid "Combined weight score required to mark this link healthy (e.g., if threshold is 2, any 2 successful checks suffice)." -msgstr "判定该接口健康所需的各指标合并通过总分值 (例如:门槛为 2,任意两项检测成功即可)。" +msgid "Target port for TCP handshake probe (e.g. 80 or 443)." +msgstr "用于 TCP 探测的测试目标端口号 (例如:80 或 443)。" msgid "Loading logs..." msgstr "正在加载运行日志..." @@ -199,71 +262,32 @@ msgstr "系统日志" msgid "No logs available." msgstr "暂无可用运行日志。" -msgid "Enable Ping Probe" -msgstr "启用 Ping 检测" - -msgid "Enable DNS Probe" -msgstr "启用 DNS 检测" - -msgid "Enable TCP Probe" -msgstr "启用 TCP 检测" - -msgid "Select a logical WAN interface." -msgstr "选择逻辑 WAN 接口。" - -msgid "Routing priority (lower = higher preference)." -msgstr "路由优先级(数值越小优先级越高)。" - -msgid "Default route metric when healthy." -msgstr "接口健康时的默认路由跃点 (Metric) 数值。" - -msgid "Combined weight score required to mark this link healthy." -msgstr "判定该接口健康所需的各检测指标成功权重分之和。" - -msgid "Collecting..." -msgstr "正在收集数据..." - -msgid "Running State" -msgstr "运行状态" - msgid "Check Type" msgstr "检测类型" -msgid "Time in seconds between each health check cycle for this link." -msgstr "该链路每次健康度检测周期之间的时间间隔 (秒)。" +msgid "Health probe method for this specific target." +msgstr "该目标采用的健康探测方式。" -msgid "Maximum wait time in seconds for each individual check probe for this link." -msgstr "该链路每个检测探针的最大等待超时时间 (秒)。" +msgid "Cannot enable service: At least 2 monitored links/gateways must be configured for failover switcher." +msgstr "无法启用服务: 容灾切换至少需要配置 2 个监控链路或网关。" -msgid "Number of consecutive successful checks required before marking this link as healthy (failback anti-flap)." -msgstr "判定该链路恢复健康所需连续检测成功的次数 (防抖动回切)。" +msgid "Cannot enable service: WAN interface must be configured in Multi-Gateway mode." +msgstr "无法启用服务: 单wan多网关模式下必须指定wan网络接口。" -msgid "Number of consecutive failed checks required before marking this link as faulted (failover anti-flap)." -msgstr "判定该链路发生故障所需连续检测失败的次数 (防抖动切换)。" +msgid "Cannot enable service: Target \"%s\" has no health check configured." +msgstr "无法启用服务: 监控目标「%s」未配置健康检测策略。" -msgid "Add Monitored Interface" -msgstr "添加监控接口" +msgid "Cannot enable service: Targets \"%s\" and \"%s\" have the same priority %s." +msgstr "无法启用服务: 目标「%s」和「%s」的路由优先级均为 %s,不允许重复。" -msgid "Edit Monitored Interface" -msgstr "编辑监控接口" +msgid "Cannot enable service: Target \"%s\" has an invalid or missing Gateway IP." +msgstr "无法启用服务: 目标「%s」缺少有效的网关 IP。" -msgid "Cannot enable service: No monitored WAN interfaces are configured." -msgstr "无法启用服务: 未配置任何监控的 WAN 接口。" +msgid "Cannot enable service: Targets \"%s\" and \"%s\" have the same Gateway IP %s." +msgstr "无法启用服务: 目标「%s」和「%s」具有相同的网关 IP %s,不允许重复。" -msgid "Cannot enable service: At least one interface must have a configured check type (Ping, DNS, or TCP)." -msgstr "无法启用服务: 至少需要为一个接口配置有效的检测类型 (Ping、DNS 或 TCP)。" - -msgid "-- Not Configured --" -msgstr "-- 未配置 --" - -msgid "Cannot enable service: At least 2 monitored WAN interfaces must be configured for failover switcher." -msgstr "无法启用服务: 链路备份切换至少需要配置 2 个监控的 WAN 接口。" - -msgid "Cannot enable service: Interface \"%s\" has no health check configured." -msgstr "无法启用服务: 接口「%s」未配置健康检测策略。" - -msgid "Cannot enable service: Interfaces \"%s\" and \"%s\" have the same priority %s." -msgstr "无法启用服务: 接口「%s」和「%s」的路由优先级均为 %s,不允许重复。" +msgid "Cannot enable service: Interface \"%s\" is configured multiple times." +msgstr "无法启用服务: 接口「%s」被重复配置。" msgid "Service has been auto-disabled because the configuration is no longer valid: " msgstr "由于配置不再合法,服务已被自动禁用: " @@ -286,37 +310,45 @@ msgstr "该接口已被配置,请选择其他接口。" msgid "Priority must not be empty." msgstr "优先级不能为空。" -msgid "Priority %s conflicts with interface \"%s\". Priorities must be unique." -msgstr "优先级 %s 与接口 \"%s\" 冲突。优先级必须是唯一的。" - -msgid "Metric must not be empty." -msgstr "默认跃点不能为空。" - -msgid "Metric %s conflicts with interface \"%s\". Metrics must be unique." -msgstr "跃点数 %s 与接口 \"%s\" 冲突。跃点数必须是唯一的。" - -msgid "Link Name" -msgstr "链路名称" - -msgid "Status" -msgstr "状态" - -msgid "Routing Attributes" -msgstr "路由属性" - -msgid "Latency Details" -msgstr "时延详情" +msgid "Priority %s conflicts with another target. Priorities must be unique." +msgstr "优先级 %s 与其他目标冲突。优先级必须是唯一的。" msgid "Active" -msgstr "活跃" +msgstr "主选活跃" msgid "Standby" -msgstr "备选" +msgstr "备选待命" msgid "Faulted" -msgstr "故障" +msgstr "故障离线" -msgid "Comma-separated list of IPs to ping (e.g., 223.5.5.5,8.8.8.8)." -msgstr "通过逗号分隔的 Ping 目标 IP 地址列表 (例如:223.5.5.5,8.8.8.8)。" +msgid "-- Not Configured --" +msgstr "-- 未配置 --" +msgid "Monitored Targets" +msgstr "监控目标列表" + +msgid "Configure monitored WAN interfaces or next-hop gateways with custom probe targets and priorities (1 = primary, 2 = backup)." +msgstr "配置监控的 WAN 接口或下一跳网关,并设置自定义探测目标与优先级 (1 = 主选,2 = 备选)。" + +msgid "Monitored Target" +msgstr "监控目标" + +msgid "Missing Gateway IP: %s" +msgstr "缺少网关 IP: %s" + +msgid "Unset" +msgstr "未设置" + +msgid "Unselected" +msgstr "未选择" + +msgid "WAN Interface is required." +msgstr "WAN 网络接口不能为空。" + +msgid "已切换为单wan多网关模式。请确保监控目标列表中配置了有效的网关 IP。" +msgstr "已切换为单wan多网关模式。请确保监控目标列表中配置了有效的网关 IP。" + +msgid "已切换为多wan口模式。请确保监控目标列表中选择了正确的 WAN 接口。" +msgstr "已切换为多wan口模式。请确保监控目标列表中选择了正确的 WAN 接口。" diff --git a/luci-app-linkback/root/usr/share/rpcd/acl.d/luci-app-linkback.json b/luci-app-linkback/root/usr/share/rpcd/acl.d/luci-app-linkback.json index c12bc9d77..b8164ae51 100644 --- a/luci-app-linkback/root/usr/share/rpcd/acl.d/luci-app-linkback.json +++ b/luci-app-linkback/root/usr/share/rpcd/acl.d/luci-app-linkback.json @@ -2,7 +2,7 @@ "luci-app-linkback": { "description": "ACL for LinkBack Multi-WAN Failover application", "read": { - "uci": [ "linkback", "network" ], + "uci": [ "linkback", "network", "firewall" ], "ubus": { "luci.linkback": [ "get_status", "get_log" ], "network.interface": [ "dump" ], diff --git a/luci-app-linkback/root/usr/share/rpcd/ucode/luci.linkback b/luci-app-linkback/root/usr/share/rpcd/ucode/luci.linkback index 4c744a548..086e95fe4 100644 --- a/luci-app-linkback/root/usr/share/rpcd/ucode/luci.linkback +++ b/luci-app-linkback/root/usr/share/rpcd/ucode/luci.linkback @@ -22,6 +22,8 @@ const methods = { if (!status) { status = { enabled: false, + mode: "multi_wan", + interface: "wan", check_interval: 5, active_link: "none", links: [] diff --git a/luci-app-open-box/Makefile b/luci-app-open-box/Makefile index feb07c9a2..0ee91aa1a 100644 --- a/luci-app-open-box/Makefile +++ b/luci-app-open-box/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-open-box -PKG_VERSION:=0.1.277 -PKG_RELEASE:=3 +PKG_VERSION:=0.1.278 +PKG_RELEASE:=4 PKG_MAINTAINER:=liandu2024 PKG_LICENSE:=GPL-3.0-or-later diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index 30d609b6b..3e4a4b409 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall PKG_VERSION:=26.10.1 -PKG_RELEASE:=306 +PKG_RELEASE:=307 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/root/usr/share/passwall/app.sh b/luci-app-passwall/root/usr/share/passwall/app.sh index e620229e0..f963d3362 100755 --- a/luci-app-passwall/root/usr/share/passwall/app.sh +++ b/luci-app-passwall/root/usr/share/passwall/app.sh @@ -930,6 +930,8 @@ clean_crontab() { start_crontab() { local update_loop + local setsid_cmd="" + command -v setsid >/dev/null 2>&1 && setsid_cmd="setsid " if [ "$ENABLED_DEFAULT_ACL" = "1" ] || [ "$ENABLED_ACLS" = "1" ]; then local start_daemon=$(config_n_get @global_delay[0] start_daemon 0) @@ -976,7 +978,7 @@ start_crontab() { if [ "$week" = "8" ]; then update_loop=1 else - echo "$svr_t /etc/init.d/$CONFIG $action cron > /dev/null 2>&1 &" >>/etc/crontabs/root + echo "$svr_t ${setsid_cmd}/etc/init.d/$CONFIG $action cron > /dev/null 2>&1 &" >>/etc/crontabs/root fi echolog "$logmsg" } @@ -996,7 +998,7 @@ start_crontab() { if [ "$rules_update_week_mode" = "8" ]; then update_loop=1 else - echo "$rule_t lua $APP_PATH/rule_update.lua log all cron > /dev/null 2>&1 &" >>/etc/crontabs/root + echo "$rule_t ${setsid_cmd}lua $APP_PATH/rule_update.lua log all cron > /dev/null 2>&1 &" >>/etc/crontabs/root fi echolog "配置定时任务:自动更新规则。" fi @@ -1024,7 +1026,7 @@ start_crontab() { if [ "$sub_update_week_mode" = "8" ]; then update_loop=1 else - echo "$sub_t lua $APP_PATH/subscribe.lua start $cfgids cron > /dev/null 2>&1 &" >>/etc/crontabs/root + echo "$sub_t ${setsid_cmd}lua $APP_PATH/subscribe.lua start $cfgids cron > /dev/null 2>&1 &" >>/etc/crontabs/root fi done rm -rf "$TMP_SUB_PATH" @@ -1050,6 +1052,22 @@ stop_crontab() { #echolog "清除定时执行命令。" } +restart_smartdns() { + rm -rf /tmp/smartdns.cache + /etc/init.d/smartdns reload >/dev/null 2>&1 +} + +del_smartdns_conf() { + command -v smartdns >/dev/null 2>&1 || return + rm -rf "/tmp/etc/smartdns/${CONFIG}.conf" + local custom_conf="/etc/smartdns/custom.conf" + if [ -f "$custom_conf" ] && grep -q "${CONFIG}" "$custom_conf"; then + sed -i "/${CONFIG}/d" "$custom_conf" >/dev/null 2>&1 + rm -rf /tmp/smartdns.cache + /etc/init.d/smartdns reload >/dev/null 2>&1 + fi +} + start_adblock() { [ "$(config_t_get global adblock 0)" != "1" ] && { [ -s $RULES_PATH/my_block_host ] && ln -sf $RULES_PATH/my_block_host $RULES_PATH/block_host || > $RULES_PATH/block_host @@ -1262,7 +1280,8 @@ start_dns() { -USE_DIRECT_LIST "${USE_DIRECT_LIST}" -USE_PROXY_LIST "${USE_PROXY_LIST}" -USE_BLOCK_LIST "${USE_BLOCK_LIST}" -USE_GFW_LIST "${USE_GFW_LIST}" -CHN_LIST "${CHN_LIST}" \ -NODE ${NODE} -DEFAULT_PROXY_MODE "${TCP_PROXY_MODE}" -NO_PROXY_IPV6 ${FILTER_PROXY_IPV6:-0} -NFTFLAG ${nftflag:-0} \ -SUBNET ${subnet_ip:-0} -NO_LOGIC_LOG ${NO_LOGIC_LOG:-0} - source $APP_PATH/helper_smartdns.sh restart + + restart_smartdns USE_DEFAULT_DNS="chinadns_ng" else @@ -1746,7 +1765,7 @@ stop() { unset XRAY_LOCATION_ASSET unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN stop_crontab $1 - source $APP_PATH/helper_smartdns.sh del + del_smartdns_conf rm -rf $GLOBAL_DNSMASQ_CONF rm -rf $GLOBAL_DNSMASQ_CONF_PATH [ "1" = "1" ] && { diff --git a/luci-app-passwall/root/usr/share/passwall/helper_smartdns.sh b/luci-app-passwall/root/usr/share/passwall/helper_smartdns.sh deleted file mode 100755 index 14254f141..000000000 --- a/luci-app-passwall/root/usr/share/passwall/helper_smartdns.sh +++ /dev/null @@ -1,30 +0,0 @@ -#!/bin/sh - -restart() { - local no_log - eval_set_val $@ - _LOG_FILE=$LOG_FILE - [ -n "$no_log" ] && LOG_FILE="/dev/null" - rm -rf /tmp/smartdns.cache - /etc/init.d/smartdns reload >/dev/null 2>&1 - LOG_FILE=${_LOG_FILE} -} - -del() { - rm -rf /tmp/etc/smartdns/passwall.conf - sed -i "/passwall/d" /etc/smartdns/custom.conf >/dev/null 2>&1 - rm -rf /tmp/smartdns.cache - /etc/init.d/smartdns reload >/dev/null 2>&1 -} - -arg1=$1 -shift -case $arg1 in -del) - del $@ - ;; -restart) - restart $@ - ;; -*) ;; -esac diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 4b33596bf..fe18202fa 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 PKG_VERSION:=26.10.1 -PKG_RELEASE:=133 +PKG_RELEASE:=134 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/root/etc/init.d/passwall2 b/luci-app-passwall2/root/etc/init.d/passwall2 index dfc587302..09fa28d36 100755 --- a/luci-app-passwall2/root/etc/init.d/passwall2 +++ b/luci-app-passwall2/root/etc/init.d/passwall2 @@ -5,11 +5,10 @@ STOP=15 . $IPKG_INSTROOT/usr/share/passwall2/utils.sh APP_FILE=${APP_PATH}/app.sh -LOCK_FILE_DIR=/var/lock -LOCK_FILE=${LOCK_FILE_DIR}/${CONFIG}.lock +LOCK_FILE=${LOCK_PATH}/${CONFIG}.lock set_lock() { - [ ! -d "$LOCK_FILE_DIR" ] && mkdir -p $LOCK_FILE_DIR + [ ! -d "$LOCK_PATH" ] && mkdir -p $LOCK_PATH exec 999>"$LOCK_FILE" flock -xn 999 } @@ -33,13 +32,16 @@ unlock() { } boot_func() { + rm -f ${LOCK_PATH}/${CONFIG}_ready.lock + rm -f ${LOCK_PATH}/${CONFIG}_rule_update.lock + rm -f ${LOCK_PATH}/${CONFIG}_subscribe.lock local delay=$(uci -q get ${CONFIG}.@global_delay[0].start_delay || echo 1) if [ "$delay" -gt 0 ]; then log_i18n 0 "Start after a delay of %s seconds!" "${delay}" sleep $delay fi restart boot - touch ${LOCK_FILE_DIR}/${CONFIG}_ready.lock + touch ${LOCK_PATH}/${CONFIG}_ready.lock } boot() { @@ -64,8 +66,8 @@ stop() { restart() { set_lock [ $? == 1 ] && log_i18n 0 "The script is already running, do not run it again. Exit." && exit 0 - rule_lock=${LOCK_FILE_DIR}/${CONFIG}_rule_update.lock - sub_lock=${LOCK_FILE_DIR}/${CONFIG}_subscribe.lock + rule_lock=${LOCK_PATH}/${CONFIG}_rule_update.lock + sub_lock=${LOCK_PATH}/${CONFIG}_subscribe.lock for i in $(seq 1 300); do if [ ! -f "$rule_lock" ] && [ ! -f "$sub_lock" ]; then break diff --git a/luci-app-passwall2/root/usr/share/passwall2/app.sh b/luci-app-passwall2/root/usr/share/passwall2/app.sh index 7f05d890a..f11e87065 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/app.sh +++ b/luci-app-passwall2/root/usr/share/passwall2/app.sh @@ -591,7 +591,7 @@ start_socks() { } clean_crontab() { - [ -f "/tmp/lock/${CONFIG}_cron.lock" ] && return + [ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && return touch /etc/crontabs/root #sed -i "/${CONFIG}/d" /etc/crontabs/root >/dev/null 2>&1 sed -i "/$(echo "/etc/init.d/${CONFIG}" | sed 's#\/#\\\/#g')/d" /etc/crontabs/root >/dev/null 2>&1 @@ -599,7 +599,7 @@ clean_crontab() { sed -i "/$(echo "lua ${APP_PATH}/subscribe.lua start" | sed 's#\/#\\\/#g')/d" /etc/crontabs/root >/dev/null 2>&1 busybox pgrep -af "${CONFIG}/" | awk '/tasks\.sh/{print $1}' | xargs kill -9 >/dev/null 2>&1 - rm -rf /tmp/lock/${CONFIG}_tasks.lock + rm -f ${LOCK_PATH}/${CONFIG}_tasks.lock } start_crontab() { @@ -608,8 +608,8 @@ start_crontab() { [ "$start_daemon" = "1" ] && { $APP_PATH/monitor.sh > /dev/null 2>&1 & } fi - [ -f "/tmp/lock/${CONFIG}_cron.lock" ] && { - rm -rf "/tmp/lock/${CONFIG}_cron.lock" + [ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && { + rm -rf "${LOCK_PATH}/${CONFIG}_cron.lock" log_i18n 0 "The task is currently running automatically as a scheduled task; no reconfiguration of the scheduled task is required." return } @@ -716,7 +716,7 @@ start_crontab() { } stop_crontab() { - [ -f "/tmp/lock/${CONFIG}_cron.lock" ] && return + [ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && return clean_crontab /etc/init.d/cron restart #log_i18n 0 "Clear scheduled commands." @@ -934,6 +934,9 @@ start() { } fi run_process_queue + [ "$1" = "boot" ] && { + rm -f "${LOCK_PATH}/${CONFIG}_cron.lock" + } start_crontab log_i18n 0 "Running complete!" echolog "\n" @@ -996,8 +999,15 @@ stop() { [ -n "${bak_bridge_nf_ip6t}" ] && sysctl -w net.bridge.bridge-nf-call-ip6tables=${bak_bridge_nf_ip6t} >/dev/null 2>&1 } rm -rf $TMP_PATH - rm -rf /tmp/lock/${CONFIG}_socks_auto_switch* - rm -rf /tmp/lock/${CONFIG}_lease2hosts* + rm -f ${LOCK_PATH}/${CONFIG}_socks_auto_switch* + rm -f ${LOCK_PATH}/${CONFIG}_lease2hosts* + rm -f ${LOCK_PATH}/${CONFIG}_monitor* + if ! busybox pgrep -af "${CONFIG}/" | grep -q '/subscribe\.lua'; then + rm -f "${LOCK_PATH}/${CONFIG}_subscribe.lock" + fi + if ! busybox pgrep -af "${CONFIG}/" | grep -q '/rule_update\.lua'; then + rm -f "${LOCK_PATH}/${CONFIG}_rule_update.lock" + fi log_i18n 0 "Clearing and closing related programs and cache complete." exit 0 } diff --git a/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua b/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua index 1a6abf223..0ef8e387c 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/rule_update.lua @@ -239,8 +239,8 @@ uci_save(true) if reboot == 1 then if arg3 == "cron" then - if not fs.access("/var/lock/" .. appname .. ".lock") then - sys.call("touch /tmp/lock/" .. appname .. "_cron.lock") + if not fs.access(api.LOCK_PREFIX .. ".lock") then + sys.call("touch %s_cron.lock" % api.LOCK_PREFIX) end end diff --git a/luci-app-ssr-plus/Makefile b/luci-app-ssr-plus/Makefile index e9fb4307c..902f584dd 100644 --- a/luci-app-ssr-plus/Makefile +++ b/luci-app-ssr-plus/Makefile @@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus LUCI_PKGARCH:=all PKG_NAME:=luci-app-ssr-plus PKG_VERSION:=196 -PKG_RELEASE:=78 +PKG_RELEASE:=79 PKG_CONFIG_DEPENDS:= \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ diff --git a/luci-app-ssr-plus/root/usr/share/shadowsocksr/gfw2ipset.sh b/luci-app-ssr-plus/root/usr/share/shadowsocksr/gfw2ipset.sh index a2a38e742..971ca897d 100755 --- a/luci-app-ssr-plus/root/usr/share/shadowsocksr/gfw2ipset.sh +++ b/luci-app-ssr-plus/root/usr/share/shadowsocksr/gfw2ipset.sh @@ -85,10 +85,10 @@ done # 此处直接使用 cat 因为有 sed '/#/d' 删除了 数据 if [ "$nft_support" = "1" ]; then sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' -e "s/.*/server=\/&\/127.0.0.1#$dns_port\nnftset=\/&\/inet#ss_spec#blacklist/" /etc/ssrplus/black.list > "$TMP_DNSMASQ_PATH/blacklist_forward.conf" - sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' -e "s/.*/server=\/&\/127.0.0.1\nnftset=\/&\/inet#ss_spec#whitelist/" /etc/ssrplus/white.list > "$TMP_DNSMASQ_PATH/whitelist_forward.conf" + sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' -e "s/.*/nftset=\/&\/4#inet#ss_spec#whitelist/" /etc/ssrplus/white.list > "$TMP_DNSMASQ_PATH/whitelist_forward.conf" elif [ "$nft_support" = "0" ]; then sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' -e "s/.*/server=\/&\/127.0.0.1#$dns_port\nipset=\/&\/blacklist/" /etc/ssrplus/black.list > "$TMP_DNSMASQ_PATH/blacklist_forward.conf" - sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' -e "s/.*/server=\/&\/127.0.0.1\nipset=\/&\/whitelist/" /etc/ssrplus/white.list > "$TMP_DNSMASQ_PATH/whitelist_forward.conf" + sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' -e "s/.*/ipset=\/&\/whitelist/" /etc/ssrplus/white.list > "$TMP_DNSMASQ_PATH/whitelist_forward.conf" fi sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' -e "s/.*/address=\/&\//" /etc/ssrplus/deny.list > "$TMP_DNSMASQ_PATH/denylist.conf" diff --git a/luci-theme-argon/Makefile b/luci-theme-argon/Makefile index dafb34d19..bc790792a 100644 --- a/luci-theme-argon/Makefile +++ b/luci-theme-argon/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=Argon Theme LUCI_DEPENDS:=+USE_APK:wget-any +!USE_APK:wget +jsonfilter PKG_VERSION:=2.4.7 -PKG_RELEASE:=24 +PKG_RELEASE:=25 CONFIG_LUCI_CSSTIDY:= diff --git a/luci-theme-argon/htdocs/luci-static/argon/css/dark.css b/luci-theme-argon/htdocs/luci-static/argon/css/dark.css index 871933e2a..e92b8a84c 100644 --- a/luci-theme-argon/htdocs/luci-static/argon/css/dark.css +++ b/luci-theme-argon/htdocs/luci-static/argon/css/dark.css @@ -3,3 +3,4 @@ body{background:#1e1e1e;color:#ccc}.spinning::before{filter:invert(1)}.login-pag .login-page.defwallpaper .login-container{background-color: rgb(136 136 136 / 14%);backdrop-filter: blur(0);margin:0 auto;} .defwallpaper.defwallpaper .main-bg img{object-position: -99999px -99999px;opacity: 0;} .main .main-right{background-color: transparent} +input[placeholder]::placeholder {color: #888888;opacity: 1} diff --git a/rtp2httpd/Makefile b/rtp2httpd/Makefile index 4520db257..d18080a88 100644 --- a/rtp2httpd/Makefile +++ b/rtp2httpd/Makefile @@ -5,8 +5,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=rtp2httpd -PKG_VERSION:=3.17.1 -PKG_RELEASE:=1 +PKG_VERSION:=3.17.2 +PKG_RELEASE:=2 PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE_URL:=https://codeload.github.com/stackia/rtp2httpd/tar.gz/v$(PKG_VERSION)?