navidrome/tests/dns_stub.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

54 lines
1.5 KiB
Go

package tests
import (
"context"
"encoding/binary"
"io"
"net"
"net/netip"
"golang.org/x/net/dns/dnsmessage"
)
// StubResolver returns a net.Resolver that answers A queries from records (absolute name to IPv4,
// e.g. "localhost." to "127.0.0.1") instead of the system DNS. Other names do not resolve.
func StubResolver(records map[string]string) *net.Resolver {
return &net.Resolver{
PreferGo: true,
Dial: func(context.Context, string, string) (net.Conn, error) {
client, server := net.Pipe()
go serveStubDNS(server, records)
return client, nil
},
}
}
// net.Pipe is not a PacketConn, so the resolver sends one query framed with a 2-byte length prefix.
func serveStubDNS(conn net.Conn, records map[string]string) {
defer conn.Close()
var size [2]byte
if _, err := io.ReadFull(conn, size[:]); err != nil {
return
}
buf := make([]byte, binary.BigEndian.Uint16(size[:]))
if _, err := io.ReadFull(conn, buf); err != nil {
return
}
var msg dnsmessage.Message
if err := msg.Unpack(buf); err != nil || len(msg.Questions) != 1 {
return
}
msg.Response = true
q := msg.Questions[0]
if ip, err := netip.ParseAddr(records[q.Name.String()]); err == nil && q.Type == dnsmessage.TypeA {
msg.Answers = []dnsmessage.Resource{{
Header: dnsmessage.ResourceHeader{Name: q.Name, Type: q.Type, Class: q.Class},
Body: &dnsmessage.AResource{A: ip.As4()},
}}
}
resp, err := msg.Pack()
if err != nil {
return
}
_, _ = conn.Write(append(binary.BigEndian.AppendUint16(nil, uint16(len(resp))), resp...))
}