mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-10-31 18:35:13 +08:00
🐤 Sync 2026-10-03 00:12:36
This commit is contained in:
1 parent
ce3df55b24
commit
cb1d978497
58 files changed
+1627
-1018
No files matched your search
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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'
|
||||||
|
|
||||||
|
|
||||||
@@ -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
@@ -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();
|
||||||
|
|||||||
@@ -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 @@
|
|||||||
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"
|
||||||
@@ -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'));
|
||||||
|
|||||||
@@ -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'
|
||||||
@@ -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
|
||||||
@@ -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 实例目录
|
||||||
uci set "$UCI_CONFIG.dns_backup=dns_backup"
|
for dir in /tmp/dnsmasq*.d; do
|
||||||
[ -n "$old_noresolv" ] && uci set "$UCI_CONFIG.dns_backup.noresolv=$old_noresolv"
|
if [ -d "$dir" ] && [ "$dir" != "/tmp/dnsmasq.d" ]; then
|
||||||
[ -n "$old_dns_redirect" ] && uci set "$UCI_CONFIG.dns_backup.dns_redirect=$old_dns_redirect"
|
cp -f /tmp/dnsmasq.d/flowproxy.conf "$dir/flowproxy.conf"
|
||||||
[ -n "$old_servers" ] && uci set "$UCI_CONFIG.dns_backup.servers=$old_servers"
|
fi
|
||||||
uci commit "$UCI_CONFIG"
|
done
|
||||||
|
|
||||||
|
# 历史遗留自愈:若早期版本存在残留的 dns_backup 节,静默移除避免脏数据
|
||||||
|
if uci -q get "$UCI_CONFIG.dns_backup" >/dev/null; then
|
||||||
|
uci -q delete "$UCI_CONFIG.dns_backup"
|
||||||
|
uci -q commit "$UCI_CONFIG"
|
||||||
fi
|
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) {
|
||||||
|
|||||||
@@ -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'],
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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+,否则依赖解析失败。
|
||||||
|
|
||||||
## 已知限制
|
## 已知限制
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -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
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -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: []
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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:= \
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
@@ -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)?
|
||||||
|
|||||||
Reference in new issue
Block a user