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"
|
"math/rand/v2"
|
||||||
"net"
|
"net"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -40,6 +41,7 @@ type Resolver interface {
|
|||||||
// to upstream DNS servers.
|
// to upstream DNS servers.
|
||||||
type Server struct {
|
type Server struct {
|
||||||
listenAddr netip.Addr
|
listenAddr netip.Addr
|
||||||
|
localSubnet netip.Prefix
|
||||||
resolver Resolver
|
resolver Resolver
|
||||||
upstreamServers []netip.AddrPort
|
upstreamServers []netip.AddrPort
|
||||||
|
|
||||||
@@ -53,7 +55,7 @@ type Server struct {
|
|||||||
// NewServer creates a new DNS server with the given configuration.
|
// 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
|
// 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.
|
// 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() {
|
if !listenAddr.IsValid() {
|
||||||
return nil, fmt.Errorf("invalid listen address: %s", listenAddr)
|
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{
|
return &Server{
|
||||||
listenAddr: listenAddr,
|
listenAddr: listenAddr,
|
||||||
|
localSubnet: localSubnet,
|
||||||
resolver: resolver,
|
resolver: resolver,
|
||||||
upstreamServers: upstreams,
|
upstreamServers: upstreams,
|
||||||
forwardSemaphore: make(chan struct{}, maxConcurrentForwards),
|
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.
|
// 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.
|
// 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 {
|
func (s *Server) handleAQuery(name string) []dns.RR {
|
||||||
serviceName := trimInternalDomain(name)
|
serviceName, mode := extractModeFromDomain(trimInternalDomain(name))
|
||||||
ips := s.resolver.Resolve(serviceName)
|
ips := s.resolver.Resolve(serviceName)
|
||||||
if len(ips) == 0 {
|
if len(ips) == 0 {
|
||||||
s.log.Debug("Failed to resolve service name.", "service", serviceName)
|
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)
|
s.log.Debug("Resolved service name.", "service", serviceName, "ips", ips)
|
||||||
|
|
||||||
if len(ips) > 1 {
|
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) {
|
rand.Shuffle(len(ips), func(i, j int) {
|
||||||
ips[i], ips[j] = ips[j], ips[i]
|
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.
|
// Create A records for each IP.
|
||||||
@@ -345,3 +366,13 @@ func trimInternalDomain(name string) string {
|
|||||||
|
|
||||||
return strings.TrimSuffix(name, "."+InternalDomain)
|
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)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("create embedded DNS server: %w", err)
|
return fmt.Errorf("create embedded DNS server: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package e2e
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@@ -224,4 +225,38 @@ func TestInternalDNS(t *testing.T) {
|
|||||||
|
|
||||||
assertNoDNSErrors(t, dnsOutput)
|
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
|
Name: worker.internal
|
||||||
Address: 10.210.1.4
|
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