Files
omarandClaude Opus 5 164b703a7d feat(l3): ping travels the tunnel by default, and every LAN zone can reach it
l3_tunnel was opt-in, and "off" had no honest win left in it. Off, a LAN ping
is decided by `untunnelable` alone and every rung is a drop (block) or a
disclosure (icmp/direct send the echo out of the WAN with the client's real
address). "Ping works" was never the state where ping was tunnelled — it was
the state where ping was leaking. On, an L3-capable outbound carries the echo
and one that is not drops it honestly: adapter.JudgeFlow returns ActionDrop for
an ICMP flow whose outbound is not a tun.Port, so no reply is forged. The price
is a standing TUN + gVisor netstack, ~2 MB RSS, and it is stated where the
option is.

The switch stays. It is a real answer on a 32/64 MB device and when bisecting
whether the L3 ingress is what broke a box — but it is now a WARNED answer:
ValidateGlobals says what the off state does to ping and names the policy that
takes over. Two combinations also changed meaning and are now reported:
untunnelable=icmp is no longer "block plus working ping" (the prerouting L3
mark claims every ICMP packet before the forward chain the echo accept lives
in, and a LAN host's ICMP errors are marked in with them and dropped in the
TUN), and the existing =direct report gains a sibling rather than standing
alone.

The fw4 seeding was the second half of the same problem. The divert set spans
every LAN inbound and every iface:/zone: rule source, but 30_shater-core seeded
a forwarding into shater_l3 for `lan` only — so on a multi-zone router ICMP
from the other zones is marked, routed, accepted by `inet shater`, and dropped
by fw4's zone policy with nothing in any log. Every zone gets a forwarding now,
guarded by a scan of the actual src/dest pairs so a re-run adds nothing. Every
zone including an uplink, because guessing which zones hold clients is wrong
somewhere and a superfluous entry authorises nothing: accept_to_shater_l3 is
`oifname "shater-l3*" accept`, and the only thing that routes a packet into
that device is our own fwmark rule.

scripts/testbed-lao.sh builds the second LAN zone this needs to be visible at
all. It is not installed by the package — that is the whole opt-in mechanism.

Verified on local_openwrt (ImmortalWrt 25.12.1 r37978): three runs of the
seeder leave exactly one forwarding per zone (lan/wan/lao) and no existing
section altered; deleting the lao forwarding removes `jump accept_to_shater_l3`
from chain forward_lao and re-seeding restores it; with the idempotency guard
disabled two runs produce nine forwardings instead of three.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BHw89tdWddzhjUc4bAH4tS
2026-07-27 00:26:00 +03:00

124 lines
4.7 KiB
Bash

#!/bin/sh
# scripts/testbed-lao.sh — add a SECOND LAN network ("lao", 10.67.1.0/24) in its
# own fw4 zone, on a testbed router.
#
# WHY IT EXISTS
#
# Almost every zone-related defect in this project is invisible on a router with
# one LAN zone, because "the zone" and "the LAN" are the same thing there. The
# divert set the daemon builds spans every LAN inbound and every `iface:`/`zone:`
# rule source, so on a multi-zone router traffic from the other zones is marked,
# routed, accepted by `inet shater` — and then dropped by fw4's zone policy,
# silently. Reproducing that needs a second zone and nothing else: no second
# physical port, no client, no traffic. This script makes one.
#
# WHY IT IS NOT SHIPPED
#
# It lives in scripts/ and is NOT installed by openwrt/shater-core/Makefile,
# which lists every file it installs by name. That is the whole opt-in mechanism,
# and it was chosen over the alternatives on purpose:
#
# - an extra /etc/uci-defaults/ file would run on EVERY install, handing a
# second network and a second firewall zone to every ordinary user — the one
# thing this must not do;
# - an environment variable read inside 30_shater-core is unreachable in
# practice: that script is deleted after its first successful run, so there
# is no later moment at which an operator could set the variable and re-run it;
# - a separate package would need a feed entry, a build, a release and a
# version, for a file that exists to be scp'd onto one VM.
#
# USAGE
#
# scp scripts/testbed-lao.sh root@testbed:/tmp/ && ssh root@testbed sh /tmp/testbed-lao.sh
# ssh root@testbed sh /tmp/testbed-lao.sh --remove
#
# It is idempotent (every section is NAMED and guarded), purely additive, and
# touches no existing section. Running it three times in a row leaves exactly one
# of everything.
set -e
REMOVE=0
[ "$1" = "--remove" ] && REMOVE=1
if [ "$REMOVE" = 1 ]; then
uci -q delete firewall.lao_fwd
uci -q delete firewall.lao
uci -q delete dhcp.lao
uci -q delete network.lao
uci -q delete network.br_lao
uci -q commit firewall
uci -q commit dhcp
uci -q commit network
/etc/init.d/network reload
/etc/init.d/firewall reload
echo "lao removed"
exit 0
fi
# --- L2: an empty bridge -----------------------------------------------------
# No ports on purpose: the point is a second ROUTED network with its own firewall
# zone, and giving it a switch port would mean re-cabling a testbed for nothing.
#
# bridge_empty is what makes a portless bridge usable. Without it netifd leaves a
# member-less bridge down (no carrier), the `lao` interface never comes up, fw4
# resolves `list network 'lao'` to an EMPTY device set, and the zone silently
# matches nothing — which would make this script a worse instrument than no
# instrument, since it would look set up and prove nothing.
if ! uci -q get network.br_lao >/dev/null; then
uci set network.br_lao=device
uci set network.br_lao.name='br-lao'
uci set network.br_lao.type='bridge'
uci set network.br_lao.bridge_empty='1'
fi
# --- L3: the interface -------------------------------------------------------
# 10.67.1.0/24 is deliberately far from anything a home LAN or a proxy node uses.
# ipaddr+netmask rather than CIDR: CIDR in `ipaddr` is a 25.12 convenience and
# this script should also run on an older testbed image.
if ! uci -q get network.lao >/dev/null; then
uci set network.lao=interface
uci set network.lao.proto='static'
uci set network.lao.device='br-lao'
uci set network.lao.ipaddr='10.67.1.1'
uci set network.lao.netmask='255.255.255.0'
fi
# --- DHCP: same shape as lan -------------------------------------------------
if ! uci -q get dhcp.lao >/dev/null; then
uci set dhcp.lao=dhcp
uci set dhcp.lao.interface='lao'
uci set dhcp.lao.start='100'
uci set dhcp.lao.limit='150'
uci set dhcp.lao.leasetime='12h'
fi
# --- Firewall: its OWN zone, which is the entire point -----------------------
# Same policies as the stock lan zone and its own forwarding to wan, so the
# network behaves like a second LAN. What it does NOT get here is a forwarding
# into shater_l3: seeding that for every zone is the job under test, done by
# /etc/uci-defaults/30_shater-core. If this script seeded it, the test would be
# testing itself.
if ! uci -q get firewall.lao >/dev/null; then
uci set firewall.lao=zone
uci set firewall.lao.name='lao'
uci set firewall.lao.input='ACCEPT'
uci set firewall.lao.output='ACCEPT'
uci set firewall.lao.forward='ACCEPT'
uci add_list firewall.lao.network='lao'
fi
if ! uci -q get firewall.lao_fwd >/dev/null; then
uci set firewall.lao_fwd=forwarding
uci set firewall.lao_fwd.src='lao'
uci set firewall.lao_fwd.dest='wan'
fi
uci commit network
uci commit dhcp
uci commit firewall
/etc/init.d/network reload
/etc/init.d/firewall reload
echo "lao seeded: br-lao 10.67.1.1/24, fw4 zone lao -> wan"