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
124 lines
4.7 KiB
Bash
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"
|