mirror of
https://github.com/psviderski/uncloud.git
synced 2026-08-28 03:53:33 +00:00
feat(dns-server): run embedded internal DNS server on machine IP in uncloudd daemon process
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
+78
-65
@@ -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,65 +117,73 @@ 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:
|
if !ok {
|
||||||
if !ok {
|
// The channel was closed, stop watching for changes.
|
||||||
// The channel was closed, stop watching for changes.
|
nc.endpointChanges = nil
|
||||||
nc.endpointChanges = nil
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
nc.state.mu.Lock()
|
|
||||||
for i := range nc.state.Network.Peers {
|
|
||||||
if nc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
|
|
||||||
nc.state.Network.Peers[i].Endpoint = &e.Endpoint
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := nc.state.Save(); err != nil {
|
|
||||||
slog.Error("Failed to save machine state.", "err", err)
|
|
||||||
}
|
|
||||||
nc.state.mu.Unlock()
|
|
||||||
|
|
||||||
slog.Debug("Preserved endpoint change in the machine state.",
|
|
||||||
"public_key", e.PublicKey, "endpoint", e.Endpoint)
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
errGroup.Go(
|
nc.state.mu.Lock()
|
||||||
func() error {
|
for i := range nc.state.Network.Peers {
|
||||||
if err := nc.wgnet.Run(ctx); err != nil {
|
if nc.state.Network.Peers[i].PublicKey.Equal(e.PublicKey) {
|
||||||
return fmt.Errorf("WireGuard network failed: %w", err)
|
nc.state.Network.Peers[i].Endpoint = &e.Endpoint
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := nc.state.Save(); err != nil {
|
||||||
|
slog.Error("Failed to save machine state.", "err", err)
|
||||||
|
}
|
||||||
|
nc.state.mu.Unlock()
|
||||||
|
|
||||||
|
slog.Debug("Preserved endpoint change in the machine state.",
|
||||||
|
"public_key", e.PublicKey, "endpoint", e.Endpoint)
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
return nil
|
}
|
||||||
},
|
})
|
||||||
)
|
|
||||||
|
errGroup.Go(func() error {
|
||||||
|
if err := nc.wgnet.Run(ctx); err != nil {
|
||||||
|
return fmt.Errorf("WireGuard network failed: %w", err)
|
||||||
|
}
|
||||||
|
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()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,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.
|
||||||
|
|||||||
Reference in New Issue
Block a user