Compare commits

..
Author SHA1 Message Date
abrown38andClaude Opus 5.5 507d75829f Release 0.2.0-rc.4+git70-83b97ad
release / release (push) Successful in 3m30s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 14:12:46 +01:00
abrown38andClaude Opus 5.5 83b97ad538 Refuse IPv4-mapped prefixes with an error that says so
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 14:12:46 +01:00
5 changed files with 38 additions and 3 deletions

No files matched your search

+3
View File
@@ -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:
+1 -1
View File
@@ -128,7 +128,7 @@ The web UI is on `http://127.0.0.1:8070/` (see `listen` in
## Status
Housecarl 0.2.0 is in testing (0.2.0-rc.4+git68-b4c0988): zones, address sets and
Housecarl 0.2.0 is in testing (0.2.0-rc.4+git70-83b97ad): zones, address sets and
services with ordered rules, IPv4 and IPv6, port forwards with hairpin NAT
and IPv6 pinholes, source NAT and redirects, built-ins, probation for
network changes (confirmed by the client that made them), the ruleset
+1 -1
View File
@@ -1 +1 @@
0.2.0-rc.4+git68-b4c0988
0.2.0-rc.4+git70-83b97ad
+5 -1
View File
@@ -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())
}
+28
View File
@@ -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)
}
}
}