small-packages/homeproxy/root/usr/share/rpcd/ucode/luci.homeproxy
2026-07-12 00:50:47 +08:00

958 lines
28 KiB
Plaintext

#!/usr/bin/ucode
/*
* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2023-2024 ImmortalWrt.org
*/
'use strict';
import { access, lstat, popen, readfile, writefile } from 'fs';
import { cursor } from 'uci';
import {
removeBlankAttrs, renderEndpoint, renderOutbound
} from '/etc/homeproxy/scripts/homeproxy.uc';
/* Kanged from ucode/luci */
function shellquote(s) {
return `'${replace(s, "'", "'\\''")}'`;
}
function hasKernelModule(kmod) {
return (system(sprintf('[ -e "/lib/modules/$(uname -r)"/%s ]', shellquote(kmod))) === 0);
}
const HP_DIR = '/etc/homeproxy';
const RUN_DIR = '/var/run/homeproxy';
const RESOURCE_SOURCES = {
china_ip4: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/surge-rules@release/cncidr.txt',
china_ip6: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/surge-rules@release/cncidr.txt',
china_list: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/surge-rules@release/direct.txt',
gfw_list: 'https://cdn.jsdelivr.net/gh/Loyalsoldier/surge-rules@release/gfw.txt'
};
const methods = {
acllist_read: {
args: { type: 'type' },
call: function(req) {
if (index(['direct_list', 'proxy_list'], req.args?.type) === -1)
return { content: null, error: 'illegal type' };
const filecontent = readfile(`${HP_DIR}/resources/${req.args?.type}.txt`);
return { content: filecontent };
}
},
acllist_write: {
args: { type: 'type', content: 'content' },
call: function(req) {
if (index(['direct_list', 'proxy_list'], req.args?.type) === -1)
return { result: false, error: 'illegal type' };
const file = `${HP_DIR}/resources/${req.args?.type}.txt`;
let content = req.args?.content;
/* Sanitize content */
if (content) {
content = trim(content);
content = replace(content, /\r\n?/g, '\n');
if (!match(content, /\n$/))
content += '\n';
}
system(`mkdir -p ${HP_DIR}/resources`);
writefile(file, content);
return { result: true };
}
},
certificate_write: {
args: { filename: 'filename' },
call: function(req) {
const writeCertificate = (filename, kind) => {
const tmpcert = '/tmp/homeproxy_certificate.tmp';
const filestat = lstat(tmpcert);
if (!filestat || filestat.type !== 'file' || filestat.size <= 0) {
system(`rm -f ${tmpcert}`);
return { result: false, error: 'empty certificate file' };
}
let filecontent = readfile(tmpcert);
if (is_binary(filecontent)) {
system(`rm -f ${tmpcert}`);
return { result: false, error: 'illegal file type: binary' };
}
/* Kanged from luci-proto-openconnect */
const beg = (kind === 'private') ? /^-----BEGIN (RSA|EC) PRIVATE KEY-----$/ :
((kind === 'ech') ? /^-----BEGIN ECH CONFIGS-----$/ : /^-----BEGIN CERTIFICATE-----$/),
end = (kind === 'private') ? /^-----END (RSA|EC) PRIVATE KEY-----$/ :
((kind === 'ech') ? /^-----END ECH CONFIGS-----$/ : /^-----END CERTIFICATE-----$/),
lines = split(trim(filecontent), /[\r\n]/);
let start = false, i;
for (i = 0; i < length(lines); i++) {
if (match(lines[i], beg))
start = true;
else if (start && !b64dec(lines[i]) && length(lines[i]) !== 64)
break;
}
if (!start || i < length(lines) - 1 || !match(lines[i], end)) {
system(`rm -f ${tmpcert}`);
return { result: false, error: 'this does not look like a correct PEM file' };
}
/* Sanitize certificate */
filecontent = trim(filecontent);
filecontent = replace(filecontent, /\r\n?/g, '\n');
if (!match(filecontent, /\n$/))
filecontent += '\n';
system(`mkdir -p ${HP_DIR}/certs`);
const certfile = `${HP_DIR}/certs/${filename}.pem`;
writefile(certfile, filecontent);
if (kind === 'private')
system(`chmod 600 ${certfile}`);
system(`rm -f ${tmpcert}`);
return { result: true };
};
const filename = req.args?.filename;
switch (filename) {
case 'client_ca':
case 'server_publickey':
return writeCertificate(filename, 'certificate');
break;
case 'client_ech_conf':
return writeCertificate(filename, 'ech');
break;
case 'server_privatekey':
return writeCertificate(filename, 'private');
break;
default:
return { result: false, error: 'illegal cerificate filename' };
break;
}
}
},
connection_check: {
args: { site: 'site' },
call: function(req) {
let url;
switch(req.args?.site) {
case 'baidu':
url = 'https://www.baidu.com';
break;
case 'google':
url = 'https://www.google.com';
break;
default:
return { result: false, error: 'illegal site' };
break;
}
return { result: (system(`/usr/bin/wget --spider -qT3 ${url} 2>"/dev/null"`, 3100) === 0) };
}
},
current_node_get: {
call: function() {
const uci = cursor();
uci.load('homeproxy');
const formatNode = (section_id, fallback_name) => {
if (!section_id || section_id in ['nil', 'same'])
return { id: section_id, label: fallback_name || section_id, type: null };
const section = uci.get_all('homeproxy', section_id);
if (!section || section['.type'] !== 'node')
return { id: section_id, label: fallback_name || section_id, type: null };
const address = trim(section.address || '');
const port = trim(section.port || '');
const label = trim(section.label || '') || ((address && port) ? `${address}:${port}` : section_id);
return {
id: section_id,
label,
type: section.type || null
};
};
const main_node = trim(uci.get('homeproxy', 'config', 'main_node') || 'nil');
if (main_node !== 'urltest')
return {
mode: main_node,
active: formatNode(main_node, (main_node === 'nil') ? 'Disable' : null),
error: null
};
const fd = popen('/usr/bin/wget -qO- http://127.0.0.1:9090/proxies/main-out 2>"/dev/null"');
if (!fd)
return {
mode: 'urltest',
active: { id: 'urltest', label: 'URLTest', type: 'urltest' },
error: 'clash_api_unavailable'
};
let payload = '', line;
for (line = fd.read('line'); length(line); line = fd.read('line'))
payload += line;
fd.close();
let data = null;
try {
data = json(payload);
} catch (e) {}
const tag = trim(data?.now || '');
if (!tag)
return {
mode: 'urltest',
active: { id: 'urltest', label: 'URLTest', type: 'urltest' },
error: 'missing_current_proxy'
};
const matched = match(tag, /^cfg-(.*)-out$/);
const section_id = matched ? matched[1] : tag;
return {
mode: 'urltest',
active: formatNode(section_id, tag),
error: null
};
}
},
node_latency_test: {
args: { node: 'node', mode: 'mode' },
call: function(req) {
const error_codes = {
invalid_node: 'invalid_node',
invalid_mode: 'invalid_mode',
mode_mismatch: 'mode_mismatch',
rp_invalid_outbound: 'rp_invalid_outbound',
rp_runtime_prepare_failed: 'rp_runtime_prepare_failed',
rp_runtime_launch_failed: 'rp_runtime_launch_failed',
rp_runtime_timeout: 'rp_runtime_timeout',
rp_delay_timeout: 'rp_delay_timeout',
rp_delay_failed: 'rp_delay_failed',
icmp_probe_unavailable: 'icmp_probe_unavailable',
icmp_timeout: 'icmp_timeout',
icmp_failed: 'icmp_failed',
probe_timeout: 'probe_timeout',
backend_execution_failed: 'backend_execution_failed',
probe_failed: 'probe_failed'
};
const probe_timeout_s = 3;
const probe_timeout_ms = probe_timeout_s * 1000;
const probe_system_timeout_ms = probe_timeout_ms + 250;
const rp_probe_url = 'https://cp.cloudflare.com/generate_204';
const rp_probe_url_encoded = 'https%3A%2F%2Fcp.cloudflare.com%2Fgenerate_204';
const rp_delay_timeout_ms = 5000;
const formatResult = (node, state, latency_ms, err, extras) => ({
node,
state,
latency_ms,
measured_at: time(),
error: err,
icmp_latency_ms: (extras && extras.icmp_latency_ms != null) ? extras.icmp_latency_ms : null,
icmp_error: (extras && extras.icmp_error) ? extras.icmp_error : null,
rp_runtime: (extras && extras.rp_runtime) ? extras.rp_runtime : null
});
const isControllerPortBusy = (port) => {
const hex_port = sprintf('%04X', int(port));
const tcp_v4 = readfile('/proc/net/tcp') || '';
const tcp_v6 = readfile('/proc/net/tcp6') || '';
for (let line in split(tcp_v4 + '\n' + tcp_v6, /\n/)) {
const fields = split(trim(line), /\s+/);
if (length(fields) < 4)
continue;
const local_addr = split(fields[1], ':');
const state = fields[3];
if (length(local_addr) !== 2)
continue;
if (local_addr[1] === hex_port && state === '0A')
return true;
}
return false;
};
const pickControllerPort = (seed) => {
const min_port = 19090;
const span = 100;
let hash_seed = 0;
for (let i = 0; i < length(seed); i++)
hash_seed += ord(seed, i);
const start = hash_seed % span;
for (let i = 0; i < span; i++) {
const candidate = min_port + ((start + i) % span);
if (candidate === 9090)
continue;
if (!isControllerPortBusy(candidate))
return candidate;
}
return null;
};
const listRuntimePidsByConfig = (config_path) => {
if (!config_path)
return [];
const scan_cmd = sprintf(
'/bin/sh -c %s -- %s',
shellquote('cfg="$1"; for proc in /proc/[0-9]*; do [ -d "$proc" ] || continue; pid="${proc#/proc/}"; [ -r "$proc/cmdline" ] || continue; found_cfg=0; has_sig=0; p1=""; p2=""; set -f; for arg in $(tr "\\000" "\\n" <"$proc/cmdline" 2>/dev/null); do [ "$arg" = "$cfg" ] && found_cfg=1; [ "${p2##*/}" = "sing-box" ] && [ "$p1" = "run" ] && [ "$arg" = "--config" ] && has_sig=1; p2="$p1"; p1="$arg"; done; set +f; [ "$has_sig" -eq 1 ] && [ "$found_cfg" -eq 1 ] && printf "%s\\n" "$pid"; done'),
shellquote(config_path)
);
const fd = popen(scan_cmd);
if (!fd)
return [];
let pids = [];
let line;
for (line = fd.read('line'); line !== null && line !== ''; line = fd.read('line')) {
const pid = trim(line);
if (match(pid, /^[0-9]+$/) && index(pids, pid) === -1)
push(pids, pid);
}
fd.close();
return pids;
};
const isRuntimeProcess = (pid, config_path) => {
if (!pid || !match(pid, /^[0-9]+$/) || !config_path)
return false;
if (system(`/bin/kill -0 ${shellquote(pid)} >/dev/null 2>&1`) !== 0)
return false;
return (index(listRuntimePidsByConfig(config_path), pid) !== -1);
};
const findRuntimePidByConfig = (config_path) => {
const pids = listRuntimePidsByConfig(config_path);
for (let i = 0; i < length(pids); i++) {
const pid = pids[i];
if (system(`/bin/kill -0 ${shellquote(pid)} >/dev/null 2>&1`) === 0)
return pid;
}
return null;
};
const resolveRuntimePid = (runtime_meta) => {
if (!runtime_meta?.config_path)
return null;
const pid_file = trim(readfile(runtime_meta.pid_path) || '');
if (isRuntimeProcess(pid_file, runtime_meta.config_path))
return pid_file;
const discovered = findRuntimePidByConfig(runtime_meta.config_path);
if (discovered)
writefile(runtime_meta.pid_path, discovered + '\n');
return discovered;
};
const cleanupProbeRuntime = (runtime_meta) => {
if (!runtime_meta)
return;
const terminateRuntimePid = (pid) => {
if (!pid)
return;
system(`/bin/kill ${shellquote(pid)} >/dev/null 2>&1`);
system('/bin/sleep 1');
if (system(`/bin/kill -0 ${shellquote(pid)} >/dev/null 2>&1`) === 0)
system(`/bin/kill -9 ${shellquote(pid)} >/dev/null 2>&1`);
};
let pid = resolveRuntimePid(runtime_meta);
while (pid) {
terminateRuntimePid(pid);
system(`/bin/rm -f ${shellquote(runtime_meta.pid_path)}`);
pid = findRuntimePidByConfig(runtime_meta.config_path);
}
system(`/bin/rm -rf ${shellquote(runtime_meta.session_dir)}`);
};
const waitControllerReady = (runtime_meta, timeout_ms) => {
try {
let retries = int((timeout_ms + 999) / 1000);
if (!retries || retries < 1)
retries = 1;
for (let i = 0; i < retries; i++) {
resolveRuntimePid(runtime_meta);
const probe_fd = popen(`/usr/bin/wget -qO- ${shellquote(runtime_meta.controller_url + '/version')} 2>"/dev/null"`);
if (probe_fd) {
let payload = '', line;
while (true) {
line = probe_fd.read('line');
if (line === null || line === '')
break;
payload += line;
}
probe_fd.close();
if (trim(payload))
return true;
}
if (i + 1 < retries)
system('/bin/sleep 1');
}
return false;
} catch (e) {
return false;
}
};
const buildProbeRuntime = (node_name, node_cfg) => {
const runtime_root = RUN_DIR + '/latency-test';
const session_id = sprintf('%s-%d-%d', node_name, time(), int(readfile('/proc/uptime') || '0'));
const session_dir = `${runtime_root}/${session_id}`;
const config_path = `${session_dir}/sing-box.json`;
const log_path = `${session_dir}/sing-box.log`;
const cache_path = `${session_dir}/cache.db`;
const pid_path = `${session_dir}/sing-box.pid`;
const controller_port = pickControllerPort(node_name || 'real-proxy');
if (!controller_port)
return { error: 'no available temporary controller port' };
const selected_tag = 'probe-out';
const config = {
log: {
disabled: false,
level: 'warn',
output: log_path,
timestamp: true
},
outbounds: [
{
type: 'direct',
tag: 'direct-out'
},
{
type: 'block',
tag: 'block-out'
}
],
endpoints: [],
route: {
final: selected_tag,
auto_detect_interface: true
},
experimental: {
clash_api: {
external_controller: `127.0.0.1:${controller_port}`
},
cache_file: {
enabled: true,
path: cache_path
}
}
};
const rendered_outbound = (node_cfg.type === 'wireguard') ? renderEndpoint(node_cfg) : renderOutbound(node_cfg);
if (rendered_outbound && type(rendered_outbound) === 'object')
rendered_outbound.tag = selected_tag;
if (!rendered_outbound || type(rendered_outbound) !== 'object' || rendered_outbound.tag !== selected_tag || !rendered_outbound.type)
return {
error: 'failed to render temporary outbound for selected node',
code: error_codes.rp_invalid_outbound
};
if (node_cfg.type === 'wireguard')
push(config.endpoints, rendered_outbound);
else
push(config.outbounds, rendered_outbound);
if (system(`/bin/mkdir -p ${shellquote(session_dir)}`) !== 0)
return { error: 'failed to prepare temporary runtime directory' };
if (!writefile(config_path, sprintf('%.J\n', removeBlankAttrs(config)))) {
system(`/bin/rm -rf ${shellquote(session_dir)}`);
return { error: 'failed to write temporary sing-box config' };
}
return {
runtime_root,
session_dir,
config_path,
log_path,
cache_path,
pid_path,
controller_port,
controller_url: `http://127.0.0.1:${controller_port}`
};
};
const launchProbeRuntime = (runtime_meta) => {
if (!runtime_meta)
return false;
const launch_cmd = sprintf(
'/usr/bin/sing-box run --config %s >/dev/null 2>&1 & pid=$!; [ -n "$pid" ] && printf "%%s\\n" "$pid" > %s',
shellquote(runtime_meta.config_path),
shellquote(runtime_meta.pid_path)
);
if (system(`/bin/sh -c ${shellquote(launch_cmd)}`) !== 0)
return false;
for (let i = 0; i < 5; i++) {
const pid = resolveRuntimePid(runtime_meta);
if (pid)
return true;
system('/bin/sleep 1');
}
return false;
};
const monotonicMs = () => {
const uptime = trim(readfile('/proc/uptime') || '');
const parsed = match(uptime, /^([0-9]+)(\.([0-9]+))?/);
if (!parsed)
return null;
const sec = int(parsed[1]);
const frac = ((parsed[3] || '') + '000');
const msec = int(substr(frac, 0, 3));
return sec * 1000 + msec;
};
const probeRealProxyDelay = (runtime_meta, node_name) => {
const selected_tag = 'probe-out';
const delay_endpoint = sprintf(
'%s/proxies/%s/delay?url=%s&timeout=%d',
runtime_meta.controller_url,
selected_tag,
rp_probe_url_encoded,
rp_delay_timeout_ms
);
const started_ms = monotonicMs();
const fd = popen(sprintf(
'/usr/bin/wget -qO- -T %d %s 2>"/dev/null"',
int((rp_delay_timeout_ms + 999) / 1000),
shellquote(delay_endpoint)
));
if (!fd)
return {
state: 'timeout',
error: {
code: error_codes.rp_delay_timeout,
message: 'failed to open temporary Clash API delay endpoint'
}
};
let payload = '', line;
while (true) {
line = fd.read('line');
if (line === null || line === '')
break;
payload += line;
}
fd.close();
const ended_ms = monotonicMs();
if (!trim(payload || '')) {
if (started_ms !== null && ended_ms !== null && ended_ms - started_ms >= rp_delay_timeout_ms)
return {
state: 'timeout',
error: {
code: error_codes.rp_delay_timeout,
message: sprintf('temporary Clash API delay request timed out after %dms', rp_delay_timeout_ms)
}
};
return {
state: 'error',
error: {
code: error_codes.rp_delay_failed,
message: 'temporary Clash API delay response is empty'
}
};
}
let data = null;
try {
data = json(payload);
} catch (e) {
return {
state: 'error',
error: {
code: error_codes.rp_delay_failed,
message: 'temporary Clash API delay response is not valid JSON'
}
};
}
const delay_ms = int(data?.delay || '0');
if (delay_ms > 0)
return {
state: 'success',
latency_ms: delay_ms
};
const failure_msg = trim(sprintf('%s', data?.message || data?.error || '')) || 'temporary Clash API delay request failed';
if (match(failure_msg, /timeout/i) || (started_ms !== null && ended_ms !== null && ended_ms - started_ms >= rp_delay_timeout_ms))
return {
state: 'timeout',
error: {
code: error_codes.rp_delay_timeout,
message: failure_msg
}
};
return {
state: 'error',
error: {
code: error_codes.rp_delay_failed,
message: failure_msg
}
};
};
const normalizeLatencyMode = (m) => (m === 'tcp') ? 'icmp' : m;
const node = trim(req.args?.node || '') || null;
const requested_mode = trim(req.args?.mode || '');
const mode = normalizeLatencyMode(requested_mode);
const allowed_modes = ['icmp', 'rp'];
const allowed_configured_modes = ['icmp', 'tcp', 'rp'];
if (!node)
return formatResult(null, 'error', null, {
code: error_codes.invalid_node,
message: 'missing node section id'
});
if (index(allowed_modes, mode) === -1)
return formatResult(node, 'error', null, {
code: error_codes.invalid_mode,
message: sprintf('invalid latency test mode: %s', mode || '(empty)')
});
const uci = cursor();
uci.load('homeproxy');
const configured_mode_raw = trim(uci.get('homeproxy', 'subscription', 'latency_test_mode') || '');
const configured_mode = normalizeLatencyMode(configured_mode_raw);
if (configured_mode_raw && index(allowed_configured_modes, configured_mode_raw) === -1)
return formatResult(node, 'error', null, {
code: error_codes.invalid_mode,
message: sprintf('configured latency test mode is invalid: %s', configured_mode_raw || '(empty)')
});
if (configured_mode && configured_mode !== mode)
return formatResult(node, 'error', null, {
code: error_codes.mode_mismatch,
message: sprintf('request mode %s does not match configured mode %s', mode, configured_mode)
});
const section = uci.get_all('homeproxy', node);
if (!section || section['.type'] !== 'node')
return formatResult(node, 'error', null, {
code: error_codes.invalid_node,
message: 'node section does not exist or is not a node section'
});
const address = trim(section.address || '');
const port = int(section.port || '0');
if (!address || port <= 0 || port > 65535)
return formatResult(node, 'error', null, {
code: error_codes.invalid_node,
message: 'node address or port is invalid'
});
if (mode === 'rp') {
let runtime_meta = null;
let runtime_active = false;
let runtime_info = {
runtime_root: RUN_DIR + '/latency-test',
session_dir: null,
config_path: null,
pid_path: null,
controller: null,
launched: false,
controller_ready: false,
cleaned_up: false
};
runtime_meta = buildProbeRuntime(node, section);
if (runtime_meta?.error)
runtime_info.cleaned_up = true;
if (runtime_meta?.error)
return formatResult(node, 'error', null, {
code: runtime_meta.code || error_codes.rp_runtime_prepare_failed,
message: runtime_meta.error
}, {
rp_runtime: runtime_info
});
runtime_info.session_dir = runtime_meta.session_dir;
runtime_info.config_path = runtime_meta.config_path;
runtime_info.pid_path = runtime_meta.pid_path;
runtime_info.controller = `127.0.0.1:${runtime_meta.controller_port}`;
runtime_active = true;
const finalizeRealProxyResult = (state, latency_ms, err) => {
if (runtime_active && runtime_meta) {
cleanupProbeRuntime(runtime_meta);
runtime_active = false;
runtime_info.cleaned_up = true;
}
return formatResult(node, state, latency_ms, err, {
rp_runtime: runtime_info
});
};
try {
if (!launchProbeRuntime(runtime_meta))
return finalizeRealProxyResult('error', null, {
code: error_codes.rp_runtime_launch_failed,
message: 'failed to launch temporary sing-box runtime'
});
runtime_info.launched = true;
if (!waitControllerReady(runtime_meta, rp_delay_timeout_ms))
return finalizeRealProxyResult('timeout', null, {
code: error_codes.rp_runtime_timeout,
message: 'temporary sing-box runtime controller did not become ready in time'
});
runtime_info.controller_ready = true;
const delay_result = probeRealProxyDelay(runtime_meta, node);
if (delay_result.state === 'success' && delay_result.latency_ms > 0)
return finalizeRealProxyResult('success', delay_result.latency_ms, null);
if (delay_result.state === 'timeout')
return finalizeRealProxyResult('timeout', null, delay_result.error || {
code: error_codes.rp_delay_timeout,
message: sprintf('temporary Clash API delay request timed out after %dms', rp_delay_timeout_ms)
});
return finalizeRealProxyResult('error', null, delay_result.error || {
code: error_codes.rp_delay_failed,
message: sprintf('temporary Clash API delay probe failed for %s', rp_probe_url)
});
} catch (e) {
return finalizeRealProxyResult('error', null, {
code: error_codes.backend_execution_failed,
message: 'unexpected backend error while handling temporary rp runtime'
});
}
}
const ping = access('/bin/ping') ? '/bin/ping' : (access('/usr/bin/ping') ? '/usr/bin/ping' : null);
const runProbe = (command, timeout_ms) => {
const started_ms = monotonicMs();
const probe_exit = system(command, timeout_ms);
const ended_ms = monotonicMs();
let latency_ms = null;
if (started_ms !== null && ended_ms !== null && ended_ms >= started_ms)
latency_ms = ended_ms - started_ms;
return { probe_exit, latency_ms };
};
let icmp_latency_ms = null;
let icmp_error = null;
if (!ping) {
icmp_error = {
code: error_codes.icmp_probe_unavailable,
message: 'missing ping executable for ICMP probe'
};
}
else {
const result = runProbe(
`${ping} -c 1 -W ${probe_timeout_s} ${shellquote(address)} >/dev/null 2>/dev/null`,
probe_system_timeout_ms
);
if (result.probe_exit === 0)
icmp_latency_ms = (result.latency_ms !== null) ? result.latency_ms : 0;
else if (result.probe_exit === -1 || (result.latency_ms !== null && result.latency_ms >= probe_timeout_ms))
icmp_error = {
code: error_codes.icmp_timeout,
message: `ICMP ping timeout after ${probe_timeout_s}s`
};
else if (result.probe_exit === 127)
icmp_error = {
code: error_codes.backend_execution_failed,
message: 'ICMP ping command execution failed'
};
else
icmp_error = {
code: error_codes.icmp_failed,
message: `ICMP ping failed with exit code ${result.probe_exit}`
};
}
if (icmp_latency_ms !== null)
return formatResult(node, 'success', icmp_latency_ms, null, {
icmp_latency_ms,
icmp_error
});
if (icmp_error?.code === error_codes.icmp_timeout)
return formatResult(node, 'timeout', null, {
code: error_codes.probe_timeout,
message: `ICMP probe timed out after ${probe_timeout_s}s`
}, {
icmp_latency_ms,
icmp_error
});
return formatResult(node, 'error', null, icmp_error || {
code: error_codes.probe_failed,
message: 'ICMP probe failed'
}, {
icmp_latency_ms,
icmp_error
});
}
},
log_clean: {
args: { type: 'type' },
call: function(req) {
if (!(req.args?.type in ['homeproxy', 'sing-box-c', 'sing-box-s']))
return { result: false, error: 'illegal type' };
const filestat = lstat(`${RUN_DIR}/${req.args?.type}.log`);
if (filestat)
writefile(`${RUN_DIR}/${req.args?.type}.log`, '');
return { result: true };
}
},
singbox_generator: {
args: { type: 'type', params: 'params' },
call: function(req) {
if (!(req.args?.type in ['ech-keypair', 'uuid', 'reality-keypair', 'vapid-keypair', 'wg-keypair']))
return { result: false, error: 'illegal type' };
const type = req.args?.type;
let result = {};
const fd = popen(`/usr/bin/sing-box generate ${type} ${shellquote(req.args?.params || '')}`);
if (fd) {
let ech_cfg_set = false;
let ech_key_set = false;
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
if (type === 'uuid')
result.uuid = trim(line);
else if (type in ['reality-keypair', 'vapid-keypair', 'wg-keypair']) {
let priv = match(trim(line), /PrivateKey: (.*)/);
if (priv)
result.private_key = priv[1];
let pub = match(trim(line), /PublicKey: (.*)/);
if (pub)
result.public_key = pub[1];
} else if (type in ['ech-keypair']) {
if (trim(line) === '-----BEGIN ECH CONFIGS-----')
ech_cfg_set = true;
else if (trim(line) === '-----BEGIN ECH KEYS-----')
ech_key_set = true;
if (ech_cfg_set)
result.ech_cfg = result.ech_cfg ? result.ech_cfg + '\n' + trim(line) : trim(line) ;
if (ech_key_set)
result.ech_key = result.ech_key ? result.ech_key + '\n' + trim(line) : trim(line) ;
if (trim(line) === '-----END ECH CONFIGS-----')
ech_cfg_set = false;
else if (trim(line) === '-----END ECH KEYS-----')
ech_key_set = false;
}
}
fd.close();
}
return { result };
}
},
singbox_get_features: {
call: function() {
let features = {};
const fd = popen('/usr/bin/sing-box version');
if (fd) {
for (let line = fd.read('line'); length(line); line = fd.read('line')) {
if (match(trim(line), /^sing-box version (.*)/))
features.version = match(trim(line), /^sing-box version (.*)/)[1];
let tags = match(trim(line), /^Tags: (.*)/);
if (tags)
for (let i in split(tags[1], ','))
features[i] = true;
}
fd.close();
}
features.hp_has_ip_full = access('/usr/libexec/ip-full');
features.hp_has_tcp_brutal = hasKernelModule('brutal.ko');
features.hp_has_tproxy = hasKernelModule('nft_tproxy.ko') || access('/etc/modules.d/nft-tproxy');
features.hp_has_tun = hasKernelModule('tun.ko') || access('/etc/modules.d/30-tun');
return features;
}
},
resources_get: {
call: function() {
let resources = [];
for (let resource_type in keys(RESOURCE_SOURCES)) {
const version = trim(readfile(`${HP_DIR}/resources/${resource_type}.ver`) || '');
push(resources, {
type: resource_type,
version: version || null,
source: RESOURCE_SOURCES[resource_type]
});
}
return { resources };
}
},
resources_update: {
call: function() {
return { status: system(`${HP_DIR}/scripts/update_resources.sh`) };
}
}
};
return { 'luci.homeproxy': methods };