From cc21462d8a5e49aed1c8bd0bf623fcf071e2d9f3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 11 Nov 2026 01:22:09 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20Sync=202026-11-11=2001:22:09?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- fancontrol/Makefile | 8 +- fancontrol/files/fancontrol.config | 1 + fancontrol/files/fancontrol.init | 1 + fancontrol/src/fancontrol.c | 398 +- luci-app-fancontrol/Makefile | 4 +- .../luci-static/resources/view/fancontrol.js | 10 + luci-app-fancontrol/po/zh_Hans/fancontrol.po | 8 +- .../resources/view/firewall/zones.js | 2 +- luci-app-nekobox/Makefile | 2 +- luci-app-nekobox/htdocs/nekobox/ping.php | 8023 +++++++++++++++++ luci-app-passwall/Makefile | 4 +- .../cbi/passwall/client/node_subscribe.lua | 13 +- .../passwall/client/node_subscribe_config.lua | 14 + .../cbi/passwall/client/type/1_sing-box.lua | 4 + .../luasrc/passwall/util_sing-box.lua | 4 +- .../luasrc/passwall/util_xray.lua | 2 +- .../passwall/node_config/link_share_man.htm | 192 + luci-app-passwall/po/zh-cn/passwall.po | 6 + .../root/usr/share/passwall/subscribe.lua | 115 + luci-app-passwall2/Makefile | 4 +- .../cbi/passwall2/client/node_subscribe.lua | 13 +- .../client/node_subscribe_config.lua | 14 + .../cbi/passwall2/client/type/1_sing-box.lua | 4 + .../luasrc/passwall2/util_sing-box.lua | 4 +- .../luasrc/passwall2/util_xray.lua | 2 +- .../passwall2/node_config/link_share_man.htm | 190 + luci-app-passwall2/po/fa/passwall2.po | 6 + luci-app-passwall2/po/ru/passwall2.po | 6 + luci-app-passwall2/po/zh-cn/passwall2.po | 6 + luci-app-passwall2/po/zh-tw/passwall2.po | 6 + .../root/usr/share/passwall2/subscribe.lua | 115 + luci-app-trafficctl/Makefile | 4 +- luci-app-v2raya/Makefile | 2 - luci-base/root/usr/share/rpcd/ucode/luci | 33 +- luci-mod-status/Makefile | 2 +- .../view/status/include/10_system.js | 5 + luci-theme-vantage/Makefile | 2 +- .../htdocs/luci-static/vantage/vantage.css | 7 +- .../po/templates/vantage-theme.pot | 12 +- .../po/zh_Hans/vantage-theme.po | 12 +- .../ucode/template/themes/vantage/sysauth.ut | 33 +- v2raya/Makefile | 190 + v2raya/files/v2raya-detect | 131 + v2raya/files/v2raya.config | 76 + v2raya/files/v2raya.init | 115 + v2raya/files/v2raya.keep | 1 + v2raya/files/web.tar.gz | Bin 0 -> 871507 bytes v2raya/test.sh | 13 + 48 files changed, 9760 insertions(+), 59 deletions(-) create mode 100644 luci-app-nekobox/htdocs/nekobox/ping.php create mode 100644 v2raya/Makefile create mode 100644 v2raya/files/v2raya-detect create mode 100644 v2raya/files/v2raya.config create mode 100644 v2raya/files/v2raya.init create mode 100644 v2raya/files/v2raya.keep create mode 100644 v2raya/files/web.tar.gz create mode 100644 v2raya/test.sh diff --git a/fancontrol/Makefile b/fancontrol/Makefile index 96cd8845c..4f01d67cb 100644 --- a/fancontrol/Makefile +++ b/fancontrol/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=fancontrol -PKG_VERSION:=2.0.0 -PKG_RELEASE:=2 +PKG_VERSION:=2.2.0 +PKG_RELEASE:=3 PKG_LICENSE:=MIT PKG_LICENSE_FILES:=LICENSE @@ -22,6 +22,10 @@ define Package/fancontrol/description Temperature-based continuous PWM fan controller using the Linux hwmon ABI. endef +define Package/fancontrol/conffiles +/etc/config/fancontrol +endef + define Build/Prepare mkdir -p $(PKG_BUILD_DIR) $(CP) ./src/Makefile ./src/fancontrol.c $(PKG_BUILD_DIR) diff --git a/fancontrol/files/fancontrol.config b/fancontrol/files/fancontrol.config index dddab40c6..062ebafbc 100644 --- a/fancontrol/files/fancontrol.config +++ b/fancontrol/files/fancontrol.config @@ -5,6 +5,7 @@ config fancontrol 'settings' option fan_file 'auto' option fan_hwmon 'auto' option enable_file 'auto' + option cooling_device 'auto' option start_temp '45' option full_speed_temp '85' option hysteresis '3' diff --git a/fancontrol/files/fancontrol.init b/fancontrol/files/fancontrol.init index d8b875a4f..d6f34337c 100755 --- a/fancontrol/files/fancontrol.init +++ b/fancontrol/files/fancontrol.init @@ -31,6 +31,7 @@ start_service() { append_option fan_file -F append_option fan_hwmon -N append_option enable_file -E + append_option cooling_device -C append_option start_temp -t append_option full_speed_temp -x append_option hysteresis -H diff --git a/fancontrol/src/fancontrol.c b/fancontrol/src/fancontrol.c index 7a051cb15..722cc91ba 100644 --- a/fancontrol/src/fancontrol.c +++ b/fancontrol/src/fancontrol.c @@ -16,11 +16,12 @@ #include #define PROGRAM_NAME "fancontrol" -#define PROGRAM_VERSION "2.0.0" +#define PROGRAM_VERSION "2.2.0" #define AUTO_VALUE "auto" #define DEFAULT_SYSFS_ROOT "/sys" #define DEFAULT_STATUS_FILE "/var/run/fancontrol.status" #define MAX_HWMON_CHANNELS 32 +#define THERMAL_TEMP_INVALID (-274000) struct fan_config { char thermal_file[PATH_MAX]; @@ -28,6 +29,7 @@ struct fan_config { char fan_file[PATH_MAX]; char fan_hwmon[128]; char enable_file[PATH_MAX]; + char cooling_device[PATH_MAX]; int start_temp; int full_speed_temp; int hysteresis; @@ -43,11 +45,21 @@ struct fan_config { bool once; }; +struct kernel_trip { + char path[PATH_MAX]; + int temperature; +}; + struct fan_runtime { char thermal_file[PATH_MAX]; char fan_file[PATH_MAX]; char enable_file[PATH_MAX]; char rpm_file[PATH_MAX]; + char kernel_state_file[PATH_MAX]; + struct kernel_trip *kernel_trips; + size_t kernel_trip_count; + int kernel_lock_fd; + bool kernel_state_saved; bool fan_active; }; @@ -64,6 +76,7 @@ static void usage(FILE *stream, const char *name) " -F path PWM sysfs path or 'auto'\n" " -N name preferred hwmon name or 'auto'\n" " -E path pwm enable sysfs path or 'auto'\n" + " -C path kernel cooling device, 'auto' or 'none'\n" " -t degrees fan start temperature\n" " -x degrees full-speed temperature\n" " -H degrees stop hysteresis\n" @@ -169,7 +182,7 @@ static int write_long_file(const char *path, int value) if (length < 0 || (size_t)length >= sizeof(buffer)) return -1; - fd = open(path, O_WRONLY | O_CLOEXEC); + fd = open(path, O_WRONLY | O_TRUNC | O_CLOEXEC); if (fd < 0) return -1; @@ -443,6 +456,7 @@ static int resolve_paths(const struct fan_config *config, struct fan_runtime *runtime) { memset(runtime, 0, sizeof(*runtime)); + runtime->kernel_lock_fd = -1; copy_string(runtime->enable_file, sizeof(runtime->enable_file), config->enable_file); @@ -497,6 +511,336 @@ static int resolve_paths(const struct fan_config *config, return 0; } +static int append_kernel_trip(struct fan_runtime *runtime, const char *path, + int temperature) +{ + struct kernel_trip *trips; + size_t i; + + for (i = 0; i < runtime->kernel_trip_count; i++) + if (strcmp(runtime->kernel_trips[i].path, path) == 0) + return 0; + + trips = realloc(runtime->kernel_trips, + (runtime->kernel_trip_count + 1) * sizeof(*trips)); + if (!trips) + return -1; + runtime->kernel_trips = trips; + if (copy_string(trips[runtime->kernel_trip_count].path, PATH_MAX, path)) + return -1; + trips[runtime->kernel_trip_count++].temperature = temperature; + return 0; +} + +static int restore_kernel_trips(struct fan_runtime *runtime) +{ + bool failed = false; + size_t i; + + if (!runtime->kernel_state_saved) + return 0; + + for (i = 0; i < runtime->kernel_trip_count; i++) { + const struct kernel_trip *trip = &runtime->kernel_trips[i]; + long current; + + if (read_long_file(trip->path, ¤t)) { + if (errno == ENOENT) + continue; + } else if (current != THERMAL_TEMP_INVALID) { + /* Preserve changes made by another administrator. */ + continue; + } else if (!write_long_file(trip->path, trip->temperature)) { + continue; + } + syslog(LOG_ERR, "failed to restore kernel fan trip %s: %s", + trip->path, strerror(errno)); + failed = true; + } + if (failed || (unlink(runtime->kernel_state_file) && errno != ENOENT)) + return -1; + + free(runtime->kernel_trips); + runtime->kernel_trips = NULL; + runtime->kernel_trip_count = 0; + runtime->kernel_state_saved = false; + return 0; +} + +static int load_kernel_state(struct fan_runtime *runtime) +{ + char line[PATH_MAX + 64]; + FILE *stream; + int result = 0; + + stream = fopen(runtime->kernel_state_file, "r"); + if (!stream) + return errno == ENOENT ? 0 : -1; + + while (fgets(line, sizeof(line), stream)) { + int temperature, offset = 0, zone, trip, length = 0; + const char *path; + size_t root_length = strlen(sysfs_root); + + line[strcspn(line, "\r\n")] = '\0'; + if (sscanf(line, "%d %n", &temperature, &offset) != 1 || + temperature <= THERMAL_TEMP_INVALID) + goto invalid; + path = line + offset; + if (strncmp(path, sysfs_root, root_length) || + sscanf(path + root_length, + "/class/thermal/thermal_zone%d/trip_point_%d_temp%n", + &zone, &trip, &length) != 2 || + zone < 0 || trip < 0 || path[root_length + length] || + append_kernel_trip(runtime, path, temperature)) + goto invalid; + continue; +invalid: + syslog(LOG_ERR, "invalid kernel fan state in %s", + runtime->kernel_state_file); + result = -1; + break; + } + if (ferror(stream)) + result = -1; + fclose(stream); + if (!result) + runtime->kernel_state_saved = true; + return result; +} + +static int save_kernel_state(struct fan_runtime *runtime) +{ + char temporary[PATH_MAX]; + FILE *stream; + bool failed = false; + size_t i; + int fd; + + if (snprintf(temporary, sizeof(temporary), "%s.%ld", + runtime->kernel_state_file, (long)getpid()) >= (int)sizeof(temporary)) + return -1; + fd = open(temporary, O_WRONLY | O_CREAT | O_EXCL | O_CLOEXEC, 0600); + if (fd < 0) + return -1; + stream = fdopen(fd, "w"); + if (!stream) { + close(fd); + unlink(temporary); + return -1; + } + for (i = 0; i < runtime->kernel_trip_count; i++) + if (fprintf(stream, "%d %s\n", runtime->kernel_trips[i].temperature, + runtime->kernel_trips[i].path) < 0) + failed = true; + if (fflush(stream) || fsync(fd)) + failed = true; + if (fclose(stream)) + failed = true; + if (failed || rename(temporary, runtime->kernel_state_file)) { + unlink(temporary); + return -1; + } + runtime->kernel_state_saved = true; + return 0; +} + +/* Cooling devices have no hwmon/OF link in sysfs. Only infer a unique pair. */ +static int find_kernel_cooling_device(const struct fan_config *config, + const struct fan_runtime *runtime, + char *result, size_t size) +{ + char pattern[PATH_MAX], path[PATH_MAX], name[128]; + glob_t paths = { 0 }; + char *slash; + int fan_count = 0, cooling_count = 0; + size_t i; + + if (strcmp(config->cooling_device, "none") == 0) + return 0; + if (!is_auto(config->cooling_device)) + return copy_string(result, size, config->cooling_device) ? -1 : 1; + + if (copy_string(path, sizeof(path), runtime->fan_file)) + return -1; + slash = strrchr(path, '/'); + if (!slash || copy_string(slash + 1, sizeof(path) - (size_t)(slash + 1 - path), + "name") || read_text_file(path, name, sizeof(name)) || + strcmp(name, "pwmfan")) + return 0; + + if (snprintf(pattern, sizeof(pattern), "%s/class/hwmon/hwmon*/name", + sysfs_root) >= (int)sizeof(pattern)) + return -1; + if (glob(pattern, 0, NULL, &paths) == 0) { + for (i = 0; i < paths.gl_pathc; i++) + if (!read_text_file(paths.gl_pathv[i], name, sizeof(name)) && + strcmp(name, "pwmfan") == 0) + fan_count++; + } + globfree(&paths); + memset(&paths, 0, sizeof(paths)); + if (snprintf(pattern, sizeof(pattern), "%s/class/thermal/cooling_device*/type", + sysfs_root) >= (int)sizeof(pattern)) + return -1; + if (glob(pattern, 0, NULL, &paths) == 0) { + for (i = 0; i < paths.gl_pathc; i++) { + if (read_text_file(paths.gl_pathv[i], name, sizeof(name)) || + strcmp(name, "pwm-fan")) + continue; + if (copy_string(result, size, paths.gl_pathv[i])) { + globfree(&paths); + return -1; + } + *strrchr(result, '/') = '\0'; + cooling_count++; + } + } + globfree(&paths); + if (!cooling_count) + return 0; + if (fan_count != 1 || cooling_count != 1) { + syslog(LOG_ERR, "multiple PWM fans or cooling devices; select cooling_device explicitly"); + return -1; + } + return 1; +} + +static bool is_cooling_binding(const char *path) +{ + const char *name = strrchr(path, '/'); + int index; + char trailing; + + return name && sscanf(name + 1, "cdev%d%c", &index, &trailing) == 1 && index >= 0; +} + +static int collect_kernel_trips(const char *cooling_device, struct fan_runtime *runtime) +{ + char pattern[PATH_MAX], path[PATH_MAX], zone[PATH_MAX], type[128]; + struct stat selected, bound; + glob_t bindings = { 0 }; + int result = -1; + size_t i, j; + + if (stat(cooling_device, &selected)) + return -1; + if (snprintf(pattern, sizeof(pattern), "%s/class/thermal/thermal_zone*/cdev*", + sysfs_root) >= (int)sizeof(pattern)) + return -1; + if (glob(pattern, 0, NULL, &bindings)) + return 0; + + for (i = 0; i < bindings.gl_pathc; i++) { + const char *binding = bindings.gl_pathv[i]; + long trip, temperature; + + if (!is_cooling_binding(binding) || stat(binding, &bound) || + selected.st_dev != bound.st_dev || selected.st_ino != bound.st_ino) + continue; + if (snprintf(path, sizeof(path), "%s_trip_point", binding) >= (int)sizeof(path) || + read_long_file(path, &trip) || trip < 0 || trip > INT_MAX || + copy_string(zone, sizeof(zone), binding)) + goto out; + *strrchr(zone, '/') = '\0'; + if (snprintf(path, sizeof(path), "%s/policy", zone) >= (int)sizeof(path) || + read_text_file(path, type, sizeof(type))) + goto out; + if (strcmp(type, "user_space") == 0) + continue; + if (strcmp(type, "step_wise")) { + syslog(LOG_ERR, "cannot take over fan from kernel governor %s in %s", type, zone); + goto out; + } + if (snprintf(path, sizeof(path), "%s/trip_point_%ld_type", zone, trip) >= + (int)sizeof(path) || read_text_file(path, type, sizeof(type))) + goto out; + if (strcmp(type, "active")) { + syslog(LOG_ERR, "refusing to disable %s fan trip in %s", type, zone); + goto out; + } + /* A trip may also control CPU frequency or another fan. Leave it alone. */ + for (j = 0; j < bindings.gl_pathc; j++) { + const char *other = bindings.gl_pathv[j]; + long other_trip; + + if (!is_cooling_binding(other) || strncmp(other, zone, strlen(zone)) || + other[strlen(zone)] != '/' || stat(other, &bound) || + (selected.st_dev == bound.st_dev && selected.st_ino == bound.st_ino)) + continue; + if (snprintf(path, sizeof(path), "%s_trip_point", other) >= (int)sizeof(path) || + read_long_file(path, &other_trip)) + goto out; + if (other_trip == trip) { + syslog(LOG_ERR, "fan trip %ld in %s is shared with another cooling device", trip, zone); + goto out; + } + } + if (snprintf(path, sizeof(path), "%s/trip_point_%ld_temp", zone, trip) >= + (int)sizeof(path) || read_long_file(path, &temperature)) + goto out; + if (temperature == THERMAL_TEMP_INVALID) + continue; + if (temperature <= THERMAL_TEMP_INVALID || temperature > INT_MAX || + access(path, W_OK) || append_kernel_trip(runtime, path, (int)temperature)) + goto out; + } + result = 0; +out: + globfree(&bindings); + return result; +} + +static int claim_kernel_fan_control(const struct fan_config *config, + struct fan_runtime *runtime) +{ + char path[PATH_MAX]; + struct flock lock = { .l_type = F_WRLCK, .l_whence = SEEK_SET }; + int found; + size_t i; + + if (snprintf(runtime->kernel_state_file, sizeof(runtime->kernel_state_file), + "%s.thermal", status_file) >= (int)sizeof(runtime->kernel_state_file) || + snprintf(path, sizeof(path), "%s.lock", status_file) >= (int)sizeof(path)) + return -1; + runtime->kernel_lock_fd = open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0600); + if (runtime->kernel_lock_fd < 0 || fcntl(runtime->kernel_lock_fd, F_SETLK, &lock)) { + syslog(LOG_ERR, "cannot lock controller state %s: %s", path, strerror(errno)); + return -1; + } + /* Recover the original thresholds if an earlier process was killed. */ + if (load_kernel_state(runtime) || restore_kernel_trips(runtime)) + return -1; + found = find_kernel_cooling_device(config, runtime, path, sizeof(path)); + if (found <= 0) + return found; + if (collect_kernel_trips(path, runtime)) + return -1; + if (!runtime->kernel_trip_count) + return 0; + /* Persist every original value before changing any sysfs attributes. */ + if (save_kernel_state(runtime)) + return -1; + for (i = 0; i < runtime->kernel_trip_count; i++) { + long temperature; + + if (write_long_file(runtime->kernel_trips[i].path, THERMAL_TEMP_INVALID)) { + syslog(LOG_ERR, "failed to pause kernel fan trip %s: %s", + runtime->kernel_trips[i].path, strerror(errno)); + return -1; + } + if (read_long_file(runtime->kernel_trips[i].path, &temperature) || + temperature != THERMAL_TEMP_INVALID) { + syslog(LOG_ERR, "kernel did not pause fan trip %s", + runtime->kernel_trips[i].path); + return -1; + } + } + syslog(LOG_NOTICE, "paused %zu kernel fan trips; hot, critical and passive trips remain active", + runtime->kernel_trip_count); + return 0; +} + static int calculate_pwm(const struct fan_config *config, int64_t temp_mc, bool fan_active) { @@ -572,7 +916,8 @@ static int install_signal_handlers(void) sigemptyset(&action.sa_mask); return sigaction(SIGINT, &action, NULL) || - sigaction(SIGTERM, &action, NULL); + sigaction(SIGTERM, &action, NULL) || + sigaction(SIGHUP, &action, NULL); } static void set_defaults(struct fan_config *config) @@ -583,6 +928,7 @@ static void set_defaults(struct fan_config *config) copy_string(config->fan_file, sizeof(config->fan_file), AUTO_VALUE); copy_string(config->fan_hwmon, sizeof(config->fan_hwmon), AUTO_VALUE); copy_string(config->enable_file, sizeof(config->enable_file), AUTO_VALUE); + copy_string(config->cooling_device, sizeof(config->cooling_device), AUTO_VALUE); config->start_temp = 45; config->full_speed_temp = 85; config->hysteresis = 3; @@ -600,7 +946,7 @@ static void parse_options(int argc, char **argv, struct fan_config *config) { int option; - while ((option = getopt(argc, argv, "T:Z:F:N:E:t:x:H:s:m:k:K:f:q:d:i:D1vh")) != -1) { + while ((option = getopt(argc, argv, "T:Z:F:N:E:C:t:x:H:s:m:k:K:f:q:d:i:D1vh")) != -1) { switch (option) { case 'T': if (copy_string(config->thermal_file, @@ -623,6 +969,10 @@ static void parse_options(int argc, char **argv, struct fan_config *config) if (copy_string(config->enable_file, sizeof(config->enable_file), optarg)) exit(EXIT_FAILURE); break; + case 'C': + if (copy_string(config->cooling_device, sizeof(config->cooling_device), optarg)) + exit(EXIT_FAILURE); + break; case 't': config->start_temp = parse_int(optarg, -100, 200, "start temperature"); break; @@ -700,6 +1050,7 @@ int main(int argc, char **argv) struct fan_runtime runtime; const char *environment; bool sensor_failed = false; + bool control_ready = false; int exit_code = EXIT_SUCCESS; set_defaults(&config); @@ -723,9 +1074,20 @@ int main(int argc, char **argv) if (resolve_paths(&config, &runtime)) return EXIT_FAILURE; - if (runtime.enable_file[0] && write_long_file(runtime.enable_file, 1)) + if (claim_kernel_fan_control(&config, &runtime)) { + syslog(LOG_ERR, "cannot safely take control of the fan from the kernel"); + if (runtime.kernel_state_saved) + set_pwm(&runtime, config.fail_safe_pwm); + exit_code = EXIT_FAILURE; + goto cleanup; + } + control_ready = true; + if (runtime.enable_file[0] && write_long_file(runtime.enable_file, 1)) { syslog(LOG_WARNING, "failed to select manual PWM mode through %s: %s", runtime.enable_file, strerror(errno)); + exit_code = EXIT_FAILURE; + goto cleanup; + } syslog(LOG_NOTICE, "using temperature input %s and PWM output %s", runtime.thermal_file, runtime.fan_file); @@ -739,8 +1101,10 @@ int main(int argc, char **argv) if (read_long_file(runtime.thermal_file, &raw_temp)) { target_pwm = config.fail_safe_pwm; - if (set_pwm(&runtime, target_pwm)) + if (set_pwm(&runtime, target_pwm)) { exit_code = EXIT_FAILURE; + break; + } runtime.fan_active = target_pwm > 0; write_status(&runtime, temp_mc, target_pwm, rpm, "temperature-read-failed"); @@ -759,14 +1123,19 @@ int main(int argc, char **argv) if (target_pwm > 0 && !runtime.fan_active && config.kick_pwm > 0 && config.kick_ms > 0) { - if (!set_pwm(&runtime, config.kick_pwm)) - sleep_ms((unsigned int)config.kick_ms); + if (set_pwm(&runtime, config.kick_pwm)) { + exit_code = EXIT_FAILURE; + break; + } + sleep_ms((unsigned int)config.kick_ms); } if (!stop_requested && (read_long_file(runtime.fan_file, &actual_pwm) || - actual_pwm != target_pwm) && set_pwm(&runtime, target_pwm)) + actual_pwm != target_pwm) && set_pwm(&runtime, target_pwm)) { exit_code = EXIT_FAILURE; + break; + } runtime.fan_active = target_pwm > 0; if (runtime.rpm_file[0]) @@ -783,9 +1152,16 @@ int main(int argc, char **argv) sleep_ms((unsigned int)config.interval * 1000); } - if (!config.once && set_pwm(&runtime, config.exit_pwm)) +cleanup: + if (control_ready && !config.once && set_pwm(&runtime, config.exit_pwm)) exit_code = EXIT_FAILURE; - unlink(status_file); + if (restore_kernel_trips(&runtime)) + exit_code = EXIT_FAILURE; + free(runtime.kernel_trips); + if (runtime.kernel_lock_fd >= 0) + close(runtime.kernel_lock_fd); + if (control_ready) + unlink(status_file); closelog(); return exit_code; } diff --git a/luci-app-fancontrol/Makefile b/luci-app-fancontrol/Makefile index 8514cdae6..b187fdf7d 100644 --- a/luci-app-fancontrol/Makefile +++ b/luci-app-fancontrol/Makefile @@ -14,8 +14,8 @@ PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=JiaY-shi PKG_NAME:=luci-app-fancontrol -PKG_VERSION:=2.0.0 -PKG_RELEASE:=3 +PKG_VERSION:=2.2.0 +PKG_RELEASE:=4 include $(TOPDIR)/feeds/luci/luci.mk # call BuildPackage - OpenWrt buildroot signature diff --git a/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js b/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js index ba92d465f..c270c4cdd 100644 --- a/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js +++ b/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js @@ -406,6 +406,16 @@ return view.extend({ o.validate = validateAutoPath; o.description = _('Optional pwmN_enable path used to select manual PWM mode. With "auto", the matching control is used when available.'); + o = s.taboption('hardware', form.Value, 'cooling_device', _('Kernel cooling device')); + o.default = 'auto'; + o.value('auto'); + o.value('none'); + o.rmempty = false; + o.validate = function(sectionId, value) { + return value === 'none' ? true : validateAutoPath(sectionId, value); + }; + o.description = _('Use auto for a single pwm-fan device, an explicit cooling_device path for multiple fans, or none when kernel fan control is already handled elsewhere.'); + o = s.taboption('hardware', form.Value, 'temp_div', _('Temperature divisor')); o.default = '1000'; o.value('1'); diff --git a/luci-app-fancontrol/po/zh_Hans/fancontrol.po b/luci-app-fancontrol/po/zh_Hans/fancontrol.po index f7f092e45..be72c0edf 100644 --- a/luci-app-fancontrol/po/zh_Hans/fancontrol.po +++ b/luci-app-fancontrol/po/zh_Hans/fancontrol.po @@ -1,6 +1,6 @@ msgid "" msgstr "" -"Project-Id-Version: luci-app-fancontrol 2.0.0\n" +"Project-Id-Version: luci-app-fancontrol 2.2.0\n" "PO-Revision-Date: 2026-07-30 00:00+0800\n" "Last-Translator: JiaY-shi \n" "Language-Team: Chinese (Simplified Han script)\n" @@ -10,6 +10,12 @@ msgstr "" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=1; plural=0;\n" +msgid "Kernel cooling device" +msgstr "内核散热设备" + +msgid "Use auto for a single pwm-fan device, an explicit cooling_device path for multiple fans, or none when kernel fan control is already handled elsewhere." +msgstr "单个 pwm-fan 设备使用 auto;多个风扇需指定对应的 cooling_device 路径;已通过其他方式处理内核风扇控制时使用 none。" + #: /home/shi/document/c-service/fancontrol/luci-app-fancontrol/htdocs/luci-static/resources/view/fancontrol.js:173 msgid "" "After starting, the fan stops only after temperature falls this many degrees " diff --git a/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js b/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js index 532ba11e1..7a80cd38b 100644 --- a/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js +++ b/luci-app-firewall/htdocs/luci-static/resources/view/firewall/zones.js @@ -163,7 +163,7 @@ return view.extend({ (value === '2' && L.hasSystemFeature('offloading_hw')) ? '1' : null); }; - if (fw4 && L.hasSystemFeature('offloading_hw')) { + if (fw4 && L.hasSystemFeature('offloading_hw') && !L.hasSystemFeature('zte_npu')) { o = s.option(form.ListValue, 'flow_offloading_hw_ipv6', _('Hardware offloading protocols')); o.value('1', _('IPv4+v6 acceleration')); o.value('0', _('IPv4 acceleration')); diff --git a/luci-app-nekobox/Makefile b/luci-app-nekobox/Makefile index 45f3f4266..98b289a95 100644 --- a/luci-app-nekobox/Makefile +++ b/luci-app-nekobox/Makefile @@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk PKG_MAINTAINER:=Thaolga PKG_NAME:=luci-app-nekobox PKG_VERSION:=2.0.9 -PKG_RELEASE:=8 +PKG_RELEASE:=10 PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME) diff --git a/luci-app-nekobox/htdocs/nekobox/ping.php b/luci-app-nekobox/htdocs/nekobox/ping.php new file mode 100644 index 000000000..b9f1b112b --- /dev/null +++ b/luci-app-nekobox/htdocs/nekobox/ping.php @@ -0,0 +1,8023 @@ +/dev/null")); + +date_default_timezone_set($timezone ?: 'UTC'); +?> + + false, + 'message' => '' + ]; + + if (is_dir($nekoDir)) { + if (deleteNekoTmpDirectory($nekoDir)) { + $response['success'] = true; + $response['message'] = 'Directory cleared successfully.'; + } else { + $response['message'] = 'Failed to delete the directory.'; + } + } else { + $response['message'] = 'The directory does not exist.'; + } + + header('Content-Type: application/json'); + echo json_encode($response); + exit; +} + +function deleteNekoTmpDirectory($dir) { + if (!file_exists($dir)) { + return true; + } + if (!is_dir($dir)) { + return unlink($dir); + } + + foreach (scandir($dir) as $item) { + if ($item === '.' || $item === '..') { + continue; + } + + if (!deleteNekoTmpDirectory($dir . DIRECTORY_SEPARATOR . $item)) { + return false; + } + } + + return rmdir($dir); +} +?> + +"; + $singbox_autostart = $autostart; +} +?> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/luci-app-passwall/Makefile b/luci-app-passwall/Makefile index f0d82808f..6aadc683d 100644 --- a/luci-app-passwall/Makefile +++ b/luci-app-passwall/Makefile @@ -7,8 +7,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall -PKG_VERSION:=26.11.3 -PKG_RELEASE:=374 +PKG_VERSION:=26.11.11 +PKG_RELEASE:=375 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua index 8762ace12..4cb055273 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe.lua @@ -10,6 +10,7 @@ local trojan_type = {} local vmess_type = {} local vless_type = {} local hysteria2_type = {} +local other_type = {} local xray_version = api.get_app_version("xray") if has_ss_rust then local s = "shadowsocks-rust" @@ -22,6 +23,7 @@ if has_singbox then table.insert(vmess_type, s) table.insert(vless_type, s) table.insert(hysteria2_type, s) + table.insert(other_type, s) end if has_xray then local s = "xray" @@ -29,6 +31,7 @@ if has_xray then table.insert(ss_type, s) table.insert(vmess_type, s) table.insert(vless_type, s) + table.insert(other_type, s) if api.compare_versions(xray_version, ">=", "26.1.13") then table.insert(hysteria2_type, s) end @@ -100,7 +103,15 @@ if #hysteria2_type > 0 then end end -if #ss_type > 0 or #trojan_type > 0 or #vmess_type > 0 or #vless_type > 0 or #hysteria2_type > 0 then +if #other_type > 0 then + o = s:option(ListValue, "other_type", translatef("%s Node Use Type", translate("Other"))) + o:value("", translate("Auto")) + for key, value in pairs(other_type) do + o:value(value) + end +end + +if #ss_type > 0 or #trojan_type > 0 or #vmess_type > 0 or #vless_type > 0 or #hysteria2_type > 0 or #other_type > 0 then o.description = string.format("%s", translate("The configured type also applies to the core specified when manually importing nodes.")) end diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua index f504e826a..531349f55 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/node_subscribe_config.lua @@ -23,6 +23,7 @@ local trojan_type = {} local vmess_type = {} local vless_type = {} local hysteria2_type = {} +local other_type = {} local xray_version = api.get_app_version("xray") if has_ss_rust then local s = "shadowsocks-rust" @@ -35,6 +36,7 @@ if has_singbox then table.insert(vmess_type, s) table.insert(vless_type, s) table.insert(hysteria2_type, s) + table.insert(other_type, s) end if has_xray then local s = "xray" @@ -42,6 +44,7 @@ if has_xray then table.insert(ss_type, s) table.insert(vmess_type, s) table.insert(vless_type, s) + table.insert(other_type, s) if api.compare_versions(xray_version, ">=", "26.1.13") then table.insert(hysteria2_type, s) end @@ -222,7 +225,18 @@ if #hysteria2_type > 0 then for key, value in pairs(hysteria2_type) do o:value(value) end +end +if #other_type > 0 then + o = s:option(ListValue, "other_type", translatef("%s Node Use Type", translate("Other"))) + o.default = "global" + o:value("global", translate("Use global config")) + for key, value in pairs(other_type) do + o:value(value) + end +end + +if #hysteria2_type > 0 then o = s:option(Value, "hysteria_up_mbps", "Hy/Hy2 " .. translate("Max upload Mbps")) o.datatype = "uinteger" diff --git a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua index c36f30739..5b9d08d82 100644 --- a/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua +++ b/luci-app-passwall/luasrc/model/cbi/passwall/client/type/1_sing-box.lua @@ -706,6 +706,10 @@ if singbox_tags:find("with_wireguard") then o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_keepAlive", translate("Keep Alive")) + o.default = "0" + o:depends({ protocol = "wireguard" }) end -- [[ TCP部分(模拟) ]]-- diff --git a/luci-app-passwall/luasrc/passwall/util_sing-box.lua b/luci-app-passwall/luasrc/passwall/util_sing-box.lua index 90a6df4fd..075b2c3b3 100644 --- a/luci-app-passwall/luasrc/passwall/util_sing-box.lua +++ b/luci-app-passwall/luasrc/passwall/util_sing-box.lua @@ -507,6 +507,7 @@ function gen_outbound(flag, node, tag, proxy_table) pre_shared_key = node.wireguard_preSharedKey, reserved = node.wireguard_reserved, mtu = tonumber(node.wireguard_mtu), + persistent_keepalive_interval = tonumber(node.wireguard_keepAlive), } end @@ -2438,7 +2439,8 @@ function gen_config(var) port = value.server_port, public_key = value.peer_public_key, pre_shared_key = value.pre_shared_key, - allowed_ips = {"0.0.0.0/0"}, + allowed_ips = {"0.0.0.0/0", "::/0"}, + persistent_keepalive_interval = value.persistent_keepalive_interval, reserved = value.reserved } }, diff --git a/luci-app-passwall/luasrc/passwall/util_xray.lua b/luci-app-passwall/luasrc/passwall/util_xray.lua index dd81a1574..5cf6a452d 100644 --- a/luci-app-passwall/luasrc/passwall/util_xray.lua +++ b/luci-app-passwall/luasrc/passwall/util_xray.lua @@ -436,7 +436,7 @@ function gen_outbound(flag, node, tag, proxy_table) peers = (node.protocol == "wireguard") and { { publicKey = node.wireguard_public_key, - endpoint = node.address .. ":" .. node.port, + endpoint = (api.is_ipv6(node.address) and ("[" .. node.address .. "]") or node.address) .. ":" .. node.port, preSharedKey = node.wireguard_preSharedKey, keepAlive = node.wireguard_keepAlive and tonumber(node.wireguard_keepAlive) or nil } diff --git a/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm b/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm index 515135548..6918a98fa 100644 --- a/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm +++ b/luci-app-passwall/luasrc/view/passwall/node_config/link_share_man.htm @@ -14,6 +14,7 @@ local trojan_type = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_x local vmess_type = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) local vless_type = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) local hysteria2_type = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"}, {has_xray,"xray"}}) +local other_type = api.get_core("other_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) local current_node = map:get(section) -%> @@ -27,6 +28,7 @@ local current_node = map:get(section) let vmess_type = "<%=vmess_type%>" let vless_type = "<%=vless_type%>" let hysteria2_type = "<%=hysteria2_type%>" + let other_type = "<%=other_type%>" function padright(str, cnt, pad) { return str + Array(cnt + 1).join(pad); @@ -191,6 +193,8 @@ local current_node = map:get(section) dom_prefix = "xray_" } else if (v_type === "sing-box") { dom_prefix = "singbox_" + } else if (v_type === "Socks") { + dom_prefix = "socks_" } var _address = "" if (dom_prefix && dom_prefix != null) { @@ -741,6 +745,74 @@ local current_node = map:get(section) params = params.substring(1); } url += params; + } else if ((v_type === "Xray" || v_type === "sing-box") && opt.get(dom_prefix + "protocol").value === "wireguard") { + protocol = "wireguard"; + var v_password = opt.get(dom_prefix + "wireguard_secret_key"); + var v_port = opt.get(dom_prefix + "port"); + url = encodeURIComponent(v_password.value) + + "@" + _address + + ":" + v_port.value + "?"; + + var params = ""; + params += opt.query("publickey", dom_prefix + "wireguard_public_key"); + params += opt.query("presharedkey", dom_prefix + "wireguard_preSharedKey"); + var addresses = opt.getlist(dom_prefix + "wireguard_local_address").map(value => value.trim()).filter(Boolean); + params += "&address=" + encodeURIComponent(addresses.join(",")); + params += opt.query("reserved", dom_prefix + "wireguard_reserved"); + params += opt.query("mtu", dom_prefix + "wireguard_mtu"); + params += opt.query("persistent-keepalive", dom_prefix + "wireguard_keepAlive"); + if (v_type === "Xray" && opt.get(dom_prefix + "use_finalmask")?.checked) { + params += opt.query("fm", dom_prefix + "finalmask"); + } + params += "#" + encodeURIComponent(v_alias.value); + if (params[0] == "&") { + params = params.substring(1); + } + url += params; + } else if (v_type === "Socks" || ((v_type === "sing-box" || v_type === "Xray") && opt.get(dom_prefix + "protocol").value === "socks")) { + protocol = "socks"; + var v_username = opt.get(dom_prefix + "username"); + var v_password = opt.get(dom_prefix + "password"); + var v_port = opt.get(dom_prefix + "port"); + url = _address + ":" + v_port.value; + if (v_username.value || v_password.value) { + url = b64encutf8safe(v_username.value + ":" + v_password.value) + "@" + url; + } + var params = ""; + params += opt.query("uot", dom_prefix + "uot"); + params += opt.query("tfo", dom_prefix + "tcp_fast_open"); + if (params) { + url += "?" + params.substring(1); + } + url += "#" + encodeURIComponent(v_alias.value); + } else if ((v_type === "sing-box" || v_type === "Xray") && opt.get(dom_prefix + "protocol").value === "http") { + protocol = opt.get(dom_prefix + "tls").checked ? "https" : "http"; + var v_username = opt.get(dom_prefix + "username"); + var v_password = opt.get(dom_prefix + "password"); + var v_port = opt.get(dom_prefix + "port"); + url = _address + ":" + v_port.value; + if (v_username.value || v_password.value) { + url = encodeURIComponent(v_username.value) + ":" + encodeURIComponent(v_password.value) + "@" + url; + } + var params = ""; + if (protocol === "https") { + params += opt.query("sni", dom_prefix + "tls_serverName"); + params += opt.query("alpn", dom_prefix + "alpn"); + params += opt.query("insecure", dom_prefix + "tls_allowInsecure"); + params += opt.query("pcs", dom_prefix + "tls_pinSHA256"); + params += opt.query("vcn", dom_prefix + "tls_CertByName"); + if (opt.get(dom_prefix + "utls")?.checked) { + params += opt.query("fp", dom_prefix + "fingerprint"); + } + if (opt.get(dom_prefix + "ech")?.checked) { + params += opt.query("ech", dom_prefix + "ech_config"); + } + } + params += opt.query("tfo", dom_prefix + "tcp_fast_open"); + if (params) { + url += "?" + params.substring(1); + } + url += "#" + encodeURIComponent(v_alias.value); } if (url) { @@ -1828,6 +1900,126 @@ local current_node = map:get(section) } } + if (link_type === "wireguard") { + if (other_type == "sing-box" && has_singbox) { + dom_prefix = "singbox_"; + opt.set('type', "sing-box"); + } else if (other_type == "xray" && has_xray) { + dom_prefix = "xray_"; + opt.set('type', "Xray"); + } + opt.set(dom_prefix + 'protocol', "wireguard"); + var info = link_split[1].split('@'); + var m = parseNodeUrl("wireguard://" + info[1]); + var host_port = info[1].split(/[/?#]/)[0]; + opt.set(dom_prefix + 'address', unbracketIP(m.hostname)); + opt.set(dom_prefix + 'port', host_port.match(/:(\d+)$/)?.[1] || '51820'); + opt.set(dom_prefix + 'wireguard_secret_key', decodeURIComponent(info[0])); + var queryParam = {}; + if (m.search.length > 1) { + var queryArray = m.search.substr(1).split('&'); + for (var i = 0; i < queryArray.length; i++) { + var index = queryArray[i].indexOf('='); + if (index > 0) { + queryParam[decodeURIComponent(queryArray[i].substr(0, index)).toLowerCase()] = decodeURIComponent(queryArray[i].substr(index + 1)); + } + } + } + opt.set(dom_prefix + 'wireguard_public_key', queryParam.publickey); + opt.set(dom_prefix + 'wireguard_preSharedKey', queryParam.presharedkey || ''); + var addresses = ((queryParam.address || queryParam.ip || '') + ',' + (queryParam.ipv6 || '')).split(/[,\s]+/).filter(Boolean); + var fullName = opt.base + '.' + dom_prefix + 'wireguard_local_address'; + var lists = document.querySelectorAll('.cbi-dynlist'); + for (var i = 0; i < lists.length; i++) { + var label = lists[i].closest('.cbi-value')?.querySelector('label.cbi-value-title'); + if (label?.getAttribute('for') === fullName) { + L.dom.callClassMethod(lists[i], 'setValue', addresses); + lists[i].dispatchEvent(new Event('change', { bubbles: true })); + break; + } + } + opt.set(dom_prefix + 'wireguard_reserved', queryParam.reserved || ''); + opt.set(dom_prefix + 'wireguard_mtu', queryParam.mtu || '1420'); + opt.set(dom_prefix + 'wireguard_keepAlive', queryParam['persistent-keepalive'] || queryParam.keepalive || '0'); + if (dom_prefix === "xray_") { + opt.set(dom_prefix + 'use_finalmask', !!queryParam.fm); + opt.set(dom_prefix + 'finalmask', queryParam.fm || ''); + } + if (m.hash) { + opt.set('remarks', decodeURIComponent(m.hash.substr(1))); + } + } + + if (link_type === "socks" || link_type === "socks5" || link_type === "socks5h" || link_type === "http" || link_type === "https") { + if (other_type == "sing-box" && has_singbox) { + dom_prefix = "singbox_"; + opt.set('type', "sing-box"); + } else if (other_type == "xray" && has_xray) { + dom_prefix = "xray_"; + opt.set('type', "Xray"); + } + var protocol = (link_type === "http" || link_type === "https") ? "http" : "socks"; + opt.set(dom_prefix + 'protocol', protocol); + var content = link_split[1]; + var index = content.search(/[?#]/); + var suffix = index >= 0 ? content.substr(index) : ''; + content = index >= 0 ? content.substr(0, index) : content; + var base64_auth = false; + if (protocol === "socks" && content.indexOf('@') === -1 && content.indexOf(':') === -1) { + content = b64decutf8safe(decodeURIComponent(content)); + base64_auth = true; + } + var auth = ''; + var index = content.lastIndexOf('@'); + if (index >= 0) { + auth = content.substr(0, index); + content = content.substr(index + 1); + } + if (protocol === "socks" && auth.indexOf(':') === -1) { + auth = b64decutf8safe(decodeURIComponent(auth)); + base64_auth = true; + } + var index = auth.indexOf(':'); + var username = index >= 0 ? auth.substr(0, index) : auth; + var password = index >= 0 ? auth.substr(index + 1) : ''; + if (!base64_auth) { + username = decodeURIComponent(username); + password = decodeURIComponent(password); + } + var m = new URL(link_type + "://" + content + suffix); + opt.set(dom_prefix + 'address', unbracketIP(m.hostname)); + opt.set(dom_prefix + 'port', m.port || (protocol === "socks" ? "1080" : (link_type === "https" ? "443" : "80"))); + opt.set(dom_prefix + 'username', username); + opt.set(dom_prefix + 'password', password); + opt.set(dom_prefix + 'transport', dom_prefix === "xray_" ? "raw" : "tcp"); + var queryParam = {}; + if (m.search.length > 1) { + var queryArray = m.search.substr(1).split('&'); + for (var i = 0; i < queryArray.length; i++) { + var index = queryArray[i].indexOf('='); + if (index > 0) { + queryParam[decodeURIComponent(queryArray[i].substr(0, index)).toLowerCase()] = decodeURIComponent(queryArray[i].substr(index + 1)); + } + } + } + opt.set(dom_prefix + 'uot', queryParam.uot === '1'); + opt.set(dom_prefix + 'tcp_fast_open', queryParam.tfo === '1'); + opt.set(dom_prefix + 'tls', link_type === "https"); + opt.set(dom_prefix + 'reality', false); + opt.set(dom_prefix + 'tls_serverName', queryParam.sni || ''); + opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default'); + opt.set(dom_prefix + 'tls_allowInsecure', (queryParam.allowinsecure || queryParam.insecure || queryParam.allow_insecure || '0') === '1'); + opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pcs || ''); + opt.set(dom_prefix + 'tls_CertByName', queryParam.vcn || ''); + opt.set(dom_prefix + 'utls', !!queryParam.fp); + opt.set(dom_prefix + 'fingerprint', queryParam.fp || 'chrome'); + opt.set(dom_prefix + 'ech', !!queryParam.ech); + opt.set(dom_prefix + 'ech_config', queryParam.ech || ''); + if (m.hash) { + opt.set('remarks', decodeURIComponent(m.hash.substr(1))); + } + } + if (switch_type) { sessionStorage.setItem("fromUrl", JSON.stringify({ timestamp: Date.now(), diff --git a/luci-app-passwall/po/zh-cn/passwall.po b/luci-app-passwall/po/zh-cn/passwall.po index dafc1bc62..2f054aa56 100644 --- a/luci-app-passwall/po/zh-cn/passwall.po +++ b/luci-app-passwall/po/zh-cn/passwall.po @@ -1355,12 +1355,18 @@ msgstr "是否允许不安全连接。当勾选时,将跳过证书验证。" msgid "%s Node Use Type" msgstr "%s 节点使用类型" +msgid "Other" +msgstr "其他" + msgid "UDP relay mode" msgstr "UDP 中继模式" msgid "Congestion control algorithm" msgstr "拥塞控制算法" +msgid "Keep Alive" +msgstr "保活间隔" + msgid "Heartbeat interval(second)" msgstr "保活心跳包发送间隔(单位:秒)" diff --git a/luci-app-passwall/root/usr/share/passwall/subscribe.lua b/luci-app-passwall/root/usr/share/passwall/subscribe.lua index 356d6f232..1464e4da8 100755 --- a/luci-app-passwall/root/usr/share/passwall/subscribe.lua +++ b/luci-app-passwall/root/usr/share/passwall/subscribe.lua @@ -86,6 +86,7 @@ local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_hysteria2,"hysteria2"},{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_OTHER_TYPE = api.get_core("other_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) local core_has = { ["xray"] = has_xray, ["sing-box"] = has_singbox, @@ -569,6 +570,7 @@ local function processData(szType, content, add_mode, group, sub_cfg) local sub_vmess_type = DEFAULT_VMESS_TYPE local sub_vless_type = DEFAULT_VLESS_TYPE local sub_hysteria2_type = DEFAULT_HYSTERIA2_TYPE + local sub_other_type = DEFAULT_OTHER_TYPE local sub_hy_up_mbps, sub_hy_down_mbps = 1000, 1000 if sub_cfg then if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then @@ -594,6 +596,10 @@ local function processData(szType, content, add_mode, group, sub_cfg) if hysteria2_type ~= "global" and core_has[hysteria2_type] then sub_hysteria2_type = hysteria2_type end + local other_type = sub_cfg.other_type or "global" + if other_type ~= "global" and core_has[other_type] then + sub_other_type = other_type + end sub_hy_up_mbps = sub_cfg.hysteria_up_mbps sub_hy_down_mbps = sub_cfg.hysteria_down_mbps end @@ -1664,6 +1670,115 @@ local function processData(szType, content, add_mode, group, sub_cfg) result.snell_mode = params.mode or "default" end result.snell_reuse = (params.reuse == "1") and "1" or "0" + elseif szType == "wireguard" then + if sub_other_type == "sing-box" and has_singbox then + result.type = "sing-box" + elseif sub_other_type == "xray" and has_xray then + result.type = "Xray" + else + log("跳过 WireGuard 节点,因未安装 WireGuard 核心程序 Xray 或 Sing-box。") + return nil + end + result.protocol = "wireguard" + + local alias = "" + if content:find("#") then + local idx_sp = content:find("#") + alias = content:sub(idx_sp + 1, -1) + content = content:sub(0, idx_sp - 1) + end + result.remarks = UrlDecode(alias) + local contents = split(content, "@") + result.wireguard_secret_key = UrlDecode(contents[1]:gsub("%+", "%%2B")) + local query = split(contents[2], "%?") + local host_port = query[1] + local params = {} + for _, v in pairs(split(query[2], '&')) do + local s = v:find("=", 1, true) + if s and s > 1 then + params[UrlDecode(v:sub(1, s - 1)):lower()] = UrlDecode(v:sub(s + 1):gsub("%+", "%%2B")) + end + end + result.address, result.port = parse_host_port(host_port, "51820") + result.wireguard_public_key = params.publickey + result.wireguard_preSharedKey = params.presharedkey + result.wireguard_local_address = {} + for value in ((params.address or params.ip or "") .. "," .. (params.ipv6 or "")):gmatch("[^,%s]+") do + result.wireguard_local_address[#result.wireguard_local_address + 1] = value + end + result.wireguard_reserved = params.reserved + result.wireguard_mtu = params.mtu or "1420" + result.wireguard_keepAlive = params["persistent-keepalive"] or params.keepalive or "0" + result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil + result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil + elseif szType == "socks" or szType == "socks5" or szType == "socks5h" or szType == "http" or szType == "https" then + if sub_other_type == "sing-box" and has_singbox then + result.type = "sing-box" + elseif sub_other_type == "xray" and has_xray then + result.type = "Xray" + else + log("跳过 " .. szType .. " 节点,因未安装 " .. szType .. " 核心程序 Xray 或 Sing-box。") + return nil + end + result.protocol = (szType == "http" or szType == "https") and "http" or "socks" + + local alias = "" + if content:find("#") then + local idx_sp = content:find("#") + alias = content:sub(idx_sp + 1, -1) + content = content:sub(0, idx_sp - 1) + end + result.remarks = UrlDecode(alias) + local query = split(content, "%?") + local Info = query[1] + local base64_auth = false + if result.protocol == "socks" and not Info:find("@", 1, true) and not Info:find(":", 1, true) then + Info = base64Decode(UrlDecode(Info:gsub("%+", "%%2B"))) + base64_auth = true + end + local auth, host_port = Info:match("^(.*)@(.*)$") + host_port = host_port or Info + auth = auth or "" + if result.protocol == "socks" and not auth:find(":", 1, true) then + auth = base64Decode(UrlDecode(auth:gsub("%+", "%%2B"))) + base64_auth = true + end + local idx = auth:find(":", 1, true) + result.username = idx and auth:sub(1, idx - 1) or auth + result.password = idx and auth:sub(idx + 1) or "" + if not base64_auth then + result.username = UrlDecode(result.username:gsub("%+", "%%2B")) + result.password = UrlDecode(result.password:gsub("%+", "%%2B")) + end + local params = {} + for _, v in pairs(split(query[2], '&')) do + local s = v:find("=", 1, true) + if s and s > 1 then + params[UrlDecode(v:sub(1, s - 1)):lower()] = UrlDecode(v:sub(s + 1):gsub("%+", "%%2B")) + end + end + local port = result.protocol == "socks" and "1080" or (szType == "https" and "443" or "80") + result.address, result.port = parse_host_port(host_port, port) + result.transport = result.type == "Xray" and "raw" or "tcp" + result.uot = params.uot + result.tcp_fast_open = params.tfo + result.tls = szType == "https" and "1" or "0" + if result.tls == "1" then + result.tls_serverName = params.sni + result.alpn = params.alpn + local insecure = params.allowinsecure or params.insecure or params.allow_insecure + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0") + result.tls_pinSHA256 = params.pcs + result.tls_CertByName = params.vcn + if params.fp and params.fp ~= "" then + result.utls = "1" + result.fingerprint = params.fp + end + if params.ech and params.ech ~= "" then + result.ech = "1" + result.ech_config = params.ech + end + end else log("暂时不支持 " .. szType .. " 类型的节点订阅,跳过此节点。") return nil diff --git a/luci-app-passwall2/Makefile b/luci-app-passwall2/Makefile index 9c5ab289a..d8905e8c3 100644 --- a/luci-app-passwall2/Makefile +++ b/luci-app-passwall2/Makefile @@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-passwall2 -PKG_VERSION:=26.11.3 -PKG_RELEASE:=175 +PKG_VERSION:=26.11.11 +PKG_RELEASE:=176 PKG_PO_VERSION:=$(PKG_VERSION) PKG_CONFIG_DEPENDS:= \ diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua index 4b09a5229..662b06e8d 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe.lua @@ -9,6 +9,7 @@ local trojan_type = {} local vmess_type = {} local vless_type = {} local hysteria2_type = {} +local other_type = {} local xray_version = api.get_app_version("xray") if has_ss_rust then local s = "shadowsocks-rust" @@ -21,6 +22,7 @@ if has_singbox then table.insert(vmess_type, s) table.insert(vless_type, s) table.insert(hysteria2_type, s) + table.insert(other_type, s) end if has_xray then local s = "xray" @@ -28,6 +30,7 @@ if has_xray then table.insert(ss_type, s) table.insert(vmess_type, s) table.insert(vless_type, s) + table.insert(other_type, s) if api.compare_versions(xray_version, ">=", "26.1.13") then table.insert(hysteria2_type, s) end @@ -95,7 +98,15 @@ if #hysteria2_type > 0 then end end -if #ss_type > 0 or #trojan_type > 0 or #vmess_type > 0 or #vless_type > 0 or #hysteria2_type > 0 then +if #other_type > 0 then + o = s:option(ListValue, "other_type", translatef("%s Node Use Type", translate("Other"))) + o:value("", translate("Auto")) + for key, value in pairs(other_type) do + o:value(value) + end +end + +if #ss_type > 0 or #trojan_type > 0 or #vmess_type > 0 or #vless_type > 0 or #hysteria2_type > 0 or #other_type > 0 then o.description = string.format("%s", translate("The configured type also applies to the core specified when manually importing nodes.")) end diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua index 36407bdc2..2d5f594ba 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/node_subscribe_config.lua @@ -22,6 +22,7 @@ local trojan_type = {} local vmess_type = {} local vless_type = {} local hysteria2_type = {} +local other_type = {} local xray_version = api.get_app_version("xray") if has_ss_rust then local s = "shadowsocks-rust" @@ -34,6 +35,7 @@ if has_singbox then table.insert(vmess_type, s) table.insert(vless_type, s) table.insert(hysteria2_type, s) + table.insert(other_type, s) end if has_xray then local s = "xray" @@ -41,6 +43,7 @@ if has_xray then table.insert(ss_type, s) table.insert(vmess_type, s) table.insert(vless_type, s) + table.insert(other_type, s) if api.compare_versions(xray_version, ">=", "26.1.13") then table.insert(hysteria2_type, s) end @@ -215,7 +218,18 @@ if #hysteria2_type > 0 then for key, value in pairs(hysteria2_type) do o:value(value) end +end +if #other_type > 0 then + o = s:option(ListValue, "other_type", translatef("%s Node Use Type", translate("Other"))) + o.default = "global" + o:value("global", translate("Use global config")) + for key, value in pairs(other_type) do + o:value(value) + end +end + +if #hysteria2_type > 0 then o = s:option(Value, "hysteria_up_mbps", "Hy/Hy2 " .. translate("Max upload Mbps")) o.datatype = "uinteger" diff --git a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua index d8eb80a77..fe73e27fd 100644 --- a/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua +++ b/luci-app-passwall2/luasrc/model/cbi/passwall2/client/type/1_sing-box.lua @@ -768,6 +768,10 @@ if singbox_tags:find("with_wireguard") then o = s:option(Value, "wireguard_reserved", translate("Reserved"), translate("Decimal numbers separated by \",\" or Base64-encoded strings.")) o:depends({ protocol = "wireguard" }) + + o = s:option(Value, "wireguard_keepAlive", translate("Keep Alive")) + o.default = "0" + o:depends({ protocol = "wireguard" }) end -- [[ TCP ]]-- diff --git a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua index 56b33aa94..c5e030e05 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_sing-box.lua @@ -523,6 +523,7 @@ function gen_outbound(flag, node, tag, proxy_table) pre_shared_key = node.wireguard_preSharedKey, reserved = node.wireguard_reserved, mtu = tonumber(node.wireguard_mtu), + persistent_keepalive_interval = tonumber(node.wireguard_keepAlive), } end @@ -2399,7 +2400,8 @@ function gen_config(var) port = value.server_port, public_key = value.peer_public_key, pre_shared_key = value.pre_shared_key, - allowed_ips = {"0.0.0.0/0"}, + allowed_ips = {"0.0.0.0/0", "::/0"}, + persistent_keepalive_interval = value.persistent_keepalive_interval, reserved = value.reserved } }, diff --git a/luci-app-passwall2/luasrc/passwall2/util_xray.lua b/luci-app-passwall2/luasrc/passwall2/util_xray.lua index 825e86fd9..01c7f568b 100644 --- a/luci-app-passwall2/luasrc/passwall2/util_xray.lua +++ b/luci-app-passwall2/luasrc/passwall2/util_xray.lua @@ -445,7 +445,7 @@ function gen_outbound(flag, node, tag, proxy_table) peers = (node.protocol == "wireguard") and { { publicKey = node.wireguard_public_key, - endpoint = node.address .. ":" .. node.port, + endpoint = (api.is_ipv6(node.address) and ("[" .. node.address .. "]") or node.address) .. ":" .. node.port, preSharedKey = node.wireguard_preSharedKey, keepAlive = node.wireguard_keepAlive and tonumber(node.wireguard_keepAlive) or nil } diff --git a/luci-app-passwall2/luasrc/view/passwall2/node_config/link_share_man.htm b/luci-app-passwall2/luasrc/view/passwall2/node_config/link_share_man.htm index 562f1ab0c..ba9555dd6 100644 --- a/luci-app-passwall2/luasrc/view/passwall2/node_config/link_share_man.htm +++ b/luci-app-passwall2/luasrc/view/passwall2/node_config/link_share_man.htm @@ -13,6 +13,7 @@ local trojan_type = api.get_core("trojan_type", {{has_singbox,"sing-box"},{has_x local vmess_type = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) local vless_type = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) local hysteria2_type = api.get_core("hysteria2_type", {{has_singbox,"sing-box"}, {has_xray,"xray"}}) +local other_type = api.get_core("other_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) local current_node = map:get(section) -%> @@ -25,6 +26,7 @@ local current_node = map:get(section) let vmess_type = "<%=vmess_type%>" let vless_type = "<%=vless_type%>" let hysteria2_type = "<%=hysteria2_type%>" + let other_type = "<%=other_type%>" function padright(str, cnt, pad) { return str + Array(cnt + 1).join(pad); @@ -713,6 +715,74 @@ local current_node = map:get(section) params = params.substring(1); } url += params; + } else if ((v_type === "Xray" || v_type === "sing-box") && opt.get(dom_prefix + "protocol").value === "wireguard") { + protocol = "wireguard"; + var v_password = opt.get(dom_prefix + "wireguard_secret_key"); + var v_port = opt.get(dom_prefix + "port"); + url = encodeURIComponent(v_password.value) + + "@" + _address + + ":" + v_port.value + "?"; + + var params = ""; + params += opt.query("publickey", dom_prefix + "wireguard_public_key"); + params += opt.query("presharedkey", dom_prefix + "wireguard_preSharedKey"); + var addresses = opt.getlist(dom_prefix + "wireguard_local_address").map(value => value.trim()).filter(Boolean); + params += "&address=" + encodeURIComponent(addresses.join(",")); + params += opt.query("reserved", dom_prefix + "wireguard_reserved"); + params += opt.query("mtu", dom_prefix + "wireguard_mtu"); + params += opt.query("persistent-keepalive", dom_prefix + "wireguard_keepAlive"); + if (v_type === "Xray" && opt.get(dom_prefix + "use_finalmask")?.checked) { + params += opt.query("fm", dom_prefix + "finalmask"); + } + params += "#" + encodeURIComponent(v_alias.value); + if (params[0] == "&") { + params = params.substring(1); + } + url += params; + } else if ((v_type === "sing-box" || v_type === "Xray") && opt.get(dom_prefix + "protocol").value === "socks") { + protocol = "socks"; + var v_username = opt.get(dom_prefix + "username"); + var v_password = opt.get(dom_prefix + "password"); + var v_port = opt.get(dom_prefix + "port"); + url = _address + ":" + v_port.value; + if (v_username.value || v_password.value) { + url = b64encutf8safe(v_username.value + ":" + v_password.value) + "@" + url; + } + var params = ""; + params += opt.query("uot", dom_prefix + "uot"); + params += opt.query("tfo", dom_prefix + "tcp_fast_open"); + if (params) { + url += "?" + params.substring(1); + } + url += "#" + encodeURIComponent(v_alias.value); + } else if ((v_type === "sing-box" || v_type === "Xray") && opt.get(dom_prefix + "protocol").value === "http") { + protocol = opt.get(dom_prefix + "tls").checked ? "https" : "http"; + var v_username = opt.get(dom_prefix + "username"); + var v_password = opt.get(dom_prefix + "password"); + var v_port = opt.get(dom_prefix + "port"); + url = _address + ":" + v_port.value; + if (v_username.value || v_password.value) { + url = encodeURIComponent(v_username.value) + ":" + encodeURIComponent(v_password.value) + "@" + url; + } + var params = ""; + if (protocol === "https") { + params += opt.query("sni", dom_prefix + "tls_serverName"); + params += opt.query("alpn", dom_prefix + "alpn"); + params += opt.query("insecure", dom_prefix + "tls_allowInsecure"); + params += opt.query("pcs", dom_prefix + "tls_pinSHA256"); + params += opt.query("vcn", dom_prefix + "tls_CertByName"); + if (opt.get(dom_prefix + "utls")?.checked) { + params += opt.query("fp", dom_prefix + "fingerprint"); + } + if (opt.get(dom_prefix + "ech")?.checked) { + params += opt.query("ech", dom_prefix + "ech_config"); + } + } + params += opt.query("tfo", dom_prefix + "tcp_fast_open"); + if (params) { + url += "?" + params.substring(1); + } + url += "#" + encodeURIComponent(v_alias.value); } if (url) { @@ -1786,6 +1856,126 @@ local current_node = map:get(section) } } + if (link_type === "wireguard") { + if (other_type == "sing-box" && has_singbox) { + dom_prefix = "singbox_"; + opt.set('type', "sing-box"); + } else if (other_type == "xray" && has_xray) { + dom_prefix = "xray_"; + opt.set('type', "Xray"); + } + opt.set(dom_prefix + 'protocol', "wireguard"); + var info = link_split[1].split('@'); + var m = parseNodeUrl("wireguard://" + info[1]); + var host_port = info[1].split(/[/?#]/)[0]; + opt.set(dom_prefix + 'address', unbracketIP(m.hostname)); + opt.set(dom_prefix + 'port', host_port.match(/:(\d+)$/)?.[1] || '51820'); + opt.set(dom_prefix + 'wireguard_secret_key', decodeURIComponent(info[0])); + var queryParam = {}; + if (m.search.length > 1) { + var queryArray = m.search.substr(1).split('&'); + for (var i = 0; i < queryArray.length; i++) { + var index = queryArray[i].indexOf('='); + if (index > 0) { + queryParam[decodeURIComponent(queryArray[i].substr(0, index)).toLowerCase()] = decodeURIComponent(queryArray[i].substr(index + 1)); + } + } + } + opt.set(dom_prefix + 'wireguard_public_key', queryParam.publickey); + opt.set(dom_prefix + 'wireguard_preSharedKey', queryParam.presharedkey || ''); + var addresses = ((queryParam.address || queryParam.ip || '') + ',' + (queryParam.ipv6 || '')).split(/[,\s]+/).filter(Boolean); + var fullName = opt.base + '.' + dom_prefix + 'wireguard_local_address'; + var lists = document.querySelectorAll('.cbi-dynlist'); + for (var i = 0; i < lists.length; i++) { + var label = lists[i].closest('.cbi-value')?.querySelector('label.cbi-value-title'); + if (label?.getAttribute('for') === fullName) { + L.dom.callClassMethod(lists[i], 'setValue', addresses); + lists[i].dispatchEvent(new Event('change', { bubbles: true })); + break; + } + } + opt.set(dom_prefix + 'wireguard_reserved', queryParam.reserved || ''); + opt.set(dom_prefix + 'wireguard_mtu', queryParam.mtu || '1420'); + opt.set(dom_prefix + 'wireguard_keepAlive', queryParam['persistent-keepalive'] || queryParam.keepalive || '0'); + if (dom_prefix === "xray_") { + opt.set(dom_prefix + 'use_finalmask', !!queryParam.fm); + opt.set(dom_prefix + 'finalmask', queryParam.fm || ''); + } + if (m.hash) { + opt.set('remarks', decodeURIComponent(m.hash.substr(1))); + } + } + + if (link_type === "socks" || link_type === "socks5" || link_type === "socks5h" || link_type === "http" || link_type === "https") { + if (other_type == "sing-box" && has_singbox) { + dom_prefix = "singbox_"; + opt.set('type', "sing-box"); + } else if (other_type == "xray" && has_xray) { + dom_prefix = "xray_"; + opt.set('type', "Xray"); + } + var protocol = (link_type === "http" || link_type === "https") ? "http" : "socks"; + opt.set(dom_prefix + 'protocol', protocol); + var content = link_split[1]; + var index = content.search(/[?#]/); + var suffix = index >= 0 ? content.substr(index) : ''; + content = index >= 0 ? content.substr(0, index) : content; + var base64_auth = false; + if (protocol === "socks" && content.indexOf('@') === -1 && content.indexOf(':') === -1) { + content = b64decutf8safe(decodeURIComponent(content)); + base64_auth = true; + } + var auth = ''; + var index = content.lastIndexOf('@'); + if (index >= 0) { + auth = content.substr(0, index); + content = content.substr(index + 1); + } + if (protocol === "socks" && auth.indexOf(':') === -1) { + auth = b64decutf8safe(decodeURIComponent(auth)); + base64_auth = true; + } + var index = auth.indexOf(':'); + var username = index >= 0 ? auth.substr(0, index) : auth; + var password = index >= 0 ? auth.substr(index + 1) : ''; + if (!base64_auth) { + username = decodeURIComponent(username); + password = decodeURIComponent(password); + } + var m = new URL(link_type + "://" + content + suffix); + opt.set(dom_prefix + 'address', unbracketIP(m.hostname)); + opt.set(dom_prefix + 'port', m.port || (protocol === "socks" ? "1080" : (link_type === "https" ? "443" : "80"))); + opt.set(dom_prefix + 'username', username); + opt.set(dom_prefix + 'password', password); + opt.set(dom_prefix + 'transport', dom_prefix === "xray_" ? "raw" : "tcp"); + var queryParam = {}; + if (m.search.length > 1) { + var queryArray = m.search.substr(1).split('&'); + for (var i = 0; i < queryArray.length; i++) { + var index = queryArray[i].indexOf('='); + if (index > 0) { + queryParam[decodeURIComponent(queryArray[i].substr(0, index)).toLowerCase()] = decodeURIComponent(queryArray[i].substr(index + 1)); + } + } + } + opt.set(dom_prefix + 'uot', queryParam.uot === '1'); + opt.set(dom_prefix + 'tcp_fast_open', queryParam.tfo === '1'); + opt.set(dom_prefix + 'tls', link_type === "https"); + opt.set(dom_prefix + 'reality', false); + opt.set(dom_prefix + 'tls_serverName', queryParam.sni || ''); + opt.set(dom_prefix + 'alpn', queryParam.alpn || 'default'); + opt.set(dom_prefix + 'tls_allowInsecure', (queryParam.allowinsecure || queryParam.insecure || queryParam.allow_insecure || '0') === '1'); + opt.set(dom_prefix + 'tls_pinSHA256', queryParam.pcs || ''); + opt.set(dom_prefix + 'tls_CertByName', queryParam.vcn || ''); + opt.set(dom_prefix + 'utls', !!queryParam.fp); + opt.set(dom_prefix + 'fingerprint', queryParam.fp || 'chrome'); + opt.set(dom_prefix + 'ech', !!queryParam.ech); + opt.set(dom_prefix + 'ech_config', queryParam.ech || ''); + if (m.hash) { + opt.set('remarks', decodeURIComponent(m.hash.substr(1))); + } + } + if (switch_type) { sessionStorage.setItem("fromUrl", JSON.stringify({ timestamp: Date.now(), diff --git a/luci-app-passwall2/po/fa/passwall2.po b/luci-app-passwall2/po/fa/passwall2.po index cd5dd963a..2b9994f1c 100644 --- a/luci-app-passwall2/po/fa/passwall2.po +++ b/luci-app-passwall2/po/fa/passwall2.po @@ -604,6 +604,9 @@ msgstr "پنجره دریافت اتصال QUIC" msgid "QUIC concurrent bidirectional streams" msgstr "جریان‌های دوطرفه همزمان QUIC" +msgid "Keep Alive" +msgstr "زنده نگه داشتن اتصال" + msgid "QUIC KeepAlive interval" msgstr "فاصله QUIC KeepAlive" @@ -1369,6 +1372,9 @@ msgstr "زمانی استفاده می‌شود که لینک گره شامل ا msgid "%s Node Use Type" msgstr "نوع استفاده گره %s" +msgid "Other" +msgstr "سایر" + msgid "UDP relay mode" msgstr "حالت بازپخش UDP" diff --git a/luci-app-passwall2/po/ru/passwall2.po b/luci-app-passwall2/po/ru/passwall2.po index 11b6f90f5..97b3487f7 100644 --- a/luci-app-passwall2/po/ru/passwall2.po +++ b/luci-app-passwall2/po/ru/passwall2.po @@ -605,6 +605,9 @@ msgstr "Размер окна приёма соединения QUIC" msgid "QUIC concurrent bidirectional streams" msgstr "Макс. количество одновременных двунаправленных потоков QUIC" +msgid "Keep Alive" +msgstr "Поддержание соединения" + msgid "QUIC KeepAlive interval" msgstr "Интервал QUIC KeepAlive" @@ -1370,6 +1373,9 @@ msgstr "Используется, если в ссылке на узел это msgid "%s Node Use Type" msgstr "Тип использования сервера %s" +msgid "Other" +msgstr "Другие" + msgid "UDP relay mode" msgstr "Режим ретрансляции UDP" diff --git a/luci-app-passwall2/po/zh-cn/passwall2.po b/luci-app-passwall2/po/zh-cn/passwall2.po index f0f67cde2..9dff3ec0d 100644 --- a/luci-app-passwall2/po/zh-cn/passwall2.po +++ b/luci-app-passwall2/po/zh-cn/passwall2.po @@ -596,6 +596,9 @@ msgstr "QUIC 连接接收窗口" msgid "QUIC concurrent bidirectional streams" msgstr "QUIC 并发双向流的最大数量" +msgid "Keep Alive" +msgstr "保持连接" + msgid "QUIC KeepAlive interval" msgstr "QUIC KeepAlive 间隔" @@ -1361,6 +1364,9 @@ msgstr "当节点链接未包含该参数时,将使用此设置。" msgid "%s Node Use Type" msgstr "%s 节点使用类型" +msgid "Other" +msgstr "其他" + msgid "UDP relay mode" msgstr "UDP中继模式" diff --git a/luci-app-passwall2/po/zh-tw/passwall2.po b/luci-app-passwall2/po/zh-tw/passwall2.po index 0d60d4425..a0c9488b4 100644 --- a/luci-app-passwall2/po/zh-tw/passwall2.po +++ b/luci-app-passwall2/po/zh-tw/passwall2.po @@ -596,6 +596,9 @@ msgstr "QUIC 連接接收窗口" msgid "QUIC concurrent bidirectional streams" msgstr "QUIC 並發双向流的最大數量" +msgid "Keep Alive" +msgstr "保持連線" + msgid "QUIC KeepAlive interval" msgstr "QUIC KeepAlive 間隔" @@ -1361,6 +1364,9 @@ msgstr "當節點連結未包含該參數時,將使用此設定。" msgid "%s Node Use Type" msgstr "%s 節點使用類型" +msgid "Other" +msgstr "其他" + msgid "UDP relay mode" msgstr "UDP中继模式" diff --git a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua index 3cc13a78f..fdf27e82a 100755 --- a/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua +++ b/luci-app-passwall2/root/usr/share/passwall2/subscribe.lua @@ -100,6 +100,7 @@ local DEFAULT_TROJAN_TYPE = api.get_core("trojan_type", {{has_singbox,"sing-box" local DEFAULT_VMESS_TYPE = api.get_core("vmess_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) local DEFAULT_VLESS_TYPE = api.get_core("vless_type", {{has_xray,"xray"},{has_singbox,"sing-box"}}) local DEFAULT_HYSTERIA2_TYPE = api.get_core("hysteria2_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) +local DEFAULT_OTHER_TYPE = api.get_core("other_type", {{has_singbox,"sing-box"},{has_xray,"xray"}}) local core_has = { ["xray"] = has_xray, ["sing-box"] = has_singbox, @@ -536,6 +537,7 @@ local function processData(szType, content, add_mode, group, sub_cfg) local sub_vmess_type = DEFAULT_VMESS_TYPE local sub_vless_type = DEFAULT_VLESS_TYPE local sub_hysteria2_type = DEFAULT_HYSTERIA2_TYPE + local sub_other_type = DEFAULT_OTHER_TYPE local sub_hy_up_mbps, sub_hy_down_mbps if sub_cfg then if sub_cfg.allowInsecure and sub_cfg.allowInsecure ~= "1" then @@ -561,6 +563,10 @@ local function processData(szType, content, add_mode, group, sub_cfg) if hysteria2_type ~= "global" and core_has[hysteria2_type] then sub_hysteria2_type = hysteria2_type end + local other_type = sub_cfg.other_type or "global" + if other_type ~= "global" and core_has[other_type] then + sub_other_type = other_type + end sub_hy_up_mbps = sub_cfg.hysteria_up_mbps sub_hy_down_mbps = sub_cfg.hysteria_down_mbps end @@ -1622,6 +1628,115 @@ local function processData(szType, content, add_mode, group, sub_cfg) result.snell_mode = params.mode or "default" end result.snell_reuse = (params.reuse == "1") and "1" or "0" + elseif szType == "wireguard" then + if sub_other_type == "sing-box" and has_singbox then + result.type = "sing-box" + elseif sub_other_type == "xray" and has_xray then + result.type = "Xray" + else + log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", "WireGuard", "Xray / sing-box")) + return nil + end + result.protocol = "wireguard" + + local alias = "" + if content:find("#") then + local idx_sp = content:find("#") + alias = content:sub(idx_sp + 1, -1) + content = content:sub(0, idx_sp - 1) + end + result.remarks = UrlDecode(alias) + local contents = split(content, "@") + result.wireguard_secret_key = UrlDecode(contents[1]:gsub("%+", "%%2B")) + local query = split(contents[2], "%?") + local host_port = query[1] + local params = {} + for _, v in pairs(split(query[2], '&')) do + local s = v:find("=", 1, true) + if s and s > 1 then + params[UrlDecode(v:sub(1, s - 1)):lower()] = UrlDecode(v:sub(s + 1):gsub("%+", "%%2B")) + end + end + result.address, result.port = parse_host_port(host_port, "51820") + result.wireguard_public_key = params.publickey + result.wireguard_preSharedKey = params.presharedkey + result.wireguard_local_address = {} + for value in ((params.address or params.ip or "") .. "," .. (params.ipv6 or "")):gmatch("[^,%s]+") do + result.wireguard_local_address[#result.wireguard_local_address + 1] = value + end + result.wireguard_reserved = params.reserved + result.wireguard_mtu = params.mtu or "1420" + result.wireguard_keepAlive = params["persistent-keepalive"] or params.keepalive or "0" + result.use_finalmask = (params.fm and params.fm ~= "") and "1" or nil + result.finalmask = (params.fm and params.fm ~= "") and api.base64Encode(params.fm) or nil + elseif szType == "socks" or szType == "socks5" or szType == "socks5h" or szType == "http" or szType == "https" then + if sub_other_type == "sing-box" and has_singbox then + result.type = "sing-box" + elseif sub_other_type == "xray" and has_xray then + result.type = "Xray" + else + log(2, i18n.translatef("Skipping the %s node is due to incompatibility with the %s core program or incorrect node usage type settings.", szType, "Xray / sing-box")) + return nil + end + result.protocol = (szType == "http" or szType == "https") and "http" or "socks" + + local alias = "" + if content:find("#") then + local idx_sp = content:find("#") + alias = content:sub(idx_sp + 1, -1) + content = content:sub(0, idx_sp - 1) + end + result.remarks = UrlDecode(alias) + local query = split(content, "%?") + local Info = query[1] + local base64_auth = false + if result.protocol == "socks" and not Info:find("@", 1, true) and not Info:find(":", 1, true) then + Info = base64Decode(UrlDecode(Info:gsub("%+", "%%2B"))) + base64_auth = true + end + local auth, host_port = Info:match("^(.*)@(.*)$") + host_port = host_port or Info + auth = auth or "" + if result.protocol == "socks" and not auth:find(":", 1, true) then + auth = base64Decode(UrlDecode(auth:gsub("%+", "%%2B"))) + base64_auth = true + end + local idx = auth:find(":", 1, true) + result.username = idx and auth:sub(1, idx - 1) or auth + result.password = idx and auth:sub(idx + 1) or "" + if not base64_auth then + result.username = UrlDecode(result.username:gsub("%+", "%%2B")) + result.password = UrlDecode(result.password:gsub("%+", "%%2B")) + end + local params = {} + for _, v in pairs(split(query[2], '&')) do + local s = v:find("=", 1, true) + if s and s > 1 then + params[UrlDecode(v:sub(1, s - 1)):lower()] = UrlDecode(v:sub(s + 1):gsub("%+", "%%2B")) + end + end + local port = result.protocol == "socks" and "1080" or (szType == "https" and "443" or "80") + result.address, result.port = parse_host_port(host_port, port) + result.transport = result.type == "Xray" and "raw" or "tcp" + result.uot = params.uot + result.tcp_fast_open = params.tfo + result.tls = szType == "https" and "1" or "0" + if result.tls == "1" then + result.tls_serverName = params.sni + result.alpn = params.alpn + local insecure = params.allowinsecure or params.insecure or params.allow_insecure + result.tls_allowInsecure = (insecure == "1" or insecure == "0") and insecure or (sub_allowinsecure and "1" or "0") + result.tls_pinSHA256 = params.pcs + result.tls_CertByName = params.vcn + if params.fp and params.fp ~= "" then + result.utls = "1" + result.fingerprint = params.fp + end + if params.ech and params.ech ~= "" then + result.ech = "1" + result.ech_config = params.ech + end + end else log(2, i18n.translatef("%s type node subscriptions are not currently supported, skip this node.", szType)) return nil diff --git a/luci-app-trafficctl/Makefile b/luci-app-trafficctl/Makefile index 03a7b8b9b..9d7bccddd 100644 --- a/luci-app-trafficctl/Makefile +++ b/luci-app-trafficctl/Makefile @@ -1,8 +1,8 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-app-trafficctl -PKG_VERSION:=1.30.0 -PKG_RELEASE:=27 +PKG_VERSION:=1.30.1 +PKG_RELEASE:=28 PKG_LICENSE:=Apache-2.0 PKG_MAINTAINER:=Denis Iusupov diff --git a/luci-app-v2raya/Makefile b/luci-app-v2raya/Makefile index 4c1fa9e51..66640668e 100644 --- a/luci-app-v2raya/Makefile +++ b/luci-app-v2raya/Makefile @@ -11,8 +11,6 @@ LUCI_DEPENDS:=+luci-base +v2raya LUCI_PKGARCH:=all LUCI_MAINTAINER:=Amr Fateem -PKG_VERSION:=2.5.13 -PKG_RELEASE:=1 PKG_LICENSE:=Apache-2.0 CONFLICTS:=luci-app-v2raya-nextgen diff --git a/luci-base/root/usr/share/rpcd/ucode/luci b/luci-base/root/usr/share/rpcd/ucode/luci index 84d7e9584..6ae684a67 100644 --- a/luci-base/root/usr/share/rpcd/ucode/luci +++ b/luci-base/root/usr/share/rpcd/ucode/luci @@ -13,12 +13,33 @@ import { connect } from 'ubus'; import timezones from 'luci.zoneinfo'; +// Cache board identity when rpcd loads this provider, not on every poll. +const sr1010_build = trim(readfile('/tmp/sysinfo/board_name') || '') == 'zte,zx279133-sr1010' && + match(readfile('/etc/openwrt_release') || '', /DISTRIB_TARGET=['"]zte\/zx279133['"]/) != null; + +function cpu_usage_result(cpuusage) { + let result = { cpuusage }; + if (sr1010_build) { + // One fixed-size kernel summary; never enumerate flows or Conntrack. + const summary = readfile('/proc/net/nf_flowtable_summary') || ''; + const hw = match(summary, /(^|[ \t])hw=([0-9]+)([ \t\r\n]|$)/); + const sw = match(summary, /(^|[ \t])sw=([0-9]+)([ \t\r\n]|$)/); + result.flows = { hw: hw ? +hw[2] : 0, sw: sw ? +sw[2] : 0 }; + } + return result; +} + function shellquote(s) { return `'${replace(s, "'", "'\\''")}'`; } -function has_supported_hw_offloading_target(target) { +function has_supported_hw_offloading_target(target, board) { + if (target == 'zte/zx279133' && board == 'zte,zx279133-sr1010') + return access('/sys/module/npu') == true && + access('/sys/module/fnoffload') == true && + length(glob('/sys/bus/platform/drivers/zx279133-eth/*/driver')) > 0; + if (target == 'ipq40xx/generic') return access('/sys/module/qca_ssdk') == true && access('/sys/module/essedma') == true; @@ -241,7 +262,7 @@ const methods = { const has_mt7621_vendor_hnat = sysboard?.release?.target == 'ramips/mt7621' && access('/sys/module/mtkhnat') == true; const has_hw_offloading = !has_mt7621_vendor_hnat && - has_supported_hw_offloading_target(sysboard?.release?.target) && + has_supported_hw_offloading_target(sysboard?.release?.target, sysboard?.board_name) && ((has_fw3 && has_ipt_flowoffload) || (has_fw4 && has_nft_flowoffload)); if (ubus) @@ -255,6 +276,8 @@ const methods = { mii_tool: access('/usr/sbin/mii-tool'), offloading: (has_fw3 && has_ipt_flowoffload) || (has_fw4 && has_nft_flowoffload), offloading_hw: has_hw_offloading, + zte_npu: sysboard?.release?.target == 'zte/zx279133' && + sysboard?.board_name == 'zte,zx279133-sr1010', fullcone: access('/sys/module/xt_FULLCONENAT/refcnt') == true || access('/sys/module/nft_fullcone/refcnt') == true, br2684ctl: access('/usr/sbin/br2684ctl') == true, swconfig: access('/sbin/swconfig') == true, @@ -774,18 +797,18 @@ const methods = { fd.close(); if (length(cpuusage)) - return { cpuusage }; + return cpu_usage_result(cpuusage); } } const fd = popen('top -n1 | awk \'/^CPU/ {printf("%d%", 100 - $8)}\''); if (!fd) - return { cpuusage: error() }; + return cpu_usage_result(error()); let cpuusage = trim(fd.read('all')) || '?'; fd.close(); - return { cpuusage }; + return cpu_usage_result(cpuusage); } }, diff --git a/luci-mod-status/Makefile b/luci-mod-status/Makefile index 28f93e97d..3029d6065 100644 --- a/luci-mod-status/Makefile +++ b/luci-mod-status/Makefile @@ -9,7 +9,7 @@ include $(TOPDIR)/rules.mk LUCI_TITLE:=LuCI Status Pages LUCI_DEPENDS:=+luci-base +luci-app-oui +luci-app-vantage +libiwinfo +rpcd-mod-iwinfo +rpcd-mod-rrdns +ucode +ucode-mod-fs +ucode-mod-ubus +ucode-mod-socket -PKG_RELEASE:=43 +PKG_RELEASE:=44 PKG_BUILD_DEPENDS:=iwinfo PKG_LICENSE:=Apache-2.0 diff --git a/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js b/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js index 6c333a74d..17ea793a0 100644 --- a/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js +++ b/luci-mod-status/htdocs/luci-static/resources/view/status/include/10_system.js @@ -67,6 +67,11 @@ return baseclass.extend({ luciversion = luciversion.branch; + var cpuUsageText = cpuusage.cpuusage; + if (L.hasSystemFeature('zte_npu')) + cpuUsageText = (cpuUsageText || '?') + ', NPU: ' + (cpuusage.flows?.hw || 0) + + ', Flow: ' + (cpuusage.flows?.sw || 0); + var datestr = null; if (unixtime) { diff --git a/luci-theme-vantage/Makefile b/luci-theme-vantage/Makefile index 0b068051c..4edf42877 100644 --- a/luci-theme-vantage/Makefile +++ b/luci-theme-vantage/Makefile @@ -11,7 +11,7 @@ include $(TOPDIR)/rules.mk PKG_NAME:=luci-theme-vantage PKG_VERSION:=1.0.17 -PKG_RELEASE:=39 +PKG_RELEASE:=40 PKG_LICENSE:=GPL-3.0-or-later LUCI_TITLE:=Vantage theme diff --git a/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css b/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css index 6c904b705..7cebbfff7 100644 --- a/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css +++ b/luci-theme-vantage/htdocs/luci-static/vantage/vantage.css @@ -2656,7 +2656,7 @@ html:not([data-darkmode="true"]) .vt-login-page::before { .vt-login-head { display: flex; align-items: center; gap: var(--sp-3); margin-bottom: var(--sp-6); } .vt-login-mark { flex: none; } .vt-login-title { margin: 0; font-size: var(--fs-lg); font-weight: 600; letter-spacing: -.01em; overflow-wrap: anywhere; } -.vt-login-error { +.vt-login-error, .vt-login-info { margin: 0 0 var(--sp-4); padding: var(--sp-2) var(--sp-3); border: 1px solid color-mix(in srgb, var(--err) 45%, transparent); @@ -2665,6 +2665,11 @@ html:not([data-darkmode="true"]) .vt-login-page::before { border-radius: var(--r); font-size: var(--fs-sm); } +.vt-login-info { + border-color: color-mix(in srgb, var(--info) 45%, transparent); + border-left-color: var(--info); + background: var(--info-soft); +} .vt-login-form { display: flex; flex-direction: column; gap: var(--sp-4); } .vt-field { display: flex; flex-direction: column; gap: 6px; } .vt-field-label { color: var(--text-3); font-size: var(--fs-micro); font-weight: 600; letter-spacing: .08em; text-transform: uppercase; } diff --git a/luci-theme-vantage/po/templates/vantage-theme.pot b/luci-theme-vantage/po/templates/vantage-theme.pot index e8fc632c0..243a9c1f2 100644 --- a/luci-theme-vantage/po/templates/vantage-theme.pot +++ b/luci-theme-vantage/po/templates/vantage-theme.pot @@ -76,7 +76,7 @@ msgctxt "vantage-theme" msgid "Appearance" msgstr "" -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:50 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:52 msgid "Authorization Required" msgstr "" @@ -296,7 +296,7 @@ msgctxt "vantage-theme" msgid "Input" msgstr "" -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:54 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:56 msgid "Invalid username and/or password! Please try again." msgstr "" @@ -314,8 +314,8 @@ msgctxt "vantage-theme" msgid "Light" msgstr "" -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:30 -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:66 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:32 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:85 msgid "Log in" msgstr "" @@ -441,7 +441,7 @@ msgctxt "vantage-theme" msgid "Passes unmatched" msgstr "" -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:63 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:65 msgid "Password" msgstr "" @@ -590,7 +590,7 @@ msgstr "" msgid "Used" msgstr "" -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:59 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:61 msgid "Username" msgstr "" diff --git a/luci-theme-vantage/po/zh_Hans/vantage-theme.po b/luci-theme-vantage/po/zh_Hans/vantage-theme.po index 75fa63de0..e4abca25b 100644 --- a/luci-theme-vantage/po/zh_Hans/vantage-theme.po +++ b/luci-theme-vantage/po/zh_Hans/vantage-theme.po @@ -82,7 +82,7 @@ msgctxt "vantage-theme" msgid "Appearance" msgstr "外观" -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:50 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:52 msgid "Authorization Required" msgstr "需要授权" @@ -307,7 +307,7 @@ msgctxt "vantage-theme" msgid "Input" msgstr "输入" -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:54 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:56 msgid "Invalid username and/or password! Please try again." msgstr "用户名或密码错误,请重试。" @@ -325,8 +325,8 @@ msgctxt "vantage-theme" msgid "Light" msgstr "浅色" -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:30 -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:66 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:32 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:85 msgid "Log in" msgstr "登录" @@ -452,7 +452,7 @@ msgctxt "vantage-theme" msgid "Passes unmatched" msgstr "放行未匹配流量" -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:63 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:65 msgid "Password" msgstr "密码" @@ -600,7 +600,7 @@ msgstr "使用浅色主题" msgid "Used" msgstr "已用" -#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:59 +#: luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut:61 msgid "Username" msgstr "用户名" diff --git a/luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut b/luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut index b2d1a6b04..14576d314 100644 --- a/luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut +++ b/luci-theme-vantage/ucode/template/themes/vantage/sysauth.ut @@ -19,6 +19,8 @@ const look_theme = look('theme', [ 'auto', 'light', 'dark' ]); const look_login = look('login', [ 'grid', 'stars', 'none' ]); const host = (() => { const h = uci.get_first('system', 'system', 'hostname'); return (type(h) == 'string' && length(h)) ? substr(h, 0, 63) : null; })(); + // A pending challenge is guidance until the user submits a response. + const auth_failed = auth_plugin && length(filter(auth_fields ?? [], field => length(http.formvalue(field.name) ?? ''))); -%} @@ -57,27 +59,44 @@

{{ entityencode(host ?? _('Authorization Required'), true) }}

-{% if (fuser): %} +{% if (fuser && !auth_plugin): %} {% endif %}