@@ -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
237284backend_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
244291backend_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
267314backend_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
272319backend_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
427481backend_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
442496backend_iptables_apply_mode () {
0 commit comments