Found while closing #51. The same weakness #51 was about, on the hairpin rule rather than the port mapping rule.
The gap
docker_mapping_loopback_host_installs_loopback_masquerade in tests/natmap_docker.rs greps iptables-save for:
-s 127.0.0.0/8 -d 172.18.0.2
and asserts that output exists. It never checks the jump target. So changing the hairpin MASQUERADE to any other target leaves the test green, even though the rule no longer masquerades and the hairpin stops working.
This is the identical weakness #51 filed for the port mapping rule, and the same experiment exposes it: mutate build_masquerade_args in crates/natmap/src/iptables.rs:184 from MASQUERADE to ACCEPT and the test passes unchanged.
Why it matters
Per CONTEXT.md, hairpin is "the ability to reach a forwarded service through its external address from inside the LAN. Requires a MASQUERADE so the reply leaves via the LAN gateway." The MASQUERADE is the whole mechanism, and a test that greps for the rule's match fields without checking the target does not verify it. The port mapping counterpart of this test now does check the target, because #51 added a full-set assertion for that path.
What to build
Extend the loopback hairpin test to assert the jump target, not just the match. The negative counterpart (..._rejects_... or similar) should confirm the rule is absent when hairpin is off.
Consider whether asserting the whole hairpin rule set with one assert_eq! on a sorted Vec<&str>, as #51 does for the port mapping path, is better than widening the existing grep. It gives a readable failure that names the exact rule and the exact difference, which a contains cannot.
Acceptance criteria
Found while closing #51. The same weakness #51 was about, on the hairpin rule rather than the port mapping rule.
The gap
docker_mapping_loopback_host_installs_loopback_masqueradeintests/natmap_docker.rsgrepsiptables-savefor:and asserts that output exists. It never checks the jump target. So changing the hairpin MASQUERADE to any other target leaves the test green, even though the rule no longer masquerades and the hairpin stops working.
This is the identical weakness #51 filed for the port mapping rule, and the same experiment exposes it: mutate
build_masquerade_argsincrates/natmap/src/iptables.rs:184fromMASQUERADEtoACCEPTand the test passes unchanged.Why it matters
Per
CONTEXT.md, hairpin is "the ability to reach a forwarded service through its external address from inside the LAN. Requires a MASQUERADE so the reply leaves via the LAN gateway." The MASQUERADE is the whole mechanism, and a test that greps for the rule's match fields without checking the target does not verify it. The port mapping counterpart of this test now does check the target, because #51 added a full-set assertion for that path.What to build
Extend the loopback hairpin test to assert the jump target, not just the match. The negative counterpart (
..._rejects_...or similar) should confirm the rule is absent when hairpin is off.Consider whether asserting the whole hairpin rule set with one
assert_eq!on a sortedVec<&str>, as #51 does for the port mapping path, is better than widening the existing grep. It gives a readable failure that names the exact rule and the exact difference, which acontainscannot.Acceptance criteria
cargo test -p lab-ops --all-features --test natmap_dockerpassescrates/natmap/src/iptables.rsis unchanged; this is a test fix only