feat(dns-server): run embedded internal DNS server on machine IP in uncloudd daemon process

This commit is contained in:
Pavel Sviderski
2025-04-30 17:24:52 +10:00
parent d6c06828b3
commit 8d16da596c
4 changed files with 105 additions and 70 deletions
+20
View File
@@ -24,6 +24,7 @@ import (
"github.com/psviderski/uncloud/internal/machine/caddyfile" "github.com/psviderski/uncloud/internal/machine/caddyfile"
"github.com/psviderski/uncloud/internal/machine/cluster" "github.com/psviderski/uncloud/internal/machine/cluster"
"github.com/psviderski/uncloud/internal/machine/corroservice" "github.com/psviderski/uncloud/internal/machine/corroservice"
"github.com/psviderski/uncloud/internal/machine/dns"
machinedocker "github.com/psviderski/uncloud/internal/machine/docker" machinedocker "github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
@@ -62,6 +63,8 @@ type Config struct {
// CaddyConfigPath specifies where the machine generates the Caddy reverse proxy configuration file for routing // CaddyConfigPath specifies where the machine generates the Caddy reverse proxy configuration file for routing
// external traffic to service containers across the internal network. Default is DataDir/caddy/caddy.json. // external traffic to service containers across the internal network. Default is DataDir/caddy/caddy.json.
CaddyConfigPath string CaddyConfigPath string
// DNSUpstreams specifies the upstream DNS servers for the embedded internal DNS server.
DNSUpstreams []netip.AddrPort
} }
// SetDefaults returns a new Config with default values set where not provided. // SetDefaults returns a new Config with default values set where not provided.
@@ -270,6 +273,15 @@ func (m *Machine) Initialised() bool {
return m.state.ID != "" return m.state.ID != ""
} }
// IP returns the machine IPv4 address in the cluster network which is the first address in the machine subnet.
func (m *Machine) IP() netip.Addr {
if !m.Initialised() {
return netip.Addr{}
}
return network.MachineIP(m.state.Network.Subnet)
}
func (m *Machine) Run(ctx context.Context) error { func (m *Machine) Run(ctx context.Context) error {
// Docker dependency is essential for the machine to function. Block until it's ready. // Docker dependency is essential for the machine to function. Block until it's ready.
if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil { if err := docker.WaitDaemonReady(ctx, m.config.DockerClient); err != nil {
@@ -373,6 +385,12 @@ func (m *Machine) Run(ctx context.Context) error {
return fmt.Errorf("create Caddyfile controller: %w", err) return fmt.Errorf("create Caddyfile controller: %w", err)
} }
dnsResolver := dns.NewClusterResolver(m.store)
dnsServer, err := dns.NewServer(m.IP(), dnsResolver, m.config.DNSUpstreams)
if err != nil {
return fmt.Errorf("create embedded DNS server: %w", err)
}
ctrl, err = newNetworkController( ctrl, err = newNetworkController(
m.state, m.state,
m.store, m.store,
@@ -380,6 +398,8 @@ func (m *Machine) Run(ctx context.Context) error {
m.config.CorrosionService, m.config.CorrosionService,
m.config.DockerClient, m.config.DockerClient,
caddyfileCtrl, caddyfileCtrl,
dnsServer,
dnsResolver,
) )
if err != nil { if err != nil {
return fmt.Errorf("initialise network controller: %w", err) return fmt.Errorf("initialise network controller: %w", err)
+39 -26
View File
@@ -4,22 +4,24 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/client"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
"log/slog" "log/slog"
"net" "net"
"net/netip" "net/netip"
"slices" "slices"
"strconv" "strconv"
"time" "time"
"github.com/cenkalti/backoff/v4"
"github.com/docker/docker/client"
"github.com/psviderski/uncloud/internal/machine/api/pb" "github.com/psviderski/uncloud/internal/machine/api/pb"
"github.com/psviderski/uncloud/internal/machine/caddyfile" "github.com/psviderski/uncloud/internal/machine/caddyfile"
"github.com/psviderski/uncloud/internal/machine/corroservice" "github.com/psviderski/uncloud/internal/machine/corroservice"
"github.com/psviderski/uncloud/internal/machine/dns"
"github.com/psviderski/uncloud/internal/machine/docker" "github.com/psviderski/uncloud/internal/machine/docker"
"github.com/psviderski/uncloud/internal/machine/network" "github.com/psviderski/uncloud/internal/machine/network"
"github.com/psviderski/uncloud/internal/machine/store" "github.com/psviderski/uncloud/internal/machine/store"
"golang.org/x/sync/errgroup"
"google.golang.org/grpc"
) )
const ( const (
@@ -38,8 +40,9 @@ type networkController struct {
dockerCli *client.Client dockerCli *client.Client
caddyfileCtrl *caddyfile.Controller caddyfileCtrl *caddyfile.Controller
// TODO: DNS server/resolver listening on the machine IP, e.g. 10.210.0.1:53. It can't listen on 127.0.X.X // dnsServer is the embedded internal DNS server for the cluster listening on the machine IP.
// like resolved does because it needs to be reachable from both the host and the containers. dnsServer *dns.Server
dnsResolver *dns.ClusterResolver
} }
func newNetworkController( func newNetworkController(
@@ -49,6 +52,8 @@ func newNetworkController(
corroService corroservice.Service, corroService corroservice.Service,
dockerCli *client.Client, dockerCli *client.Client,
caddyfileCtrl *caddyfile.Controller, caddyfileCtrl *caddyfile.Controller,
dnsServer *dns.Server,
dnsResolver *dns.ClusterResolver,
) ( ) (
*networkController, error, *networkController, error,
) { ) {
@@ -68,6 +73,8 @@ func newNetworkController(
corroService: corroService, corroService: corroService,
dockerCli: dockerCli, dockerCli: dockerCli,
caddyfileCtrl: caddyfileCtrl, caddyfileCtrl: caddyfileCtrl,
dnsServer: dnsServer,
dnsResolver: dnsResolver,
}, nil }, nil
} }
@@ -110,27 +117,38 @@ func (nc *networkController) Run(ctx context.Context) error {
}, },
) )
errGroup.Go(func() error {
slog.Info("Starting embedded DNS resolver.")
if err := nc.dnsResolver.Run(ctx); err != nil {
return fmt.Errorf("embedded DNS resolver failed: %w", err)
}
return nil
})
errGroup.Go(func() error {
slog.Info("Starting embedded DNS server.")
if err := nc.dnsServer.Run(ctx); err != nil {
return fmt.Errorf("embedded DNS server failed: %w", err)
}
return nil
})
// Setup Docker network and synchronise containers to the cluster store. // Setup Docker network and synchronise containers to the cluster store.
errGroup.Go( errGroup.Go(func() error {
func() error {
return nc.prepareAndWatchDocker(ctx) return nc.prepareAndWatchDocker(ctx)
}, })
)
// Handle machine changes in the cluster. Handling machine and endpoint changes should be done // Handle machine changes in the cluster. Handling machine and endpoint changes should be done
// in separate goroutines to avoid a deadlock when reconfiguring the network. // in separate goroutines to avoid a deadlock when reconfiguring the network.
errGroup.Go( errGroup.Go(func() error {
func() error {
if err := nc.handleMachineChanges(ctx); err != nil { if err := nc.handleMachineChanges(ctx); err != nil {
return fmt.Errorf("handle new machines: %w", err) return fmt.Errorf("handle new machines: %w", err)
} }
return nil return nil
}, })
)
// Watch for endpoint changes and update the machine state accordingly. // Watch for endpoint changes and update the machine state accordingly.
errGroup.Go( errGroup.Go(func() error {
func() error {
for { for {
select { select {
case e, ok := <-nc.endpointChanges: case e, ok := <-nc.endpointChanges:
@@ -158,17 +176,14 @@ func (nc *networkController) Run(ctx context.Context) error {
return nil return nil
} }
} }
}, })
)
errGroup.Go( errGroup.Go(func() error {
func() error {
if err := nc.wgnet.Run(ctx); err != nil { if err := nc.wgnet.Run(ctx); err != nil {
return fmt.Errorf("WireGuard network failed: %w", err) return fmt.Errorf("WireGuard network failed: %w", err)
} }
return nil return nil
}, })
)
errGroup.Go(func() error { errGroup.Go(func() error {
slog.Info("Starting Caddyfile controller.") slog.Info("Starting Caddyfile controller.")
@@ -180,16 +195,14 @@ func (nc *networkController) Run(ctx context.Context) error {
}) })
// Wait for the context to be done and stop the network API server. // Wait for the context to be done and stop the network API server.
errGroup.Go( errGroup.Go(func() error {
func() error {
<-ctx.Done() <-ctx.Done()
slog.Info("Stopping network API server.") slog.Info("Stopping network API server.")
// TODO: implement timeout for graceful shutdown. // TODO: implement timeout for graceful shutdown.
nc.server.GracefulStop() nc.server.GracefulStop()
slog.Info("Network API server stopped.") slog.Info("Network API server stopped.")
return nil return nil
}, })
)
return errGroup.Wait() return errGroup.Wait()
} }
+5 -4
View File
@@ -2,10 +2,11 @@ package network
import ( import (
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/secret"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
"net" "net"
"net/netip" "net/netip"
"github.com/psviderski/uncloud/internal/secret"
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
) )
type Config struct { type Config struct {
@@ -13,7 +14,7 @@ type Config struct {
// in the subnet. Other IP addresses are allocated to containers running on the machine. // in the subnet. Other IP addresses are allocated to containers running on the machine.
Subnet netip.Prefix Subnet netip.Prefix
// ManagementIP is the IPv6 address assigned to the machine within the WireGuard network. This address is used // ManagementIP is the IPv6 address assigned to the machine within the WireGuard network. This address is used
// for cluster management traffic, such as gRPC communication with the machine API server and Serf gossip. // for cluster management traffic, such as gRPC communication with the machine API server and Corrosion gossip.
ManagementIP netip.Addr ManagementIP netip.Addr
PrivateKey secret.Secret PrivateKey secret.Secret
PublicKey secret.Secret PublicKey secret.Secret
@@ -23,7 +24,7 @@ type Config struct {
type PeerConfig struct { type PeerConfig struct {
Subnet *netip.Prefix `json:",omitempty"` Subnet *netip.Prefix `json:",omitempty"`
// ManagementIP is the IPv6 address assigned to the peer within the WireGuard network. This address is used // ManagementIP is the IPv6 address assigned to the peer within the WireGuard network. This address is used
// for cluster management traffic, such as gRPC communication with the machine API server and Serf gossip. // for cluster management traffic, such as gRPC communication with the machine API server and Corrosion gossip.
ManagementIP netip.Addr ManagementIP netip.Addr
Endpoint *netip.AddrPort `json:",omitempty"` Endpoint *netip.AddrPort `json:",omitempty"`
AllEndpoints []netip.AddrPort `json:",omitempty"` AllEndpoints []netip.AddrPort `json:",omitempty"`
+2 -1
View File
@@ -2,9 +2,10 @@ package network
import ( import (
"fmt" "fmt"
"github.com/psviderski/uncloud/internal/secret"
"net" "net"
"net/netip" "net/netip"
"github.com/psviderski/uncloud/internal/secret"
) )
// MachineIP returns the IP address of the machine which is the first address in the subnet. // MachineIP returns the IP address of the machine which is the first address in the subnet.