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
abrown38andClaude Opus 5.5 715cb772e7 Release 0.2.0-rc.4+git68-b4c0988
release / release (push) Successful in 1m7s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:06:39 +01:00
abrown38andClaude Opus 5.5 b4c098890c hc forward show: hits, comment, and hairpin without a stray dash
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 18:06:39 +01:00
6 changed files with 50 additions and 4 deletions

No files matched your search

+5
View File
@@ -162,6 +162,11 @@ All notable changes to Housecarl are documented here. The format follows
### Fixed ### 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: - A history entry's ruleset diff showed the object it changed as moved:
stepping back put the old form at the end of its list, so updating stepping back put the old form at the end of its list, so updating
one port forward (turning hairpin off) also showed its forward and one port forward (turning hairpin off) also showed its forward and
+1 -1
View File
@@ -128,7 +128,7 @@ The web UI is on `http://127.0.0.1:8070/` (see `listen` in
## Status ## Status
Housecarl 0.2.0 is in testing (0.2.0-rc.4): 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 services with ordered rules, IPv4 and IPv6, port forwards with hairpin NAT
and IPv6 pinholes, source NAT and redirects, built-ins, probation for and IPv6 pinholes, source NAT and redirects, built-ins, probation for
network changes (confirmed by the client that made them), the ruleset network changes (confirmed by the client that made them), the ruleset
+1 -1
View File
@@ -1 +1 @@
0.2.0-rc.4 0.2.0-rc.4+git70-83b97ad
+10 -1
View File
@@ -605,7 +605,16 @@ func forwardCmd() *cobra.Command {
if f.ToPort != "" { if f.ToPort != "" {
fmt.Fprintf(w, "To port:\t%s\n", f.ToPort) fmt.Fprintf(w, "To port:\t%s\n", f.ToPort)
} }
fmt.Fprintf(w, "To (IPv6):\t%s\nFrom:\t%s\nHairpin:\t%s %s\n", orDash(f.To6), list(f.Sources), yesNo(f.Hairpin), list(f.HairpinSources)) hairpin := yesNo(f.Hairpin)
if f.Hairpin && len(f.HairpinSources) > 0 {
hairpin += ", from " + list(f.HairpinSources)
}
fmt.Fprintf(w, "To (IPv6):\t%s\nFrom:\t%s\nHairpin:\t%s\n", orDash(f.To6), list(f.Sources), hairpin)
if f.Comment != "" {
fmt.Fprintf(w, "Comment:\t%s\n", f.Comment)
}
cs := counters()
fmt.Fprintf(w, "Hits:\t%s\n", hits(cs, fmt.Sprintf("f%d", f.ID), fmt.Sprintf("f%d-6", f.ID)))
})) }))
var pf model.PortForward var pf model.PortForward
var to string var to string
+5 -1
View File
@@ -84,7 +84,11 @@ func ParseAddress(s string) (Address, error) {
if err != nil { if err != nil {
return Address{}, fmt.Errorf("%q is not an address or prefix", s) 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 { if p.Masked() != p {
return Address{}, fmt.Errorf("%q has host bits set; the prefix is %s", s, p.Masked()) 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)
}
}
}