Merge pull request #155 from Control-D-Inc/release-branch-v1.3.7

Release branch v1.3.7
This commit is contained in:
Cuong Manh Le
2024-05-31 15:04:47 +07:00
committed by GitHub
18 changed files with 433 additions and 261 deletions
+2
View File
@@ -4,6 +4,8 @@
[![Go Reference](https://pkg.go.dev/badge/github.com/Control-D-Inc/ctrld.svg)](https://pkg.go.dev/github.com/Control-D-Inc/ctrld) [![Go Reference](https://pkg.go.dev/badge/github.com/Control-D-Inc/ctrld.svg)](https://pkg.go.dev/github.com/Control-D-Inc/ctrld)
[![Go Report Card](https://goreportcard.com/badge/github.com/Control-D-Inc/ctrld)](https://goreportcard.com/report/github.com/Control-D-Inc/ctrld) [![Go Report Card](https://goreportcard.com/badge/github.com/Control-D-Inc/ctrld)](https://goreportcard.com/report/github.com/Control-D-Inc/ctrld)
![ctrld spash image](/docs/ctrldsplash.png)
A highly configurable DNS forwarding proxy with support for: A highly configurable DNS forwarding proxy with support for:
- Multiple listeners for incoming queries - Multiple listeners for incoming queries
- Multiple upstreams with fallbacks - Multiple upstreams with fallbacks
+209 -172
View File
@@ -18,15 +18,14 @@ import (
"path/filepath" "path/filepath"
"reflect" "reflect"
"runtime" "runtime"
"runtime/debug"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/Masterminds/semver" "github.com/Masterminds/semver"
"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"
@@ -86,7 +85,7 @@ var rootCmd = &cobra.Command{
Use: "ctrld", Use: "ctrld",
Short: strings.TrimLeft(rootShortDesc, "\n"), Short: strings.TrimLeft(rootShortDesc, "\n"),
Version: curVersion(), Version: curVersion(),
PreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging() initConsoleLogging()
}, },
} }
@@ -127,9 +126,6 @@ func initCLI() {
Use: "run", Use: "run",
Short: "Run the DNS proxy server", Short: "Run the DNS proxy server",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
RunCobraCommand(cmd) RunCobraCommand(cmd)
}, },
@@ -158,7 +154,6 @@ func initCLI() {
startCmd := &cobra.Command{ startCmd := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Use: "start", Use: "start",
@@ -187,11 +182,11 @@ func initCLI() {
return return
} }
status, err := s.Status() status, _ := s.Status()
isCtrldInstalled := !errors.Is(err, service.ErrNotInstalled) isCtrldRunning := status == service.StatusRunning
// If pin code was set, do not allow running start command. // If pin code was set, do not allow running start command.
if status == service.StatusRunning { if isCtrldRunning {
if err := checkDeactivationPin(s, nil); isCheckDeactivationPinErr(err) { if err := checkDeactivationPin(s, nil); isCheckDeactivationPinErr(err) {
os.Exit(deactivationPinInvalidExitCode) os.Exit(deactivationPinInvalidExitCode)
} }
@@ -255,13 +250,14 @@ func initCLI() {
// A buffer channel to gather log output from runCmd and report // A buffer channel to gather log output from runCmd and report
// to user in case self-check process failed. // to user in case self-check process failed.
runCmdLogCh := make(chan string, 256) runCmdLogCh := make(chan string, 256)
if dir, err := userHomeDir(); err == nil { ud, err := userHomeDir()
setWorkingDirectory(sc, dir) sockDir := ud
if err == nil {
setWorkingDirectory(sc, ud)
if configPath == "" && writeDefaultConfig { if configPath == "" && writeDefaultConfig {
defaultConfigFile = filepath.Join(dir, defaultConfigFile) defaultConfigFile = filepath.Join(ud, defaultConfigFile)
} }
sc.Arguments = append(sc.Arguments, "--homedir="+dir) sc.Arguments = append(sc.Arguments, "--homedir="+ud)
sockDir := dir
if d, err := socketDir(); err == nil { if d, err := socketDir(); err == nil {
sockDir = d sockDir = d
} }
@@ -312,18 +308,11 @@ func initCLI() {
} }
tasks := []task{ tasks := []task{
resetDnsTask(p, s),
{s.Stop, false}, {s.Stop, false},
{func() error { return doGenerateNextDNSConfig(nextdns) }, true}, {func() error { return doGenerateNextDNSConfig(nextdns) }, true},
{func() error { return ensureUninstall(s) }, false}, {func() error { return ensureUninstall(s) }, false},
{func() error { {func() error {
// If ctrld is installed, we should not save current DNS settings, because:
//
// - The DNS settings was being set by ctrld already.
// - We could not determine the state of DNS settings before installing ctrld.
if isCtrldInstalled {
return nil
}
// Save current DNS so we can restore later. // Save current DNS so we can restore later.
withEachPhysicalInterfaces("", "save DNS settings", func(i *net.Interface) error { withEachPhysicalInterfaces("", "save DNS settings", func(i *net.Interface) error {
return saveCurrentStaticDNS(i) return saveCurrentStaticDNS(i)
@@ -343,7 +332,7 @@ func initCLI() {
return return
} }
ok, status, err := selfCheckStatus(s) ok, status, err := selfCheckStatus(s, ud, sockDir)
switch { switch {
case ok && status == service.StatusRunning: case ok && status == service.StatusRunning:
mainLog.Load().Notice().Msg("Service started") mainLog.Load().Notice().Msg("Service started")
@@ -381,7 +370,15 @@ func initCLI() {
uninstall(p, s) uninstall(p, s)
os.Exit(1) os.Exit(1)
} }
p.setDNS() if cc := newSocketControlClient(s, sockDir); cc != nil {
if resp, _ := cc.post(ifacePath, nil); resp != nil && resp.StatusCode == http.StatusOK {
if iface == "auto" {
iface = defaultIfaceName()
}
logger := mainLog.Load().With().Str("iface", iface).Logger()
logger.Debug().Msg("setting DNS successfully")
}
}
} }
}, },
} }
@@ -401,12 +398,10 @@ func initCLI() {
startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`)
startCmd.Flags().StringVarP(&nextdns, nextdnsFlagName, "", "", "NextDNS resolver id") startCmd.Flags().StringVarP(&nextdns, nextdnsFlagName, "", "", "NextDNS resolver id")
startCmd.Flags().StringVarP(&cdUpstreamProto, "proto", "", ctrld.ResolverTypeDOH, `Control D upstream type, either "doh" or "doh3"`) startCmd.Flags().StringVarP(&cdUpstreamProto, "proto", "", ctrld.ResolverTypeDOH, `Control D upstream type, either "doh" or "doh3"`)
startCmd.Flags().BoolVarP(&skipSelfChecks, "skip_self_checks", "", false, `Skip self checks after installing ctrld service`)
routerCmd := &cobra.Command{ routerCmd := &cobra.Command{
Use: "setup", Use: "setup",
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
},
Run: func(cmd *cobra.Command, _ []string) { Run: func(cmd *cobra.Command, _ []string) {
exe, err := os.Executable() exe, err := os.Executable()
if err != nil { if err != nil {
@@ -434,7 +429,6 @@ func initCLI() {
stopCmd := &cobra.Command{ stopCmd := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Use: "stop", Use: "stop",
@@ -456,6 +450,23 @@ func initCLI() {
if doTasks([]task{{s.Stop, true}}) { if doTasks([]task{{s.Stop, true}}) {
p.router.Cleanup() p.router.Cleanup()
p.resetDNS() p.resetDNS()
if router.WaitProcessExited() {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
for {
select {
case <-ctx.Done():
mainLog.Load().Error().Msg("timeout while waiting for service to stop")
return
default:
}
time.Sleep(time.Second)
if status, _ := s.Status(); status == service.StatusStopped {
break
}
}
}
mainLog.Load().Notice().Msg("Service stopped") mainLog.Load().Notice().Msg("Service stopped")
} }
}, },
@@ -466,14 +477,16 @@ func initCLI() {
restartCmd := &cobra.Command{ restartCmd := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Use: "restart", Use: "restart",
Short: "Restart the ctrld service", Short: "Restart the ctrld service",
Args: cobra.NoArgs, Args: cobra.NoArgs,
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
s, err := newService(&prog{}, svcConfig) readConfig(false)
v.Unmarshal(&cfg)
p := &prog{router: router.New(&cfg, runInCdMode())}
s, err := newService(p, svcConfig)
if err != nil { if err != nil {
mainLog.Load().Error().Msg(err.Error()) mainLog.Load().Error().Msg(err.Error())
return return
@@ -484,6 +497,7 @@ func initCLI() {
} }
initLogging() initLogging()
iface = runningIface(s)
tasks := []task{ tasks := []task{
{s.Stop, false}, {s.Stop, false},
{s.Start, true}, {s.Start, true},
@@ -494,10 +508,12 @@ func initCLI() {
mainLog.Load().Warn().Err(err).Msg("Service was restarted, but could not ping the control server") mainLog.Load().Warn().Err(err).Msg("Service was restarted, but could not ping the control server")
return return
} }
if cc := newSocketControlClient(s, dir); cc == nil { cc := newSocketControlClient(s, dir)
if cc == nil {
mainLog.Load().Notice().Msg("Service was not restarted") mainLog.Load().Notice().Msg("Service was not restarted")
os.Exit(1) os.Exit(1)
} }
_, _ = cc.post(ifacePath, nil)
mainLog.Load().Notice().Msg("Service restarted") mainLog.Load().Notice().Msg("Service restarted")
} }
}, },
@@ -505,7 +521,6 @@ func initCLI() {
reloadCmd := &cobra.Command{ reloadCmd := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Use: "reload", Use: "reload",
@@ -551,9 +566,6 @@ func initCLI() {
Use: "status", Use: "status",
Short: "Show status of the ctrld service", Short: "Show status of the ctrld service",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
s, err := newService(&prog{}, svcConfig) s, err := newService(&prog{}, svcConfig)
if err != nil { if err != nil {
@@ -581,14 +593,12 @@ func initCLI() {
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
// On darwin, running status command without privileges may return wrong information. // On darwin, running status command without privileges may return wrong information.
statusCmd.PreRun = func(cmd *cobra.Command, args []string) { statusCmd.PreRun = func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
} }
} }
uninstallCmd := &cobra.Command{ uninstallCmd := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Use: "uninstall", Use: "uninstall",
@@ -623,9 +633,6 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
Use: "list", Use: "list",
Short: "List network interfaces of the host", Short: "List network interfaces of the host",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
},
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { err := interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) {
fmt.Printf("Index : %d\n", i.Index) fmt.Printf("Index : %d\n", i.Index)
@@ -686,7 +693,6 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
rootCmd.AddCommand(serviceCmd) rootCmd.AddCommand(serviceCmd)
startCmdAlias := &cobra.Command{ startCmdAlias := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Use: "start", Use: "start",
@@ -704,7 +710,6 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
rootCmd.AddCommand(startCmdAlias) rootCmd.AddCommand(startCmdAlias)
stopCmdAlias := &cobra.Command{ stopCmdAlias := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Use: "stop", Use: "stop",
@@ -723,7 +728,6 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
restartCmdAlias := &cobra.Command{ restartCmdAlias := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Use: "restart", Use: "restart",
@@ -736,7 +740,6 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
reloadCmdAlias := &cobra.Command{ reloadCmdAlias := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Use: "reload", Use: "reload",
@@ -751,16 +754,12 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
Use: "status", Use: "status",
Short: "Show status of the ctrld service", Short: "Show status of the ctrld service",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) { Run: statusCmd.Run,
initConsoleLogging()
},
Run: statusCmd.Run,
} }
rootCmd.AddCommand(statusCmdAlias) rootCmd.AddCommand(statusCmdAlias)
uninstallCmdAlias := &cobra.Command{ uninstallCmdAlias := &cobra.Command{
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Use: "uninstall", Use: "uninstall",
@@ -785,7 +784,6 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
Short: "List clients that ctrld discovered", Short: "List clients that ctrld discovered",
Args: cobra.NoArgs, Args: cobra.NoArgs,
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
@@ -873,25 +871,31 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
ValidArgs: []string{upgradeChannelDev, upgradeChannelProd}, ValidArgs: []string{upgradeChannelDev, upgradeChannelProd},
Args: cobra.MaximumNArgs(1), Args: cobra.MaximumNArgs(1),
PreRun: func(cmd *cobra.Command, args []string) { PreRun: func(cmd *cobra.Command, args []string) {
initConsoleLogging()
checkHasElevatedPrivilege() checkHasElevatedPrivilege()
}, },
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
s, err := newService(&prog{}, svcConfig)
if err != nil {
mainLog.Load().Error().Msg(err.Error())
return
}
if _, err := s.Status(); errors.Is(err, service.ErrNotInstalled) {
mainLog.Load().Warn().Msg("service not installed")
return
}
bin, err := os.Executable() bin, err := os.Executable()
if err != nil { if err != nil {
mainLog.Load().Fatal().Err(err).Msg("failed to get current ctrld binary path") mainLog.Load().Fatal().Err(err).Msg("failed to get current ctrld binary path")
} }
sc := &service.Config{}
*sc = *svcConfig
sc.Executable = bin
readConfig(false)
v.Unmarshal(&cfg)
p := &prog{router: router.New(&cfg, runInCdMode())}
s, err := newService(p, sc)
if err != nil {
mainLog.Load().Error().Msg(err.Error())
return
}
svcInstalled := true
if _, err := s.Status(); errors.Is(err, service.ErrNotInstalled) {
svcInstalled = false
}
oldBin := bin + "_previous" oldBin := bin + "_previous"
urlString := upgradeChannel[upgradeChannelDefault] baseUrl := upgradeChannel[upgradeChannelDefault]
if len(args) > 0 { if len(args) > 0 {
channel := args[0] channel := args[0]
switch channel { switch channel {
@@ -899,12 +903,9 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
default: default:
mainLog.Load().Fatal().Msgf("uprade argument must be either %q or %q", upgradeChannelProd, upgradeChannelDev) mainLog.Load().Fatal().Msgf("uprade argument must be either %q or %q", upgradeChannelProd, upgradeChannelDev)
} }
urlString = upgradeChannel[channel] baseUrl = upgradeChannel[channel]
}
dlUrl := fmt.Sprintf("%s/%s-%s/ctrld", urlString, runtime.GOOS, runtime.GOARCH)
if runtime.GOOS == "windows" {
dlUrl += ".exe"
} }
dlUrl := upgradeUrl(baseUrl)
mainLog.Load().Debug().Msgf("Downloading binary: %s", dlUrl) mainLog.Load().Debug().Msgf("Downloading binary: %s", dlUrl)
resp, err := http.Get(dlUrl) resp, err := http.Get(dlUrl)
if err != nil { if err != nil {
@@ -923,18 +924,26 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`,
} }
doRestart := func() bool { doRestart := func() bool {
if !svcInstalled {
return true
}
tasks := []task{ tasks := []task{
{s.Stop, false}, {s.Stop, false},
{s.Start, false}, {s.Start, false},
} }
if doTasks(tasks) { if doTasks(tasks) {
if dir, err := socketDir(); err == nil { if dir, err := socketDir(); err == nil {
return newSocketControlClient(s, dir) != nil if cc := newSocketControlClient(s, dir); cc != nil {
_, _ = cc.post(ifacePath, nil)
return true
}
} }
} }
return false return false
} }
mainLog.Load().Debug().Msg("Restarting ctrld service using new binary") if svcInstalled {
mainLog.Load().Debug().Msg("Restarting ctrld service using new binary")
}
if doRestart() { if doRestart() {
_ = os.Remove(oldBin) _ = os.Remove(oldBin)
_ = os.Chmod(bin, 0755) _ = os.Chmod(bin, 0755)
@@ -1608,7 +1617,7 @@ func defaultIfaceName() string {
// - External testing, ensuring query could be sent from ctrld -> upstream. // - External testing, ensuring query could be sent from ctrld -> upstream.
// //
// Self-check is considered success only if both tests are ok. // Self-check is considered success only if both tests are ok.
func selfCheckStatus(s service.Service) (bool, service.Status, error) { func selfCheckStatus(s service.Service, homedir, sockDir string) (bool, service.Status, error) {
status, err := s.Status() status, err := s.Status()
if err != nil { if err != nil {
mainLog.Load().Warn().Err(err).Msg("could not get service status") mainLog.Load().Warn().Err(err).Msg("could not get service status")
@@ -1618,117 +1627,80 @@ func selfCheckStatus(s service.Service) (bool, service.Status, error) {
if status != service.StatusRunning { if status != service.StatusRunning {
return false, status, nil return false, status, nil
} }
dir, err := socketDir() // Skip self checks if set.
if err != nil { if skipSelfChecks {
mainLog.Load().Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory") return true, status, nil
return false, status, err
} }
mainLog.Load().Debug().Msg("waiting for ctrld listener to be ready") mainLog.Load().Debug().Msg("waiting for ctrld listener to be ready")
cc := newSocketControlClient(s, dir) cc := newSocketControlClient(s, sockDir)
if cc == nil { if cc == nil {
return false, status, errors.New("could not connect to control server") return false, status, errors.New("could not connect to control server")
} }
resp, err := cc.post(startedPath, nil) v = viper.NewWithOptions(viper.KeyDelimiter("::"))
if err != nil { ctrld.SetConfigNameWithPath(v, "ctrld", homedir)
mainLog.Load().Error().Err(err).Msg("failed to connect to control server") if configPath != "" {
v.SetConfigFile(configPath)
}
if err := v.ReadInConfig(); err != nil {
mainLog.Load().Error().Err(err).Msgf("failed to re-read configuration file: %s", v.ConfigFileUsed())
return false, status, err return false, status, err
} }
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { cfg = ctrld.Config{}
mainLog.Load().Error().Msg("ctrld listener is not ready") if err := v.Unmarshal(&cfg); err != nil {
return false, status, errors.New("ctrld listener is not ready") mainLog.Load().Error().Err(err).Msg("failed to update new config")
return false, status, err
} }
// Not a ctrld upstream, return status as-is. selfCheckExternalDomain := cfg.FirstUpstream().VerifyDomain()
if cfg.FirstUpstream().VerifyDomain() == "" { if selfCheckExternalDomain == "" {
// Nothing to do, return the status as-is.
return true, status, nil return true, status, nil
} }
mainLog.Load().Debug().Msg("ctrld listener is ready") mainLog.Load().Debug().Msg("ctrld listener is ready")
mainLog.Load().Debug().Msg("performing self-check") mainLog.Load().Debug().Msg("performing self-check")
bo := backoff.NewBackoff("self-check", logf, 10*time.Second)
bo.LogLongerThan = 500 * time.Millisecond
ctx := context.Background()
maxAttempts := 20
c := new(dns.Client)
var (
lcChanged map[string]*ctrld.ListenerConfig
ucChanged map[string]*ctrld.UpstreamConfig
mu sync.Mutex
)
if err := v.ReadInConfig(); err != nil { lc := cfg.FirstListener()
mainLog.Load().Fatal().Err(err).Msg("failed to read new config") addr := net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))
} if err := selfCheckResolveDomain(context.TODO(), addr, "internal", selfCheckInternalTestDomain); err != nil {
if err := v.Unmarshal(&cfg); err != nil {
mainLog.Load().Fatal().Err(err).Msg("failed to update new config")
}
domain := cfg.FirstUpstream().VerifyDomain()
if domain == "" {
// Nothing to do, return the status as-is.
return true, status, nil
}
watcher, err := fsnotify.NewWatcher()
if err != nil {
mainLog.Load().Error().Err(err).Msg("could not watch config change")
return false, status, err return false, status, err
} }
defer watcher.Close() if err := selfCheckResolveDomain(context.TODO(), addr, "external", selfCheckExternalDomain); err != nil {
return false, status, err
}
return true, status, nil
}
// selfCheckResolveDomain performs DNS test query against ctrld listener.
func selfCheckResolveDomain(ctx context.Context, addr, scope string, domain string) error {
bo := backoff.NewBackoff("self-check", logf, 10*time.Second)
bo.LogLongerThan = 500 * time.Millisecond
maxAttempts := 20
c := new(dns.Client)
v.OnConfigChange(func(in fsnotify.Event) {
mu.Lock()
defer mu.Unlock()
if err := v.UnmarshalKey("listener", &lcChanged); err != nil {
mainLog.Load().Error().Msgf("failed to unmarshal listener config: %v", err)
return
}
if err := v.UnmarshalKey("upstream", &ucChanged); err != nil {
mainLog.Load().Error().Msgf("failed to unmarshal upstream config: %v", err)
return
}
})
v.WatchConfig()
var ( var (
lastAnswer *dns.Msg lastAnswer *dns.Msg
lastErr error lastErr error
internalTested bool
) )
for i := 0; i < maxAttempts; i++ {
mu.Lock()
if lcChanged != nil {
cfg.Listener = lcChanged
}
if ucChanged != nil {
cfg.Upstream = ucChanged
}
mu.Unlock()
lc := cfg.FirstListener()
domain = cfg.FirstUpstream().VerifyDomain()
if !internalTested {
domain = selfCheckInternalTestDomain
}
if domain == "" {
continue
}
for i := 0; i < maxAttempts; i++ {
if domain == "" {
return errors.New("empty test domain")
}
m := new(dns.Msg) m := new(dns.Msg)
m.SetQuestion(domain+".", dns.TypeA) m.SetQuestion(domain+".", dns.TypeA)
m.RecursionDesired = true m.RecursionDesired = true
r, _, exErr := exchangeContextWithTimeout(c, time.Second, m, net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))) r, _, exErr := exchangeContextWithTimeout(c, time.Second, m, addr)
if r != nil && r.Rcode == dns.RcodeSuccess && len(r.Answer) > 0 { if r != nil && r.Rcode == dns.RcodeSuccess && len(r.Answer) > 0 {
internalTested = domain == selfCheckInternalTestDomain mainLog.Load().Debug().Msgf("%s self-check against %q succeeded", scope, domain)
if internalTested { return nil
mainLog.Load().Debug().Msgf("internal self-check against %q succeeded", domain)
continue // internal domain test ok, continue with external test.
} else {
mainLog.Load().Debug().Msgf("external self-check against %q succeeded", domain)
}
return true, status, nil
} }
// Return early if this is a connection refused. // Return early if this is a connection refused.
if errConnectionRefused(exErr) { if errConnectionRefused(exErr) {
return false, status, exErr return exErr
} }
lastAnswer = r lastAnswer = r
lastErr = exErr lastErr = exErr
@@ -1741,8 +1713,6 @@ func selfCheckStatus(s service.Service) (bool, service.Status, error) {
mainLog.Load().Err(err).Msgf("failed to connect to upstream.%s, endpoint: %s", name, uc.Endpoint) mainLog.Load().Err(err).Msgf("failed to connect to upstream.%s, endpoint: %s", name, uc.Endpoint)
} }
} }
lc := cfg.FirstListener()
addr := net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))
marker := strings.Repeat("=", 32) marker := strings.Repeat("=", 32)
mainLog.Load().Debug().Msg(marker) mainLog.Load().Debug().Msg(marker)
mainLog.Load().Debug().Msgf("listener address : %s", addr) mainLog.Load().Debug().Msgf("listener address : %s", addr)
@@ -1753,9 +1723,8 @@ func selfCheckStatus(s service.Service) (bool, service.Status, error) {
for _, s := range strings.Split(lastAnswer.String(), "\n") { for _, s := range strings.Split(lastAnswer.String(), "\n") {
mainLog.Load().Debug().Msgf("%s", s) mainLog.Load().Debug().Msgf("%s", s)
} }
return false, status, errSelfCheckNoAnswer
} }
return false, status, lastErr return errSelfCheckNoAnswer
} }
func userHomeDir() (string, error) { func userHomeDir() (string, error) {
@@ -2293,6 +2262,10 @@ func removeProvTokenFromArgs(sc *service.Config) {
// newSocketControlClient returns new control client after control server was started. // newSocketControlClient returns new control client after control server was started.
func newSocketControlClient(s service.Service, dir string) *controlClient { func newSocketControlClient(s service.Service, dir string) *controlClient {
// Return early if service is not running.
if status, err := s.Status(); err != nil || status != service.StatusRunning {
return nil
}
bo := backoff.NewBackoff("self-check", logf, 10*time.Second) bo := backoff.NewBackoff("self-check", logf, 10*time.Second)
bo.LogLongerThan = 10 * time.Second bo.LogLongerThan = 10 * time.Second
ctx := context.Background() ctx := context.Background()
@@ -2302,28 +2275,21 @@ func newSocketControlClient(s service.Service, dir string) *controlClient {
defer timeout.Stop() defer timeout.Stop()
// The socket control server may not start yet, so attempt to ping // The socket control server may not start yet, so attempt to ping
// it until we got a response. For each iteration, check ctrld status // it until we got a response.
// to make sure ctrld is still running.
for { for {
curStatus, err := s.Status() _, err := cc.post(startedPath, nil)
if err != nil { if err == nil {
return nil
}
if curStatus != service.StatusRunning {
return nil
}
if _, err := cc.post("/", nil); err == nil {
// Server was started, stop pinging. // Server was started, stop pinging.
break break
} }
// The socket control server is not ready yet, backoff for waiting it to be ready. // The socket control server is not ready yet, backoff for waiting it to be ready.
bo.BackOff(ctx, err) bo.BackOff(ctx, err)
select { select {
case <-timeout.C: case <-timeout.C:
return nil return nil
default: default:
} }
continue
} }
return cc return cc
@@ -2496,11 +2462,6 @@ func powershell(cmd string) ([]byte, error) {
// windowsHasLocalDnsServerRunning reports whether we are on Windows and having Dns server running. // windowsHasLocalDnsServerRunning reports whether we are on Windows and having Dns server running.
func windowsHasLocalDnsServerRunning() bool { func windowsHasLocalDnsServerRunning() bool {
if runtime.GOOS == "windows" { if runtime.GOOS == "windows" {
out, _ := powershell("Get-WindowsFeature -Name DNS")
if !bytes.Contains(bytes.ToLower(out), []byte("installed")) {
return false
}
_, err := powershell("Get-Process -Name DNS") _, err := powershell("Get-Process -Name DNS")
return err == nil return err == nil
} }
@@ -2535,3 +2496,79 @@ func runInCdMode() bool {
} }
return false return false
} }
// goArm returns the GOARM value for the binary.
func goArm() string {
if runtime.GOARCH != "arm" {
return ""
}
if bi, ok := debug.ReadBuildInfo(); ok {
for _, setting := range bi.Settings {
if setting.Key == "GOARM" {
return setting.Value
}
}
}
// Use ARM v5 as a fallback, since it works on all others.
return "5"
}
// upgradeUrl returns the url for downloading new ctrld binary.
func upgradeUrl(baseUrl string) string {
dlPath := fmt.Sprintf("%s-%s/ctrld", runtime.GOOS, runtime.GOARCH)
if armVersion := goArm(); armVersion != "" {
dlPath = fmt.Sprintf("%s-%sv%s/ctrld", runtime.GOOS, runtime.GOARCH, armVersion)
}
dlUrl := fmt.Sprintf("%s/%s", baseUrl, dlPath)
if runtime.GOOS == "windows" {
dlUrl += ".exe"
}
return dlUrl
}
// runningIface returns the value of the iface variable used by ctrld process which is running.
func runningIface(s service.Service) string {
if sockDir, err := socketDir(); err == nil {
if cc := newSocketControlClient(s, sockDir); cc != nil {
resp, err := cc.post(ifacePath, nil)
if err != nil {
return ""
}
defer resp.Body.Close()
if buf, _ := io.ReadAll(resp.Body); len(buf) > 0 {
return string(buf)
}
}
}
return ""
}
// resetDnsNoLog performs resetting DNS with logging disable.
func resetDnsNoLog(p *prog) {
lvl := zerolog.GlobalLevel()
zerolog.SetGlobalLevel(zerolog.Disabled)
p.resetDNS()
zerolog.SetGlobalLevel(lvl)
}
// resetDnsTask returns a task which perform reset DNS operation.
func resetDnsTask(p *prog, s service.Service) task {
status, err := s.Status()
isCtrldInstalled := !errors.Is(err, service.ErrNotInstalled)
isCtrldRunning := status == service.StatusRunning
return task{func() error {
// Always reset DNS first, ensuring DNS setting is in a good state.
// resetDNS must use the "iface" value of current running ctrld
// process to reset what setDNS has done properly.
oldIface := iface
iface = "auto"
if isCtrldRunning {
iface = runningIface(s)
}
if isCtrldInstalled {
resetDnsNoLog(p)
}
iface = oldIface
return nil
}, false}
}
+14
View File
@@ -10,6 +10,8 @@ import (
"sort" "sort"
"time" "time"
"github.com/kardianos/service"
dto "github.com/prometheus/client_model/go" dto "github.com/prometheus/client_model/go"
"github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld"
@@ -22,6 +24,7 @@ const (
reloadPath = "/reload" reloadPath = "/reload"
deactivationPath = "/deactivation" deactivationPath = "/deactivation"
cdPath = "/cd" cdPath = "/cd"
ifacePath = "/iface"
) )
type controlServer struct { type controlServer struct {
@@ -179,6 +182,17 @@ func (p *prog) registerControlServerHandler() {
} }
w.WriteHeader(http.StatusBadRequest) w.WriteHeader(http.StatusBadRequest)
})) }))
p.cs.register(ifacePath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
// p.setDNS is only called when running as a service
if !service.Interactive() {
<-p.csSetDnsDone
if p.csSetDnsOk {
w.Write([]byte(iface))
return
}
}
w.WriteHeader(http.StatusBadRequest)
}))
} }
func jsonResponse(next http.Handler) http.Handler { func jsonResponse(next http.Handler) http.Handler {
+7 -11
View File
@@ -210,12 +210,9 @@ func (p *prog) serveDNS(listenerNum string) error {
addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)) addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port))
s, errCh := runDNSServer(addr, proto, handler) s, errCh := runDNSServer(addr, proto, handler)
defer s.Shutdown() defer s.Shutdown()
select {
case err := <-errCh: p.started <- struct{}{}
return err
case <-time.After(5 * time.Second):
p.started <- struct{}{}
}
select { select {
case <-p.stopCh: case <-p.stopCh:
case <-ctx.Done(): case <-ctx.Done():
@@ -752,20 +749,19 @@ func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-cha
Handler: handler, Handler: handler,
} }
waitLock := sync.Mutex{} startedCh := make(chan struct{})
waitLock.Lock() s.NotifyStartedFunc = func() { sync.OnceFunc(func() { close(startedCh) })() }
s.NotifyStartedFunc = waitLock.Unlock
errCh := make(chan error) errCh := make(chan error)
go func() { go func() {
defer close(errCh) defer close(errCh)
if err := s.ListenAndServe(); err != nil { if err := s.ListenAndServe(); err != nil {
waitLock.Unlock() s.NotifyStartedFunc()
mainLog.Load().Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr) mainLog.Load().Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr)
errCh <- err errCh <- err
} }
}() }()
waitLock.Lock() <-startedCh
return s, errCh return s, errCh
} }
+4
View File
@@ -105,6 +105,10 @@ func (p *prog) checkDnsLoop() {
for uid := range p.loop { for uid := range p.loop {
msg := loopTestMsg(uid) msg := loopTestMsg(uid)
uc := upstream[uid] uc := upstream[uid]
// Skipping upstream which is being marked as down.
if uc == nil {
continue
}
resolver, err := ctrld.NewResolver(uc) resolver, err := ctrld.NewResolver(uc)
if err != nil { if err != nil {
mainLog.Load().Warn().Err(err).Msgf("could not perform loop check for upstream: %q, endpoint: %q", uc.Name, uc.Endpoint) mainLog.Load().Warn().Err(err).Msgf("could not perform loop check for upstream: %q, endpoint: %q", uc.Name, uc.Endpoint)
+1
View File
@@ -35,6 +35,7 @@ var (
nextdns string nextdns string
cdUpstreamProto string cdUpstreamProto string
deactivationPin int64 deactivationPin int64
skipSelfChecks bool
mainLog atomic.Pointer[zerolog.Logger] mainLog atomic.Pointer[zerolog.Logger]
consoleWriter zerolog.ConsoleWriter consoleWriter zerolog.ConsoleWriter
+27 -14
View File
@@ -41,17 +41,12 @@ func setDNS(iface *net.Interface, nameservers []string) error {
// Configuring the Dns server to forward queries to ctrld instead. // Configuring the Dns server to forward queries to ctrld instead.
if windowsHasLocalDnsServerRunning() { if windowsHasLocalDnsServerRunning() {
file := absHomeDir(forwardersFilename) file := absHomeDir(forwardersFilename)
if data, _ := os.ReadFile(file); len(data) > 0 { oldForwardersContent, _ := os.ReadFile(file)
if err := removeDnsServerForwarders(strings.Split(string(data), ",")); err != nil {
mainLog.Load().Error().Err(err).Msg("could not remove current forwarders settings")
} else {
mainLog.Load().Debug().Msg("removed current forwarders settings.")
}
}
if err := os.WriteFile(file, []byte(strings.Join(nameservers, ",")), 0600); err != nil { if err := os.WriteFile(file, []byte(strings.Join(nameservers, ",")), 0600); err != nil {
mainLog.Load().Warn().Err(err).Msg("could not save forwarders settings") mainLog.Load().Warn().Err(err).Msg("could not save forwarders settings")
} }
if err := addDnsServerForwarders(nameservers); err != nil { oldForwarders := strings.Split(string(oldForwardersContent), ",")
if err := addDnsServerForwarders(nameservers, oldForwarders); err != nil {
mainLog.Load().Warn().Err(err).Msg("could not set forwarders settings") mainLog.Load().Warn().Err(err).Msg("could not set forwarders settings")
} }
} }
@@ -213,14 +208,32 @@ func currentStaticDNS(iface *net.Interface) ([]string, error) {
return ns, nil return ns, nil
} }
// addDnsServerForwarders adds given nameservers to DNS server forwarders list. // addDnsServerForwarders adds given nameservers to DNS server forwarders list,
func addDnsServerForwarders(nameservers []string) error { // and also removing old forwarders if provided.
for _, ns := range nameservers { func addDnsServerForwarders(nameservers, old []string) error {
cmd := fmt.Sprintf("Add-DnsServerForwarder -IPAddress %s", ns) newForwardersMap := make(map[string]struct{})
if out, err := powershell(cmd); err != nil { newForwarders := make([]string, len(nameservers))
return fmt.Errorf("%w: %s", err, string(out)) for i := range nameservers {
newForwardersMap[nameservers[i]] = struct{}{}
newForwarders[i] = fmt.Sprintf("%q", nameservers[i])
}
oldForwarders := old[:0]
for _, fwd := range old {
if _, ok := newForwardersMap[fwd]; !ok {
oldForwarders = append(oldForwarders, fwd)
} }
} }
// NOTE: It is important to add new forwarder before removing old one.
// Testing on Windows Server 2022 shows that removing forwarder1
// then adding forwarder2 sometimes ends up adding both of them
// to the forwarders list.
cmd := fmt.Sprintf("Add-DnsServerForwarder -IPAddress %s", strings.Join(newForwarders, ","))
if len(oldForwarders) > 0 {
cmd = fmt.Sprintf("%s ; Remove-DnsServerForwarder -IPAddress %s -Force", cmd, strings.Join(oldForwarders, ","))
}
if out, err := powershell(cmd); err != nil {
return fmt.Errorf("%w: %s", err, string(out))
}
return nil return nil
} }
+39 -18
View File
@@ -69,6 +69,8 @@ type prog struct {
reloadDoneCh chan struct{} reloadDoneCh chan struct{}
logConn net.Conn logConn net.Conn
cs *controlServer cs *controlServer
csSetDnsDone chan struct{}
csSetDnsOk bool
cfg *ctrld.Config cfg *ctrld.Config
localUpstreams []string localUpstreams []string
@@ -194,9 +196,6 @@ func (p *prog) runWait() {
} }
func (p *prog) preRun() { func (p *prog) preRun() {
if !service.Interactive() {
p.setDNS()
}
if runtime.GOOS == "darwin" { if runtime.GOOS == "darwin" {
p.onStopped = append(p.onStopped, func() { p.onStopped = append(p.onStopped, func() {
if !service.Interactive() { if !service.Interactive() {
@@ -206,6 +205,15 @@ func (p *prog) preRun() {
} }
} }
func (p *prog) postRun() {
if !service.Interactive() {
p.resetDNS()
ns := ctrld.InitializeOsResolver()
mainLog.Load().Debug().Msgf("initialized OS resolver with nameservers: %v", ns)
p.setDNS()
}
}
func (p *prog) setupUpstream(cfg *ctrld.Config) { func (p *prog) setupUpstream(cfg *ctrld.Config) {
localUpstreams := make([]string, 0, len(cfg.Upstream)) localUpstreams := make([]string, 0, len(cfg.Upstream))
ptrNameservers := make([]string, 0, len(cfg.Upstream)) ptrNameservers := make([]string, 0, len(cfg.Upstream))
@@ -249,6 +257,14 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
numListeners := len(p.cfg.Listener) numListeners := len(p.cfg.Listener)
if !reload { if !reload {
p.started = make(chan struct{}, numListeners) p.started = make(chan struct{}, numListeners)
if p.cs != nil {
p.csSetDnsDone = make(chan struct{}, 1)
p.registerControlServerHandler()
if err := p.cs.start(); err != nil {
mainLog.Load().Warn().Err(err).Msg("could not start control server")
}
mainLog.Load().Debug().Msgf("control server started: %s", p.cs.addr)
}
} }
p.onStartedDone = make(chan struct{}) p.onStartedDone = make(chan struct{})
p.loop = make(map[string]bool) p.loop = make(map[string]bool)
@@ -381,12 +397,7 @@ func (p *prog) run(reload bool, reloadCh chan struct{}) {
if p.logConn != nil { if p.logConn != nil {
_ = p.logConn.Close() _ = p.logConn.Close()
} }
if p.cs != nil { p.postRun()
p.registerControlServerHandler()
if err := p.cs.start(); err != nil {
mainLog.Load().Warn().Err(err).Msg("could not start control server")
}
}
} }
wg.Wait() wg.Wait()
} }
@@ -430,17 +441,25 @@ func (p *prog) deAllocateIP() error {
} }
func (p *prog) setDNS() { func (p *prog) setDNS() {
setDnsOK := false
defer func() {
p.csSetDnsOk = setDnsOK
p.csSetDnsDone <- struct{}{}
close(p.csSetDnsDone)
}()
if cfg.Listener == nil { if cfg.Listener == nil {
return return
} }
if iface == "" { if iface == "" {
return return
} }
runningIface := iface
// allIfaces tracks whether we should set DNS for all physical interfaces. // allIfaces tracks whether we should set DNS for all physical interfaces.
allIfaces := false allIfaces := false
if iface == "auto" { if runningIface == "auto" {
iface = defaultIfaceName() runningIface = defaultIfaceName()
// If iface is "auto", it means user does not specify "--iface" flag. // If runningIface is "auto", it means user does not specify "--iface" flag.
// In this case, ctrld has to set DNS for all physical interfaces, so // In this case, ctrld has to set DNS for all physical interfaces, so
// thing will still work when user switch from one to the other. // thing will still work when user switch from one to the other.
allIfaces = requiredMultiNICsConfig() allIfaces = requiredMultiNICsConfig()
@@ -449,8 +468,8 @@ func (p *prog) setDNS() {
if lc == nil { if lc == nil {
return return
} }
logger := mainLog.Load().With().Str("iface", iface).Logger() logger := mainLog.Load().With().Str("iface", runningIface).Logger()
netIface, err := netInterface(iface) netIface, err := netInterface(runningIface)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("could not get interface") logger.Error().Err(err).Msg("could not get interface")
return return
@@ -484,6 +503,7 @@ func (p *prog) setDNS() {
logger.Error().Err(err).Msgf("could not set DNS for interface") logger.Error().Err(err).Msgf("could not set DNS for interface")
return return
} }
setDnsOK = true
logger.Debug().Msg("setting DNS successfully") logger.Debug().Msg("setting DNS successfully")
if shouldWatchResolvconf() { if shouldWatchResolvconf() {
servers := make([]netip.Addr, len(nameservers)) servers := make([]netip.Addr, len(nameservers))
@@ -503,14 +523,15 @@ func (p *prog) resetDNS() {
if iface == "" { if iface == "" {
return return
} }
runningIface := iface
allIfaces := false allIfaces := false
if iface == "auto" { if runningIface == "auto" {
iface = defaultIfaceName() runningIface = defaultIfaceName()
// See corresponding comments in (*prog).setDNS function. // See corresponding comments in (*prog).setDNS function.
allIfaces = requiredMultiNICsConfig() allIfaces = requiredMultiNICsConfig()
} }
logger := mainLog.Load().With().Str("iface", iface).Logger() logger := mainLog.Load().With().Str("iface", runningIface).Logger()
netIface, err := netInterface(iface) netIface, err := netInterface(runningIface)
if err != nil { if err != nil {
logger.Error().Err(err).Msg("could not get interface") logger.Error().Err(err).Msg("could not get interface")
return return
+28 -1
View File
@@ -1,7 +1,12 @@
package cli package cli
import ( import (
"bufio"
"bytes"
"io"
"os" "os"
"os/exec"
"strings"
"github.com/kardianos/service" "github.com/kardianos/service"
@@ -24,12 +29,34 @@ func setDependencies(svc *service.Config) {
"After=network-online.target", "After=network-online.target",
"Wants=NetworkManager-wait-online.service", "Wants=NetworkManager-wait-online.service",
"After=NetworkManager-wait-online.service", "After=NetworkManager-wait-online.service",
"Wants=systemd-networkd-wait-online.service",
"Wants=nss-lookup.target", "Wants=nss-lookup.target",
"After=nss-lookup.target", "After=nss-lookup.target",
} }
if out, _ := exec.Command("networkctl", "--no-pager").CombinedOutput(); len(out) > 0 {
if wantsSystemDNetworkdWaitOnline(bytes.NewReader(out)) {
svc.Dependencies = append(svc.Dependencies, "Wants=systemd-networkd-wait-online.service")
}
}
} }
func setWorkingDirectory(svc *service.Config, dir string) { func setWorkingDirectory(svc *service.Config, dir string) {
svc.WorkingDirectory = dir svc.WorkingDirectory = dir
} }
// wantsSystemDNetworkdWaitOnline reports whether "systemd-networkd-wait-online" service
// is required to be added to ctrld dependencies services.
// The input reader r is the output of "networkctl --no-pager" command.
func wantsSystemDNetworkdWaitOnline(r io.Reader) bool {
scanner := bufio.NewScanner(r)
// Skip header
scanner.Scan()
configured := false
for scanner.Scan() {
fields := strings.Fields(scanner.Text())
if len(fields) > 0 && fields[len(fields)-1] == "configured" {
configured = true
break
}
}
return configured
}
+48
View File
@@ -0,0 +1,48 @@
package cli
import (
"io"
"strings"
"testing"
)
const (
networkctlUnmanagedOutput = `IDX LINK TYPE OPERATIONAL SETUP
1 lo loopback carrier unmanaged
2 wlp0s20f3 wlan routable unmanaged
3 tailscale0 none routable unmanaged
4 br-9ac33145e060 bridge no-carrier unmanaged
5 docker0 bridge no-carrier unmanaged
5 links listed.
`
networkctlManagedOutput = `IDX LINK TYPE OPERATIONAL SETUP
1 lo loopback carrier unmanaged
2 wlp0s20f3 wlan routable configured
3 tailscale0 none routable unmanaged
4 br-9ac33145e060 bridge no-carrier unmanaged
5 docker0 bridge no-carrier unmanaged
5 links listed.
`
)
func Test_wantsSystemDNetworkdWaitOnline(t *testing.T) {
tests := []struct {
name string
r io.Reader
required bool
}{
{"unmanaged", strings.NewReader(networkctlUnmanagedOutput), false},
{"managed", strings.NewReader(networkctlManagedOutput), true},
{"empty", strings.NewReader(""), false},
}
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
if required := wantsSystemDNetworkdWaitOnline(tc.r); required != tc.required {
t.Errorf("wants %v got %v", tc.required, required)
}
})
}
}
+11 -5
View File
@@ -21,7 +21,7 @@ func newService(i service.Interface, c *service.Config) (service.Service, error)
} }
switch { switch {
case router.IsOldOpenwrt(), router.IsNetGearOrbi(): case router.IsOldOpenwrt(), router.IsNetGearOrbi():
return &procd{&sysV{s}}, nil return &procd{sysV: &sysV{s}, svcConfig: c}, nil
case router.IsGLiNet(): case router.IsGLiNet():
return &sysV{s}, nil return &sysV{s}, nil
case s.Platform() == "unix-systemv": case s.Platform() == "unix-systemv":
@@ -89,18 +89,24 @@ func (s *sysV) Status() (service.Status, error) {
// like old GL.iNET Opal router. // like old GL.iNET Opal router.
type procd struct { type procd struct {
*sysV *sysV
svcConfig *service.Config
} }
func (s *procd) Status() (service.Status, error) { func (s *procd) Status() (service.Status, error) {
if !s.installed() { if !s.installed() {
return service.StatusUnknown, service.ErrNotInstalled return service.StatusUnknown, service.ErrNotInstalled
} }
exe, err := os.Executable() bin := s.svcConfig.Executable
if err != nil { if bin == "" {
return service.StatusUnknown, nil exe, err := os.Executable()
if err != nil {
return service.StatusUnknown, nil
}
bin = exe
} }
// Looking for something like "/sbin/ctrld run ". // Looking for something like "/sbin/ctrld run ".
shellCmd := fmt.Sprintf("ps | grep -q %q", exe+" [r]un ") shellCmd := fmt.Sprintf("ps | grep -q %q", bin+" [r]un ")
if err := exec.Command("sh", "-c", shellCmd).Run(); err != nil { if err := exec.Command("sh", "-c", shellCmd).Run(); err != nil {
return service.StatusStopped, nil return service.StatusStopped, nil
} }
+1 -1
View File
@@ -336,7 +336,7 @@ The protocol that `ctrld` will use to send DNS requests to upstream.
- Type: string - Type: string
- Required: yes - Required: yes
- Valid values: `doh`, `doh3`, `dot`, `doq`, `legacy`, `os` - Valid values: `doh`, `doh3`, `dot`, `doq`, `legacy`
### ip_stack ### ip_stack
Specifying what kind of ip stack that `ctrld` will use to connect to upstream. Specifying what kind of ip stack that `ctrld` will use to connect to upstream.
Binary file not shown.

After

Width:  |  Height:  |  Size: 458 KiB

+1
View File
@@ -18,6 +18,7 @@ start_service() {
procd_set_param stdout 1 # forward stdout of the command to logd procd_set_param stdout 1 # forward stdout of the command to logd
procd_set_param stderr 1 # same for stderr procd_set_param stderr 1 # same for stderr
procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop procd_set_param pidfile ${pid_file} # write a pid file on instance start and remove it on stop
procd_set_param term_timeout 10
procd_close_instance procd_close_instance
echo "${name} has been started" echo "${name} has been started"
} }
+5
View File
@@ -98,6 +98,11 @@ func IsOldOpenwrt() bool {
return cmd == "" return cmd == ""
} }
// WaitProcessExited reports whether the "ctrld stop" command have to wait until ctrld process exited.
func WaitProcessExited() bool {
return Name() == openwrt.Name
}
var routerPlatform atomic.Pointer[router] var routerPlatform atomic.Pointer[router]
type router struct { type router struct {
+8 -4
View File
@@ -49,11 +49,15 @@ func (s *merlinSvc) Platform() string {
} }
func (s *merlinSvc) configPath() string { func (s *merlinSvc) configPath() string {
path, err := os.Executable() bin := s.Config.Executable
if err != nil { if bin == "" {
return "" path, err := os.Executable()
if err != nil {
return ""
}
bin = path
} }
return path + ".startup" return bin + ".startup"
} }
func (s *merlinSvc) template() *template.Template { func (s *merlinSvc) template() *template.Template {
+13 -33
View File
@@ -1,12 +1,9 @@
package ctrld package ctrld
import ( import (
"net"
"syscall" "syscall"
"golang.zx2c4.com/wireguard/windows/tunnel/winipcfg" "golang.zx2c4.com/wireguard/windows/tunnel/winipcfg"
"golang.org/x/sys/windows"
) )
func dnsFns() []dnsFn { func dnsFns() []dnsFn {
@@ -20,40 +17,23 @@ func dnsFromAdapter() []string {
} }
ns := make([]string, 0, len(aas)*2) ns := make([]string, 0, len(aas)*2)
seen := make(map[string]bool) seen := make(map[string]bool)
do := func(addr windows.SocketAddress) { addressMap := make(map[string]struct{})
sa, err := addr.Sockaddr.Sockaddr() for _, aa := range aas {
if err != nil { for a := aa.FirstUnicastAddress; a != nil; a = a.Next {
return addressMap[a.Address.IP().String()] = struct{}{}
} }
var ip net.IP
switch sa := sa.(type) {
case *syscall.SockaddrInet4:
ip = net.IPv4(sa.Addr[0], sa.Addr[1], sa.Addr[2], sa.Addr[3])
case *syscall.SockaddrInet6:
ip = make(net.IP, net.IPv6len)
copy(ip, sa.Addr[:])
if ip[0] == 0xfe && ip[1] == 0xc0 {
// Ignore these fec0/10 ones. Windows seems to
// populate them as defaults on its misc rando
// interfaces.
return
}
default:
return
}
if ip.IsLoopback() || seen[ip.String()] {
return
}
seen[ip.String()] = true
ns = append(ns, ip.String())
} }
for _, aa := range aas { for _, aa := range aas {
for dns := aa.FirstDNSServerAddress; dns != nil; dns = dns.Next { for dns := aa.FirstDNSServerAddress; dns != nil; dns = dns.Next {
do(dns.Address) ip := dns.Address.IP()
} if ip == nil || ip.IsLoopback() || seen[ip.String()] {
for gw := aa.FirstGatewayAddress; gw != nil; gw = gw.Next { continue
do(gw.Address) }
if _, ok := addressMap[ip.String()]; ok {
continue
}
seen[ip.String()] = true
ns = append(ns, ip.String())
} }
} }
return ns return ns
+15 -2
View File
@@ -35,13 +35,26 @@ const bootstrapDNS = "76.76.2.22"
// or is the Resolver used for ResolverTypeOS. // or is the Resolver used for ResolverTypeOS.
var or = &osResolver{nameservers: defaultNameservers()} var or = &osResolver{nameservers: defaultNameservers()}
// defaultNameservers returns OS nameservers plus ctrld bootstrap nameserver. // defaultNameservers returns nameservers used by the OS.
// If no nameservers can be found, ctrld bootstrap nameserver will be used.
func defaultNameservers() []string { func defaultNameservers() []string {
ns := nameservers() ns := nameservers()
ns = append(ns, net.JoinHostPort(bootstrapDNS, "53")) if len(ns) == 0 {
ns = append(ns, net.JoinHostPort(bootstrapDNS, "53"))
}
return ns return ns
} }
// InitializeOsResolver initializes OS resolver using the current system DNS settings.
// It returns the nameservers that is going to be used by the OS resolver.
//
// It's the caller's responsibility to ensure the system DNS is in a clean state before
// calling this function.
func InitializeOsResolver() []string {
or.nameservers = defaultNameservers()
return or.nameservers
}
// Resolver is the interface that wraps the basic DNS operations. // Resolver is the interface that wraps the basic DNS operations.
// //
// Resolve resolves the DNS query, return the result and the corresponding error. // Resolve resolves the DNS query, return the result and the corresponding error.