mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-26 19:13:34 +00:00
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:
@@ -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, ""
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package e2e
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -224,4 +225,38 @@ func TestInternalDNS(t *testing.T) {
|
||||
|
||||
assertNoDNSErrors(t, dnsOutput)
|
||||
})
|
||||
|
||||
t.Run("nearest mode prioritizes local subnet IPs", func(t *testing.T) {
|
||||
// Test the "nearest" mode which should sort local subnet IPs first
|
||||
dnsOutput := runDNSQuery(t, "dns-test-nearest", "nearest."+serviceName+".internal", "/tmp/dns_result_nearest.txt")
|
||||
t.Logf("Nearest mode DNS query output:\n%s", dnsOutput)
|
||||
|
||||
// Find which machine ran the query
|
||||
querySvc, err := cli.InspectService(ctx, "dns-test-nearest")
|
||||
require.NoError(t, err)
|
||||
require.NotEmpty(t, querySvc.Containers, "Query service should have a container")
|
||||
queryMachineID := querySvc.Containers[0].MachineID
|
||||
|
||||
// Find the local container IP (on the same machine as the query)
|
||||
var localIP string
|
||||
for _, ctr := range svc.Containers {
|
||||
if ctr.MachineID == queryMachineID {
|
||||
localIP = ctr.Container.UncloudNetworkIP().String()
|
||||
break
|
||||
}
|
||||
}
|
||||
require.NotEmpty(t, localIP, "Should find local container IP on query machine %s", queryMachineID)
|
||||
|
||||
// Extract the first IP address from the DNS output using regex
|
||||
// Pattern matches: "Name: nearest.test-dns-service.internal" followed by "Address: X.X.X.X"
|
||||
re := regexp.MustCompile(`(?m)Name:\s+[\w\.\-]+\s+Address:\s+([\d\.]+)`)
|
||||
matches := re.FindStringSubmatch(dnsOutput)
|
||||
require.Len(t, matches, 2, "Should find Name's Address in DNS output")
|
||||
firstIP := matches[1]
|
||||
assert.Equal(t, localIP, firstIP,
|
||||
"Nearest mode should return local subnet IP first (query machine: %s, local IP: %s, first DNS result: %s)",
|
||||
queryMachineID, localIP, firstIP)
|
||||
|
||||
assertNoDNSErrors(t, dnsOutput)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -59,3 +59,75 @@ Address: 10.210.1.3
|
||||
Name: worker.internal
|
||||
Address: 10.210.1.4
|
||||
```
|
||||
|
||||
## IP Ordering Mode
|
||||
|
||||
Additionally, the IP ordering preference can be specified with a `rr` (round-robin) or `nearest` subdomain prefix.
|
||||
|
||||
### `rr` (round-robin) *current default*
|
||||
Randomly shuffled order on each lookup.
|
||||
|
||||
```
|
||||
$ nslookup rr.worker.internal
|
||||
Server: 127.0.0.11
|
||||
Address: 127.0.0.11#53
|
||||
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.0.3
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.0.4
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.1.3
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.1.4
|
||||
|
||||
$ nslookup rr.worker.internal
|
||||
Server: 127.0.0.11
|
||||
Address: 127.0.0.11#53
|
||||
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.0.4
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.1.3
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.1.4
|
||||
Name: rr.worker.internal
|
||||
Address: 10.210.0.3
|
||||
```
|
||||
|
||||
## Nearest scope
|
||||
Returns machine-local instances first.
|
||||
|
||||
`machine-a`:
|
||||
```
|
||||
$ nslookup nearest.worker.internal
|
||||
Server: 127.0.0.11
|
||||
Address: 127.0.0.11#53
|
||||
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.0.3
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.0.4
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.1.3
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.1.4
|
||||
```
|
||||
|
||||
`machine-b`:
|
||||
```
|
||||
$ nslookup nearest.worker.internal
|
||||
Server: 127.0.0.11
|
||||
Address: 127.0.0.11#53
|
||||
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.1.3
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.1.4
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.0.3
|
||||
Name: nearest.worker.internal
|
||||
Address: 10.210.0.4
|
||||
```
|
||||
|
||||
The prefixes can be used with service ID and machine-scoped service names, as well (e.g. `nearest.3ecb3a8bbec5fd3f46efb056a934714a.internal` or `rr.0903f0ee483aa97d559eeeaac5e22283.m.worker.internal`).
|
||||
|
||||
Reference in New Issue
Block a user