cmd/ctrld: fixing incorrect reading base64 config

When reading base64 config, either via command line or via custom config
from Control D API, we do want new config entirely instead of mixing
with old config. So new viper instance should be re-recreated before
reading in new config.

That also helps simplifying self-check process, because the config is
now always set correctly, instead of watching change made by "ctrld run"
command.

However, log file and listener config need a special handling, because
they could be changed/unset from Control D API:

 - Log file can change dynamically each time ctrld runs, so init logging
   process need to take care of re-initializing if log setup changed.

 - For listener setup, users could leave ip and port empty, and ctrld
   will pick a random loopback 127.0.0.x:53. However, on Linux systems
   which use systemd-resolved, the stub listener won't forward queries
   from its address 127.0.0.53 to 127.0.0.x, so ctrld will use the
   default router interface address instead.
This commit is contained in:
Cuong Manh Le
2023-08-09 23:54:23 +07:00
committed by Cuong Manh Le
parent 9fe6af684f
commit 12148ec231
4 changed files with 84 additions and 34 deletions
+52 -30
View File
@@ -7,6 +7,7 @@ import (
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/netip" "net/netip"
"os" "os"
@@ -15,15 +16,14 @@ import (
"runtime" "runtime"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/cuonglm/osinfo" "github.com/cuonglm/osinfo"
"github.com/fsnotify/fsnotify"
"github.com/go-playground/validator/v10" "github.com/go-playground/validator/v10"
"github.com/kardianos/service" "github.com/kardianos/service"
"github.com/miekg/dns" "github.com/miekg/dns"
"github.com/pelletier/go-toml/v2" "github.com/pelletier/go-toml/v2"
"github.com/rs/zerolog"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/viper" "github.com/spf13/viper"
"tailscale.com/logtail/backoff" "tailscale.com/logtail/backoff"
@@ -150,6 +150,12 @@ func initCLI() {
mainLog.Fatal().Msgf("failed to unmarshal config: %v", err) mainLog.Fatal().Msgf("failed to unmarshal config: %v", err)
} }
processLogAndCacheFlags()
// Log config do not have thing to validate, so it's safe to init log here,
// so it's able to log information in processCDFlags.
initLogging()
mainLog.Info().Msgf("starting ctrld %s", curVersion()) mainLog.Info().Msgf("starting ctrld %s", curVersion())
oi := osinfo.New() oi := osinfo.New()
mainLog.Info().Msgf("os: %s", oi.String()) mainLog.Info().Msgf("os: %s", oi.String())
@@ -158,10 +164,6 @@ func initCLI() {
if !ctrldnet.Up() { if !ctrldnet.Up() {
mainLog.Fatal().Msg("network is not up yet") mainLog.Fatal().Msg("network is not up yet")
} }
processLogAndCacheFlags()
// Log config do not have thing to validate, so it's safe to init log here,
// so it's able to log information in processCDFlags.
initLogging()
// Processing --cd flag require connecting to ControlD API, which needs valid // Processing --cd flag require connecting to ControlD API, which needs valid
// time for validating server certificate. Some routers need NTP synchronization // time for validating server certificate. Some routers need NTP synchronization
@@ -170,7 +172,21 @@ func initCLI() {
mainLog.Fatal().Err(err).Msg("failed to perform router pre-run check") mainLog.Fatal().Err(err).Msg("failed to perform router pre-run check")
} }
oldLogPath := cfg.Service.LogPath
processCDFlags() processCDFlags()
if newLogPath := cfg.Service.LogPath; newLogPath != "" && oldLogPath != newLogPath {
// After processCDFlags, log config may change, so reset mainLog and re-init logging.
mainLog = zerolog.New(io.Discard)
// Copy logs written so far to new log file if possible.
if buf, err := os.ReadFile(oldLogPath); err == nil {
if err := os.WriteFile(newLogPath, buf, os.FileMode(0o600)); err != nil {
mainLog.Warn().Err(err).Msg("could not copy old log file")
}
}
initLoggingWithBackup(false)
}
if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil { if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil {
mainLog.Fatal().Msgf("invalid config: %v", err) mainLog.Fatal().Msgf("invalid config: %v", err)
} }
@@ -293,10 +309,7 @@ func initCLI() {
mainLog.Fatal().Msgf("failed to unmarshal config: %v", err) mainLog.Fatal().Msgf("failed to unmarshal config: %v", err)
} }
logPath := cfg.Service.LogPath
cfg.Service.LogPath = ""
initLogging() initLogging()
cfg.Service.LogPath = logPath
processCDFlags() processCDFlags()
@@ -648,6 +661,15 @@ func readBase64Config(configBase64 string) {
if err != nil { if err != nil {
mainLog.Fatal().Msgf("invalid base64 config: %v", err) mainLog.Fatal().Msgf("invalid base64 config: %v", err)
} }
// readBase64Config is called when:
//
// - "--base64_config" flag set.
// - Reading custom config when "--cd" flag set.
//
// So we need to re-create viper instance to discard old one.
v = viper.NewWithOptions(viper.KeyDelimiter("::"))
v.SetConfigType("toml")
if err := v.ReadConfig(bytes.NewReader(configStr)); err != nil { if err := v.ReadConfig(bytes.NewReader(configStr)); err != nil {
mainLog.Fatal().Msgf("failed to read base64 config: %v", err) mainLog.Fatal().Msgf("failed to read base64 config: %v", err)
} }
@@ -734,6 +756,9 @@ func processCDFlags() {
} }
logger.Info().Msg("generating ctrld config from Control-D configuration") logger.Info().Msg("generating ctrld config from Control-D configuration")
cfg = ctrld.Config{Listener: map[string]*ctrld.ListenerConfig{
"0": {Port: 53},
}}
if resolverConfig.Ctrld.CustomConfig != "" { if resolverConfig.Ctrld.CustomConfig != "" {
logger.Info().Msg("using defined custom config of Control-D resolver") logger.Info().Msg("using defined custom config of Control-D resolver")
readBase64Config(resolverConfig.Ctrld.CustomConfig) readBase64Config(resolverConfig.Ctrld.CustomConfig)
@@ -748,14 +773,27 @@ func processCDFlags() {
listener.Port = 53 listener.Port = 53
} }
} }
if setupRouter { switch {
case setupRouter:
if lc := cfg.Listener["0"]; lc != nil { if lc := cfg.Listener["0"]; lc != nil {
lc.IP = router.ListenIP() lc.IP = router.ListenIP()
lc.Port = router.ListenPort() lc.Port = router.ListenPort()
} }
case useSystemdResolved:
if lc := cfg.Listener["0"]; lc != nil {
// systemd-resolved does not allow forwarding DNS queries from 127.0.0.53 to loopback
// ip address, so trying to listen on default route interface address instead.
if netIface, _ := net.InterfaceByName(defaultIfaceName()); netIface != nil {
addrs, _ := netIface.Addrs()
for _, addr := range addrs {
if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil {
lc.IP = netIP.IP.To4().String()
}
}
}
}
} }
} else { } else {
cfg = ctrld.Config{}
cfg.Network = make(map[string]*ctrld.NetworkConfig) cfg.Network = make(map[string]*ctrld.NetworkConfig)
cfg.Network["0"] = &ctrld.NetworkConfig{ cfg.Network["0"] = &ctrld.NetworkConfig{
Name: "Network 0", Name: "Network 0",
@@ -785,9 +823,10 @@ func processCDFlags() {
lc.Port = router.ListenPort() lc.Port = router.ListenPort()
} }
cfg.Listener["0"] = lc cfg.Listener["0"] = lc
processLogAndCacheFlags()
} }
processLogAndCacheFlags()
if err := writeConfigFile(); err != nil { if err := writeConfigFile(); err != nil {
logger.Fatal().Err(err).Msg("failed to write config file") logger.Fatal().Err(err).Msg("failed to write config file")
} else { } else {
@@ -872,26 +911,9 @@ func selfCheckStatus(status service.Status, domain string) service.Status {
ctx := context.Background() ctx := context.Background()
maxAttempts := 20 maxAttempts := 20
mainLog.Debug().Msg("Performing self-check") mainLog.Debug().Msg("Performing self-check")
var (
lcChanged map[string]*ctrld.ListenerConfig
mu sync.Mutex
)
v.OnConfigChange(func(in fsnotify.Event) {
mu.Lock()
defer mu.Unlock()
if err := v.UnmarshalKey("listener", &lcChanged); err != nil {
mainLog.Error().Msgf("failed to unmarshal listener config: %v", err)
return
}
})
v.WatchConfig()
for i := 0; i < maxAttempts; i++ { for i := 0; i < maxAttempts; i++ {
lc := cfg.Listener["0"] lc := cfg.Listener["0"]
mu.Lock()
if lcChanged != nil {
lc = lcChanged["0"]
}
mu.Unlock()
m := new(dns.Msg) m := new(dns.Msg)
m.SetQuestion(domain+".", dns.TypeA) m.SetQuestion(domain+".", dns.TypeA)
m.RecursionDesired = true m.RecursionDesired = true
+23 -4
View File
@@ -78,7 +78,18 @@ func initConsoleLogging() {
} }
} }
// initLogging initializes global logging setup.
func initLogging() { func initLogging() {
initLoggingWithBackup(true)
}
// initLoggingWithBackup initializes log setup base on current config.
// If doBackup is true, backup old log file with ".1" suffix.
//
// This is only used in runCmd for special handling in case of logging config
// change in cd mode. Without special reason, the caller should use initLogging
// wrapper instead of calling this function directly.
func initLoggingWithBackup(doBackup bool) {
writers := []io.Writer{io.Discard} writers := []io.Writer{io.Discard}
if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" { if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" {
// Create parent directory if necessary. // Create parent directory if necessary.
@@ -86,11 +97,19 @@ func initLogging() {
mainLog.Error().Msgf("failed to create log path: %v", err) mainLog.Error().Msgf("failed to create log path: %v", err)
os.Exit(1) os.Exit(1)
} }
// Backup old log file with .1 suffix.
if err := os.Rename(logFilePath, logFilePath+".1"); err != nil && !os.IsNotExist(err) { // Default open log file in append mode.
mainLog.Error().Msgf("could not backup old log file: %v", err) flags := os.O_CREATE | os.O_RDWR | os.O_APPEND
if doBackup {
// Backup old log file with .1 suffix.
if err := os.Rename(logFilePath, logFilePath+".1"); err != nil && !os.IsNotExist(err) {
mainLog.Error().Msgf("could not backup old log file: %v", err)
} else {
// Backup was created, set flags for truncating old log file.
flags = os.O_CREATE | os.O_RDWR
}
} }
logFile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_RDWR, os.FileMode(0o600)) logFile, err := os.OpenFile(logFilePath, flags, os.FileMode(0o600))
if err != nil { if err != nil {
mainLog.Error().Msgf("failed to create log file: %v", err) mainLog.Error().Msgf("failed to create log file: %v", err)
os.Exit(1) os.Exit(1)
+2
View File
@@ -32,6 +32,8 @@ var svcConfig = &service.Config{
Option: service.KeyValue{}, Option: service.KeyValue{},
} }
var useSystemdResolved = false
type prog struct { type prog struct {
mu sync.Mutex mu sync.Mutex
waitCh chan struct{} waitCh chan struct{}
+7
View File
@@ -3,9 +3,16 @@ package main
import ( import (
"github.com/kardianos/service" "github.com/kardianos/service"
"github.com/Control-D-Inc/ctrld/internal/dns"
"github.com/Control-D-Inc/ctrld/internal/router" "github.com/Control-D-Inc/ctrld/internal/router"
) )
func init() {
if r, err := dns.NewOSConfigurator(logf, "lo"); err == nil {
useSystemdResolved = r.Mode() == "systemd-resolved"
}
}
func (p *prog) preRun() { func (p *prog) preRun() {
if !service.Interactive() { if !service.Interactive() {
p.setDNS() p.setDNS()