Features: - Per-Section MAC Exclusions: nft, generator_routes, section.js, i18n - Per-Section Routed DNS: connections, generator_routes, nft, validator, types.ts, section.js, state.uc, reload.uc - Boot Latency Sweep: ui.uc latency boot sweep with fixture dispatchers - WDTT provider: full backend (common, validator, runtime), sing-box outbound, diagnostics, component install, updater, i18n, section.js UI - OlcRTC provider: full backend (common, validator, runtime), sing-box outbound, diagnostics, component install, updater, i18n, section.js UI - WireGuard-over-TURN tunnel (WDTT) and TCP-over-WebRTC tunnel (OlcRTC) as sidecar processes with local SOCKS5, routed through sing-box outbound Bug fixes: - route.uc: restrict hijack-dns to local DNS inbounds only (dns-in, source-dns-in, dns-health-*) — fixes socket collision between sing-box and dnsmasq on port 53 via SO_REUSEADDR transparent write-back sockets - runtime.uc: add Port 53 socket collision diagnostic with auto-fix (sing-box restart to clear parasitic UNCONN sockets with Recv-Q) - runtime.uc: add dnsmasq dns_redirect check with auto-fix — disables firewall4 DNAT that causes TProxy to rewrite destination to router :53 - failover.uc: add wdtt/olcrtc to failover candidate actions - telegram.uc: show WDTT/OlcRTC installed status in /info Tests: 135/155 shell + 591/591 frontend
240 lines
6.5 KiB
Ucode
240 lines
6.5 KiB
Ucode
let common = require("core.common");
|
|
let fs = require("fs");
|
|
|
|
let as_string = common.as_string;
|
|
|
|
const WDTT_MODES = [ "selective", "lan-all", "full" ];
|
|
const QWDTT_MODES = [ "rawtun", "socks", "vpn" ];
|
|
const WDTT_COMMUNITY_LISTS = [
|
|
"russia-inside", "russia-outside", "ukraine", "telegram", "meta",
|
|
"youtube", "discord", "tiktok", "twitter", "hdrezka", "roblox",
|
|
"cloudflare", "cloudfront", "google_ai", "google_meet", "google_play",
|
|
"hetzner", "ovh", "digitalocean", "anime", "news", "geoblock",
|
|
"block", "porn", "hodca"
|
|
];
|
|
|
|
function url_decode(value) {
|
|
let result = "";
|
|
let len = length(value);
|
|
let i = 0;
|
|
while (i < len) {
|
|
if (value[i] == '%' && i + 2 < len) {
|
|
let hex = substr(value, i + 1, 2);
|
|
let code = parseInt(hex, 16);
|
|
if (code != null && !isNaN(code))
|
|
result += chr(code);
|
|
else
|
|
result += '%';
|
|
i += 3;
|
|
}
|
|
else if (value[i] == '+') {
|
|
result += ' ';
|
|
i += 1;
|
|
}
|
|
else {
|
|
result += value[i];
|
|
i += 1;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function parse_query_string(query) {
|
|
let params = {};
|
|
if (query == "" || query == null)
|
|
return params;
|
|
let pairs = split(query, "&");
|
|
for (let pair in pairs) {
|
|
let kv = split(pair, "=", 2);
|
|
if (length(kv) == 2)
|
|
params[kv[0]] = url_decode(kv[1]);
|
|
else if (length(kv) == 1)
|
|
params[kv[0]] = "";
|
|
}
|
|
return params;
|
|
}
|
|
|
|
function parse_wdtt_uri(value) {
|
|
value = trim(as_string(value));
|
|
if (substr(value, 0, 7) != "wdtt://")
|
|
return { valid: false, reason: "URI must start with wdtt://" };
|
|
|
|
let rest = substr(value, 7);
|
|
let hash_idx = index(rest, "#");
|
|
let label = "";
|
|
if (hash_idx >= 0) {
|
|
label = substr(rest, hash_idx + 1);
|
|
rest = substr(rest, 0, hash_idx);
|
|
}
|
|
|
|
let params = parse_query_string(rest);
|
|
let peer = params.peer || "";
|
|
let hashes = params.hashes || "";
|
|
let pass = params.pass || "";
|
|
let workers = params.workers || "";
|
|
let port = params.port || "";
|
|
|
|
if (peer == "")
|
|
return { valid: false, reason: "wdtt:// URI must contain peer parameter" };
|
|
|
|
return {
|
|
valid: true,
|
|
peer: peer,
|
|
hashes: hashes,
|
|
pass: pass,
|
|
workers: workers,
|
|
port: port,
|
|
label: label
|
|
};
|
|
}
|
|
|
|
function valid_peer(value) {
|
|
value = trim(as_string(value));
|
|
if (value == "" || value == null)
|
|
return true;
|
|
return match(value, /^[^ \t\r\n:]+:[0-9]{1,5}$/) != null;
|
|
}
|
|
|
|
function valid_url(value) {
|
|
value = trim(as_string(value));
|
|
if (value == "" || value == null)
|
|
return true;
|
|
return match(value, /^https?:\/\/[^ \t\r\n]+$/) != null;
|
|
}
|
|
|
|
function valid_number(value, min, max) {
|
|
value = as_string(value);
|
|
if (value == "" || value == null)
|
|
return true;
|
|
let num = int(value);
|
|
if (num == null || isNaN(num))
|
|
return false;
|
|
return num >= min && num <= max;
|
|
}
|
|
|
|
function valid_mode(value) {
|
|
if (value == "" || value == null)
|
|
return true;
|
|
return array_contains(WDTT_MODES, value);
|
|
}
|
|
|
|
function valid_qwdtt_mode(value) {
|
|
if (value == "" || value == null)
|
|
return true;
|
|
return array_contains(QWDTT_MODES, value);
|
|
}
|
|
|
|
function valid_community_list(value) {
|
|
return array_contains(WDTT_COMMUNITY_LISTS, value);
|
|
}
|
|
|
|
function list_values(value) {
|
|
if (type(value) == "array")
|
|
return value;
|
|
value = trim(as_string(value));
|
|
if (value == "" || value == null)
|
|
return [];
|
|
return split(value, " \t\n\r");
|
|
}
|
|
|
|
function validation_result(valid, message, options) {
|
|
let result = { valid: valid, message: message || "" };
|
|
if (options != null)
|
|
for (let key in options)
|
|
result[key] = options[key];
|
|
return result;
|
|
}
|
|
|
|
function validate_section(section) {
|
|
let errors = [];
|
|
|
|
let peer = as_string(section.peer || "");
|
|
if (peer != "" && !valid_peer(peer))
|
|
errors.push("peer must be in HOST:PORT format");
|
|
|
|
let mode = as_string(section.mode || "");
|
|
if (!valid_mode(mode))
|
|
errors.push("mode must be one of: " + join(", ", WDTT_MODES));
|
|
|
|
let qwdtt_mode = as_string(section.qwdtt_mode || "");
|
|
if (!valid_qwdtt_mode(qwdtt_mode))
|
|
errors.push("qwdtt_mode must be one of: " + join(", ", QWDTT_MODES));
|
|
|
|
let workers = as_string(section.workers || "");
|
|
if (workers != "" && !valid_number(workers, 1, 1024))
|
|
errors.push("workers must be 1-1024");
|
|
|
|
let max_hashes = as_string(section.max_hashes || "");
|
|
if (max_hashes != "" && !valid_number(max_hashes, 1, 64))
|
|
errors.push("max_hashes must be 1-64");
|
|
|
|
let mtu = as_string(section.mtu || "");
|
|
if (mtu != "" && !valid_number(mtu, 576, 1500))
|
|
errors.push("mtu must be 576-1500");
|
|
|
|
let subscription_links = list_values(section.subscription_links);
|
|
for (let link in subscription_links) {
|
|
link = trim(as_string(link));
|
|
if (link == "")
|
|
continue;
|
|
if (substr(link, 0, 7) == "wdtt://") {
|
|
let parsed = parse_wdtt_uri(link);
|
|
if (!parsed.valid)
|
|
errors.push("invalid wdtt:// URI: " + parsed.reason);
|
|
}
|
|
else if (substr(link, 0, 1) == "/") {
|
|
/* local file — ok */
|
|
}
|
|
else if (!valid_url(link))
|
|
errors.push("subscription link must be HTTP URL, wdtt:// URI, or local file path: " + link);
|
|
}
|
|
|
|
let community_lists = list_values(section.community_lists);
|
|
for (let cl in community_lists) {
|
|
if (!valid_community_list(cl))
|
|
errors.push("unknown community list: " + cl);
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
function validate_section_json() {
|
|
let input = read_stdin();
|
|
let data = object_or_empty(json(input));
|
|
let errors = validate_section(data);
|
|
if (length(errors) > 0)
|
|
write_json({ valid: false, message: join("; ", errors) });
|
|
else
|
|
write_json({ valid: true, message: "" });
|
|
return length(errors) == 0;
|
|
}
|
|
|
|
function read_stdin() {
|
|
let input = fs.open("/dev/stdin", "r");
|
|
if (!input)
|
|
return "";
|
|
let data = input.read("all");
|
|
input.close();
|
|
return data == null ? "" : data;
|
|
}
|
|
|
|
function write_json(value) {
|
|
printf("%s\n", sprintf("%J", value));
|
|
}
|
|
|
|
return {
|
|
validate_section,
|
|
validate_section_json,
|
|
parse_wdtt_uri,
|
|
url_decode,
|
|
valid_peer,
|
|
valid_mode,
|
|
valid_qwdtt_mode,
|
|
valid_url,
|
|
valid_community_list,
|
|
list_values,
|
|
modes: WDTT_MODES,
|
|
qwdtt_modes: QWDTT_MODES,
|
|
community_lists: WDTT_COMMUNITY_LISTS
|
|
};
|