🐤 Sync 2026-10-03 00:12:36

This commit is contained in:
github-actions[bot] committed 2026-10-03 00:12:36 +08:00
1 parent ce3df55b24
commit cb1d978497
58 files changed
+1625 -1016

No files matched your search

+2 -2
View File
@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=antileech PKG_NAME:=antileech
PKG_RELEASE:=3 PKG_RELEASE:=4
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_DATE:=2021-12-05 PKG_SOURCE_DATE:=2021-12-05
PKG_SOURCE_URL:=https://github.com/persmule/amule-dlp.antiLeech.git PKG_SOURCE_URL:=https://github.com/persmule/amule-dlp.antiLeech.git
PKG_SOURCE_VERSION:=1b14ed828ed227933333de31ffb8e24e6c10451c PKG_SOURCE_VERSION:=bc3ae6a86c958dab1bde9c2cea51b523530aec4b
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
PKG_FIXUP:=autoreconf PKG_FIXUP:=autoreconf
+4 -4
View File
@@ -1,17 +1,17 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=clashoo PKG_NAME:=clashoo
PKG_SHORT_SHA:=88dcbf7 PKG_SHORT_SHA:=9f053c4
PKG_COMMIT_DATE:=2026.09.30 PKG_COMMIT_DATE:=2026.10.02
PKG_VERSION:=$(PKG_COMMIT_DATE)~$(PKG_SHORT_SHA) 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:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA).tar.gz
PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA) PKG_SOURCE_SUBDIR:=$(PKG_NAME)-alpha-$(PKG_SHORT_SHA)
PKG_BUILD_DIR:=$(BUILD_DIR)/$(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_URL:=https://github.com/kenzok8/openwrt-clashoo/releases/download/mihomo-src
PKG_SOURCE_VERSION:=f0c95853839454a5d1a9eda873d94b3d6c8cc8c3 PKG_SOURCE_VERSION:=c10fd37cf083bc4922237b7ec915d62614be0623
PKG_HASH:=skip PKG_HASH:=skip
PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA) PKG_BUILD_VERSION:=alpha-$(PKG_SHORT_SHA)
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=geoview PKG_NAME:=geoview
PKG_VERSION:=0.2.7 PKG_VERSION:=0.2.6
PKG_RELEASE:=6 PKG_RELEASE:=7
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/snowie2000/geoview/tar.gz/$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/snowie2000/geoview/tar.gz/$(PKG_VERSION)?
+4 -4
View File
@@ -5,12 +5,12 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=gn PKG_NAME:=gn
PKG_RELEASE:=7 PKG_RELEASE:=8
PKG_SOURCE_PROTO:=git PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://gn.googlesource.com/gn.git PKG_SOURCE_URL:=https://gn.googlesource.com/gn.git
PKG_SOURCE_DATE:=2026-01-14 PKG_SOURCE_DATE:=2026-06-27
PKG_SOURCE_VERSION:=103f8b437f5e791e0aef9d5c372521a5d675fabb PKG_SOURCE_VERSION:=8d1b023dec36ab50138d427e807175fd99b5bb57
PKG_MIRROR_HASH:=skip PKG_MIRROR_HASH:=skip
PKG_LICENSE:=BSD 3-Clause PKG_LICENSE:=BSD 3-Clause
@@ -43,7 +43,7 @@ define Host/Configure
endef endef
define Host/Compile define Host/Compile
+$(NINJA) -C $(HOST_BUILD_DIR)/out +$(NINJA) -C $(HOST_BUILD_DIR)/out gn
endef endef
define Host/Install define Host/Install
+2 -2
View File
@@ -3,7 +3,7 @@
#ifndef OUT_LAST_COMMIT_POSITION_H_ #ifndef OUT_LAST_COMMIT_POSITION_H_
#define OUT_LAST_COMMIT_POSITION_H_ #define OUT_LAST_COMMIT_POSITION_H_
#define LAST_COMMIT_POSITION_NUM 2319 #define LAST_COMMIT_POSITION_NUM 2434
#define LAST_COMMIT_POSITION "2319 (103f8b437f5e)" #define LAST_COMMIT_POSITION "2434 (8d1b023dec36)"
#endif // OUT_LAST_COMMIT_POSITION_H_ #endif // OUT_LAST_COMMIT_POSITION_H_
+2 -2
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=linkback PKG_NAME:=linkback
PKG_VERSION:=1.0.0 PKG_VERSION:=1.0.0
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_MAINTAINER:=Dante PKG_MAINTAINER:=Dante
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
@@ -59,7 +59,7 @@ define Package/$(PKG_NAME)/install
endef endef
define Package/$(PKG_NAME)/postinst define Package/$(PKG_NAME)/postinst
#!/sh #!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || { [ -n "$${IPKG_INSTROOT}" ] || {
/etc/init.d/linkback enable /etc/init.d/linkback enable
/etc/init.d/linkback restart /etc/init.d/linkback restart
+3 -2
View File
@@ -1,9 +1,10 @@
config global config global
option enabled '0' option enabled '0'
option mode 'multi_wan'
option interface 'wan'
option check_interval '5' option check_interval '5'
option check_timeout '3' option check_timeout '1'
option recovery_delay '3' option recovery_delay '3'
option failover_delay '2' option failover_delay '2'
+1 -1
View File
@@ -2,7 +2,7 @@ TARGET := linkbackd
OBJS := linkbackd.o OBJS := linkbackd.o
CFLAGS += -Wall -O2 CFLAGS += -Wall -O2
LIBS := -luci LIBS := -luci -lubox -lubus -lblobmsg_json
all: $(TARGET) all: $(TARGET)
+275 -101
View File
@@ -12,7 +12,12 @@
#include <arpa/inet.h> #include <arpa/inet.h>
#include <fcntl.h> #include <fcntl.h>
#include <errno.h> #include <errno.h>
#include <uci.h> #include <uci.h>
#include <libubox/blobmsg.h>
#include <libubox/blobmsg_json.h>
#include <libubus.h>
#include "linkbackd.h" #include "linkbackd.h"
// DNS Header struct for raw check // DNS Header struct for raw check
@@ -30,6 +35,7 @@ static global_config_t global_cfg;
static link_t links[MAX_LINKS]; static link_t links[MAX_LINKS];
static int link_count = 0; static int link_count = 0;
static volatile bool keep_running = true; static volatile bool keep_running = true;
static struct ubus_context *ubus_ctx = NULL;
// Prototypes // Prototypes
static void restore_all_metrics(void); static void restore_all_metrics(void);
@@ -39,76 +45,156 @@ static void handle_signal(int sig) {
keep_running = false; keep_running = false;
} }
// Ubus popen JSON parser // Ubus Connection Management
static bool get_interface_ubus_status(const char *ifname, char *device, int dev_len, char *gateway, int gw_len, bool *is_up) { static bool init_ubus_connection(void) {
char cmd[256]; if (ubus_ctx) {
snprintf(cmd, sizeof(cmd), "ubus call network.interface.%s status 2>/dev/null", ifname); return true;
FILE *fp = popen(cmd, "r"); }
if (!fp) return false; 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}; static void close_ubus_connection(void) {
int bytes_read = fread(buf, 1, sizeof(buf) - 1, fp); if (ubus_ctx) {
pclose(fp); 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 enum {
*is_up = false; IFACE_ATTR_UP,
char *p_up = strstr(buf, "\"up\":"); IFACE_ATTR_DEVICE,
if (p_up) { IFACE_ATTR_L3_DEVICE,
char *p_true = strstr(p_up, "true"); IFACE_ATTR_ROUTE,
// Ensure 'true' belongs to this property and isn't far away __IFACE_ATTR_MAX
if (p_true && p_true - p_up < 10) { };
*is_up = true;
} 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 if (tb[IFACE_ATTR_L3_DEVICE]) {
device[0] = '\0'; strncpy(data->device, blobmsg_get_string(tb[IFACE_ATTR_L3_DEVICE]), sizeof(data->device) - 1);
char *p_dev = strstr(buf, "\"l3_device\":"); } else if (tb[IFACE_ATTR_DEVICE]) {
int dev_key_len = 12; strncpy(data->device, blobmsg_get_string(tb[IFACE_ATTR_DEVICE]), sizeof(data->device) - 1);
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';
}
}
} }
// Extract gateway/nexthop from route list if (tb[IFACE_ATTR_ROUTE]) {
gateway[0] = '\0'; struct blob_attr *cur;
char *p_route = strstr(buf, "\"route\":"); int rem;
if (p_route) { blobmsg_for_each_attr(cur, tb[IFACE_ATTR_ROUTE], rem) {
char *p_nexthop = strstr(p_route, "\"nexthop\":"); struct blob_attr *rt_tb[__ROUTE_ATTR_MAX];
if (p_nexthop) { blobmsg_parse(route_policy, __ROUTE_ATTR_MAX, rt_tb, blobmsg_data(cur), blobmsg_len(cur));
char *p_start = strchr(p_nexthop + 10, '"');
if (p_start) { if (rt_tb[ROUTE_ATTR_NEXTHOP]) {
char *p_end = strchr(p_start + 1, '"'); const char *nh = blobmsg_get_string(rt_tb[ROUTE_ATTR_NEXTHOP]);
if (p_end) { if (nh && nh[0] != '\0') {
int len = p_end - p_start - 1; strncpy(data->gateway, nh, sizeof(data->gateway) - 1);
if (len >= gw_len) len = gw_len - 1; // Default route target 0.0.0.0 is top priority
strncpy(gateway, p_start + 1, len); if (rt_tb[ROUTE_ATTR_TARGET]) {
gateway[len] = '\0'; 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; 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) { 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 (device[0] == '\0' || target[0] == '\0') return false;
if (timeout <= 0) timeout = 1;
char cmd[256]; char cmd[256];
snprintf(cmd, sizeof(cmd), "ping -I %s -c 1 -W %d %s 2>/dev/null", device, timeout, target); 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); 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) { 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 (device[0] == '\0' || dns_server[0] == '\0' || domain[0] == '\0') return false;
if (timeout <= 0) timeout = 1;
struct timeval start, end; struct timeval start, end;
gettimeofday(&start, NULL); gettimeofday(&start, NULL);
@@ -181,12 +268,9 @@ static bool run_dns_check(const char *device, const char *dns_server, const char
} }
*dst++ = 0; *dst++ = 0;
unsigned short *qtype = (unsigned short *)dst; // Safe byte-by-byte write to prevent unaligned memory access SIGBUS on MIPS/ARM
*qtype = htons(1); // A Record *dst++ = 0; *dst++ = 1; // QTYPE: A (0x0001)
dst += 2; *dst++ = 0; *dst++ = 1; // QCLASS: IN (0x0001)
unsigned short *qclass = (unsigned short *)dst;
*qclass = htons(1); // IN
dst += 2;
int packet_len = dst - packet; 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; if (resp_len < 12) return false;
struct dns_header *resp_dns = (struct dns_header *)response; 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); gettimeofday(&end, NULL);
*rtt_ms = (int)((end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000); *rtt_ms = (int)((end.tv_sec - start.tv_sec) * 1000 + (end.tv_usec - start.tv_usec) / 1000);
return true; 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) { 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 (device[0] == '\0' || tcp_target[0] == '\0' || port <= 0) return false;
if (timeout <= 0) timeout = 1;
struct timeval start, end; struct timeval start, end;
gettimeofday(&start, NULL); gettimeofday(&start, NULL);
@@ -305,8 +390,15 @@ static bool load_config(void) {
return false; return false;
} }
// Parse global config // Default global settings
global_cfg.enabled = false; 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"); struct uci_section *global_sec = uci_lookup_section(ctx, pkg, "global");
if (!global_sec) { if (!global_sec) {
@@ -323,6 +415,30 @@ static bool load_config(void) {
if (global_sec) { if (global_sec) {
const char *enabled = uci_lookup_option_string(ctx, global_sec, "enabled"); const char *enabled = uci_lookup_option_string(ctx, global_sec, "enabled");
global_cfg.enabled = (enabled && strcmp(enabled, "1") == 0); 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 // Parse links
@@ -339,7 +455,7 @@ static bool load_config(void) {
memset(link, 0, sizeof(link_t)); memset(link, 0, sizeof(link_t));
const char *name = uci_lookup_option_string(ctx, s, "name"); 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); strncpy(link->name, name, MAX_NAME_LEN - 1);
link->enabled = true; link->enabled = true;
@@ -351,6 +467,13 @@ static bool load_config(void) {
link->metric = link->priority * 10; link->metric = link->priority * 10;
link->current_metric = link->metric; 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) // Parse ping targets (split by comma or space)
const char *pings = uci_lookup_option_string(ctx, s, "ping_targets"); const char *pings = uci_lookup_option_string(ctx, s, "ping_targets");
if (pings) { if (pings) {
@@ -359,11 +482,9 @@ static bool load_config(void) {
tmp[sizeof(tmp) - 1] = '\0'; tmp[sizeof(tmp) - 1] = '\0';
char *token = strtok(tmp, ", \t"); char *token = strtok(tmp, ", \t");
while (token && link->ping_target_count < MAX_TARGETS) { while (token && link->ping_target_count < MAX_TARGETS) {
// Trim leading whitespace
while (*token == ' ' || *token == '\t' || *token == '\r' || *token == '\n') { while (*token == ' ' || *token == '\t' || *token == '\r' || *token == '\n') {
token++; token++;
} }
// Trim trailing whitespace
char *end = token + strlen(token) - 1; char *end = token + strlen(token) - 1;
while (end > token && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) { while (end > token && (*end == ' ' || *end == '\t' || *end == '\r' || *end == '\n')) {
*end = '\0'; *end = '\0';
@@ -373,7 +494,7 @@ static bool load_config(void) {
strncpy(link->ping_targets[link->ping_target_count], token, MAX_IP_LEN - 1); strncpy(link->ping_targets[link->ping_target_count], token, MAX_IP_LEN - 1);
link->ping_target_count++; 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); if (tcp_p) link->tcp_port = atoi(tcp_p);
const char *interval = uci_lookup_option_string(ctx, s, "check_interval"); 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"); 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"); 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"); 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 // Default runtime states
link->healthy = true; link->healthy = true;
@@ -422,8 +543,7 @@ static bool load_config(void) {
return true; return true;
} }
// Startup-time defensive validation. LuCI-side checks can be bypassed by // Startup-time defensive validation.
// direct UCI edits, so daemon must refuse unsafe/incomplete configs.
static bool validate_loaded_config(void) { static bool validate_loaded_config(void) {
if (!global_cfg.enabled) { if (!global_cfg.enabled) {
return true; return true;
@@ -438,10 +558,22 @@ static bool validate_loaded_config(void) {
link_t *a = &links[i]; link_t *a = &links[i];
if (a->name[0] == '\0') { 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; 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) { if (a->priority <= 0) {
syslog(LOG_ERR, "Invalid config: link %s has invalid priority %d (must be > 0).", a->name, a->priority); syslog(LOG_ERR, "Invalid config: link %s has invalid priority %d (must be > 0).", a->name, a->priority);
return false; return false;
@@ -452,13 +584,24 @@ static bool validate_loaded_config(void) {
return false; return false;
} }
// No duplicate priorities // Check for conflicts
for (int j = i + 1; j < link_count; j++) { for (int j = i + 1; j < link_count; j++) {
link_t *b = &links[j]; link_t *b = &links[j];
if (a->priority == b->priority) { if (a->priority == b->priority) {
syslog(LOG_ERR, "Invalid config: duplicate priority %d on links %s and %s.", a->priority, a->name, b->name); syslog(LOG_ERR, "Invalid config: duplicate priority %d on links %s and %s.", a->priority, a->name, b->name);
return false; 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); bool has_ping = (a->ping_target_count > 0);
@@ -499,26 +642,38 @@ static bool validate_loaded_config(void) {
return true; return true;
} }
// Retrieve the real default route metric for a device from /proc/net/route // 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, int expected_metric) { static int get_system_route_metric(const char *device, const char *gateway, int expected_metric) {
if (device[0] == '\0') return -1; if (device[0] == '\0') return -1;
FILE *fp = fopen("/proc/net/route", "r"); FILE *fp = fopen("/proc/net/route", "r");
if (!fp) return -1; if (!fp) return -1;
char line[256]; char line[256];
char iface[32]; char iface[32];
unsigned long dest; unsigned long dest = 0;
unsigned long gw_hex = 0;
int metric = -1; int metric = -1;
int first_found_metric = -1; int first_found_metric = -1;
bool found_expected = false; 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 // Skip header line
if (fgets(line, sizeof(line), fp)) { if (fgets(line, sizeof(line), fp)) {
while (fgets(line, sizeof(line), fp)) { while (fgets(line, sizeof(line), fp)) {
// Destination is 2nd column, Metric is 7th column, dest is in hex. // Format: Iface Destination Gateway Flags RefCnt Use Metric Mask MTU Window IRTT
// Use %*s for Gateway and Flags to safely skip non-numeric characters. if (sscanf(line, "%31s %lx %lx %*s %*d %*d %d", iface, &dest, &gw_hex, &metric) == 4) {
if (sscanf(line, "%31s %lx %*s %*s %*d %*d %d", iface, &dest, &metric) == 3) {
if (strcmp(iface, device) == 0 && dest == 0) { 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) { if (first_found_metric == -1) {
first_found_metric = metric; 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); link->device, new_metric);
} }
syslog(LOG_INFO, "Applying route metric update on link %s (%s, priority %d): %d -> %d", syslog(LOG_INFO, "Applying route metric update on link %s (dev=%s, gw=%s, priority=%d): %d -> %d",
link->name, link->device, link->priority, link->current_metric, new_metric); link->name, link->device, link->gateway[0] ? link->gateway : "none", link->priority, link->current_metric, new_metric);
int rc = system(cmd); int rc = system(cmd);
if (rc == 0) { if (rc == 0) {
@@ -635,7 +790,9 @@ static void write_status_json(void) {
fprintf(fp, "{\n"); fprintf(fp, "{\n");
fprintf(fp, " \"enabled\": %s,\n", global_cfg.enabled ? "true" : "false"); 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) // Find current active gateway link (first healthy link ordered by priority)
char active_link[MAX_NAME_LEN] = "none"; 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) // Sort links by priority (lowest priority number first)
qsort(links, link_count, sizeof(link_t), compare_links); 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 // Core check loop
while (keep_running) { while (keep_running) {
time_t now = time(NULL); time_t now = time(NULL);
bool any_checked = false; 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++) { for (int i = 0; i < link_count; i++) {
link_t *link = &links[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) { if (now - link->last_checked < link->check_interval) {
continue; continue;
} }
@@ -728,13 +895,22 @@ int main(int argc, char **argv) {
char gw[MAX_IP_LEN] = {0}; char gw[MAX_IP_LEN] = {0};
bool is_up = false; 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; link->is_up = is_up;
strncpy(link->device, dev, MAX_NAME_LEN - 1); strncpy(link->device, dev, MAX_NAME_LEN - 1);
strncpy(link->gateway, gw, MAX_IP_LEN - 1); strncpy(link->gateway, gw, MAX_IP_LEN - 1);
// P2 optimization: Interface down short-circuit
if (!is_up || dev[0] == '\0') { if (!is_up || dev[0] == '\0') {
// Interface is down in netifd, mark unhealthy immediately
link->healthy = false; link->healthy = false;
link->current_score = 0; link->current_score = 0;
link->ping_ok = false; link->ping_ok = false;
@@ -743,14 +919,14 @@ int main(int argc, char **argv) {
link->consecutive_success = 0; link->consecutive_success = 0;
link->consecutive_failure = 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) { if (link->current_metric == link->metric) {
update_route_metric(link, 1000 + link->metric, -1); update_route_metric(link, 1000 + link->metric, -1);
} }
continue; continue;
} }
// 2. Perform health checks // 2. Perform health checks (P2: 1s non-blocking timeout)
bool check_success = false; bool check_success = false;
if (link->ping_target_count > 0) { if (link->ping_target_count > 0) {
@@ -789,8 +965,7 @@ int main(int argc, char **argv) {
link->current_score = check_success ? 1 : 0; 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) { if (check_success) {
link->consecutive_success++; link->consecutive_success++;
link->consecutive_failure = 0; link->consecutive_failure = 0;
@@ -798,8 +973,8 @@ int main(int argc, char **argv) {
if (!link->healthy && link->consecutive_success >= link->recovery_delay) { if (!link->healthy && link->consecutive_success >= link->recovery_delay) {
// Recovered! Failback! // Recovered! Failback!
link->healthy = true; link->healthy = true;
syslog(LOG_NOTICE, "Link %s (%s, priority %d) recovered to healthy after %d successes.", syslog(LOG_NOTICE, "Link %s (%s, gw=%s, priority %d) recovered to healthy after %d successes.",
link->name, link->device, link->priority, link->consecutive_success); link->name, link->device, link->gateway[0] ? link->gateway : "none", link->priority, link->consecutive_success);
// Restore original metric // Restore original metric
update_route_metric(link, link->metric, -1); 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) { if (link->healthy && link->consecutive_failure >= link->failover_delay) {
// Failed! Failover! // Failed! Failover!
link->healthy = false; link->healthy = false;
syslog(LOG_WARNING, "Link %s (%s, priority %d) went down after %d failures.", syslog(LOG_WARNING, "Link %s (%s, gw=%s, priority %d) went down after %d failures.",
link->name, link->device, link->priority, link->consecutive_failure); link->name, link->device, link->gateway[0] ? link->gateway : "none", link->priority, link->consecutive_failure);
// Push metric out of choice range // Push metric out of choice range
update_route_metric(link, 1000 + link->metric, -1); 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 // 4. Active routing metric self-healing to prevent external/netifd interference
if (link->device[0] != '\0') { if (link->device[0] != '\0') {
int expected_metric = (link->is_up && link->healthy) ? link->metric : (1000 + link->metric); 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) { 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...", syslog(LOG_WARNING, "Route metric mismatch detected on %s (%s, gw=%s, priority %d): expected %d, got %d. Correcting...",
link->name, link->device, link->priority, expected_metric, real_metric); link->name, link->device, link->gateway[0] ? link->gateway : "none", link->priority, expected_metric, real_metric);
update_route_metric(link, expected_metric, real_metric); update_route_metric(link, expected_metric, real_metric);
} }
} }
} }
if (any_checked) { if (any_checked) {
// Write states to shared JSON file
write_status_json(); write_status_json();
} }
// High-responsiveness scheduler ticks every second
sleep(1); sleep(1);
} }
// Terminating: clean up routes before exit // Terminating: clean up routes before exit
restore_all_metrics(); restore_all_metrics();
close_ubus_connection();
unlink(STATUS_FILE); unlink(STATUS_FILE);
syslog(LOG_INFO, "LinkBack daemon terminated successfully."); syslog(LOG_INFO, "LinkBack daemon terminated successfully.");
closelog(); closelog();
+16 -1
View File
@@ -11,12 +11,20 @@
#define MAX_DOMAIN_LEN 128 #define MAX_DOMAIN_LEN 128
#define STATUS_FILE "/var/run/linkback.json" #define STATUS_FILE "/var/run/linkback.json"
typedef enum {
MODE_MULTI_WAN = 0,
MODE_MULTI_GW = 1
} linkback_mode_t;
typedef struct { typedef struct {
char name[MAX_NAME_LEN]; char name[MAX_NAME_LEN];
bool enabled; bool enabled;
int priority; int priority;
int metric; int metric;
// Explicit gateway IP for multi_gw mode
char gateway_cfg[MAX_IP_LEN];
// Ping config // Ping config
char ping_targets[MAX_TARGETS][MAX_IP_LEN]; char ping_targets[MAX_TARGETS][MAX_IP_LEN];
int ping_target_count; int ping_target_count;
@@ -40,7 +48,7 @@ typedef struct {
int failover_delay; int failover_delay;
// Runtime state // 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 char gateway[MAX_IP_LEN]; // gateway IP address
bool is_up; // whether interface is reported up by netifd bool is_up; // whether interface is reported up by netifd
bool healthy; // daemon link health state bool healthy; // daemon link health state
@@ -62,6 +70,13 @@ typedef struct {
typedef struct { typedef struct {
bool enabled; 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; } global_config_t;
#endif // LINKBACKD_H #endif // LINKBACKD_H
+1 -1
View File
@@ -1 +1 @@
snapshot both
@@ -8,6 +8,31 @@
'require ui'; 'require ui';
'require validation'; '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 */ /* Member */
const rulesetdoc = [ const rulesetdoc = [
'data:text/html;base64,', 'data:text/html;base64,',
@@ -511,25 +536,17 @@ const CBIDynamicList = form.DynamicList.extend({ // @less_25_12
const CBIMultiValue = form.MultiValue.extend({ // @pr8758_merged const CBIMultiValue = form.MultiValue.extend({ // @pr8758_merged
__name__: 'CBI.MultiValue', __name__: 'CBI.MultiValue',
renderWidget(section_id, option_index, cfgvalue) { renderWidget(/* ... */) {
const value = (cfgvalue != null) ? cfgvalue : this.default; const sb = funcSed(
const choices = this.transformChoices(); 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, { return sb;
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();
} }
}); });
@@ -827,354 +844,88 @@ const UIDynamicList = ui.DynamicList.extend({ // @less_25_12
// keep selected order for multiple selection // keep selected order for multiple selection
const UIDropdown = ui.Dropdown.extend({ // @pr8758_merged const UIDropdown = ui.Dropdown.extend({ // @pr8758_merged
__init__(value, choices, options) { __init__(value, choices, options) {
if (typeof(choices) != 'object') const result = ui.Dropdown.prototype.__init__.apply(this, arguments);
choices = {};
if (!Array.isArray(value))
this.values = (value != null && value != '') ? [ value ] : [];
else
this.values = value;
this.orderCounter = this.values.length; this.orderCounter = this.values.length;
this.choices = choices; this.options.keep_order = options?.keep_order ?? false;
this.options = Object.assign({
sort: true, return result;
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);
}, },
render() { render(/* ... */) {
const sb = E('div', { const sb = funcSed(
'id': this.options.id, ui.Dropdown.prototype.render,
'class': 'cbi-dropdown', [
'multiple': this.options.multiple ? '' : null, [new RegExp(/('multiple':[^\n]*this\.options\.multiple)/), `'keep_order': this.options.keep_order ? '' : null, $1`]
'keep_order': this.options.keep_order ? '' : null, ],
'optional': this.options.optional ? '' : null, ['dom']
'disabled': this.options.disabled ? '' : null, ).call(this, dom, ...arguments);
'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);
return sb; return sb;
}, },
toggleItem(sb, li, force_state) { bind(/* ... */) {
const ul = li.parentNode; 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 sb;
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);
}, },
saveValues(sb, ul) { toggleItem(/* ... */) {
const sel = Array.from(ul.querySelectorAll('li[selected]')); const result = funcSed(
const div = sb.lastElementChild; ui.Dropdown.prototype.toggleItem,
const name = this.options.name; [
let strval = ''; [
const values = []; 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) { return result;
sel.sort((a, b) => },
(+a.getAttribute('data-order') || 0) -
(+b.getAttribute('data-order') || 0)
);
}
while (div.lastElementChild) saveValues(/* ... */) {
div.removeChild(div.lastElementChild); 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 => { return result;
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
}));
} }
}); });
@@ -117,7 +117,8 @@ return view.extend({
/* Netfilter flow offload support */ /* 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'), s = m.section(form.TypedSection, 'defaults', _('Routing/NAT Offloading'),
_('Not fully compatible with QoS/SQM.')); _('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.')); o.value('1', _("Software flow offloading"), _('Software based offloading for routing/NAT.'));
if (L.hasSystemFeature('offloading_hw')) if (L.hasSystemFeature('offloading_hw'))
o.value('2', _("Hardware flow offloading"), _('Hardware based offloading for routing with/without NAT.') + ' ' + _(' Requires hardware NAT support.')); 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.optional = false;
o.load = function (section_id) { o.load = function (section_id) {
const flow_offloading = uci.get('firewall', section_id, 'flow_offloading'); const flow_offloading = uci.get('firewall', section_id, 'flow_offloading');
const flow_offloading_hw = uci.get('firewall', section_id, 'flow_offloading_hw'); const flow_offloading_hw = uci.get('firewall', section_id, 'flow_offloading_hw');
const has_hw_offloading = L.hasSystemFeature('offloading_hw'); const has_hw_offloading = L.hasSystemFeature('offloading_hw');
if (hasMediatekHNAT && flow_offloading === '3')
return '3';
return (flow_offloading === '1') return (flow_offloading === '1')
? ((flow_offloading_hw === '1' && has_hw_offloading) ? '2' : '1') ? ((flow_offloading_hw === '1' && has_hw_offloading) ? '2' : '1')
: '0'; : '0';
}; };
o.write = function(section_id, value) { 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', uci.set('firewall', section_id, 'flow_offloading_hw',
(value === '2' && L.hasSystemFeature('offloading_hw')) ? '1' : null); (value === '2' && L.hasSystemFeature('offloading_hw')) ? '1' : null);
}; };
@@ -2,6 +2,8 @@
tcp_cca="$(cat /proc/sys/net/ipv4/tcp_congestion_control 2>/dev/null)" tcp_cca="$(cat /proc/sys/net/ipv4/tcp_congestion_control 2>/dev/null)"
nat_mode="disabled" nat_mode="disabled"
hnat_available=false
[ -e /sys/kernel/debug/hnat/hook_toggle ] && hnat_available=true
rules="$(iptables-save -t nat 2>/dev/null)" rules="$(iptables-save -t nat 2>/dev/null)"
@@ -19,4 +21,4 @@ else
fi fi
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"
+11 -3
View File
@@ -1,13 +1,21 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-flowproxy PKG_NAME:=luci-app-flowproxy
PKG_VERSION:=1.0.3 PKG_VERSION:=1.0.4
PKG_RELEASE:=1 PKG_RELEASE:=2
LUCI_TITLE:=LuCI Support for FlowProxy Traffic Diversion 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 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 define Package/$(PKG_NAME)/prerm
#!/bin/sh #!/bin/sh
if [ -z "$${IPKG_INSTROOT}" ]; then if [ -z "$${IPKG_INSTROOT}" ]; then
@@ -40,6 +40,18 @@ var callRestartService = rpc.declare({
method: 'restart_service' 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({ return L.view.extend({
load: function() { load: function() {
return Promise.all([ 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; 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 = 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.placeholder = '192.168.1.254';
o.rmempty = true;
o.validate = function(section_id, value) { 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) { 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 parts = cidrStr.split('/');
var cidrIp = parts[0]; var cidrIp = parts[0];
var maskBits = parseInt(parts[1] || '32', 10); var maskBits = parseInt(parts[1] || '32', 10);
@@ -209,15 +264,19 @@ return L.view.extend({
var ipNum = ipToNum(ipStr); var ipNum = ipToNum(ipStr);
var cidrNum = ipToNum(cidrIp); var cidrNum = ipToNum(cidrIp);
if (maskBits === 0) return { inSubnet: true, isBcastOrNet: false };
if (maskBits === 0) return true;
var mask = (0xffffffff << (32 - maskBits)) >>> 0; 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'); var selected_interface = s.formvalue(section_id, 'interface');
if (!value || !selected_interface) return true; if (!selected_interface) return true;
var target_iface = null; var target_iface = null;
for (var i = 0; i < ifaces.length; i++) { for (var i = 0; i < ifaces.length; i++) {
@@ -234,9 +293,12 @@ return L.view.extend({
if (!ips || ips.length === 0) return true; if (!ips || ips.length === 0) return true;
var inSubnet = false; var inSubnet = false;
var isBcastOrNet = false;
for (var j = 0; j < ips.length; j++) { 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; inSubnet = true;
if (res.isBcastOrNet) isBcastOrNet = true;
break; 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(', ')); 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; return true;
}; };
o = s.taboption('settings', form.Value, 'proxy_server_dns_port', _('Proxy server DNS port')); o = s.taboption('settings', form.Value, 'proxy_server_dns_port', _('Proxy server DNS port'));
o.datatype = 'port'; o.default = '5353'; o.rmempty = false; o.datatype = 'port'; o.default = '5353'; o.rmempty = false;
o = s.taboption('settings', form.ListValue, 'interface', _('Proxy server interface')); var iface_opt = s.taboption('settings', form.ListValue, 'interface', _('Proxy server interface'));
devices.forEach(function(d) { o.value(d.getName(), d.getName()); }); devices.forEach(function(d) { iface_opt.value(d.getName(), d.getName()); });
o.default = 'br-lan'; 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)'; o = s.taboption('settings', form.Value, 'traffic_mark', _('Traffic Mark'));
s.taboption('settings', form.Value, 'routing_table', _('Routing Table ID')).datatype = 'and(uinteger,range(1, 4294967295))'; o.datatype = 'or(uinteger, hex)';
s.taboption('settings', form.Value, 'rule_priority', _('Rule Priority')).datatype = 'and(uinteger,range(0, 4294967295))'; 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 --- // --- Rules ---
var nftsets = uci.sections('flowproxy', 'nftset').map(function(ss) { return '@' + ss['.name']; }); var nftsets = uci.sections('flowproxy', 'nftset').map(function(ss) { return '@' + ss['.name']; });
@@ -419,16 +522,14 @@ return L.view.extend({
} }
return true; return true;
}; };
// 联动:当 match_type 改变时,强制触发 match_value 的校验 // 联动:当 match_type 改变时,仅在已有输入内容时重新校验 match_value
match_type.onchange = function(ev, sid, val) { match_type.onchange = function(ev, sid, val) {
var value_input = m.lookupOption('match_value', sid)[0]; var value_input = m.lookupOption('match_value', sid)[0];
if (value_input) setTimeout(function() { if (value_input) setTimeout(function() {
var el = document.getElementById(value_input.cbid(sid)); var el = document.getElementById(value_input.cbid(sid));
if (el) { if (el) {
var inputEl = el.tagName === 'INPUT' ? el : el.querySelector('input'); var inputEl = el.tagName === 'INPUT' ? el : el.querySelector('input');
if (inputEl) { if (inputEl && inputEl.value && inputEl.value.trim() !== '') {
inputEl.dispatchEvent(new CustomEvent('change', { bubbles: true }));
inputEl.dispatchEvent(new CustomEvent('blur', { bubbles: true }));
inputEl.dispatchEvent(new CustomEvent('input', { bubbles: true })); 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 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 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); 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; return node;
}); });
}; };
@@ -474,14 +583,21 @@ return L.view.extend({
if (!path) path = '/usr/share/flowproxy/chnroute.txt'; if (!path) path = '/usr/share/flowproxy/chnroute.txt';
if (!url) { ui.addNotification(null, E('p', _('Please set download_url first')), 'error'); return; } 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...') ]) ]); 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'); 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.') ]); } 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); setTimeout(ui.hideModal, 1500);
} else { ui.hideModal(); ui.addNotification(null, E('p', _('Download failed')), 'error'); } } else {
}).catch(function(e) { ui.hideModal(); ui.addNotification(null, E('p', _('Error: %s').format(e.message)), 'error'); }); 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')); var sg = s.taboption('lists', form.SectionValue, '_list_custom', form.GridSection, 'nftset', _('Custom nftables sets'));
+52 -1
View File
@@ -118,7 +118,7 @@ msgstr ""
msgid "Match Value" msgid "Match Value"
msgstr "" msgstr ""
msgid "Network interface" msgid "Proxy server interface"
msgstr "" msgstr ""
msgid "Please set download_url first" msgid "Please set download_url first"
@@ -145,6 +145,9 @@ msgstr ""
msgid "Routing Table ID" msgid "Routing Table ID"
msgstr "" msgstr ""
msgid "Rule Priority"
msgstr ""
msgid "Rules" msgid "Rules"
msgstr "" msgstr ""
@@ -226,3 +229,51 @@ msgstr ""
msgid "this will delete ALL current %s rules and generate default templates. are you sure?" msgid "this will delete ALL current %s rules and generate default templates. are you sure?"
msgstr "" 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 ""
@@ -250,3 +250,32 @@ msgstr "RPC 错误: %s"
msgid "The proxy server IP address must be within the subnet of the selected interface (%s)." msgid "The proxy server IP address must be within the subnet of the selected interface (%s)."
msgstr "代理服务器 IP 地址必须在所选网络接口的子网网段之内 (%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 地址格式"
@@ -102,3 +102,4 @@ config nftset 'no_proxy_dst_tcp_ports'
config nftset 'no_proxy_dst_udp_ports' config nftset 'no_proxy_dst_udp_ports'
option enabled '1' option enabled '1'
option type 'inet_service' option type 'inet_service'
list elements '123'
+13
View File
@@ -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
+133 -58
View File
@@ -4,6 +4,8 @@
START=99 START=99
STOP=10 STOP=10
USE_PROCD=1 USE_PROCD=1
EXTRA_COMMANDS="add_route"
EXTRA_HELP=" add_route Add routing rules and policy"
NAME="flowproxy" NAME="flowproxy"
UCI_CONFIG="flowproxy" UCI_CONFIG="flowproxy"
@@ -57,9 +59,62 @@ check_env() {
check_config() { check_config() {
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr") 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; } [ -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 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() { apply_nft_rules() {
log_msg info "Generating nftables configuration..." log_msg info "Generating nftables configuration..."
local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level" || echo "info") local conf_level=$(uci -q get "$UCI_CONFIG.global.log_level" || echo "info")
@@ -100,11 +155,17 @@ apply_nft_rules() {
} }
add_route() { add_route() {
check_config || return 1
local proxy_server_ip_addr=$(uci -q get "$UCI_CONFIG.global.proxy_server_ip_addr") 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 interface=$(uci -q get "$UCI_CONFIG.global.interface")
local traffic_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark" || echo "0x666") interface="${interface:-br-lan}"
local routing_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888") local traffic_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark")
local rule_priority=$(uci -q get "$UCI_CONFIG.global.rule_priority" || echo "1000") 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. 先尝试用旧记录清理一次,防止由于配置变更导致的残留 # 1. 先尝试用旧记录清理一次,防止由于配置变更导致的残留
delete_route "quiet" delete_route "quiet"
@@ -127,6 +188,12 @@ add_route() {
else else
log_msg error "Failed to add ip rule: $err" log_msg error "Failed to add ip rule: $err"
fi 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() { delete_route() {
@@ -143,9 +210,14 @@ delete_route() {
[ "$quiet" != "quiet" ] && rm -f "/var/run/flowproxy.mark" [ "$quiet" != "quiet" ] && rm -f "/var/run/flowproxy.mark"
fi fi
local curr_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark" || echo "0x666") local curr_mark=$(uci -q get "$UCI_CONFIG.global.traffic_mark")
local curr_table=$(uci -q get "$UCI_CONFIG.global.routing_table" || echo "888") curr_mark="${curr_mark:-0x666}"
local curr_prio=$(uci -q get "$UCI_CONFIG.global.rule_priority" || echo "1000") 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)..." [ "$quiet" != "quiet" ] && log_msg info "Cleaning up FlowProxy routes (Atomic Mode)..."
@@ -153,6 +225,10 @@ delete_route() {
safe_del_rule() { safe_del_rule() {
local m="$1" t="$2" p="$3" local m="$1" t="$2" p="$3"
[ -z "$m" ] || [ -z "$t" ] && return [ -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}" 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 [ -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" safe_del_rule "$old_mark" "$old_table" "$old_prio"
[ "$old_mark" != "$curr_mark" ] || [ "$old_table" != "$curr_table" ] && \ [ "$old_mark" != "$curr_mark" ] || [ "$old_table" != "$curr_table" ] && \
safe_del_rule "$curr_mark" "$curr_table" "$curr_prio" 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() { 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; } [ -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 mkdir -p /tmp/dnsmasq.d
if ! uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then cat > /tmp/dnsmasq.d/flowproxy.conf << EOF
local old_noresolv=$(uci -q get dhcp.@dnsmasq[0].noresolv) # Generated by FlowProxy - DO NOT EDIT MANUALLY
local old_dns_redirect=$(uci -q get dhcp.@dnsmasq[0].dns_redirect) no-resolv
local old_servers=$(uci -q get dhcp.@dnsmasq[0].server | tr '\n' ',' | sed 's/,$//') server=$proxy_server_ip_addr#$proxy_server_dns_port
EOF
log_msg debug "Backing up DNS: noresolv=${old_noresolv:-empty}, redirect=${old_dns_redirect:-empty}, servers=${old_servers:-empty}" # 同步分发至各个可能存在的 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
uci set "$UCI_CONFIG.dns_backup=dns_backup" # 历史遗留自愈:若早期版本存在残留的 dns_backup 节,静默移除避免脏数据
[ -n "$old_noresolv" ] && uci set "$UCI_CONFIG.dns_backup.noresolv=$old_noresolv" if uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
[ -n "$old_dns_redirect" ] && uci set "$UCI_CONFIG.dns_backup.dns_redirect=$old_dns_redirect" uci -q delete "$UCI_CONFIG.dns_backup"
[ -n "$old_servers" ] && uci set "$UCI_CONFIG.dns_backup.servers=$old_servers" uci -q commit "$UCI_CONFIG"
uci commit "$UCI_CONFIG"
fi 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 /etc/init.d/dnsmasq restart
log_msg info "DNS drop-in configuration applied successfully"
} }
cleanup_dns() { 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 if uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
log_msg info "Restoring original DNS configuration..." uci -q delete "$UCI_CONFIG.dns_backup"
uci -q commit "$UCI_CONFIG"
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
fi fi
/etc/init.d/dnsmasq restart
} }
start_service() { start_service() {
@@ -265,6 +328,12 @@ start_service() {
check_env || return 1 check_env || return 1
check_config || 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 apply_nft_rules || return 1
add_route add_route
setup_dns setup_dns
@@ -272,6 +341,7 @@ start_service() {
touch "$STATE_FILE" touch "$STATE_FILE"
log_msg info "FlowProxy service started successfully" log_msg info "FlowProxy service started successfully"
} }
stop_service() { stop_service() {
log_msg info "Stopping FlowProxy..." log_msg info "Stopping FlowProxy..."
# 强制先移除标记,防止状态误报 # 强制先移除标记,防止状态误报
@@ -282,6 +352,12 @@ stop_service() {
log_msg info "FlowProxy service stopped" log_msg info "FlowProxy service stopped"
} }
reload_service() {
log_msg info "Reloading FlowProxy..."
stop_service
start_service
}
restart_service() { restart_service() {
stop_service stop_service
start_service start_service
@@ -292,8 +368,7 @@ service_triggers() {
procd_add_reload_trigger "$UCI_CONFIG" procd_add_reload_trigger "$UCI_CONFIG"
# 监听防火墙重载事件,防止规则被 fw4 清空后无法自动恢复 # 监听防火墙重载事件,防止规则被 fw4 清空后无法自动恢复
procd_add_reload_trigger "firewall" procd_add_reload_trigger "firewall"
} # 监听网络接口事件
local iface=$(uci -q get "$UCI_CONFIG.global.interface" || echo "br-lan")
reload_service() { procd_add_reload_interface_trigger "$iface"
restart
} }
@@ -44,20 +44,15 @@ gen_set_definition() {
[ -n "$uci_elems" ] && { [ -n "$elems" ] && elems="${elems}, "; elems="${elems}${uci_elems}"; } [ -n "$uci_elems" ] && { [ -n "$elems" ] && elems="${elems}, "; elems="${elems}${uci_elems}"; }
file_path=$(uci -q get "$CONFIG.$section.file_path") file_path=$(uci -q get "$CONFIG.$section.file_path")
download_url=$(uci -q get "$CONFIG.$section.download_url") if [ -n "$file_path" ]; then
if [ -n "$download_url" ] && [ -n "$file_path" ]; then if [ -f "$file_path" ]; then
if [ ! -f "$file_path" ]; then local file_elems=$(get_file_elements "$file_path")
echo "Downloading $section data from $download_url to $file_path..." >&2 if [ -n "$file_elems" ]; then
wget -q -O "$file_path" "$download_url" --timeout=10 --no-check-certificate [ -n "$elems" ] && elems="${elems}, "
[ $? -ne 0 ] && { echo "Failed to download $section data" >&2; rm -f "$file_path"; } elems="${elems}${file_elems}"
fi fi
fi else
log_debug "Set file $file_path not found, continuing without file elements"
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}"
fi fi
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") UDP_ENABLED=$(uci -q get "$CONFIG.global.udp_enabled" || echo "1")
TRAFFIC_MARK=$(uci -q get "$CONFIG.global.traffic_mark" || echo "0x666") TRAFFIC_MARK=$(uci -q get "$CONFIG.global.traffic_mark" || echo "0x666")
PROXY_SERVER_IP_ADDR=$(uci -q get "$CONFIG.global.proxy_server_ip_addr") 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 if [ "$TCP_ENABLED" = "1" ]; then
cat >> "$OUTPUT_FILE" << EOF cat >> "$OUTPUT_FILE" << EOF
chain LAN_MARKFLOW_TCP { chain LAN_MARKFLOW_TCP {
type filter hook prerouting priority mangle; policy accept; type filter hook prerouting priority mangle; policy accept;
iifname != "$INTERFACE" return
fib daddr type { unspec, local, anycast, multicast } counter return fib daddr type { unspec, local, anycast, multicast } counter return
EOF EOF
if [ -n "$PROXY_SERVER_IP_ADDR" ]; then 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 fi
for s in $SECTIONS_TCP; do process_rule "$s" "tcp" >> "$OUTPUT_FILE"; done 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" 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 cat >> "$OUTPUT_FILE" << EOF
chain LAN_MARKFLOW_UDP { chain LAN_MARKFLOW_UDP {
type filter hook prerouting priority mangle; policy accept; type filter hook prerouting priority mangle; policy accept;
iifname != "$INTERFACE" return
fib daddr type { unspec, local, anycast, multicast } counter return fib daddr type { unspec, local, anycast, multicast } counter return
EOF EOF
if [ -n "$PROXY_SERVER_IP_ADDR" ]; then 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 fi
for s in $SECTIONS_UDP; do process_rule "$s" "udp" >> "$OUTPUT_FILE"; done 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" echo " ip protocol udp counter meta mark set $TRAFFIC_MARK" >> "$OUTPUT_FILE"
@@ -13,11 +13,6 @@
}, },
"write": { "write": {
"uci": [ "flowproxy" ], "uci": [ "flowproxy" ],
"file": {
"/usr/bin/wget": [ "exec" ],
"/usr/bin/wc": [ "exec" ],
"/usr/share/flowproxy/generate_nft.sh": [ "exec" ]
},
"ubus": { "ubus": {
"luci.flowproxy": [ "*" ] "luci.flowproxy": [ "*" ]
} }
@@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { access, readfile, writefile, stat } from 'fs'; import { access, readfile, writefile, stat, popen } from 'fs';
import { cursor } from 'uci'; import { cursor } from 'uci';
import { connect } from 'ubus'; import { connect } from 'ubus';
@@ -9,6 +9,7 @@ const STATE_FILE = '/var/run/flowproxy.running';
const IP_CACHE = '/tmp/flowproxy/lan_ip.cache'; const IP_CACHE = '/tmp/flowproxy/lan_ip.cache';
const LOG_FILE = '/var/log/flowproxy.log'; const LOG_FILE = '/var/log/flowproxy.log';
const NFT_TABLE = 'flowproxy'; const NFT_TABLE = 'flowproxy';
const DNS_DROPIN_FILE = '/tmp/dnsmasq.d/flowproxy.conf';
// 辅助函数:安全 trim // 辅助函数:安全 trim
function safe_trim(str) { function safe_trim(str) {
@@ -18,10 +19,10 @@ function safe_trim(str) {
const methods = { const methods = {
get_status: { get_status: {
call: function() { call: function() {
// 1. 快速探测 // 1. 快速探测标记文件
let is_running = access(STATE_FILE, 'f'); 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 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 udp_active = (system('nft list chain inet ' + NFT_TABLE + ' LAN_MARKFLOW_UDP >/dev/null 2>&1') === 0);
let real_running = (tcp_active || udp_active); let real_running = (tcp_active || udp_active);
@@ -40,7 +41,6 @@ const methods = {
if (ubus_conn) { if (ubus_conn) {
let ifaces = ubus_conn.call('network.interface', 'dump'); let ifaces = ubus_conn.call('network.interface', 'dump');
if (type(ifaces?.interface) === 'array') { if (type(ifaces?.interface) === 'array') {
// 优先寻找名为 'lan' 的接口,或者寻找拥有 ipv4 地址的第一个非回环接口
let fallback_ip = ''; let fallback_ip = '';
for (let iface in ifaces.interface) { for (let iface in ifaces.interface) {
if (iface.interface === 'lan' && iface['ipv4-address']?.[0]?.address) { if (iface.interface === 'lan' && iface['ipv4-address']?.[0]?.address) {
@@ -64,9 +64,8 @@ const methods = {
} }
let uci_cursor = cursor(); let uci_cursor = cursor();
let proxy_server_ip_addr = uci_cursor.get(UCI_CONFIG, 'global', 'proxy_server_ip_addr') || ''; 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 { return {
running: real_running ? 1 : 0, 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: { generate_nft_config: {
call: function() { call: function() {
system('mkdir -p /tmp/flowproxy'); system('mkdir -p /tmp/flowproxy');
@@ -125,19 +185,18 @@ const methods = {
get_runtime_config: { get_runtime_config: {
call: function() { call: function() {
let res_file = '/tmp/flowproxy/runtime.tmp'; let res_file = '/tmp/flowproxy/runtime.tmp';
let dns_file = '/tmp/flowproxy/dns_runtime.tmp';
system('mkdir -p /tmp/flowproxy'); system('mkdir -p /tmp/flowproxy');
system('/usr/share/flowproxy/generate_nft.sh runtime > ' + res_file + ' 2>&1'); system('/usr/share/flowproxy/generate_nft.sh runtime > ' + res_file + ' 2>&1');
let nft_runtime = readfile(res_file) || "(failed)"; let nft_runtime = readfile(res_file) || "(failed)";
system('rm -f ' + res_file); 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_runtime = "\n--- [ DNSmasq Drop-in Runtime Configuration ] ---\n";
let dns_out = readfile(dns_file); let dns_content = readfile(DNS_DROPIN_FILE);
system('rm -f ' + dns_file); if (dns_content) {
dns_runtime += dns_content;
let dns_runtime = "\n--- [ DNSmasq Runtime Configuration ] ---\n"; } else {
dns_runtime += (dns_out || "(no active dnsmasq overrides)"); dns_runtime += "(no active dnsmasq drop-in overrides)";
}
return { runtime: safe_trim(nft_runtime + dns_runtime) }; return { runtime: safe_trim(nft_runtime + dns_runtime) };
} }
@@ -149,7 +208,6 @@ const methods = {
try { try {
let ubus_conn = connect(); let ubus_conn = connect();
if (ubus_conn) { if (ubus_conn) {
// 使用 network.device status 以获取所有物理设备 (包括 eth0 等 bridge 成员)
let dev_status = ubus_conn.call('network.device', 'status'); let dev_status = ubus_conn.call('network.device', 'status');
if (dev_status) { if (dev_status) {
for (let name in dev_status) { for (let name in dev_status) {
+2 -2
View File
@@ -11,8 +11,8 @@ LUCI_DESCRIPTION:=Live firewall log table (firewall4/nft; iptables-tagged lines
LUCI_DEPENDS:=+luci-base +logd +jsonfilter LUCI_DEPENDS:=+luci-base +logd +jsonfilter
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_VERSION:=0.1.49 PKG_VERSION:=0.1.50
PKG_RELEASE:=55 PKG_RELEASE:=56
# Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI). # Reproducible build: honor SOURCE_DATE_EPOCH from the build environment (set by docker-sdk.sh / CI).
ifdef SOURCE_DATE_EPOCH ifdef SOURCE_DATE_EPOCH
@@ -9,7 +9,7 @@
*/ */
return baseclass.extend({ return baseclass.extend({
/* Keep in sync with openwrt-feed/luci-app-fwlive/Makefile PKG_VERSION. */ /* 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], ROW_LIMIT_OPTIONS: [25, 50, 100, 250, 500, 1000, 2000],
DEFAULT_ROW_LIMIT: 100, DEFAULT_ROW_LIMIT: 100,
FETCH_MODE_OPTIONS: ['auto', 'manual'], FETCH_MODE_OPTIONS: ['auto', 'manual'],
+3 -13
View File
@@ -6,12 +6,6 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
LUCI_TITLE:=The modern ImmortalWrt proxy platform for ARM64/AMD64 (sing-box 1.14) 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_PKGARCH:=all
LUCI_DEPENDS:= \ LUCI_DEPENDS:= \
+sing-box \ +sing-box \
@@ -23,14 +17,10 @@ LUCI_DEPENDS:= \
PKG_NAME:=luci-app-homeproxy-pro PKG_NAME:=luci-app-homeproxy-pro
PKG_VERSION:=28.10.1.14 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 define Package/luci-app-homeproxy-pro/conffiles
/etc/config/homeproxy-pro /etc/config/homeproxy-pro
/etc/homeproxy-pro/resources/china_ip4.txt /etc/homeproxy-pro/resources/china_ip4.txt
+28 -4
View File
@@ -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}/` | | 后端架构 | 单文件巨型(`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/` | | 前端架构 | 单一 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` | | 协议建模 | 单 `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 形态 | 证据 | | 维度 | 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) | | 架构守卫 | 无 | `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/` 下 80 个文件:ucode 套件 + 8 个 frontend 验证器 + golden snapshot + mocks + 离线 runtime 测试;arch-guard 单跑 135 个 check | `tests/print-stats.sh` 实测 | | 测试规模 | 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` | | 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` | | 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` | | 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 | | 资源更新 | 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` | | 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/<target>/<subtarget>/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+,否则依赖解析失败。
## 已知限制 ## 已知限制
@@ -136,9 +136,9 @@ function render(ctx) {
o.default = o.enabled; o.default = o.enabled;
o.rmempty = false; o.rmempty = false;
/* §2.7 (linux.json 与 pro 的差距分析): opt-in switch to the tutorial's /* Opt-in switch to the universal sniffer list + 100ms timeout. Default
* universal sniffer list + 100ms timeout. Default off so an upgrade is * off so an upgrade is invisible; arch-guard 39 pins the default '0'
* invisible; arch-guard 40 pins the default '0' against drift. */ * against drift. */
o = s.taboption('routing', form.Flag, 'sniffer_advanced_mode', o = s.taboption('routing', form.Flag, 'sniffer_advanced_mode',
_('Sniffer: advanced mode (100ms, universal list)'), _('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.')); _('Use the tutorial-recommended sniffer profile (100ms timeout, http/tls/stun/quic/dns). Default keeps the 300ms / unconstrained-list behaviour.'));
@@ -186,7 +186,7 @@ export function attachSchema(config) {
* gfwlist / proxy_mainland_china / global with cold-start DNS on every * gfwlist / proxy_mainland_china / global with cold-start DNS on every
* reload and made the difference between modes hard to explain. * 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 * explicit `reverse_mapping: true` here (so a future sing-box change to
* the default would not silently move us off the mapping table). The * 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 * sing-box 1.14.0-r1 we test against rejects that field as
@@ -216,7 +216,7 @@ export function build_context(dm, env) {
? (dm.dns.settings || {}).default_strategy ? (dm.dns.settings || {}).default_strategy
: ((ipv6_support !== '1') ? 'ipv4_only' : null), : ((ipv6_support !== '1') ? 'ipv4_only' : null),
/* cn_ip_fallback: evaluate/match_response fallback for unknown-host /* 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; * The v28.9.1.16 flip turns this on by default for fresh installs;
* migrate_config.uc writes '0' explicitly for upgrade-existing users * migrate_config.uc writes '0' explicitly for upgrade-existing users
* so they keep the prior behaviour until they opt in. '0' is truthy * 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. */ * exactly what they had. */
cn_ip_fallback: dm.general.cn_ip_fallback || '1', cn_ip_fallback: dm.general.cn_ip_fallback || '1',
/* sniffer_advanced_mode: when '1' (opt-in), the route sniff rule /* sniffer_advanced_mode: when '1' (opt-in), the route sniff rule
* gets the universal protocol list + 100ms timeout (tutorial §2.7 / * gets the universal protocol list + 100ms timeout.
* docs/linux.json 与 pro 的差距分析.md). Default '0' preserves the * Default '0' preserves the
* 300ms / default-list behaviour so an upgrade is invisible. */ * 300ms / default-list behaviour so an upgrade is invisible. */
sniffer_advanced_mode: dm.general.sniffer_advanced_mode || '0', sniffer_advanced_mode: dm.general.sniffer_advanced_mode || '0',
main_urltest_nodes: dm.general.main_urltest_nodes || [], main_urltest_nodes: dm.general.main_urltest_nodes || [],
@@ -321,8 +321,10 @@ function append_proxy_dns(config, dm, ctx) {
* configured one of these encrypted upstreams; without the predefined * configured one of these encrypted upstreams; without the predefined
* entry, the resolver itself has to be resolved through the system DNS, * 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 * and a DNS outage of any kind taken once out leaves the resolver
* unreachable - a chicken-and-egg loop. Mirrors the hosts server in * unreachable - a chicken-and-egg loop. The well-known sing-box reference
* linux.json line 10; see docs/linux.json 与 pro 的差距分析.md §2.4. * 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; * Additions go here when the user's pool of encrypted resolvers grows;
* a new entry needs no code change beyond this table. */ * 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) ----- */ /* --- custom routing-mode path (user-defined dns_server / dns_rule) ----- */
function append_custom_dns(config, dm, ctx) { 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 * encrypted-resolver hostname the user configured to its real IPs. The
* loop also remembers which server sections hit the table, so the user * loop also remembers which server sections hit the table, so the user
* server loop below can stamp `domain_resolver: 'hp-dns-hosts'` onto * server loop below can stamp `domain_resolver: 'hp-dns-hosts'` onto
* them - sing-box only consults the hosts table for resolvers that * them - sing-box only consults the hosts table for resolvers that
* explicitly point at it, mirroring linux.json line 7 * explicitly point at it, so the table is inert without the stamp. Without
* (`{"domain_resolver": "hosts"}`). Without the stamp, the resolver * it, the resolver still has to look up its own hostname through the
* still has to look up its own hostname through the system DNS and the * system DNS and the chicken-and-egg loop we are trying to break stays
* chicken-and-egg loop we are trying to break stays open. */ * open. */
let doh_predefined = null; let doh_predefined = null;
const doh_resolved_hosts = {}; const doh_resolved_hosts = {};
for (let cfg in (dm.dns.servers || [])) { 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 /* sing-box >= 1.14: legacy address-filter rules are auto-wrapped with an
evaluate action; deprecated strategy/accept_empty fields are dropped. */ evaluate action; deprecated strategy/accept_empty fields are dropped. */
const builtin_dns_rules = []; 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 * queries. A client that learns the answer from these record types
* connects to the embedded IP without going through the resolver again, * connects to the embedded IP without going through the resolver again,
* which makes Fake-IP and any route that hinges on A/AAAA resolution * which makes Fake-IP and any route that hinges on A/AAAA resolution
@@ -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). * that every mode shares (dns-in hijack, sniff, optional auto_detect).
* The caller must have created an empty config.route; we mutate it. * The caller must have created an empty config.route; we mutate it.
* *
* Sniff tuning follows §2.7 of docs/linux.json 与 pro 的差距分析.md: * Two sniff profiles, and the default is deliberately the conservative one:
* the default rule is the conservative 300ms / unconstrained sniffer list * 300ms with no sniffer list, so sing-box applies its own default set. The
* (tutorial demo recommends 100ms + ['http','tls','stun','quic','dns'], * alternative - 100ms plus an explicit
* which costs less CPU but is a behaviour change). sniffer_advanced_mode * ['http','tls','stun','quic','dns'] - is cheaper on CPU but changes what a
* is the opt-in switch that flips to the tutorial profile. */ * 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) { function initRoute(config, ctx) {
const sniff_rule = (ctx.sniffer_advanced_mode === '1') const sniff_rule = (ctx.sniffer_advanced_mode === '1')
? { ? {
@@ -90,7 +90,7 @@ if (!migration_crontab) {
if (isEmpty(uci.get(uciconfig, ucimain, 'log_level'))) if (isEmpty(uci.get(uciconfig, ucimain, 'log_level')))
uci.set(uciconfig, ucimain, 'log_level', 'warn'); 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 * 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 * '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). * from /etc/config/homeproxy-pro (the package ships with that as the default).
@@ -541,7 +541,7 @@ hp_start_generated_config() {
# the *new* (failing) value and start_service reinstalls a layer that no # the *new* (failing) value and start_service reinstalls a layer that no
# longer matches the sing-box file it just restored. # 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 # 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 # 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 # keys are `tun_addr4` / `tun_addr6` (config/loader.uc:411), so that line was
+82 -13
View File
@@ -1009,9 +1009,25 @@ fi
# No shipped file may cite docs/. The whole directory is gitignored, so every # 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 # 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 # came to reference an agent guide and an improvement plan that no
# contributor can open. Kept here (with the docs/ pattern, not the names) # contributor can open.
# 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)' \ # 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)" "$ROOT/README.md" "$ROOT/root" "$ROOT/htdocs" 2>/dev/null || true)"
if [ -z "$DEAD_DOCS" ]; then if [ -z "$DEAD_DOCS" ]; then
pass "no shipped file cites the gitignored docs/ tree" pass "no shipped file cites the gitignored docs/ tree"
@@ -1020,6 +1036,18 @@ else
printf '%s\n' "$DEAD_DOCS" printf '%s\n' "$DEAD_DOCS"
fi 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
echo "== guard 21: dnsmasq snippet writer is incremental ==" 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 # 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 # 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 # same query_type could shadow the safety net. The fix prepends the same
# docs/linux.json 与 pro 的差距分析.md §2.2) prepends the same literal in # literal in append_custom_dns. Two checks keep that ordering:
# append_custom_dns. Two checks keep that ordering:
# #
# (a) the literal appears in BOTH append_proxy_dns and append_custom_dns; # (a) the literal appears in BOTH append_proxy_dns and append_custom_dns;
# (b) the second occurrence (the one inside append_custom_dns) comes # (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" pass "the HTTPS/SVCB reject appears in both append_proxy_dns and append_custom_dns"
else else
fail "expected 2 occurrences of 'query_type: [64, 65]' in generator/dns.uc, found $reject_count" 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 fi
# (b): the custom-path reject is the SECOND occurrence (append_custom_dns # (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' ==" echo "== guard 39: sniffer_advanced_mode default stays at '0' =="
# §2.7 (linux.json 与 pro 的差距分析.md): the advanced-mode sniff profile # The advanced-mode sniff profile
# §2.7 (linux.json 与 pro 的差距分析.md): the advanced-mode sniff profile
# (100ms / universal list) is gated behind a UCI opt-in so an upgrade # (100ms / universal list) is gated behind a UCI opt-in so an upgrade
# does not change the sniffer behaviour. The default must remain '0'; # does not change the sniffer behaviour. The default must remain '0';
# the generator falls back to '0' on a missing UCI value, and the # 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() ==" 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 # attachExperimental() used to wrap the experimental.cache_file block in
# `if (routing_mode in ['bypass_mainland_china', 'custom'])`, leaving # `if (routing_mode in ['bypass_mainland_china', 'custom'])`, leaving
# gfwlist / proxy_mainland_china / global with cold-start DNS on every # 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 ==" 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) # 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 # 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 # comment that explained them was on the same line as the push. The fix
@@ -1948,7 +1973,7 @@ fi
echo echo
echo "== guard 43: the rollback UCI snapshot matches the keys the intercept layer reads ==" 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* # 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 # 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()" pass "no view calls the non-existent form.Map.formvalue()"
fi 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-<type>-" 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 echo
printf '%s checks, %s failures\n' "$checks" "$([ "$FAILED" = 0 ] && echo 0 || echo 'nonzero')" printf '%s checks, %s failures\n' "$checks" "$([ "$FAILED" = 0 ] && echo 0 || echo 'nonzero')"
if [ "$FAILED" != 0 ]; then if [ "$FAILED" != 0 ]; then
@@ -133,7 +133,7 @@ expect "uci restore: missing snapshot returns 1" "$?" "1"
# Happy path: snapshot present, all keys applied, commit called. # Happy path: snapshot present, all keys applied, commit called.
# The snapshot format carries the UCI type, because `uci set` on a list option # 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 # 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`. # with `uci delete` + `uci add_list`.
: > "$HP_TEST_UCI_OUT" : > "$HP_TEST_UCI_OUT"
write_uci_stub write_uci_stub
@@ -578,7 +578,7 @@ fi
run_case custom "$ROOT/tests/fixtures/generators/custom.uci" generate_client.uc sing-box-c.json 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. # 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 # Without a dns_server in custom.uci the rule block is empty, so the
# generated config still has the reject at index 0. # generated config still has the reject at index 0.
@@ -613,7 +613,7 @@ EOF
fi fi
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 # `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 # 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 # 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 # purpose: the only assertion that matters is that the generated config
# passes sing-box check, which the test runner does for every case. # 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 # 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 # 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 # resolver itself has to be resolved through the system DNS, and a DNS
@@ -676,7 +676,7 @@ EOF
fi fi
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 # for fresh installs (the package ships option cn_ip_fallback '1' and
# context.uc falls back to '1' on a missing value). Existing users # context.uc falls back to '1' on a missing value). Existing users
# upgrading get '0' written by migrate_config.uc and keep the prior # upgrading get '0' written by migrate_config.uc and keep the prior
@@ -719,7 +719,8 @@ EOF
fi fi
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 # 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 # config above); here we stage a variant with '0' to assert the
# upgrade-safe path of migrate_config.uc - existing users see no change. # upgrade-safe path of migrate_config.uc - existing users see no change.
@@ -740,7 +741,7 @@ else
fi fi
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 # '0'. Client mode must emit the conservative sniff rule (300ms / no
# sniffer list); advanced mode emits the 100ms / universal-list profile. # sniffer list); advanced mode emits the 100ms / universal-list profile.
if [ ! -f "$client_json" ]; then if [ ! -f "$client_json" ]; then
@@ -776,7 +777,7 @@ EOF
fi fi
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. # 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 \ 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'#" "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 ------------------------------------------------- # --- review@2026-09-29 batch -------------------------------------------------
# #
# Six checks that pin the five generator fixes from # Six checks that pin the five generator fixes from the 2026-09-29 review.
# docs/review-report-20260929.md §7. Earlier rounds attempted ucode # Earlier rounds attempted ucode
# probes (arrow functions, optional chaining on arrays, ...) and each # probes (arrow functions, optional chaining on arrays, ...) and each
# round surfaced a different ucode dialect issue on the testbed - the # round surfaced a different ucode dialect issue on the testbed - the
# macOS dev box has no ucode so the loop could not iterate locally. # 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 # it runs identically on macOS and the testbed. ucode-stdout probes
# are out; structural assertions against the emitted bytes are in. # 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 # 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 # declares it as a tag). Indent cannot disambiguate dns.rules[] from
# route.rules[] (both pretty-print at 4 tabs on the testbed; both are # route.rules[] (both pretty-print at 4 tabs on the testbed; both are
@@ -962,7 +963,7 @@ else
fi fi
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), # on ipv6_support. With ipv6_support='0' (client.uci's value),
# china-dns.strategy must be 'ipv4_only', not the pre-r28 # china-dns.strategy must be 'ipv4_only', not the pre-r28
# unconditional 'prefer_ipv6'. The generated JSON emits the dns # unconditional 'prefer_ipv6'. The generated JSON emits the dns
@@ -1083,7 +1084,7 @@ else
fi fi
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 # emitted as a single domain_suffix entry backed by the
# NAPTR_BYPASS_SUFFIXES constant. ucode's pretty-printer emits the # NAPTR_BYPASS_SUFFIXES constant. ucode's pretty-printer emits the
# query_type array and the domain_suffix array as multi-line: # query_type array and the domain_suffix array as multi-line:
@@ -1121,7 +1122,7 @@ else
fi fi
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 # on routing_mode in [bypass_mainland_china, custom]. After the r28
# fix, every routing_mode gets a cache_file block. Three sed # fix, every routing_mode gets a cache_file block. Three sed
# variations of the same fixture cover gfwlist / proxy_mainland_china # 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'" echo "PASS: cache-file-custom: experimental.cache_file emitted for routing_mode='custom'"
fi 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 # whitelist (e.g. /etc/passwd) used to silently set the field to
# null. The fix is to die() with a message naming the offender. # null. The fix is to die() with a message naming the offender.
# run_case_type_error observes the refusal. The variation targets # 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 \ "$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'%" "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 # a dedicated 'testbed-dialect' sprint. The multi-line sed to
# attach an extra_tags list onto the existing rs_local ruleset # attach an extra_tags list onto the existing rs_local ruleset
# drifted at the first whitespace change on the testbed, and # drifted at the first whitespace change on the testbed, and
@@ -59,7 +59,7 @@ expect('server.insecure forced null', serverInsecure.insecure, null);
/* 4. cert_path whitelist - the validator accepts /etc/homeproxy-pro/... and /* 4. cert_path whitelist - the validator accepts /etc/homeproxy-pro/... and
* /tmp/homeproxy_..., and rejects everything else (including * /tmp/homeproxy_..., and rejects everything else (including
* /etc/passwd and relative paths). This is the security patch * /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. */ * 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 /etc/homeproxy-pro', validateHomeProxyPath('/etc/homeproxy-pro/certs/server_publickey.pem'), true);
expect('cert.path /tmp/homeproxy_', validateHomeProxyPath('/tmp/homeproxy_test/foo.pem'), true); expect('cert.path /tmp/homeproxy_', validateHomeProxyPath('/tmp/homeproxy_test/foo.pem'), true);
+2 -2
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-linkback PKG_NAME:=luci-app-linkback
PKG_VERSION:=1.0.0 PKG_VERSION:=1.0.0
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_MAINTAINER:=Dante PKG_MAINTAINER:=Dante
LUCI_TITLE:=LuCI support for Multi-WAN LinkBack Failover/Failback 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 LUCI_PKGARCH:=all
define Package/$(PKG_NAME)/postinst define Package/$(PKG_NAME)/postinst
#!/sh #!/bin/sh
[ -n "$${IPKG_INSTROOT}" ] || { [ -n "$${IPKG_INSTROOT}" ] || {
/etc/init.d/rpcd restart /etc/init.d/rpcd restart
} }
@@ -49,10 +49,10 @@ return view.extend({
var container = E('div', { 'class': 'cbi-map' }, [ var container = E('div', { 'class': 'cbi-map' }, [
E('h2', {}, _('LinkBack 链路守护') + ' - ' + _('Overview')), 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 stateText = _('Collecting...');
var stateColor = '#999'; var stateColor = '#999';
@@ -84,15 +84,19 @@ return view.extend({
click: ui.createHandlerFn(this, 'handleServiceAction', 'restart') click: ui.createHandlerFn(this, 'handleServiceAction', 'restart')
}, _('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); container.appendChild(statusSection);
// --- Link Status Section (Aligned with lucky style) --- // --- Link Status Section ---
var linkSection = E('fieldset', { 'class': 'cbi-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', { 'class': 'cbi-section-node' }, [
E('div', { id: 'linkback-status-box' }, [ E('div', { id: 'linkback-status-box' }, [
E('p', { 'class': 'spinning' }, _('Loading real-time link status...')) 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'); var restartBtn = document.getElementById('linkback-restart-btn');
if (restartBtn) { if (restartBtn) {
if (status && status.links && status.links.length > 0) { if (status && status.links && status.links.length > 0) {
@@ -143,7 +157,7 @@ return view.extend({
if (!status || !status.links || status.links.length === 0) { if (!status || !status.links || status.links.length === 0) {
dom.content(box, [ dom.content(box, [
E('p', { style: 'color:#f44336; font-weight:bold; padding:20px;' }, 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; return;
} }
@@ -154,7 +168,7 @@ return view.extend({
var table = E('table', { 'class': 'table cbi-section-table' }, [ var table = E('table', { 'class': 'table cbi-section-table' }, [
E('tr', { 'class': 'tr table-titles' }, [ E('tr', { 'class': 'tr table-titles' }, [
E('th', { 'class': 'th', 'style': 'text-align:center; width:5%;' }, '#'), 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;' }, _('Status')),
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Routing Attributes')), E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Routing Attributes')),
E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Latency Details')) E('th', { 'class': 'th', 'style': 'text-align:left;' }, _('Latency Details'))
@@ -8,42 +8,70 @@ return view.extend({
load: function() { load: function() {
return Promise.all([ return Promise.all([
uci.load('linkback'), uci.load('linkback'),
uci.load('network') uci.load('network'),
uci.load('firewall').catch(function() { return {}; })
]); ]);
}, },
// Comprehensive validation: check all conditions required for the service // Comprehensive validation: check all conditions required for the service
// to be safely enabled. Returns an error message string, or null if OK. // to be safely enabled. Returns an error message string, or null if OK.
_validateServiceConfig: function() { _validateServiceConfig: function() {
var global_sec = uci.sections('linkback', 'global')[0] || {};
var mode = global_sec.mode || 'multi_wan';
var link_sections = uci.sections('linkback', 'link') || []; var link_sections = uci.sections('linkback', 'link') || [];
// Rule 1: At least 2 interfaces // Rule 1: At least 2 targets
if (link_sections.length < 2) { 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++) { for (var i = 0; i < link_sections.length; i++) {
var s_id = link_sections[i]['.name']; 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') || var has_check = uci.get('linkback', s_id, 'ping_targets') ||
uci.get('linkback', s_id, 'dns_server') || uci.get('linkback', s_id, 'dns_server') ||
uci.get('linkback', s_id, 'tcp_target'); uci.get('linkback', s_id, 'tcp_target');
if (!has_check) { 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 // Priority uniqueness
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';
if (priorities[prio]) { 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; return null;
@@ -53,11 +81,48 @@ return view.extend({
var m, s, o; var m, s, o;
var self = this; 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 // 智能判断当前是否为中文环境,并提供实时校验的翻译 fallback
var is_zh = (_('Base Metric') === '默认跃点' || _('Base Metric') === '默认跃点 (Metric)'); var is_zh = (_('Base Metric') === '默认跃点' || _('Base Metric') === '默认跃点 (Metric)');
var t_priority_empty = is_zh ? '优先级不能为空。' : _('Priority must not be empty.'); 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 // Helper function to expand table column controls and eliminate right empty space
var makeTableColumnExpand = function(opt, width) { var makeTableColumnExpand = function(opt, width) {
@@ -81,17 +146,16 @@ return view.extend({
m = new form.Map('linkback', m = new form.Map('linkback',
_('LinkBack 链路守护') + ' - ' + _('Settings'), _('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 --- // --- Global Settings Section ---
s = m.section(form.TypedSection, 'global', _('Global Settings')); s = m.section(form.TypedSection, 'global', _('Global Settings'));
s.anonymous = true; s.anonymous = true;
// Enable switch (总开关) - must be the first option // 1. Enable switch
o = s.option(form.Flag, 'enabled', _('Enable Service'), o = s.option(form.Flag, 'enabled', _('Enable Service'),
_('Master switch to enable or disable the LinkBack failover daemon.')); _('Master switch to enable or disable the LinkBack failover daemon.'));
o.rmempty = false; o.rmempty = false;
// Intercept at UCI write level to validate before enabling.
o.write = function(section_id, value) { o.write = function(section_id, value) {
if (value === '1') { if (value === '1') {
var err = self._validateServiceConfig(); var err = self._validateServiceConfig();
@@ -104,27 +168,94 @@ return view.extend({
uci.set('linkback', section_id, 'enabled', value); uci.set('linkback', section_id, 'enabled', value);
}; };
// --- Monitored Links Section --- // 2. Working Mode
s = m.section(form.GridSection, 'link', _('Monitored WAN Interfaces'), o = s.option(form.ListValue, 'mode', _('Working Mode'),
_('Add and prioritize your WAN interfaces. Lower priority number means higher preference (e.g., 1 = primary, 2 = backup).')); _('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.anonymous = true;
s.addremove = true; s.addremove = true;
// Custom dynamic Modal title for gorgeous UX // Custom dynamic Modal title
s.modaltitle = function(section_id) { s.modaltitle = function(section_id) {
var parent_title = _('LinkBack 链路守护') + ' - ' + _('Settings'); var parent_title = _('LinkBack 链路守护') + ' - ' + _('Settings');
var is_new = (this.map.addedSection === section_id) || !uci.get('linkback', section_id, 'name'); var mode = getActiveMode();
if (is_new) { var is_new = (this.map.addedSection === section_id) ||
return parent_title + ' - ' + _('Add Monitored Interface'); (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 { } else {
var name = uci.get('linkback', section_id, 'name') || section_id; if (is_new) {
return parent_title + ' - ' + _('Edit Monitored Interface') + ' (' + name + ')'; 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; var origRemove = s.handleRemove;
s.handleRemove = function(section_id, ev) { s.handleRemove = function(section_id, ev) {
return origRemove.apply(this, arguments).then(function() { 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 = s.option(form.Flag, 'enabled', _('Enabled'));
o.default = '1'; o.default = '1';
o.rmempty = false; o.rmempty = false;
makeTableColumnExpand(o, '8%'); makeTableColumnExpand(o, '8%');
// 2. Interface name - dynamic dropdown (No description for main column) // 2. Monitored Target Display (Table only)
o = s.option(form.ListValue, 'name', _('Interface')); o = s.option(form.DummyValue, '_target_disp', _('Monitored Target'));
o.rmempty = false; o.modalonly = false;
o.cfgvalue = function(section_id) {
// Once-off load of all network interfaces safely avoiding race conditions var gw = uci.get('linkback', section_id, 'gateway');
var network_interfaces = {}; var name = uci.get('linkback', section_id, 'name');
uci.sections('network', 'interface').forEach(function(sec) { var mode = getActiveMode();
var n = sec['.name']; if (mode === 'multi_gw' || gw) {
if (n !== 'loopback' && n !== 'lan') { if (gw) {
network_interfaces[n] = true; if (name && name !== gw) {
o.value(n); return gw + ' (' + name + ')';
} }
}); return gw;
// 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;
} }
}); return name ? (_('Missing Gateway IP: %s').format(name)) : _('Unset');
} else {
return name || _('Unselected');
}
};
makeTableColumnExpand(o, '24%');
// 动态生成仅针对当前渲染 section_id 的 choices 和 keylist // 3a. Gateway IP (Multi-GW mode only, Modal only)
var filtered_choices = {}; var o_gw = s.option(form.Value, 'gateway', _('Gateway IP'),
var filtered_keylist = []; _('Next-hop IPv4 address of this gateway (e.g. 192.168.1.254).'));
if (Array.isArray(this.keylist)) { o_gw.datatype = 'ip4addr';
for (var i = 0; i < this.keylist.length; i++) { o_gw.modalonly = true;
var key = this.keylist[i]; o_gw.placeholder = '192.168.1.254';
if (!used_names[key]) { var origRenderGw = o_gw.render;
filtered_keylist.push(key); o_gw.render = function(option_index, section_id, in_table) {
filtered_choices[key] = this.vallist[i]; 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)
}); });
if (conflict) {
return widget.render(); 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; var added = false;
uci.sections('linkback', 'link').forEach(function(sec) { uci.sections('linkback', 'link').forEach(function(sec) {
if (sec['.name'] !== section_id && sec.name === value) { if (sec['.name'] !== section_id && sec.name === value) {
@@ -224,11 +424,16 @@ return view.extend({
} }
return true; 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
o = s.option(form.Value, 'priority', _('Priority'),
// 3. Priority (No description for main column) _('Lower number indicates higher priority (e.g. 1 = Primary, 2 = Backup).'));
o = s.option(form.Value, 'priority', _('Priority'));
o.datatype = 'uinteger'; o.datatype = 'uinteger';
o.default = '1'; o.default = '1';
o.rmempty = false; o.rmempty = false;
@@ -238,7 +443,6 @@ return view.extend({
} }
var self_opt = this; var self_opt = this;
var has_conflict = false; var has_conflict = false;
var conflicting_iface = '';
uci.sections('linkback', 'link').forEach(function(sec) { uci.sections('linkback', 'link').forEach(function(sec) {
var sid = sec['.name']; var sid = sec['.name'];
if (sid !== section_id) { if (sid !== section_id) {
@@ -248,16 +452,15 @@ return view.extend({
} }
if (other_val != null && other_val !== '' && String(other_val) === String(value)) { if (other_val != null && other_val !== '' && String(other_val) === String(value)) {
has_conflict = true; has_conflict = true;
conflicting_iface = uci.get('linkback', sid, 'name') || sid;
} }
} }
}); });
if (has_conflict) { if (has_conflict) {
return t_priority_conflict.format(value, conflicting_iface); return t_priority_conflict.format(value);
} }
return true; return true;
}; };
makeTableColumnExpand(o, '15%'); makeTableColumnExpand(o, '12%');
// 4. Metric (Read-only, generated from priority * 10) // 4. Metric (Read-only, generated from priority * 10)
var metric_title = _('Base Metric'); var metric_title = _('Base Metric');
@@ -273,7 +476,7 @@ return view.extend({
} }
return prio_val * 10; return prio_val * 10;
}; };
makeTableColumnExpand(o, '15%'); makeTableColumnExpand(o, '12%');
// 5. Dummy display option for Check Type in main Grid table (read-only) // 5. Dummy display option for Check Type in main Grid table (read-only)
o = s.option(form.DummyValue, 'check_type_disp', _('Check Type')); o = s.option(form.DummyValue, 'check_type_disp', _('Check Type'));
@@ -287,10 +490,11 @@ return view.extend({
return _('TCP Probe'); return _('TCP Probe');
return _('-- Not Configured --'); return _('-- Not Configured --');
}; };
makeTableColumnExpand(o, '20%'); makeTableColumnExpand(o, '18%');
// 6. Check Type Dropdown (Virtual field, Modal only to avoid Save & Apply side-effects) // 6. Check Type Dropdown (Virtual field, Modal only)
o = s.option(form.ListValue, 'check_type', _('Check Type')); o = s.option(form.ListValue, 'check_type', _('Check Type'),
_('Health probe method for this specific target.'));
o.value('ping', _('Ping Probe')); o.value('ping', _('Ping Probe'));
o.value('dns', _('DNS Probe')); o.value('dns', _('DNS Probe'));
o.value('tcp', _('TCP Probe')); o.value('tcp', _('TCP Probe'));
@@ -298,7 +502,6 @@ return view.extend({
o.rmempty = false; o.rmempty = false;
o.modalonly = true; o.modalonly = true;
// Derive the check type from which probe target is configured in UCI.
o.cfgvalue = function(section_id) { o.cfgvalue = function(section_id) {
if (uci.get('linkback', section_id, 'ping_targets')) if (uci.get('linkback', section_id, 'ping_targets'))
return 'ping'; return 'ping';
@@ -306,22 +509,19 @@ return view.extend({
return 'dns'; return 'dns';
if (uci.get('linkback', section_id, 'tcp_target')) if (uci.get('linkback', section_id, 'tcp_target'))
return 'tcp'; 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) { o.write = function(section_id, value) {
var current = uci.get('linkback', section_id, 'ping_targets') ? 'ping' : var current = uci.get('linkback', section_id, 'ping_targets') ? 'ping' :
(uci.get('linkback', section_id, 'dns_server') ? 'dns' : (uci.get('linkback', section_id, 'dns_server') ? 'dns' :
(uci.get('linkback', section_id, 'tcp_target') ? 'tcp' : '')); (uci.get('linkback', section_id, 'tcp_target') ? 'tcp' : ''));
var next = (value == null) ? 'ping' : String(value); var next = (value == null) ? 'ping' : String(value);
// No change - do nothing
if (next === current) { if (next === current) {
return; return;
} }
// Clear old type's fields and any legacy weights/thresholds
uci.remove('linkback', section_id, 'weight_threshold'); uci.remove('linkback', section_id, 'weight_threshold');
uci.remove('linkback', section_id, 'ping_weight'); uci.remove('linkback', section_id, 'ping_weight');
uci.remove('linkback', section_id, 'dns_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'), 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).')); _('Custom IP list to ping through this link/gateway (comma-separated, e.g. 223.5.5.5,119.29.29.29).'));
o.rmempty = true; o.default = '223.5.5.5,119.29.29.29';
o.rmempty = false;
o.modalonly = true; o.modalonly = true;
o.depends('check_type', 'ping'); o.depends('check_type', 'ping');
o.validate = function(section_id, value) { o.validate = function(section_id, value) {
if (!value) return true; if (!value) return _('Ping Targets is required.');
var ips = value.replace(/\s+/g, '').split(','); var ips = value.replace(/\s+/g, '').split(',');
for (var i = 0; i < ips.length; i++) { 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]?)$/; 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'), 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.datatype = 'ip4addr';
o.rmempty = true; o.rmempty = false;
o.modalonly = true; o.modalonly = true;
o.depends('check_type', 'dns'); o.depends('check_type', 'dns');
o = s.option(form.Value, 'dns_domain', _('DNS Domain'), o = s.option(form.Value, 'dns_domain', _('DNS Domain'),
_('Domain name to resolve for DNS probe (e.g., www.baidu.com).')); _('Domain name to resolve for DNS probe (e.g. www.baidu.com).'));
o.rmempty = true; o.default = 'www.baidu.com';
o.rmempty = false;
o.modalonly = true; o.modalonly = true;
o.depends('check_type', 'dns'); o.depends('check_type', 'dns');
// 8. TCP Probe Parameters // 9. Custom TCP Probe Parameters
o = s.option(form.Value, 'tcp_target', _('TCP Target'), o = s.option(form.Value, 'tcp_target', _('TCP Target'),
_('Target IP for TCP handshake probe.')); _('Target IP for TCP handshake probe.'));
o.datatype = 'ip4addr'; o.datatype = 'ip4addr';
o.rmempty = true; o.rmempty = false;
o.modalonly = true; o.modalonly = true;
o.depends('check_type', 'tcp'); o.depends('check_type', 'tcp');
o = s.option(form.Value, 'tcp_port', _('TCP Port'), 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.datatype = 'port';
o.rmempty = true; o.default = '80';
o.rmempty = false;
o.modalonly = true; o.modalonly = true;
o.depends('check_type', 'tcp'); 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(); return m.render();
} }
}); });
+158 -126
View File
@@ -1,5 +1,5 @@
msgid "" msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8" msgstr "Content-Type: text/plain; charset=UTF-8\n"
msgid "LinkBack 链路守护" msgid "LinkBack 链路守护"
msgstr "网关切换" msgstr "网关切换"
@@ -16,6 +16,12 @@ msgstr "日志"
msgid "Real-time health state, priority mapping, and service control for Multi-WAN default routing metrics." msgid "Real-time health state, priority mapping, and service control for Multi-WAN default routing metrics."
msgstr "用于 Multi-WAN 默认路由 Metric 的实时健康状态、优先级映射及服务控制。" 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" msgid "Service Status"
msgstr "服务状态" msgstr "服务状态"
@@ -37,6 +43,9 @@ msgstr "停止"
msgid "Link Status" msgid "Link Status"
msgstr "链路状态" msgstr "链路状态"
msgid "Link / Gateway Status"
msgstr "链路 / 网关状态"
msgid "Loading real-time link status..." msgid "Loading real-time link status..."
msgstr "正在加载实时链路状态..." msgstr "正在加载实时链路状态..."
@@ -49,18 +58,93 @@ msgstr "已停止"
msgid "LinkBack daemon is not running or no links are configured. Please check Settings page." msgid "LinkBack daemon is not running or no links are configured. Please check Settings page."
msgstr "网关切换守护进程未运行或未配置任何链路。请检查设置页面。" msgstr "网关切换守护进程未运行或未配置任何链路。请检查设置页面。"
msgid "主选链路 / Active" msgid "LinkBack daemon is not running or no targets are configured. Please check Settings page."
msgstr "主选网关 / 活跃中" msgstr "网关切换守护进程未运行或未配置任何监控目标。请检查设置页面。"
msgid "次选链路 / Standby" msgid "Working Mode"
msgstr "备选网关 / 待命" msgstr "工作模式"
msgid "备选链路 / Faulted" msgid "多wan口模式"
msgstr "故障网关 / 已断开" 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" msgid "Priority"
msgstr "优先级" msgstr "优先级"
msgid "Lower number indicates higher priority (e.g. 1 = Primary, 2 = Backup)."
msgstr "数值越小代表优先级越高 (例如:1 = 主选,2 = 备选)。"
msgid "Metric" msgid "Metric"
msgstr "路由跃点 (Metric)" msgstr "路由跃点 (Metric)"
@@ -85,9 +169,6 @@ msgstr "服务操作执行失败。"
msgid "RPC error: %s" msgid "RPC error: %s"
msgstr "RPC 错误: %s" msgstr "RPC 错误: %s"
msgid "Configure Multi-WAN failover service, health check parameters, and monitored link interfaces."
msgstr "配置多 WAN 故障切换服务、健康度检查参数及被监控的网关接口。"
msgid "Global Settings" msgid "Global Settings"
msgstr "全局设置" msgstr "全局设置"
@@ -100,39 +181,30 @@ msgstr "启用或禁用网关切换服务的全局总开关。"
msgid "Check Interval (s)" msgid "Check Interval (s)"
msgstr "检测间隔 (秒)" msgstr "检测间隔 (秒)"
msgid "Time in seconds between each health check cycle." msgid "Shared interval in seconds between health check cycles for all targets."
msgstr "每个健康度检测周期之间的时间间隔。" msgstr "所有链路与网关共用的健康检测周期时间 (秒)。"
msgid "Check Timeout (s)" msgid "Check Timeout (s)"
msgstr "检测超时 (秒)" msgstr "检测超时 (秒)"
msgid "Maximum wait time in seconds for each individual check probe." msgid "Shared maximum wait time in seconds for each probe (1s recommended to prevent blocking)."
msgstr "每个检测探针的最大等待超时时间。" msgstr "所有探针共用的最大等待超时时间 (推荐 1 秒以杜绝网络阻塞)。"
msgid "Recovery Delay" msgid "Recovery Delay"
msgstr "恢复判定延迟" 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 "故障网关恢复健康所需连续检测成功的次数 (防抖动回切)。" msgstr "故障网关恢复健康所需连续检测成功的次数 (防抖动回切)。"
msgid "Failover Delay" msgid "Failover Delay"
msgstr "故障判定延迟" 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 "健康网关判定为故障所需连续检测失败的次数 (防抖动切换)。" 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" msgid "Interface"
msgstr "网络接口" msgstr "网络接口"
msgid "Select a logical WAN interface from Network settings."
msgstr "从系统网络配置中选择一个逻辑 WAN 接口。"
msgid "Enabled" msgid "Enabled"
msgstr "已启用" 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)。" msgstr "当该接口健康时应用的默认路由 Metric 跃点数值 (例如:wan=10, wan2=20)。"
msgid "Ping Targets" 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)." msgid "Custom IP list to ping through this link/gateway (comma-separated, e.g. 223.5.5.5,119.29.29.29)."
msgstr "通过空格分隔的 Ping 目标 IP 地址列表 (例如:223.5.5.5 8.8.8.8)。" msgstr "通过该链路/网关探测的目标 IP 列表 (逗号分隔,例如:223.5.5.5,119.29.29.29)。"
msgid "Ping Weight" msgid "Ping Targets is required."
msgstr "Ping 权重" msgstr "Ping 探测目标 IP 不能为空。"
msgid "Invalid IP address: \"%s\""
msgstr "无效的 IP 地址: \"%s\""
msgid "DNS Server" msgid "DNS Server"
msgstr "DNS 服务器" 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)。" msgstr "用于 UDP 解析探测的 DNS 服务器 IP (例如:119.29.29.29)。"
msgid "DNS Domain" msgid "DNS Domain"
msgstr "DNS 解析域名" 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)。" msgstr "用于 DNS 探针解析的测试目标域名 (例如:www.baidu.com)。"
msgid "DNS Weight"
msgstr "DNS 权重"
msgid "TCP Target" msgid "TCP Target"
msgstr "TCP 检测目标" msgstr "TCP 检测目标"
@@ -175,17 +247,8 @@ msgstr "用于 TCP 三次握手探测的目标主机 IP 地址。"
msgid "TCP Port" msgid "TCP Port"
msgstr "TCP 检测端口" msgstr "TCP 检测端口"
msgid "Target port for TCP handshake probe." msgid "Target port for TCP handshake probe (e.g. 80 or 443)."
msgstr "用于 TCP 探测的测试目标端口号。" msgstr "用于 TCP 探测的测试目标端口号 (例如:80 或 443)。"
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 "Loading logs..." msgid "Loading logs..."
msgstr "正在加载运行日志..." msgstr "正在加载运行日志..."
@@ -199,71 +262,32 @@ msgstr "系统日志"
msgid "No logs available." msgid "No logs available."
msgstr "暂无可用运行日志。" 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" msgid "Check Type"
msgstr "检测类型" msgstr "检测类型"
msgid "Time in seconds between each health check cycle for this link." msgid "Health probe method for this specific target."
msgstr "该链路每次健康度检测周期之间的时间间隔 (秒)。" msgstr "该目标采用的健康探测方式。"
msgid "Maximum wait time in seconds for each individual check probe for this link." msgid "Cannot enable service: At least 2 monitored links/gateways must be configured for failover switcher."
msgstr "该链路每个检测探针的最大等待超时时间 (秒)。" msgstr "无法启用服务: 容灾切换至少需要配置 2 个监控链路或网关。"
msgid "Number of consecutive successful checks required before marking this link as healthy (failback anti-flap)." msgid "Cannot enable service: WAN interface must be configured in Multi-Gateway mode."
msgstr "判定该链路恢复健康所需连续检测成功的次数 (防抖动回切)。" msgstr "无法启用服务: 单wan多网关模式下必须指定wan网络接口。"
msgid "Number of consecutive failed checks required before marking this link as faulted (failover anti-flap)." msgid "Cannot enable service: Target \"%s\" has no health check configured."
msgstr "判定该链路发生故障所需连续检测失败的次数 (防抖动切换)。" msgstr "无法启用服务: 监控目标「%s」未配置健康检测策略。"
msgid "Add Monitored Interface" msgid "Cannot enable service: Targets \"%s\" and \"%s\" have the same priority %s."
msgstr "添加监控接口" msgstr "无法启用服务: 目标「%s」和「%s」的路由优先级均为 %s,不允许重复。"
msgid "Edit Monitored Interface" msgid "Cannot enable service: Target \"%s\" has an invalid or missing Gateway IP."
msgstr "编辑监控接口" msgstr "无法启用服务: 目标「%s」缺少有效的网关 IP。"
msgid "Cannot enable service: No monitored WAN interfaces are configured." msgid "Cannot enable service: Targets \"%s\" and \"%s\" have the same Gateway IP %s."
msgstr "无法启用服务: 未配置任何监控的 WAN 接口。" msgstr "无法启用服务: 目标「%s」和「%s」具有相同的网关 IP %s,不允许重复。"
msgid "Cannot enable service: At least one interface must have a configured check type (Ping, DNS, or TCP)." msgid "Cannot enable service: Interface \"%s\" is configured multiple times."
msgstr "无法启用服务: 至少需要为一个接口配置有效的检测类型 (Ping、DNS 或 TCP)。" msgstr "无法启用服务: 接口「%s」被重复配置。"
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 "Service has been auto-disabled because the configuration is no longer valid: " msgid "Service has been auto-disabled because the configuration is no longer valid: "
msgstr "由于配置不再合法,服务已被自动禁用: " msgstr "由于配置不再合法,服务已被自动禁用: "
@@ -286,37 +310,45 @@ msgstr "该接口已被配置,请选择其他接口。"
msgid "Priority must not be empty." msgid "Priority must not be empty."
msgstr "优先级不能为空。" msgstr "优先级不能为空。"
msgid "Priority %s conflicts with interface \"%s\". Priorities must be unique." msgid "Priority %s conflicts with another target. Priorities must be unique."
msgstr "优先级 %s 与接口 \"%s\" 冲突。优先级必须是唯一的。" msgstr "优先级 %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 "Active" msgid "Active"
msgstr "活跃" msgstr "主选活跃"
msgid "Standby" msgid "Standby"
msgstr "备选" msgstr "备选待命"
msgid "Faulted" msgid "Faulted"
msgstr "故障" msgstr "故障离线"
msgid "Comma-separated list of IPs to ping (e.g., 223.5.5.5,8.8.8.8)." msgid "-- Not Configured --"
msgstr "通过逗号分隔的 Ping 目标 IP 地址列表 (例如:223.5.5.5,8.8.8.8)。" 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 接口。"
@@ -2,7 +2,7 @@
"luci-app-linkback": { "luci-app-linkback": {
"description": "ACL for LinkBack Multi-WAN Failover application", "description": "ACL for LinkBack Multi-WAN Failover application",
"read": { "read": {
"uci": [ "linkback", "network" ], "uci": [ "linkback", "network", "firewall" ],
"ubus": { "ubus": {
"luci.linkback": [ "get_status", "get_log" ], "luci.linkback": [ "get_status", "get_log" ],
"network.interface": [ "dump" ], "network.interface": [ "dump" ],
@@ -22,6 +22,8 @@ const methods = {
if (!status) { if (!status) {
status = { status = {
enabled: false, enabled: false,
mode: "multi_wan",
interface: "wan",
check_interval: 5, check_interval: 5,
active_link: "none", active_link: "none",
links: [] links: []
+2 -2
View File
@@ -1,8 +1,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-open-box PKG_NAME:=luci-app-open-box
PKG_VERSION:=0.1.277 PKG_VERSION:=0.1.278
PKG_RELEASE:=3 PKG_RELEASE:=4
PKG_MAINTAINER:=liandu2024 PKG_MAINTAINER:=liandu2024
PKG_LICENSE:=GPL-3.0-or-later PKG_LICENSE:=GPL-3.0-or-later
+1 -1
View File
@@ -8,7 +8,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall PKG_NAME:=luci-app-passwall
PKG_VERSION:=26.10.1 PKG_VERSION:=26.10.1
PKG_RELEASE:=306 PKG_RELEASE:=307
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
@@ -930,6 +930,8 @@ clean_crontab() {
start_crontab() { start_crontab() {
local update_loop 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 if [ "$ENABLED_DEFAULT_ACL" = "1" ] || [ "$ENABLED_ACLS" = "1" ]; then
local start_daemon=$(config_n_get @global_delay[0] start_daemon 0) local start_daemon=$(config_n_get @global_delay[0] start_daemon 0)
@@ -976,7 +978,7 @@ start_crontab() {
if [ "$week" = "8" ]; then if [ "$week" = "8" ]; then
update_loop=1 update_loop=1
else 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 fi
echolog "$logmsg" echolog "$logmsg"
} }
@@ -996,7 +998,7 @@ start_crontab() {
if [ "$rules_update_week_mode" = "8" ]; then if [ "$rules_update_week_mode" = "8" ]; then
update_loop=1 update_loop=1
else 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 fi
echolog "配置定时任务:自动更新规则。" echolog "配置定时任务:自动更新规则。"
fi fi
@@ -1024,7 +1026,7 @@ start_crontab() {
if [ "$sub_update_week_mode" = "8" ]; then if [ "$sub_update_week_mode" = "8" ]; then
update_loop=1 update_loop=1
else 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 fi
done done
rm -rf "$TMP_SUB_PATH" rm -rf "$TMP_SUB_PATH"
@@ -1050,6 +1052,22 @@ stop_crontab() {
#echolog "清除定时执行命令。" #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() { start_adblock() {
[ "$(config_t_get global adblock 0)" != "1" ] && { [ "$(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 [ -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}" \ -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} \ -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} -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" USE_DEFAULT_DNS="chinadns_ng"
else else
@@ -1746,7 +1765,7 @@ stop() {
unset XRAY_LOCATION_ASSET unset XRAY_LOCATION_ASSET
unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN unset SS_SYSTEM_DNS_RESOLVER_FORCE_BUILTIN
stop_crontab $1 stop_crontab $1
source $APP_PATH/helper_smartdns.sh del del_smartdns_conf
rm -rf $GLOBAL_DNSMASQ_CONF rm -rf $GLOBAL_DNSMASQ_CONF
rm -rf $GLOBAL_DNSMASQ_CONF_PATH rm -rf $GLOBAL_DNSMASQ_CONF_PATH
[ "1" = "1" ] && { [ "1" = "1" ] && {
@@ -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
+1 -1
View File
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
PKG_NAME:=luci-app-passwall2 PKG_NAME:=luci-app-passwall2
PKG_VERSION:=26.10.1 PKG_VERSION:=26.10.1
PKG_RELEASE:=133 PKG_RELEASE:=134
PKG_PO_VERSION:=$(PKG_VERSION) PKG_PO_VERSION:=$(PKG_VERSION)
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
+8 -6
View File
@@ -5,11 +5,10 @@ STOP=15
. $IPKG_INSTROOT/usr/share/passwall2/utils.sh . $IPKG_INSTROOT/usr/share/passwall2/utils.sh
APP_FILE=${APP_PATH}/app.sh APP_FILE=${APP_PATH}/app.sh
LOCK_FILE_DIR=/var/lock LOCK_FILE=${LOCK_PATH}/${CONFIG}.lock
LOCK_FILE=${LOCK_FILE_DIR}/${CONFIG}.lock
set_lock() { set_lock() {
[ ! -d "$LOCK_FILE_DIR" ] && mkdir -p $LOCK_FILE_DIR [ ! -d "$LOCK_PATH" ] && mkdir -p $LOCK_PATH
exec 999>"$LOCK_FILE" exec 999>"$LOCK_FILE"
flock -xn 999 flock -xn 999
} }
@@ -33,13 +32,16 @@ unlock() {
} }
boot_func() { 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) local delay=$(uci -q get ${CONFIG}.@global_delay[0].start_delay || echo 1)
if [ "$delay" -gt 0 ]; then if [ "$delay" -gt 0 ]; then
log_i18n 0 "Start after a delay of %s seconds!" "${delay}" log_i18n 0 "Start after a delay of %s seconds!" "${delay}"
sleep $delay sleep $delay
fi fi
restart boot restart boot
touch ${LOCK_FILE_DIR}/${CONFIG}_ready.lock touch ${LOCK_PATH}/${CONFIG}_ready.lock
} }
boot() { boot() {
@@ -64,8 +66,8 @@ stop() {
restart() { restart() {
set_lock set_lock
[ $? == 1 ] && log_i18n 0 "The script is already running, do not run it again. Exit." && exit 0 [ $? == 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 rule_lock=${LOCK_PATH}/${CONFIG}_rule_update.lock
sub_lock=${LOCK_FILE_DIR}/${CONFIG}_subscribe.lock sub_lock=${LOCK_PATH}/${CONFIG}_subscribe.lock
for i in $(seq 1 300); do for i in $(seq 1 300); do
if [ ! -f "$rule_lock" ] && [ ! -f "$sub_lock" ]; then if [ ! -f "$rule_lock" ] && [ ! -f "$sub_lock" ]; then
break break
@@ -591,7 +591,7 @@ start_socks() {
} }
clean_crontab() { clean_crontab() {
[ -f "/tmp/lock/${CONFIG}_cron.lock" ] && return [ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && return
touch /etc/crontabs/root touch /etc/crontabs/root
#sed -i "/${CONFIG}/d" /etc/crontabs/root >/dev/null 2>&1 #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 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 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 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() { start_crontab() {
@@ -608,8 +608,8 @@ start_crontab() {
[ "$start_daemon" = "1" ] && { $APP_PATH/monitor.sh > /dev/null 2>&1 & } [ "$start_daemon" = "1" ] && { $APP_PATH/monitor.sh > /dev/null 2>&1 & }
fi fi
[ -f "/tmp/lock/${CONFIG}_cron.lock" ] && { [ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && {
rm -rf "/tmp/lock/${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." log_i18n 0 "The task is currently running automatically as a scheduled task; no reconfiguration of the scheduled task is required."
return return
} }
@@ -716,7 +716,7 @@ start_crontab() {
} }
stop_crontab() { stop_crontab() {
[ -f "/tmp/lock/${CONFIG}_cron.lock" ] && return [ -f "${LOCK_PATH}/${CONFIG}_cron.lock" ] && return
clean_crontab clean_crontab
/etc/init.d/cron restart /etc/init.d/cron restart
#log_i18n 0 "Clear scheduled commands." #log_i18n 0 "Clear scheduled commands."
@@ -934,6 +934,9 @@ start() {
} }
fi fi
run_process_queue run_process_queue
[ "$1" = "boot" ] && {
rm -f "${LOCK_PATH}/${CONFIG}_cron.lock"
}
start_crontab start_crontab
log_i18n 0 "Running complete!" log_i18n 0 "Running complete!"
echolog "\n" 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 [ -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_PATH
rm -rf /tmp/lock/${CONFIG}_socks_auto_switch* rm -f ${LOCK_PATH}/${CONFIG}_socks_auto_switch*
rm -rf /tmp/lock/${CONFIG}_lease2hosts* 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." log_i18n 0 "Clearing and closing related programs and cache complete."
exit 0 exit 0
} }
@@ -239,8 +239,8 @@ uci_save(true)
if reboot == 1 then if reboot == 1 then
if arg3 == "cron" then if arg3 == "cron" then
if not fs.access("/var/lock/" .. appname .. ".lock") then if not fs.access(api.LOCK_PREFIX .. ".lock") then
sys.call("touch /tmp/lock/" .. appname .. "_cron.lock") sys.call("touch %s_cron.lock" % api.LOCK_PREFIX)
end end
end end
+1 -1
View File
@@ -4,7 +4,7 @@ LUCI_TITLE:=luci-app-ssr-plus
LUCI_PKGARCH:=all LUCI_PKGARCH:=all
PKG_NAME:=luci-app-ssr-plus PKG_NAME:=luci-app-ssr-plus
PKG_VERSION:=196 PKG_VERSION:=196
PKG_RELEASE:=78 PKG_RELEASE:=79
PKG_CONFIG_DEPENDS:= \ PKG_CONFIG_DEPENDS:= \
CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \ CONFIG_PACKAGE_$(PKG_NAME)_Iptables_Transparent_Proxy \
@@ -85,10 +85,10 @@ done
# 此处直接使用 cat 因为有 sed '/#/d' 删除了 数据 # 此处直接使用 cat 因为有 sed '/#/d' 删除了 数据
if [ "$nft_support" = "1" ]; then 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#$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 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#$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 fi
sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' -e "s/.*/address=\/&\//" /etc/ssrplus/deny.list > "$TMP_DNSMASQ_PATH/denylist.conf" sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' -e "s/.*/address=\/&\//" /etc/ssrplus/deny.list > "$TMP_DNSMASQ_PATH/denylist.conf"
+1 -1
View File
@@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=Argon Theme LUCI_TITLE:=Argon Theme
LUCI_DEPENDS:=+USE_APK:wget-any +!USE_APK:wget +jsonfilter LUCI_DEPENDS:=+USE_APK:wget-any +!USE_APK:wget +jsonfilter
PKG_VERSION:=2.4.7 PKG_VERSION:=2.4.7
PKG_RELEASE:=24 PKG_RELEASE:=25
CONFIG_LUCI_CSSTIDY:= CONFIG_LUCI_CSSTIDY:=
@@ -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;} .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;} .defwallpaper.defwallpaper .main-bg img{object-position: -99999px -99999px;opacity: 0;}
.main .main-right{background-color: transparent} .main .main-right{background-color: transparent}
input[placeholder]::placeholder {color: #888888;opacity: 1}
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk include $(TOPDIR)/rules.mk
PKG_NAME:=rtp2httpd PKG_NAME:=rtp2httpd
PKG_VERSION:=3.17.1 PKG_VERSION:=3.17.2
PKG_RELEASE:=1 PKG_RELEASE:=2
PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz PKG_SOURCE:=$(PKG_NAME)-$(PKG_VERSION).tar.gz
PKG_SOURCE_URL:=https://codeload.github.com/stackia/rtp2httpd/tar.gz/v$(PKG_VERSION)? PKG_SOURCE_URL:=https://codeload.github.com/stackia/rtp2httpd/tar.gz/v$(PKG_VERSION)?