From cad71997aa03bcf42ea2b60012caaa464a98c48e Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 21 Feb 2023 23:52:50 +0700 Subject: [PATCH] cmd/ctrld: allocate new ip instead of port So the alternative listener address can still be used as system resolver. --- cmd/ctrld/cli.go | 33 +++++++++++++++++++++------------ cmd/ctrld/os_linux.go | 4 ++-- cmd/ctrld/prog.go | 43 ++++++++++++++++--------------------------- config.go | 2 +- go.mod | 2 +- 5 files changed, 41 insertions(+), 43 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index e3ca198..f419813 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -17,6 +17,8 @@ import ( "strings" "time" + "github.com/fsnotify/fsnotify" + "github.com/go-playground/validator/v10" "github.com/kardianos/service" "github.com/miekg/dns" @@ -87,7 +89,6 @@ func initCLI() { if daemon && runtime.GOOS == "windows" { log.Fatal("Cannot run in daemon mode. Please install a Windows service.") } - noConfigStart := isNoConfigStart(cmd) writeDefaultConfig := !noConfigStart && configBase64 == "" configs := []struct { @@ -196,18 +197,23 @@ func initCLI() { } setDependencies(sc) sc.Arguments = append([]string{"run"}, osArgs...) + + // No config path, generating config in HOME directory. + noConfigStart := isNoConfigStart(cmd) + writeDefaultConfig := !noConfigStart && configBase64 == "" + if configPath != "" { + v.SetConfigFile(configPath) + } if dir, err := os.UserHomeDir(); err == nil { setWorkingDirectory(sc, dir) - // No config path, generating config in HOME directory. - noConfigStart := isNoConfigStart(cmd) - writeDefaultConfig := !noConfigStart && configBase64 == "" if configPath == "" && writeDefaultConfig { defaultConfigFile = filepath.Join(dir, defaultConfigFile) - readConfigFile(writeDefaultConfig && cdUID == "") + v.SetConfigFile(defaultConfigFile) } sc.Arguments = append(sc.Arguments, "--homedir="+dir) } + readConfigFile(writeDefaultConfig && cdUID == "") if err := v.Unmarshal(&cfg); err != nil { log.Fatalf("failed to unmarshal config: %v", err) } @@ -480,7 +486,7 @@ func writeConfigFile() error { } } enc := toml.NewEncoder(f).SetIndentTables(true) - if err := enc.Encode(v.AllSettings()); err != nil { + if err := enc.Encode(&cfg); err != nil { return err } if err := f.Close(); err != nil { @@ -495,6 +501,13 @@ func readConfigFile(writeDefaultConfig bool) bool { if err == nil { fmt.Println("loading config file from:", v.ConfigFileUsed()) defaultConfigFile = v.ConfigFileUsed() + v.OnConfigChange(func(in fsnotify.Event) { + if err := v.UnmarshalKey("listener", &cfg.Listener); err != nil { + log.Printf("failed to unmarshal listener config: %v", err) + return + } + }) + v.WatchConfig() return true } @@ -630,10 +643,6 @@ func processCDFlags() { }, } - v = viper.NewWithOptions(viper.KeyDelimiter("::")) - v.Set("network", cfg.Network) - v.Set("upstream", cfg.Upstream) - v.Set("listener", cfg.Listener) processLogAndCacheFlags() if err := writeConfigFile(); err != nil { logger.Fatal().Err(err).Msg("failed to write config file") @@ -705,14 +714,14 @@ func defaultIfaceName() string { func selfCheckStatus(status service.Status) service.Status { c := new(dns.Client) - lc := cfg.Listener["0"] bo := backoff.NewBackoff("self-check", logf, 10*time.Second) bo.LogLongerThan = 500 * time.Millisecond ctx := context.Background() err := errors.New("query failed") - maxAttempts := 10 + maxAttempts := 20 mainLog.Debug().Msg("Performing self-check") for i := 0; i < maxAttempts; i++ { + lc := cfg.Listener["0"] m := new(dns.Msg) m.SetQuestion(selfCheckFQDN+".", dns.TypeA) m.RecursionDesired = true diff --git a/cmd/ctrld/os_linux.go b/cmd/ctrld/os_linux.go index a29c7f4..839d99d 100644 --- a/cmd/ctrld/os_linux.go +++ b/cmd/ctrld/os_linux.go @@ -27,8 +27,8 @@ import ( // sudo ip a add 127.0.0.2/24 dev lo func allocateIP(ip string) error { cmd := exec.Command("ip", "a", "add", ip+"/24", "dev", "lo") - if err := cmd.Run(); err != nil { - mainLog.Error().Err(err).Msg("allocateIP failed") + if out, err := cmd.CombinedOutput(); err != nil { + mainLog.Error().Err(err).Msgf("allocateIP failed: %s", string(out)) return err } return nil diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index e3d6239..edb43bf 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -2,6 +2,8 @@ package main import ( "errors" + "fmt" + "math/rand" "net" "os" "strconv" @@ -120,7 +122,7 @@ func (p *prog) run() { addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)) mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, addr) err := p.serveUDP(listenerNum) - if err != nil && !defaultConfigWritten { + if err != nil && !defaultConfigWritten && cdUID == "" { mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) return } @@ -128,39 +130,21 @@ func (p *prog) run() { return } - if opErr, ok := err.(*net.OpError); ok { + if opErr, ok := err.(*net.OpError); ok && listenerNum == "0" { if sErr, ok := opErr.Err.(*os.SyscallError); ok && errors.Is(opErr.Err, syscall.EADDRINUSE) || errors.Is(sErr.Err, errWindowsAddrInUse) { mainLog.Warn().Msgf("Address %s already in used, pick a random one", addr) - pc, err := net.ListenPacket("udp", net.JoinHostPort(listenerConfig.IP, "0")) - if err != nil { - mainLog.Fatal().Err(err).Msg("failed to listen packet") - return - } - _, portStr, _ := net.SplitHostPort(pc.LocalAddr().String()) - port, err := strconv.Atoi(portStr) - if err != nil { - mainLog.Fatal().Err(err).Msg("malformed port") - return - } - listenerConfig.Port = port - v.Set("listener", map[string]*ctrld.ListenerConfig{ - "0": { - IP: "127.0.0.1", - Port: port, - }, - }) + ip := randomLocalIP() + listenerConfig.IP = ip + port := listenerConfig.Port + cfg.Upstream = map[string]*ctrld.UpstreamConfig{"0": cfg.Upstream["0"]} if err := writeConfigFile(); err != nil { mainLog.Fatal().Err(err).Msg("failed to write config file") } else { mainLog.Info().Msg("writing config file to: " + defaultConfigFile) } - mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, pc.LocalAddr()) - // There can be a race between closing the listener and start our own UDP server, but it's - // rare, and we only do this once, so let conservative here. - if err := pc.Close(); err != nil { - mainLog.Fatal().Err(err).Msg("failed to close packet conn") - return - } + p.cfg.Service.AllocateIP = true + p.preRun() + mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, net.JoinHostPort(ip, strconv.Itoa(port))) if err := p.serveUDP(listenerNum); err != nil { mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) return @@ -254,3 +238,8 @@ func (p *prog) resetDNS() { } logger.Debug().Msg("Restoring DNS successfully") } + +func randomLocalIP() string { + n := rand.Intn(254-2) + 2 + return fmt.Sprintf("127.0.0.%d", n) +} diff --git a/config.go b/config.go index 1364b4e..f8d7736 100644 --- a/config.go +++ b/config.go @@ -66,9 +66,9 @@ func InitConfig(v *viper.Viper, name string) { // Config represents ctrld supported configuration. type Config struct { Service ServiceConfig `mapstructure:"service" toml:"service,omitempty"` + Listener map[string]*ListenerConfig `mapstructure:"listener" toml:"listener" validate:"min=1,dive"` Network map[string]*NetworkConfig `mapstructure:"network" toml:"network" validate:"min=1,dive"` Upstream map[string]*UpstreamConfig `mapstructure:"upstream" toml:"upstream" validate:"min=1,dive"` - Listener map[string]*ListenerConfig `mapstructure:"listener" toml:"listener" validate:"min=1,dive"` } // ServiceConfig specifies the general ctrld config. diff --git a/go.mod b/go.mod index af47c82..99ef4e0 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534 github.com/frankban/quicktest v1.14.3 + github.com/fsnotify/fsnotify v1.6.0 github.com/go-playground/validator/v10 v10.11.1 github.com/godbus/dbus/v5 v5.0.6 github.com/hashicorp/golang-lru/v2 v2.0.1 @@ -26,7 +27,6 @@ require ( require ( github.com/alexbrainman/sspi v0.0.0-20210105120005-909beea2cc74 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect github.com/go-playground/locales v0.14.0 // indirect github.com/go-playground/universal-translator v0.18.0 // indirect github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect