- fix(updater): support single release JSON object for specific tag queries and unblock prereleases on explicit selection - fix(action): replace wget --header with -U for uclient-fetch compatibility on OpenWrt 23.05 - fix(action): make package list updates best-effort to prevent failures on snapshot kmod feeds - fix(action): add offline/corrupt extract fallbacks (apk extract and opkg data.tar.gz) for kernel dependency mismatch - fix(action): ensure /var/lock and /tmp/run directories exist before opkg/procd operations - fix(action): fix fallback asset URL pattern for remittor zapret releases - fix(packages): add binary presence and --version detection fallbacks for zapret, zapret2, byedpi, sing-box - fix(byedpi): support direct apk list parsing for installed package version - fix(state): include schedule, profile, and guest_mode in reload state signatures - fix(imports): fix missing command_status_from_args, write_json, and remove_file imports - fix(updates): extract meaningful ucode errors and strip trailing caret lines
1140 lines
37 KiB
Ucode
1140 lines
37 KiB
Ucode
#!/usr/bin/env ucode
|
|
|
|
let fs = require("fs");
|
|
let core_ip = require("core.ip");
|
|
let core_url = require("core.url");
|
|
let uci = require("core.uci");
|
|
let common = require("core.common");
|
|
let as_string = common.as_string;
|
|
let shell_quote = common.shell_quote;
|
|
let read_json_file = common.read_json_file;
|
|
let read_stdin = common.read_stdin;
|
|
let read_stdin_json = common.read_stdin_json;
|
|
let write_json = common.write_json;
|
|
|
|
const CONFIG_NAME = getenv("TACHYON_CONFIG_NAME") || "tachyon";
|
|
|
|
function ascii_lower(value) {
|
|
let upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
let lower = "abcdefghijklmnopqrstuvwxyz";
|
|
return replace(as_string(value), /[A-Z]/g, function(ch) {
|
|
return substr(lower, index(upper, ch), 1);
|
|
});
|
|
}
|
|
|
|
function run(command) {
|
|
return system(command) == 0;
|
|
}
|
|
|
|
function output(command) {
|
|
let pipe = fs.popen(command, "r");
|
|
if (!pipe)
|
|
return "";
|
|
|
|
let data = pipe.read("all");
|
|
let status = pipe.close();
|
|
if (status != 0 || data == null)
|
|
return "";
|
|
|
|
return replace(as_string(data), /[\r\n]+$/g, "");
|
|
}
|
|
|
|
function log(message, level) {
|
|
level = as_string(level || "info");
|
|
run("logger -t " + shell_quote("tachyon") + " " + shell_quote("[" + level + "] " + as_string(message)));
|
|
}
|
|
|
|
function fatal(message) {
|
|
log(message, "fatal");
|
|
exit(1);
|
|
}
|
|
|
|
function uci_get(path) {
|
|
return uci.get(path);
|
|
}
|
|
|
|
function uci_set(path, value) {
|
|
return uci.set(path, value);
|
|
}
|
|
|
|
function uci_commit(package_name) {
|
|
return uci.commit(package_name);
|
|
}
|
|
|
|
function words(value) {
|
|
value = trim(as_string(value));
|
|
return value == "" ? [] : split(value, /[ \t\r\n]+/);
|
|
}
|
|
|
|
function uci_sections(type_name) {
|
|
return uci.sections(CONFIG_NAME, type_name);
|
|
}
|
|
|
|
function config_path(section, option) {
|
|
return CONFIG_NAME + "." + as_string(section) + "." + as_string(option);
|
|
}
|
|
|
|
function config_get(section, option, fallback) {
|
|
let value = uci_get(config_path(section, option));
|
|
return value == "" ? as_string(fallback || "") : value;
|
|
}
|
|
|
|
let SERVER_DEFAULTS_CHANGED = false;
|
|
|
|
function server_default_set_option(section, option, value) {
|
|
value = as_string(value);
|
|
if (value == "")
|
|
return;
|
|
if (uci_get(config_path(section, option)) != "")
|
|
return;
|
|
if (uci_set(config_path(section, option), value))
|
|
SERVER_DEFAULTS_CHANGED = true;
|
|
}
|
|
|
|
function server_set_option(section, option, value) {
|
|
value = as_string(value);
|
|
if (value == "")
|
|
return;
|
|
if (uci_get(config_path(section, option)) == value)
|
|
return;
|
|
if (uci_set(config_path(section, option), value))
|
|
SERVER_DEFAULTS_CHANGED = true;
|
|
}
|
|
|
|
function stdin_first_nonempty_line() {
|
|
for (let line in split(read_stdin(), "\n")) {
|
|
if (trim(as_string(line)) != "") {
|
|
print(line, "\n");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
function stdin_remove_newlines() {
|
|
print(replace(read_stdin(), /\n/g, ""));
|
|
}
|
|
|
|
function shell_single_quote(value) {
|
|
value = as_string(value);
|
|
if (value == "")
|
|
return;
|
|
print("'", replace(value, /'/g, "'\\''"), "'\n");
|
|
}
|
|
|
|
function array_or_empty(value) {
|
|
return type(value) == "array" ? value : [];
|
|
}
|
|
|
|
function array_append_string(value) {
|
|
let result = array_or_empty(read_stdin_json());
|
|
push(result, as_string(value));
|
|
write_json(result);
|
|
}
|
|
|
|
function regex_matches(value, pattern) {
|
|
pattern = as_string(pattern);
|
|
if (pattern == "")
|
|
return false;
|
|
|
|
try {
|
|
return match(as_string(value), regexp(pattern)) != null;
|
|
}
|
|
catch (e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
function valid_dotted_ipv4(value) {
|
|
return regex_matches(value, "^([0-9]{1,3}\\.){3}[0-9]{1,3}$");
|
|
}
|
|
|
|
function valid_file_path(value) {
|
|
value = as_string(value);
|
|
return value != "" &&
|
|
value != "/" &&
|
|
substr(value, length(value) - 1) != "/" &&
|
|
regex_matches(value, "^/[A-Za-z0-9_./-]+$");
|
|
}
|
|
|
|
function valid_duration(value) {
|
|
value = as_string(value);
|
|
return value != "" && regex_matches(value, "^([0-9]+(ns|us|ms|s|m|h))+$");
|
|
}
|
|
|
|
function valid_client_value(value) {
|
|
value = as_string(value);
|
|
return value != "" && !regex_matches(value, "[[:cntrl:]]");
|
|
}
|
|
|
|
function valid_port(value) {
|
|
value = as_string(value);
|
|
if (value == "" || regex_matches(value, "[^0-9]"))
|
|
return false;
|
|
|
|
value = int(value);
|
|
return value >= 1 && value <= 65535;
|
|
}
|
|
|
|
function normalize_host(value) {
|
|
value = as_string(value);
|
|
if (substr(value, 0, 1) == "[")
|
|
value = substr(value, 1);
|
|
if (substr(value, length(value) - 1) == "]")
|
|
value = substr(value, 0, length(value) - 1);
|
|
return lc(value);
|
|
}
|
|
|
|
function str_endswith(value, suffix) {
|
|
value = as_string(value);
|
|
suffix = as_string(suffix);
|
|
return length(value) >= length(suffix) && substr(value, length(value) - length(suffix)) == suffix;
|
|
}
|
|
|
|
function str_remove_suffix(value, suffix) {
|
|
value = as_string(value);
|
|
suffix = as_string(suffix);
|
|
return str_endswith(value, suffix) ? substr(value, 0, length(value) - length(suffix)) : value;
|
|
}
|
|
|
|
function valid_ipv4(value) {
|
|
return core_ip.valid_ipv4(value, false, false);
|
|
}
|
|
|
|
function valid_domain(value) {
|
|
return regex_matches(ascii_lower(value), "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$");
|
|
}
|
|
|
|
function valid_host(value) {
|
|
value = normalize_host(value);
|
|
return value != "" && (valid_ipv4(value) || core_ip.valid_ipv6(value) || valid_domain(value));
|
|
}
|
|
|
|
function valid_absolute_path(value) {
|
|
return substr(as_string(value), 0, 1) == "/";
|
|
}
|
|
|
|
function valid_http_url(value) {
|
|
value = as_string(value);
|
|
let scheme = index(value, "://");
|
|
if (scheme < 0)
|
|
return false;
|
|
|
|
let prefix = substr(value, 0, scheme + 3);
|
|
if (prefix != "http://" && prefix != "https://")
|
|
return false;
|
|
|
|
let rest = substr(value, scheme + 3);
|
|
if (rest == "")
|
|
return false;
|
|
|
|
return valid_host(core_url.host(value));
|
|
}
|
|
|
|
function valid_uuid(value) {
|
|
return regex_matches(value, "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$");
|
|
}
|
|
|
|
function valid_nonnegative_integer(value) {
|
|
return regex_matches(value, "^[0-9]+$");
|
|
}
|
|
|
|
function valid_reality_short_id(value) {
|
|
return regex_matches(value, "^[0-9a-fA-F]{1,8}$");
|
|
}
|
|
|
|
function valid_transport_service_name(value) {
|
|
return regex_matches(value, "^[A-Za-z0-9_.-]+$");
|
|
}
|
|
|
|
function mtproto_base_secret_value(value) {
|
|
value = lc(as_string(value));
|
|
if (regex_matches(value, "^[0-9a-f]{32}$"))
|
|
return value;
|
|
if (regex_matches(value, "^ee[0-9a-f]{32}([0-9a-f]{2})+$"))
|
|
return substr(value, 2, 32);
|
|
return "";
|
|
}
|
|
|
|
function mtproto_base_secret_from_value(value) {
|
|
let result = mtproto_base_secret_value(value);
|
|
if (result == "")
|
|
exit(1);
|
|
print(result, "\n");
|
|
}
|
|
|
|
function hex_digit_value(value) {
|
|
value = ord(lc(as_string(value)));
|
|
if (value >= 48 && value <= 57)
|
|
return value - 48;
|
|
if (value >= 97 && value <= 102)
|
|
return value - 87;
|
|
return -1;
|
|
}
|
|
|
|
function hex_to_string_value(value) {
|
|
value = as_string(value);
|
|
if (length(value) == 0 || length(value) % 2 != 0 || !regex_matches(value, "^([0-9a-fA-F]{2})+$"))
|
|
return null;
|
|
|
|
let result = "";
|
|
for (let i = 0; i < length(value); i += 2) {
|
|
let high = hex_digit_value(substr(value, i, 1));
|
|
let low = hex_digit_value(substr(value, i + 1, 1));
|
|
if (high < 0 || low < 0)
|
|
return null;
|
|
result += chr(high * 16 + low);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function string_to_hex(value) {
|
|
value = as_string(value);
|
|
for (let i = 0; i < length(value); i++)
|
|
print(sprintf("%02x", ord(substr(value, i, 1))));
|
|
}
|
|
|
|
function mtproto_faketls_value(value) {
|
|
value = lc(as_string(value));
|
|
if (!regex_matches(value, "^ee[0-9a-f]{32}([0-9a-f]{2})+$"))
|
|
return "";
|
|
let result = hex_to_string_value(substr(value, 34));
|
|
return result == null ? "" : result;
|
|
}
|
|
|
|
function mtproto_faketls_from_secret(value) {
|
|
let result = mtproto_faketls_value(value);
|
|
if (result == "")
|
|
exit(1);
|
|
print(result);
|
|
}
|
|
|
|
function mtproto_build_secret(base, faketls, padding) {
|
|
if (as_string(padding) != "1")
|
|
exit(1);
|
|
|
|
print("ee", as_string(base));
|
|
string_to_hex(faketls);
|
|
print("\n");
|
|
}
|
|
|
|
function valid_mtproto_base_secret(value) {
|
|
value = lc(as_string(value));
|
|
return regex_matches(value, "^[0-9a-f]{32}$") && value != "00000000000000000000000000000000";
|
|
}
|
|
|
|
function default_security_for_protocol(protocol) {
|
|
protocol = as_string(protocol);
|
|
if (protocol == "vless")
|
|
print("reality\n");
|
|
else if (protocol == "trojan" || protocol == "hysteria2")
|
|
print("tls\n");
|
|
else
|
|
print("none\n");
|
|
}
|
|
|
|
function safe_filename(value) {
|
|
value = as_string(value);
|
|
for (let i = 0; i < length(value); i++) {
|
|
let chr = substr(value, i, 1);
|
|
print(regex_matches(chr, "^[A-Za-z0-9_.-]$") ? chr : "_");
|
|
}
|
|
}
|
|
|
|
function valid_public_ipv4(value) {
|
|
value = as_string(value);
|
|
if (!valid_ipv4(value))
|
|
return false;
|
|
|
|
let parts = split(value, ".");
|
|
let a = int(parts[0]);
|
|
let b = int(parts[1]);
|
|
|
|
if (a == 0 || a == 10 || a == 127 || a >= 224)
|
|
return false;
|
|
if (a == 169 && b == 254)
|
|
return false;
|
|
if (a == 192 && (b == 168 || b == 0 || b == 2))
|
|
return false;
|
|
if (a == 198 && (b == 18 || b == 19 || b == 51))
|
|
return false;
|
|
if (a == 203 && b == 0)
|
|
return false;
|
|
if (a == 100 && b >= 64 && b <= 127)
|
|
return false;
|
|
if (a == 172 && b >= 16 && b <= 31)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
function valid_ipv6_literal(value) {
|
|
return core_ip.valid_ipv6(value);
|
|
}
|
|
|
|
function valid_cidr(value) {
|
|
value = as_string(value);
|
|
let slash = index(value, "/");
|
|
if (slash < 0 || index(substr(value, slash + 1), "/") >= 0)
|
|
return false;
|
|
|
|
let address = substr(value, 0, slash);
|
|
let prefix = substr(value, slash + 1);
|
|
if (prefix == "" || regex_matches(prefix, "[^0-9]"))
|
|
return false;
|
|
|
|
prefix = int(prefix);
|
|
if (valid_ipv4(address))
|
|
return prefix >= 0 && prefix <= 32;
|
|
if (valid_ipv6_literal(address))
|
|
return prefix >= 0 && prefix <= 128;
|
|
|
|
return false;
|
|
}
|
|
|
|
function server_users_from_tsv(protocol, path) {
|
|
let result = [];
|
|
let data = fs.readfile(path);
|
|
|
|
if (data != null) {
|
|
for (let line in split(as_string(data), "\n")) {
|
|
if (line == "")
|
|
continue;
|
|
|
|
let parts = split(line, "\t");
|
|
let name = as_string(parts[0] || "");
|
|
let credential = as_string(parts[1] || "");
|
|
let flow = as_string(parts[2] || "");
|
|
|
|
if (credential == "")
|
|
continue;
|
|
|
|
if (protocol == "vless") {
|
|
let user = { uuid: credential };
|
|
if (name != "")
|
|
user.name = name;
|
|
if (flow != "")
|
|
user.flow = flow;
|
|
push(result, user);
|
|
}
|
|
else if (protocol == "vmess") {
|
|
let user = { uuid: credential, alterId: int(flow || "0", 10) || 0 };
|
|
if (name != "")
|
|
user.name = name;
|
|
push(result, user);
|
|
}
|
|
else if (protocol == "trojan") {
|
|
let user = { password: credential };
|
|
if (name != "")
|
|
user.name = name;
|
|
push(result, user);
|
|
}
|
|
else if (protocol == "hysteria2") {
|
|
let user = { password: credential };
|
|
if (name != "")
|
|
user.name = name;
|
|
push(result, user);
|
|
}
|
|
else if (protocol == "mtproto") {
|
|
let user = { secret: credential };
|
|
if (name != "")
|
|
user.name = name;
|
|
push(result, user);
|
|
}
|
|
else if (protocol == "socks") {
|
|
let user = {
|
|
username: name != "" ? name : "user",
|
|
password: credential
|
|
};
|
|
push(result, user);
|
|
}
|
|
}
|
|
}
|
|
|
|
write_json(result);
|
|
}
|
|
|
|
function json_length(path) {
|
|
let value = read_json_file(path);
|
|
if (type(value) == "array" || type(value) == "object")
|
|
print(length(value), "\n");
|
|
else
|
|
print("0\n");
|
|
}
|
|
|
|
function error_response(message) {
|
|
write_json({
|
|
success: false,
|
|
message: as_string(message)
|
|
});
|
|
}
|
|
|
|
function reality_keypair_response(private_key, public_key) {
|
|
write_json({
|
|
success: true,
|
|
private_key: as_string(private_key),
|
|
public_key: as_string(public_key)
|
|
});
|
|
}
|
|
|
|
function tls_fingerprint_response(fingerprint) {
|
|
write_json({
|
|
success: true,
|
|
sha256: as_string(fingerprint)
|
|
});
|
|
}
|
|
|
|
function first_key_value_line_value(data, prefix) {
|
|
prefix = as_string(prefix);
|
|
for (let line in split(as_string(data), "\n")) {
|
|
line = as_string(line);
|
|
if (!regex_matches(line, "^" + prefix + ":"))
|
|
continue;
|
|
|
|
let value = substr(line, length(prefix) + 1);
|
|
return replace(value, /^[ \t\r]*/, "");
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function reality_keypair_tsv() {
|
|
let data = read_stdin();
|
|
let private_key = first_key_value_line_value(data, "PrivateKey");
|
|
let public_key = first_key_value_line_value(data, "PublicKey");
|
|
|
|
if (private_key == null || public_key == null || private_key == "" || public_key == "")
|
|
exit(1);
|
|
|
|
print(private_key, "\t", public_key, "\n");
|
|
}
|
|
|
|
function pem_block(data, begin_marker, end_marker) {
|
|
let lines = [];
|
|
let in_block = false;
|
|
|
|
for (let line in split(as_string(data), "\n")) {
|
|
line = str_remove_suffix(as_string(line), "\r");
|
|
if (!in_block && line != begin_marker)
|
|
continue;
|
|
|
|
in_block = true;
|
|
push(lines, line);
|
|
|
|
if (line == end_marker)
|
|
return join("\n", lines);
|
|
}
|
|
|
|
return "";
|
|
}
|
|
|
|
function write_tls_keypair_files(key_path, certificate_path) {
|
|
let data = read_stdin();
|
|
let key = pem_block(data, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");
|
|
let cert = pem_block(data, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
|
|
|
|
if (key == "" || cert == "")
|
|
exit(1);
|
|
|
|
if (!fs.writefile(key_path, key + "\n"))
|
|
exit(1);
|
|
if (!fs.writefile(certificate_path, cert + "\n"))
|
|
exit(1);
|
|
}
|
|
|
|
function certificate_base64(path) {
|
|
let data = fs.readfile(path);
|
|
let cert = pem_block(data, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
|
|
let lines = [];
|
|
|
|
if (cert == "")
|
|
exit(1);
|
|
|
|
for (let line in split(cert, "\n")) {
|
|
line = replace(as_string(line), /\r/g, "");
|
|
if (line == "-----BEGIN CERTIFICATE-----" || line == "-----END CERTIFICATE-----" || line == "")
|
|
continue;
|
|
|
|
push(lines, line);
|
|
}
|
|
|
|
if (length(lines) == 0)
|
|
exit(1);
|
|
|
|
print(join("\n", lines), "\n");
|
|
}
|
|
|
|
function valid_sha256_hex(value) {
|
|
return regex_matches(value, "^[0-9a-f]{64}$");
|
|
}
|
|
|
|
function command_first_nonempty_line(command) {
|
|
for (let line in split(output(command), "\n"))
|
|
if (trim(as_string(line)) != "")
|
|
return line;
|
|
return "";
|
|
}
|
|
|
|
function server_generate_uuid() {
|
|
return command_first_nonempty_line("sing-box generate uuid 2>/dev/null");
|
|
}
|
|
|
|
function server_generate_password() {
|
|
return replace(output("sing-box generate rand --base64 18 2>/dev/null"), /\n/g, "");
|
|
}
|
|
|
|
function server_generate_short_id() {
|
|
return replace(output("sing-box generate rand --hex 4 2>/dev/null"), /\n/g, "");
|
|
}
|
|
|
|
function server_generate_mtproto_secret() {
|
|
let random_hex = replace(output("sing-box generate rand --hex 16 2>/dev/null"), /\n/g, "");
|
|
if (random_hex == "")
|
|
random_hex = replace(output("head -c 16 /dev/urandom | hexdump -ve '1/1 \"%02x\"' 2>/dev/null"), /\n/g, "");
|
|
if (random_hex == "00000000000000000000000000000000")
|
|
random_hex = "11111111111111111111111111111111";
|
|
return random_hex;
|
|
}
|
|
|
|
function default_security_value(protocol) {
|
|
protocol = as_string(protocol);
|
|
if (protocol == "vless")
|
|
return "reality";
|
|
if (protocol == "trojan" || protocol == "hysteria2")
|
|
return "tls";
|
|
return "none";
|
|
}
|
|
|
|
function server_effective_security_value(section, protocol) {
|
|
protocol = as_string(protocol);
|
|
let security = config_get(section, "security", "");
|
|
if (security == "")
|
|
security = default_security_value(protocol);
|
|
|
|
if (protocol == "shadowsocks" || protocol == "socks" || protocol == "mtproto" || protocol == "tailscale" || protocol == "json_inbound" || protocol == "awg")
|
|
security = "none";
|
|
else if (protocol == "hysteria2")
|
|
security = "tls";
|
|
else if ((protocol == "vmess" || protocol == "trojan") && security == "reality")
|
|
security = default_security_value(protocol);
|
|
|
|
return security;
|
|
}
|
|
|
|
function print_effective_security(section, protocol) {
|
|
print(server_effective_security_value(section, protocol), "\n");
|
|
}
|
|
|
|
function generate_reality_keypair_values() {
|
|
let data = output("sing-box generate reality-keypair 2>/dev/null");
|
|
let private_key = first_key_value_line_value(data, "PrivateKey");
|
|
let public_key = first_key_value_line_value(data, "PublicKey");
|
|
|
|
if (private_key == null || public_key == null || private_key == "" || public_key == "")
|
|
return null;
|
|
|
|
return {
|
|
private_key: private_key,
|
|
public_key: public_key
|
|
};
|
|
}
|
|
|
|
function generate_wg_keypair_values() {
|
|
let data = output("sing-box generate wg-keypair 2>/dev/null");
|
|
let private_key = first_key_value_line_value(data, "PrivateKey");
|
|
let public_key = first_key_value_line_value(data, "PublicKey");
|
|
|
|
if (private_key == null || public_key == null || private_key == "" || public_key == "")
|
|
return null;
|
|
|
|
return {
|
|
private_key: private_key,
|
|
public_key: public_key
|
|
};
|
|
}
|
|
|
|
function generate_wg_keypair_cli() {
|
|
let pair = generate_wg_keypair_values();
|
|
if (pair == null) {
|
|
error_response("Failed to generate WireGuard key pair");
|
|
exit(1);
|
|
}
|
|
|
|
print(sprintf("PrivateKey: %s\nPublicKey: %s\n", pair.private_key, pair.public_key));
|
|
}
|
|
|
|
function generate_reality_keypair_cli() {
|
|
let pair = generate_reality_keypair_values();
|
|
if (pair == null) {
|
|
error_response("Failed to generate Reality key pair");
|
|
exit(1);
|
|
}
|
|
|
|
reality_keypair_response(pair.private_key, pair.public_key);
|
|
}
|
|
|
|
function write_tls_keypair_data(data, key_path, certificate_path) {
|
|
let key = pem_block(data, "-----BEGIN PRIVATE KEY-----", "-----END PRIVATE KEY-----");
|
|
let cert = pem_block(data, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
|
|
|
|
if (key == "" || cert == "")
|
|
return false;
|
|
|
|
return fs.writefile(key_path, key + "\n") && fs.writefile(certificate_path, cert + "\n");
|
|
}
|
|
|
|
function server_generate_tls_keypair_files(server_name, certificate_path, key_path) {
|
|
let data = output("sing-box generate tls-keypair " + shell_quote(server_name) + " 2>/dev/null");
|
|
if (data == "")
|
|
return false;
|
|
|
|
let key_dir = replace(as_string(key_path), /\/[^\/]*$/, "");
|
|
let cert_dir = replace(as_string(certificate_path), /\/[^\/]*$/, "");
|
|
if (!run("mkdir -p " + shell_quote(key_dir) + " " + shell_quote(cert_dir)))
|
|
return false;
|
|
|
|
if (!write_tls_keypair_data(data, key_path, certificate_path))
|
|
return false;
|
|
|
|
run("chmod 600 " + shell_quote(key_path) + " >/dev/null 2>&1");
|
|
return true;
|
|
}
|
|
|
|
function certificate_base64_text(path) {
|
|
let data = fs.readfile(path);
|
|
let cert = pem_block(data, "-----BEGIN CERTIFICATE-----", "-----END CERTIFICATE-----");
|
|
let lines = [];
|
|
|
|
if (cert == "")
|
|
return "";
|
|
|
|
for (let line in split(cert, "\n")) {
|
|
line = replace(as_string(line), /\r/g, "");
|
|
if (line == "-----BEGIN CERTIFICATE-----" || line == "-----END CERTIFICATE-----" || line == "")
|
|
continue;
|
|
push(lines, line);
|
|
}
|
|
|
|
return length(lines) == 0 ? "" : join("\n", lines) + "\n";
|
|
}
|
|
|
|
function server_tls_certificate_sha256_from_file(certificate_path) {
|
|
if (!valid_file_path(certificate_path))
|
|
return "";
|
|
let base64_text = certificate_base64_text(certificate_path);
|
|
if (base64_text == "")
|
|
return "";
|
|
|
|
let tmp_path = output("mktemp 2>/dev/null");
|
|
if (tmp_path == "")
|
|
return "";
|
|
|
|
if (!fs.writefile(tmp_path, base64_text)) {
|
|
fs.unlink(tmp_path);
|
|
return "";
|
|
}
|
|
|
|
let fingerprint = output("base64 -d " + shell_quote(tmp_path) + " 2>/dev/null | sha256sum 2>/dev/null");
|
|
fs.unlink(tmp_path);
|
|
fingerprint = substr(fingerprint, 0, index(fingerprint, " ") >= 0 ? index(fingerprint, " ") : length(fingerprint));
|
|
return valid_sha256_hex(fingerprint) ? fingerprint : "";
|
|
}
|
|
|
|
function get_tls_certificate_sha256_cli(section) {
|
|
if (as_string(section) == "") {
|
|
error_response("Missing server section");
|
|
exit(1);
|
|
}
|
|
|
|
let protocol = config_get(section, "protocol", "vless");
|
|
let security = server_effective_security_value(section, protocol);
|
|
if (security != "tls") {
|
|
error_response("Server does not use TLS");
|
|
exit(1);
|
|
}
|
|
|
|
let certificate_path = config_get(section, "tls_certificate_path", "");
|
|
if (certificate_path == "") {
|
|
error_response("TLS certificate path is empty");
|
|
exit(1);
|
|
}
|
|
|
|
let fingerprint = server_tls_certificate_sha256_from_file(certificate_path);
|
|
if (fingerprint == "") {
|
|
error_response("Failed to read TLS certificate fingerprint");
|
|
exit(1);
|
|
}
|
|
|
|
tls_fingerprint_response(fingerprint);
|
|
}
|
|
|
|
function network_status_ipv4_address(data) {
|
|
try {
|
|
data = json(as_string(data));
|
|
}
|
|
catch (e) {
|
|
return "";
|
|
}
|
|
|
|
let addresses = data["ipv4-address"];
|
|
if (type(addresses) != "array")
|
|
return "";
|
|
|
|
for (let item in addresses)
|
|
if (type(item) == "object" && as_string(item.address) != "")
|
|
return as_string(item.address);
|
|
|
|
return "";
|
|
}
|
|
|
|
function server_detect_default_public_host() {
|
|
for (let interface in [ "wan", "wwan" ]) {
|
|
let ip = network_status_ipv4_address(output("ubus -S call " + shell_quote("network.interface." + interface) + " status 2>/dev/null"));
|
|
if (valid_public_ipv4(ip))
|
|
return ip;
|
|
}
|
|
|
|
let lan_ip = uci_get("network.lan.ipaddr");
|
|
return valid_dotted_ipv4(lan_ip) ? lan_ip : "";
|
|
}
|
|
|
|
function first_server_user(section) {
|
|
let users = words(uci_get(config_path(section, "server_users")));
|
|
return length(users) > 0 ? users[0] : "";
|
|
}
|
|
|
|
function server_prepare_legacy_user_defaults(section, protocol) {
|
|
let entry = first_server_user(section);
|
|
if (entry == "")
|
|
return;
|
|
|
|
let parts = split(entry, "|");
|
|
let name = "client";
|
|
let credential = entry;
|
|
let extra = "";
|
|
if (length(parts) > 1) {
|
|
name = as_string(parts[0]);
|
|
credential = as_string(parts[1]);
|
|
extra = as_string(parts[2] || "");
|
|
}
|
|
|
|
if (protocol == "vless" || protocol == "vmess")
|
|
server_default_set_option(section, "server_uuid", credential);
|
|
else if (protocol == "shadowsocks" || protocol == "socks" || protocol == "trojan" || protocol == "hysteria2")
|
|
server_default_set_option(section, "server_password", credential);
|
|
else if (protocol == "mtproto") {
|
|
let mtproto_faketls = mtproto_faketls_value(credential);
|
|
let base_secret = mtproto_base_secret_value(credential);
|
|
server_default_set_option(section, "mtproto_secret", base_secret != "" ? base_secret : credential);
|
|
server_default_set_option(section, "mtproto_faketls", mtproto_faketls != "" ? mtproto_faketls : "google.com");
|
|
server_default_set_option(section, "mtproto_padding", "1");
|
|
}
|
|
|
|
if (protocol == "socks" && name != "")
|
|
server_default_set_option(section, "server_username", name);
|
|
if (protocol == "vless" && extra != "")
|
|
server_default_set_option(section, "vless_flow", extra);
|
|
}
|
|
|
|
function server_prepare_tls_defaults(section, protocol, security) {
|
|
if (security != "tls")
|
|
return;
|
|
if (config_get(section, "enabled", "1") == "0")
|
|
return;
|
|
|
|
let tls_server_name = config_get(section, "tls_server_name", "");
|
|
if (tls_server_name == "")
|
|
tls_server_name = "www.microsoft.com";
|
|
server_default_set_option(section, "tls_server_name", tls_server_name);
|
|
|
|
let certificate_path = config_get(section, "tls_certificate_path", "");
|
|
let key_path = config_get(section, "tls_key_path", "");
|
|
if (certificate_path == "" || key_path == "") {
|
|
let safe_name = safe_filename_string(section);
|
|
let default_dir = "/etc/tachyon/server-certs";
|
|
if (certificate_path == "")
|
|
certificate_path = default_dir + "/" + safe_name + ".crt";
|
|
if (key_path == "")
|
|
key_path = default_dir + "/" + safe_name + ".key";
|
|
server_set_option(section, "tls_certificate_path", certificate_path);
|
|
server_set_option(section, "tls_key_path", key_path);
|
|
}
|
|
|
|
if (!valid_file_path(certificate_path))
|
|
fatal("Server '" + section + "' has invalid TLS certificate path. Specify a file path. Aborted.");
|
|
if (!valid_file_path(key_path))
|
|
fatal("Server '" + section + "' has invalid TLS key path. Specify a file path. Aborted.");
|
|
if (certificate_path == key_path)
|
|
fatal("Server '" + section + "' has the same TLS certificate and key path. Specify different files. Aborted.");
|
|
|
|
if (!run("[ -s " + shell_quote(certificate_path) + " ] && [ -s " + shell_quote(key_path) + " ]")) {
|
|
log("Generating self-signed TLS certificate for server '" + section + "'", "info");
|
|
if (!server_generate_tls_keypair_files(tls_server_name, certificate_path, key_path))
|
|
fatal("Failed to generate TLS certificate for server '" + section + "'. Aborted.");
|
|
}
|
|
}
|
|
|
|
function safe_filename_string(value) {
|
|
value = as_string(value);
|
|
let result = [];
|
|
for (let i = 0; i < length(value); i++) {
|
|
let chr = substr(value, i, 1);
|
|
push(result, regex_matches(chr, "^[A-Za-z0-9_.-]$") ? chr : "_");
|
|
}
|
|
return join("", result);
|
|
}
|
|
|
|
function prepare_server_defaults(section) {
|
|
let protocol = config_get(section, "protocol", "vless");
|
|
if (!(protocol == "shadowsocks" || protocol == "socks" || protocol == "vmess" || protocol == "vless" ||
|
|
protocol == "trojan" || protocol == "hysteria2" || protocol == "mtproto" || protocol == "tailscale" ||
|
|
protocol == "json_inbound" || protocol == "awg"))
|
|
fatal("Server '" + section + "' has unsupported protocol '" + protocol + "'. Aborted.");
|
|
|
|
server_default_set_option(section, "protocol", protocol);
|
|
server_default_set_option(section, "label", section);
|
|
server_default_set_option(section, "enabled", "1");
|
|
server_default_set_option(section, "routing_mode", "rules");
|
|
|
|
if (protocol == "json_inbound") {
|
|
server_set_option(section, "security", "none");
|
|
return;
|
|
}
|
|
|
|
server_default_set_option(section, "listen", "0.0.0.0");
|
|
server_default_set_option(section, "listen_port", "443");
|
|
server_default_set_option(section, "public_host", server_detect_default_public_host());
|
|
|
|
let security = server_effective_security_value(section, protocol);
|
|
server_set_option(section, "security", security);
|
|
server_prepare_legacy_user_defaults(section, protocol);
|
|
|
|
if (protocol == "vless" || protocol == "vmess") {
|
|
if (uci_get(config_path(section, "server_uuid")) == "") {
|
|
let uuid = server_generate_uuid();
|
|
server_default_set_option(section, "server_uuid", uuid);
|
|
}
|
|
}
|
|
else if (protocol == "shadowsocks" || protocol == "socks" || protocol == "trojan" || protocol == "hysteria2") {
|
|
if (uci_get(config_path(section, "server_password")) == "") {
|
|
let password = server_generate_password();
|
|
server_default_set_option(section, "server_password", password);
|
|
}
|
|
}
|
|
|
|
if (protocol == "mtproto") {
|
|
let mtproto_secret = uci_get(config_path(section, "mtproto_secret"));
|
|
let mtproto_faketls = "";
|
|
let mtproto_base_secret = "";
|
|
if (mtproto_secret != "") {
|
|
mtproto_faketls = mtproto_faketls_value(mtproto_secret);
|
|
mtproto_base_secret = mtproto_base_secret_value(mtproto_secret);
|
|
}
|
|
if (mtproto_base_secret == "")
|
|
mtproto_base_secret = server_generate_mtproto_secret();
|
|
if (mtproto_secret != mtproto_base_secret)
|
|
server_set_option(section, "mtproto_secret", mtproto_base_secret);
|
|
server_default_set_option(section, "mtproto_faketls", mtproto_faketls != "" ? mtproto_faketls : "google.com");
|
|
server_default_set_option(section, "mtproto_padding", "1");
|
|
server_default_set_option(section, "mtproto_domain_fronting_port", "443");
|
|
server_default_set_option(section, "mtproto_prefer_ip", "prefer-ipv4");
|
|
server_default_set_option(section, "mtproto_tolerate_time_skewness", "3s");
|
|
server_default_set_option(section, "mtproto_idle_timeout", "5m");
|
|
server_default_set_option(section, "mtproto_handshake_timeout", "10s");
|
|
}
|
|
|
|
if (protocol == "socks") {
|
|
server_default_set_option(section, "socks_auth_enabled", "1");
|
|
if (uci_get(config_path(section, "server_username")) == "")
|
|
server_default_set_option(section, "server_username", config_get(section, "label", section));
|
|
}
|
|
if (protocol == "vless")
|
|
server_default_set_option(section, "vless_flow", "none");
|
|
if (protocol == "vmess")
|
|
server_default_set_option(section, "vmess_alter_id", "0");
|
|
if (protocol == "shadowsocks")
|
|
server_default_set_option(section, "shadowsocks_method", "aes-128-gcm");
|
|
if (protocol == "awg") {
|
|
server_default_set_option(section, "listen_port", "51820");
|
|
server_default_set_option(section, "awg_mtu", "1280");
|
|
server_default_set_option(section, "awg_local_address", "10.0.0.1/24");
|
|
server_default_set_option(section, "awg_client_address", "10.0.0.2/32");
|
|
server_default_set_option(section, "awg_jc", "4");
|
|
server_default_set_option(section, "awg_jmin", "40");
|
|
server_default_set_option(section, "awg_jmax", "70");
|
|
server_default_set_option(section, "awg_s1", "0");
|
|
server_default_set_option(section, "awg_s2", "0");
|
|
server_default_set_option(section, "awg_s3", "0");
|
|
server_default_set_option(section, "awg_s4", "0");
|
|
server_default_set_option(section, "awg_h1", "1");
|
|
server_default_set_option(section, "awg_h2", "2");
|
|
server_default_set_option(section, "awg_h3", "3");
|
|
server_default_set_option(section, "awg_h4", "4");
|
|
server_default_set_option(section, "awg_keepalive", "25");
|
|
|
|
let server_private_key = uci_get(config_path(section, "awg_private_key"));
|
|
let peer_public_key = uci_get(config_path(section, "awg_peer_public_key"));
|
|
if (server_private_key == "" || peer_public_key == "") {
|
|
let srv_pair = generate_wg_keypair_values();
|
|
let cli_pair = generate_wg_keypair_values();
|
|
if (srv_pair != null) {
|
|
server_set_option(section, "awg_private_key", srv_pair.private_key);
|
|
server_set_option(section, "awg_server_public_key", srv_pair.public_key);
|
|
}
|
|
if (cli_pair != null) {
|
|
server_set_option(section, "awg_client_private_key", cli_pair.private_key);
|
|
server_set_option(section, "awg_peer_public_key", cli_pair.public_key);
|
|
}
|
|
}
|
|
}
|
|
if (protocol == "tailscale") {
|
|
let safe_name = safe_filename_string(section);
|
|
server_default_set_option(section, "tailscale_control_url", "https://controlplane.tailscale.com");
|
|
server_default_set_option(section, "tailscale_hostname", "tachyon-" + safe_name);
|
|
server_default_set_option(section, "tailscale_advertise_exit_node", "1");
|
|
}
|
|
|
|
if (security == "reality") {
|
|
server_default_set_option(section, "tls_server_name", "www.microsoft.com");
|
|
server_default_set_option(section, "client_fingerprint", "chrome");
|
|
server_default_set_option(section, "reality_handshake_server", "www.microsoft.com");
|
|
server_default_set_option(section, "reality_handshake_server_port", "443");
|
|
if (uci_get(config_path(section, "reality_short_id")) == "")
|
|
server_default_set_option(section, "reality_short_id", server_generate_short_id());
|
|
server_default_set_option(section, "reality_max_time_difference", "1m");
|
|
|
|
let reality_private_key = uci_get(config_path(section, "reality_private_key"));
|
|
let reality_public_key = uci_get(config_path(section, "reality_public_key"));
|
|
if (reality_private_key == "" || reality_public_key == "") {
|
|
log("Generating Reality key pair for server '" + section + "'", "info");
|
|
let pair = generate_reality_keypair_values();
|
|
if (pair == null)
|
|
fatal("Failed to generate Reality key pair for server '" + section + "'. Aborted.");
|
|
server_set_option(section, "reality_private_key", pair.private_key);
|
|
server_set_option(section, "reality_public_key", pair.public_key);
|
|
}
|
|
}
|
|
|
|
server_prepare_tls_defaults(section, protocol, security);
|
|
}
|
|
|
|
function cleanup_orphaned_server_certs() {
|
|
let cert_dir = "/etc/tachyon/server-certs";
|
|
try {
|
|
let dir = fs.opendir(cert_dir);
|
|
if (!dir) return;
|
|
let active_names = {};
|
|
for (let section in uci_sections("server")) {
|
|
let name = safe_filename_string(section);
|
|
active_names[name + ".crt"] = true;
|
|
active_names[name + ".key"] = true;
|
|
}
|
|
let entry;
|
|
while ((entry = dir.read()) != null) {
|
|
if (substr(entry, 0, 1) == ".") continue;
|
|
if (active_names[entry]) continue;
|
|
let path = cert_dir + "/" + entry;
|
|
// Absent file already satisfies the caller; fs.unlink throws on ENOENT.
|
|
try { fs.unlink(path); } catch(e) {}
|
|
}
|
|
dir.close();
|
|
}
|
|
catch (e) {
|
|
// Best-effort housekeeping: a missing cert directory or an unreadable
|
|
// UCI section leaves stale certs on disk, which costs a few kilobytes
|
|
// and nothing else. Never worth failing the caller over.
|
|
}
|
|
}
|
|
|
|
function prepare_all_server_defaults() {
|
|
SERVER_DEFAULTS_CHANGED = false;
|
|
for (let section in uci_sections("server"))
|
|
prepare_server_defaults(section);
|
|
cleanup_orphaned_server_certs();
|
|
if (SERVER_DEFAULTS_CHANGED)
|
|
uci_commit(CONFIG_NAME);
|
|
}
|
|
|
|
let mode = ARGV[0] || "";
|
|
|
|
if (mode == "valid-file-path")
|
|
exit(valid_file_path(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-dotted-ipv4")
|
|
exit(valid_dotted_ipv4(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "stdin-first-nonempty-line")
|
|
stdin_first_nonempty_line();
|
|
else if (mode == "stdin-remove-newlines")
|
|
stdin_remove_newlines();
|
|
else if (mode == "shell-single-quote")
|
|
shell_single_quote(ARGV[1]);
|
|
else if (mode == "valid-port")
|
|
exit(valid_port(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "normalize-host")
|
|
print(normalize_host(ARGV[1]), "\n");
|
|
else if (mode == "valid-host")
|
|
exit(valid_host(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-absolute-path")
|
|
exit(valid_absolute_path(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-http-url")
|
|
exit(valid_http_url(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-duration")
|
|
exit(valid_duration(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-client-value")
|
|
exit(valid_client_value(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-uuid")
|
|
exit(valid_uuid(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-nonnegative-integer")
|
|
exit(valid_nonnegative_integer(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-reality-short-id")
|
|
exit(valid_reality_short_id(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-transport-service-name")
|
|
exit(valid_transport_service_name(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "mtproto-base-secret")
|
|
mtproto_base_secret_from_value(ARGV[1]);
|
|
else if (mode == "mtproto-faketls-from-secret")
|
|
mtproto_faketls_from_secret(ARGV[1]);
|
|
else if (mode == "mtproto-build-secret")
|
|
mtproto_build_secret(ARGV[1], ARGV[2], ARGV[3]);
|
|
else if (mode == "valid-mtproto-base-secret")
|
|
exit(valid_mtproto_base_secret(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "default-security-for-protocol")
|
|
default_security_for_protocol(ARGV[1]);
|
|
else if (mode == "effective-security")
|
|
print_effective_security(ARGV[1], ARGV[2]);
|
|
else if (mode == "safe-filename")
|
|
safe_filename(ARGV[1]);
|
|
else if (mode == "valid-public-ipv4")
|
|
exit(valid_public_ipv4(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "valid-cidr")
|
|
exit(valid_cidr(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "server-users-from-tsv")
|
|
server_users_from_tsv(ARGV[1], ARGV[2]);
|
|
else if (mode == "json-length")
|
|
json_length(ARGV[1]);
|
|
else if (mode == "array-append-string")
|
|
array_append_string(ARGV[1]);
|
|
else if (mode == "error-response")
|
|
error_response(ARGV[1]);
|
|
else if (mode == "prepare-all-defaults")
|
|
prepare_all_server_defaults();
|
|
else if (mode == "generate-reality-keypair")
|
|
generate_reality_keypair_cli();
|
|
else if (mode == "generate-wg-keypair")
|
|
generate_wg_keypair_cli();
|
|
else if (mode == "tls-certificate-sha256")
|
|
get_tls_certificate_sha256_cli(ARGV[1]);
|
|
else if (mode == "reality-keypair-response")
|
|
reality_keypair_response(ARGV[1], ARGV[2]);
|
|
else if (mode == "reality-keypair-tsv")
|
|
reality_keypair_tsv();
|
|
else if (mode == "write-tls-keypair-files")
|
|
write_tls_keypair_files(ARGV[1], ARGV[2]);
|
|
else if (mode == "certificate-base64")
|
|
certificate_base64(ARGV[1]);
|
|
else if (mode == "valid-sha256-hex")
|
|
exit(valid_sha256_hex(ARGV[1]) ? 0 : 1);
|
|
else if (mode == "tls-fingerprint-response")
|
|
tls_fingerprint_response(ARGV[1]);
|
|
else {
|
|
warn("Usage: server/service.uc <operation> ...\n");
|
|
exit(1);
|
|
} |