/* * SPDX-License-Identifier: GPL-2.0-only * * Field validators for the nftables statements firewall_post.ut renders. * * Why this module exists: every UCI value the template concatenates into an * nft expression is an injection surface. A malformed entry closes a set * expression with `}`, and the next thing nft sees is whatever the attacker * put after it - up to and including `accept` for a chain that should be a * drop, or a `reject` outside any chain that the fw4 reload fails to * compile, taking the whole router firewall with it. The homeproxy-pro section * in /etc/config/homeproxy-pro is UCI-writable from the subscription updater, * from any shell on the LAN and from any cron job the user has not locked * down; `tests/ucode/test_firewall_validators.uc` covers that the helpers * drop these inputs rather than letting them through, and `tests/arch-guard.sh` * guard 11 covers that firewall_post.ut routes every field of these families * through the helpers. * * Each helper returns a defensive shape: * - null when the input was empty or every entry was rejected * - otherwise `{ v1, v2, ... }` (the nftables anonymous-set spelling) * so the template can plug the result into a statement unchanged. Invalid * entries are dropped, matching the IPv6 helper's behaviour so the IPv4 * path is finally as defensive as the IPv6 path was. * * No logging on the drop path: utpl templates render to stdout, and any * print() here would inject text into the nft ruleset. If a debug trail * is ever wanted, do it from the template itself, not from these helpers. */ 'use strict'; /* isEmpty() lives in homeproxy-pro.uc, not in the ucode globals. Without this * import every helper below threw "access to undeclared variable isEmpty" the * moment it was called, which no host-side test caught because the test * harness stages its own stand-in module. */ import { isEmpty } from './homeproxy-pro.uc'; /* IPv4 (with optional /CIDR) - the regex is the standard four-octet body * followed by an optional /0..32. Anything outside that - "1.2.3.4.5", * "1.2.3", " 1.2.3.4 ", "1.2.3.4 " - is rejected. * * Spelled out as one literal rather than built with `new RegExp(...)`: ucode * has no `new` keyword, so the constructor form was a compile error * ("Unexpected token") and the module never loaded. The capture groups are * plain `(...)` because ucode regexes go through POSIX regcomp(), which has * no `(?:...)` non-capturing group; nothing here reads the captures, so the * extra ones are harmless. */ const IPV4_CIDR_RE = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)(\/(3[0-2]|[12]?[0-9]))?$/; export function isValidIPv4(s) { if (!s || type(s) !== 'string') return false; return !!match(s, IPV4_CIDR_RE); }; export function ipv4_to_nftarr(list) { if (isEmpty(list)) return null; let out = []; for (let ip in list) { if (isValidIPv4(ip)) push(out, ip); } return isEmpty(out) ? null : `{ ${join(', ', uniq(out))} }`; }; /* MAC address: six colon-separated hex octets, case-insensitive. */ const MAC_RE = /^[0-9a-fA-F]{2}(:[0-9a-fA-F]{2}){5}$/; export function isValidMAC(s) { if (!s || type(s) !== 'string') return false; return !!match(s, MAC_RE); }; export function mac_to_nftarr(list) { if (isEmpty(list)) return null; let out = []; for (let m in list) { if (isValidMAC(m)) push(out, m); } return isEmpty(out) ? null : `{ ${join(', ', uniq(out))} }`; }; /* Linux interface names: alnum, dot, underscore, hyphen. Anything that * would survive `ip link show` parsing is accepted here. */ const IFACE_RE = /^[A-Za-z0-9._-]+$/; export function isValidIface(s) { if (!s || type(s) !== 'string') return false; return !!match(s, IFACE_RE); }; export function iface_to_nftarr(list) { if (isEmpty(list)) return null; let out = []; for (let i in list) { if (isValidIface(i)) push(out, i); } return isEmpty(out) ? null : `{ ${join(', ', uniq(out))} }`; }; /* Routing-port validator. UCI delivers routing_port as a comma-separated * string ('22,53,80,443,...'), so we split first and then validate each * piece as a single port. The old template did `split(routing_port, ',')` * and `join(', ', ...)` directly: an entry like '7890; reject' or '22,foo' * would land verbatim in an nft inet_service set. */ const PORT_RE = /^[0-9]+$/; export function isValidPort(s) { if (!s || type(s) !== 'string') return false; if (!match(s, PORT_RE)) return false; const n = +s; /* Port 0 is not a port: nft rejects `th dport 0` and `redirect to :0`, and a * ruleset that carries one is rolled back as a whole by fw4. The callers * use this as "the value is usable", so 0 has to be a rejection. */ return n >= 1 && n <= 65535; }; export function ports_to_nftarr(s) { if (isEmpty(s) || type(s) !== 'string') return null; let out = []; for (let p in split(s, ',')) { const t = trim(p); if (isValidPort(t)) push(out, t); } return isEmpty(out) ? null : `{ ${join(', ', uniq(out))} }`; };