Skip to content

Commit 7472dfa

Browse files
committed
fix(firewall): harden iptables apply/fallback and real-kernel validation
1 parent 5a0396d commit 7472dfa

1 file changed

Lines changed: 103 additions & 49 deletions

File tree

lib/firewall/backend_iptables.sh

Lines changed: 103 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,54 @@ iptables_add_if_missing() {
127127

128128
ipt="$(iptables_cmd)"
129129
if ! "${ipt}" -t "${table}" -C "$@" >/dev/null 2>&1; then
130-
"${ipt}" -t "${table}" -A "$@"
130+
if ! "${ipt}" -t "${table}" -A "$@"; then
131+
return 1
132+
fi
133+
# iptables-nft can emit append failures on stderr in some kernels while
134+
# still returning success; verify rule presence to enforce correctness.
135+
if ! "${ipt}" -t "${table}" -C "$@" >/dev/null 2>&1; then
136+
return 1
137+
fi
138+
fi
139+
return 0
140+
}
141+
142+
iptables_try_add_if_missing_quiet() {
143+
local table="${1:?missing table}"
144+
shift
145+
local ipt
146+
147+
ipt="$(iptables_cmd)"
148+
if "${ipt}" -t "${table}" -C "$@" >/dev/null 2>&1; then
149+
return 0
150+
fi
151+
if ! "${ipt}" -t "${table}" -A "$@" >/dev/null 2>&1; then
152+
return 1
153+
fi
154+
"${ipt}" -t "${table}" -C "$@" >/dev/null 2>&1
155+
}
156+
157+
backend_iptables_rule_desc() {
158+
local rendered
159+
printf -v rendered '%q ' "$@"
160+
printf '%s\n' "${rendered% }"
161+
}
162+
163+
backend_iptables_ensure_chain_checked() {
164+
local table="${1:?missing table}"
165+
local chain="${2:?missing chain}"
166+
if ! iptables_ensure_chain "${table}" "${chain}"; then
167+
FW_LAST_ERROR="failed to ensure chain table=${table} chain=${chain}"
168+
return "${E_FIREWALL_APPLY}"
169+
fi
170+
}
171+
172+
backend_iptables_add_rule_checked() {
173+
local table="${1:?missing table}"
174+
shift
175+
if ! iptables_add_if_missing "${table}" "$@"; then
176+
FW_LAST_ERROR="failed to add rule table=${table} rule=$(backend_iptables_rule_desc "$@")"
177+
return "${E_FIREWALL_APPLY}"
131178
fi
132179
}
133180

@@ -235,38 +282,38 @@ backend_iptables_cleanup() {
235282
}
236283

237284
backend_iptables_apply_anti_loop() {
238-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -i lo -j RETURN
239-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -d 127.0.0.0/8 -j RETURN
240-
iptables_add_if_missing nat "${BOX_CHAIN_NAT}" -i lo -j RETURN
241-
iptables_add_if_missing nat "${BOX_CHAIN_NAT}" -d 127.0.0.0/8 -j RETURN
285+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -i lo -j RETURN
286+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -d 127.0.0.0/8 -j RETURN
287+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_NAT}" -i lo -j RETURN
288+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_NAT}" -d 127.0.0.0/8 -j RETURN
242289
}
243290

244291
backend_iptables_apply_tailscale_bypass() {
245292
# Preserve tailscale transport and route ownership.
246-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -i "${BOX_TAILSCALE_IFACE}" -j RETURN
247-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -o "${BOX_TAILSCALE_IFACE}" -j RETURN
248-
iptables_add_if_missing nat "${BOX_CHAIN_NAT}" -i "${BOX_TAILSCALE_IFACE}" -j RETURN
249-
iptables_add_if_missing nat "${BOX_CHAIN_NAT}" -o "${BOX_TAILSCALE_IFACE}" -j RETURN
293+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -i "${BOX_TAILSCALE_IFACE}" -j RETURN
294+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -o "${BOX_TAILSCALE_IFACE}" -j RETURN
295+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_NAT}" -i "${BOX_TAILSCALE_IFACE}" -j RETURN
296+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_NAT}" -o "${BOX_TAILSCALE_IFACE}" -j RETURN
250297

251-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -d "${BOX_TAILNET_IPV4_CIDR}" -j RETURN
252-
iptables_add_if_missing nat "${BOX_CHAIN_NAT}" -d "${BOX_TAILNET_IPV4_CIDR}" -j RETURN
298+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -d "${BOX_TAILNET_IPV4_CIDR}" -j RETURN
299+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_NAT}" -d "${BOX_TAILNET_IPV4_CIDR}" -j RETURN
253300

254301
# Keep existing tailscale-marked packets out of Box interception.
255-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -m mark --mark "${BOX_TAILSCALE_FWMARK}" -j RETURN
302+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -m mark --mark "${BOX_TAILSCALE_FWMARK}" -j RETURN
256303

257304
# Preserve MagicDNS resolver reachability.
258-
iptables_add_if_missing mangle "${BOX_CHAIN_DNS_MANGLE}" -d "${BOX_TAILSCALE_DNS_RESOLVER}" -p udp --dport 53 -j RETURN
259-
iptables_add_if_missing mangle "${BOX_CHAIN_DNS_MANGLE}" -d "${BOX_TAILSCALE_DNS_RESOLVER}" -p tcp --dport 53 -j RETURN
260-
iptables_add_if_missing nat "${BOX_CHAIN_DNS_NAT}" -d "${BOX_TAILSCALE_DNS_RESOLVER}" -p udp --dport 53 -j RETURN
261-
iptables_add_if_missing nat "${BOX_CHAIN_DNS_NAT}" -d "${BOX_TAILSCALE_DNS_RESOLVER}" -p tcp --dport 53 -j RETURN
305+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_DNS_MANGLE}" -d "${BOX_TAILSCALE_DNS_RESOLVER}" -p udp --dport 53 -j RETURN
306+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_DNS_MANGLE}" -d "${BOX_TAILSCALE_DNS_RESOLVER}" -p tcp --dport 53 -j RETURN
307+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_DNS_NAT}" -d "${BOX_TAILSCALE_DNS_RESOLVER}" -p udp --dport 53 -j RETURN
308+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_DNS_NAT}" -d "${BOX_TAILSCALE_DNS_RESOLVER}" -p tcp --dport 53 -j RETURN
262309

263310
# IPv6 tailnet bypass is handled by not touching ip6tables in this backend.
264311
# TODO(phase-3): add dedicated ip6tables/nft backend for explicit v6 chain rules.
265312
}
266313

267314
backend_iptables_apply_policy_placeholders() {
268315
# TODO(phase-3): UID/GID/interface/MAC policy graph.
269-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -m comment --comment "BOX_POLICY_PLACEHOLDER" -j RETURN
316+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -m comment --comment "BOX_POLICY_PLACEHOLDER" -j RETURN
270317
}
271318

272319
backend_iptables_ensure_policy_route() {
@@ -351,31 +398,36 @@ backend_iptables_apply_mode_rules() {
351398

352399
case "${mode}" in
353400
tun)
354-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -j RETURN
401+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -j RETURN
355402
;;
356403
redirect)
357-
iptables_add_if_missing nat "${BOX_CHAIN_NAT}" -p tcp -j REDIRECT --to-ports "${BOX_REDIR_PORT}"
404+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_NAT}" -p tcp -j REDIRECT --to-ports "${BOX_REDIR_PORT}"
358405
;;
359406
tproxy)
360-
if backend_iptables_probe_tproxy; then
361-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -p tcp -j TPROXY --on-port "${BOX_TPROXY_PORT}" --tproxy-mark "${BOX_FWMARK}"
362-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -p udp -j TPROXY --on-port "${BOX_TPROXY_PORT}" --tproxy-mark "${BOX_FWMARK}"
407+
if backend_iptables_probe_tproxy \
408+
&& iptables_try_add_if_missing_quiet mangle "${BOX_CHAIN_MANGLE}" -p tcp -j TPROXY --on-port "${BOX_TPROXY_PORT}" --tproxy-mark "${BOX_FWMARK}" \
409+
&& iptables_try_add_if_missing_quiet mangle "${BOX_CHAIN_MANGLE}" -p udp -j TPROXY --on-port "${BOX_TPROXY_PORT}" --tproxy-mark "${BOX_FWMARK}"; then
410+
:
363411
else
412+
iptables_delete_all mangle "${BOX_CHAIN_MANGLE}" -p tcp -j TPROXY --on-port "${BOX_TPROXY_PORT}" --tproxy-mark "${BOX_FWMARK}"
413+
iptables_delete_all mangle "${BOX_CHAIN_MANGLE}" -p udp -j TPROXY --on-port "${BOX_TPROXY_PORT}" --tproxy-mark "${BOX_FWMARK}"
364414
log "WARN" "firewall" "FW_TPROXY_DOWNGRADE" "TPROXY unavailable; using MARK fallback"
365-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -p tcp -j MARK --set-xmark "${BOX_FWMARK}"
366-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -p udp -j MARK --set-xmark "${BOX_FWMARK}"
415+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -p tcp -j MARK --set-xmark "${BOX_FWMARK}"
416+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -p udp -j MARK --set-xmark "${BOX_FWMARK}"
367417
fi
368418
if ! backend_iptables_ensure_policy_route; then
369419
return "${E_FIREWALL_APPLY}"
370420
fi
371421
;;
372422
mixed|enhance)
373-
iptables_add_if_missing nat "${BOX_CHAIN_NAT}" -p tcp -j REDIRECT --to-ports "${BOX_REDIR_PORT}"
374-
if backend_iptables_probe_tproxy; then
375-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -p udp -j TPROXY --on-port "${BOX_TPROXY_PORT}" --tproxy-mark "${BOX_FWMARK}"
423+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_NAT}" -p tcp -j REDIRECT --to-ports "${BOX_REDIR_PORT}"
424+
if backend_iptables_probe_tproxy \
425+
&& iptables_try_add_if_missing_quiet mangle "${BOX_CHAIN_MANGLE}" -p udp -j TPROXY --on-port "${BOX_TPROXY_PORT}" --tproxy-mark "${BOX_FWMARK}"; then
426+
:
376427
else
428+
iptables_delete_all mangle "${BOX_CHAIN_MANGLE}" -p udp -j TPROXY --on-port "${BOX_TPROXY_PORT}" --tproxy-mark "${BOX_FWMARK}"
377429
log "WARN" "firewall" "FW_TPROXY_DOWNGRADE" "TPROXY unavailable for UDP; using MARK fallback"
378-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -p udp -j MARK --set-xmark "${BOX_FWMARK}"
430+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -p udp -j MARK --set-xmark "${BOX_FWMARK}"
379431
fi
380432
if ! backend_iptables_ensure_policy_route; then
381433
return "${E_FIREWALL_APPLY}"
@@ -395,24 +447,26 @@ backend_iptables_apply_dns_strategy() {
395447

396448
case "${dns_mode}" in
397449
disable)
398-
iptables_add_if_missing mangle "${BOX_CHAIN_DNS_MANGLE}" -j RETURN
399-
iptables_add_if_missing nat "${BOX_CHAIN_DNS_NAT}" -j RETURN
450+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_DNS_MANGLE}" -j RETURN
451+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_DNS_NAT}" -j RETURN
400452
;;
401453
redirect)
402-
iptables_add_if_missing nat "${BOX_CHAIN_DNS_NAT}" -p udp --dport 53 -j REDIRECT --to-ports "${BOX_DNS_PORT}"
403-
iptables_add_if_missing nat "${BOX_CHAIN_DNS_NAT}" -p tcp --dport 53 -j REDIRECT --to-ports "${BOX_DNS_PORT}"
454+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_DNS_NAT}" -p udp --dport 53 -j REDIRECT --to-ports "${BOX_DNS_PORT}"
455+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_DNS_NAT}" -p tcp --dport 53 -j REDIRECT --to-ports "${BOX_DNS_PORT}"
404456
;;
405457
tproxy)
406-
if backend_iptables_probe_tproxy; then
407-
iptables_add_if_missing mangle "${BOX_CHAIN_DNS_MANGLE}" -p udp --dport 53 -j TPROXY --on-port "${BOX_DNS_PORT}" --tproxy-mark "${BOX_FWMARK}"
408-
iptables_add_if_missing mangle "${BOX_CHAIN_DNS_MANGLE}" -p tcp --dport 53 -j TPROXY --on-port "${BOX_DNS_PORT}" --tproxy-mark "${BOX_FWMARK}"
458+
if backend_iptables_probe_tproxy \
459+
&& iptables_try_add_if_missing_quiet mangle "${BOX_CHAIN_DNS_MANGLE}" -p udp --dport 53 -j TPROXY --on-port "${BOX_DNS_PORT}" --tproxy-mark "${BOX_FWMARK}" \
460+
&& iptables_try_add_if_missing_quiet mangle "${BOX_CHAIN_DNS_MANGLE}" -p tcp --dport 53 -j TPROXY --on-port "${BOX_DNS_PORT}" --tproxy-mark "${BOX_FWMARK}"; then
409461
if ! backend_iptables_ensure_policy_route; then
410462
return "${E_FIREWALL_APPLY}"
411463
fi
412464
else
465+
iptables_delete_all mangle "${BOX_CHAIN_DNS_MANGLE}" -p udp --dport 53 -j TPROXY --on-port "${BOX_DNS_PORT}" --tproxy-mark "${BOX_FWMARK}"
466+
iptables_delete_all mangle "${BOX_CHAIN_DNS_MANGLE}" -p tcp --dport 53 -j TPROXY --on-port "${BOX_DNS_PORT}" --tproxy-mark "${BOX_FWMARK}"
413467
log "WARN" "firewall" "FW_DNS_TPROXY_DOWNGRADE" "DNS tproxy unavailable; redirecting DNS instead"
414-
iptables_add_if_missing nat "${BOX_CHAIN_DNS_NAT}" -p udp --dport 53 -j REDIRECT --to-ports "${BOX_DNS_PORT}"
415-
iptables_add_if_missing nat "${BOX_CHAIN_DNS_NAT}" -p tcp --dport 53 -j REDIRECT --to-ports "${BOX_DNS_PORT}"
468+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_DNS_NAT}" -p udp --dport 53 -j REDIRECT --to-ports "${BOX_DNS_PORT}"
469+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_DNS_NAT}" -p tcp --dport 53 -j REDIRECT --to-ports "${BOX_DNS_PORT}"
416470
fi
417471
;;
418472
*)
@@ -425,18 +479,18 @@ backend_iptables_apply_dns_strategy() {
425479
}
426480

427481
backend_iptables_create_base() {
428-
iptables_ensure_chain mangle "${BOX_CHAIN_MANGLE}"
429-
iptables_ensure_chain nat "${BOX_CHAIN_NAT}"
430-
iptables_ensure_chain mangle "${BOX_CHAIN_DNS_MANGLE}"
431-
iptables_ensure_chain nat "${BOX_CHAIN_DNS_NAT}"
432-
433-
iptables_add_if_missing mangle PREROUTING -j "${BOX_CHAIN_MANGLE}"
434-
iptables_add_if_missing mangle OUTPUT -j "${BOX_CHAIN_MANGLE}"
435-
iptables_add_if_missing nat PREROUTING -j "${BOX_CHAIN_NAT}"
436-
iptables_add_if_missing nat OUTPUT -j "${BOX_CHAIN_NAT}"
437-
438-
iptables_add_if_missing mangle "${BOX_CHAIN_MANGLE}" -j "${BOX_CHAIN_DNS_MANGLE}"
439-
iptables_add_if_missing nat "${BOX_CHAIN_NAT}" -j "${BOX_CHAIN_DNS_NAT}"
482+
backend_iptables_ensure_chain_checked mangle "${BOX_CHAIN_MANGLE}"
483+
backend_iptables_ensure_chain_checked nat "${BOX_CHAIN_NAT}"
484+
backend_iptables_ensure_chain_checked mangle "${BOX_CHAIN_DNS_MANGLE}"
485+
backend_iptables_ensure_chain_checked nat "${BOX_CHAIN_DNS_NAT}"
486+
487+
backend_iptables_add_rule_checked mangle PREROUTING -j "${BOX_CHAIN_MANGLE}"
488+
backend_iptables_add_rule_checked mangle OUTPUT -j "${BOX_CHAIN_MANGLE}"
489+
backend_iptables_add_rule_checked nat PREROUTING -j "${BOX_CHAIN_NAT}"
490+
backend_iptables_add_rule_checked nat OUTPUT -j "${BOX_CHAIN_NAT}"
491+
492+
backend_iptables_add_rule_checked mangle "${BOX_CHAIN_MANGLE}" -j "${BOX_CHAIN_DNS_MANGLE}"
493+
backend_iptables_add_rule_checked nat "${BOX_CHAIN_NAT}" -j "${BOX_CHAIN_DNS_NAT}"
440494
}
441495

442496
backend_iptables_apply_mode() {

0 commit comments

Comments
 (0)