Files

100 lines
3.7 KiB
Ucode
Executable File

#!/usr/bin/ucode
/* SPDX-License-Identifier: GPL-2.0-only
*
* Copyright (C) 2025 ImmortalWrt.org
*/
'use strict';
import { writefile } from 'fs';
import { cursor } from 'uci';
import { isEmpty, RUN_DIR, validation } from './homeproxy-pro.uc';
const cfgname = 'homeproxy-pro';
const uci = cursor();
uci.load(cfgname);
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china',
proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy';
let outbound_node, tun_name;
if (match(proxy_mode, /tun/)) {
if (routing_mode === 'custom')
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
else
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
/* Validate the interface name before it is interpolated into an nft
* ruleset that fw4 loads as root. It is a UCI value, and this file's own
* threat model treats UCI as untrusted - the port and network fields a few
* lines below are validated for exactly that reason, while tun_name was
* not. `;` and `}` need no newline, so a value like
* "singtun0 } ; chain evil { ..." would balance the template's trailing
* brace and add a chain of the attacker's choosing.
*
* Linux interface names are at most 15 characters and cannot contain
* whitespace or shell/nft metacharacters; anything outside that is not a
* name we could have created, so the tun rules are simply not emitted. */
if (outbound_node !== 'nil') {
const candidate = uci.get(cfgname, 'infra', 'tun_name') || 'singtun0';
if (match(candidate, /^[A-Za-z0-9_.-]{1,15}$/) && candidate !== '.' && candidate !== '..')
tun_name = candidate;
else
print(sprintf('WARN: ignoring invalid tun_name "%s" (not a valid interface name).', candidate));
}
}
const server_enabled = uci.get(cfgname, 'server', 'enabled');
let forward = [],
input = [];
if (tun_name) {
push(forward, `oifname ${tun_name} counter accept comment "!${cfgname}: accept tun forward"`);
push(input ,`iifname ${tun_name} counter accept comment "!${cfgname}: accept tun input"`);
}
if (server_enabled === '1') {
uci.foreach(cfgname, 'server', (s) => {
if (s.enabled !== '1' || s.firewall !== '1')
return;
/* The section name is interpolated into the nft comment below, in a
* file fw4 loads as root. libuci only accepts [A-Za-z0-9_] in a
* section name (a `"`, `}`, `;`, space, `.` or `-` is "invalid
* character in name field" / "Invalid argument" on both the parser
* and the `uci set` path), so today no name that could close the
* comment can reach this loop through UCI at all - this mirrors the
* tun_name guard above rather than trusting a writer's in-memory
* cursor or a future libuci. The alphabet is exactly libuci's and
* there is deliberately no length cap (libuci has none, and a cap
* would skip a legal name and silently drop that server's rule).
* The check runs first so the two warnings below only ever echo a
* name that already passed it. */
if (!match(s['.name'], /^[A-Za-z0-9_]+$/)) {
print(`WARN: skipping server ${s['.name']}: invalid section name.`);
return;
}
if (!validation('port', s.port)) {
print(`WARN: skipping server ${s['.name']}: invalid port "${s.port}".`);
return;
}
let proto = s.network || '{ tcp, udp }';
if (!(proto in ['tcp', 'udp', '{ tcp, udp }'])) {
print(`WARN: skipping server ${s['.name']}: invalid network "${proto}".`);
return;
}
push(input, `meta l4proto ${proto} th dport ${s.port} counter accept comment "!${cfgname}: accept server ${s['.name']}"`);
});
}
if (!isEmpty(forward))
writefile(RUN_DIR + '/fw4_forward.nft', join('\n', forward) + '\n');
if (!isEmpty(input))
writefile(RUN_DIR + '/fw4_input.nft', join('\n', input) + '\n');