'use strict'; import { ubus } from 'ubus'; import { uci } from 'uci'; import { fs } from 'fs'; const SWITCH_SCRIPT = '/usr/sbin/zt-gateway-switch'; const DRAIN_PIDFILE = '/var/run/zt-gateway-drain.pid'; const DRAIN_NEWFILE = '/var/run/zt-gateway-drain.new'; const DRAIN_OLDFILE = '/var/run/zt-gateway-drain.old'; function load_gateways() { const cursor = uci.cursor(); const result = []; const sections = uci.sections(cursor, 'zt-gateway', 'gateway'); for (let i = 0; i < length(sections); i++) { const s = sections[i]; push(result, { region: s.region ?? '', label: s.label ?? s.region ?? '', ip: s.ip ?? '', default: s.default === '1', health_check: s.health_check ?? 'ping' }); } return result; } function gateway_by_region(region) { const gws = load_gateways(); for (let i = 0; i < length(gws); i++) { if (gws[i].region === region) { return gws[i]; } } return null; } function read_active_region() { const cursor = uci.cursor(); return cursor.get('zt-gateway', 'global', 'active_gateway') ?? ''; } function read_global_option(option, default_value) { const cursor = uci.cursor(); return cursor.get('zt-gateway', 'global', option) ?? default_value; } function drain_active() { const st = fs.stat(DRAIN_PIDFILE); return st?.type === 'file'; } function read_drain_pid() { const data = fs.readfile(DRAIN_PIDFILE); if (!data) { return null; } const s = trim(data); return length(s) > 0 ? +s : null; } function read_drain_state() { let new_ip = ''; let old_ip = ''; const n = fs.readfile(DRAIN_NEWFILE); if (n) { new_ip = trim(n); } const o = fs.readfile(DRAIN_OLDFILE); if (o) { old_ip = trim(o); } return { new_ip, old_ip }; } function run_switch_script_capture(ip, mode) { const mode_lc = mode ?? 'force'; let cmd = `${SWITCH_SCRIPT} ${shell_quote(ip)} ${shell_quote(mode_lc)}`; if (mode_lc === 'graceful') { const to = +read_global_option('drain_timeout', '600'); cmd += ` ${to > 0 ? to : 600}`; } const stderr_pipe = '/tmp/zt-gw-stderr'; const code = system(`${cmd} 2>${stderr_pipe}`); const err = fs.readfile(stderr_pipe) ?? ''; fs.unlink(stderr_pipe); return { code: code ?? 0, stderr: trim(err) }; } function run_switch_script_force_capture(ip) { const stderr_pipe = '/tmp/zt-gw-stderr'; const code = system(`${SWITCH_SCRIPT} ${shell_quote(ip)} force 2>${stderr_pipe}`); const err = fs.readfile(stderr_pipe) ?? ''; fs.unlink(stderr_pipe); return { code: code ?? 0, stderr: trim(err) }; } function system_output(cmd) { const tmp = `/tmp/zt-gw-out-${getpid()}`; system(`${cmd} >${tmp} 2>/dev/null`); const out = fs.readfile(tmp) ?? ''; fs.unlink(tmp); return out; } function shell_quote(s) { if (!s) { return "''"; } if (s ~ /^[A-Za-z0-9_./:@=-]+$/) { return s; } return `'${replace(s, "'", "'\\''")}'`; } function rpc_status(req) { const gws = load_gateways(); const active_region = read_active_region(); let active_ip = ''; const active_gw = gateway_by_region(active_region); if (active_gw) { active_ip = active_gw.ip; } const drain_state = read_drain_state(); let remaining = 0; if (drain_active()) { const out = system_output(`conntrack -L -m 0x100 2>/dev/null | wc -l`); remaining = +trim(out) || 0; } const cursor = uci.cursor(); const settings = { switch_mode: cursor.get('zt-gateway', 'global', 'switch_mode') ?? 'force', drain_timeout: +(cursor.get('zt-gateway', 'global', 'drain_timeout') ?? 600), health_interval: +(cursor.get('zt-gateway', 'global', 'health_interval') ?? 60), auto_failback: cursor.get('zt-gateway', 'global', 'auto_failback') === '1' }; ubus.reply(req, { active_gateway: active_region, active_ip: active_ip, settings: settings, gateways: gws, drain: { active: drain_active(), new_ip: drain_state.new_ip, old_ip: drain_state.old_ip, remaining_connections: remaining } }); } function rpc_switch(req, msg) { const region = msg?.region; const mode = msg?.mode ?? read_global_option('switch_mode', 'force'); if (!region || (mode !== 'force' && mode !== 'graceful')) { ubus.reply(req, { success: false, message: 'Missing or invalid argument: region required, mode must be force|graceful' }); return; } if (drain_active() && mode === 'graceful') { ubus.reply(req, { success: false, message: 'A graceful drain is already in progress. Cancel it first.' }); return; } const gw = gateway_by_region(region); if (!gw) { ubus.reply(req, { success: false, message: `Unknown gateway region: ${region}` }); return; } const current_region = read_active_region(); if (current_region === region && !drain_active()) { ubus.reply(req, { success: true, message: `Already on ${region}.` }); return; } const result = run_switch_script_capture(gw.ip, mode); if (result.code !== 0) { ubus.reply(req, { success: false, message: result.stderr || `Switch script failed with exit code ${result.code}` }); return; } const cursor = uci.cursor(); cursor.set('zt-gateway', 'global', 'active_gateway', region); cursor.commit('zt-gateway'); ubus.reply(req, { success: true, message: `Switched to ${region} (${gw.ip}) via ${mode}`, active_gateway: region, mode: mode, drain_active: (mode === 'graceful') }); } function rpc_health(req, msg) { const region = msg?.region; if (!region) { ubus.reply(req, { reachable: false, message: 'Missing region' }); return; } const gw = gateway_by_region(region); if (!gw) { ubus.reply(req, { reachable: false, message: `Unknown region ${region}` }); return; } const out = system_output( `ping -c 2 -W 3 -I br-zt ${shell_quote(gw.ip)} 2>/dev/null; echo "EXIT:$?"` ); const exit_match = match(out, /EXIT:(\d+)/); const exit_code = exit_match ? +exit_match[1] : 1; let latency_ms = null; if (exit_code === 0) { const stats = match(out, /rtt min\/avg\/max[^=]*=\s*[\d.]+\/([\d.]+)\//); if (stats) { latency_ms = +stats[1]; } } ubus.reply(req, { region: region, ip: gw.ip, reachable: exit_code === 0, latency_ms: latency_ms }); } function rpc_drain_status(req) { const active = drain_active(); const state = read_drain_state(); let remaining = 0; if (active) { const out = system_output(`conntrack -L -m 0x100 2>/dev/null | wc -l`); remaining = +trim(out) || 0; } ubus.reply(req, { active: active, new_ip: state.new_ip, old_ip: state.old_ip, remaining_connections: remaining }); } function rpc_cancel_drain(req) { if (!drain_active()) { ubus.reply(req, { success: false, message: 'No drain in progress' }); return; } const state = read_drain_state(); const pid = read_drain_pid(); if (pid) { system(`kill -TERM ${pid} 2>/dev/null`); } if (!state.new_ip) { fs.unlink(DRAIN_PIDFILE); ubus.reply(req, { success: true, message: 'Drain cancelled (new gateway unknown; route left as-is).' }); return; } const result = run_switch_script_force_capture(state.new_ip); fs.unlink(DRAIN_PIDFILE); fs.unlink(DRAIN_NEWFILE); fs.unlink(DRAIN_OLDFILE); ubus.reply(req, { success: result.code === 0, message: result.code === 0 ? `Drain cancelled; force-switched to ${state.new_ip}.` : (result.stderr || `Cancel failed with code ${result.code}`) }); } export default { status: rpc_status, switch: rpc_switch, health: rpc_health, drain_status: rpc_drain_status, cancel_drain: rpc_cancel_drain };