feat: add "nearest.<service>.internal" mode to internal DNS (#159)

* feat: Adding a "nearest.service.local" mode to internal DNS

* Pass listenAddr to internal DNS server (rather than calling into network.MachineIP)

Also adds an exact round-robin via "rr.{service}" mode. Not sure we want that over random or not, though.

* Remove the resolved count and rotating round-robin. "rr.name" mode is now the same as default with random shuffle.

* Add test on nearest internal DNS lookup

* Add docs on internal DNS order modes
This commit is contained in:
Justin Bradford
2025-10-29 14:25:59 +10:00
committed by GitHub
parent 6af7a9861d
commit ba6290d616
4 changed files with 147 additions and 4 deletions
+34 -3
View File
@@ -8,6 +8,7 @@ import (
"math/rand/v2"
"net"
"net/netip"
"slices"
"strconv"
"strings"
"sync"
@@ -40,6 +41,7 @@ type Resolver interface {
// to upstream DNS servers.
type Server struct {
listenAddr netip.Addr
localSubnet netip.Prefix
resolver Resolver
upstreamServers []netip.AddrPort
@@ -53,7 +55,7 @@ type Server struct {
// NewServer creates a new DNS server with the given configuration.
// If upstreams is nil, nameservers from /etc/resolv.conf will be used. An empty upstreams list means to only resolve
// internal DNS queries and not forward any external queries.
func NewServer(listenAddr netip.Addr, resolver Resolver, upstreams []netip.AddrPort) (*Server, error) {
func NewServer(listenAddr netip.Addr, localSubnet netip.Prefix, resolver Resolver, upstreams []netip.AddrPort) (*Server, error) {
if !listenAddr.IsValid() {
return nil, fmt.Errorf("invalid listen address: %s", listenAddr)
}
@@ -87,6 +89,7 @@ func NewServer(listenAddr netip.Addr, resolver Resolver, upstreams []netip.AddrP
return &Server{
listenAddr: listenAddr,
localSubnet: localSubnet,
resolver: resolver,
upstreamServers: upstreams,
forwardSemaphore: make(chan struct{}, maxConcurrentForwards),
@@ -287,7 +290,7 @@ func (s *Server) forwardRequest(req *dns.Msg, proto string) (*dns.Msg, error) {
// handleAQuery processes an A query for the internal domain and returns A records for the requested name.
// The internal domain suffix is already stripped from the name. An empty list is returned if no records are found.
func (s *Server) handleAQuery(name string) []dns.RR {
serviceName := trimInternalDomain(name)
serviceName, mode := extractModeFromDomain(trimInternalDomain(name))
ips := s.resolver.Resolve(serviceName)
if len(ips) == 0 {
s.log.Debug("Failed to resolve service name.", "service", serviceName)
@@ -296,10 +299,28 @@ func (s *Server) handleAQuery(name string) []dns.RR {
s.log.Debug("Resolved service name.", "service", serviceName, "ips", ips)
if len(ips) > 1 {
// TODO: sort by proximity to the requesting container/machine. For now, just shuffle the IPs.
// Shuffle the IPs to approximate round-robin.
// We want to do this as a baseline for "nearest" mode, as well.
rand.Shuffle(len(ips), func(i, j int) {
ips[i], ips[j] = ips[j], ips[i]
})
// Default (mode == "") currently behaves the same as round-robin,
// and nothing additional to do for round-robin (mode == "rr").
if mode == "nearest" {
// Sort IPs on local subnet to the top.
slices.SortFunc(ips, func(a, b netip.Addr) int {
aIsLocal := s.localSubnet.Contains(a)
bIsLocal := s.localSubnet.Contains(b)
if aIsLocal && !bIsLocal {
return -1
} else if bIsLocal && !aIsLocal {
return 1
}
return 0
})
}
}
// Create A records for each IP.
@@ -345,3 +366,13 @@ func trimInternalDomain(name string) string {
return strings.TrimSuffix(name, "."+InternalDomain)
}
func extractModeFromDomain(name string) (string, string) {
modes := []string{"nearest", "rr"}
for _, mode := range modes {
if cut, found := strings.CutPrefix(name, mode+"."); found {
return cut, mode
}
}
return name, ""
}
+6 -1
View File
@@ -430,7 +430,12 @@ func (m *Machine) Run(ctx context.Context) error {
}
dnsResolver := dns.NewClusterResolver(m.store)
dnsServer, err := dns.NewServer(m.IP(), dnsResolver, m.config.DNSUpstreams)
dnsServer, err := dns.NewServer(
m.IP(),
m.state.Network.Subnet,
dnsResolver,
m.config.DNSUpstreams,
)
if err != nil {
return fmt.Errorf("create embedded DNS server: %w", err)
}