1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
// shunt - ip rule and route renderer
//
// Renders the argv arrays for the policy routing tables and their rules.
// Pure, like nft.uc - nothing here talks to the kernel.
//
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2026 Dirk Brenken <dev@brenken.org>
import { addr_family, DEFAULTS } from 'shunt.nft';
const RE_IFACE = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,14}$/;
const BLACKHOLE_METRIC = 9999;
export function compile(policies, marks, opts) {
let mask = opts?.mask ?? DEFAULTS.mask;
let add = [], del = [], tables = [], issues = [];
let by_name = {};
for (let m in (marks ?? []))
by_name[m.name] = m;
function reject(policy, entry, reason) {
push(issues, { policy, entry, reason });
}
for (let p in (policies ?? [])) {
let m = by_name[p?.name];
if (!m)
continue;
let iface = p.interface;
if (type(iface) != 'string' || match(iface, RE_IFACE) == null) {
reject(p.name, iface, 'invalid or missing interface');
continue;
}
let fb = p.fallback ?? 'main';
if (fb != 'main' && fb != 'block') {
reject(p.name, p.fallback, "fallback must be 'main' or 'block'");
continue;
}
let gw = { '4': null, '6': null };
let gw_bad = false;
for (let fam in [ '4', '6' ]) {
let g = p[`gw${fam}`];
if (g == null)
continue;
if (sprintf('%d', addr_family(g)) == fam && index(g, '/') < 0)
gw[fam] = g;
else {
reject(p.name, g, `invalid gw${fam}`);
gw_bad = true;
}
}
if (gw_bad)
continue;
let fwmark = sprintf('0x%x/0x%x', m.mark, mask);
let table = sprintf('%d', m.rt_table);
let pref = sprintf('%d', m.rt_prio);
push(tables, sprintf('%d\tshunt_%s', m.rt_table, m.name));
for (let fam in [ '4', '6' ]) {
let v = `-${fam}`;
let route = [ 'ip', v, 'route', 'replace', 'default' ];
if (gw[fam])
push(route, 'via', gw[fam]);
push(route, 'dev', iface, 'table', table);
push(add, route);
if (fb == 'block')
push(add, [ 'ip', v, 'route', 'replace', 'blackhole',
'default', 'metric',
sprintf('%d', BLACKHOLE_METRIC),
'table', table ]);
push(add, [ 'ip', v, 'rule', 'add', 'pref', pref,
'fwmark', fwmark, 'lookup', table ]);
unshift(del, [ 'ip', v, 'route', 'flush', 'table', table ]);
unshift(del, [ 'ip', v, 'rule', 'del', 'pref', pref ]);
}
}
return {
add,
del,
rt_tables: length(tables) ? join('\n', tables) + '\n' : '',
issues
};
};
|