✨ Sync 2026-11-11 01:22:09

This commit is contained in:
github-actions[bot] committed 2026-11-11 01:22:09 +08:00
1 parent 9c5aa2e662
commit cc21462d8a
48 files changed
+9760 -59

No files matched your search

+6 -2
View File
@@ -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)
+1
View File
@@ -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'
+1
View File
@@ -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
View File
@@ -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, &current)) {
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;
}
+2 -2
View File
@@ -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');
+7 -1
View File
@@ -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'));
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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(),
+6
View File
@@ -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
+2 -2
View File
@@ -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(),
+6
View File
@@ -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"
+6
View File
@@ -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"
+6
View File
@@ -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中继模式"
+6
View File
@@ -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
+2 -2
View File
@@ -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>
-2
View File
@@ -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
+28 -5
View File
@@ -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);
}
},
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -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
View File
@@ -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))
+131
View File
@@ -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
+76
View File
@@ -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'
+115
View File
@@ -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"
}
+1
View File
@@ -0,0 +1 @@
/etc/v2raya
Binary file not shown.
+13
View File
@@ -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