navidrome/plugins/host_netguard.go
Deluan Quintão 101145742f
test(plugins): stub DNS in the host SSRF guard tests (#6208)
The dial-time SSRF guard runs on the resolved IP, so the tests that prove a
symbolic hostname cannot reach loopback used "localhost." — a trailing dot never
matches /etc/hosts, so Go queries real DNS. Machines whose resolver does not
answer "localhost." (a VPN DNS, for example) got "no such host" before the dial
guard ever ran, failing three specs.

Add tests.StubResolver, a net.Resolver backed by an in-memory DNS responder over
net.Pipe, and let the plugin dialers take a resolver so tests can inject it.
Name resolution in those specs no longer depends on the machine's DNS.
2026-09-23 18:42:11 -04:00

58 lines
1.5 KiB
Go

package plugins
import (
"fmt"
"net"
"slices"
"github.com/navidrome/navidrome/utils/netguard"
)
// dialResolver is nil in production (the system resolver); tests swap in a stub to avoid real DNS.
var dialResolver *net.Resolver
// checkPrivateDial runs at dial time on the resolved IP, so hostnames can't reach private addresses unless a
// literal IP/CIDR entry or a bare "*" (plugins targeting user-configured LAN services) allows it.
func checkPrivateDial(requiredHosts []string, address string) error {
if slices.Contains(requiredHosts, "*") {
return nil
}
host, _, err := net.SplitHostPort(address)
if err != nil {
return err
}
ip := net.ParseIP(host)
if ip == nil || !netguard.IsPrivateIP(ip) {
return nil
}
for _, entry := range requiredHosts {
if ipMatchesEntry(entry, ip) {
return nil
}
}
return fmt.Errorf("dial to private/loopback address %q blocked: requires an explicit IP or CIDR in requiredHosts", address)
}
func isHostInAllowlist(requiredHosts []string, hostname string) bool {
ip := net.ParseIP(hostname)
for _, pattern := range requiredHosts {
if matchHostPattern(pattern, hostname) {
return true
}
if ip != nil && ipMatchesEntry(pattern, ip) {
return true
}
}
return false
}
// ipMatchesEntry reports whether a requiredHosts entry is a literal IP or CIDR that covers ip.
func ipMatchesEntry(entry string, ip net.IP) bool {
if _, cidr, err := net.ParseCIDR(entry); err == nil {
return cidr.Contains(ip)
}
if entryIP := net.ParseIP(entry); entryIP != nil {
return entryIP.Equal(ip)
}
return false
}