fix: accept host:port in validateResolverAddr; release v0.1.8

The root helper's resolver-address validator only accepted bare IPs,
so `resolvectl dns <bridge> 127.0.0.1:42069` — banger's own auto-wire
call to point systemd-resolved at the in-process DNS server — was
rejected before it ever reached resolvectl. The auto-wire is
best-effort and only logs a warning on failure, so .vm resolution
silently broke on the NSS path: dig @127.0.0.1 worked, curl <vm>.vm
didn't.

Validator now allows both bare IPs and IP:port (matching what
`resolvectl dns` itself accepts), with new test coverage for the
port'd form.

Existing installs need a one-time `sudo banger system restart` after
updating to v0.1.8 so the daemon re-runs the auto-wire with the
fixed validator.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Thales Maciel 2026-05-01 15:42:11 -03:00
parent 403f60dbbf
commit 9400bab6fd
No known key found for this signature in database
GPG key ID: 33112E6833C34679
3 changed files with 41 additions and 7 deletions

View file

@ -10,6 +10,30 @@ changed between versions.
## [Unreleased]
## [v0.1.8] - 2026-05-01
### Fixed
- `<vm>.vm` resolution from the host (NSS path: curl, ssh hostname,
etc.) now works on systemd-resolved hosts. The root helper's
`validateResolverAddr` was rejecting the `host:port` form
(`127.0.0.1:42069`) that banger constructs to point resolved at the
in-process DNS server, so the auto-wire silently failed at every
daemon startup. `dig @127.0.0.1` worked because that bypasses NSS;
any tool going through glibc's resolver chain didn't.
- Validator now accepts both bare IPs and `IP:port` (matching what
`resolvectl dns` itself accepts) with new test coverage for the
port'd form.
### Notes
- Existing v0.1.x installs that already booted with the broken
validator have stale per-link resolved state. After updating to
v0.1.8, run `sudo banger system restart` once to re-trigger the
auto-wire, or restart the host. systemd-resolved restarts also
wipe per-link state — banger restores it on its own daemon
startup but won't re-run for an already-running daemon.
## [v0.1.7] - 2026-05-01
### Added
@ -250,7 +274,8 @@ root filesystem and network, and exits on demand.
the swap rather than starting up against an incompatible store.
- Linux only. amd64 only. KVM required.
[Unreleased]: https://git.thaloco.com/thaloco/banger/compare/v0.1.7...HEAD
[Unreleased]: https://git.thaloco.com/thaloco/banger/compare/v0.1.8...HEAD
[v0.1.8]: https://git.thaloco.com/thaloco/banger/releases/tag/v0.1.8
[v0.1.7]: https://git.thaloco.com/thaloco/banger/releases/tag/v0.1.7
[v0.1.6]: https://git.thaloco.com/thaloco/banger/releases/tag/v0.1.6
[v0.1.5]: https://git.thaloco.com/thaloco/banger/releases/tag/v0.1.5

View file

@ -1296,18 +1296,24 @@ func validateIPv4(ip string) error {
return nil
}
// validateResolverAddr confirms s parses as an IP address (v4 or v6).
// resolvectl accepts either; reject anything that doesn't parse so a
// compromised daemon can't wedge resolved with garbage input.
// validateResolverAddr confirms s parses as an IP address, optionally
// with a ":port" suffix. resolvectl accepts both bare IPs and the
// "IP:port" form (used to point at a non-default DNS port — banger's
// in-process server binds to 127.0.0.1:42069). Reject anything that
// doesn't parse so a compromised daemon can't wedge resolved with
// garbage input.
func validateResolverAddr(s string) error {
s = strings.TrimSpace(s)
if s == "" {
return errors.New("resolver address is required")
}
if net.ParseIP(s) == nil {
return fmt.Errorf("invalid resolver address %q", s)
if net.ParseIP(s) != nil {
return nil
}
return nil
if host, _, err := net.SplitHostPort(s); err == nil && net.ParseIP(host) != nil {
return nil
}
return fmt.Errorf("invalid resolver address %q", s)
}
func validateTapName(tapName string) error {

View file

@ -566,8 +566,11 @@ func TestValidateResolverAddr(t *testing.T) {
}{
{name: "ipv4", arg: "192.168.1.1", ok: true},
{name: "ipv6", arg: "fe80::1", ok: true},
{name: "ipv4_with_port", arg: "127.0.0.1:42069", ok: true},
{name: "ipv6_with_port", arg: "[fe80::1]:42069", ok: true},
{name: "empty", arg: "", ok: false},
{name: "garbage", arg: "resolver.example", ok: false},
{name: "garbage_with_port", arg: "resolver.example:53", ok: false},
} {
tc := tc
t.Run(tc.name, func(t *testing.T) {