mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-01 16:44:23 +08:00
1087 lines
42 KiB
Plaintext
Executable File
1087 lines
42 KiB
Plaintext
Executable File
#!/usr/bin/utpl -S
|
|
|
|
{%-
|
|
/*
|
|
* SPDX-License-Identifier: GPL-2.0-only
|
|
*
|
|
* Copyright (C) 2025 ImmortalWrt.org
|
|
*
|
|
* Declares every homeproxy_* fw4 chain/set. The inventory of those objects is
|
|
* tracked in scripts/fw4_names.sh, which init.d/homeproxy-pro uses to clean them up
|
|
* on stop; add/remove entries there together with any change below.
|
|
* tests/ucode/test_fw4_names.sh compares both lists, and
|
|
* tests/ucode/test_firewall_template.sh renders this template and checks the
|
|
* objects actually survive into the generated nftables script.
|
|
*
|
|
* Every UCI-derived value (IPv4, MAC, interface name, port) goes through
|
|
* one of the *_to_nftarr() helpers in firewall_utils.uc - not because the
|
|
* UCI writers are untrusted (UI validation is there), but because UCI is
|
|
* writable from anywhere on the LAN and the package principle is "backend
|
|
* must enforce". Why those helpers exist, and why dropping silently
|
|
* matches the IPv6 path, is pinned by tests/ucode/test_firewall_validators.uc
|
|
* and guard 11 in tests/arch-guard.sh.
|
|
*
|
|
* Keep this comment inside the ucode block: text placed before `{%-` has its
|
|
* trailing newline trimmed, which would glue it onto the first nft statement.
|
|
*/
|
|
import { readfile } from 'fs';
|
|
import { cursor } from 'uci';
|
|
import { isEmpty, isValidCIDR, udp_transport_verdict } from '/etc/homeproxy-pro/scripts/homeproxy-pro.uc';
|
|
import {
|
|
ipv4_to_nftarr,
|
|
mac_to_nftarr,
|
|
iface_to_nftarr,
|
|
ports_to_nftarr,
|
|
isValidPort,
|
|
isValidIface
|
|
} from '/etc/homeproxy-pro/scripts/firewall_utils.uc';
|
|
|
|
const fw4 = require('fw4');
|
|
|
|
function array_to_nftarr(array) {
|
|
if (type(array) !== 'array')
|
|
return null;
|
|
|
|
return `{ ${join(', ', uniq(array))} }`;
|
|
}
|
|
|
|
function resolve_ipv6(str) {
|
|
if (isEmpty(str))
|
|
return null;
|
|
|
|
let ipv6 = fw4.parse_subnet(str)?.[0];
|
|
if (!ipv6 || ipv6.family !== 6)
|
|
return null;
|
|
|
|
if (ipv6.bits > -1)
|
|
return `${ipv6.addr}/${ipv6.bits}`;
|
|
else
|
|
return ipv6.addr;
|
|
}
|
|
|
|
/* The node server addresses the generator exported (generate_client.uc's
|
|
* export_node_addresses()), as an nftables anonymous set - or null when there
|
|
* is nothing to protect.
|
|
*
|
|
* Only the literal-address form is rendered here. A node configured with a
|
|
* host name reaches the same set at runtime: the dnsmasq snippet runtime/dns.sh
|
|
* writes resolves the name and back-fills the set through `nftset=`, so one
|
|
* mechanism covers both without this template needing a resolver of its own.
|
|
* A literal address has no name to resolve, so it can only come from here.
|
|
*
|
|
* readfile() rather than a UCI read on purpose: the generator is the only
|
|
* place that knows which nodes are actually in use (main_node, main_udp_node,
|
|
* urltest membership, custom-mode routing_nodes, wireguard endpoints), and a
|
|
* copy of that selection in this template is a copy that can drift. A missing
|
|
* file renders null, which is the behaviour before this existed. */
|
|
function node_addr_to_nftarr(run_dir, family) {
|
|
let raw = readfile(run_dir + '/node-addr-ips.txt');
|
|
if (isEmpty(raw))
|
|
return null;
|
|
|
|
let out = [];
|
|
for (let entry in split(trim(raw), /[\r\n]/)) {
|
|
let parsed = fw4.parse_subnet(entry)?.[0];
|
|
if (!parsed || parsed.family !== family)
|
|
continue;
|
|
push(out, (parsed.bits > -1) ? `${parsed.addr}/${parsed.bits}` : parsed.addr);
|
|
}
|
|
|
|
return isEmpty(out) ? null : array_to_nftarr(out);
|
|
}
|
|
|
|
/* Whether the node bypass has anything to protect at all.
|
|
|
|
Either export file counts. The domains file is the one that matters most:
|
|
a host-name node - the common case, and the one the device this was written
|
|
for uses - appears only there, and its set has to exist before dnsmasq can
|
|
put anything in it. Gating on the literal-address file instead is what left
|
|
the whole mechanism doing nothing. */
|
|
function node_addr_protected(run_dir) {
|
|
return !isEmpty(readfile(run_dir + '/node-addr-ips.txt'))
|
|
|| !isEmpty(readfile(run_dir + '/node-addr-domains.txt'));
|
|
}
|
|
|
|
/* Normalize a list of IPv6 entries into an nftables anonymous set
|
|
(or null when empty), so IPv6 ACLs match in a single set lookup
|
|
instead of one rule per address. */
|
|
function ipv6_to_nftarr(list) {
|
|
if (isEmpty(list))
|
|
return null;
|
|
|
|
let out = [];
|
|
for (let ip in list) {
|
|
const r = resolve_ipv6(ip);
|
|
if (!isEmpty(r))
|
|
push(out, r);
|
|
}
|
|
|
|
return isEmpty(out) ? null : `{ ${join(', ', out)} }`;
|
|
}
|
|
|
|
function resolve_mark(str) {
|
|
if (isEmpty(str))
|
|
return null;
|
|
|
|
let mark = fw4.parse_mark(str);
|
|
if (isEmpty(mark))
|
|
return null;
|
|
|
|
if (mark.mask === 0xffffffff)
|
|
return fw4.hex(mark.mark);
|
|
else if (mark.mark === 0)
|
|
return `mark and ${fw4.hex(~mark.mask & 0xffffffff)}`;
|
|
else if (mark.mark === mark.mask)
|
|
return `mark or ${fw4.hex(mark.mark)}`;
|
|
else if (mark.mask === 0)
|
|
return `mark xor ${fw4.hex(mark.mark)}`;
|
|
else
|
|
return `mark and ${fw4.hex(~mark.mask & 0xffffffff)} xor ${fw4.hex(mark.mark)}`;
|
|
}
|
|
|
|
/* Single-value guards for the five fields that were still read straight from
|
|
* UCI and interpolated into the nft text: self_mark, redirect_port,
|
|
* tproxy_port, dns_port and tun_name. H1 fixed the *_to_nftarr() families but
|
|
* missed these; a ';' in one of them is a statement boundary in nft, and a
|
|
* non-numeric value makes fw4 reject the whole ruleset, which takes the
|
|
* router's firewall down with it. A rejected value falls back to the built-in
|
|
* default rather than being emitted, so the side keeps working. */
|
|
function port_or(str, fallback) {
|
|
return isValidPort(str) ? str : fallback;
|
|
}
|
|
|
|
function iface_or(str, fallback) {
|
|
return isValidIface(str) ? str : fallback;
|
|
}
|
|
|
|
/* self_mark is used as a *match* value (`meta mark <x>`), so it has to be a
|
|
* plain number. resolve_mark() also returns set-expressions such as
|
|
* `mark and 0xff xor 0x100` for masked input, and those are only valid after
|
|
* `mark set`; anything that is not a bare hex value falls back. */
|
|
function mark_value_or(str, fallback) {
|
|
const m = resolve_mark(str);
|
|
return match(m || '', /^0x[0-9a-fA-F]+$/) ? m : fallback;
|
|
}
|
|
|
|
/* gfwlist mode routes by the address set that dnsmasq fills through
|
|
* `nftset=`, and the gates below compare the destination against it. A dnsmasq
|
|
* built without HAVE_NFTSET never fills the set, and `ip daddr != @<empty set>`
|
|
* is true for every packet - the whole mode then silently routes everything
|
|
* direct while the DNS side keeps answering through sing-box. init.d records
|
|
* the capability in resources/.dnsmasq-nftset before this template is rendered,
|
|
* so the gates can be omitted there and sing-box decides by geoip instead.
|
|
* A missing file means "capability unknown" and keeps the historical output. */
|
|
function dnsmasq_nftset() {
|
|
let marker = null;
|
|
try {
|
|
marker = trim(readfile(resources_dir + '/.dnsmasq-nftset'));
|
|
} catch (e) {
|
|
return true;
|
|
}
|
|
return marker === '' || marker === '1';
|
|
}
|
|
|
|
/* Misc config */
|
|
const resources_dir = '/etc/homeproxy-pro/resources';
|
|
const run_dir = '/var/run/homeproxy-pro';
|
|
|
|
/* Resolved here, in the ucode block, rather than with `{% set %}` in the
|
|
* template body below: utpl's `set` tag takes a plain expression and rejects a
|
|
* call outright - `{% set x = f(); %}` is a parse error ("Unexpected token,
|
|
* Expecting ';'"). A parse error kills the whole render, fw4_post.nft comes
|
|
* out empty, and the health gate silently keeps the previous ruleset, so a
|
|
* template that fails this way does not look broken at all. r41 shipped
|
|
* `{% set node_addr_v4 = node_addr_to_nftarr(4); %}` and never rendered once.
|
|
*
|
|
* Both helpers take run_dir as a parameter rather than closing over it. In
|
|
* this ucode a closure captures the block variables that already exist where
|
|
* the function is DEFINED, not where it is called: a function defined above
|
|
* `const run_dir` cannot see run_dir even when it is called far below, and
|
|
* the error is "Reference error: access to undeclared variable run_dir" -
|
|
* which reads as though run_dir were missing, when it is in fact declared 80
|
|
* lines above the call. Passing it in is the only spelling that works. */
|
|
const node_protect = node_addr_protected(run_dir);
|
|
const node_addr_v4 = node_addr_to_nftarr(run_dir, 4);
|
|
const node_addr_v6 = node_addr_to_nftarr(run_dir, 6);
|
|
|
|
/* UCI config start */
|
|
const cfgname = 'homeproxy-pro';
|
|
const uci = cursor();
|
|
uci.load(cfgname);
|
|
|
|
const routing_mode = uci.get(cfgname, 'config', 'routing_mode') || 'bypass_mainland_china';
|
|
let outbound_node, outbound_udp_node, china_dns_server, bypass_cn_traffic;
|
|
|
|
if (routing_mode !== 'custom') {
|
|
outbound_node = uci.get(cfgname, 'config', 'main_node') || 'nil';
|
|
outbound_udp_node = uci.get(cfgname, 'config', 'main_udp_node') || 'nil';
|
|
china_dns_server = uci.get(cfgname, 'config', 'china_dns_server');
|
|
} else {
|
|
outbound_node = uci.get(cfgname, 'routing', 'default_outbound') || 'nil';
|
|
bypass_cn_traffic = uci.get(cfgname, 'routing', 'bypass_cn_traffic') || '0';
|
|
}
|
|
|
|
let routing_port = uci.get(cfgname, 'config', 'routing_port');
|
|
if (routing_port === 'common')
|
|
routing_port = uci.get(cfgname, 'infra', 'common_port') || '22,53,80,143,443,465,587,853,873,993,995,8080,8443,9418';
|
|
|
|
const proxy_mode = uci.get(cfgname, 'config', 'proxy_mode') || 'redirect_tproxy',
|
|
ipv6_support = uci.get(cfgname, 'config', 'ipv6_support') || '0';
|
|
|
|
/* --- can IPv6 be classified as mainland at all? ------------------------
|
|
*
|
|
* Every IPv6 decision in this template used to hang off ipv6_support
|
|
* alone. That flag only says whether the user *wants* IPv6 handled; it
|
|
* says nothing about whether we can tell a mainland IPv6 destination from
|
|
* a foreign one. That test is the homeproxy_mainland_addr_v6 set, and the
|
|
* set is filled straight from resources/china_ip6.txt - so an install whose
|
|
* list is missing, empty or entirely malformed produced a *syntactically
|
|
* valid but permanently empty* set. `ip6 daddr @homeproxy_mainland_addr_v6
|
|
* counter return` then matched nothing, and every mainland IPv6 connection
|
|
* fell through to the proxy hop at the bottom of the chain: a silent,
|
|
* unlogged inversion of the mode's promise, and the reason a user who
|
|
* enables IPv6 finds Chinese sites taking the long way round.
|
|
*
|
|
* nft happily accepts `elements = { }`, so nothing downstream could notice.
|
|
* The count is taken here, once, with the same isValidCIDR() the set
|
|
* rendering uses - a list of entries this function rejects would render an
|
|
* empty set too, so counting something else would not be the same test.
|
|
*
|
|
* v6_handled is what every v6 branch below now keys off. When the list is
|
|
* unusable it is false, and the ruleset degrades to exactly the IPv6-off
|
|
* shape: IPv6 is not intercepted, not marked, not redirected. That is a
|
|
* deliberate choice of failure direction. The alternative - keep
|
|
* intercepting and proxy everything - trades a wrong-but-working path for
|
|
* a leaked one, and the user cannot see either from the client. Passing
|
|
* IPv6 through unproxied is the state the router was in before this package
|
|
* touched it, it is recoverable (fix the list, reload), and it is announced:
|
|
* see the comment on the dstnat jump below and the warning
|
|
* hp_prepare_runtime_files() logs on every start. */
|
|
let cn_ipv6_count = 0;
|
|
for (let entry in split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/))
|
|
if (isValidCIDR(entry, 6))
|
|
cn_ipv6_count++;
|
|
|
|
const cn_ipv6_ready = (cn_ipv6_count > 0);
|
|
const v6_handled = (ipv6_support === '1') && cn_ipv6_ready;
|
|
|
|
/* The degraded-state warning, as a ready-made nft comment suffix.
|
|
*
|
|
* A real `comment` rather than a `#` line on purpose: fw4 keeps `#` lines
|
|
* only in the generated file, while a rule comment is part of the rule and
|
|
* survives into `nft list ruleset` - which is where someone debugging "why
|
|
* is my IPv6 not going through the proxy" actually looks.
|
|
*
|
|
* Computed once and interpolated into all three jump rules rather than
|
|
* wrapped around each of them: the rule text is identical either way, only
|
|
* the comment differs, and one string is one place to change if the wording
|
|
* ever has to. */
|
|
const v6_warn_comment = ((ipv6_support === '1') && !cn_ipv6_ready)
|
|
? ' comment "!homeproxy-pro: WARNING china_ip6.txt has 0 usable prefixes - IPv6 is NOT proxied, run update_resources.sh china_ip6"'
|
|
: '';
|
|
|
|
let self_mark, redirect_port,
|
|
tproxy_port, tproxy_mark,
|
|
tun_name, tun_mark, tun_self_mark;
|
|
|
|
if (match(proxy_mode, /redirect/)) {
|
|
self_mark = mark_value_or(uci.get(cfgname, 'infra', 'self_mark'), resolve_mark('100'));
|
|
redirect_port = port_or(uci.get(cfgname, 'infra', 'redirect_port'), '5331');
|
|
}
|
|
/* Whether QUIC has to be rejected, and why.
|
|
*
|
|
* This used to ask "is there a UDP node?" and that question has been wrong in
|
|
* both directions.
|
|
*
|
|
* Wrong by too much: it keyed off `routing_mode !== 'custom'` alone, so a
|
|
* router with `main_udp_node` set still rejected QUIC on UDP 443 (measured on
|
|
* the device: 1722 QUIC packets rejected in one sample) and browsers that had
|
|
* already learned HTTP/3 fell back to TCP needlessly.
|
|
*
|
|
* Wrong by too little, which is what the current form is: "a UDP node exists"
|
|
* says nothing about what that node's transport is. `main_udp_node = same`
|
|
* with a TCP-transport main node is the shape that slips through - the UI
|
|
* label reads "Same as main node" and nothing tells the user the consequence.
|
|
* The UDP is proxied, yes, but it is tunnelled over TCP, and QUIC over a TCP
|
|
* tunnel stalls: measured with both nodes on the SAME server and only the
|
|
* transport differing (anytls TCP:443 vs hysteria2 QUIC/UDP:50466), UDP came
|
|
* back at 174 ms +/- 1 ms through hysteria2 and produced a 568 ms spike with
|
|
* 37 ms jitter and outright loss through anytls.
|
|
*
|
|
* So the gate is now the transport, resolved by udp_transport_verdict() in
|
|
* homeproxy-pro.uc - one definition, shared with the diagnostic report so the log
|
|
* and the rule cannot disagree.
|
|
*
|
|
* NOTE the deliberate asymmetry with the tproxy chain below and in the UDP
|
|
* tproxy block: those still key off the plain "is there a UDP node" test,
|
|
* because they decide whether UDP is PROXIED at all. This gate decides
|
|
* whether QUIC is WORTH proxying. A TCP-transport UDP node still proxies
|
|
* games, voice and everything else; only QUIC is turned away, so the browser
|
|
* falls straight back to a TCP path that the same tunnel carries far better. */
|
|
const udp_transport = udp_transport_verdict(cfgname, uci);
|
|
const udp_quic_rejected = !udp_transport.native;
|
|
|
|
if (match(proxy_mode, /tproxy/))
|
|
if (outbound_udp_node !== 'nil' || routing_mode === 'custom') {
|
|
tproxy_port = port_or(uci.get(cfgname, 'infra', 'tproxy_port'), '5332');
|
|
tproxy_mark = resolve_mark(uci.get(cfgname, 'infra', 'tproxy_mark') || '101') || resolve_mark('101');
|
|
}
|
|
if (match(proxy_mode, /tun/)) {
|
|
tun_name = iface_or(uci.get(cfgname, 'infra', 'tun_name'), 'singtun0');
|
|
tun_mark = resolve_mark(uci.get(cfgname, 'infra', 'tun_mark') || '102') || resolve_mark('102');
|
|
/* The mark sing-box stamps on its own outbound sockets in tun mode
|
|
* (generator: route.default_mark, same UCI option as self_mark elsewhere).
|
|
* The TUN output chain has to return on it, otherwise the tunnel captures
|
|
* sing-box's own dial - including the one to the node - and the only guard
|
|
* it had was `iifname <tun>`, which cannot match on the output hook. */
|
|
tun_self_mark = mark_value_or(uci.get(cfgname, 'infra', 'self_mark'), resolve_mark('100'));
|
|
}
|
|
|
|
const control_options = [
|
|
"listen_interfaces", "lan_proxy_mode",
|
|
"lan_direct_mac_addrs", "lan_direct_ipv4_ips", "lan_direct_ipv6_ips",
|
|
"lan_proxy_mac_addrs", "lan_proxy_ipv4_ips", "lan_proxy_ipv6_ips",
|
|
"lan_gaming_mode_mac_addrs", "lan_gaming_mode_ipv4_ips", "lan_gaming_mode_ipv6_ips",
|
|
"lan_global_proxy_mac_addrs", "lan_global_proxy_ipv4_ips", "lan_global_proxy_ipv6_ips",
|
|
"wan_proxy_ipv4_ips", "wan_proxy_ipv6_ips",
|
|
"wan_direct_ipv4_ips", "wan_direct_ipv6_ips"
|
|
];
|
|
const control_info = {};
|
|
|
|
for (let i in control_options)
|
|
control_info[i] = uci.get(cfgname, 'control', i);
|
|
|
|
/* Devices of the wan zone, resolved by fw4 itself.
|
|
*
|
|
* The intercept layer must not cover the wan side. When the user names
|
|
* interfaces the guards below use exactly those; when the option is empty the
|
|
* UI says "all", and "all" used to include wan - so wan-inbound traffic to the
|
|
* router's own address on a routing_port was captured by the redirect / tproxy
|
|
* jump and by the DNS hijack as well. It happened to be masked on the device
|
|
* (dnsmasq's own dns_redirect suppressed the DNS half, fw4's input policy
|
|
* dropped the rest), but the hole was structural: any wan-side service on one
|
|
* of the routing ports would have been hijacked. Empty now means "everything
|
|
* except the wan zone".
|
|
*
|
|
* fw4's own ruleset template renders zone devices from `zone.match_devices`,
|
|
* so this asks the same source. A render outside fw4 (the off-target test
|
|
* harness has no zone state) leaves the list empty and the guards below fall
|
|
* back to the previous behaviour. */
|
|
let wan_devices = [];
|
|
|
|
try {
|
|
/* fw4.load() is what resolves a zone's *logical* networks to the devices
|
|
* that actually appear in `iifname` - the wan zone is `network wan` but
|
|
* the device is `pppoe-wan`, and this template runs OUTSIDE fw4's own
|
|
* process, where the module has no state at all. Two wrong answers were
|
|
* measured on the device before this:
|
|
*
|
|
* - no load at all: `zones()` is null, the loop threw, the catch left
|
|
* the list empty and the guard silently disappeared from the rendered
|
|
* ruleset (that is the state this package shipped in r46);
|
|
* - fw4.read_state(): parses /var/run/fw4.state, which carries the
|
|
* LOGICAL names ("wan", "wan_6"), so the guard rendered as
|
|
* `meta iifname { wan }` and could never match - a guard that looks
|
|
* present and does nothing.
|
|
*
|
|
* load() builds the model through ubus the same way fw4 itself does, and
|
|
* yields match_devices = [ "pppoe-wan" ]. It is read-only, ~tens of ms,
|
|
* and its warnings go to stderr, which the caller discards. */
|
|
fw4.load();
|
|
|
|
for (let zone in (fw4.zones() || []))
|
|
if (zone.name === 'wan')
|
|
for (let device in zone.match_devices)
|
|
push(wan_devices, device);
|
|
}
|
|
catch (e) { }
|
|
|
|
wan_devices = sort(uniq(wan_devices));
|
|
|
|
/* Validated, nft-ready form of every UCI list, computed once.
|
|
*
|
|
* The template used to gate on the *raw* UCI value and interpolate the
|
|
* validator-backed helper. A list that is non-empty but has no usable entry -
|
|
* hand-edited UCI, an address in the wrong family - therefore rendered
|
|
* `ip daddr counter return`, an nft syntax error that fails the whole fw4
|
|
* transaction, or `elements = { }`. Gating on the helper's result instead
|
|
* means an unusable list simply emits no rule. The rendered output for a
|
|
* usable configuration is unchanged (the helpers already dropped invalid
|
|
* entries one by one). */
|
|
const NFTARR_FIELDS = {
|
|
listen_interfaces: iface_to_nftarr,
|
|
lan_proxy_ipv4_ips: ipv4_to_nftarr,
|
|
lan_proxy_ipv6_ips: ipv6_to_nftarr,
|
|
lan_proxy_mac_addrs: mac_to_nftarr,
|
|
lan_direct_ipv4_ips: ipv4_to_nftarr,
|
|
lan_direct_ipv6_ips: ipv6_to_nftarr,
|
|
lan_direct_mac_addrs: mac_to_nftarr,
|
|
lan_gaming_mode_ipv4_ips: ipv4_to_nftarr,
|
|
lan_gaming_mode_ipv6_ips: ipv6_to_nftarr,
|
|
lan_gaming_mode_mac_addrs: mac_to_nftarr,
|
|
lan_global_proxy_ipv4_ips: ipv4_to_nftarr,
|
|
lan_global_proxy_ipv6_ips: ipv6_to_nftarr,
|
|
lan_global_proxy_mac_addrs: mac_to_nftarr,
|
|
wan_proxy_ipv4_ips: ipv4_to_nftarr,
|
|
wan_proxy_ipv6_ips: ipv6_to_nftarr,
|
|
wan_direct_ipv4_ips: ipv4_to_nftarr,
|
|
wan_direct_ipv6_ips: ipv6_to_nftarr
|
|
};
|
|
const nftarr = {};
|
|
|
|
for (let field in NFTARR_FIELDS)
|
|
nftarr[field] = NFTARR_FIELDS[field](control_info[field] || []);
|
|
|
|
/* The local-output chain also exempts `lo`, which the tproxy guard adds by
|
|
* hand. Same precomputation, so the guard cannot disagree with the list. */
|
|
nftarr.listen_interfaces_lo = iface_to_nftarr([ ...(control_info.listen_interfaces || []), 'lo' ]);
|
|
|
|
/* The routing-port filter. Gating on the raw option while interpolating
|
|
* ports_to_nftarr() had the same defect, and there the fallback direction
|
|
* matters: an empty rendering would make `tcp dport != @homeproxy_routing_port`
|
|
* match everything and send all TCP traffic direct. An unusable list
|
|
* therefore disables the filter (ports are proxied) instead of widening it. */
|
|
const routing_port_set = routing_port ? ports_to_nftarr(routing_port) : null;
|
|
|
|
const dns_redirect = uci.get(cfgname, 'infra', 'dns_redirect') || '1';
|
|
const dns_hijacked = uci.get('dhcp', '@dnsmasq[0]', 'dns_redirect') || '0',
|
|
/* dnsmasq's own port - NOT the sing-box DNS inbound port (infra.dns_port,
|
|
* 5333). Both were called `dns_port` here and in the generator, which
|
|
* made the hijack rule look like it pointed at the sing-box listener. */
|
|
dnsmasq_port = port_or(uci.get('dhcp', '@dnsmasq[0]', 'port'), '53');
|
|
/* UCI config end */
|
|
-%}
|
|
|
|
{# Reserved addresses -#}
|
|
set homeproxy_local_addr_v4 {
|
|
type ipv4_addr
|
|
flags interval
|
|
auto-merge
|
|
elements = {
|
|
0.0.0.0/8,
|
|
10.0.0.0/8,
|
|
100.64.0.0/10,
|
|
127.0.0.0/8,
|
|
169.254.0.0/16,
|
|
172.16.0.0/12,
|
|
192.0.0.0/24,
|
|
192.0.2.0/24,
|
|
192.31.196.0/24,
|
|
192.52.193.0/24,
|
|
192.88.99.0/24,
|
|
192.168.0.0/16,
|
|
192.175.48.0/24,
|
|
198.18.0.0/15,
|
|
198.51.100.0/24,
|
|
203.0.113.0/24,
|
|
224.0.0.0/4,
|
|
240.0.0.0/4
|
|
}
|
|
}
|
|
{% if (v6_handled): %}
|
|
set homeproxy_local_addr_v6 {
|
|
type ipv6_addr
|
|
flags interval
|
|
auto-merge
|
|
elements = {
|
|
::/128,
|
|
::1/128,
|
|
::ffff:0:0/96,
|
|
100::/64,
|
|
64:ff9b::/96,
|
|
2001::/32,
|
|
2001:10::/28,
|
|
2001:20::/28,
|
|
2001:db8::/28,
|
|
2002::/16,
|
|
fc00::/7,
|
|
fe80::/10,
|
|
ff00::/8
|
|
}
|
|
}
|
|
{% endif %}
|
|
|
|
{% if (routing_mode === 'gfwlist'): %}
|
|
set homeproxy_gfw_list_v4 {
|
|
type ipv4_addr
|
|
flags interval
|
|
auto-merge
|
|
}
|
|
{% if (v6_handled): %}
|
|
set homeproxy_gfw_list_v6 {
|
|
type ipv6_addr
|
|
flags interval
|
|
auto-merge
|
|
}
|
|
{% endif /* v6_handled */ %}
|
|
{% elif (match(routing_mode, /mainland_china/) || bypass_cn_traffic === '1'): %}
|
|
set homeproxy_mainland_addr_v4 {
|
|
type ipv4_addr
|
|
flags interval
|
|
auto-merge
|
|
elements = {
|
|
{% for (let cnip4 in split(trim(readfile(resources_dir + '/china_ip4.txt')), /[\r\n]/)): %}
|
|
{% if (isValidCIDR(cnip4, 4)): %}
|
|
{{ cnip4 }},
|
|
{% endif %}
|
|
{% endfor %}
|
|
}
|
|
}
|
|
{% if (v6_handled): %}
|
|
set homeproxy_mainland_addr_v6 {
|
|
type ipv6_addr
|
|
flags interval
|
|
auto-merge
|
|
elements = {
|
|
{% for (let cnip6 in split(trim(readfile(resources_dir + '/china_ip6.txt')), /[\r\n]/)): %}
|
|
{% if (isValidCIDR(cnip6, 6)): %}
|
|
{{ cnip6 }},
|
|
{% endif %}
|
|
{% endfor %}
|
|
}
|
|
}
|
|
{% endif /* v6_handled */ %}
|
|
{% endif /* routing_mode */ %}
|
|
|
|
{# WAN ACL addresses #}
|
|
set homeproxy_wan_proxy_addr_v4 {
|
|
type ipv4_addr
|
|
flags interval
|
|
auto-merge
|
|
{% if (nftarr.wan_proxy_ipv4_ips): %}
|
|
elements = { {{ nftarr.wan_proxy_ipv4_ips }} }
|
|
{% endif %}
|
|
}
|
|
|
|
{% if (v6_handled): %}
|
|
set homeproxy_wan_proxy_addr_v6 {
|
|
type ipv6_addr
|
|
flags interval
|
|
auto-merge
|
|
{% if (nftarr.wan_proxy_ipv6_ips): %}
|
|
elements = { {{ nftarr.wan_proxy_ipv6_ips }} }
|
|
{% endif /* wan_proxy_ipv6_ips*/ %}
|
|
}
|
|
{% endif /* v6_handled */ %}
|
|
|
|
set homeproxy_wan_direct_addr_v4 {
|
|
type ipv4_addr
|
|
flags interval
|
|
auto-merge
|
|
{% if (nftarr.wan_direct_ipv4_ips): %}
|
|
elements = { {{ nftarr.wan_direct_ipv4_ips }} }
|
|
{% endif %}
|
|
}
|
|
|
|
{% if (v6_handled): %}
|
|
set homeproxy_wan_direct_addr_v6 {
|
|
type ipv6_addr
|
|
flags interval
|
|
auto-merge
|
|
{% if (nftarr.wan_direct_ipv6_ips): %}
|
|
elements = { {{ nftarr.wan_direct_ipv6_ips }} }
|
|
{% endif /* wan_direct_ipv6_ips */ %}
|
|
}
|
|
{% endif /* v6_handled */ %}
|
|
|
|
{% if (routing_port_set): %}
|
|
set homeproxy_routing_port {
|
|
type inet_service
|
|
flags interval
|
|
auto-merge
|
|
elements = { {{ routing_port_set }} }
|
|
}
|
|
{% endif %}
|
|
|
|
{# Node server addresses.
|
|
Declared when the generator exported any node address at all - and NOT
|
|
only when there is a literal address to put in it.
|
|
|
|
That distinction is the whole point of the set existing. A node configured
|
|
with a host name contributes no element here; dnsmasq resolves the name and
|
|
back-fills this set through the nftset= snippet runtime/dns.sh writes. A
|
|
set that is declared only when it already has elements is therefore never
|
|
declared for a host-name node, the back-fill has nothing to land in, and the
|
|
return rules below - which are what actually exclude the address - are not
|
|
emitted either. r41 gated both on the literal form and was inert for the
|
|
configuration it was written for.
|
|
|
|
An empty set is harmless: nft accepts one with no elements, and the cost is
|
|
one lookup per packet until the first answer back-fills it. #}
|
|
{% if (node_protect): %}
|
|
set homeproxy_node_addr_v4 {
|
|
type ipv4_addr
|
|
flags interval
|
|
auto-merge
|
|
{% if (node_addr_v4): %}
|
|
elements = { {{ node_addr_v4 }} }
|
|
{% endif %}
|
|
}
|
|
{% endif %}
|
|
{% if (node_protect && v6_handled): %}
|
|
set homeproxy_node_addr_v6 {
|
|
type ipv6_addr
|
|
flags interval
|
|
auto-merge
|
|
{% if (node_addr_v6): %}
|
|
elements = { {{ node_addr_v6 }} }
|
|
{% endif %}
|
|
}
|
|
{% endif /* node_addr_v6 */ %}
|
|
|
|
{# DNS hijack & TCP redirect #}
|
|
chain dstnat {
|
|
{% if ((dns_redirect === '1') && (dns_hijacked !== '1')): %}
|
|
{% if (nftarr.listen_interfaces): %}
|
|
meta iifname {{ nftarr.listen_interfaces }}
|
|
{%- elif (!isEmpty(wan_devices)): %}
|
|
meta iifname != {{ array_to_nftarr(wan_devices) }}
|
|
{%- endif /* listen_interfaces */ %}
|
|
meta nfproto { ipv4, ipv6 } udp dport 53 counter redirect to :{{ dnsmasq_port }} comment "!{{ cfgname }}: DNS hijack"
|
|
{% endif /* dns_hijacked */ %}
|
|
{% if (match(proxy_mode, /redirect/)): %}
|
|
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect_lanac{{ v6_warn_comment }}
|
|
{% endif /* proxy_mode */ %}
|
|
}
|
|
|
|
{# TCP redirect #}
|
|
{% if (match(proxy_mode, /redirect/)): %}
|
|
chain homeproxy_redirect_proxy {
|
|
meta l4proto tcp counter redirect to :{{ redirect_port }}
|
|
}
|
|
|
|
chain homeproxy_redirect_proxy_port {
|
|
{% if (routing_port_set): %}
|
|
tcp dport != @homeproxy_routing_port counter return
|
|
{% endif %}
|
|
goto homeproxy_redirect_proxy
|
|
}
|
|
|
|
chain homeproxy_redirect_lanac {
|
|
{% if (nftarr.listen_interfaces): %}
|
|
meta iifname != {{ nftarr.listen_interfaces }} counter return
|
|
{% elif (!isEmpty(wan_devices)): %}
|
|
meta iifname {{ array_to_nftarr(wan_devices) }} counter return comment "!{{ cfgname }}: not the WAN side"
|
|
{% endif %}
|
|
meta mark {{ self_mark }} counter return
|
|
|
|
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
|
{% if (nftarr.lan_proxy_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_proxy_ipv4_ips }} counter goto homeproxy_redirect
|
|
{% endif /* lan_proxy_ipv4_ips */ %}
|
|
{% if (nftarr.lan_proxy_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_proxy_ipv6_ips }} counter goto homeproxy_redirect
|
|
{% endif /* lan_proxy_ipv6_ips */ %}
|
|
{% if (nftarr.lan_proxy_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_proxy_mac_addrs }} counter goto homeproxy_redirect
|
|
{% endif /* lan_proxy_mac_addrs */ %}
|
|
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
|
{% if (nftarr.lan_direct_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_direct_ipv4_ips }} counter return
|
|
{% endif /* lan_direct_ipv4_ips */ %}
|
|
{% if (nftarr.lan_direct_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_direct_ipv6_ips }} counter return
|
|
{% endif /* lan_direct_ipv6_ips */ %}
|
|
{% if (nftarr.lan_direct_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_direct_mac_addrs }} counter return
|
|
{% endif /* lan_direct_mac_addrs */ %}
|
|
{% endif /* lan_proxy_mode */ %}
|
|
|
|
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
|
counter goto homeproxy_redirect
|
|
{% endif %}
|
|
}
|
|
|
|
chain homeproxy_redirect {
|
|
meta mark {{ self_mark }} counter return
|
|
|
|
{% if (node_protect): %}
|
|
ip daddr @homeproxy_node_addr_v4 counter return comment "!{{ cfgname }}: node address"
|
|
{% endif %}
|
|
{% if (node_protect && v6_handled): %}
|
|
ip6 daddr @homeproxy_node_addr_v6 counter return comment "!{{ cfgname }}: node address"
|
|
{% endif %}
|
|
|
|
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_redirect_proxy_port
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_redirect_proxy_port
|
|
{% endif %}
|
|
|
|
ip daddr @homeproxy_local_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_local_addr_v6 counter return
|
|
{% endif %}
|
|
|
|
{% if (routing_mode !== 'custom'): %}
|
|
{% if (nftarr.lan_global_proxy_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_global_proxy_ipv4_ips }} counter goto homeproxy_redirect_proxy_port
|
|
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
|
{% if (nftarr.lan_global_proxy_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_global_proxy_ipv6_ips }} counter goto homeproxy_redirect_proxy_port
|
|
{% endif /* lan_global_proxy_ipv6_ips */ %}
|
|
{% if (nftarr.lan_global_proxy_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_global_proxy_mac_addrs }} counter goto homeproxy_redirect_proxy_port
|
|
{% endif /* lan_global_proxy_mac_addrs */ %}
|
|
{% endif /* routing_mode */ %}
|
|
|
|
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
|
|
{# No QUIC reject here: this whole block only renders when UDP is
|
|
proxyable (see the gate above), so rejecting QUIC on UDP 443 would
|
|
break HTTP/3 for traffic that can be proxied. The TUN block below,
|
|
which renders either way, carries the reject when UDP cannot be
|
|
proxied. #}
|
|
{% if (routing_mode === 'gfwlist' && dnsmasq_nftset()): %}
|
|
ip daddr != @homeproxy_gfw_list_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
|
ip daddr @homeproxy_mainland_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
|
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% endif /* routing_mode */ %}
|
|
|
|
{% if (nftarr.lan_gaming_mode_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_gaming_mode_ipv4_ips }} counter goto homeproxy_redirect_proxy
|
|
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
|
{% if (nftarr.lan_gaming_mode_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_gaming_mode_ipv6_ips }} counter goto homeproxy_redirect_proxy
|
|
{% endif /* lan_gaming_mode_ipv6_ips */ %}
|
|
{% if (nftarr.lan_gaming_mode_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_gaming_mode_mac_addrs }} counter goto homeproxy_redirect_proxy
|
|
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
|
|
|
counter goto homeproxy_redirect_proxy_port
|
|
}
|
|
|
|
chain homeproxy_output_redir {
|
|
type nat hook output priority filter -105; policy accept
|
|
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto tcp jump homeproxy_redirect
|
|
}
|
|
{% endif %}
|
|
|
|
{# UDP tproxy #}
|
|
{% if (match(proxy_mode, /tproxy/) && (outbound_udp_node !== 'nil' || routing_mode === 'custom')): %}
|
|
chain homeproxy_mangle_tproxy {
|
|
meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip to 127.0.0.1:{{ tproxy_port }} counter accept
|
|
{% if (v6_handled): %}
|
|
meta l4proto udp meta mark set {{ tproxy_mark }} tproxy ip6 to [::1]:{{ tproxy_port }} counter accept
|
|
{% endif %}
|
|
}
|
|
|
|
chain homeproxy_mangle_tproxy_port {
|
|
{% if (routing_port_set): %}
|
|
udp dport != @homeproxy_routing_port counter return
|
|
{% endif %}
|
|
goto homeproxy_mangle_tproxy
|
|
}
|
|
|
|
chain homeproxy_mangle_mark {
|
|
{% if (routing_port_set): %}
|
|
udp dport != @homeproxy_routing_port counter return
|
|
{% endif %}
|
|
meta l4proto udp meta mark set {{ tproxy_mark }} counter accept
|
|
}
|
|
|
|
chain homeproxy_mangle_lanac {
|
|
{% if (nftarr.listen_interfaces): %}
|
|
meta iifname != {{ nftarr.listen_interfaces_lo }} counter return
|
|
{% elif (!isEmpty(wan_devices)): %}
|
|
meta iifname {{ array_to_nftarr(wan_devices) }} counter return comment "!{{ cfgname }}: not the WAN side"
|
|
{% endif %}
|
|
meta iifname != lo udp dport 53 counter return
|
|
meta mark {{ self_mark }} counter return
|
|
|
|
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
|
{% if (nftarr.lan_proxy_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_proxy_ipv4_ips }} counter goto homeproxy_mangle_prerouting
|
|
{% endif /* lan_proxy_ipv4_ips */ %}
|
|
{% if (nftarr.lan_proxy_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_proxy_ipv6_ips }} counter goto homeproxy_mangle_prerouting
|
|
{% endif /* lan_proxy_ipv6_ips */ %}
|
|
{% if (nftarr.lan_proxy_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_proxy_mac_addrs }} counter goto homeproxy_mangle_prerouting
|
|
{% endif /* lan_proxy_mac_addrs */ %}
|
|
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
|
{% if (nftarr.lan_direct_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_direct_ipv4_ips }} counter return
|
|
{% endif /* lan_direct_ipv4_ips */ %}
|
|
{% if (nftarr.lan_direct_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_direct_ipv6_ips }} counter return
|
|
{% endif /* lan_direct_ipv6_ips */ %}
|
|
{% if (nftarr.lan_direct_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_direct_mac_addrs }} counter return
|
|
{% endif /* lan_direct_mac_addrs */ %}
|
|
{% endif /* lan_proxy_mode */ %}
|
|
|
|
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
|
counter goto homeproxy_mangle_prerouting
|
|
{% endif %}
|
|
}
|
|
|
|
chain homeproxy_mangle_prerouting {
|
|
{% if (node_protect): %}
|
|
ip daddr @homeproxy_node_addr_v4 counter return comment "!{{ cfgname }}: node address"
|
|
{% endif %}
|
|
{% if (node_protect && v6_handled): %}
|
|
ip6 daddr @homeproxy_node_addr_v6 counter return comment "!{{ cfgname }}: node address"
|
|
{% endif %}
|
|
|
|
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tproxy_port
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tproxy_port
|
|
{% endif %}
|
|
|
|
ip daddr @homeproxy_local_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_local_addr_v6 counter return
|
|
{% endif %}
|
|
|
|
{% if (routing_mode !== 'custom'): %}
|
|
{% if (nftarr.lan_global_proxy_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_global_proxy_ipv4_ips }} counter goto homeproxy_mangle_tproxy_port
|
|
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
|
{% if (nftarr.lan_global_proxy_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_global_proxy_ipv6_ips }} counter goto homeproxy_mangle_tproxy_port
|
|
{% endif /* lan_global_proxy_ipv6_ips */ %}
|
|
{% if (nftarr.lan_global_proxy_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_global_proxy_mac_addrs }} counter goto homeproxy_mangle_tproxy_port
|
|
{% endif /* lan_global_proxy_mac_addrs */ %}
|
|
{% endif /* routing_mode */ %}
|
|
|
|
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
|
|
{% if (routing_mode === 'gfwlist' && dnsmasq_nftset()): %}
|
|
ip daddr != @homeproxy_gfw_list_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
|
ip daddr @homeproxy_mainland_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
|
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% endif /* routing_mode */ %}
|
|
|
|
{% if (nftarr.lan_gaming_mode_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_gaming_mode_ipv4_ips }} counter goto homeproxy_mangle_tproxy
|
|
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
|
{% if (nftarr.lan_gaming_mode_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_gaming_mode_ipv6_ips }} counter goto homeproxy_mangle_tproxy
|
|
{% endif /* lan_gaming_mode_ipv6_ips */ %}
|
|
{% if (nftarr.lan_gaming_mode_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_gaming_mode_mac_addrs }} counter goto homeproxy_mangle_tproxy
|
|
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
|
|
|
counter goto homeproxy_mangle_tproxy_port
|
|
}
|
|
|
|
chain homeproxy_mangle_output {
|
|
meta mark {{ self_mark }} counter return
|
|
|
|
{% if (node_protect): %}
|
|
ip daddr @homeproxy_node_addr_v4 counter return comment "!{{ cfgname }}: node address"
|
|
{% endif %}
|
|
{% if (node_protect && v6_handled): %}
|
|
ip6 daddr @homeproxy_node_addr_v6 counter return comment "!{{ cfgname }}: node address"
|
|
{% endif %}
|
|
|
|
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_mark
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_mark
|
|
{% endif %}
|
|
|
|
ip daddr @homeproxy_local_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_local_addr_v6 counter return
|
|
{% endif %}
|
|
|
|
ip daddr @homeproxy_wan_direct_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_wan_direct_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
|
|
{% if (routing_mode === 'gfwlist' && dnsmasq_nftset()): %}
|
|
ip daddr != @homeproxy_gfw_list_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
|
ip daddr @homeproxy_mainland_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
|
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% endif /* routing_mode */ %}
|
|
|
|
counter goto homeproxy_mangle_mark
|
|
}
|
|
|
|
chain mangle_prerouting {
|
|
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_lanac{{ v6_warn_comment }}
|
|
}
|
|
|
|
chain mangle_output {
|
|
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto udp jump homeproxy_mangle_output
|
|
}
|
|
{% endif %}
|
|
|
|
{# TUN #}
|
|
{% if (match(proxy_mode, /tun/)): %}
|
|
chain homeproxy_mangle_lanac {
|
|
iifname {{ tun_name }} counter return
|
|
udp dport 53 counter return
|
|
|
|
{% if (nftarr.listen_interfaces): %}
|
|
meta iifname != {{ nftarr.listen_interfaces }} counter return
|
|
{% elif (!isEmpty(wan_devices)): %}
|
|
meta iifname {{ array_to_nftarr(wan_devices) }} counter return comment "!{{ cfgname }}: not the WAN side"
|
|
{% endif %}
|
|
|
|
{% if (control_info.lan_proxy_mode === 'listed_only'): %}
|
|
{% if (nftarr.lan_proxy_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_proxy_ipv4_ips }} counter goto homeproxy_mangle_tun
|
|
{% endif /* lan_proxy_ipv4_ips */ %}
|
|
{% if (nftarr.lan_proxy_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_proxy_ipv6_ips }} counter goto homeproxy_mangle_tun
|
|
{% endif /* lan_proxy_ipv6_ips */ %}
|
|
{% if (nftarr.lan_proxy_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_proxy_mac_addrs }} counter goto homeproxy_mangle_tun
|
|
{% endif /* lan_proxy_mac_addrs */ %}
|
|
{% elif (control_info.lan_proxy_mode === 'except_listed'): %}
|
|
{% if (nftarr.lan_direct_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_direct_ipv4_ips }} counter return
|
|
{% endif /* lan_direct_ipv4_ips */ %}
|
|
{% if (nftarr.lan_direct_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_direct_ipv6_ips }} counter return
|
|
{% endif /* lan_direct_ipv6_ips */ %}
|
|
{% if (nftarr.lan_direct_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_direct_mac_addrs }} counter return
|
|
{% endif /* lan_direct_mac_addrs */ %}
|
|
{% endif /* lan_proxy_mode */ %}
|
|
|
|
{% if (control_info.lan_proxy_mode !== 'listed_only'): %}
|
|
counter goto homeproxy_mangle_tun
|
|
{% endif %}
|
|
}
|
|
|
|
chain homeproxy_mangle_tun_mark {
|
|
{% if (routing_port_set): %}
|
|
{% if (proxy_mode === 'tun'): %}
|
|
tcp dport != @homeproxy_routing_port counter return
|
|
{% endif /* proxy_mode */ %}
|
|
udp dport != @homeproxy_routing_port counter return
|
|
{% endif /* routing_port */ %}
|
|
|
|
counter meta mark set {{ tun_mark }}
|
|
}
|
|
|
|
chain homeproxy_mangle_tun {
|
|
iifname {{ tun_name }} counter return
|
|
{% if (tun_self_mark): %}
|
|
meta mark {{ tun_self_mark }} counter return comment "!{{ cfgname }}: sing-box's own traffic"
|
|
{% endif %}
|
|
|
|
{% if (node_protect): %}
|
|
ip daddr @homeproxy_node_addr_v4 counter return comment "!{{ cfgname }}: node address"
|
|
{% endif %}
|
|
{% if (node_protect && v6_handled): %}
|
|
ip6 daddr @homeproxy_node_addr_v6 counter return comment "!{{ cfgname }}: node address"
|
|
{% endif %}
|
|
|
|
ip daddr @homeproxy_wan_proxy_addr_v4 counter goto homeproxy_mangle_tun_mark
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_wan_proxy_addr_v6 counter goto homeproxy_mangle_tun_mark
|
|
{% endif %}
|
|
|
|
ip daddr @homeproxy_local_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_local_addr_v6 counter return
|
|
{% endif %}
|
|
|
|
{% if (routing_mode !== 'custom'): %}
|
|
{% if (nftarr.lan_global_proxy_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_global_proxy_ipv4_ips }} counter goto homeproxy_mangle_tun_mark
|
|
{% endif /* lan_global_proxy_ipv4_ips */ %}
|
|
{% if (nftarr.lan_global_proxy_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_global_proxy_ipv6_ips }} counter goto homeproxy_mangle_tun_mark
|
|
{% endif /* lan_global_proxy_ipv6_ips */ %}
|
|
{% if (nftarr.lan_global_proxy_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_global_proxy_mac_addrs }} counter goto homeproxy_mangle_tun_mark
|
|
{% endif /* lan_global_proxy_mac_addrs */ %}
|
|
{% endif /* routing_mode */ %}
|
|
|
|
{% if (nftarr.wan_direct_ipv4_ips): %}
|
|
ip daddr {{ nftarr.wan_direct_ipv4_ips }} counter return
|
|
{% endif /* wan_direct_ipv4_ips */ %}
|
|
{% if (nftarr.wan_direct_ipv6_ips): %}
|
|
ip6 daddr {{ nftarr.wan_direct_ipv6_ips }} counter return
|
|
{% endif /* wan_direct_ipv6_ips */ %}
|
|
|
|
{% if (routing_mode === 'gfwlist' && dnsmasq_nftset()): %}
|
|
ip daddr != @homeproxy_gfw_list_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr != @homeproxy_gfw_list_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% if (udp_quic_rejected): %}
|
|
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (UDP path is not UDP-native)"
|
|
{% endif /* udp_quic_rejected */ %}
|
|
{% elif (routing_mode === 'bypass_mainland_china' || bypass_cn_traffic === '1'): %}
|
|
ip daddr @homeproxy_mainland_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr @homeproxy_mainland_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% if (udp_quic_rejected): %}
|
|
udp dport { 80, 443 } counter reject comment "!{{ cfgname }}: Block QUIC (UDP path is not UDP-native)"
|
|
{% endif /* udp_quic_rejected */ %}
|
|
{% elif (routing_mode === 'proxy_mainland_china'): %}
|
|
ip daddr != @homeproxy_mainland_addr_v4 counter return
|
|
{% if (v6_handled): %}
|
|
ip6 daddr != @homeproxy_mainland_addr_v6 counter return
|
|
{% endif /* v6_handled */ %}
|
|
{% endif /* routing_mode */ %}
|
|
|
|
{% if (nftarr.lan_gaming_mode_ipv4_ips): %}
|
|
ip saddr {{ nftarr.lan_gaming_mode_ipv4_ips }} counter meta mark set {{ tun_mark }}
|
|
{% endif /* lan_gaming_mode_ipv4_ips */ %}
|
|
{% if (nftarr.lan_gaming_mode_ipv6_ips): %}
|
|
ip6 saddr {{ nftarr.lan_gaming_mode_ipv6_ips }} counter meta mark set {{ tun_mark }}
|
|
{% endif /* lan_gaming_mode_ipv6_ips */ %}
|
|
{% if (nftarr.lan_gaming_mode_mac_addrs): %}
|
|
ether saddr {{ nftarr.lan_gaming_mode_mac_addrs }} counter meta mark set {{ tun_mark }}
|
|
{% endif /* lan_gaming_mode_mac_addrs */ %}
|
|
|
|
counter goto homeproxy_mangle_tun_mark
|
|
}
|
|
|
|
chain mangle_prerouting {
|
|
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_lanac{{ v6_warn_comment }}
|
|
}
|
|
|
|
chain mangle_output {
|
|
meta nfproto { {{ (v6_handled) ? 'ipv4, ipv6' : 'ipv4' }} } meta l4proto { {{ (proxy_mode === 'tun') ? 'tcp, udp' : 'udp' }} } jump homeproxy_mangle_tun
|
|
}
|
|
{% endif %}
|