From 83b97ad53843c12e6353a7a7693e96ee43bad8bc Mon Sep 17 00:00:00 2001 From: abrown38 Date: Sat, 24 Oct 2026 14:12:46 +0100 Subject: [PATCH] Refuse IPv4-mapped prefixes with an error that says so Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +++ internal/model/addr.go | 6 +++++- internal/model/addr_test.go | 28 ++++++++++++++++++++++++++++ 3 files changed, 36 insertions(+), 1 deletion(-) create mode 100644 internal/model/addr_test.go diff --git a/CHANGELOG.md b/CHANGELOG.md index 547ec0a..55bf90d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -162,6 +162,9 @@ All notable changes to Housecarl are documented here. The format follows ### Fixed +- An IPv4-mapped prefix (`::ffff:0:0/96`) is refused with an error that + says so; it was refused as "has host bits set; the prefix is invalid + Prefix". - `hc forward show` shows the forward's hits and comment, and its hairpin as `no`, `yes` or `yes, from …` (it printed `no -`). - A history entry's ruleset diff showed the object it changed as moved: diff --git a/internal/model/addr.go b/internal/model/addr.go index 57f0c90..4205da7 100644 --- a/internal/model/addr.go +++ b/internal/model/addr.go @@ -84,7 +84,11 @@ func ParseAddress(s string) (Address, error) { if err != nil { return Address{}, fmt.Errorf("%q is not an address or prefix", s) } - p = netip.PrefixFrom(p.Addr().Unmap(), p.Bits()) + if p.Addr().Is4In6() { + // Unmapping would leave an IPv4 address with IPv6 bits + // (::ffff:0:0/96 as 0.0.0.0/96). + return Address{}, fmt.Errorf("%q is an IPv4-mapped prefix, which can't be used: write IPv4 prefixes as IPv4 (10.0.0.0/8)", s) + } if p.Masked() != p { return Address{}, fmt.Errorf("%q has host bits set; the prefix is %s", s, p.Masked()) } diff --git a/internal/model/addr_test.go b/internal/model/addr_test.go new file mode 100644 index 0000000..1feb84e --- /dev/null +++ b/internal/model/addr_test.go @@ -0,0 +1,28 @@ +package model + +import ( + "strings" + "testing" +) + +func TestParseAddress(t *testing.T) { + for _, c := range []struct{ in, want, err string }{ + {in: "10.0.0.0/8", want: "10.0.0.0/8"}, + {in: "2001:db8::/32", want: "2001:db8::/32"}, + {in: "::ffff:10.0.0.1", want: "10.0.0.1"}, + {in: "10.0.0.1/8", err: "host bits"}, + {in: "::ffff:0:0/96", err: "IPv4-mapped"}, + {in: "::ffff:10.0.0.0/104", err: "IPv4-mapped"}, + } { + a, err := ParseAddress(c.in) + if c.err != "" { + if err == nil || !strings.Contains(err.Error(), c.err) { + t.Errorf("%s: error %v, want one saying %q", c.in, err, c.err) + } + continue + } + if err != nil || a.String() != c.want { + t.Errorf("%s: %q, %v; want %q", c.in, a.String(), err, c.want) + } + } +}