🦄 Sync 2026-10-06 03:16:48

This commit is contained in:
github-actions[bot] committed 2026-10-06 03:16:48 +08:00
1 parent 487c4fad50
commit d1c43b1bc6
15 files changed
+370 -123

No files matched your search

@@ -22,6 +22,11 @@ local function get_device(interface)
return ""
end
-- %q is a Lua literal, not shell quoting: $(...) and backticks survive it
local function shell_quote(value)
return "'" .. tostring(value or ""):gsub("'", "'\"'\"'") .. "'"
end
local function exec_output(cmd)
local lines = {}
local f = io.popen(cmd)
@@ -40,7 +45,7 @@ end
-- non-zero: when the circular buffer is partially filled the last valid row still gets
-- a trailing comma. Strip it before wrapping with "[...]" to produce valid JSON.
local function bwc_rows(dev)
local f = io.popen(string.format("luci-bwc -i %q 2>/dev/null", dev))
local f = io.popen("luci-bwc -i " .. shell_quote(dev) .. " 2>/dev/null")
if not f then return {} end
local output = f:read("*all")
f:close()
@@ -856,9 +861,9 @@ local methods = {
end
local cmd
if interface == "" then
cmd = "tracebox -s /usr/share/tracebox/omr-mptcp-trace.lua " .. server
cmd = "tracebox -s /usr/share/tracebox/omr-mptcp-trace.lua " .. shell_quote(server)
else
cmd = "tracebox -s /usr/share/tracebox/omr-mptcp-trace.lua -i " .. interface .. " " .. server
cmd = "tracebox -s /usr/share/tracebox/omr-mptcp-trace.lua -i " .. shell_quote(interface) .. " " .. shell_quote(server)
end
return { output = exec_output(cmd) }
end
@@ -21,15 +21,19 @@
"mptcp_connections",
"mptcp_check_trace",
"get_settings",
"set_settings",
"get_interface_settings",
"set_interface_settings",
"diagnose"
]
}
},
"write": {
"uci": [ "openmptcprouter", "network" ]
"uci": [ "openmptcprouter", "network" ],
"ubus": {
"luci.mptcp": [
"set_settings",
"set_interface_settings"
]
}
}
}
}
+2 -2
View File
@@ -6,8 +6,8 @@ include $(TOPDIR)/rules.mk
LUCI_TITLE:=LuCI support for shunt
LUCI_DEPENDS:=+luci-base +shunt
PKG_VERSION:=0.1.10
PKG_RELEASE:=3
PKG_VERSION:=0.1.11
PKG_RELEASE:=4
PKG_LICENSE:=Apache-2.0
PKG_MAINTAINER:=Dirk Brenken <dev@brenken.org>
@@ -226,7 +226,8 @@ function renderPolicies(st) {
rows.push(E('tr', { 'class': 'tr' }, [
E('td', { 'class': 'td' }, [p.name]),
E('td', { 'class': 'td' }, [p.action || 'route']),
E('td', { 'class': 'td' }, [p.interface || '-']),
E('td', { 'class': 'td' }, [p.interface ||
(p.tproxy_port ? ':%d'.format(p.tproxy_port) : '-')]),
E('td', { 'class': 'td' }, [fmtMark(p.mark)]),
E('td', { 'class': 'td' }, [bypass ? '-' : fmtCount(p.rt_table)]),
E('td', { 'class': 'td' }, [bypass ? '-' : fmtCount(p.rules)]),
@@ -105,10 +105,11 @@ return view.extend({
o.editable = true;
o = s.option(form.ListValue, 'action', _('Action'),
_('Route the selected traffic into the interface below, or \
bypass it - a bypass policy marks nothing and exempts the traffic from every policy after it.'));
_('Route the selected traffic into the interface below, \
bypass it - a bypass policy marks nothing and exempts the traffic from every policy after it - or hand it to a transparent proxy on the router.'));
o.value('route', _('Route into a policy interface'));
o.value('bypass', _('Bypass - leave the traffic alone'));
o.value('tproxy', _('Transparent proxy on the router'));
o.default = 'route';
o.editable = true;
@@ -124,11 +125,21 @@ return view.extend({
}
});
o = s.option(form.ListValue, 'fallback', _('Fallback Behavior'));
o = s.option(form.Value, 'tproxy_port', _('Proxy Port'),
_('The port the transparent proxy listens on, for tcp and udp. \
It must listen on loopback or on the wildcard address. Needs kmod-nft-tproxy.'));
o.datatype = 'port';
o.rmempty = false;
o.depends('action', 'tproxy');
o = s.option(form.ListValue, 'fallback', _('Fallback Behavior'),
_('What new traffic does when the interface is down or, for a \
transparent proxy, when nothing listens on its port.'));
o.value('main', _('Fall through to the normal uplink'));
o.value('block', _('Block the traffic (killswitch)'));
o.default = 'main';
o.depends('action', 'route');
o.depends('action', 'tproxy');
o = s.option(form.Flag, 'keep_local', _('Keep Local Traffic'),
_('Traffic to networks the routing table already has a route for \
+87 -66
View File
@@ -1,7 +1,7 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:358
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:359
msgid "%d name(s) every %ds"
msgstr ""
@@ -54,7 +54,7 @@ msgstr ""
msgid "Apply failed: %s"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:129
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:139
msgid "Block the traffic (killswitch)"
msgstr ""
@@ -66,7 +66,7 @@ msgstr ""
msgid "Bytes routed"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:427
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:428
msgid ""
"Ceiling in seconds for a learned address in its Set. An address learned from "
"a DNS answer expires with the TTL of that answer, clamped to the range 60 to "
@@ -74,7 +74,7 @@ msgid ""
"interval."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:147
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:158
msgid "Client MACs this policy applies to, ORed with the addresses above."
msgstr ""
@@ -82,7 +82,7 @@ msgstr ""
msgid "Client Selectors"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:142
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:153
msgid ""
"Client addresses or prefixes this policy applies to. Leave empty to apply to "
"every client."
@@ -92,7 +92,7 @@ msgstr ""
msgid "Compression pointer in the question"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:407
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:408
msgid "DNS Observer Settings"
msgstr ""
@@ -100,23 +100,23 @@ msgstr ""
msgid "DNS responses observed on %s since the service started"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:413
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:414
msgid "Debug Logging"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:162
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:173
msgid "Destination Addresses"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:157
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:168
msgid "Destination Ports"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:163
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:174
msgid "Destination addresses or prefixes to route into this policy."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:172
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:183
msgid "Domain Files"
msgstr ""
@@ -124,15 +124,15 @@ msgstr ""
msgid "Domain is in no policy"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:167
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:178
msgid "Domains"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:410
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:411
msgid "Enable the shunt service."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:409
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:410
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:102
msgid "Enabled"
msgstr ""
@@ -148,7 +148,7 @@ msgid ""
"and only to those domains."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:179
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:190
msgid "Expecting a bare file name or an absolute path, without whitespace"
msgstr ""
@@ -160,7 +160,7 @@ msgstr ""
msgid "Extension header chain too long"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:128
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:138
msgid "Fall through to the normal uplink"
msgstr ""
@@ -168,11 +168,11 @@ msgstr ""
msgid "Fallback"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:127
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:135
msgid "Fallback Behavior"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:173
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:184
msgid ""
"Files with one domain pattern per line, for large or community-maintained "
"lists. A bare name lives under /tmp/shunt, an absolute path is used as "
@@ -184,7 +184,7 @@ msgstr ""
msgid "Frame ends mid-structure"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:406
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:407
msgid "General Settings"
msgstr ""
@@ -196,11 +196,11 @@ msgstr ""
msgid "IP fragment"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:184
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:195
msgid "IPv4 Gateway Override"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:190
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:201
msgid "IPv6 Gateway Override"
msgstr ""
@@ -213,15 +213,15 @@ msgid "Inconsistent UDP length"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:212
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:115
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:116
msgid "Interface"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:133
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:144
msgid "Keep Local Traffic"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:380
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:381
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:126
msgid "Learned addresses"
msgstr ""
@@ -230,14 +230,14 @@ msgstr ""
msgid "Learned addresses (%d)"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:414
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:415
msgid ""
"Log every observed DNS answer and every set write. Useful for a bug report, "
"noisy in normal operation - on a router running adblock roughly half of all "
"answers are error replies, and each one gets a line."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:417
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:418
msgid "Manage rp_filter"
msgstr ""
@@ -245,7 +245,7 @@ msgstr ""
msgid "Mark"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:426
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:427
msgid "Maximum Entry Lifetime"
msgstr ""
@@ -285,7 +285,7 @@ msgstr ""
msgid "No policy is active."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:271
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:272
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:73
msgid "No shunt config found!"
msgstr ""
@@ -312,11 +312,11 @@ msgid ""
"observed DNS answers."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:446
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:447
msgid "Observed Devices"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:185
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:196
msgid ""
"Only needed when the gateway discovered from netifd is wrong. Point to point "
"interfaces need no gateway at all."
@@ -330,7 +330,7 @@ msgstr ""
msgid "Packets routed"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:441
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:442
msgid "Passive DNS Observer"
msgstr ""
@@ -344,17 +344,17 @@ msgstr ""
msgid "Policy"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:278
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:279
msgid ""
"Policy based routing by mac, source, destination and domain. For further "
"information please check the %s."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:383
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:384
msgid "Poll"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:421
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:422
msgid "Poll Interval"
msgstr ""
@@ -362,11 +362,15 @@ msgstr ""
msgid "Processing Log"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:151
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:162
msgid "Protocols"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:442
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:128
msgid "Proxy Port"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:443
msgid ""
"Read DNS answers as they pass the LAN device, whichever resolver produced "
"them. Required for wildcard domains, which cannot be resolved ahead of time."
@@ -380,7 +384,7 @@ msgstr ""
msgid "Refresh"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:213
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:224
msgid "Refresh Domain Files"
msgstr ""
@@ -388,7 +392,7 @@ msgstr ""
msgid "Refresh failed: %s"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:252
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:253
msgid "Rejected settings"
msgstr ""
@@ -396,11 +400,11 @@ msgstr ""
msgid "Reserved label type"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:152
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:163
msgid "Restrict to tcp, udp or both. A port without a protocol covers both."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:343
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:344
msgid "Reverse path filtering is strict"
msgstr ""
@@ -410,8 +414,9 @@ msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:108
msgid ""
"Route the selected traffic into the interface below, or bypass it - a bypass "
"policy marks nothing and exempts the traffic from every policy after it."
"Route the selected traffic into the interface below, bypass it - a bypass "
"policy marks nothing and exempts the traffic from every policy after it - or "
"hand it to a transparent proxy on the router."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:216
@@ -422,16 +427,16 @@ msgstr ""
msgid "Rules"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:470
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:205
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:471
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:216
msgid "Save & Restart"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:422
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:423
msgid "Seconds between poll cycles."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:375
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:376
msgid "Service"
msgstr ""
@@ -440,31 +445,31 @@ msgstr ""
msgid "Set Reporting"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:418
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:419
msgid ""
"Set rp_filter to 2 on shunt's own policy interfaces, at start and when one "
"comes up."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:403
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:404
msgid "Settings"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:435
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:436
msgid ""
"Should be at least twice the poll interval (%d), otherwise entries expire "
"between cycles."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:158
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:169
msgid "Single ports or ranges like 8000-8080, ANDed with the addresses below."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:141
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:152
msgid "Source Addresses"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:146
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:157
msgid "Source MAC Addresses"
msgstr ""
@@ -472,11 +477,11 @@ msgstr ""
msgid "Static Destinations"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:463
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:464
msgid "Stop"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:345
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:346
msgid ""
"Strict rp_filter will drop shunt's marked traffic on %s. Set rp_filter to 2 "
"on the policy interface, enable rp_filter_manage to have shunt do it, or "
@@ -487,23 +492,29 @@ msgstr ""
msgid "Table"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:447
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:448
msgid ""
"The LAN device the DNS answers cross on their way to the clients, normally "
"br-lan. One entry per network segment, see the README."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:116
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:117
msgid ""
"The device or logical interface this policy routes into. A netifd name is "
"resolved to its device; any other device name is used as entered."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:129
msgid ""
"The port the transparent proxy listens on, for tcp and udp. It must listen "
"on loopback or on the wildcard address. Needs kmod-nft-tproxy."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/logtemplate.js:44
msgid "The syslog output, pre-filtered for messages related to: %s"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:254
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:255
msgid ""
"These entries were skipped. Everything else was applied - a rejected entry "
"never takes the service down."
@@ -517,28 +528,38 @@ msgstr ""
msgid "Too many stacked VLAN tags"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:134
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:145
msgid ""
"Traffic to networks the routing table already has a route for - your other "
"subnets, your static routes - stays on the normal path instead of entering "
"the policy interface. Turn this off only for a hermetic killswitch."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:112
msgid "Transparent proxy on the router"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:134
msgid "Truncated response"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:377
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:378
msgid "Version"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:136
msgid ""
"What new traffic does when the interface is down or, for a transparent "
"proxy, when nothing listens on its port."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/setreport.js:235
msgid ""
"What the nftables Sets currently hold. Counters are reset whenever an entry "
"is refreshed, so they show recent activity, not a lifetime total."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:382
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:383
msgid "across all policies"
msgstr ""
@@ -546,17 +567,17 @@ msgstr ""
msgid "all policies"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:168
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:179
msgid ""
"example.com matches that name only, *.example.com matches its subdomains but "
"not the apex - list both to cover both."
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:123
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/policies.js:124
msgid "interface"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:353
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:354
msgid "n/a"
msgstr ""
@@ -564,7 +585,7 @@ msgstr ""
msgid "no answer from the backend"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:279
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:280
msgid "online documentation"
msgstr ""
@@ -592,10 +613,10 @@ msgstr ""
msgid "stopped"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:359
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:360
msgid "unavailable - ucode-mod-resolv missing"
msgstr ""
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:384
#: applications/luci-app-shunt/htdocs/luci-static/resources/view/shunt/overview.js:385
msgid "wildcards are covered by the observer only"
msgstr ""
+2 -2
View File
@@ -5,8 +5,8 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=shunt
PKG_VERSION:=0.1.10
PKG_RELEASE:=4
PKG_VERSION:=0.1.11
PKG_RELEASE:=5
PKG_LICENSE:=GPL-3.0-or-later
PKG_LICENSE_FILES:=
PKG_MAINTAINER:=Dirk Brenken <dev@brenken.org>
+44 -6
View File
@@ -52,7 +52,7 @@ shunt check
* Wildcard domains (`*.example.com`), learned passively as clients use them
* Domain lists from a file - a community-maintained set of thousands of names, without a single UCI entry per name
* Per-policy killswitch: hold the traffic when the interface drops, instead of leaking it out of the normal uplink
* Per-policy action: `route` marks the traffic for the policy interface, `bypass` exempts it from every policy below
* Per-policy action: `route` marks the traffic for the policy interface, `bypass` exempts it from every policy below, `tproxy` hands it to a transparent proxy on the router
* Your own networks stay reachable from a policy client, without listing them anywhere
* Puts its own table back when something deletes it, `fw4 flush` included
* Own nftables table and routing tables, disjoint mark range - runs beside `pbr` and `mwan3`
@@ -65,6 +65,8 @@ shunt check
* OpenWrt with fw4/nftables
* `ucode` plus `ucode-mod-fs`, `ucode-mod-socket`, `ucode-mod-uci`, `ucode-mod-uloop`, `ucode-mod-resolv`, `ucode-mod-ubus`, `ucode-mod-rtnl`, `ucode-mod-log` and `rpcd-mod-ucode` - all pulled in by the package; `ip` is not needed, routes and rules are written over rtnetlink
`kmod-nft-tproxy` only for a `tproxy` policy. It is not pulled in: without it a tproxy policy is skipped as an issue and everything else is applied.
`ucode-mod-resolv` and `ucode-mod-ubus` are soft at runtime: without resolv, poll is skipped and the observer carries the service alone; without ubus, gateway discovery and interface events are skipped and the config's own values are used. Both cost one warning in the log, not a failed start.
<a id="which-tunnels-work"></a>
@@ -186,9 +188,10 @@ Each `config policy` section is one routing policy. **The section must be named,
| Option | Description |
| :--- | :--- |
| enabled | `0` skips the section entirely |
| action | `route` (default) or `bypass`, see below |
| action | `route` (default), `bypass` or `tproxy`, see below |
| interface | netifd logical name (`wan`, `trm_wwan`) or raw netdev (`wg0`, `phy0-sta0`); `route` only |
| fallback | `main` (default) or `block`, see below; `route` only |
| tproxy_port | port the transparent proxy listens on; `tproxy` only, required there |
| fallback | `main` (default) or `block`, see below; `route` and `tproxy` |
| keep_local | `1` (default) keeps traffic on `main` where `main` has a route for it, see below; `route` only |
| gw4 / gw6 | gateway override; normally unnecessary |
| src | client addresses or CIDRs whose traffic this policy owns |
@@ -276,7 +279,7 @@ Run from cron that covers the reboot case as well: the first run after boot crea
What a large list costs: 100k patterns take about two seconds to load and roughly 40 MB of memory on an x86 test box, so expect several seconds and a proportionally smaller footprint on a router, at start and on every refresh. A file above 4 MiB is refused outright as an issue. `shunt check` prints how many files were read and how many patterns they contributed, `shunt refresh` does the same for the running daemon, and `ubus call shunt status` shows the count under `files` together with the time of the last refresh.
### Actions: route or bypass
### Actions: route, bypass or tproxy
`action 'route'` (default) is the policy shape everything above describes: matching traffic is marked and looked up in the policy's own table.
@@ -297,6 +300,40 @@ config policy 'vpn'
Order matters and only order: a bypass section placed after the policy it is meant to except from never sees the packet.
<a id="transparent-proxy-tproxy"></a>
### Transparent proxy: tproxy
`action 'tproxy'` hands the selected traffic to a transparent proxy running on the router - `hev-socks5-tproxy`, `sing-box` or `xray` in tproxy mode, or anything else that accepts `IP_TRANSPARENT` connections. shunt only steers: it does not start, configure or watch the proxy. It needs `kmod-nft-tproxy`.
```
config policy 'proxy'
option action 'tproxy'
option tproxy_port '1088'
option fallback 'block'
list src '192.168.1.0/24'
list domain '*.example.com'
```
The selectors are the usual ones. tproxy carries tcp and udp only, so a tproxy policy without `proto` covers both, and the proxy must listen for every protocol the policy selects. A policy consumes a mark and gets a routing table holding a single `local default dev lo` route - that is how the selected packets reach a socket on the router without their destination being rewritten.
shunt delivers to `127.0.0.1` and `::1`. A proxy listening on loopback or on the wildcard address is found; one bound to the router's LAN address is not. The address is not left to the kernel on purpose: without one it picks the address of the incoming interface, and a proxy bound to loopback would never be found - the statement fails silently and the traffic is not proxied.
`fallback` decides what a **new** flow does when no listener is found - proxy stopped, crashed, wrong port: `main` (default) lets it take the normal uplink, `block` drops it. A flow that was already proxied is dropped either way once its listener is gone. Its state is in the proxy, and its conntrack entry was never NATed, so forwarding the rest of it would put the client's own address on the uplink - measured, which is why this does not follow `fallback`.
`interface`, `keep_local` and the gateway overrides are not read, and `keep_local '1'` set explicitly is reported. It would break the policy: main would forward packets the kernel has already handed to the proxy's socket, and the kernel drops those.
Only traffic passing through the router is proxied. tproxy exists in the prerouting hook alone, so the router's own traffic is not touched by a tproxy policy - which also keeps the proxy's upstream connections from looping back into it.
The proxied traffic is delivered to the router itself, so fw4 applies its **input** rules, not its forward rules. On `lan`, with input `ACCEPT`, nothing needs doing. A zone with input `REJECT` or `DROP` - a guest network, typically - drops it, and shunt cannot override that from its own table, because a drop in any base chain is final. The packet still carries its original destination port, so a rule on the proxy port does not match; match the policy's mark instead (`shunt check` prints it):
```
config rule
option name 'Allow-guest-tproxy'
option src 'guest'
option mark '0x01000000/0xff000000'
option target 'ACCEPT'
```
<a id="local-traffic-keep_local"></a>
### Local traffic: keep_local
@@ -468,11 +505,11 @@ fwmark <index> << 24, mask 0xff000000
ct mark same bits, set on a flow's first packet
ip rule pref 31000 + <index> keep_local's main lookup
31500 + <index> the policy table
routing table 8000 + <index>
routing table 8000 + <index>, for tproxy: local default dev lo
/etc/iproute2/rt_tables.d/shunt.conf the table name mapping, for `ip route show` only
```
A `bypass` policy is only a rule in the prerouting and output chains: no mark, no table, no ip rule, and it does not count against the 255. The mark mask is fixed at `0xff000000`, which allows 255 policies. The `output` chain is `type route` so the router's own marked traffic is re-routed after the mark is set.
A `bypass` policy is only a rule in the prerouting and output chains: no mark, no table, no ip rule, and it does not count against the 255. A `tproxy` policy has rules in prerouting only, and no keep_local rule. The mark mask is fixed at `0xff000000`, which allows 255 policies. The `output` chain is `type route` so the router's own marked traffic is re-routed after the mark is set.
Every set carries per-element counters, so "is this element ever hit" is one look at `nft list set inet shunt <set>` rather than a tcpdump session. The two kinds count different things: nftables tests a rule left to right, so a **client** set counts every packet that matched the selector, whether or not the destination matched afterwards; a **learned** set is the last lookup in the rule, so a hit there means the packet really was marked. A busy client beside learned addresses at zero is a client that has not visited any of the routed domains, not a fault.
@@ -595,6 +632,7 @@ These are consequences of the design, stated rather than worked around:
* **DNS over TCP is not observed.** Port 53 over TCP needs reassembly, which is out of scope; answers large enough to force TCP are rare in the traffic shunt cares about.
* **Route and rule application is best effort.** At boot a tunnel interface may not exist yet. A rule over an empty table falls through to `main`, so the failure mode is "policy not applied yet", never "traffic broken". Each distinct reason is one warning line.
* **No interface hotplug.** A device that appears later is picked up on the next `ifup` event or within one poll interval, not immediately.
* **tproxy is detected by its module file.** shunt applies a tproxy policy when `nft_tproxy` is loaded or installed under `/lib/modules`. A custom kernel with tproxy built in has neither, and its tproxy policies are skipped as an issue.
* **An outside flush of nftables is repaired, not prevented.** Any tool may delete shunt's table - `fw4 flush` does. shunt re-applies it within one poll interval, or sooner, and until then nothing is marked.
**Out of scope permanently:** resolver-integrated set population (dnsmasq `nftset`, AdGuard Home etc.). Being independent of the DNS backend is the entire point of the project, so adopting a backend-specific mechanism would give up the one property that distinguishes it. Also out: DSCP tagging and user include files.
+14 -5
View File
@@ -7,7 +7,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2026 Dirk Brenken <dev@brenken.org>
import { popen, readfile } from 'fs';
import { popen, readfile, access } from 'fs';
const ubus = require('ubus');
const rtnl = require('rtnl');
@@ -64,12 +64,20 @@ function render() {
cfg.policies = f.policies;
let m = match_compile(cfg.policies);
let n = nft_compile(cfg.policies);
let n = nft_compile(cfg.policies, { tproxy: tproxy_available() });
let r = route_compile(cfg.policies, n.marks);
return { cfg, files: f, matcher: m, nft: n, route: r };
}
// Same test as the daemon's, so LuCI reports the policy the daemon skips.
function tproxy_available() {
let rel = trim(readfile('/proc/sys/kernel/osrelease') ?? '');
return !!(access('/sys/module/nft_tproxy') ||
(length(rel) && access(`/lib/modules/${rel}/nft_tproxy.ko`)));
}
// Policy devices whose marked traffic the kernel would drop: max(all, <dev>),
// blocked only when all is strict (1) and the device is not loose itself. Same
// logic as the daemon. When rp_filter_manage is on the daemon has already set
@@ -268,12 +276,13 @@ return {
rt_table: m.rt_table,
rt_prio: m.rt_prio,
interface: (m.action == 'route') ? p?.interface : null,
fallback: (m.action == 'route') ? p?.fallback : null,
fallback: (m.action != 'bypass') ? p?.fallback : null,
tproxy_port: m.port,
domains: length(p?.domains ?? []),
file_domains: length(p?.file_domains ?? []),
rules: (m.action == 'route' && rules)
rules: (m.action != 'bypass' && rules)
? length(rules[m.name] ?? []) : null,
routes: (m.action == 'route') ? routes[m.name] : null
routes: (m.action != 'bypass') ? routes[m.name] : null
});
}
+19 -2
View File
@@ -8,7 +8,7 @@
// SPDX-License-Identifier: GPL-3.0-or-later
// Copyright (c) 2026 Dirk Brenken <dev@brenken.org>
import { popen, writefile, readfile, unlink, mkdir, error as fs_error } from 'fs';
import { popen, writefile, readfile, unlink, mkdir, access, error as fs_error } from 'fs';
import { openlog, syslog, LOG_PID, LOG_DAEMON, LOG_ERR, LOG_WARNING,
LOG_NOTICE, LOG_INFO, LOG_DEBUG } from 'log';
import { load as cfg_load, parse as cfg_parse, MIN as cfg_min } from 'shunt.config';
@@ -287,6 +287,17 @@ function check_rp_filter(policies) {
dev, dev));
}
// kmod-nft-tproxy is not a dependency, so a tproxy policy may name a module
// that is not there - and nft loads the ruleset as one batch, so one rule
// the kernel cannot resolve would take every policy down. Loaded, or
// installed and left to the kernel's autoload on first use.
function tproxy_available() {
let rel = trim(readfile('/proc/sys/kernel/osrelease') ?? '');
return !!(access('/sys/module/nft_tproxy') ||
(length(rel) && access(`/lib/modules/${rel}/nft_tproxy.ko`)));
}
// silent: build only what a teardown consumes and say nothing about the
// configuration - flush() needs route.del and nothing else.
function build_state(silent) {
@@ -312,7 +323,10 @@ function build_state(silent) {
report('domain', matcher.issues);
}
let n = nft_compile(cfg.policies);
// A teardown renders without the check: a policy whose module has
// gone since it was applied still owns a table and rules to remove.
let n = nft_compile(cfg.policies,
{ tproxy: silent ? true : tproxy_available() });
if (!silent)
report('nft', n.issues);
@@ -749,6 +763,9 @@ function check() {
for (let m in state.nft.marks)
if (m.action == 'bypass')
printf(' %-16s bypass\n', m.name);
else if (m.action == 'tproxy')
printf(' %-16s mark 0x%08x table %d pref %d tproxy :%d\n',
m.name, m.mark, m.rt_table, m.rt_prio, m.port);
else
printf(' %-16s mark 0x%08x table %d pref %d\n',
m.name, m.mark, m.rt_table, m.rt_prio);
+1
View File
@@ -166,6 +166,7 @@ export function parse(sections) {
keep_local: v.keep_local,
gw4: v.gw4,
gw6: v.gw6,
tproxy_port: v.tproxy_port,
src: to_list(v.src),
src_mac: to_list(v.src_mac),
dport: to_list(v.dport),
+133 -26
View File
@@ -17,8 +17,16 @@ export const DEFAULTS = {
// What a policy does with the traffic it selects. `route` marks it for its
// own table, `bypass` only ends rule evaluation, so a later policy cannot
// claim the same packet. Everything else is a configuration error.
export const ACTIONS = { route: true, bypass: true };
// claim the same packet, `tproxy` hands it to a transparent proxy listening
// on the router. Everything else is a configuration error.
export const ACTIONS = { route: true, bypass: true, tproxy: true };
// Where tproxy delivers. Without an address the kernel takes the primary
// address of the incoming interface, and a proxy bound to loopback - the
// usual way to keep it off the LAN - is then never found: the statement
// fails, and the rule with it. Loopback is found by a loopback listener and
// a wildcard one alike, whichever interface the packet came in on.
export const TPROXY_ADDR = { '4': '127.0.0.1', '6': '[::1]' };
const RE_NAME = /^[A-Za-z0-9_]{1,24}$/;
const RE_V4 = /^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})(\/([0-9]{1,2}))?$/;
@@ -132,7 +140,7 @@ export function compile(policies, opts) {
// Rule records, not strings: a MAC rule belongs in prerouting only, and
// both chains must render from one ordered list or precedence breaks.
let issues = [], marks = [], sets = [], rules4 = [], rules6 = [];
let restore = [];
let restore = [], restore_out = [];
let idx = 0;
let learn = {};
@@ -154,10 +162,40 @@ export function compile(policies, opts) {
if (action == null) {
reject(pname, p.action,
"invalid action - 'route' or 'bypass'");
"invalid action - 'route', 'bypass' or 'tproxy'");
continue;
}
let tp_port = null, tp_block = false;
if (action == 'tproxy') {
// Checked here and not left to nft: the ruleset is one batch, and
// a tproxy rule the kernel cannot load would take every other
// policy down with it.
if (opts?.tproxy === false) {
reject(pname, null,
'tproxy needs kmod-nft-tproxy - policy skipped');
continue;
}
tp_port = port_spec(p.tproxy_port);
if (tp_port == null || index(tp_port, '-') >= 0) {
reject(pname, p.tproxy_port,
'invalid or missing tproxy_port - expected 1-65535');
continue;
}
let fb = p.fallback ?? 'main';
if (fb != 'main' && fb != 'block') {
reject(pname, p.fallback, "fallback must be 'main' or 'block'");
continue;
}
tp_block = (fb == 'block');
}
let src = { '4': [], '6': [] }, dst = { '4': [], '6': [] };
for (let a in (p.src ?? [])) {
@@ -210,6 +248,11 @@ export function compile(policies, opts) {
if (length(ports) && !length(protos))
protos = [ 'tcp', 'udp' ];
// tproxy has nothing to hand over but tcp and udp, and the kernel
// refuses the statement without the protocol pinned.
if (action == 'tproxy' && !length(protos))
protos = [ 'tcp', 'udp' ];
// A named domain file counts as a domain selector whether or not it
// was readable at this moment: the policy's sets exist and stay
// empty until the file is, and `shunt flush` tears the policy down
@@ -245,14 +288,14 @@ export function compile(policies, opts) {
// A bypass policy owns no mark, no table and no rule - it only ends
// evaluation - so it costs nothing from the mark capacity.
if (action == 'route' && ++idx > capacity) {
if (action != 'bypass' && ++idx > capacity) {
reject(pname, null,
sprintf('mark capacity exceeded (%d policies fit in mask 0x%08x)',
capacity, mask));
continue;
}
let mark = (action == 'route') ? idx << shift : null;
let mark = (action != 'bypass') ? idx << shift : null;
// One transport term for all three rule shapes. `th dport` reads the
// port at the transport header offset, which works for tcp and udp
// alike, so a port without a protocol needs no rule per protocol.
@@ -273,10 +316,36 @@ export function compile(policies, opts) {
// route also records the mark on the conntrack entry, so the flow's
// later packets can be marked from it without another lookup - see
// the restore rules below.
let stmt = (action == 'bypass')
? sprintf('%scounter return', l4)
: sprintf('%smeta mark set (meta mark & 0x%08x) | 0x%08x ct mark set (ct mark & 0x%08x) | 0x%08x counter return',
l4, ~mask & 0xffffffff, mark, ~mask & 0xffffffff, mark);
let setmark = (mark != null)
? sprintf('meta mark set (meta mark & 0x%08x) | 0x%08x ct mark set (ct mark & 0x%08x) | 0x%08x ',
~mask & 0xffffffff, mark, ~mask & 0xffffffff, mark)
: '';
// tproxy names the family of its address, so its statement exists
// once per family. When the socket lookup finds no listener the
// statement ends the rule unmatched and the packet goes on to the
// next one - with fallback 'block' that is the drop rendered right
// after it, otherwise the normal uplink.
function stmt(fam) {
if (action == 'bypass')
return sprintf('%scounter return', l4);
if (action == 'tproxy')
return sprintf('%stproxy %s to %s:%s %scounter return', l4,
fam == '4' ? 'ip' : 'ip6', TPROXY_ADDR[fam], tp_port, setmark);
return sprintf('%s%scounter return', l4, setmark);
}
// A rule, and for a blocking tproxy policy the drop that catches it
// failing. tproxy exists in prerouting only, so none of it goes to
// output: the router's own traffic is not proxied, and the proxy's
// own upstream connections cannot loop back into it.
function emit(rules, out, pre, fam) {
push(rules, { out: out && action != 'tproxy',
text: sprintf('\t\t%s%s', pre, stmt(fam)) });
if (tp_block)
push(rules, { out: false,
text: sprintf('\t\t%s%scounter drop', pre, l4) });
}
// The decision for a flow is made on its first packet and kept:
// later packets in the original direction take the mark from the
@@ -288,22 +357,52 @@ export function compile(policies, opts) {
// nft has no expression-to-expression OR. Family-agnostic, so one
// rule serves both. Replies stay unmarked, as they always did - a
// marked reply would look up the policy table and miss the LAN.
if (action == 'route')
push(restore, sprintf(
'\t\tct state != new ct direction original ct mark & 0x%08x == 0x%08x meta mark set (meta mark & 0x%08x) | 0x%08x counter return',
mask, mark, ~mask & 0xffffffff, mark));
let ctm = sprintf('ct state != new ct direction original ct mark & 0x%08x == 0x%08x ',
mask, mark);
let rmark = sprintf('meta mark set (meta mark & 0x%08x) | 0x%08x ',
~mask & 0xffffffff, mark);
if (action == 'route') {
let r = sprintf('\t\t%s%scounter return', ctm, rmark);
push(restore, r);
push(restore_out, r);
}
// A proxied flow needs the statement on every packet, not only the
// mark: a udp proxy answers from a socket bound to the original
// destination, so the mark alone delivers the client's next datagram
// to that socket rather than the listener - measured. tcp would find
// its connected socket either way. Without a listener the flow
// cannot go on anywhere: its state is in the proxy, and the conntrack
// entry was never NATed, so forwarding it would put the client's
// own address on the uplink - measured too. It is dropped whatever
// the fallback; fallback decides about new flows only.
if (action == 'tproxy') {
let pl = length(protos) == 1 ? protos[0] : sprintf('{ %s }', join(', ', protos));
for (let fam in [ '4', '6' ])
push(restore, sprintf(
'\t\t%smeta nfproto ipv%s meta l4proto %s tproxy %s to %s:%s %scounter return',
ctm, fam, pl, fam == '4' ? 'ip' : 'ip6', TPROXY_ADDR[fam],
tp_port, rmark));
push(restore, sprintf('\t\t%scounter drop', ctm));
}
// Two rule priorities per routing policy, in two bands 500 apart:
// keep_local's main lookup keeps the band a released version already
// used, so an upgrade removes the old rule with the new one's del,
// and the policy table rule moves up out of the way.
push(marks, (action == 'bypass')
let entry = (action == 'bypass')
? { name: pname, action, index: null, mark: null,
rt_table: null, rt_prio: null, rt_prio_local: null }
: { name: pname, action, index: idx, mark,
rt_table: 8000 + idx,
rt_prio: 31500 + idx,
rt_prio_local: 31000 + idx });
rt_prio_local: 31000 + idx };
if (action == 'tproxy')
entry.port = +tp_port;
push(marks, entry);
if (has_mac)
push(sets, sprintf(
@@ -359,9 +458,8 @@ export function compile(policies, opts) {
for (let px in prefixes)
if (length(dst[fam]))
push(rules, { out: px.out,
text: sprintf('\t\t%s%s daddr @%s %s',
px.pre, ip, set_name('s', fam, pname), stmt) });
emit(rules, px.out, sprintf('%s%s daddr @%s ',
px.pre, ip, set_name('s', fam, pname)), fam);
if (has_dom)
learn[set_name('d', fam, pname)] = true;
@@ -374,13 +472,21 @@ export function compile(policies, opts) {
for (let px in prefixes) {
if (has_dom)
push(rules, { out: px.out,
text: sprintf('\t\t%s%s daddr @%s %s',
px.pre, ip, set_name('d', fam, pname), stmt) });
emit(rules, px.out, sprintf('%s%s daddr @%s ',
px.pre, ip, set_name('d', fam, pname)), fam);
if (!has_dst_any && !has_dom && (px.per_family || fam == '4'))
push(rules, { out: px.out,
text: sprintf('\t\t%s%s', px.pre, stmt) });
// A rule with no address in it serves both families at once,
// except under tproxy, whose statement names one: there it
// is rendered per family, pinned with nfproto.
if (!has_dst_any && !has_dom) {
if (px.per_family)
emit(rules, px.out, px.pre, fam);
else if (action == 'tproxy')
emit(rules, px.out,
sprintf('%smeta nfproto ipv%s ', px.pre, fam), fam);
else if (fam == '4')
emit(rules, px.out, px.pre, fam);
}
}
}
}
@@ -390,6 +496,7 @@ export function compile(policies, opts) {
// has and is not re-evaluated against sets that changed since.
let settled = length(restore)
? [ '\t\tct state != new counter return' ] : [];
let settled_out = length(restore_out) ? settled : [];
let setup = join('\n', [
`destroy table ${TABLE}`,
@@ -402,7 +509,7 @@ export function compile(policies, opts) {
'\t}',
'\tchain output {',
'\t\ttype route hook output priority mangle; policy accept;',
...restore, ...settled,
...restore_out, ...settled_out,
...map(filter(rules4, (r) => r.out), (r) => r.text),
...map(filter(rules6, (r) => r.out), (r) => r.text),
'\t}',
+33
View File
@@ -20,10 +20,12 @@ const BLACKHOLE_METRIC = 9999;
const AF = { '4': 2, '6': 10 };
const ANY = { '4': '0.0.0.0/0', '6': '::/0' };
const RTN_UNICAST = 1;
const RTN_LOCAL = 2;
const RTN_BLACKHOLE = 6;
const RTPROT_BOOT = 3;
const RT_SCOPE_UNIVERSE = 0;
const RT_SCOPE_LINK = 253;
const RT_SCOPE_HOST = 254;
const RT_TABLE_MAIN = 254;
const FR_ACT_TO_TBL = 1;
@@ -63,6 +65,37 @@ export function compile(policies, marks, opts) {
if (m.action == 'bypass')
continue;
// tproxy delivers to a socket on the router, so its table holds a
// single local default route and nothing of the interface options
// applies. keep_local in particular must not: main would forward
// a packet the tproxy statement has already given a socket, and
// the kernel drops those on forward - measured. The fallback is
// nft's, see there.
if (m.action == 'tproxy') {
if (to_bool(p.keep_local, false) === true)
reject(p.name, p.keep_local, 'keep_local does not apply to tproxy, ignored');
push(tables, sprintf('%d\tshunt_%s', m.rt_table, m.name));
for (let fam in [ '4', '6' ]) {
let family = AF[fam];
push(add, { cmd: 'newroute', msg: { family,
table: m.rt_table, dst: ANY[fam], oif: 'lo',
type: RTN_LOCAL, protocol: RTPROT_BOOT,
scope: RT_SCOPE_HOST } });
push(add, { cmd: 'newrule', msg: { family,
action: FR_ACT_TO_TBL, priority: m.rt_prio,
fwmark: m.mark, fwmask: mask, table: m.rt_table } });
unshift(del, { cmd: 'flush', msg: { family, table: m.rt_table } });
unshift(del, { cmd: 'delrule', msg: { family, priority: m.rt_prio } });
unshift(del, { cmd: 'delrule', msg: { family, priority: m.rt_prio_local } });
}
continue;
}
let iface = p.interface;
if (type(iface) != 'string' || match(iface, RE_IFACE) == null) {
reject(p.name, iface, 'invalid or missing interface');
+1 -1
View File
@@ -90,7 +90,7 @@ export function describe(op) {
switch (op?.cmd) {
case 'newroute':
return sprintf('ip %s route replace %sdefault%s%s%s table %d', v,
m.type == 6 ? 'blackhole ' : '',
m.type == 6 ? 'blackhole ' : m.type == 2 ? 'local ' : '',
m.gateway ? ` via ${m.gateway}` : '',
m.oif ? ` dev ${m.oif}` : '',
m.priority != null ? ` metric ${m.priority}` : '', m.table);
+3 -3
View File
@@ -50,12 +50,12 @@
include $(TOPDIR)/rules.mk
PKG_NAME:=wwand
PKG_VERSION:=1.7.0_pre10
PKG_RELEASE:=72
PKG_VERSION:=1.7.0_pre11
PKG_RELEASE:=73
PKG_SOURCE_PROTO:=git
PKG_SOURCE_URL:=https://github.com/ddimension/wwand.git
PKG_SOURCE_VERSION:=dae47c97f79756c61db259dde7f30a228972d2eb
PKG_SOURCE_VERSION:=61f7cdf34dd060ba8e9f3fdb7271a9451a0cf82b
PKG_MIRROR_HASH:=skip
PKG_LICENSE:=GPL-2.0-only