mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-13 20:08:05 +08:00
✨ Sync 2026-11-11 01:22:09
This commit is contained in:
1 parent
9c5aa2e662
commit
cc21462d8a
48 files changed
+9760
-59
No files matched your search
+6
-2
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
|
||||
+387
-11
@@ -16,11 +16,12 @@
|
||||
#include <unistd.h>
|
||||
|
||||
#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;
|
||||
}
|
||||
@@ -14,8 +14,8 @@ PKG_LICENSE:=Apache-2.0
|
||||
PKG_MAINTAINER:=JiaY-shi <shi05275@163.com>
|
||||
|
||||
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
|
||||
@@ -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');
|
||||
|
||||
@@ -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 <shi05275@163.com>\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 "
|
||||
|
||||
@@ -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'));
|
||||
|
||||
@@ -7,7 +7,7 @@ include $(TOPDIR)/rules.mk
|
||||
PKG_MAINTAINER:=Thaolga <https://github.com/Thaolga/luci-app-nekobox>
|
||||
PKG_NAME:=luci-app-nekobox
|
||||
PKG_VERSION:=2.0.9
|
||||
PKG_RELEASE:=8
|
||||
PKG_RELEASE:=10
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/$(PKG_NAME)
|
||||
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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:= \
|
||||
|
||||
@@ -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("<font color='red'>%s</font>",
|
||||
translate("The configured type also applies to the core specified when manually importing nodes."))
|
||||
end
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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部分(模拟) ]]--
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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 "保活心跳包发送间隔(单位:秒)"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:= \
|
||||
|
||||
@@ -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("<font color='red'>%s</font>",
|
||||
translate("The configured type also applies to the core specified when manually importing nodes."))
|
||||
end
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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 ]]--
|
||||
|
||||
@@ -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
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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中继模式"
|
||||
|
||||
|
||||
@@ -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中继模式"
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 <yusdyr@gmail.com>
|
||||
|
||||
@@ -11,8 +11,6 @@ LUCI_DEPENDS:=+luci-base +v2raya
|
||||
LUCI_PKGARCH:=all
|
||||
LUCI_MAINTAINER:=Amr Fateem <amr.fateem@yahoo.com>
|
||||
|
||||
PKG_VERSION:=2.5.13
|
||||
PKG_RELEASE:=1
|
||||
|
||||
PKG_LICENSE:=Apache-2.0
|
||||
CONFLICTS:=luci-app-v2raya-nextgen
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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 ""
|
||||
|
||||
|
||||
@@ -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 "用户名"
|
||||
|
||||
|
||||
@@ -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) ?? '')));
|
||||
-%}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ entityencode(dispatcher.lang ?? 'en', true) }}" data-darkmode="true" data-vt-look-theme="{{ look_theme }}" data-vt-look-login="{{ look_login }}">
|
||||
@@ -57,27 +59,44 @@
|
||||
<h1 class="vt-login-title">{{ entityencode(host ?? _('Authorization Required'), true) }}</h1>
|
||||
</div>
|
||||
|
||||
{% if (fuser): %}
|
||||
{% if (fuser && !auth_plugin): %}
|
||||
<div class="vt-login-error" role="alert">{{ entityencode(_('Invalid username and/or password! Please try again.'), true) }}</div>
|
||||
{% endif %}
|
||||
|
||||
<form method="post" class="vt-login-form" data-https-ports="{{ entityencode(sprintf('%J', tls_ports), true) }}" data-https-probe="{{ entityencode(resource + '/icons/loading.svg', true) }}">
|
||||
<label class="vt-field">
|
||||
<label class="vt-field" for="luci_username">
|
||||
<span class="vt-field-label">{{ entityencode(_('Username'), true) }}</span>
|
||||
<input class="cbi-input-text" type="text" name="luci_username" autocomplete="username" autocapitalize="off" spellcheck="false" required value="{{ entityencode(duser ?? '', true) }}">
|
||||
<input class="cbi-input-text" id="luci_username" type="text" name="luci_username" autocomplete="username" autocapitalize="off" spellcheck="false" required value="{{ entityencode(duser ?? '', true) }}">
|
||||
</label>
|
||||
<label class="vt-field">
|
||||
<label class="vt-field" for="luci_password">
|
||||
<span class="vt-field-label">{{ entityencode(_('Password'), true) }}</span>
|
||||
<input class="cbi-input-password" type="password" name="luci_password" autocomplete="current-password" autofocus>
|
||||
<input class="cbi-input-password" id="luci_password" type="password" name="luci_password" autocomplete="current-password">
|
||||
</label>
|
||||
{% if (auth_fields): %}
|
||||
{% for (let field in auth_fields): %}
|
||||
<label class="vt-field" for="{{ entityencode(field.name, true) }}">
|
||||
<span class="vt-field-label">{{ entityencode(_(field.label ?? field.name), true) }}</span>
|
||||
<input class="cbi-input-text" id="{{ entityencode(field.name, true) }}" type="{{ entityencode(field.type ?? 'text', true) }}" name="{{ entityencode(field.name, true) }}"
|
||||
{% if (field.placeholder): %}placeholder="{{ entityencode(field.placeholder, true) }}"{% endif %}
|
||||
{% if (field.inputmode): %}inputmode="{{ entityencode(field.inputmode, true) }}"{% endif %}
|
||||
{% if (field.pattern): %}pattern="{{ entityencode(field.pattern, true) }}"{% endif %}
|
||||
{% if (field.maxlength): %}maxlength="{{ entityencode('' + field.maxlength, true) }}"{% endif %}
|
||||
{% if (field.autocomplete): %}autocomplete="{{ entityencode(field.autocomplete, true) }}"{% endif %}
|
||||
{% if (field.required): %}required{% endif %}>
|
||||
</label>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
{% if (auth_message): %}
|
||||
<div class="{{ auth_failed ? 'vt-login-error' : 'vt-login-info' }}" role="{{ auth_failed ? 'alert' : 'status' }}">{{ entityencode(auth_message, true) }}</div>
|
||||
{% endif %}
|
||||
<button type="submit" class="btn cbi-button cbi-button-apply vt-login-submit">{{ entityencode(_('Log in'), true) }}</button>
|
||||
</form>
|
||||
</div>
|
||||
</main>
|
||||
<script>
|
||||
(function (form) {
|
||||
var pw = document.querySelector('input[name="luci_password"]');
|
||||
if (pw) pw.focus();
|
||||
var input = form && form.querySelector('input[name="luci_password"]');
|
||||
if (input) input.focus();
|
||||
if (!form || location.protocol === 'https:') return;
|
||||
var ports = [];
|
||||
try { ports = JSON.parse(form.getAttribute('data-https-ports') || '[]'); } catch (e) {}
|
||||
|
||||
+190
@@ -0,0 +1,190 @@
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#
|
||||
# Copyright (C) 2021 ImmortalWrt.org
|
||||
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
|
||||
#
|
||||
# v2rayA OpenWrt packaging.
|
||||
#
|
||||
# The structure follows the packaging work by Tianling Shen
|
||||
# <cnsztl@immortalwrt.org> in the v2raya-openwrt feed and the OpenWrt
|
||||
# packages feed, updated for the 2.5.x layout where the service and the
|
||||
# matching v2raya_core binary are separate Go modules.
|
||||
|
||||
include $(TOPDIR)/rules.mk
|
||||
|
||||
PKG_NAME:=v2raya
|
||||
PKG_VERSION:=2.5.14
|
||||
PKG_RELEASE:=3
|
||||
|
||||
PKG_SOURCE:=v2rayA-$(PKG_VERSION).tar.gz
|
||||
PKG_SOURCE_URL:=https://codeload.github.com/v2rayA/v2rayA/tar.gz/v$(PKG_VERSION)?
|
||||
PKG_HASH:=skip
|
||||
|
||||
PKG_BUILD_DIR:=$(BUILD_DIR)/v2rayA-$(PKG_VERSION)/service
|
||||
|
||||
PKG_LICENSE:=AGPL-3.0-only
|
||||
PKG_LICENSE_FILES:=../LICENSE
|
||||
PKG_MAINTAINER:=Amr Fateem <amr.fateem@yahoo.com>
|
||||
|
||||
PKG_BUILD_DEPENDS:=golang/host
|
||||
PKG_BUILD_PARALLEL:=1
|
||||
PKG_BUILD_FLAGS:=no-mips16
|
||||
|
||||
# v2rayA 2.5.x needs Go 1.26+ and v2raya_core needs Go 1.27+. The host Go
|
||||
# shipped by older OpenWrt releases is older than that, so let GOTOOLCHAIN
|
||||
# fetch the matching toolchain from proxy.golang.org during the build.
|
||||
# scripts/update-version.sh refreshes this after a major Go bump.
|
||||
V2RAYA_GO_TOOLCHAIN?=go1.27.3+auto
|
||||
|
||||
GO_PKG:=github.com/v2rayA/v2rayA
|
||||
GO_PKG_BUILD_PKG:=github.com/v2rayA/v2rayA
|
||||
GO_PKG_LDFLAGS_X:= \
|
||||
$(GO_PKG)/conf.Version=$(PKG_VERSION) \
|
||||
$(GO_PKG)/kernel/iptables.TproxyNotSkipBr=true
|
||||
|
||||
include $(INCLUDE_DIR)/package.mk
|
||||
include $(TOPDIR)/feeds/packages/lang/golang/golang-package.mk
|
||||
|
||||
# v2rayA's SQLite layer has no pure-Go driver for mips* or loong64. Those
|
||||
# targets must use cgo (and therefore the target C toolchain); all other
|
||||
# targets get fully static, CGO-free binaries so one build runs on every
|
||||
# OpenWrt release with the same CPU architecture.
|
||||
V2RAYA_CGO:=0
|
||||
ifneq ($(filter mips% loong64,$(GO_ARCH)),)
|
||||
V2RAYA_CGO:=1
|
||||
endif
|
||||
|
||||
GO_PKG_TARGET_VARS:=$(filter-out CGO_ENABLED=%,$(GO_PKG_TARGET_VARS)) CGO_ENABLED=$(V2RAYA_CGO)
|
||||
ifeq ($(V2RAYA_CGO),0)
|
||||
GO_PKG_DEFAULT_LDFLAGS:=$(filter-out -linkmode external,$(GO_PKG_DEFAULT_LDFLAGS))
|
||||
endif
|
||||
GO_PKG_BUILD_VARS:=$(filter-out GOTOOLCHAIN=%,$(GO_PKG_BUILD_VARS)) GOTOOLCHAIN=$(V2RAYA_GO_TOOLCHAIN)
|
||||
|
||||
TAR_OPTIONS+= --strip-components 1
|
||||
TAR_CMD=$(HOST_TAR) -C $(1)/.. $(TAR_OPTIONS)
|
||||
|
||||
# geoip.dat and geosite.dat are intentionally not hard dependencies: v2rayA
|
||||
# downloads missing files on first start. Install v2raya-geoip and
|
||||
# v2raya-geosite for offline/package-managed data.
|
||||
define Package/v2raya
|
||||
TITLE:=v2rayA - web client for its own Xray-based core
|
||||
SECTION:=net
|
||||
CATEGORY:=Network
|
||||
SUBMENU:=Web Servers/Proxies
|
||||
DEPENDS:=$(GO_ARCH_DEPENDS) \
|
||||
+v2raya-core \
|
||||
+ca-bundle \
|
||||
+nftables \
|
||||
+kmod-nft-tproxy \
|
||||
+ip-full \
|
||||
+kmod-tun
|
||||
CONFLICTS:=v2raya-nextgen
|
||||
URL:=https://github.com/v2rayA/v2rayA
|
||||
endef
|
||||
|
||||
define Package/v2raya/description
|
||||
v2rayA is a web client for its own Xray-based core. It imports
|
||||
subscriptions and share links, selects the fastest node, and provides
|
||||
transparent proxy on Linux.
|
||||
|
||||
This package tracks the current upstream 2.5.x release. It installs the
|
||||
service together with the matching v2raya_core binary.
|
||||
endef
|
||||
|
||||
define Package/v2raya/conffiles
|
||||
/etc/config/v2raya
|
||||
endef
|
||||
|
||||
define Package/v2raya/postinst
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
mkdir -p /etc/v2raya /var/log/v2raya
|
||||
chmod 750 /etc/v2raya
|
||||
chmod 755 /var/log/v2raya
|
||||
/etc/init.d/v2raya enable
|
||||
if [ "$$(uci -q get v2raya.config.enabled)" = "1" ]; then
|
||||
/etc/init.d/v2raya restart
|
||||
fi
|
||||
}
|
||||
endef
|
||||
|
||||
define Package/v2raya/prerm
|
||||
#!/bin/sh
|
||||
[ -n "$${IPKG_INSTROOT}" ] || {
|
||||
[ "$$1" = "upgrade" ] && exit 0
|
||||
/etc/init.d/v2raya stop
|
||||
/etc/init.d/v2raya disable
|
||||
}
|
||||
endef
|
||||
|
||||
# v2rayA publishes the built web UI as web.tar.gz on its releases. The
|
||||
# GitHub release CDN has been returning 504/404 for it, which makes every
|
||||
# matrix build fail. Keep the 871 KiB asset in the package instead; the
|
||||
# hash of the upstream v2.5.13 asset is
|
||||
# 76774f195ee74604fbc43a48fe74e5f3363e81ec68af02d1c26feeeacc0f63ae.
|
||||
|
||||
define Build/Prepare
|
||||
$(call Build/Prepare/Default)
|
||||
|
||||
# OpenWrt's LAN is a bridge (br-lan). v2rayA 2.5.x defaults to excluding
|
||||
# br-* from transparent proxy, which silently bypasses every LAN client.
|
||||
# The old OpenWrt package removed that exclusion with TproxyNotSkipBr;
|
||||
# 2.5.x moved it into this stored default, so strip it here as well.
|
||||
$(SED) 's/,br-\*"/"/' $(PKG_BUILD_DIR)/db/configure/setting.go
|
||||
|
||||
# v2rayA 2.2.7 (the old OpenWrt package) did not hijack DNS. 2.5.x turns
|
||||
# DNS interception on by default and sends queries through the proxy. With
|
||||
# a vless/ws or other UDP-incapable node this breaks all client DNS, so the
|
||||
# OpenWrt default stays off. Users can still enable it in the web UI.
|
||||
$(SED) 's/DnsHijack: *Yes,/DnsHijack: No,/' \
|
||||
$(PKG_BUILD_DIR)/db/configure/setting.go
|
||||
$(SED) '/if setting.DnsHijack == ""/,/}/ s/setting.DnsHijack = Yes/setting.DnsHijack = No/' \
|
||||
$(PKG_BUILD_DIR)/db/configure/migrate_dns.go
|
||||
|
||||
# Router defaults: warn-level logging reduces flash writes, QUIC
|
||||
# sniffing is pointless when the LAN QUIC block is in place, and the
|
||||
# DNS cache is bounded for the case where a user enables the DNS
|
||||
# module later.
|
||||
$(SED) 's/LogLevel: *"info"/LogLevel: "warn"/' \
|
||||
$(PKG_BUILD_DIR)/db/configure/setting.go
|
||||
$(SED) 's/InboundSniffing: *"http,tls,quic"/InboundSniffing: "http,tls"/' \
|
||||
$(PKG_BUILD_DIR)/db/configure/setting.go
|
||||
$(SED) 's/DnsCacheSize: *4096/DnsCacheSize: 1024/' \
|
||||
$(PKG_BUILD_DIR)/db/configure/setting.go
|
||||
$(SED) 's/DnsPrefetch: *true/DnsPrefetch: false/' \
|
||||
$(PKG_BUILD_DIR)/db/configure/setting.go
|
||||
$(SED) 's/DnsNegativeCache: *true/DnsNegativeCache: false/' \
|
||||
$(PKG_BUILD_DIR)/db/configure/setting.go
|
||||
|
||||
( \
|
||||
mkdir -p $(PKG_BUILD_DIR)/server/router/web ; \
|
||||
gzip -dc $(CURDIR)/files/web.tar.gz | $(HOST_TAR) -C $(PKG_BUILD_DIR)/server/router/web $(TAR_OPTIONS) ; \
|
||||
)
|
||||
endef
|
||||
|
||||
define Package/v2raya/install
|
||||
$(call GoPackage/Package/Install/Bin,$(PKG_INSTALL_DIR))
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/bin
|
||||
$(INSTALL_BIN) $(PKG_INSTALL_DIR)/usr/bin/v2rayA $(1)/usr/bin/v2raya
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/config
|
||||
$(INSTALL_CONF) $(CURDIR)/files/v2raya.config $(1)/etc/config/v2raya
|
||||
|
||||
$(INSTALL_DIR) $(1)/etc/init.d
|
||||
$(INSTALL_BIN) $(CURDIR)/files/v2raya.init $(1)/etc/init.d/v2raya
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/libexec
|
||||
$(INSTALL_BIN) $(CURDIR)/files/v2raya-detect $(1)/usr/libexec/v2raya-detect
|
||||
|
||||
# Keep the database and the web password across sysupgrade. Listing
|
||||
# /etc/v2raya as an opkg conffile copies the whole directory and has
|
||||
# broken real upgrades.
|
||||
$(INSTALL_DIR) $(1)/lib/upgrade/keep.d
|
||||
$(INSTALL_DATA) $(CURDIR)/files/v2raya.keep $(1)/lib/upgrade/keep.d/v2raya
|
||||
|
||||
$(INSTALL_DIR) $(1)/usr/share/v2raya
|
||||
endef
|
||||
|
||||
$(eval $(call GoBinPackage,v2raya))
|
||||
$(eval $(call BuildPackage,v2raya))
|
||||
@@ -0,0 +1,131 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#
|
||||
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
|
||||
#
|
||||
# Emit a small key=value report about installed proxy cores and services.
|
||||
# Called by the LuCI overview; values are single-line and contain no newlines.
|
||||
#
|
||||
# This script is read-only. It never installs, removes, starts or stops
|
||||
# anything. It exists so the UI can show that an existing Xray-core or
|
||||
# sing-box installation is present and will not be duplicated or replaced.
|
||||
|
||||
set -u
|
||||
|
||||
first_bin() {
|
||||
for path in "$@"; do
|
||||
if [ -x "$path" ]; then
|
||||
printf '%s' "$path"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
version_line() {
|
||||
bin="$1"
|
||||
shift
|
||||
[ -n "$bin" ] || return 0
|
||||
"$bin" "$@" 2>/dev/null | sed -n '1p' | tr -d '\r'
|
||||
}
|
||||
|
||||
pkg_list=""
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
pkg_list=$(opkg list-installed 2>/dev/null | awk '{ print $1 }')
|
||||
elif command -v apk >/dev/null 2>&1; then
|
||||
pkg_list=$(apk info 2>/dev/null)
|
||||
fi
|
||||
|
||||
pkg_has() {
|
||||
printf '%s\n' "$pkg_list" | grep -qx "$1"
|
||||
}
|
||||
|
||||
core_bin=$(first_bin /usr/bin/v2raya_core /usr/sbin/v2raya_core) || core_bin=""
|
||||
xray_bin=$(first_bin /usr/bin/xray /usr/sbin/xray /usr/bin/xray-core) || xray_bin=""
|
||||
singbox_bin=$(first_bin /usr/bin/sing-box /usr/sbin/sing-box /usr/bin/singbox /usr/sbin/singbox) || singbox_bin=""
|
||||
v2ray_bin=$(first_bin /usr/bin/v2ray /usr/sbin/v2ray /usr/bin/v2ray-core) || v2ray_bin=""
|
||||
|
||||
xray_pkg=0
|
||||
pkg_has xray-core && xray_pkg=1
|
||||
pkg_has xray && xray_pkg=1
|
||||
singbox_pkg=0
|
||||
pkg_has sing-box && singbox_pkg=1
|
||||
v2ray_pkg=0
|
||||
pkg_has v2ray-core && v2ray_pkg=1
|
||||
pkg_has v2ray && v2ray_pkg=1
|
||||
pkg_version() {
|
||||
if command -v opkg >/dev/null 2>&1; then
|
||||
opkg list-installed "$1" 2>/dev/null | awk '{ print $3 }' | sed -n '1p'
|
||||
elif [ -f /lib/apk/db/installed ]; then
|
||||
awk -v p="$1" '$0 == "P:" p { getline; sub(/^V:/, ""); print; exit }' \
|
||||
/lib/apk/db/installed
|
||||
fi
|
||||
}
|
||||
|
||||
version_lt() {
|
||||
[ -n "$1" ] || return 1
|
||||
awk -v a="$1" -v b="$2" 'BEGIN {
|
||||
n = split(a, x, "."); m = split(b, y, ".");
|
||||
for (i = 1; i <= 3; i++) {
|
||||
xs = (i <= n) ? x[i] : 0;
|
||||
ys = (i <= m) ? y[i] : 0;
|
||||
sub(/-.*/, "", xs); sub(/-.*/, "", ys);
|
||||
if (xs + 0 < ys + 0) exit 0;
|
||||
if (xs + 0 > ys + 0) exit 1;
|
||||
}
|
||||
exit 1;
|
||||
}'
|
||||
}
|
||||
|
||||
old_v2raya_pkg=0
|
||||
pkg_has v2raya-nextgen && old_v2raya_pkg=1
|
||||
pkg_has v2raya-core-nextgen && old_v2raya_pkg=1
|
||||
pkg_has luci-app-v2raya-nextgen && old_v2raya_pkg=1
|
||||
if pkg_has v2raya; then
|
||||
version_lt "$(pkg_version v2raya)" "2.5" && old_v2raya_pkg=1
|
||||
fi
|
||||
|
||||
# The upstream packages install to /usr/share/v2ray, while this feed uses
|
||||
# /usr/share/v2raya. Check the configured directory first, then both
|
||||
# well-known paths, so the report is correct with either package set.
|
||||
assets_dirs=""
|
||||
configured_assets=$(uci -q get v2raya.config.v2ray_assetsdir 2>/dev/null) || configured_assets=""
|
||||
[ -n "$configured_assets" ] && assets_dirs="$configured_assets "
|
||||
assets_dirs="$assets_dirs/usr/share/v2ray /usr/local/share/v2ray /usr/share/v2raya /usr/local/share/v2raya"
|
||||
|
||||
geoip_present=0
|
||||
geosite_present=0
|
||||
for dir in $assets_dirs; do
|
||||
[ -f "$dir/geoip.dat" ] && geoip_present=1
|
||||
[ -f "$dir/geosite.dat" ] && geosite_present=1
|
||||
done
|
||||
|
||||
present=""
|
||||
enabled=""
|
||||
for service in xray sing-box v2ray passwall passwall2 ssrplus openclash shellcrash mihomo clash byedpi; do
|
||||
[ -x "/etc/init.d/$service" ] || continue
|
||||
|
||||
present="${present}${present:+,}$service"
|
||||
if "/etc/init.d/$service" enabled >/dev/null 2>&1; then
|
||||
enabled="${enabled}${enabled:+,}$service"
|
||||
fi
|
||||
done
|
||||
|
||||
cat <<EOF
|
||||
core_path=$core_bin
|
||||
core_version=$(version_line "$core_bin" version)
|
||||
xray_path=$xray_bin
|
||||
xray_version=$(version_line "$xray_bin" version)
|
||||
xray_package=$xray_pkg
|
||||
singbox_path=$singbox_bin
|
||||
singbox_version=$(version_line "$singbox_bin" version)
|
||||
singbox_package=$singbox_pkg
|
||||
v2ray_path=$v2ray_bin
|
||||
v2ray_version=$(version_line "$v2ray_bin" version)
|
||||
v2ray_package=$v2ray_pkg
|
||||
old_v2raya_package=$old_v2raya_pkg
|
||||
geoip_present=$geoip_present
|
||||
geosite_present=$geosite_present
|
||||
services_present=$present
|
||||
services_enabled=$enabled
|
||||
EOF
|
||||
@@ -0,0 +1,76 @@
|
||||
# v2rayA configuration
|
||||
#
|
||||
# This file is read by /etc/init.d/v2raya and translated to V2RAYA_*
|
||||
# environment variables. The names intentionally match the old v2rayA feed
|
||||
# so an existing /etc/config/v2raya keeps working after the upgrade.
|
||||
|
||||
config v2raya 'config'
|
||||
# Start the service at boot. The procd instance is also enabled/disabled
|
||||
# by this flag, not only by /etc/rc.d/S99v2raya.
|
||||
option enabled '0'
|
||||
|
||||
# Web UI listening address. Use 127.0.0.1:2017 if you only access it
|
||||
# through an SSH tunnel or a reverse proxy.
|
||||
option address '0.0.0.0:2017'
|
||||
|
||||
# URL prefix when v2rayA sits behind a reverse proxy. Must start with /.
|
||||
option baseurl '/'
|
||||
|
||||
# auto, on or off. Make sure IPv6 works before forcing it on.
|
||||
option ipv6_support 'auto'
|
||||
|
||||
# auto, on or off. nftables is the default on OpenWrt 22.03+.
|
||||
option nftables_support 'auto'
|
||||
|
||||
# Seconds to wait for v2raya_core to start. Raise this on slow MIPS
|
||||
# devices if the core is reported as failing to start.
|
||||
option core_startup_timeout '15'
|
||||
|
||||
# trace, debug, info, warn or error. Warn keeps the flash writes down
|
||||
# on a router; use info while debugging.
|
||||
option log_level 'warn'
|
||||
|
||||
# Number of days to keep rotated log files.
|
||||
option log_max_days '1'
|
||||
|
||||
# The log file already contains timestamps and no terminal colours.
|
||||
option log_disable_color '1'
|
||||
option log_disable_timestamp '0'
|
||||
|
||||
# Soft Go memory limit for both v2raya and v2raya_core (GOMEMLIMIT).
|
||||
# 0 disables the limit. 64MiB is a safe start on a 256MiB router.
|
||||
option gomemlimit '64MiB'
|
||||
|
||||
# Go scheduler threads (GOMAXPROCS). 0 uses every CPU core; 2 is
|
||||
# enough for a home router and uses fewer scheduler resources.
|
||||
option gomaxprocs '2'
|
||||
|
||||
# Go GC target percentage (GOGC). 0 keeps the Go default; lower values
|
||||
# use less memory at the cost of more CPU.
|
||||
option gogc '0'
|
||||
|
||||
# Maximum open file descriptors. 16384 is plenty for a home router and
|
||||
# stops a connection flood from exhausting memory.
|
||||
option nofile '16384'
|
||||
|
||||
# Directory containing geoip.dat and geosite.dat. Leave empty to let
|
||||
# v2rayA search the standard locations.
|
||||
option v2ray_assetsdir '/usr/share/v2raya'
|
||||
|
||||
# Path to v2raya_core. Leave empty to auto-detect the binary next to
|
||||
# /usr/bin/v2raya or in PATH.
|
||||
option v2ray_bin ''
|
||||
|
||||
# Extra v2ray configuration directory merged into the generated config.
|
||||
option v2ray_confdir ''
|
||||
|
||||
# Hooks run around transparent-proxy and core life-cycle events.
|
||||
# v2rayA passes --stage (pre-start, post-start, pre-stop, post-stop) and,
|
||||
# for the transparent hook, --transparent-type (tproxy, redirect).
|
||||
option transparent_hook ''
|
||||
option core_hook ''
|
||||
option plugin_manager ''
|
||||
|
||||
# [Linux] In redirect mode, let sockets bound to a device bypass the
|
||||
# proxy. Requires kernel 5.14+ and cgroup v2.
|
||||
option redirect_respect_bound_device '0'
|
||||
@@ -0,0 +1,115 @@
|
||||
#!/bin/sh /etc/rc.common
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#
|
||||
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
|
||||
#
|
||||
# procd init script for v2rayA.
|
||||
#
|
||||
# All settings are kept in UCI (/etc/config/v2raya) and passed to v2rayA as
|
||||
# V2RAYA_* environment variables, which is the interface gonfig documents
|
||||
# upstream. Keeping the command line short also means paths with spaces or
|
||||
# shell metacharacters cannot break the service.
|
||||
#
|
||||
# shellcheck disable=SC2034
|
||||
# shellcheck disable=SC3043
|
||||
|
||||
USE_PROCD=1
|
||||
START=99
|
||||
STOP=10
|
||||
|
||||
CONF="v2raya"
|
||||
PROG="/usr/bin/v2raya"
|
||||
CONF_DIR="/etc/v2raya"
|
||||
LOG_DIR="/var/log/v2raya"
|
||||
DEFAULT_ASSETS="/usr/share/v2raya"
|
||||
|
||||
is_enabled() {
|
||||
local enabled
|
||||
config_get_bool enabled "$1" "$2" "${3:-0}"
|
||||
[ "$enabled" -eq 1 ]
|
||||
}
|
||||
|
||||
append_env() {
|
||||
procd_append_param env "V2RAYA_$(echo "$1" | tr 'a-z' 'A-Z')=$2"
|
||||
}
|
||||
|
||||
append_env_arg() {
|
||||
local value
|
||||
config_get value "$1" "$2" "$3"
|
||||
[ -n "$value" ] && append_env "$2" "$value"
|
||||
}
|
||||
|
||||
append_env_bool() {
|
||||
is_enabled "$1" "$2" && append_env "$2" "true"
|
||||
}
|
||||
|
||||
start_service() {
|
||||
config_load "$CONF"
|
||||
|
||||
is_enabled "config" "enabled" || return 0
|
||||
|
||||
local assets_dir gomemlimit gomaxprocs gogc nofile
|
||||
config_get assets_dir "config" "v2ray_assetsdir" "$DEFAULT_ASSETS"
|
||||
config_get gomemlimit "config" "gomemlimit" "64MiB"
|
||||
config_get gomaxprocs "config" "gomaxprocs" "2"
|
||||
config_get gogc "config" "gogc" "0"
|
||||
config_get nofile "config" "nofile" "16384"
|
||||
|
||||
mkdir -p "$CONF_DIR" "$LOG_DIR"
|
||||
[ -n "$assets_dir" ] && mkdir -p "$assets_dir"
|
||||
chmod 750 "$CONF_DIR"
|
||||
chmod 755 "$LOG_DIR"
|
||||
|
||||
procd_open_instance "$CONF"
|
||||
procd_set_param command "$PROG"
|
||||
procd_set_param env "V2RAYA_CONFIG=$CONF_DIR" "V2RAYA_LOG_FILE=$LOG_DIR/v2raya.log"
|
||||
|
||||
# The core inherits this environment, so one limit covers both processes.
|
||||
[ -n "$gomemlimit" ] && [ "$gomemlimit" != "0" ] && \
|
||||
procd_append_param env "GOMEMLIMIT=$gomemlimit"
|
||||
[ -n "$gomaxprocs" ] && [ "$gomaxprocs" -gt 0 ] 2>/dev/null && \
|
||||
procd_append_param env "GOMAXPROCS=$gomaxprocs"
|
||||
[ -n "$gogc" ] && [ "$gogc" -gt 0 ] 2>/dev/null && \
|
||||
procd_append_param env "GOGC=$gogc"
|
||||
|
||||
append_env_arg "config" "v2ray_assetsdir" "$DEFAULT_ASSETS"
|
||||
append_env_arg "config" "address" "0.0.0.0:2017"
|
||||
append_env_arg "config" "baseurl" "/"
|
||||
append_env_arg "config" "ipv6_support" "auto"
|
||||
append_env_arg "config" "nftables_support" "auto"
|
||||
append_env_arg "config" "core_startup_timeout" "15"
|
||||
append_env_arg "config" "log_level" "warn"
|
||||
append_env_arg "config" "log_max_days" "1"
|
||||
append_env_arg "config" "v2ray_bin"
|
||||
append_env_arg "config" "v2ray_confdir"
|
||||
append_env_arg "config" "transparent_hook"
|
||||
append_env_arg "config" "core_hook"
|
||||
append_env_arg "config" "plugin_manager"
|
||||
|
||||
append_env_bool "config" "log_disable_color"
|
||||
append_env_bool "config" "log_disable_timestamp"
|
||||
append_env_bool "config" "redirect_respect_bound_device"
|
||||
|
||||
# No core dumps on a router: they fill the overlay and can trip a
|
||||
# marginal NAND. A 16384 descriptor ceiling is plenty for the LAN and
|
||||
# keeps a connection flood from allocating unbounded socket buffers.
|
||||
[ -n "$nofile" ] || nofile=16384
|
||||
procd_set_param limits core="0 0"
|
||||
procd_set_param limits nofile="$nofile $nofile"
|
||||
procd_set_param respawn
|
||||
procd_set_param term_timeout 30
|
||||
procd_set_param stdout 1
|
||||
procd_set_param stderr 1
|
||||
|
||||
procd_close_instance
|
||||
}
|
||||
|
||||
reload_service() {
|
||||
# v2rayA does not reload its configuration on SIGHUP; a restart is the
|
||||
# only safe way to apply new UCI values.
|
||||
restart
|
||||
}
|
||||
|
||||
service_triggers() {
|
||||
procd_add_reload_trigger "$CONF"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
/etc/v2raya
|
||||
Binary file not shown.
@@ -0,0 +1,13 @@
|
||||
#!/bin/sh
|
||||
# SPDX-License-Identifier: GPL-3.0-only
|
||||
#
|
||||
# Copyright (C) 2026 Amr Fateem <amr.fateem@yahoo.com>
|
||||
|
||||
case "$1" in
|
||||
v2raya)
|
||||
/usr/bin/v2raya --version | grep -F "$2"
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
Reference in new issue
Block a user