From e684c7d8c4ff69d4bf9c313386a12aa8783c06be Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 12 Jun 2023 16:04:54 +0000 Subject: [PATCH 01/84] Follow CNAME chain to find correct target To prevent abusive response from some malicious DNS server, ctrld ignores the response if the target does not match question domain. However, that would break CNAME chain, which is allowed the mismatch happens. --- resolver.go | 21 +++++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/resolver.go b/resolver.go index 3162d62..f61c9bc 100644 --- a/resolver.go +++ b/resolver.go @@ -159,15 +159,27 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) timeoutMs = timeout } questionDomain := dns.Fqdn(domain) - ipFromRecord := func(record dns.RR) string { + + // Getting the real target domain name from CNAME if presents. + targetDomain := func(answers []dns.RR) string { + for _, a := range answers { + switch ar := a.(type) { + case *dns.CNAME: + return ar.Target + } + } + return questionDomain + } + // Getting ip address from A or AAAA record. + ipFromRecord := func(record dns.RR, target string) string { switch ar := record.(type) { case *dns.A: - if ar.Hdr.Name != questionDomain { + if ar.Hdr.Name != target { return "" } return ar.A.String() case *dns.AAAA: - if ar.Hdr.Name != questionDomain { + if ar.Hdr.Name != target { return "" } return ar.AAAA.String() @@ -195,8 +207,9 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) ProxyLog.Error().Msg("no answer from OS resolver") return } + target := targetDomain(r.Answer) for _, a := range r.Answer { - if ip := ipFromRecord(a); ip != "" { + if ip := ipFromRecord(a, target); ip != "" { ips = append(ips, ip) } } From 60d6734e1f4a1d0c1bceaee61cc264121f9929b0 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 13 Jun 2023 00:28:22 +0700 Subject: [PATCH 02/84] cmd/ctrld: support older GL-inet devices The openwrt version in old GL-inet devices do not support checking status using /etc/init.d/, so the sysV wrapping trick won't work. Instead, we need to parse "ps" command output to check whether ctrld process is running or not. While at it, making newService as a wrapper of service.New function, prevent the caller from calling the latter without following call to the former, causing mismatch in service operations. --- cmd/ctrld/cli.go | 24 ++++++--------- cmd/ctrld/service.go | 73 +++++++++++++++++++++++++++++++++++--------- 2 files changed, 67 insertions(+), 30 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 8cfca02..85b7f85 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -131,11 +131,10 @@ func initCLI() { waitCh: waitCh, stopCh: stopCh, } - s, err := service.New(p, svcConfig) + s, err := newService(p, svcConfig) if err != nil { mainLog.Fatal().Err(err).Msg("failed create new service") } - s = newService(s) if err := s.Run(); err != nil { mainLog.Error().Err(err).Msg("failed to start service") } @@ -305,12 +304,11 @@ func initCLI() { } prog := &prog{} - s, err := service.New(prog, sc) + s, err := newService(prog, sc) if err != nil { mainLog.Error().Msg(err.Error()) return } - s = newService(s) tasks := []task{ {s.Stop, false}, {s.Uninstall, false}, @@ -322,7 +320,7 @@ func initCLI() { mainLog.Warn().Err(err).Msg("post installation failed, please check system/service log for details error") return } - status, err := serviceStatus(s) + status, err := s.Status() if err != nil { mainLog.Warn().Err(err).Msg("could not get service status") return @@ -370,12 +368,11 @@ func initCLI() { Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { prog := &prog{} - s, err := service.New(prog, svcConfig) + s, err := newService(prog, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) return } - s = newService(s) initLogging() if doTasks([]task{{s.Stop, true}}) { prog.resetDNS() @@ -394,12 +391,11 @@ func initCLI() { Short: "Restart the ctrld service", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - s, err := service.New(&prog{}, svcConfig) + s, err := newService(&prog{}, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) return } - s = newService(s) initLogging() if doTasks([]task{{s.Restart, true}}) { mainLog.Notice().Msg("Service restarted") @@ -415,13 +411,12 @@ func initCLI() { initConsoleLogging() }, Run: func(cmd *cobra.Command, args []string) { - s, err := service.New(&prog{}, svcConfig) + s, err := newService(&prog{}, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) return } - s = newService(s) - status, err := serviceStatus(s) + status, err := s.Status() if err != nil { mainLog.Error().Msg(err.Error()) os.Exit(1) @@ -460,7 +455,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { prog := &prog{} - s, err := service.New(prog, svcConfig) + s, err := newService(prog, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) return @@ -712,12 +707,11 @@ func processCDFlags() { logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID) resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev) if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode { - s, err := service.New(&prog{}, svcConfig) + s, err := newService(&prog{}, svcConfig) if err != nil { logger.Warn().Err(err).Msg("failed to create new service") return } - if netIface, _ := netInterface(iface); netIface != nil { if err := restoreNetworkManager(); err != nil { logger.Error().Err(err).Msg("could not restore NetworkManager") diff --git a/cmd/ctrld/service.go b/cmd/ctrld/service.go index bce6503..adf0a28 100644 --- a/cmd/ctrld/service.go +++ b/cmd/ctrld/service.go @@ -3,6 +3,7 @@ package main import ( "bytes" "errors" + "fmt" "os" "os/exec" @@ -11,37 +12,87 @@ import ( "github.com/Control-D-Inc/ctrld/internal/router" ) -func newService(s service.Service) service.Service { - // TODO: unify for other SysV system. - switch { - case router.IsGLiNet(), router.IsOldOpenwrt(): - return &sysV{s} +// newService wraps service.New call to return service.Service +// wrapper which is suitable for the current platform. +func newService(i service.Interface, c *service.Config) (service.Service, error) { + s, err := service.New(i, c) + if err != nil { + return nil, err } - return s + switch { + case router.IsOldOpenwrt(): + return &procd{&sysV{s}}, nil + case router.IsGLiNet(): // TODO: unify for other SysV system. + return &sysV{s}, nil + } + return s, nil } // sysV wraps a service.Service, and provide start/stop/status command // base on "/etc/init.d/". // -// Use this on system wherer "service" command is not available, like GL.iNET router. +// Use this on system where "service" command is not available, like GL.iNET router. type sysV struct { service.Service } +func (s *sysV) installed() bool { + fi, err := os.Stat("/etc/init.d/ctrld") + if err != nil { + return false + } + mode := fi.Mode() + return mode.IsRegular() && (mode&0111) != 0 +} + func (s *sysV) Start() error { + if !s.installed() { + return service.ErrNotInstalled + } _, err := exec.Command("/etc/init.d/ctrld", "start").CombinedOutput() return err } func (s *sysV) Stop() error { + if !s.installed() { + return service.ErrNotInstalled + } _, err := exec.Command("/etc/init.d/ctrld", "stop").CombinedOutput() return err } func (s *sysV) Status() (service.Status, error) { + if !s.installed() { + return service.StatusUnknown, service.ErrNotInstalled + } return unixSystemVServiceStatus() } +// procd wraps a service.Service, and provide start/stop command +// base on "/etc/init.d/", status command base on parsing "ps" command output. +// +// Use this on system where "/etc/init.d/ status" command is not available, +// like old GL.iNET Opal router. +type procd struct { + *sysV +} + +func (s *procd) Status() (service.Status, error) { + if !s.installed() { + return service.StatusUnknown, service.ErrNotInstalled + } + exe, err := os.Executable() + if err != nil { + return service.StatusUnknown, nil + } + // Looking for something like "/sbin/ctrld run ". + shellCmd := fmt.Sprintf("ps | grep -q %q", exe+" [r]un ") + if err := exec.Command("sh", "-c", shellCmd).Run(); err != nil { + return service.StatusStopped, nil + } + return service.StatusRunning, nil +} + type task struct { f func() error abortOnError bool @@ -73,14 +124,6 @@ func checkHasElevatedPrivilege() { } } -func serviceStatus(s service.Service) (service.Status, error) { - status, err := s.Status() - if err != nil && service.Platform() == "unix-systemv" { - return unixSystemVServiceStatus() - } - return status, err -} - func unixSystemVServiceStatus() (service.Status, error) { out, err := exec.Command("/etc/init.d/ctrld", "status").CombinedOutput() if err != nil { From d5e6c7b13f22a21f46867adc9d664505b5081847 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 14 Jun 2023 04:57:02 +0000 Subject: [PATCH 03/84] Add Dockerfile for building docker image --- .dockerignore | 2 ++ Dockerfile | 32 ++++++++++++++++++++++++++++++++ README.md | 7 +++++++ 3 files changed, 41 insertions(+) create mode 100644 .dockerignore create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..95d7144 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +Dockerfile +.git/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..d0d2b54 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,32 @@ +# Using Debian bullseye for building regular image. +# Using scratch image for minimal image size. +# The final image has: +# +# - Timezone info file. +# - CA certs file. +# - /etc/{passwd,group} file. +# - Non-cgo ctrld binary. +# +# CI_COMMIT_TAG is used to set the version of ctrld binary. +FROM golang:bullseye as base + +WORKDIR /app + +RUN apt-get update && apt-get install -y upx-ucl + +COPY . . + +ARG tag=master +ENV CI_COMMIT_TAG=$tag +RUN CGO_ENABLED=0 ./scripts/build.sh linux/amd64 + +FROM scratch + +COPY --from=base /usr/share/zoneinfo /usr/share/zoneinfo +COPY --from=base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ +COPY --from=base /etc/passwd /etc/passwd +COPY --from=base /etc/group /etc/group + +COPY --from=base /app/ctrld-linux-amd64-nocgo ctrld + +ENTRYPOINT ["./ctrld", "run"] diff --git a/README.md b/README.md index 3db1536..69018d6 100644 --- a/README.md +++ b/README.md @@ -67,6 +67,13 @@ or $ go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest ``` +## Docker + +``` +$ docker build -t controld/ctrld . +$ docker run -d --name=ctrld -p 53:53/tcp -p 53:53/udp controld/ctrld --cd=p2 -vv +``` + # Usage The cli is self documenting, so free free to run `--help` on any sub-command to get specific usages. From 41139b334324339b94a0939c3f270e2fafa7ae21 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 16 Jun 2023 19:04:30 +0700 Subject: [PATCH 04/84] all: add configuration to limit max concurrent requests Currently, there's no upper bound for how many requests that ctrld will handle at a time. This could be problem on some low capacity routers, where CPU/RAM is very limited. This commit adds a configuration to limit how many requests that will be handled concurrently. The default is 256, which should works well for most routers (the default concurrent requests of dnsmasq is 150). --- cmd/ctrld/dns_proxy.go | 3 ++- cmd/ctrld/prog.go | 12 ++++++++++++ cmd/ctrld/sema.go | 24 ++++++++++++++++++++++++ config.go | 17 +++++++++-------- config_test.go | 8 ++++++++ docs/config.md | 8 ++++++++ 6 files changed, 63 insertions(+), 9 deletions(-) create mode 100644 cmd/ctrld/sema.go diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 9f9fa30..366fafb 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -47,6 +47,8 @@ func (p *prog) serveDNS(listenerNum string) error { failoverRcodes = listenerConfig.Policy.FailoverRcodeNumbers } handler := dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) { + p.sema.acquire() + defer p.sema.release() q := m.Question[0] domain := canonicalName(q.Name) reqId := requestID() @@ -60,7 +62,6 @@ func (p *prog) serveDNS(listenerNum string) error { if !matched && listenerConfig.Restricted { answer = new(dns.Msg) answer.SetRcode(m, dns.RcodeRefused) - } else { answer = p.proxy(ctx, upstreams, failoverRcodes, m) rtt := time.Since(t) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 807d22c..5ec372c 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -17,6 +17,8 @@ import ( "github.com/Control-D-Inc/ctrld/internal/router" ) +const defaultSemaphoreCap = 256 + var logf = func(format string, args ...any) { mainLog.Debug().Msgf(format, args...) } @@ -36,6 +38,7 @@ type prog struct { cfg *ctrld.Config cache dnscache.Cacher + sema semaphore } func (p *prog) Start(s service.Service) error { @@ -56,6 +59,15 @@ func (p *prog) run() { p.cache = cacher } } + p.sema = &chanSemaphore{ready: make(chan struct{}, defaultSemaphoreCap)} + if mcr := p.cfg.Service.MaxConcurrentRequests; mcr != nil { + n := *mcr + if n == 0 { + p.sema = &noopSemaphore{} + } else { + p.sema = &chanSemaphore{ready: make(chan struct{}, n)} + } + } var wg sync.WaitGroup wg.Add(len(p.cfg.Listener)) diff --git a/cmd/ctrld/sema.go b/cmd/ctrld/sema.go new file mode 100644 index 0000000..8faa9d2 --- /dev/null +++ b/cmd/ctrld/sema.go @@ -0,0 +1,24 @@ +package main + +type semaphore interface { + acquire() + release() +} + +type noopSemaphore struct{} + +func (n noopSemaphore) acquire() {} + +func (n noopSemaphore) release() {} + +type chanSemaphore struct { + ready chan struct{} +} + +func (c *chanSemaphore) acquire() { + c.ready <- struct{}{} +} + +func (c *chanSemaphore) release() { + <-c.ready +} diff --git a/config.go b/config.go index bdd335b..6fa54e4 100644 --- a/config.go +++ b/config.go @@ -122,14 +122,15 @@ func (c *Config) HasUpstreamSendClientInfo() bool { // ServiceConfig specifies the general ctrld config. type ServiceConfig struct { - LogLevel string `mapstructure:"log_level" toml:"log_level,omitempty"` - LogPath string `mapstructure:"log_path" toml:"log_path,omitempty"` - CacheEnable bool `mapstructure:"cache_enable" toml:"cache_enable,omitempty"` - CacheSize int `mapstructure:"cache_size" toml:"cache_size,omitempty"` - CacheTTLOverride int `mapstructure:"cache_ttl_override" toml:"cache_ttl_override,omitempty"` - CacheServeStale bool `mapstructure:"cache_serve_stale" toml:"cache_serve_stale,omitempty"` - Daemon bool `mapstructure:"-" toml:"-"` - AllocateIP bool `mapstructure:"-" toml:"-"` + LogLevel string `mapstructure:"log_level" toml:"log_level,omitempty"` + LogPath string `mapstructure:"log_path" toml:"log_path,omitempty"` + CacheEnable bool `mapstructure:"cache_enable" toml:"cache_enable,omitempty"` + CacheSize int `mapstructure:"cache_size" toml:"cache_size,omitempty"` + CacheTTLOverride int `mapstructure:"cache_ttl_override" toml:"cache_ttl_override,omitempty"` + CacheServeStale bool `mapstructure:"cache_serve_stale" toml:"cache_serve_stale,omitempty"` + MaxConcurrentRequests *int `mapstructure:"max_concurrent_requests" toml:"max_concurrent_requests,omitempty" validate:"omitempty,gte=0"` + Daemon bool `mapstructure:"-" toml:"-"` + AllocateIP bool `mapstructure:"-" toml:"-"` } // NetworkConfig specifies configuration for networks where ctrld will handle requests. diff --git a/config_test.go b/config_test.go index ddbc97b..27f9d40 100644 --- a/config_test.go +++ b/config_test.go @@ -75,6 +75,7 @@ func TestConfigValidation(t *testing.T) { {"os upstream", configWithOsUpstream(t), false}, {"invalid rules", configWithInvalidRules(t), true}, {"invalid dns rcodes", configWithInvalidRcodes(t), true}, + {"invalid max concurrent requests", configWithInvalidMaxConcurrentRequests(t), true}, } for _, tc := range tests { @@ -176,3 +177,10 @@ func configWithInvalidRcodes(t *testing.T) *ctrld.Config { } return cfg } + +func configWithInvalidMaxConcurrentRequests(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + n := -1 + cfg.Service.MaxConcurrentRequests = &n + return cfg +} diff --git a/docs/config.md b/docs/config.md index f699b4b..8af8e40 100644 --- a/docs/config.md +++ b/docs/config.md @@ -157,6 +157,14 @@ stale cached records (regardless of their TTLs) until upstream comes online. - Required: no - Default: false +### max_concurrent_requests +The number of concurrent requests that will be handled, must be a non-negative integer. +Tweaking this value depends on the capacity of your system. + +- Type: number +- Required: no +- Default: 256 + ## Upstream The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to. From 48b2031269b949294b13ddb0b34058e50e117303 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 16 Jun 2023 19:09:40 +0700 Subject: [PATCH 05/84] internal/net: make ParallelDialer closes un-used conn So the connection can be reclaimed more quickly, reduce resources usage of ctrld, improving the performance a bit on low capacity devices. --- internal/net/net.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/net/net.go b/internal/net/net.go index 1c43bbb..9b47f2d 100644 --- a/internal/net/net.go +++ b/internal/net/net.go @@ -110,6 +110,8 @@ func (d *ParallelDialer) DialContext(ctx context.Context, network string, addrs ctx, cancel := context.WithCancel(ctx) defer cancel() + done := make(chan struct{}) + defer close(done) ch := make(chan *parallelDialerResult, len(addrs)) var wg sync.WaitGroup wg.Add(len(addrs)) @@ -122,7 +124,13 @@ func (d *ParallelDialer) DialContext(ctx context.Context, network string, addrs go func(addr string) { defer wg.Done() conn, err := d.Dialer.DialContext(ctx, network, addr) - ch <- ¶llelDialerResult{conn: conn, err: err} + select { + case ch <- ¶llelDialerResult{conn: conn, err: err}: + case <-done: + if conn != nil { + conn.Close() + } + } }(addr) } @@ -134,6 +142,5 @@ func (d *ParallelDialer) DialContext(ctx context.Context, network string, addrs } errs = append(errs, res.err) } - return nil, errors.Join(errs...) } From c315d21be9bcf778ecbacbca702220821b04a39f Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 16 Jun 2023 20:04:54 +0700 Subject: [PATCH 06/84] cmd/ctrld: do not retry failed query Most the client will retry failed request itself. Doing this on the server give no benefit, and could cause un-necessary load when the server is busy. --- cmd/ctrld/dns_proxy.go | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 366fafb..776224f 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -244,15 +244,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i } } answer, err := resolve1(n, upstreamConfig, msg) - // Only do re-bootstrapping if bootstrap ip is not explicitly set by user. - if err != nil && upstreamConfig.BootstrapIP == "" { - ctrld.Log(ctx, mainLog.Debug().Err(err), "could not resolve query on first attempt, retrying...") - // If any error occurred, re-bootstrap transport/ip, retry the request. - upstreamConfig.ReBootstrap() - answer, err = resolve1(n, upstreamConfig, msg) - if err == nil { - return answer - } + if err != nil { ctrld.Log(ctx, mainLog.Error().Err(err), "failed to resolve query") return nil } From 32482809b7a929419a0012997af19cf6d8d4aa8d Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 16 Jun 2023 20:11:01 +0700 Subject: [PATCH 07/84] Rework DoH/DoH3 transport setup/bootstrapping The current transport setup is using mutex lock for synchronization. This could work ok in normal device, but on low capacity routers, this high contention may affect the performance, causing ctrld hangs. Instead of using mutex lock, using atomic operation for synchronization yield a better performance: - There's no lock, so other requests won't be blocked. And even theses requests use old broken transport, it would be fine, because the client will retry them later. - The setup transport is now done once, on demand when the transport is accessed, or when signal rebootsrapping. The first call to dohTransport will block others, but the transport is warmup before ctrld start serving requests, so client requests won't be affected. That helps ctrld handling the requests better when running on low capacity device. Further more, the transport configuration is also tweaked for better default performance: - MaxIdleConnsPerHost is set to 100 (default is 2), which allows more connections to be reused, reduce the load to open/close connections on demand. See [1] for a real example. - Due to the raising of MaxIdleConnsPerHost, once the transport is GC-ed, it must explicitly close its idle connections. - TLS client session cache is now enabled. Last but not least, the upstream ping process is also reworked. DoH transport is an HTTP transport, so doing a HEAD request is enough to warmup the transport, instead of doing a full DNS query. [1]: https://gitlab.com/gitlab-org/gitlab-pages/-/merge_requests/274 --- cmd/ctrld/prog.go | 2 +- config.go | 111 ++++++++++++++++++++++++-------------------- config_quic.go | 51 ++++++++++---------- config_quic_free.go | 1 - 4 files changed, 86 insertions(+), 79 deletions(-) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 5ec372c..2f17f82 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -91,7 +91,7 @@ func (p *prog) run() { mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Using bootstrap IP for upstream.%s", n) } uc.SetCertPool(rootCertPool) - uc.SetupTransport() + go uc.Ping() } go p.watchLinkState() diff --git a/config.go b/config.go index 6fa54e4..ac6904b 100644 --- a/config.go +++ b/config.go @@ -5,13 +5,16 @@ import ( "crypto/tls" "crypto/x509" "errors" + "io" "math/rand" "net" "net/http" "net/url" "os" + "runtime" "strings" "sync" + "sync/atomic" "time" "github.com/go-playground/validator/v10" @@ -154,11 +157,12 @@ type UpstreamConfig struct { SendClientInfo *bool `mapstructure:"send_client_info" toml:"send_client_info,omitempty"` g singleflight.Group - mu sync.Mutex + rebootstrap atomic.Bool bootstrapIPs []string bootstrapIPs4 []string bootstrapIPs6 []string transport *http.Transport + transportOnce sync.Once transport4 *http.Transport transport6 *http.Transport http3RoundTripper http.RoundTripper @@ -306,20 +310,11 @@ func (uc *UpstreamConfig) ReBootstrap() { } _, _, _ = uc.g.Do("ReBootstrap", func() (any, error) { ProxyLog.Debug().Msg("re-bootstrapping upstream ip") - uc.setupTransportWithoutPingUpstream() + uc.rebootstrap.Store(true) return true, nil }) } -func (uc *UpstreamConfig) setupTransportWithoutPingUpstream() { - switch uc.Type { - case ResolverTypeDOH: - uc.setupDOHTransportWithoutPingUpstream() - case ResolverTypeDOH3: - uc.setupDOH3TransportWithoutPingUpstream() - } -} - // SetupTransport initializes the network transport used to connect to upstream server. // For now, only DoH upstream is supported. func (uc *UpstreamConfig) SetupTransport() { @@ -332,14 +327,31 @@ func (uc *UpstreamConfig) SetupTransport() { } func (uc *UpstreamConfig) setupDOHTransport() { - uc.setupDOHTransportWithoutPingUpstream() - go uc.pingUpstream() + switch uc.IPStack { + case IpStackBoth, "": + uc.transport = uc.newDOHTransport(uc.bootstrapIPs) + case IpStackV4: + uc.transport = uc.newDOHTransport(uc.bootstrapIPs4) + case IpStackV6: + uc.transport = uc.newDOHTransport(uc.bootstrapIPs6) + case IpStackSplit: + uc.transport4 = uc.newDOHTransport(uc.bootstrapIPs4) + if hasIPv6() { + uc.transport6 = uc.newDOHTransport(uc.bootstrapIPs6) + } else { + uc.transport6 = uc.transport4 + } + uc.transport = uc.newDOHTransport(uc.bootstrapIPs) + } } func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport { transport := http.DefaultTransport.(*http.Transport).Clone() - transport.IdleConnTimeout = 5 * time.Second - transport.TLSClientConfig = &tls.Config{RootCAs: uc.certPool} + transport.MaxIdleConnsPerHost = 100 + transport.TLSClientConfig = &tls.Config{ + RootCAs: uc.certPool, + ClientSessionCache: tls.NewLRUClientSessionCache(0), + } dialerTimeoutMs := 2000 if uc.Timeout > 0 && uc.Timeout < dialerTimeoutMs { @@ -368,44 +380,39 @@ func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport { Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", conn.RemoteAddr()) return conn, nil } + runtime.SetFinalizer(transport, func(transport *http.Transport) { + transport.CloseIdleConnections() + }) return transport } -func (uc *UpstreamConfig) setupDOHTransportWithoutPingUpstream() { - uc.mu.Lock() - defer uc.mu.Unlock() - switch uc.IPStack { - case IpStackBoth, "": - uc.transport = uc.newDOHTransport(uc.bootstrapIPs) - case IpStackV4: - uc.transport = uc.newDOHTransport(uc.bootstrapIPs4) - case IpStackV6: - uc.transport = uc.newDOHTransport(uc.bootstrapIPs6) - case IpStackSplit: - uc.transport4 = uc.newDOHTransport(uc.bootstrapIPs4) - if hasIPv6() { - uc.transport6 = uc.newDOHTransport(uc.bootstrapIPs6) - } else { - uc.transport6 = uc.transport4 - } - - uc.transport = uc.newDOHTransport(uc.bootstrapIPs) - } -} - -func (uc *UpstreamConfig) pingUpstream() { - // Warming up the transport by querying a test packet. - dnsResolver, err := NewResolver(uc) - if err != nil { - ProxyLog.Error().Err(err).Msgf("failed to create resolver for upstream: %s", uc.Name) +// Ping warms up the connection to DoH/DoH3 upstream. +func (uc *UpstreamConfig) Ping() { + switch uc.Type { + case ResolverTypeDOH, ResolverTypeDOH3: + default: return } - msg := new(dns.Msg) - msg.SetQuestion(".", dns.TypeNS) - msg.MsgHdr.RecursionDesired = true - ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - _, _ = dnsResolver.Resolve(ctx, msg) + + ping := func(t http.RoundTripper) { + if t == nil { + return + } + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + req, _ := http.NewRequestWithContext(ctx, "HEAD", uc.Endpoint, nil) + resp, _ := t.RoundTrip(req) + if resp == nil { + return + } + defer resp.Body.Close() + _, _ = io.Copy(io.Discard, resp.Body) + } + + for _, typ := range []uint16{dns.TypeA, dns.TypeAAAA} { + ping(uc.dohTransport(typ)) + ping(uc.doh3Transport(typ)) + } } func (uc *UpstreamConfig) isControlD() bool { @@ -424,8 +431,12 @@ func (uc *UpstreamConfig) isControlD() bool { } func (uc *UpstreamConfig) dohTransport(dnsType uint16) http.RoundTripper { - uc.mu.Lock() - defer uc.mu.Unlock() + uc.transportOnce.Do(func() { + uc.SetupTransport() + }) + if uc.rebootstrap.CompareAndSwap(true, false) { + uc.SetupTransport() + } switch uc.IPStack { case IpStackBoth, IpStackV4, IpStackV6: return uc.transport diff --git a/config_quic.go b/config_quic.go index 085476e..32d338e 100644 --- a/config_quic.go +++ b/config_quic.go @@ -19,8 +19,24 @@ import ( ) func (uc *UpstreamConfig) setupDOH3Transport() { - uc.setupDOH3TransportWithoutPingUpstream() - go uc.pingUpstream() + switch uc.IPStack { + case IpStackBoth, "": + uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs) + case IpStackV4: + uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs4) + case IpStackV6: + uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs6) + case IpStackSplit: + uc.http3RoundTripper4 = uc.newDOH3Transport(uc.bootstrapIPs4) + ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) + defer cancel() + if ctrldnet.IPv6Available(ctx) { + uc.http3RoundTripper6 = uc.newDOH3Transport(uc.bootstrapIPs6) + } else { + uc.http3RoundTripper6 = uc.http3RoundTripper4 + } + uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs) + } } func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper { @@ -58,32 +74,13 @@ func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper { return rt } -func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() { - uc.mu.Lock() - defer uc.mu.Unlock() - switch uc.IPStack { - case IpStackBoth, "": - uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs) - case IpStackV4: - uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs4) - case IpStackV6: - uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs6) - case IpStackSplit: - uc.http3RoundTripper4 = uc.newDOH3Transport(uc.bootstrapIPs4) - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - defer cancel() - if ctrldnet.IPv6Available(ctx) { - uc.http3RoundTripper6 = uc.newDOH3Transport(uc.bootstrapIPs6) - } else { - uc.http3RoundTripper6 = uc.http3RoundTripper4 - } - uc.http3RoundTripper = uc.newDOH3Transport(uc.bootstrapIPs) - } -} - func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper { - uc.mu.Lock() - defer uc.mu.Unlock() + uc.transportOnce.Do(func() { + uc.SetupTransport() + }) + if uc.rebootstrap.CompareAndSwap(true, false) { + uc.SetupTransport() + } switch uc.IPStack { case IpStackBoth, IpStackV4, IpStackV6: return uc.http3RoundTripper diff --git a/config_quic_free.go b/config_quic_free.go index a4b1bdd..a674a1b 100644 --- a/config_quic_free.go +++ b/config_quic_free.go @@ -6,5 +6,4 @@ import "net/http" func (uc *UpstreamConfig) setupDOH3Transport() {} -func (uc *UpstreamConfig) setupDOH3TransportWithoutPingUpstream() {} func (uc *UpstreamConfig) doh3Transport(dnsType uint16) http.RoundTripper { return nil } From 67e4afc06e75386572ffb09f20f36a394635f525 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 16 Jun 2023 20:56:21 +0700 Subject: [PATCH 08/84] cmd/ctrld: improving ctrld stability on router The current state of ctrld is very "high stakes" and easy to mess up, and is unforgiving when "ctrld start" failed. That would cause the router is in broken state, unrecoverable. This commit makes these changes to improve the state: - Moving router setup process after ctrld listeners are ready, so dnsmasq won't flood requests to ctrld even though the listeners are not ready to serve requests. - On router, when ctrld stopped, restore router DNS setup. That leaves the router in good state on reboot/startup, help removing the custom DNS server for NTP synchronization on some routers. - If self-check failed, uninstall ctrld to restore router to good state, prevent confusion that ctrld process is still running even though self-check reports it did not started. --- cmd/ctrld/cli.go | 104 +++++++++++++++++++++++--------------- cmd/ctrld/dns_proxy.go | 83 +++++++++++++++--------------- cmd/ctrld/prog.go | 18 +++++-- cmd/ctrld/prog_darwin.go | 11 ++-- cmd/ctrld/prog_freebsd.go | 2 - cmd/ctrld/prog_linux.go | 2 - cmd/ctrld/prog_others.go | 2 - internal/router/router.go | 2 +- 8 files changed, 121 insertions(+), 103 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 85b7f85..5e16aae 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -123,14 +123,14 @@ func initCLI() { waitCh := make(chan struct{}) stopCh := make(chan struct{}) + p := &prog{ + waitCh: waitCh, + stopCh: stopCh, + } if !daemon { // We need to call s.Run() as soon as possible to response to the OS manager, so it // can see ctrld is running and don't mark ctrld as failed service. go func() { - p := &prog{ - waitCh: waitCh, - stopCh: stopCh, - } s, err := newService(p, svcConfig) if err != nil { mainLog.Fatal().Err(err).Msg("failed create new service") @@ -163,14 +163,11 @@ func initCLI() { // so it's able to log information in processCDFlags. initLogging() - if setupRouter { - s, errCh := runDNSServerForNTPD(router.ListenAddress()) - if err := router.PreRun(); err != nil { - mainLog.Fatal().Err(err).Msg("failed to perform router pre-start check") - } - if err := s.Shutdown(); err != nil && errCh != nil { - mainLog.Fatal().Err(err).Msg("failed to shutdown dns server for ntpd") - } + // Processing --cd flag require connecting to ControlD API, which needs valid + // time for validating server certificate. Some routers need NTP synchronization + // to set the current time, so this check must happen before processCDFlags. + if err := router.PreRun(); err != nil { + mainLog.Fatal().Err(err).Msg("failed to perform router pre-run check") } processCDFlags() @@ -207,20 +204,34 @@ func initCLI() { rootCertPool = certs.CACertPool() fallthrough case platform != "": - mainLog.Debug().Msg("Router setup") - err := router.Configure(&cfg) - if errors.Is(err, router.ErrNotSupported) { + if !router.IsSupported(platform) { unsupportedPlatformHelp(cmd) os.Exit(1) } - if err != nil { - mainLog.Fatal().Err(err).Msg("failed to configure router") - } + p.onStarted = append(p.onStarted, func() { + mainLog.Debug().Msg("Router setup") + if err := router.Configure(&cfg); err != nil { + mainLog.Error().Err(err).Msg("could not configure router") + } + }) + p.onStopped = append(p.onStopped, func() { + mainLog.Debug().Msg("Router cleanup") + if err := router.Cleanup(svcConfig); err != nil { + mainLog.Error().Err(err).Msg("could not cleanup router") + } + if err := router.Stop(); err != nil { + mainLog.Error().Err(err).Msg("problem occurred while stopping router") + } + p.resetDNS() + }) } } close(waitCh) <-stopCh + for _, f := range p.onStopped { + f() + } }, } runCmd.Flags().BoolVarP(&daemon, "daemon", "d", false, "Run as daemon") @@ -303,12 +314,16 @@ func initCLI() { sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile) } - prog := &prog{} - s, err := newService(prog, sc) + p := &prog{} + s, err := newService(p, sc) if err != nil { mainLog.Error().Msg(err.Error()) return } + + mainLog.Debug().Msg("cleaning up router before installing") + _ = router.Cleanup(svcConfig) + tasks := []task{ {s.Stop, false}, {s.Uninstall, false}, @@ -333,12 +348,10 @@ func initCLI() { mainLog.Notice().Msg("Service started") default: mainLog.Error().Msg("Service did not start, please check system/service log for details error") - if runtime.GOOS == "linux" { - prog.resetDNS() - } + uninstall(p, s) os.Exit(1) } - prog.setDNS() + p.setDNS() } }, } @@ -454,29 +467,16 @@ func initCLI() { NOTE: Uninstalling will set DNS to values provided by DHCP.`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - prog := &prog{} - s, err := newService(prog, svcConfig) + p := &prog{} + s, err := newService(p, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) return } - tasks := []task{ - {s.Stop, false}, - {s.Uninstall, true}, - } - initLogging() - if doTasks(tasks) { - if iface == "" { - iface = "auto" - } - prog.resetDNS() - mainLog.Debug().Msg("Router cleanup") - if err := router.Cleanup(svcConfig); err != nil { - mainLog.Warn().Err(err).Msg("could not cleanup router") - } - mainLog.Notice().Msg("Service uninstalled") - return + if iface == "" { + iface = "auto" } + uninstall(p, s) }, } uninstallCmd.Flags().StringVarP(&iface, "iface", "", "", `Reset DNS setting for iface, use "auto" for the default gateway interface`) @@ -951,3 +951,23 @@ func tryReadingConfig(writeDefaultConfig bool) { } } } + +func uninstall(p *prog, s service.Service) { + tasks := []task{ + {s.Stop, false}, + {s.Uninstall, true}, + } + initLogging() + if doTasks(tasks) { + // Stop already reset DNS on router. + if router.Name() == "" { + p.resetDNS() + } + mainLog.Debug().Msg("Router cleanup") + // Stop already did router.Cleanup and report any error if happens, + // ignoring error here to prevent false positive. + _ = router.Cleanup(svcConfig) + mainLog.Notice().Msg("Service uninstalled") + return + } +} diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 776224f..626c329 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -5,7 +5,9 @@ import ( "crypto/rand" "encoding/hex" "fmt" + "io" "net" + "os" "runtime" "strconv" "strings" @@ -13,7 +15,9 @@ import ( "time" "github.com/miekg/dns" + "go4.org/mem" "golang.org/x/sync/errgroup" + "tailscale.com/util/lineread" "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/dnscache" @@ -101,6 +105,12 @@ func (p *prog) serveDNS(listenerNum string) error { } } select { + case err := <-errCh: + return err + case <-time.After(5 * time.Second): + p.started <- struct{}{} + } + select { case <-ctx.Done(): return nil case err := <-errCh: @@ -463,50 +473,37 @@ func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-cha return s, errCh } -// runDNSServerForNTPD starts a DNS server listening on router.ListenAddress(). It must only be called when ctrld -// running on router, before router.PreRun() to serve DNS request for NTP synchronization. The caller must call -// s.Shutdown() explicitly when NTP is synced successfully. -func runDNSServerForNTPD(addr string) (*dns.Server, <-chan error) { - if addr == "" { - return &dns.Server{}, nil - } - dnsResolver := ctrld.NewBootstrapResolver() - s := &dns.Server{ - Addr: addr, - Net: "udp", - Handler: dns.HandlerFunc(func(w dns.ResponseWriter, m *dns.Msg) { - mainLog.Debug().Msg("Serving query for ntpd") - resolveCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - if osUpstreamConfig.Timeout > 0 { - timeoutCtx, cancel := context.WithTimeout(resolveCtx, time.Millisecond*time.Duration(osUpstreamConfig.Timeout)) - defer cancel() - resolveCtx = timeoutCtx - } - answer, err := dnsResolver.Resolve(resolveCtx, m) - if err != nil { - mainLog.Error().Err(err).Msgf("could not resolve: %v", m) - return - } - if err := w.WriteMsg(answer); err != nil { - mainLog.Error().Err(err).Msg("runDNSServerForNTPD: failed to send DNS response") - } - }), +// inContainer reports whether we're running in a container. +// +// Copied from https://github.com/tailscale/tailscale/blob/v1.42.0/hostinfo/hostinfo.go#L260 +// with modification for ctrld usage. +func inContainer() bool { + if runtime.GOOS != "linux" { + return false } - waitLock := sync.Mutex{} - waitLock.Lock() - s.NotifyStartedFunc = waitLock.Unlock - - errCh := make(chan error) - go func() { - defer close(errCh) - if err := s.ListenAndServe(); err != nil { - waitLock.Unlock() - mainLog.Error().Err(err).Msgf("could not listen and serve on: %s", s.Addr) - errCh <- err + var ret bool + if _, err := os.Stat("/.dockerenv"); err == nil { + return true + } + if _, err := os.Stat("/run/.containerenv"); err == nil { + // See https://github.com/cri-o/cri-o/issues/5461 + return true + } + lineread.File("/proc/1/cgroup", func(line []byte) error { + if mem.Contains(mem.B(line), mem.S("/docker/")) || + mem.Contains(mem.B(line), mem.S("/lxc/")) { + ret = true + return io.EOF // arbitrary non-nil error to stop loop } - }() - waitLock.Lock() - return s, errCh + return nil + }) + lineread.File("/proc/mounts", func(line []byte) error { + if mem.Contains(mem.B(line), mem.S("fuse.lxcfs")) { + ret = true + return io.EOF + } + return nil + }) + return ret } diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 2f17f82..5e563bf 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -39,6 +39,10 @@ type prog struct { cfg *ctrld.Config cache dnscache.Cacher sema semaphore + + started chan struct{} + onStarted []func() + onStopped []func() } func (p *prog) Start(s service.Service) error { @@ -51,6 +55,8 @@ func (p *prog) run() { // Wait the caller to signal that we can do our logic. <-p.waitCh p.preRun() + numListeners := len(p.cfg.Listener) + p.started = make(chan struct{}, numListeners) if p.cfg.Service.CacheEnable { cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize) if err != nil { @@ -143,20 +149,22 @@ func (p *prog) run() { }(listenerNum) } + for i := 0; i < numListeners; i++ { + <-p.started + } + for _, f := range p.onStarted { + f() + } wg.Wait() } func (p *prog) Stop(s service.Service) error { + close(p.stopCh) if err := p.deAllocateIP(); err != nil { mainLog.Error().Err(err).Msg("de-allocate ip failed") return err } - p.preStop() - if err := router.Stop(); err != nil { - mainLog.Warn().Err(err).Msg("problem occurred while stopping router") - } mainLog.Info().Msg("Service stopped") - close(p.stopCh) return nil } diff --git a/cmd/ctrld/prog_darwin.go b/cmd/ctrld/prog_darwin.go index 2b82eb5..4d9ad0a 100644 --- a/cmd/ctrld/prog_darwin.go +++ b/cmd/ctrld/prog_darwin.go @@ -8,6 +8,11 @@ func (p *prog) preRun() { if !service.Interactive() { p.setDNS() } + p.onStopped = append(p.onStopped, func() { + if !service.Interactive() { + p.resetDNS() + } + }) } func setDependencies(svc *service.Config) {} @@ -15,9 +20,3 @@ func setDependencies(svc *service.Config) {} func setWorkingDirectory(svc *service.Config, dir string) { svc.WorkingDirectory = dir } - -func (p *prog) preStop() { - if !service.Interactive() { - p.resetDNS() - } -} diff --git a/cmd/ctrld/prog_freebsd.go b/cmd/ctrld/prog_freebsd.go index 24a90ba..63d8179 100644 --- a/cmd/ctrld/prog_freebsd.go +++ b/cmd/ctrld/prog_freebsd.go @@ -18,5 +18,3 @@ func setDependencies(svc *service.Config) { } func setWorkingDirectory(svc *service.Config, dir string) {} - -func (p *prog) preStop() {} diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 0b49a33..36bc316 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -31,5 +31,3 @@ func setDependencies(svc *service.Config) { func setWorkingDirectory(svc *service.Config, dir string) { svc.WorkingDirectory = dir } - -func (p *prog) preStop() {} diff --git a/cmd/ctrld/prog_others.go b/cmd/ctrld/prog_others.go index b26c0b6..50fcf0d 100644 --- a/cmd/ctrld/prog_others.go +++ b/cmd/ctrld/prog_others.go @@ -12,5 +12,3 @@ func setWorkingDirectory(svc *service.Config, dir string) { // WorkingDirectory is not supported on Windows. svc.WorkingDirectory = dir } - -func (p *prog) preStop() {} diff --git a/internal/router/router.go b/internal/router/router.go index f9b8be8..4916db4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -118,7 +118,7 @@ func PreRun() (err error) { switch Name() { case Merlin, Tomato: // Wait until `ntp_ready=1` set. - b := backoff.NewBackoff("PreStart", func(format string, args ...any) {}, 10*time.Second) + b := backoff.NewBackoff("PreRun", func(format string, args ...any) {}, 10*time.Second) for { out, err := nvram("get", "ntp_ready") if err != nil { From 03781d4cec95fd201cce13eebc808cb9955b0628 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 20 Jun 2023 18:09:06 +0700 Subject: [PATCH 09/84] internal/router: add UniFi Gateway support UniFi Gateway (USG) uses its own DNS forwarding rule, which is configured default in /etc/dnsmasq.conf file. Adding ctrld own config in /etc/dnsmasq.d won't take effects. Instead, we must make changes directly to /etc/dnsmasq.conf, configuring ctrld as the only upstream. --- internal/router/edgeos.go | 94 ++++++++++++++++++++++++++++++++++++--- internal/router/router.go | 7 +++ 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/internal/router/edgeos.go b/internal/router/edgeos.go index ccdb164..f447608 100644 --- a/internal/router/edgeos.go +++ b/internal/router/edgeos.go @@ -1,37 +1,119 @@ package router import ( + "bufio" + "bytes" "fmt" "os" "os/exec" + "strings" ) -const edgeOSDNSMasqConfigPath = "/etc/dnsmasq.d/dnsmasq-zzz-ctrld.conf" +const ( + edgeOSDNSMasqConfigPath = "/etc/dnsmasq.d/dnsmasq-zzz-ctrld.conf" + UsgDNSMasqConfigPath = "/etc/dnsmasq.conf" + UsgDNSMasqBackupConfigPath = "/etc/dnsmasq.conf.bak" +) + +var ( + isUSG bool +) func setupEdgeOS() error { + if isUSG { + return setupUSG() + } + return setupUDM() +} + +func setupUDM() error { // Disable dnsmasq as DNS server. dnsMasqConfigContent, err := dnsMasqConf() if err != nil { - return err + return fmt.Errorf("setupUDM: generating dnsmasq config: %w", err) } if err := os.WriteFile(edgeOSDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { - return err + return fmt.Errorf("setupUDM: generating dnsmasq config: %w", err) } // Restart dnsmasq service. if err := restartDNSMasq(); err != nil { - return err + return fmt.Errorf("setupUDM: restartDNSMasq: %w", err) + } + return nil +} + +func setupUSG() error { + // On USG, dnsmasq is configured to forward queries to external provider by default. + // So instead of generating config in /etc/dnsmasq.d, we need to create a backup of + // the config, then modify it to forward queries to ctrld listener. + + // Creating a backup. + buf, err := os.ReadFile(UsgDNSMasqConfigPath) + if err != nil { + return fmt.Errorf("setupUSG: reading current config: %w", err) + } + if err := os.WriteFile(UsgDNSMasqBackupConfigPath, buf, 0600); err != nil { + return fmt.Errorf("setupUSG: backup current config: %w", err) + } + + // Removing all configured upstreams. + var sb strings.Builder + scanner := bufio.NewScanner(bytes.NewReader(buf)) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "server=") { + continue + } + if strings.HasPrefix(line, "all-servers") { + continue + } + sb.WriteString(line) + } + + // Adding ctrld listener as the only upstream. + dnsMasqConfigContent, err := dnsMasqConf() + if err != nil { + return fmt.Errorf("setupUSG: generating dnsmasq config: %w", err) + } + sb.WriteString("\n") + sb.WriteString(dnsMasqConfigContent) + if err := os.WriteFile(UsgDNSMasqConfigPath, []byte(sb.String()), 0644); err != nil { + return fmt.Errorf("setupUSG: writing dnsmasq config: %w", err) + } + + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return fmt.Errorf("setupUSG: restartDNSMasq: %w", err) } return nil } func cleanupEdgeOS() error { + if isUSG { + return cleanupUSG() + } + return cleanupUDM() +} + +func cleanupUDM() error { // Remove the custom dnsmasq config if err := os.Remove(edgeOSDNSMasqConfigPath); err != nil { - return err + return fmt.Errorf("cleanupUDM: os.Remove: %w", err) } // Restart dnsmasq service. if err := restartDNSMasq(); err != nil { - return err + return fmt.Errorf("cleanupUDM: restartDNSMasq: %w", err) + } + return nil +} + +func cleanupUSG() error { + if err := os.Rename(UsgDNSMasqBackupConfigPath, UsgDNSMasqConfigPath); err != nil { + return fmt.Errorf("cleanupUSG: os.Rename: %w", err) + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return fmt.Errorf("cleanupUSG: restartDNSMasq: %w", err) } return nil } diff --git a/internal/router/router.go b/internal/router/router.go index 4916db4..29a95d5 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -220,8 +220,10 @@ func distroName() string { case bytes.HasPrefix(unameO(), []byte("Tomato")): return Tomato case haveDir("/config/scripts/post-config.d"): + checkUSG() return EdgeOS case haveFile("/etc/ubnt/init/vyatta-router"): + checkUSG() return EdgeOS // For 2.x case isPfsense(): return Pfsense @@ -253,3 +255,8 @@ func isPfsense() bool { b, err := os.ReadFile("/etc/platform") return err == nil && bytes.HasPrefix(b, []byte("pfSense")) } + +func checkUSG() { + out, _ := exec.Command("mca-cli-op", "info").Output() + isUSG = bytes.Contains(out, []byte("UniFi-Gateway-")) +} From 350d8355b18c864bbbeec0cc6591a07b66952531 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 21 Jun 2023 00:28:33 +0700 Subject: [PATCH 10/84] all: add firewalla support --- cmd/ctrld/prog_linux.go | 7 +++ internal/router/client_info.go | 21 +++---- internal/router/dnsmasq.go | 11 +++- internal/router/firewalla.go | 103 +++++++++++++++++++++++++++++++++ internal/router/router.go | 84 +++++++++++++++++++-------- 5 files changed, 188 insertions(+), 38 deletions(-) create mode 100644 internal/router/firewalla.go diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 36bc316..7dc14d9 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -26,6 +26,13 @@ func setDependencies(svc *service.Config) { svc.Dependencies = append(svc.Dependencies, "Wants=dnsmasq.service") svc.Dependencies = append(svc.Dependencies, "After=dnsmasq.service") } + // On Firewalla, ctrld needs to start after firerouter_{dhcp,dns}, so it can read leases file. + if router.Name() == router.Firewalla { + svc.Dependencies = append(svc.Dependencies, "Wants=firerouter_dhcp.service") + svc.Dependencies = append(svc.Dependencies, "After=firerouter_dhcp.service") + svc.Dependencies = append(svc.Dependencies, "Wants=firerouter_dns.service") + svc.Dependencies = append(svc.Dependencies, "After=firerouter_dns.service") + } } func setWorkingDirectory(svc *service.Config, dir string) { diff --git a/internal/router/client_info.go b/internal/router/client_info.go index 8fc5709..c708b75 100644 --- a/internal/router/client_info.go +++ b/internal/router/client_info.go @@ -21,16 +21,17 @@ type readClientInfoFunc func(name string) error // clientInfoFiles specifies client info files and how to read them on supported platforms. var clientInfoFiles = map[string]readClientInfoFunc{ - "/tmp/dnsmasq.leases": dnsmasqReadClientInfoFile, // ddwrt - "/tmp/dhcp.leases": dnsmasqReadClientInfoFile, // openwrt - "/var/lib/misc/dnsmasq.leases": dnsmasqReadClientInfoFile, // merlin - "/mnt/data/udapi-config/dnsmasq.lease": dnsmasqReadClientInfoFile, // UDM Pro - "/data/udapi-config/dnsmasq.lease": dnsmasqReadClientInfoFile, // UDR - "/etc/dhcpd/dhcpd-leases.log": dnsmasqReadClientInfoFile, // Synology - "/tmp/var/lib/misc/dnsmasq.leases": dnsmasqReadClientInfoFile, // Tomato - "/run/dnsmasq-dhcp.leases": dnsmasqReadClientInfoFile, // EdgeOS - "/run/dhcpd.leases": iscDHCPReadClientInfoFile, // EdgeOS - "/var/dhcpd/var/db/dhcpd.leases": iscDHCPReadClientInfoFile, // Pfsense + "/tmp/dnsmasq.leases": dnsmasqReadClientInfoFile, // ddwrt + "/tmp/dhcp.leases": dnsmasqReadClientInfoFile, // openwrt + "/var/lib/misc/dnsmasq.leases": dnsmasqReadClientInfoFile, // merlin + "/mnt/data/udapi-config/dnsmasq.lease": dnsmasqReadClientInfoFile, // UDM Pro + "/data/udapi-config/dnsmasq.lease": dnsmasqReadClientInfoFile, // UDR + "/etc/dhcpd/dhcpd-leases.log": dnsmasqReadClientInfoFile, // Synology + "/tmp/var/lib/misc/dnsmasq.leases": dnsmasqReadClientInfoFile, // Tomato + "/run/dnsmasq-dhcp.leases": dnsmasqReadClientInfoFile, // EdgeOS + "/run/dhcpd.leases": iscDHCPReadClientInfoFile, // EdgeOS + "/var/dhcpd/var/db/dhcpd.leases": iscDHCPReadClientInfoFile, // Pfsense + "/home/pi/.router/run/dhcp/dnsmasq.leases": dnsmasqReadClientInfoFile, // Firewalla } // watchClientInfoTable watches changes happens in dnsmasq/dhcpd diff --git a/internal/router/dnsmasq.go b/internal/router/dnsmasq.go index 4d43d20..2ca3249 100644 --- a/internal/router/dnsmasq.go +++ b/internal/router/dnsmasq.go @@ -1,6 +1,7 @@ package router import ( + "fmt" "strings" "text/template" ) @@ -49,8 +50,10 @@ func dnsMasqConf() (string, error) { var sb strings.Builder var tmplText string switch Name() { - case EdgeOS, DDWrt, OpenWrt, Ubios, Synology, Tomato: + case DDWrt, EdgeOS, OpenWrt, Ubios, Synology, Tomato: tmplText = dnsMasqConfigContentTmpl + case Firewalla: + tmplText = dnsMasqConfigContentTmpl + fmt.Sprintf("listen-address=127.0.0.1\n") case Merlin: tmplText = merlinDNSMasqPostConfTmpl } @@ -68,10 +71,12 @@ func dnsMasqConf() (string, error) { func restartDNSMasq() error { switch Name() { - case EdgeOS: - return edgeOSRestartDNSMasq() case DDWrt: return ddwrtRestartDNSMasq() + case EdgeOS: + return edgeOSRestartDNSMasq() + case Firewalla: + return firewallaRestartDNSMasq() case Merlin: return merlinRestartDNSMasq() case OpenWrt: diff --git a/internal/router/firewalla.go b/internal/router/firewalla.go new file mode 100644 index 0000000..7b7e6ea --- /dev/null +++ b/internal/router/firewalla.go @@ -0,0 +1,103 @@ +package router + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" +) + +const ( + firewallaDNSMasqConfigPath = "/home/pi/.firewalla/config/dnsmasq_local" + firewallaDNSMasqBackupConfigPath = "/home/pi/.firewalla/config/dnsmasq_local.bak" + firewallaConfigPostMainDir = "/home/pi/.firewalla/config/post_main.d" + firewallaCtrldInitScriptPath = "/home/pi/.firewalla/config/post_main.d/start_ctrld.sh" +) + +func setupFirewalla() error { + fi, err := os.Stat(firewallaDNSMasqConfigPath) + if err != nil { + return fmt.Errorf("setupFirewalla: get current config directory: %w", err) + } + + _ = os.RemoveAll(firewallaDNSMasqBackupConfigPath) + + // Creating a backup. + if err := os.Rename(firewallaDNSMasqConfigPath, firewallaDNSMasqBackupConfigPath); err != nil { + return fmt.Errorf("setupFirewalla: backup current config: %w", err) + } + + // Creating our own config. + if err := os.MkdirAll(firewallaDNSMasqConfigPath, fi.Mode()); err != nil { + return fmt.Errorf("setupFirewalla: creating config dir: %w", err) + } + + // Adding ctrld listener as the only upstream. + dnsMasqConfigContent, err := dnsMasqConf() + if err != nil { + return fmt.Errorf("setupFirewalla: generating dnsmasq config: %w", err) + } + ctrldConfPath := filepath.Join(firewallaDNSMasqConfigPath, "ctrld") + if err := os.WriteFile(ctrldConfPath, []byte(dnsMasqConfigContent), 0600); err != nil { + return fmt.Errorf("setupFirewalla: writing ctrld config: %w", err) + } + + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return fmt.Errorf("setupFirewalla: restartDNSMasq: %w", err) + } + + return nil +} + +func cleanupFirewalla() error { + // Do nothing if there's no backup config. + if _, err := os.Stat(firewallaDNSMasqBackupConfigPath); err != nil && os.IsNotExist(err) { + return nil + } + + // Removing current config. + if err := os.RemoveAll(firewallaDNSMasqConfigPath); err != nil { + return fmt.Errorf("cleanupFirewalla: removing ctrld config: %w", err) + } + + // Restoring backup. + if err := os.Rename(firewallaDNSMasqBackupConfigPath, firewallaDNSMasqConfigPath); err != nil { + return fmt.Errorf("cleanupFirewalla: restoring backup config: %w", err) + } + + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return fmt.Errorf("cleaupFirewalla: restartDNSMasq: %w", err) + } + + return nil +} + +func postInstallFirewalla() error { + // Writing startup script. + if err := writeFirewallStartupScript(); err != nil { + return fmt.Errorf("postInstallFirewalla: writing startup script: %w", err) + } + return nil +} + +func firewallaRestartDNSMasq() error { + return exec.Command("systemctl", "restart", "firerouter_dns").Run() +} + +func writeFirewallStartupScript() error { + if err := os.MkdirAll(firewallaConfigPostMainDir, 0775); err != nil { + return err + } + exe, err := os.Executable() + if err != nil { + return err + } + // This is called when "ctrld start ..." runs, so recording + // the same command line arguments to use in startup script. + argStr := strings.Join(os.Args[1:], " ") + script := fmt.Sprintf("#!/bin/bash\n\nsudo %q %s\n", exe, argStr) + return os.WriteFile(firewallaCtrldInitScriptPath, []byte(script), 0755) +} diff --git a/internal/router/router.go b/internal/router/router.go index 29a95d5..d3beda7 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -19,14 +19,15 @@ import ( ) const ( - OpenWrt = "openwrt" - DDWrt = "ddwrt" - Merlin = "merlin" - Ubios = "ubios" - Synology = "synology" - Tomato = "tomato" - EdgeOS = "edgeos" - Pfsense = "pfsense" + DDWrt = "ddwrt" + EdgeOS = "edgeos" + Firewalla = "firewalla" + Merlin = "merlin" + OpenWrt = "openwrt" + Pfsense = "pfsense" + Synology = "synology" + Tomato = "tomato" + Ubios = "ubios" ) // ErrNotSupported reports the current router is not supported error. @@ -44,7 +45,15 @@ type router struct { // IsSupported reports whether the given platform is supported by ctrld. func IsSupported(platform string) bool { switch platform { - case EdgeOS, DDWrt, Merlin, OpenWrt, Pfsense, Synology, Tomato, Ubios: + case DDWrt, + EdgeOS, + Firewalla, + Merlin, + OpenWrt, + Pfsense, + Synology, + Tomato, + Ubios: return true } return false @@ -52,25 +61,44 @@ func IsSupported(platform string) bool { // SupportedPlatforms return all platforms that can be configured to run with ctrld. func SupportedPlatforms() []string { - return []string{EdgeOS, DDWrt, Merlin, OpenWrt, Pfsense, Synology, Tomato, Ubios} + return []string{ + DDWrt, + EdgeOS, + Firewalla, + Merlin, + OpenWrt, + Pfsense, + Synology, + Tomato, + Ubios, + } } var configureFunc = map[string]func() error{ - EdgeOS: setupEdgeOS, - DDWrt: setupDDWrt, - Merlin: setupMerlin, - OpenWrt: setupOpenWrt, - Pfsense: setupPfsense, - Synology: setupSynology, - Tomato: setupTomato, - Ubios: setupUbiOS, + DDWrt: setupDDWrt, + EdgeOS: setupEdgeOS, + Firewalla: setupFirewalla, + Merlin: setupMerlin, + OpenWrt: setupOpenWrt, + Pfsense: setupPfsense, + Synology: setupSynology, + Tomato: setupTomato, + Ubios: setupUbiOS, } // Configure configures things for running ctrld on the router. func Configure(c *ctrld.Config) error { name := Name() switch name { - case EdgeOS, DDWrt, Merlin, OpenWrt, Pfsense, Synology, Tomato, Ubios: + case DDWrt, + EdgeOS, + Firewalla, + Merlin, + OpenWrt, + Pfsense, + Synology, + Tomato, + Ubios: if c.HasUpstreamSendClientInfo() { r := routerPlatform.Load() r.sendClientInfo = true @@ -107,7 +135,7 @@ func ConfigureService(sc *service.Config) error { sc.Option["SysvScript"] = openWrtScript case Pfsense: sc.Option["SysvScript"] = pfsenseInitScript - case EdgeOS, Merlin, Synology, Tomato, Ubios: + case EdgeOS, Firewalla, Merlin, Synology, Tomato, Ubios: } return nil } @@ -138,10 +166,12 @@ func PreRun() (err error) { func PostInstall(svc *service.Config) error { name := Name() switch name { - case EdgeOS: - return postInstallEdgeOS() case DDWrt: return postInstallDDWrt() + case EdgeOS: + return postInstallEdgeOS() + case Firewalla: + return postInstallFirewalla() case Merlin: return postInstallMerlin() case OpenWrt: @@ -162,10 +192,12 @@ func PostInstall(svc *service.Config) error { func Cleanup(svc *service.Config) error { name := Name() switch name { - case EdgeOS: - return cleanupEdgeOS() case DDWrt: return cleanupDDWrt() + case EdgeOS: + return cleanupEdgeOS() + case Firewalla: + return cleanupFirewalla() case Merlin: return cleanupMerlin() case OpenWrt: @@ -186,7 +218,7 @@ func Cleanup(svc *service.Config) error { func ListenAddress() string { name := Name() switch name { - case EdgeOS, DDWrt, Merlin, OpenWrt, Synology, Tomato, Ubios: + case DDWrt, Firewalla, Merlin, OpenWrt, Synology, Tomato, Ubios: return "127.0.0.1:5354" case Pfsense: // On pfsense, we run ctrld as DNS resolver. @@ -227,6 +259,8 @@ func distroName() string { return EdgeOS // For 2.x case isPfsense(): return Pfsense + case haveFile("/etc/firewalla_release"): + return Firewalla } return "" } From 50bfed706dec10c1bb98613c47a5996cc9112b5c Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 21 Jun 2023 15:38:08 +0700 Subject: [PATCH 11/84] all: writing correct routers setup to config file When running on routers, ctrld leverages default setup, let dnsmasq runs on port 53, and forward queries to ctrld listener on port 5354. However, this setup is not serialized to config file, causing confusion to users. Fixing this by writing the correct routers setup to config file. While at it, updating documentation to refelct that, and also adding note that changing default router setup could break things. --- README.md | 15 +++++++++++++++ cmd/ctrld/cli.go | 14 +++++++++----- cmd/ctrld/dns_proxy.go | 7 +++++-- docs/config.md | 4 ++-- internal/router/firewalla.go | 2 +- internal/router/router.go | 13 +++++++++---- 6 files changed, 41 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 69018d6..9839d88 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,13 @@ $ go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest $ docker build -t controld/ctrld . $ docker run -d --name=ctrld -p 53:53/tcp -p 53:53/udp controld/ctrld --cd=p2 -vv ``` +----- +*NOTE* +When running inside container, and listener address is set to "127.0.0.1:53", `ctrld` will change +the listen address to "0.0.0.0:53", so users can expose the port to outside. + +---- # Usage The cli is self documenting, so free free to run `--help` on any sub-command to get specific usages. @@ -176,6 +182,15 @@ In order to start `ctrld` as a DNS provider, simply run `./ctrld setup auto` com In this mode, and when Control D upstreams are used, the router will [relay your network topology](https://docs.controld.com/docs/device-clients) to Control D (LAN IPs, MAC addresses, and hostnames), and you will be able to see your LAN devices in the web panel, view analytics and apply unique profiles to them. +---- +*NOTE* + +`ctrld` will try leveraging default setup on routers, so changing routers default configuration would causes things won't work as expected (For example, changing dnsmasq configuration). + +Advanced users who want to play around, just run `ctrld` in [Service Mode](#service-mode) with custom configuration. + +---- + ### Control D Auto Configuration Application can be started with a specific resolver config, instead of the default one. Simply supply your Resolver ID with a `--cd` flag, when using the `run` (foreground) or `start` (service) or `setup` (router) modes. diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 5e16aae..dc80ebd 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -751,11 +751,10 @@ func processCDFlags() { listener.Port = 53 } } - // On router, we want to keep the listener address point to dnsmasq listener, aka 127.0.0.1:53. - if router.Name() != "" { + if setupRouter { if lc := cfg.Listener["0"]; lc != nil { - lc.IP = "127.0.0.1" - lc.Port = 53 + lc.IP = router.ListenIP() + lc.Port = router.ListenPort() } } } else { @@ -776,7 +775,7 @@ func processCDFlags() { rules = append(rules, ctrld.Rule{domain: []string{}}) } cfg.Listener = make(map[string]*ctrld.ListenerConfig) - cfg.Listener["0"] = &ctrld.ListenerConfig{ + lc := &ctrld.ListenerConfig{ IP: "127.0.0.1", Port: 53, Policy: &ctrld.ListenerPolicyConfig{ @@ -784,6 +783,11 @@ func processCDFlags() { Rules: rules, }, } + if setupRouter { + lc.IP = router.ListenIP() + lc.Port = router.ListenPort() + } + cfg.Listener["0"] = lc processLogAndCacheFlags() } diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 626c329..970d52e 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -402,8 +402,11 @@ func needLocalIPv6Listener() bool { } func dnsListenAddress(lcNum string, lc *ctrld.ListenerConfig) string { - if addr := router.ListenAddress(); setupRouter && addr != "" && lcNum == "0" { - return addr + addr := net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)) + // If we are inside container and the listener address is localhost, + // Change it to 0.0.0.0:53, so user can expose the port to outside. + if addr == "127.0.0.1:53" && cdUID != "" && inContainer() { + return "0.0.0.0:53" } return net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)) } diff --git a/docs/config.md b/docs/config.md index 8af8e40..c652afe 100644 --- a/docs/config.md +++ b/docs/config.md @@ -316,14 +316,14 @@ IP address that serves the incoming requests. If `ip` is empty, ctrld will liste - Type: ip address string - Required: no -- Default: "" +- Default: "" or "127.0.0.1" in [Router Mode](../README.md#router-mode). ### port Port number that the listener will listen on for incoming requests. If `port` is `0`, a random available port will be chosen. - Type: number - Required: no -- Default: 0 +- Default: 0 or 5354 in [Router Mode](../README.md#router-mode). ### restricted If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`. diff --git a/internal/router/firewalla.go b/internal/router/firewalla.go index 7b7e6ea..1182890 100644 --- a/internal/router/firewalla.go +++ b/internal/router/firewalla.go @@ -69,7 +69,7 @@ func cleanupFirewalla() error { // Restart dnsmasq service. if err := restartDNSMasq(); err != nil { - return fmt.Errorf("cleaupFirewalla: restartDNSMasq: %w", err) + return fmt.Errorf("cleanupFirewalla: restartDNSMasq: %w", err) } return nil diff --git a/internal/router/router.go b/internal/router/router.go index d3beda7..153f97e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -214,16 +214,21 @@ func Cleanup(svc *service.Config) error { return nil } -// ListenAddress returns the listener address of ctrld on router. -func ListenAddress() string { +// ListenIP returns the listener IP of ctrld on router. +func ListenIP() string { + return "127.0.0.1" +} + +// ListenPort returns the listener port of ctrld on router. +func ListenPort() int { name := Name() switch name { case DDWrt, Firewalla, Merlin, OpenWrt, Synology, Tomato, Ubios: - return "127.0.0.1:5354" + return 5354 case Pfsense: // On pfsense, we run ctrld as DNS resolver. } - return "" + return 53 } // Name returns name of the router platform. From 472bb05e95f188b1e265ec801bb088699be8569c Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 21 Jun 2023 16:54:28 +0700 Subject: [PATCH 12/84] Support building docker images multi arches --- Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile b/Dockerfile index d0d2b54..f328790 100644 --- a/Dockerfile +++ b/Dockerfile @@ -18,7 +18,7 @@ COPY . . ARG tag=master ENV CI_COMMIT_TAG=$tag -RUN CGO_ENABLED=0 ./scripts/build.sh linux/amd64 +RUN CTRLD_NO_QF=yes CGO_ENABLED=0 ./scripts/build.sh FROM scratch @@ -27,6 +27,6 @@ COPY --from=base /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=base /etc/passwd /etc/passwd COPY --from=base /etc/group /etc/group -COPY --from=base /app/ctrld-linux-amd64-nocgo ctrld +COPY --from=base /app/ctrld-linux-*-nocgo ctrld ENTRYPOINT ["./ctrld", "run"] From 9fe6af684f21a5588da8d2c19f5b7aafa59a3f19 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 21 Jun 2023 23:40:10 +0700 Subject: [PATCH 13/84] all: watch lease files if send client info enabled So users who run ctrld in Linux can still see clients info, even though it's not an router platform that ctrld supports. --- client_info.go | 8 + cmd/ctrld/cli.go | 3 - cmd/ctrld/dns_proxy.go | 5 +- cmd/ctrld/prog.go | 12 + internal/clientinfo/client_info.go | 219 ++++++++++++++++++ .../client_info_test.go | 18 +- internal/router/client_info.go | 194 ---------------- internal/router/router.go | 14 -- 8 files changed, 250 insertions(+), 223 deletions(-) create mode 100644 internal/clientinfo/client_info.go rename internal/{router => clientinfo}/client_info_test.go (88%) delete mode 100644 internal/router/client_info.go diff --git a/client_info.go b/client_info.go index d0d993a..acd7c5e 100644 --- a/client_info.go +++ b/client_info.go @@ -9,3 +9,11 @@ type ClientInfo struct { IP string Hostname string } + +// LeaseFileFormat specifies the format of DHCP lease file. +type LeaseFileFormat string + +const ( + Dnsmasq LeaseFileFormat = "dnsmasq" + IscDhcpd = "isc-dhcpd" +) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index dc80ebd..36b38e2 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -219,9 +219,6 @@ func initCLI() { if err := router.Cleanup(svcConfig); err != nil { mainLog.Error().Err(err).Msg("could not cleanup router") } - if err := router.Stop(); err != nil { - mainLog.Error().Err(err).Msg("problem occurred while stopping router") - } p.resetDNS() }) } diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 970d52e..38a0ba4 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -22,7 +22,6 @@ import ( "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/dnscache" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" - "github.com/Control-D-Inc/ctrld/internal/router" ) const ( @@ -56,7 +55,7 @@ func (p *prog) serveDNS(listenerNum string) error { q := m.Question[0] domain := canonicalName(q.Name) reqId := requestID() - remoteAddr := spoofRemoteAddr(w.RemoteAddr(), router.GetClientInfoByMac(macFromMsg(m))) + remoteAddr := spoofRemoteAddr(w.RemoteAddr(), p.mt.GetClientInfoByMac(macFromMsg(m))) fmtSrcToDest := fmtRemoteToLocal(listenerNum, remoteAddr.String(), w.LocalAddr().String()) t := time.Now() ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId) @@ -247,7 +246,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i } resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg { if upstreamConfig.UpstreamSendClientInfo() { - ci := router.GetClientInfoByMac(macFromMsg(msg)) + ci := p.mt.GetClientInfoByMac(macFromMsg(msg)) if ci != nil { ctrld.Log(ctx, mainLog.Debug(), "including client info with the request") ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, ci) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 5e563bf..fcdfc8d 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -13,6 +13,7 @@ import ( "github.com/kardianos/service" "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/clientinfo" "github.com/Control-D-Inc/ctrld/internal/dnscache" "github.com/Control-D-Inc/ctrld/internal/router" ) @@ -39,6 +40,7 @@ type prog struct { cfg *ctrld.Config cache dnscache.Cacher sema semaphore + mt *clientinfo.MacTable started chan struct{} onStarted []func() @@ -100,6 +102,16 @@ func (p *prog) run() { go uc.Ping() } + p.mt = clientinfo.NewMacTable() + if p.cfg.HasUpstreamSendClientInfo() { + mainLog.Debug().Msg("Sending client info enabled") + if err := p.mt.Init(); err == nil { + mainLog.Debug().Msg("Start watching client info changes") + go p.mt.WatchLeaseFiles() + } else { + mainLog.Warn().Err(err).Msg("could not record client info") + } + } go p.watchLinkState() for listenerNum := range p.cfg.Listener { diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go new file mode 100644 index 0000000..48b8678 --- /dev/null +++ b/internal/clientinfo/client_info.go @@ -0,0 +1,219 @@ +package clientinfo + +import ( + "bufio" + "bytes" + "fmt" + "io" + "log" + "net" + "os" + "strings" + "sync" + "time" + + "github.com/fsnotify/fsnotify" + "tailscale.com/util/lineread" + + "github.com/Control-D-Inc/ctrld" +) + +// clientInfoFiles specifies client info files and how to read them on supported platforms. +var clientInfoFiles = map[string]ctrld.LeaseFileFormat{ + "/tmp/dnsmasq.leases": ctrld.Dnsmasq, // ddwrt + "/tmp/dhcp.leases": ctrld.Dnsmasq, // openwrt + "/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // merlin + "/mnt/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDM Pro + "/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDR + "/etc/dhcpd/dhcpd-leases.log": ctrld.Dnsmasq, // Synology + "/tmp/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // Tomato + "/run/dnsmasq-dhcp.leases": ctrld.Dnsmasq, // EdgeOS + "/run/dhcpd.leases": ctrld.IscDhcpd, // EdgeOS + "/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense + "/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla +} + +// NewMacTable returns new Mac table to record client information. +func NewMacTable() *MacTable { + return &MacTable{} +} + +// MacTable records clients information by MAC address. +type MacTable struct { + mac sync.Map + watcher *fsnotify.Watcher +} + +// Init initializes recording client info. +func (mt *MacTable) Init() error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + mt.watcher = watcher + for file, format := range clientInfoFiles { + // Ignore errors for default lease files. + _ = mt.AddLeaseFile(file, format) + } + return nil +} + +// AddLeaseFile adds given lease file for reading/watching clients info. +func (mt *MacTable) AddLeaseFile(name string, format ctrld.LeaseFileFormat) error { + if err := mt.readLeaseFile(name, format); err != nil { + return fmt.Errorf("could not read lease file: %w", err) + } + clientInfoFiles[name] = format + return mt.watcher.Add(name) +} + +// GetClientInfoByMac returns ClientInfo for the client associated with the given MAC address. +func (mt *MacTable) GetClientInfoByMac(mac string) *ctrld.ClientInfo { + if mac == "" { + return nil + } + val, ok := mt.mac.Load(mac) + if !ok { + return nil + } + return val.(*ctrld.ClientInfo) +} + +// WatchLeaseFiles watches changes happens in dnsmasq/dhcpd +// lease files, perform updating to mac table if necessary. +func (mt *MacTable) WatchLeaseFiles() { + if mt.watcher == nil { + return + } + timer := time.NewTicker(time.Minute * 5) + for { + select { + case <-timer.C: + for _, name := range mt.watcher.WatchList() { + format := clientInfoFiles[name] + if err := mt.readLeaseFile(name, format); err != nil { + ctrld.ProxyLog.Err(err).Str("file", name).Msg("failed to update lease file") + } + } + case event, ok := <-mt.watcher.Events: + if !ok { + return + } + if event.Has(fsnotify.Write) { + format := clientInfoFiles[event.Name] + if err := mt.readLeaseFile(event.Name, format); err != nil && !os.IsNotExist(err) { + ctrld.ProxyLog.Err(err).Str("file", event.Name).Msg("leases file changed but failed to update client info") + } + } + case err, ok := <-mt.watcher.Errors: + if !ok { + return + } + ctrld.ProxyLog.Err(err).Msg("could not watch client info file") + } + } +} + +// readLeaseFile reads the lease file with given format, saving client information to mac table. +func (mt *MacTable) readLeaseFile(name string, format ctrld.LeaseFileFormat) error { + switch format { + case ctrld.Dnsmasq: + return mt.dnsmasqReadClientInfoFile(name) + case ctrld.IscDhcpd: + return mt.iscDHCPReadClientInfoFile(name) + } + return fmt.Errorf("unsupported format: %s, file: %s", format, name) +} + +// dnsmasqReadClientInfoFile populates mac table with client info reading from dnsmasq lease file. +func (mt *MacTable) dnsmasqReadClientInfoFile(name string) error { + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + return mt.dnsmasqReadClientInfoReader(f) + +} + +// dnsmasqReadClientInfoReader likes ctrld.Dnsmasq, but reading from an io.Reader instead of file. +func (mt *MacTable) dnsmasqReadClientInfoReader(reader io.Reader) error { + return lineread.Reader(reader, func(line []byte) error { + fields := bytes.Fields(line) + if len(fields) < 4 { + return nil + } + mac := string(fields[1]) + if _, err := net.ParseMAC(mac); err != nil { + // The second field is not a mac, skip. + return nil + } + ip := normalizeIP(string(fields[2])) + if net.ParseIP(ip) == nil { + log.Printf("invalid ip address entry: %q", ip) + ip = "" + } + hostname := string(fields[3]) + mt.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname}) + return nil + }) +} + +// iscDHCPReadClientInfoFile populates mac table with client info reading from isc-dhcpd lease file. +func (mt *MacTable) iscDHCPReadClientInfoFile(name string) error { + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + return mt.iscDHCPReadClientInfoReader(f) +} + +// iscDHCPReadClientInfoReader likes ctrld.IscDhcpd, but reading from an io.Reader instead of file. +func (mt *MacTable) iscDHCPReadClientInfoReader(reader io.Reader) error { + s := bufio.NewScanner(reader) + var ip, mac, hostname string + for s.Scan() { + line := s.Text() + if strings.HasPrefix(line, "}") { + if mac != "" { + mt.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname}) + ip, mac, hostname = "", "", "" + } + continue + } + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + switch fields[0] { + case "lease": + ip = normalizeIP(strings.ToLower(fields[1])) + if net.ParseIP(ip) == nil { + log.Printf("invalid ip address entry: %q", ip) + ip = "" + } + case "hardware": + if len(fields) >= 3 { + mac = strings.ToLower(strings.TrimRight(fields[2], ";")) + if _, err := net.ParseMAC(mac); err != nil { + // Invalid mac, skip. + mac = "" + } + } + case "client-hostname": + hostname = strings.Trim(fields[1], `";`) + } + } + return nil +} + +// normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file. +func normalizeIP(in string) string { + // dnsmasq may put ip with interface index in lease file, strip it here. + ip, _, found := strings.Cut(in, "%") + if found { + return ip + } + return in +} diff --git a/internal/router/client_info_test.go b/internal/clientinfo/client_info_test.go similarity index 88% rename from internal/router/client_info_test.go rename to internal/clientinfo/client_info_test.go index fac801c..2f2e092 100644 --- a/internal/router/client_info_test.go +++ b/internal/clientinfo/client_info_test.go @@ -1,4 +1,4 @@ -package router +package clientinfo import ( "io" @@ -31,6 +31,7 @@ func Test_normalizeIP(t *testing.T) { } func Test_readClientInfoReader(t *testing.T) { + mt := NewMacTable() tests := []struct { name string in string @@ -41,7 +42,7 @@ func Test_readClientInfoReader(t *testing.T) { "good dnsmasq", `1683329857 e6:20:59:b8:c1:6d 192.168.1.186 * 01:e6:20:59:b8:c1:6d `, - dnsmasqReadClientInfoReader, + mt.dnsmasqReadClientInfoReader, "e6:20:59:b8:c1:6d", }, { @@ -50,7 +51,7 @@ func Test_readClientInfoReader(t *testing.T) { duid 00:01:00:01:2b:e4:2e:2c:52:52:14:26:dc:1c 1683322985 117442354 2600:4040:b0e6:b700::111 ASDASD 00:01:00:01:2a:d0:b9:81:00:07:32:4c:1c:07 `, - dnsmasqReadClientInfoReader, + mt.dnsmasqReadClientInfoReader, "e6:20:59:b8:c1:6e", }, { @@ -60,7 +61,7 @@ duid 00:01:00:01:2b:e4:2e:2c:52:52:14:26:dc:1c client-hostname "host-1"; } `, - iscDHCPReadClientInfoReader, + mt.iscDHCPReadClientInfoReader, "00:00:00:00:00:01", }, { @@ -75,25 +76,24 @@ lease 192.168.1.2 { client-hostname "host-2"; } `, - iscDHCPReadClientInfoReader, + mt.iscDHCPReadClientInfoReader, "00:00:00:00:00:02", }, { "", `1685794060 00:00:00:00:00:04 192.168.0.209 cuonglm-ThinkPad-X1-Carbon-Gen-9 00:00:00:00:00:04 9`, - dnsmasqReadClientInfoReader, + mt.dnsmasqReadClientInfoReader, "00:00:00:00:00:04", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { - r := routerPlatform.Load() - r.mac.Delete(tc.mac) + mt.mac.Delete(tc.mac) if err := tc.readFunc(strings.NewReader(tc.in)); err != nil { t.Errorf("readClientInfoReader() error = %v", err) } - info, existed := r.mac.Load(tc.mac) + info, existed := mt.mac.Load(tc.mac) if !existed { t.Error("client info missing") } diff --git a/internal/router/client_info.go b/internal/router/client_info.go deleted file mode 100644 index c708b75..0000000 --- a/internal/router/client_info.go +++ /dev/null @@ -1,194 +0,0 @@ -package router - -import ( - "bufio" - "bytes" - "io" - "log" - "net" - "os" - "strings" - "time" - - "github.com/fsnotify/fsnotify" - "tailscale.com/util/lineread" - - "github.com/Control-D-Inc/ctrld" -) - -// readClientInfoFunc represents the function for reading client info. -type readClientInfoFunc func(name string) error - -// clientInfoFiles specifies client info files and how to read them on supported platforms. -var clientInfoFiles = map[string]readClientInfoFunc{ - "/tmp/dnsmasq.leases": dnsmasqReadClientInfoFile, // ddwrt - "/tmp/dhcp.leases": dnsmasqReadClientInfoFile, // openwrt - "/var/lib/misc/dnsmasq.leases": dnsmasqReadClientInfoFile, // merlin - "/mnt/data/udapi-config/dnsmasq.lease": dnsmasqReadClientInfoFile, // UDM Pro - "/data/udapi-config/dnsmasq.lease": dnsmasqReadClientInfoFile, // UDR - "/etc/dhcpd/dhcpd-leases.log": dnsmasqReadClientInfoFile, // Synology - "/tmp/var/lib/misc/dnsmasq.leases": dnsmasqReadClientInfoFile, // Tomato - "/run/dnsmasq-dhcp.leases": dnsmasqReadClientInfoFile, // EdgeOS - "/run/dhcpd.leases": iscDHCPReadClientInfoFile, // EdgeOS - "/var/dhcpd/var/db/dhcpd.leases": iscDHCPReadClientInfoFile, // Pfsense - "/home/pi/.router/run/dhcp/dnsmasq.leases": dnsmasqReadClientInfoFile, // Firewalla -} - -// watchClientInfoTable watches changes happens in dnsmasq/dhcpd -// lease files, perform updating to mac table if necessary. -func (r *router) watchClientInfoTable() { - if r.watcher == nil { - return - } - timer := time.NewTicker(time.Minute * 5) - for { - select { - case <-timer.C: - for _, name := range r.watcher.WatchList() { - _ = clientInfoFiles[name](name) - } - case event, ok := <-r.watcher.Events: - if !ok { - return - } - if event.Has(fsnotify.Write) { - readFunc := clientInfoFiles[event.Name] - if readFunc == nil { - log.Println("unknown file format:", event.Name) - continue - } - if err := readFunc(event.Name); err != nil && !os.IsNotExist(err) { - log.Println("could not read client info file:", err) - } - } - case err, ok := <-r.watcher.Errors: - if !ok { - return - } - log.Println("error:", err) - } - } -} - -// Stop performs tasks need to be done before the router stopped. -func Stop() error { - if Name() == "" { - return nil - } - r := routerPlatform.Load() - if r.watcher != nil { - if err := r.watcher.Close(); err != nil { - return err - } - } - return nil -} - -// GetClientInfoByMac returns ClientInfo for the client associated with the given mac. -func GetClientInfoByMac(mac string) *ctrld.ClientInfo { - if mac == "" { - return nil - } - _ = Name() - r := routerPlatform.Load() - val, ok := r.mac.Load(mac) - if !ok { - return nil - } - return val.(*ctrld.ClientInfo) -} - -// dnsmasqReadClientInfoFile populates mac table with client info reading from dnsmasq lease file. -func dnsmasqReadClientInfoFile(name string) error { - f, err := os.Open(name) - if err != nil { - return err - } - defer f.Close() - return dnsmasqReadClientInfoReader(f) - -} - -// dnsmasqReadClientInfoReader likes dnsmasqReadClientInfoFile, but reading from an io.Reader instead of file. -func dnsmasqReadClientInfoReader(reader io.Reader) error { - r := routerPlatform.Load() - return lineread.Reader(reader, func(line []byte) error { - fields := bytes.Fields(line) - if len(fields) < 4 { - return nil - } - mac := string(fields[1]) - if _, err := net.ParseMAC(mac); err != nil { - // The second field is not a mac, skip. - return nil - } - ip := normalizeIP(string(fields[2])) - if net.ParseIP(ip) == nil { - log.Printf("invalid ip address entry: %q", ip) - ip = "" - } - hostname := string(fields[3]) - r.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname}) - return nil - }) -} - -// iscDHCPReadClientInfoFile populates mac table with client info reading from isc-dhcpd lease file. -func iscDHCPReadClientInfoFile(name string) error { - f, err := os.Open(name) - if err != nil { - return err - } - defer f.Close() - return iscDHCPReadClientInfoReader(f) -} - -// iscDHCPReadClientInfoReader likes iscDHCPReadClientInfoFile, but reading from an io.Reader instead of file. -func iscDHCPReadClientInfoReader(reader io.Reader) error { - r := routerPlatform.Load() - s := bufio.NewScanner(reader) - var ip, mac, hostname string - for s.Scan() { - line := s.Text() - if strings.HasPrefix(line, "}") { - if mac != "" { - r.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname}) - ip, mac, hostname = "", "", "" - } - continue - } - fields := strings.Fields(line) - if len(fields) < 2 { - continue - } - switch fields[0] { - case "lease": - ip = normalizeIP(strings.ToLower(fields[1])) - if net.ParseIP(ip) == nil { - log.Printf("invalid ip address entry: %q", ip) - ip = "" - } - case "hardware": - if len(fields) >= 3 { - mac = strings.ToLower(strings.TrimRight(fields[2], ";")) - if _, err := net.ParseMAC(mac); err != nil { - // Invalid mac, skip. - mac = "" - } - } - case "client-hostname": - hostname = strings.Trim(fields[1], `";`) - } - } - return nil -} - -// normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file. -func normalizeIP(in string) string { - // dnsmasq may put ip with interface index in lease file, strip it here. - ip, _, found := strings.Cut(in, "%") - if found { - return ip - } - return in -} diff --git a/internal/router/router.go b/internal/router/router.go index 153f97e..79f5901 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -7,11 +7,9 @@ import ( "fmt" "os" "os/exec" - "sync" "sync/atomic" "time" - "github.com/fsnotify/fsnotify" "github.com/kardianos/service" "tailscale.com/logtail/backoff" @@ -38,8 +36,6 @@ var routerPlatform atomic.Pointer[router] type router struct { name string sendClientInfo bool - mac sync.Map - watcher *fsnotify.Watcher } // IsSupported reports whether the given platform is supported by ctrld. @@ -102,16 +98,6 @@ func Configure(c *ctrld.Config) error { if c.HasUpstreamSendClientInfo() { r := routerPlatform.Load() r.sendClientInfo = true - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - r.watcher = watcher - go r.watchClientInfoTable() - for file, readClienInfoFunc := range clientInfoFiles { - _ = readClienInfoFunc(file) - _ = r.watcher.Add(file) - } } configure := configureFunc[name] if err := configure(); err != nil { From 12148ec23148bf927730d228a02c88bca9a05dcb Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 22 Jun 2023 21:14:07 +0700 Subject: [PATCH 14/84] 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. --- cmd/ctrld/cli.go | 82 ++++++++++++++++++++++++++--------------- cmd/ctrld/main.go | 27 ++++++++++++-- cmd/ctrld/prog.go | 2 + cmd/ctrld/prog_linux.go | 7 ++++ 4 files changed, 84 insertions(+), 34 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 36b38e2..7b9a307 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -7,6 +7,7 @@ import ( "encoding/base64" "errors" "fmt" + "io" "net" "net/netip" "os" @@ -15,15 +16,14 @@ import ( "runtime" "strconv" "strings" - "sync" "time" "github.com/cuonglm/osinfo" - "github.com/fsnotify/fsnotify" "github.com/go-playground/validator/v10" "github.com/kardianos/service" "github.com/miekg/dns" "github.com/pelletier/go-toml/v2" + "github.com/rs/zerolog" "github.com/spf13/cobra" "github.com/spf13/viper" "tailscale.com/logtail/backoff" @@ -150,6 +150,12 @@ func initCLI() { 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()) oi := osinfo.New() mainLog.Info().Msgf("os: %s", oi.String()) @@ -158,10 +164,6 @@ func initCLI() { if !ctrldnet.Up() { 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 // 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") } + oldLogPath := cfg.Service.LogPath 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 { mainLog.Fatal().Msgf("invalid config: %v", err) } @@ -293,10 +309,7 @@ func initCLI() { mainLog.Fatal().Msgf("failed to unmarshal config: %v", err) } - logPath := cfg.Service.LogPath - cfg.Service.LogPath = "" initLogging() - cfg.Service.LogPath = logPath processCDFlags() @@ -648,6 +661,15 @@ func readBase64Config(configBase64 string) { if err != nil { 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 { 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") + cfg = ctrld.Config{Listener: map[string]*ctrld.ListenerConfig{ + "0": {Port: 53}, + }} if resolverConfig.Ctrld.CustomConfig != "" { logger.Info().Msg("using defined custom config of Control-D resolver") readBase64Config(resolverConfig.Ctrld.CustomConfig) @@ -748,14 +773,27 @@ func processCDFlags() { listener.Port = 53 } } - if setupRouter { + switch { + case setupRouter: if lc := cfg.Listener["0"]; lc != nil { lc.IP = router.ListenIP() 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 { - cfg = ctrld.Config{} cfg.Network = make(map[string]*ctrld.NetworkConfig) cfg.Network["0"] = &ctrld.NetworkConfig{ Name: "Network 0", @@ -785,9 +823,10 @@ func processCDFlags() { lc.Port = router.ListenPort() } cfg.Listener["0"] = lc - processLogAndCacheFlags() } + processLogAndCacheFlags() + if err := writeConfigFile(); err != nil { logger.Fatal().Err(err).Msg("failed to write config file") } else { @@ -872,26 +911,9 @@ func selfCheckStatus(status service.Status, domain string) service.Status { ctx := context.Background() maxAttempts := 20 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++ { lc := cfg.Listener["0"] - mu.Lock() - if lcChanged != nil { - lc = lcChanged["0"] - } - mu.Unlock() m := new(dns.Msg) m.SetQuestion(domain+".", dns.TypeA) m.RecursionDesired = true diff --git a/cmd/ctrld/main.go b/cmd/ctrld/main.go index bc3edf0..9f6ec60 100644 --- a/cmd/ctrld/main.go +++ b/cmd/ctrld/main.go @@ -78,7 +78,18 @@ func initConsoleLogging() { } } +// initLogging initializes global logging setup. 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} if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" { // Create parent directory if necessary. @@ -86,11 +97,19 @@ func initLogging() { mainLog.Error().Msgf("failed to create log path: %v", err) os.Exit(1) } - // 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) + + // Default open log file in append mode. + 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 { mainLog.Error().Msgf("failed to create log file: %v", err) os.Exit(1) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index fcdfc8d..54cad6d 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -32,6 +32,8 @@ var svcConfig = &service.Config{ Option: service.KeyValue{}, } +var useSystemdResolved = false + type prog struct { mu sync.Mutex waitCh chan struct{} diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 7dc14d9..86d4caa 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -3,9 +3,16 @@ package main import ( "github.com/kardianos/service" + "github.com/Control-D-Inc/ctrld/internal/dns" "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() { if !service.Interactive() { p.setDNS() From 2f46d512c69245f61353366657143aedde4a6bef Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 22 Jun 2023 22:33:51 +0700 Subject: [PATCH 15/84] Not send client info with non-Control D upstream by default --- config.go | 7 ++---- config_internal_test.go | 55 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/config.go b/config.go index ac6904b..cb51e97 100644 --- a/config.go +++ b/config.go @@ -248,11 +248,8 @@ func (uc *UpstreamConfig) VerifyDomain() string { // - Lan IP // - Hostname func (uc *UpstreamConfig) UpstreamSendClientInfo() bool { - if uc.SendClientInfo != nil && !(*uc.SendClientInfo) { - return false - } - if uc.SendClientInfo == nil { - return true + if uc.SendClientInfo != nil { + return *uc.SendClientInfo } switch uc.Type { case ResolverTypeDOH, ResolverTypeDOH3: diff --git a/config_internal_test.go b/config_internal_test.go index fb3692e..6fc1844 100644 --- a/config_internal_test.go +++ b/config_internal_test.go @@ -223,6 +223,61 @@ func TestUpstreamConfig_VerifyDomain(t *testing.T) { }) } } + +func TestUpstreamConfig_UpstreamSendClientInfo(t *testing.T) { + tests := []struct { + name string + uc *UpstreamConfig + sendClientInfo bool + }{ + { + "default with controld upstream DoH", + &UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH}, + true, + }, + { + "default with controld upstream DoH3", + &UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH3}, + true, + }, + { + "default with non-ControlD upstream", + &UpstreamConfig{Endpoint: "https://dns.google/dns-query", Type: ResolverTypeDOH}, + false, + }, + { + "set false with controld upstream", + &UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", Type: ResolverTypeDOH, SendClientInfo: ptrBool(false)}, + false, + }, + { + "set true with controld upstream", + &UpstreamConfig{Endpoint: "https://freedns.controld.com/p2", SendClientInfo: ptrBool(true)}, + true, + }, + { + "set false with non-ControlD upstream", + &UpstreamConfig{Endpoint: "https://dns.google/dns-query", SendClientInfo: ptrBool(false)}, + false, + }, + { + "set true with non-ControlD upstream", + &UpstreamConfig{Endpoint: "https://dns.google/dns-query", Type: ResolverTypeDOH, SendClientInfo: ptrBool(true)}, + true, + }, + } + + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + if got := tc.uc.UpstreamSendClientInfo(); got != tc.sendClientInfo { + t.Errorf("unexpected result, want: %v, got: %v", tc.sendClientInfo, got) + } + }) + } +} + func ptrBool(b bool) *bool { return &b } From 3f211d3cc2a8b068b18c7809fc3cbf88f8eebcb2 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 23 Jun 2023 10:32:19 +0700 Subject: [PATCH 16/84] cmd/ctrld: remove firerouter_dns dependency in systemd unit on firewalla On firewalla, firerouter_dns is a shell script, which forks dnsmasq processes. At the end of ctrld stopping process, ctrld attempts to restart firerouter_dns. The systemd v237 on firewalla somehow hangs, because ctrld depends on firerouter_dns, but attempts to restart it before ctrld stopping. However, thing in firewalla is ephemeral, so after reboot, ctrld is re-installed at the end of boot process. Thus, ctrld don't have to depend on any services. --- cmd/ctrld/prog_linux.go | 7 ------- 1 file changed, 7 deletions(-) diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 86d4caa..08cbfbf 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -33,13 +33,6 @@ func setDependencies(svc *service.Config) { svc.Dependencies = append(svc.Dependencies, "Wants=dnsmasq.service") svc.Dependencies = append(svc.Dependencies, "After=dnsmasq.service") } - // On Firewalla, ctrld needs to start after firerouter_{dhcp,dns}, so it can read leases file. - if router.Name() == router.Firewalla { - svc.Dependencies = append(svc.Dependencies, "Wants=firerouter_dhcp.service") - svc.Dependencies = append(svc.Dependencies, "After=firerouter_dhcp.service") - svc.Dependencies = append(svc.Dependencies, "Wants=firerouter_dns.service") - svc.Dependencies = append(svc.Dependencies, "After=firerouter_dns.service") - } } func setWorkingDirectory(svc *service.Config, dir string) { From de951fd89591036de40d87307fae5c1d7341a0ee Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 23 Jun 2023 20:49:07 +0700 Subject: [PATCH 17/84] Upgrade dependencies for security/bug fixes - tailscale.com to its latest v1.44.0 - github.com/spf13/viper to its latest v1.16.0 --- go.mod | 67 ++++++++++----------- go.sum | 185 ++++++++++++++++++++++++--------------------------------- 2 files changed, 109 insertions(+), 143 deletions(-) diff --git a/go.mod b/go.mod index 169dc34..6024239 100644 --- a/go.mod +++ b/go.mod @@ -5,28 +5,29 @@ go 1.20 require ( github.com/coreos/go-systemd/v22 v22.5.0 github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19 - github.com/frankban/quicktest v1.14.3 + github.com/frankban/quicktest v1.14.5 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/godbus/dbus/v5 v5.1.0 github.com/hashicorp/golang-lru/v2 v2.0.1 github.com/illarion/gonotify v1.0.1 - github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 + github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 github.com/kardianos/service v1.2.1 - github.com/miekg/dns v1.1.50 - github.com/pelletier/go-toml/v2 v2.0.6 + github.com/miekg/dns v1.1.55 + github.com/pelletier/go-toml/v2 v2.0.8 github.com/quic-go/quic-go v0.32.0 github.com/rs/zerolog v1.28.0 - github.com/spf13/cobra v1.4.0 - github.com/spf13/viper v1.14.0 - github.com/stretchr/testify v1.8.1 - github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 - golang.org/x/net v0.7.0 - golang.org/x/sync v0.1.0 - golang.org/x/sys v0.5.0 + github.com/spf13/cobra v1.7.0 + github.com/spf13/viper v1.16.0 + github.com/stretchr/testify v1.8.3 + github.com/vishvananda/netlink v1.2.1-beta.2 + go4.org/mem v0.0.0-20220726221520-4f986261bf13 + golang.org/x/net v0.10.0 + golang.org/x/sync v0.2.0 + golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a golang.zx2c4.com/wireguard/windows v0.5.3 - tailscale.com v1.38.3 + tailscale.com v1.44.0 ) require ( @@ -39,42 +40,40 @@ require ( github.com/google/go-cmp v0.5.9 // indirect github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.0.0 // indirect - github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b // indirect - github.com/kr/pretty v0.3.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/jsimonetti/rtnetlink v1.3.2 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect github.com/leodido/go-urn v1.2.1 // indirect github.com/magiconair/properties v1.8.7 // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.18 // indirect github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect - github.com/mdlayher/netlink v1.7.1 // indirect + github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect - github.com/mdlayher/socket v0.4.0 // indirect + github.com/mdlayher/socket v0.4.1 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/onsi/ginkgo/v2 v2.2.0 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pierrec/lz4/v4 v4.1.17 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-18 v0.2.0 // indirect github.com/quic-go/qtls-go1-19 v0.2.0 // indirect github.com/quic-go/qtls-go1-20 v0.1.0 // indirect - github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect - github.com/spf13/afero v1.9.3 // indirect - github.com/spf13/cast v1.5.0 // indirect + github.com/rogpeppe/go-internal v1.10.0 // indirect + github.com/spf13/afero v1.9.5 // indirect + github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - github.com/u-root/uio v0.0.0-20221213070652-c3537552635f // indirect - github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 // indirect - go4.org/mem v0.0.0-20210711025021-927187094b94 // indirect - golang.org/x/crypto v0.6.0 // indirect - golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect - golang.org/x/mod v0.7.0 // indirect - golang.org/x/text v0.7.0 // indirect - golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d // indirect + github.com/subosito/gotenv v1.4.2 // indirect + github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect + github.com/vishvananda/netns v0.0.4 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 // indirect + golang.org/x/mod v0.10.0 // indirect + golang.org/x/text v0.9.0 // indirect + golang.org/x/tools v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 3b884b7..be89ee2 100644 --- a/go.sum +++ b/go.sum @@ -46,15 +46,14 @@ github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cilium/ebpf v0.8.1 h1:bLSSEbBLqGPXxls55pGr5qWZaTqcmfDJHhou7t254ao= -github.com/cilium/ebpf v0.8.1/go.mod h1:f5zLIM0FSNuAkSyLAN7X+Hy6yznlF1mNiWUMfxMtrgk= +github.com/cilium/ebpf v0.10.0 h1:nk5HPMeoBXtOzbkZBWym+ZWq1GIiHUsBFXxwewXAHLQ= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19 h1:7P/f19Mr0oa3ug8BYt4JuRe/Zq3dF4Mrr4m8+Kw+Hcs= github.com/cuonglm/osinfo v0.0.0-20230329055532-c513f836da19/go.mod h1:G45410zMgmnSjLVKCq4f6GpbYAzoP2plX9rPwgx6C24= @@ -67,10 +66,8 @@ github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1m github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fanliao/go-promise v0.0.0-20141029170127-1890db352a72/go.mod h1:PjfxuH4FZdUyfMdtBio2lsRr1AKEaVPwelzuHuh8Lqc= -github.com/frankban/quicktest v1.14.0/go.mod h1:NeW+ay9A/U67EYXNFA1nPE8e/tnQv/09mUdL/ijj8og= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/frankban/quicktest v1.14.5 h1:dfYrrRyLtiqT9GyKXgdh+k4inNeTvmGbuSgZ3lx3GhA= +github.com/frankban/quicktest v1.14.5/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= @@ -87,8 +84,8 @@ github.com/go-playground/validator/v10 v10.11.1/go.mod h1:i+3WkQ1FvaUjjxh1kSvIA4 github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 h1:p104kn46Q8WdvHunIJ9dAyjPVtrBPhSr3KT2yUst43I= github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/godbus/dbus/v5 v5.0.6 h1:mkgN1ofwASrYnJ5W6U/BxG15eXXXjirgZc7CLqkcaro= -github.com/godbus/dbus/v5 v5.0.6/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -116,7 +113,7 @@ github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvq github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= @@ -128,8 +125,6 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -152,7 +147,6 @@ github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+ github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru/v2 v2.0.1 h1:5pv5N1lT1fjLg2VQ5KWc7kmucp2x/kvFOnxuVTqZ6x4= @@ -160,36 +154,30 @@ github.com/hashicorp/golang-lru/v2 v2.0.1/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyf github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714 h1:/jC7qQFrv8CrSJVmaolDVOxTfS9kc36uB6H40kdbQq8= -github.com/hugelgupf/socketpair v0.0.0-20190730060125-05d35a94e714/go.mod h1:2Goc3h8EklBH5mspfHFxBnEoURQCGzQQH1ga9Myjvis= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/illarion/gonotify v1.0.1 h1:F1d+0Fgbq/sDWjj/r66ekjDG+IDeecQKUFH4wNwsoio= github.com/illarion/gonotify v1.0.1/go.mod h1:zt5pmDofZpU1f8aqlK0+95eQhoEAn/d4G4B/FjVW4jE= -github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8 h1:Z72DOke2yOK0Ms4Z2LK1E1OrRJXOxSj5DllTz2FYTRg= -github.com/insomniacslk/dhcp v0.0.0-20221215072855-de60144f33f8/go.mod h1:m5WMe03WCvWcXjRnhvaAbAAXdCnu20J5P+mmH44ZzpE= -github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16 h1:+aAGyK41KRn8jbF2Q7PLL0Sxwg6dShGcQSeCC7nZQ8E= +github.com/insomniacslk/dhcp v0.0.0-20230407062729-974c6f05fe16/go.mod h1:IKrnDWs3/Mqq5n0lI+RxA2sB7MvN/vbMBP3ehXg65UI= github.com/josharian/native v1.0.1-0.20221213033349-c1e37c09b531/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 h1:elKwZS1OcdQ0WwEDBeqxKwb7WB62QX8bvZ/FJnVXIfk= github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86/go.mod h1:aFAMtuldEgx/4q7iSGazk22+IcgvtiC+HIimFO9XlS8= -github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw= -github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ= -github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok= -github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg= -github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b h1:Yws7RV6kZr2O7PPdT+RkbSmmOponA8i/1DuGHe8BRsM= -github.com/jsimonetti/rtnetlink v1.1.2-0.20220408201609-d380b505068b/go.mod h1:TzDCVOZKUa79z6iXbbXqhtAflVgUKaFkZ21M5tK5tzY= +github.com/jsimonetti/rtnetlink v1.3.2 h1:dcn0uWkfxycEEyNy0IGfx3GrhQ38LH7odjxAghimsVI= +github.com/jsimonetti/rtnetlink v1.3.2/go.mod h1:BBu4jZCpTjP6Gk0/wfrO8qcqymnN3g0hoFqObRmUo6U= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kardianos/service v1.2.1 h1:AYndMsehS+ywIS6RB9KOlcXzteWUzxgMgBymJD7+BYk= github.com/kardianos/service v1.2.1/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -198,36 +186,34 @@ github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= +github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= -github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA= -github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M= -github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY= -github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o= -github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA= -github.com/mdlayher/netlink v1.7.1 h1:FdUaT/e33HjEXagwELR8R3/KL1Fq5x3G5jgHLp/BTmg= -github.com/mdlayher/netlink v1.7.1/go.mod h1:nKO5CSjE/DJjVhk/TNp6vCE1ktVxEA8VEh8drhZzxsQ= +github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= +github.com/mdlayher/netlink v1.7.2/go.mod h1:xraEF7uJbxLhc5fpHL4cPe221LI2bdttWlU+ZGLfQSw= github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w= github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg= -github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs= -github.com/mdlayher/socket v0.4.0 h1:280wsy40IC9M9q1uPGcLBwXpcTQDtoGwVt+BNoITxIw= -github.com/mdlayher/socket v0.4.0/go.mod h1:xxFqz5GRCUN3UEOm9CZqEJsAbe1C8OwSK46NlmWuVoc= -github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= -github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= +github.com/mdlayher/socket v0.4.1 h1:eM9y2/jlbs1M615oshPQOHZzj6R6wMT7bX5NPiQvn2U= +github.com/mdlayher/socket v0.4.1/go.mod h1:cAqeGjoufqdxWkD7DkpyS+wcefOtmu5OQ8KuoJGIReA= +github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= +github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= -github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pierrec/lz4/v4 v4.1.17 h1:kV4Ip+/hUBC+8T6+2EgburRtkE9ef4nbY3f4dFhGjMc= +github.com/pierrec/lz4/v4 v4.1.17/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= @@ -247,24 +233,23 @@ github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+ github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 h1:Ha8xCaq6ln1a+R91Km45Oq6lPXj2Mla6CRJYcuV2h1w= -github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= -github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= -github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= -github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= +github.com/spf13/afero v1.9.5 h1:stMpOSZFs//0Lv29HduCmli3GUfpFoF3Y1Q/aXj/wVM= +github.com/spf13/afero v1.9.5/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= +github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= +github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= +github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= +github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.14.0 h1:Rg7d3Lo706X9tHsJMUjdiwMpHB7W8WnSVOssIY+JElU= -github.com/spf13/viper v1.14.0/go.mod h1:WT//axPky3FdvXHzGw33dNdXXXfFQqmEalje+egj8As= +github.com/spf13/viper v1.16.0 h1:rGGH0XDZhdUOryiDWjmIvUSWpbNqisK8Wk0Vyefw8hc= +github.com/spf13/viper v1.16.0/go.mod h1:yg78JgCJcbrQOvV9YLXgkLaZqUidkY9K+Dd1FofRzQg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= @@ -276,17 +261,17 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= -github.com/u-root/uio v0.0.0-20221213070652-c3537552635f h1:dpx1PHxYqAnXzbryJrWP1NQLzEjwcVgFLhkknuFQ7ww= -github.com/u-root/uio v0.0.0-20221213070652-c3537552635f/go.mod h1:IogEAUBXDEwX7oR/BMmCctShYs80ql4hF0ySdzGxf7E= -github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So= -github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/subosito/gotenv v1.4.2 h1:X1TuBLAMDFbaTAChgCBLu3DU3UPyELpnF2jjJ2cz/S8= +github.com/subosito/gotenv v1.4.2/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 h1:YcojQL98T/OO+rybuzn2+5KrD5dBwXIvYBvQ2cD3Avg= +github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63/go.mod h1:eLL9Nub3yfAho7qB0MzZizFhTU2QkLeoVsWdHtDW264= +github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= +github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74 h1:gga7acRE695APm9hlsSMoOoE65U4/TcqNj90mc69Rlg= -github.com/vishvananda/netns v0.0.0-20211101163701-50045581ed74/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= +github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= +github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -298,18 +283,18 @@ go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go4.org/mem v0.0.0-20210711025021-927187094b94 h1:OAAkygi2Js191AJP1Ds42MhJRgeofeKGjuoUqNp1QC4= -go4.org/mem v0.0.0-20210711025021-927187094b94/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= +go4.org/mem v0.0.0-20220726221520-4f986261bf13 h1:CbZeCBZ0aZj8EfVgnqQcYZgf0lpZ3H9rmp5nkDTAst8= +go4.org/mem v0.0.0-20220726221520-4f986261bf13/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= -golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= @@ -320,8 +305,8 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o= -golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53 h1:5llv2sWeaMSnA3w2kS57ouQQ4pudlXrR0dCgw51QK9o= +golang.org/x/exp v0.0.0-20230425010034-47ecfdc1ba53/go.mod h1:V1LtkGg67GoY2N1AnLN78QLrzxkLyJw7RJb1gzOOz9w= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -346,8 +331,8 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= +golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -361,8 +346,6 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191007182048-72f939374954/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -377,20 +360,15 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -411,12 +389,11 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.2.0 h1:PUR+T4wwASmuSTYdKjYHI5TD22Wy5ogLU5qZCOLxBrI= +golang.org/x/sync v0.2.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -425,9 +402,7 @@ golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -447,9 +422,7 @@ golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -462,17 +435,15 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.1-0.20230131160137-e7d7f63158de/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a h1:qMsju+PNttu/NMbq8bQ9waDdxgJMu9QNoUDuhnBaYt0= +golang.org/x/sys v0.8.1-0.20230609144347-5059a07aa46a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -481,8 +452,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -492,7 +463,6 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -535,9 +505,8 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d h1:9ZNWAi4CYhNv60mXGgAncgq7SGc5qa7C8VZV8Tg7Ggs= -golang.org/x/tools v0.4.1-0.20221208213631-3f74d914ae6d/go.mod h1:UE5sM2OK9E/d67R0ANs2xJizIymRP5gJU295PvKXxjQ= +golang.org/x/tools v0.9.1 h1:8WMNJAz3zrtPmnYC7ISf5dEn3MT0gY7jBJfw27yrrLo= +golang.org/x/tools v0.9.1/go.mod h1:owI94Op576fPu3cIGQeHs3joujW/2Oc6MtlxbF5dfNc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -632,7 +601,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= @@ -641,8 +610,6 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= @@ -657,5 +624,5 @@ honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= -tailscale.com v1.38.3 h1:2aX3+u0Re8QcN6nq7zf9Aa4ZCR2Nf6Imv3isqdQrb58= -tailscale.com v1.38.3/go.mod h1:UWLQxcd8dz+lds2I+HpfXSruHrvXM1j4zd4zdx86t7w= +tailscale.com v1.44.0 h1:MPos9n30kJvdyfL52045gVFyNg93K+bwgDsr8gqKq2o= +tailscale.com v1.44.0/go.mod h1:+iYwTdeHyVJuNDu42Zafwihq1Uqfh+pW7pRaY1GD328= From eaa907a6470a93b29c02fa49243170201fea621e Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 23 Jun 2023 23:12:44 +0700 Subject: [PATCH 18/84] cmd/ctrld: fix a race in using logf While at it, also fix the import and not use error. --- cmd/ctrld/prog_linux.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 08cbfbf..2c070a6 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -8,7 +8,7 @@ import ( ) func init() { - if r, err := dns.NewOSConfigurator(logf, "lo"); err == nil { + if r, err := dns.NewOSConfigurator(func(format string, args ...any) {}, "lo"); err == nil { useSystemdResolved = r.Mode() == "systemd-resolved" } } From cc28b929354d97102e1a4814507f06872b5543b6 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Sat, 24 Jun 2023 00:29:49 +0700 Subject: [PATCH 19/84] all: fallback to br0 as nameserver if 127.0.0.1 is used On Firewalla, lo interface is excluded in all dnsmasq settings of all interfaces, to prevent conflicts. The one that ctrld adds in dnsmasq_local directory could not work if there're multiple dnsmasq configs for multiple interfaces (real example from an user who uses VLAN in router setup). Instead, if we detect 127.0.0.1 on Firewalla, fallback to "br0" interface IP address instead. --- cmd/ctrld/cli.go | 4 +++ cmd/ctrld/prog.go | 18 ++++++++++++- internal/router/dnsmasq.go | 5 +--- internal/router/firewalla.go | 49 ++++++++++-------------------------- internal/router/router.go | 18 +++++++++++++ 5 files changed, 53 insertions(+), 41 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 7b9a307..1b0262b 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -982,6 +982,10 @@ func uninstall(p *prog, s service.Service) { } initLogging() if doTasks(tasks) { + if err := router.PostUninstall(svcConfig); err != nil { + mainLog.Warn().Err(err).Msg("post uninstallation failed, please check system/service log for details error") + return + } // Stop already reset DNS on router. if router.Name() == "" { p.resetDNS() diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 54cad6d..e917f88 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -235,7 +235,23 @@ func (p *prog) setDNS() { return } logger.Debug().Msg("setting DNS for interface") - if err := setDNS(netIface, []string{cfg.Listener["0"].IP}); err != nil { + ns := cfg.Listener["0"].IP + if router.Name() == router.Firewalla && ns == "127.0.0.1" { + // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. + // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. + logger.Warn().Msg("127.0.0.1 won't work on Firewalla") + if netIface, err := net.InterfaceByName("br0"); err == nil { + addrs, _ := netIface.Addrs() + for _, addr := range addrs { + if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { + logger.Warn().Msg("using br0 interface IP address as DNS server") + ns = netIP.IP.To4().String() + break + } + } + } + } + if err := setDNS(netIface, []string{ns}); err != nil { logger.Error().Err(err).Msgf("could not set DNS for interface") return } diff --git a/internal/router/dnsmasq.go b/internal/router/dnsmasq.go index 2ca3249..9df4132 100644 --- a/internal/router/dnsmasq.go +++ b/internal/router/dnsmasq.go @@ -1,7 +1,6 @@ package router import ( - "fmt" "strings" "text/template" ) @@ -50,10 +49,8 @@ func dnsMasqConf() (string, error) { var sb strings.Builder var tmplText string switch Name() { - case DDWrt, EdgeOS, OpenWrt, Ubios, Synology, Tomato: + case DDWrt, EdgeOS, Firewalla, OpenWrt, Ubios, Synology, Tomato: tmplText = dnsMasqConfigContentTmpl - case Firewalla: - tmplText = dnsMasqConfigContentTmpl + fmt.Sprintf("listen-address=127.0.0.1\n") case Merlin: tmplText = merlinDNSMasqPostConfTmpl } diff --git a/internal/router/firewalla.go b/internal/router/firewalla.go index 1182890..6d57409 100644 --- a/internal/router/firewalla.go +++ b/internal/router/firewalla.go @@ -4,42 +4,21 @@ import ( "fmt" "os" "os/exec" - "path/filepath" "strings" ) const ( - firewallaDNSMasqConfigPath = "/home/pi/.firewalla/config/dnsmasq_local" - firewallaDNSMasqBackupConfigPath = "/home/pi/.firewalla/config/dnsmasq_local.bak" - firewallaConfigPostMainDir = "/home/pi/.firewalla/config/post_main.d" - firewallaCtrldInitScriptPath = "/home/pi/.firewalla/config/post_main.d/start_ctrld.sh" + firewallaDNSMasqConfigPath = "/home/pi/.firewalla/config/dnsmasq_local/ctrld" + firewallaConfigPostMainDir = "/home/pi/.firewalla/config/post_main.d" + firewallaCtrldInitScriptPath = "/home/pi/.firewalla/config/post_main.d/start_ctrld.sh" ) func setupFirewalla() error { - fi, err := os.Stat(firewallaDNSMasqConfigPath) - if err != nil { - return fmt.Errorf("setupFirewalla: get current config directory: %w", err) - } - - _ = os.RemoveAll(firewallaDNSMasqBackupConfigPath) - - // Creating a backup. - if err := os.Rename(firewallaDNSMasqConfigPath, firewallaDNSMasqBackupConfigPath); err != nil { - return fmt.Errorf("setupFirewalla: backup current config: %w", err) - } - - // Creating our own config. - if err := os.MkdirAll(firewallaDNSMasqConfigPath, fi.Mode()); err != nil { - return fmt.Errorf("setupFirewalla: creating config dir: %w", err) - } - - // Adding ctrld listener as the only upstream. dnsMasqConfigContent, err := dnsMasqConf() if err != nil { return fmt.Errorf("setupFirewalla: generating dnsmasq config: %w", err) } - ctrldConfPath := filepath.Join(firewallaDNSMasqConfigPath, "ctrld") - if err := os.WriteFile(ctrldConfPath, []byte(dnsMasqConfigContent), 0600); err != nil { + if err := os.WriteFile(firewallaDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { return fmt.Errorf("setupFirewalla: writing ctrld config: %w", err) } @@ -52,21 +31,11 @@ func setupFirewalla() error { } func cleanupFirewalla() error { - // Do nothing if there's no backup config. - if _, err := os.Stat(firewallaDNSMasqBackupConfigPath); err != nil && os.IsNotExist(err) { - return nil - } - // Removing current config. - if err := os.RemoveAll(firewallaDNSMasqConfigPath); err != nil { + if err := os.Remove(firewallaDNSMasqConfigPath); err != nil { return fmt.Errorf("cleanupFirewalla: removing ctrld config: %w", err) } - // Restoring backup. - if err := os.Rename(firewallaDNSMasqBackupConfigPath, firewallaDNSMasqConfigPath); err != nil { - return fmt.Errorf("cleanupFirewalla: restoring backup config: %w", err) - } - // Restart dnsmasq service. if err := restartDNSMasq(); err != nil { return fmt.Errorf("cleanupFirewalla: restartDNSMasq: %w", err) @@ -83,6 +52,14 @@ func postInstallFirewalla() error { return nil } +func postUninstallFirewalla() error { + // Removing startup script. + if err := os.Remove(firewallaCtrldInitScriptPath); err != nil { + return fmt.Errorf("postUninstallFirewalla: removing startup script: %w", err) + } + return nil +} + func firewallaRestartDNSMasq() error { return exec.Command("systemctl", "restart", "firerouter_dns").Run() } diff --git a/internal/router/router.go b/internal/router/router.go index 79f5901..c53178f 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -174,6 +174,24 @@ func PostInstall(svc *service.Config) error { return nil } +// PostUninstall performs task after uninstalling ctrld on router. +func PostUninstall(svc *service.Config) error { + name := Name() + switch name { + case DDWrt: + case EdgeOS: + case Firewalla: + return postUninstallFirewalla() + case Merlin: + case OpenWrt: + case Pfsense: + case Synology: + case Tomato: + case Ubios: + } + return nil +} + // Cleanup cleans ctrld setup on the router. func Cleanup(svc *service.Config) error { name := Name() From a4c1983657905b4037e0f57d79401ea50141987f Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 26 Jun 2023 22:07:03 +0700 Subject: [PATCH 20/84] cmd/ctrld: make setDNS works on system using systemd-networkd On Ubuntu 18.04 VM with some cloud provider, using dbus call to set DNS is forbidden. A possible solution is stopping networkd entirely then using systemd-resolve to set DNS when ctrld starts. While at it, only set DNS during start command on Windows. On other platforms, "ctrld run" does set DNS in service mode already. When using systemd-resolved, only change listener address to default route interface address if a loopback address is used. Also fixing a bug in upstream tailscale code for checking in container. See tailscale/tailscale#8444 --- cmd/ctrld/cli.go | 25 ++++++++++----- cmd/ctrld/dns_proxy.go | 2 +- cmd/ctrld/os_linux.go | 64 +++++++++++++++++++++++++++++++++++++- cmd/ctrld/os_linux_test.go | 23 ++++++++++++++ cmd/ctrld/prog_linux.go | 2 ++ 5 files changed, 106 insertions(+), 10 deletions(-) create mode 100644 cmd/ctrld/os_linux_test.go diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 1b0262b..080faec 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -361,7 +361,12 @@ func initCLI() { uninstall(p, s) os.Exit(1) } - p.setDNS() + // On Linux, Darwin, Freebsd, ctrld set DNS on startup, because the DNS setting could be + // reset after rebooting. On windows, we only need to set once here. See prog.preRun in + // prog_*.go file for dedicated code on each platforms. + if runtime.GOOS == "windows" { + p.setDNS() + } } }, } @@ -781,13 +786,17 @@ func processCDFlags() { } 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() + if ip := net.ParseIP(lc.IP); ip != nil && ip.IsLoopback() { + mainLog.Warn().Msg("using loopback interface do not work with systemd-resolved") + // 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() + mainLog.Warn().Msgf("use %s as listener address", lc.IP) + } } } } diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 38a0ba4..fad8e9c 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -501,7 +501,7 @@ func inContainer() bool { return nil }) lineread.File("/proc/mounts", func(line []byte) error { - if mem.Contains(mem.B(line), mem.S("fuse.lxcfs")) { + if mem.Contains(mem.B(line), mem.S("lxcfs /proc/cpuinfo fuse.lxcfs")) { ret = true return io.EOF } diff --git a/cmd/ctrld/os_linux.go b/cmd/ctrld/os_linux.go index 307ee3a..55f6e7c 100644 --- a/cmd/ctrld/os_linux.go +++ b/cmd/ctrld/os_linux.go @@ -5,6 +5,7 @@ import ( "bytes" "context" "fmt" + "io" "net" "net/netip" "os/exec" @@ -63,8 +64,15 @@ func setDNS(iface *net.Interface, nameservers []string) error { SearchDomains: []dnsname.FQDN{}, } + trySystemdResolve := false for i := 0; i < maxSetDNSAttempts; i++ { if err := r.SetDNS(osConfig); err != nil { + if strings.Contains(err.Error(), "Rejected send message") && + strings.Contains(err.Error(), "org.freedesktop.network1.Manager") { + mainLog.Warn().Msg("Interfaces are managed by systemd-networkd, switch to systemd-resolve for setting DNS") + trySystemdResolve = true + break + } return err } currentNS := currentDNS(iface) @@ -72,6 +80,26 @@ func setDNS(iface *net.Interface, nameservers []string) error { return nil } } + if trySystemdResolve { + // Stop systemd-networkd and retry setting DNS. + if out, err := exec.Command("systemctl", "stop", "systemd-networkd").CombinedOutput(); err != nil { + return fmt.Errorf("%s: %w", string(out), err) + } + args := []string{"--interface=" + iface.Name, "--set-domain=~"} + for _, nameserver := range nameservers { + args = append(args, "--set-dns="+nameserver) + } + for i := 0; i < maxSetDNSAttempts; i++ { + if out, err := exec.Command("systemd-resolve", args...).CombinedOutput(); err != nil { + return fmt.Errorf("%s: %w", string(out), err) + } + currentNS := currentDNS(iface) + if reflect.DeepEqual(currentNS, nameservers) { + return nil + } + time.Sleep(time.Second) + } + } mainLog.Debug().Msg("DNS was not set for some reason") return nil } @@ -81,6 +109,10 @@ func resetDNS(iface *net.Interface) (err error) { if err == nil { return } + // Start systemd-networkd if present. + if exe, _ := exec.LookPath("/lib/systemd/systemd-networkd"); exe != "" { + _ = exec.Command("systemctl", "restart", "systemd-networkd").Run() + } if r, oerr := dns.NewOSConfigurator(logf, iface.Name); oerr == nil { _ = r.SetDNS(dns.OSConfig{}) if err := r.Close(); err != nil { @@ -139,7 +171,7 @@ func resetDNS(iface *net.Interface) (err error) { } func currentDNS(iface *net.Interface) []string { - for _, fn := range []getDNS{getDNSByResolvectl, getDNSByNmcli, resolvconffile.NameServers} { + for _, fn := range []getDNS{getDNSByResolvectl, getDNSBySystemdResolved, getDNSByNmcli, resolvconffile.NameServers} { if ns := fn(iface.Name); len(ns) > 0 { return ns } @@ -160,6 +192,36 @@ func getDNSByResolvectl(iface string) []string { return nil } +func getDNSBySystemdResolved(iface string) []string { + b, err := exec.Command("systemd-resolve", "--status", iface).Output() + if err != nil { + return nil + } + return getDNSBySystemdResolvedFromReader(bytes.NewReader(b)) +} + +func getDNSBySystemdResolvedFromReader(r io.Reader) []string { + scanner := bufio.NewScanner(r) + var ret []string + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if len(ret) > 0 { + if net.ParseIP(line) != nil { + ret = append(ret, line) + } + continue + } + after, found := strings.CutPrefix(line, "DNS Servers: ") + if !found { + continue + } + if net.ParseIP(after) != nil { + ret = append(ret, after) + } + } + return ret +} + func getDNSByNmcli(iface string) []string { b, err := exec.Command("nmcli", "dev", "show", iface).Output() if err != nil { diff --git a/cmd/ctrld/os_linux_test.go b/cmd/ctrld/os_linux_test.go new file mode 100644 index 0000000..671f1b4 --- /dev/null +++ b/cmd/ctrld/os_linux_test.go @@ -0,0 +1,23 @@ +package main + +import ( + "reflect" + "strings" + "testing" +) + +func Test_getDNSBySystemdResolvedFromReader(t *testing.T) { + r := strings.NewReader(`Link 2 (eth0) + Current Scopes: DNS + LLMNR setting: yes +MulticastDNS setting: no + DNSSEC setting: no + DNSSEC supported: no + DNS Servers: 8.8.8.8 + 8.8.4.4`) + want := []string{"8.8.8.8", "8.8.4.4"} + ns := getDNSBySystemdResolvedFromReader(r) + if !reflect.DeepEqual(ns, want) { + t.Logf("unexpected result, want: %v, got: %v", want, ns) + } +} diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 2c070a6..38cd1a5 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -25,6 +25,8 @@ func setDependencies(svc *service.Config) { "After=network-online.target", "Wants=NetworkManager-wait-online.service", "After=NetworkManager-wait-online.service", + "Wants=systemd-networkd-wait-online.service", + "After=systemd-networkd-wait-online.service", } // On EdeOS, ctrld needs to start after vyatta-dhcpd, so it can read leases file. if router.Name() == router.EdgeOS { From f3a3227f2140eeb0f0bc86df1bb43edaf76fc8dd Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 27 Jun 2023 01:22:32 +0700 Subject: [PATCH 21/84] all: dealing with VLAN config on Firewalla Firewalla ignores 127.0.0.1 in all VLAN config, so making 127.0.0.1 as dnsmasq upstream would break thing when multiple VLAN presents. To deal with this, we need to gather all interfaces available, and making them as upstream of dnsmasq. Then changing ctrld to listen on all interfaces, too. It also leads to better improvement for dnsmasq configuration template, as the upstream server can now be generated dynamically instead of hard coding to 127.0.0.1:5354. --- cmd/ctrld/prog.go | 8 ++++++-- internal/router/dnsmasq.go | 19 +++++++++++++++++-- internal/router/firewalla.go | 26 ++++++++++++++++++++++++++ internal/router/router.go | 7 +++++++ 4 files changed, 56 insertions(+), 4 deletions(-) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index e917f88..e09289a 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -236,10 +236,14 @@ func (p *prog) setDNS() { } logger.Debug().Msg("setting DNS for interface") ns := cfg.Listener["0"].IP - if router.Name() == router.Firewalla && ns == "127.0.0.1" { + if router.Name() == router.Firewalla && (ns == "127.0.0.1" || ns == "0.0.0.0" || ns == "") { // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. - logger.Warn().Msg("127.0.0.1 won't work on Firewalla") + if ns == "127.0.0.1" { + logger.Warn().Msg("127.0.0.1 as DNS server won't work on Firewalla") + } else { + logger.Warn().Msgf("%q could not be used as DNS server", ns) + } if netIface, err := net.InterfaceByName("br0"); err == nil { addrs, _ := netIface.Addrs() for _, addr := range addrs { diff --git a/internal/router/dnsmasq.go b/internal/router/dnsmasq.go index 9df4132..68fc78a 100644 --- a/internal/router/dnsmasq.go +++ b/internal/router/dnsmasq.go @@ -7,7 +7,9 @@ import ( const dnsMasqConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY no-resolv -server=127.0.0.1#5354 +{{- range .Upstreams}} +server={{ .Ip }}#{{ .Port }} +{{- end}} {{- if .SendClientInfo}} add-mac {{- end}} @@ -45,6 +47,11 @@ if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then fi ` +type dnsmasqUpstream struct { + Ip string + Port int +} + func dnsMasqConf() (string, error) { var sb strings.Builder var tmplText string @@ -55,10 +62,18 @@ func dnsMasqConf() (string, error) { tmplText = merlinDNSMasqPostConfTmpl } tmpl := template.Must(template.New("").Parse(tmplText)) + upstreams := []dnsmasqUpstream{{ListenIP(), ListenPort()}} + if Name() == Firewalla { + if fu := firewallaDnsmasqUpstreams(); len(fu) > 0 { + upstreams = fu + } + } var to = &struct { SendClientInfo bool + Upstreams []dnsmasqUpstream }{ - routerPlatform.Load().sendClientInfo, + SendClientInfo: routerPlatform.Load().sendClientInfo, + Upstreams: upstreams, } if err := tmpl.Execute(&sb, to); err != nil { return "", err diff --git a/internal/router/firewalla.go b/internal/router/firewalla.go index 6d57409..7e81b24 100644 --- a/internal/router/firewalla.go +++ b/internal/router/firewalla.go @@ -2,8 +2,10 @@ package router import ( "fmt" + "net" "os" "os/exec" + "path/filepath" "strings" ) @@ -78,3 +80,27 @@ func writeFirewallStartupScript() error { script := fmt.Sprintf("#!/bin/bash\n\nsudo %q %s\n", exe, argStr) return os.WriteFile(firewallaCtrldInitScriptPath, []byte(script), 0755) } + +func firewallaDnsmasqUpstreams() []dnsmasqUpstream { + matches, err := filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf") + if err != nil { + return nil + } + upstreams := make([]dnsmasqUpstream, 0, len(matches)) + for _, match := range matches { + // Trim prefix and suffix to get the iface name only. + ifaceName := strings.TrimSuffix(strings.TrimPrefix(match, "/home/pi/firerouter/etc/dnsmasq.dns."), ".conf") + if netIface, _ := net.InterfaceByName(ifaceName); netIface != nil { + addrs, _ := netIface.Addrs() + for _, addr := range addrs { + if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { + upstreams = append(upstreams, dnsmasqUpstream{ + Ip: netIP.IP.To4().String(), + Port: ListenPort(), + }) + } + } + } + } + return upstreams +} diff --git a/internal/router/router.go b/internal/router/router.go index c53178f..0c18f50 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -220,6 +220,13 @@ func Cleanup(svc *service.Config) error { // ListenIP returns the listener IP of ctrld on router. func ListenIP() string { + name := Name() + switch name { + case Firewalla: + // Firewalla excepts 127.0.0.1 in all interfaces config. So we need to listen on all interfaces, + // making dnsmasq to be able to forward DNS query to specific interface based on VLAN config. + return "0.0.0.0" + } return "127.0.0.1" } From 1aa991298a1392a8f3a0668c1cb19a58c39d3fea Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 27 Jun 2023 09:19:05 +0700 Subject: [PATCH 22/84] all: cleaning up router before waiting ntp synchronization On some Merlin routers reported by users, ctrld some how is not stopped properly. So the router does not have a working DNS at boot time to do ntp synchronization. To fix it, just clean up the router before start waiting for ntp ready. --- cmd/ctrld/cli.go | 2 +- internal/router/router.go | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 080faec..6ef1c0f 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -168,7 +168,7 @@ func initCLI() { // Processing --cd flag require connecting to ControlD API, which needs valid // time for validating server certificate. Some routers need NTP synchronization // to set the current time, so this check must happen before processCDFlags. - if err := router.PreRun(); err != nil { + if err := router.PreRun(svcConfig); err != nil { mainLog.Fatal().Err(err).Msg("failed to perform router pre-run check") } diff --git a/internal/router/router.go b/internal/router/router.go index 0c18f50..38c65b6 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -127,10 +127,13 @@ func ConfigureService(sc *service.Config) error { } // PreRun blocks until the router is ready for running ctrld. -func PreRun() (err error) { +func PreRun(svc *service.Config) (err error) { // On some routers, NTP may out of sync, so waiting for it to be ready. switch Name() { - case Merlin, Tomato: + case DDWrt, Merlin, Tomato: + // Cleanup router to ensure valid DNS for NTP synchronization. + _ = Cleanup(svc) + // Wait until `ntp_ready=1` set. b := backoff.NewBackoff("PreRun", func(format string, args ...any) {}, 10*time.Second) for { From c0c69d0739fb518569a8893d11ab6ce1fe296d59 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 27 Jun 2023 20:14:37 +0700 Subject: [PATCH 23/84] cmd/ctrld: do not assume iface "auto" in cd mode --- cmd/ctrld/cli.go | 3 --- 1 file changed, 3 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 6ef1c0f..c895f11 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -724,9 +724,6 @@ func processCDFlags() { if cdUID == "" { return } - if iface == "" { - iface = "auto" - } logger := mainLog.With().Str("mode", "cd").Logger() logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID) resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev) From 1d3f8757bc3cdcdf703e0cfa843c2cba8cb9fd36 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 28 Jun 2023 00:03:35 +0700 Subject: [PATCH 24/84] internal/router: fix missing EdgeOS in router ListenPort The EdgeOS case was removed unintentionally when adding Firewalla. --- internal/router/router.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/router/router.go b/internal/router/router.go index 38c65b6..b4575b0 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -237,7 +237,7 @@ func ListenIP() string { func ListenPort() int { name := Name() switch name { - case DDWrt, Firewalla, Merlin, OpenWrt, Synology, Tomato, Ubios: + case EdgeOS, DDWrt, Firewalla, Merlin, OpenWrt, Synology, Tomato, Ubios: return 5354 case Pfsense: // On pfsense, we run ctrld as DNS resolver. From 78a7c87ecc4eeb3ba41c8bcf3802c194dae78872 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 28 Jun 2023 10:29:37 +0700 Subject: [PATCH 25/84] cmd/ctrld: only overwrite listener if not defined in cd mode --- cmd/ctrld/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index c895f11..d536fb8 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -777,7 +777,7 @@ func processCDFlags() { } switch { case setupRouter: - if lc := cfg.Listener["0"]; lc != nil { + if lc := cfg.Listener["0"]; lc != nil && lc.IP == "" { lc.IP = router.ListenIP() lc.Port = router.ListenPort() } From aec2596262faf1564e41004e50b5c5952e4de226 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 28 Jun 2023 20:08:52 +0700 Subject: [PATCH 26/84] all: refactor router code to use interface So the code is more modular, easier to read/maintain. --- cmd/ctrld/cli.go | 56 ++-- cmd/ctrld/prog.go | 19 +- cmd/ctrld/prog_linux.go | 3 +- internal/controld/config.go | 3 +- internal/router/ddwrt.go | 72 ----- internal/router/ddwrt/ddwrt.go | 115 +++++++ internal/router/dnsmasq.go | 104 ------ internal/router/dnsmasq/dnsmasq.go | 115 +++++++ internal/router/dummy.go | 37 +++ internal/router/{ => edgeos}/edgeos.go | 156 +++++---- internal/router/firewalla.go | 106 ------- internal/router/firewalla/firewalla.go | 110 +++++++ internal/router/merlin.go | 89 ------ internal/router/merlin/merlin.go | 133 ++++++++ internal/router/{ => merlin}/merlin_test.go | 8 +- internal/router/ntp/ntp.go | 26 ++ internal/router/nvram.go | 110 ------- internal/router/nvram/nvram.go | 89 ++++++ internal/router/{ => openwrt}/openwrt.go | 82 +++-- internal/router/{ => openwrt}/procd.go | 2 +- internal/router/{ => pfsense}/pfsense.go | 89 ++++-- internal/router/router.go | 330 ++++++-------------- internal/router/service.go | 13 +- internal/router/service_ddwrt.go | 20 +- internal/router/service_merlin.go | 6 +- internal/router/service_tomato.go | 19 +- internal/router/service_ubios.go | 3 + internal/router/synology.go | 55 ---- internal/router/synology/synology.go | 88 ++++++ internal/router/tomato.go | 82 ----- internal/router/tomato/tomato.go | 131 ++++++++ internal/router/ubios.go | 73 ----- internal/router/ubios/ubios.go | 96 ++++++ 33 files changed, 1347 insertions(+), 1093 deletions(-) delete mode 100644 internal/router/ddwrt.go create mode 100644 internal/router/ddwrt/ddwrt.go delete mode 100644 internal/router/dnsmasq.go create mode 100644 internal/router/dnsmasq/dnsmasq.go create mode 100644 internal/router/dummy.go rename internal/router/{ => edgeos}/edgeos.go (52%) delete mode 100644 internal/router/firewalla.go create mode 100644 internal/router/firewalla/firewalla.go delete mode 100644 internal/router/merlin.go create mode 100644 internal/router/merlin/merlin.go rename internal/router/{ => merlin}/merlin_test.go (83%) create mode 100644 internal/router/ntp/ntp.go delete mode 100644 internal/router/nvram.go create mode 100644 internal/router/nvram/nvram.go rename internal/router/{ => openwrt}/openwrt.go (54%) rename internal/router/{ => openwrt}/procd.go (97%) rename internal/router/{ => pfsense}/pfsense.go (55%) delete mode 100644 internal/router/synology.go create mode 100644 internal/router/synology/synology.go delete mode 100644 internal/router/tomato.go create mode 100644 internal/router/tomato/tomato.go delete mode 100644 internal/router/ubios.go create mode 100644 internal/router/ubios/ubios.go diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index d536fb8..f3cfe01 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -34,6 +34,9 @@ import ( "github.com/Control-D-Inc/ctrld/internal/controld" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" "github.com/Control-D-Inc/ctrld/internal/router" + "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" + "github.com/Control-D-Inc/ctrld/internal/router/merlin" + "github.com/Control-D-Inc/ctrld/internal/router/tomato" ) var ( @@ -165,15 +168,20 @@ func initCLI() { mainLog.Fatal().Msg("network is not up yet") } + p.router = router.NewDummyRouter() + if setupRouter { + p.router = router.New(&cfg) + } + // Processing --cd flag require connecting to ControlD API, which needs valid // time for validating server certificate. Some routers need NTP synchronization // to set the current time, so this check must happen before processCDFlags. - if err := router.PreRun(svcConfig); err != nil { + if err := p.router.PreRun(); err != nil { mainLog.Fatal().Err(err).Msg("failed to perform router pre-run check") } oldLogPath := cfg.Service.LogPath - processCDFlags() + processCDFlags(p) 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) @@ -216,7 +224,7 @@ func initCLI() { if setupRouter { switch platform := router.Name(); { - case platform == router.DDWrt: + case platform == ddwrt.Name: rootCertPool = certs.CACertPool() fallthrough case platform != "": @@ -226,13 +234,13 @@ func initCLI() { } p.onStarted = append(p.onStarted, func() { mainLog.Debug().Msg("Router setup") - if err := router.Configure(&cfg); err != nil { + if err := p.router.Setup(); err != nil { mainLog.Error().Err(err).Msg("could not configure router") } }) p.onStopped = append(p.onStopped, func() { mainLog.Debug().Msg("Router cleanup") - if err := router.Cleanup(svcConfig); err != nil { + if err := p.router.Cleanup(); err != nil { mainLog.Error().Err(err).Msg("could not cleanup router") } p.resetDNS() @@ -285,7 +293,13 @@ func initCLI() { } setDependencies(sc) sc.Arguments = append([]string{"run"}, osArgs...) - if err := router.ConfigureService(sc); err != nil { + + p := &prog{router: router.NewDummyRouter()} + if setupRouter { + p.router = router.New(&cfg) + } + + if err := p.router.ConfigureService(sc); err != nil { mainLog.Fatal().Err(err).Msg("failed to configure service on router") } @@ -311,7 +325,7 @@ func initCLI() { initLogging() - processCDFlags() + processCDFlags(p) if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil { mainLog.Fatal().Msgf("invalid config: %v", err) @@ -324,7 +338,6 @@ func initCLI() { sc.Arguments = append(sc.Arguments, "--config="+defaultConfigFile) } - p := &prog{} s, err := newService(p, sc) if err != nil { mainLog.Error().Msg(err.Error()) @@ -332,7 +345,7 @@ func initCLI() { } mainLog.Debug().Msg("cleaning up router before installing") - _ = router.Cleanup(svcConfig) + _ = p.router.Cleanup() tasks := []task{ {s.Stop, false}, @@ -341,7 +354,7 @@ func initCLI() { {s.Start, true}, } if doTasks(tasks) { - if err := router.PostInstall(svcConfig); err != nil { + if err := p.router.Install(sc); err != nil { mainLog.Warn().Err(err).Msg("post installation failed, please check system/service log for details error") return } @@ -720,7 +733,7 @@ func processNoConfigFlags(noConfigStart bool) { v.Set("upstream", upstream) } -func processCDFlags() { +func processCDFlags(p *prog) { if cdUID == "" { return } @@ -778,8 +791,9 @@ func processCDFlags() { switch { case setupRouter: if lc := cfg.Listener["0"]; lc != nil && lc.IP == "" { - lc.IP = router.ListenIP() - lc.Port = router.ListenPort() + if err := p.router.Configure(); err != nil { + mainLog.Fatal().Err(err).Msg("failed to change ctrld config for router") + } } case useSystemdResolved: if lc := cfg.Listener["0"]; lc != nil { @@ -824,11 +838,12 @@ func processCDFlags() { Rules: rules, }, } - if setupRouter { - lc.IP = router.ListenIP() - lc.Port = router.ListenPort() - } cfg.Listener["0"] = lc + if setupRouter { + if err := p.router.Configure(); err != nil { + mainLog.Fatal().Err(err).Msg("failed to change ctrld config for router") + } + } } processLogAndCacheFlags() @@ -940,7 +955,7 @@ func unsupportedPlatformHelp(cmd *cobra.Command) { func userHomeDir() (string, error) { switch router.Name() { - case router.DDWrt, router.Merlin, router.Tomato: + case ddwrt.Name, merlin.Name, tomato.Name: exe, err := os.Executable() if err != nil { return "", err @@ -988,7 +1003,8 @@ func uninstall(p *prog, s service.Service) { } initLogging() if doTasks(tasks) { - if err := router.PostUninstall(svcConfig); err != nil { + r := router.New(&cfg) + if err := r.Uninstall(svcConfig); err != nil { mainLog.Warn().Err(err).Msg("post uninstallation failed, please check system/service log for details error") return } @@ -999,7 +1015,7 @@ func uninstall(p *prog, s service.Service) { mainLog.Debug().Msg("Router cleanup") // Stop already did router.Cleanup and report any error if happens, // ignoring error here to prevent false positive. - _ = router.Cleanup(svcConfig) + _ = r.Cleanup() mainLog.Notice().Msg("Service uninstalled") return } diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index e09289a..cf062b5 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -16,6 +16,10 @@ import ( "github.com/Control-D-Inc/ctrld/internal/clientinfo" "github.com/Control-D-Inc/ctrld/internal/dnscache" "github.com/Control-D-Inc/ctrld/internal/router" + "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" + "github.com/Control-D-Inc/ctrld/internal/router/firewalla" + "github.com/Control-D-Inc/ctrld/internal/router/openwrt" + "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) const defaultSemaphoreCap = 256 @@ -39,10 +43,11 @@ type prog struct { waitCh chan struct{} stopCh chan struct{} - cfg *ctrld.Config - cache dnscache.Cacher - sema semaphore - mt *clientinfo.MacTable + cfg *ctrld.Config + cache dnscache.Cacher + sema semaphore + mt *clientinfo.MacTable + router router.Router started chan struct{} onStarted []func() @@ -207,7 +212,7 @@ func (p *prog) deAllocateIP() error { func (p *prog) setDNS() { switch router.Name() { - case router.DDWrt, router.OpenWrt, router.Ubios: + case ddwrt.Name, openwrt.Name, ubios.Name: // On router, ctrld run as a DNS forwarder, it does not have to change system DNS. // Except for: // + EdgeOS, which /etc/resolv.conf could be managed by vyatta_update_resolv.pl script. @@ -236,7 +241,7 @@ func (p *prog) setDNS() { } logger.Debug().Msg("setting DNS for interface") ns := cfg.Listener["0"].IP - if router.Name() == router.Firewalla && (ns == "127.0.0.1" || ns == "0.0.0.0" || ns == "") { + if router.Name() == firewalla.Name && (ns == "127.0.0.1" || ns == "0.0.0.0" || ns == "") { // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. if ns == "127.0.0.1" { @@ -264,7 +269,7 @@ func (p *prog) setDNS() { func (p *prog) resetDNS() { switch router.Name() { - case router.DDWrt, router.OpenWrt, router.Ubios: + case ddwrt.Name, openwrt.Name, ubios.Name: // See comment in p.setDNS method. return } diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 38cd1a5..0748b51 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -5,6 +5,7 @@ import ( "github.com/Control-D-Inc/ctrld/internal/dns" "github.com/Control-D-Inc/ctrld/internal/router" + "github.com/Control-D-Inc/ctrld/internal/router/edgeos" ) func init() { @@ -29,7 +30,7 @@ func setDependencies(svc *service.Config) { "After=systemd-networkd-wait-online.service", } // On EdeOS, ctrld needs to start after vyatta-dhcpd, so it can read leases file. - if router.Name() == router.EdgeOS { + if router.Name() == edgeos.Name { svc.Dependencies = append(svc.Dependencies, "Wants=vyatta-dhcpd.service") svc.Dependencies = append(svc.Dependencies, "After=vyatta-dhcpd.service") svc.Dependencies = append(svc.Dependencies, "Wants=dnsmasq.service") diff --git a/internal/controld/config.go b/internal/controld/config.go index eef98f9..6bc5544 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -14,6 +14,7 @@ import ( "github.com/Control-D-Inc/ctrld/internal/certs" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" "github.com/Control-D-Inc/ctrld/internal/router" + "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" ) const ( @@ -92,7 +93,7 @@ func FetchResolverConfig(uid, version string, cdDev bool) (*ResolverConfig, erro return d.DialContext(ctx, network, addrs) } - if router.Name() == router.DDWrt { + if router.Name() == ddwrt.Name { transport.TLSClientConfig = &tls.Config{RootCAs: certs.CACertPool()} } client := http.Client{ diff --git a/internal/router/ddwrt.go b/internal/router/ddwrt.go deleted file mode 100644 index 92318b1..0000000 --- a/internal/router/ddwrt.go +++ /dev/null @@ -1,72 +0,0 @@ -package router - -import ( - "errors" - "fmt" - "os/exec" -) - -const ( - nvramCtrldKeyPrefix = "ctrld_" - nvramCtrldSetupKey = "ctrld_setup" - nvramCtrldInstallKey = "ctrld_install" - nvramRCStartupKey = "rc_startup" -) - -//lint:ignore ST1005 This error is for human. -var errDdwrtJffs2NotEnabled = errors.New(`could not install service without jffs, follow this guide to enable: - -https://wiki.dd-wrt.com/wiki/index.php/Journalling_Flash_File_System -`) - -func setupDDWrt() error { - // Already setup. - if val, _ := nvram("get", nvramCtrldSetupKey); val == "1" { - return nil - } - - data, err := dnsMasqConf() - if err != nil { - return err - } - - nvramKvMap := nvramSetupKV() - nvramKvMap["dnsmasq_options"] = data - if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); err != nil { - return err - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func cleanupDDWrt() error { - // Restore old configs. - if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func postInstallDDWrt() error { - return nil -} - -func ddwrtRestartDNSMasq() error { - if out, err := exec.Command("restart_dns").CombinedOutput(); err != nil { - return fmt.Errorf("restart_dns: %s, %w", string(out), err) - } - return nil -} - -func ddwrtJff2Enabled() bool { - out, _ := nvram("get", "enable_jffs2") - return out == "1" -} diff --git a/internal/router/ddwrt/ddwrt.go b/internal/router/ddwrt/ddwrt.go new file mode 100644 index 0000000..cf45b30 --- /dev/null +++ b/internal/router/ddwrt/ddwrt.go @@ -0,0 +1,115 @@ +package ddwrt + +import ( + "errors" + "fmt" + "os/exec" + + "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + "github.com/Control-D-Inc/ctrld/internal/router/ntp" + "github.com/Control-D-Inc/ctrld/internal/router/nvram" +) + +const Name = "ddwrt" + +//lint:ignore ST1005 This error is for human. +var errDdwrtJffs2NotEnabled = errors.New(`could not install service without jffs, follow this guide to enable: + +https://wiki.dd-wrt.com/wiki/index.php/Journalling_Flash_File_System +`) + +var nvramKvMap = map[string]string{ + "dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it. + "dnsmasq_options": "", // Configuration of dnsmasq set by ctrld, filled by setupDDWrt. + "dns_crypt": "0", // Disable DNSCrypt. + "dnssec": "0", // Disable DNSSEC. +} + +type Ddwrt struct { + cfg *ctrld.Config +} + +// New returns a router.Router for configuring/setup/run ctrld on ddwrt routers. +func New(cfg *ctrld.Config) *Ddwrt { + return &Ddwrt{cfg: cfg} +} + +func (d *Ddwrt) ConfigureService(config *service.Config) error { + if !ddwrtJff2Enabled() { + return errDdwrtJffs2NotEnabled + } + return nil +} + +func (d *Ddwrt) Install(_ *service.Config) error { + return nil +} + +func (d *Ddwrt) Uninstall(_ *service.Config) error { + return nil +} + +func (d *Ddwrt) PreRun() error { + _ = d.Cleanup() + return ntp.Wait() +} + +func (d *Ddwrt) Configure() error { + d.cfg.Listener["0"].IP = "127.0.0.1" + d.cfg.Listener["0"].Port = 5354 + return nil +} + +func (d *Ddwrt) Setup() error { + // Already setup. + if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { + return nil + } + + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, d.cfg) + if err != nil { + return err + } + + nvramKvMap["dnsmasq_options"] = data + if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil { + return err + } + + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func (d *Ddwrt) Cleanup() error { + if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { + nvramKvMap["dnsmasq_options"] = "" + // Restore old configs. + if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { + return err + } + } + + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func restartDNSMasq() error { + if out, err := exec.Command("restart_dns").CombinedOutput(); err != nil { + return fmt.Errorf("restart_dns: %s, %w", string(out), err) + } + return nil +} + +func ddwrtJff2Enabled() bool { + out, _ := nvram.Run("get", "enable_jffs2") + return out == "1" +} diff --git a/internal/router/dnsmasq.go b/internal/router/dnsmasq.go deleted file mode 100644 index 68fc78a..0000000 --- a/internal/router/dnsmasq.go +++ /dev/null @@ -1,104 +0,0 @@ -package router - -import ( - "strings" - "text/template" -) - -const dnsMasqConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY -no-resolv -{{- range .Upstreams}} -server={{ .Ip }}#{{ .Port }} -{{- end}} -{{- if .SendClientInfo}} -add-mac -{{- end}} -` - -const merlinDNSMasqPostConfPath = "/jffs/scripts/dnsmasq.postconf" -const merlinDNSMasqPostConfMarker = `# GENERATED BY ctrld - EOF` - -const merlinDNSMasqPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY - -#!/bin/sh - -config_file="$1" -. /usr/sbin/helper.sh - -pid=$(cat /tmp/ctrld.pid 2>/dev/null) -if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then - pc_delete "servers-file" "$config_file" # no WAN DNS settings - pc_append "no-resolv" "$config_file" # do not read /etc/resolv.conf - pc_append "server=127.0.0.1#5354" "$config_file" # use ctrld as upstream - {{- if .SendClientInfo}} - pc_append "add-mac" "$config_file" # add client mac - {{- end}} - pc_delete "dnssec" "$config_file" # disable DNSSEC - pc_delete "trust-anchor=" "$config_file" # disable DNSSEC - - # For John fork - pc_delete "resolv-file" "$config_file" # no WAN DNS settings - - # Change /etc/resolv.conf, which may be changed by WAN DNS setup - pc_delete "nameserver" /etc/resolv.conf - pc_append "nameserver 127.0.0.1" /etc/resolv.conf - - exit 0 -fi -` - -type dnsmasqUpstream struct { - Ip string - Port int -} - -func dnsMasqConf() (string, error) { - var sb strings.Builder - var tmplText string - switch Name() { - case DDWrt, EdgeOS, Firewalla, OpenWrt, Ubios, Synology, Tomato: - tmplText = dnsMasqConfigContentTmpl - case Merlin: - tmplText = merlinDNSMasqPostConfTmpl - } - tmpl := template.Must(template.New("").Parse(tmplText)) - upstreams := []dnsmasqUpstream{{ListenIP(), ListenPort()}} - if Name() == Firewalla { - if fu := firewallaDnsmasqUpstreams(); len(fu) > 0 { - upstreams = fu - } - } - var to = &struct { - SendClientInfo bool - Upstreams []dnsmasqUpstream - }{ - SendClientInfo: routerPlatform.Load().sendClientInfo, - Upstreams: upstreams, - } - if err := tmpl.Execute(&sb, to); err != nil { - return "", err - } - return sb.String(), nil -} - -func restartDNSMasq() error { - switch Name() { - case DDWrt: - return ddwrtRestartDNSMasq() - case EdgeOS: - return edgeOSRestartDNSMasq() - case Firewalla: - return firewallaRestartDNSMasq() - case Merlin: - return merlinRestartDNSMasq() - case OpenWrt: - return openwrtRestartDNSMasq() - case Ubios: - return ubiosRestartDNSMasq() - case Synology: - return synologyRestartDNSMasq() - case Tomato: - return tomatoRestartService(tomatoDNSMasqSvcName) - } - panic("not supported platform") -} diff --git a/internal/router/dnsmasq/dnsmasq.go b/internal/router/dnsmasq/dnsmasq.go new file mode 100644 index 0000000..6089c43 --- /dev/null +++ b/internal/router/dnsmasq/dnsmasq.go @@ -0,0 +1,115 @@ +package dnsmasq + +import ( + "html/template" + "net" + "path/filepath" + "strings" + + "github.com/Control-D-Inc/ctrld" +) + +const ConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY +no-resolv +{{- range .Upstreams}} +server={{ .Ip }}#{{ .Port }} +{{- end}} +{{- if .SendClientInfo}} +add-mac +{{- end}} +` + +const MerlinPostConfPath = "/jffs/scripts/dnsmasq.postconf" +const MerlinPostConfMarker = `# GENERATED BY ctrld - EOF` +const MerlinPostConfTmpl = `# GENERATED BY ctrld - DO NOT MODIFY + +#!/bin/sh + +config_file="$1" +. /usr/sbin/helper.sh + +pid=$(cat /tmp/ctrld.pid 2>/dev/null) +if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then + pc_delete "servers-file" "$config_file" # no WAN DNS settings + pc_append "no-resolv" "$config_file" # do not read /etc/resolv.conf + # use ctrld as upstream + pc_delete "server=" "$config_file" + {{- range .Upstreams}} + pc_append "server={{ .Ip }}#{{ .Port }}" "$config_file" + {{- end}} + {{- if .SendClientInfo}} + pc_append "add-mac" "$config_file" # add client mac + {{- end}} + pc_delete "dnssec" "$config_file" # disable DNSSEC + pc_delete "trust-anchor=" "$config_file" # disable DNSSEC + + # For John fork + pc_delete "resolv-file" "$config_file" # no WAN DNS settings + + # Change /etc/resolv.conf, which may be changed by WAN DNS setup + pc_delete "nameserver" /etc/resolv.conf + pc_append "nameserver 127.0.0.1" /etc/resolv.conf + + exit 0 +fi +` + +type Upstream struct { + Ip string + Port int +} + +func ConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) { + upstreams := make([]Upstream, 0, len(cfg.Listener)) + for _, listener := range cfg.Listener { + upstreams = append(upstreams, Upstream{Ip: listener.IP, Port: listener.Port}) + } + return confTmpl(tmplText, upstreams, cfg.HasUpstreamSendClientInfo()) +} + +func FirewallaConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) { + if lc := cfg.Listener["0"]; lc != nil && lc.IP == "0.0.0.0" { + return confTmpl(tmplText, firewallaUpstreams(lc.Port), cfg.HasUpstreamSendClientInfo()) + } + return ConfTmpl(tmplText, cfg) +} + +func confTmpl(tmplText string, upstreams []Upstream, sendClientInfo bool) (string, error) { + tmpl := template.Must(template.New("").Parse(tmplText)) + var to = &struct { + SendClientInfo bool + Upstreams []Upstream + }{ + SendClientInfo: sendClientInfo, + Upstreams: upstreams, + } + var sb strings.Builder + if err := tmpl.Execute(&sb, to); err != nil { + return "", err + } + return sb.String(), nil +} + +func firewallaUpstreams(port int) []Upstream { + matches, err := filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf") + if err != nil { + return nil + } + upstreams := make([]Upstream, 0, len(matches)) + for _, match := range matches { + // Trim prefix and suffix to get the iface name only. + ifaceName := strings.TrimSuffix(strings.TrimPrefix(match, "/home/pi/firerouter/etc/dnsmasq.dns."), ".conf") + if netIface, _ := net.InterfaceByName(ifaceName); netIface != nil { + addrs, _ := netIface.Addrs() + for _, addr := range addrs { + if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { + upstreams = append(upstreams, Upstream{ + Ip: netIP.IP.To4().String(), + Port: port, + }) + } + } + } + } + return upstreams +} diff --git a/internal/router/dummy.go b/internal/router/dummy.go new file mode 100644 index 0000000..71d7a82 --- /dev/null +++ b/internal/router/dummy.go @@ -0,0 +1,37 @@ +package router + +import "github.com/kardianos/service" + +type dummy struct{} + +func NewDummyRouter() Router { + return &dummy{} +} + +func (d *dummy) ConfigureService(_ *service.Config) error { + return nil +} + +func (d *dummy) Install(_ *service.Config) error { + return nil +} + +func (d *dummy) Uninstall(_ *service.Config) error { + return nil +} + +func (d *dummy) PreRun() error { + return nil +} + +func (d *dummy) Configure() error { + return nil +} + +func (d *dummy) Setup() error { + return nil +} + +func (d *dummy) Cleanup() error { + return nil +} diff --git a/internal/router/edgeos.go b/internal/router/edgeos/edgeos.go similarity index 52% rename from internal/router/edgeos.go rename to internal/router/edgeos/edgeos.go index f447608..c6b1e3c 100644 --- a/internal/router/edgeos.go +++ b/internal/router/edgeos/edgeos.go @@ -1,4 +1,4 @@ -package router +package edgeos import ( "bufio" @@ -7,52 +7,91 @@ import ( "os" "os/exec" "strings" + + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + + "github.com/Control-D-Inc/ctrld" + "github.com/kardianos/service" ) const ( + Name = "edgeos" edgeOSDNSMasqConfigPath = "/etc/dnsmasq.d/dnsmasq-zzz-ctrld.conf" - UsgDNSMasqConfigPath = "/etc/dnsmasq.conf" - UsgDNSMasqBackupConfigPath = "/etc/dnsmasq.conf.bak" + usgDNSMasqConfigPath = "/etc/dnsmasq.conf" + usgDNSMasqBackupConfigPath = "/etc/dnsmasq.conf.bak" + toggleContentFilteringLink = "https://community.ui.com/questions/UDM-Pro-disable-enable-DNS-filtering/e2cc4060-e56a-4139-b200-62d7f773ff8f" ) -var ( +var ErrContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n +To disable it, folowing instruction here: %s`, toggleContentFilteringLink) + +type EdgeOS struct { + cfg *ctrld.Config isUSG bool -) - -func setupEdgeOS() error { - if isUSG { - return setupUSG() - } - return setupUDM() } -func setupUDM() error { - // Disable dnsmasq as DNS server. - dnsMasqConfigContent, err := dnsMasqConf() - if err != nil { - return fmt.Errorf("setupUDM: generating dnsmasq config: %w", err) - } - if err := os.WriteFile(edgeOSDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { - return fmt.Errorf("setupUDM: generating dnsmasq config: %w", err) - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("setupUDM: restartDNSMasq: %w", err) +// New returns a router.Router for configuring/setup/run ctrld on EdgeOS routers. +func New(cfg *ctrld.Config) *EdgeOS { + e := &EdgeOS{cfg: cfg} + e.isUSG = checkUSG() + return e +} + +func (e *EdgeOS) ConfigureService(config *service.Config) error { + return nil +} + +func (e *EdgeOS) Install(_ *service.Config) error { + // If "Content Filtering" is enabled, UniFi OS will create firewall rules to intercept all DNS queries + // from outside, and route those queries to separated interfaces (e.g: dnsfilter-2@if79) created by UniFi OS. + // Thus, those queries will never reach ctrld listener. UniFi OS does not provide any mechanism to toggle this + // feature via command line, so there's nothing ctrld can do to disable this feature. For now, reporting an + // error and guiding users to disable the feature using UniFi OS web UI. + if ContentFilteringEnabled() { + return ErrContentFilteringEnabled } return nil } -func setupUSG() error { +func (e *EdgeOS) Uninstall(_ *service.Config) error { + return nil +} + +func (e *EdgeOS) PreRun() error { + return nil +} + +func (e *EdgeOS) Configure() error { + e.cfg.Listener["0"].IP = "127.0.0.1" + e.cfg.Listener["0"].Port = 5354 + return nil +} + +func (e *EdgeOS) Setup() error { + if e.isUSG { + return e.setupUSG() + } + return e.setupUDM() +} + +func (e *EdgeOS) Cleanup() error { + if e.isUSG { + return e.cleanupUSG() + } + return e.cleanupUDM() +} + +func (e *EdgeOS) setupUSG() error { // On USG, dnsmasq is configured to forward queries to external provider by default. // So instead of generating config in /etc/dnsmasq.d, we need to create a backup of // the config, then modify it to forward queries to ctrld listener. // Creating a backup. - buf, err := os.ReadFile(UsgDNSMasqConfigPath) + buf, err := os.ReadFile(usgDNSMasqConfigPath) if err != nil { return fmt.Errorf("setupUSG: reading current config: %w", err) } - if err := os.WriteFile(UsgDNSMasqBackupConfigPath, buf, 0600); err != nil { + if err := os.WriteFile(usgDNSMasqBackupConfigPath, buf, 0600); err != nil { return fmt.Errorf("setupUSG: backup current config: %w", err) } @@ -70,14 +109,13 @@ func setupUSG() error { sb.WriteString(line) } - // Adding ctrld listener as the only upstream. - dnsMasqConfigContent, err := dnsMasqConf() + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, e.cfg) if err != nil { - return fmt.Errorf("setupUSG: generating dnsmasq config: %w", err) + return err } sb.WriteString("\n") - sb.WriteString(dnsMasqConfigContent) - if err := os.WriteFile(UsgDNSMasqConfigPath, []byte(sb.String()), 0644); err != nil { + sb.WriteString(data) + if err := os.WriteFile(usgDNSMasqConfigPath, []byte(sb.String()), 0644); err != nil { return fmt.Errorf("setupUSG: writing dnsmasq config: %w", err) } @@ -88,14 +126,33 @@ func setupUSG() error { return nil } -func cleanupEdgeOS() error { - if isUSG { - return cleanupUSG() +func (e *EdgeOS) setupUDM() error { + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, e.cfg) + if err != nil { + return err } - return cleanupUDM() + if err := os.WriteFile(edgeOSDNSMasqConfigPath, []byte(data), 0600); err != nil { + return fmt.Errorf("setupUDM: generating dnsmasq config: %w", err) + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return fmt.Errorf("setupUDM: restartDNSMasq: %w", err) + } + return nil } -func cleanupUDM() error { +func (e *EdgeOS) cleanupUSG() error { + if err := os.Rename(usgDNSMasqBackupConfigPath, usgDNSMasqConfigPath); err != nil { + return fmt.Errorf("cleanupUSG: os.Rename: %w", err) + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return fmt.Errorf("cleanupUSG: restartDNSMasq: %w", err) + } + return nil +} + +func (e *EdgeOS) cleanupUDM() error { // Remove the custom dnsmasq config if err := os.Remove(edgeOSDNSMasqConfigPath); err != nil { return fmt.Errorf("cleanupUDM: os.Remove: %w", err) @@ -107,30 +164,17 @@ func cleanupUDM() error { return nil } -func cleanupUSG() error { - if err := os.Rename(UsgDNSMasqBackupConfigPath, UsgDNSMasqConfigPath); err != nil { - return fmt.Errorf("cleanupUSG: os.Rename: %w", err) - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("cleanupUSG: restartDNSMasq: %w", err) - } - return nil +func ContentFilteringEnabled() bool { + st, err := os.Stat("/run/dnsfilter/dnsfilter") + return err == nil && !st.IsDir() } -func postInstallEdgeOS() error { - // If "Content Filtering" is enabled, UniFi OS will create firewall rules to intercept all DNS queries - // from outside, and route those queries to separated interfaces (e.g: dnsfilter-2@if79) created by UniFi OS. - // Thus, those queries will never reach ctrld listener. UniFi OS does not provide any mechanism to toggle this - // feature via command line, so there's nothing ctrld can do to disable this feature. For now, reporting an - // error and guiding users to disable the feature using UniFi OS web UI. - if contentFilteringEnabled() { - return errContentFilteringEnabled - } - return nil +func checkUSG() bool { + out, _ := exec.Command("mca-cli-op", "info").Output() + return bytes.Contains(out, []byte("UniFi-Gateway-")) } -func edgeOSRestartDNSMasq() error { +func restartDNSMasq() error { if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil { return fmt.Errorf("edgeosRestartDNSMasq: %s, %w", string(out), err) } diff --git a/internal/router/firewalla.go b/internal/router/firewalla.go deleted file mode 100644 index 7e81b24..0000000 --- a/internal/router/firewalla.go +++ /dev/null @@ -1,106 +0,0 @@ -package router - -import ( - "fmt" - "net" - "os" - "os/exec" - "path/filepath" - "strings" -) - -const ( - firewallaDNSMasqConfigPath = "/home/pi/.firewalla/config/dnsmasq_local/ctrld" - firewallaConfigPostMainDir = "/home/pi/.firewalla/config/post_main.d" - firewallaCtrldInitScriptPath = "/home/pi/.firewalla/config/post_main.d/start_ctrld.sh" -) - -func setupFirewalla() error { - dnsMasqConfigContent, err := dnsMasqConf() - if err != nil { - return fmt.Errorf("setupFirewalla: generating dnsmasq config: %w", err) - } - if err := os.WriteFile(firewallaDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { - return fmt.Errorf("setupFirewalla: writing ctrld config: %w", err) - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("setupFirewalla: restartDNSMasq: %w", err) - } - - return nil -} - -func cleanupFirewalla() error { - // Removing current config. - if err := os.Remove(firewallaDNSMasqConfigPath); err != nil { - return fmt.Errorf("cleanupFirewalla: removing ctrld config: %w", err) - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return fmt.Errorf("cleanupFirewalla: restartDNSMasq: %w", err) - } - - return nil -} - -func postInstallFirewalla() error { - // Writing startup script. - if err := writeFirewallStartupScript(); err != nil { - return fmt.Errorf("postInstallFirewalla: writing startup script: %w", err) - } - return nil -} - -func postUninstallFirewalla() error { - // Removing startup script. - if err := os.Remove(firewallaCtrldInitScriptPath); err != nil { - return fmt.Errorf("postUninstallFirewalla: removing startup script: %w", err) - } - return nil -} - -func firewallaRestartDNSMasq() error { - return exec.Command("systemctl", "restart", "firerouter_dns").Run() -} - -func writeFirewallStartupScript() error { - if err := os.MkdirAll(firewallaConfigPostMainDir, 0775); err != nil { - return err - } - exe, err := os.Executable() - if err != nil { - return err - } - // This is called when "ctrld start ..." runs, so recording - // the same command line arguments to use in startup script. - argStr := strings.Join(os.Args[1:], " ") - script := fmt.Sprintf("#!/bin/bash\n\nsudo %q %s\n", exe, argStr) - return os.WriteFile(firewallaCtrldInitScriptPath, []byte(script), 0755) -} - -func firewallaDnsmasqUpstreams() []dnsmasqUpstream { - matches, err := filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf") - if err != nil { - return nil - } - upstreams := make([]dnsmasqUpstream, 0, len(matches)) - for _, match := range matches { - // Trim prefix and suffix to get the iface name only. - ifaceName := strings.TrimSuffix(strings.TrimPrefix(match, "/home/pi/firerouter/etc/dnsmasq.dns."), ".conf") - if netIface, _ := net.InterfaceByName(ifaceName); netIface != nil { - addrs, _ := netIface.Addrs() - for _, addr := range addrs { - if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { - upstreams = append(upstreams, dnsmasqUpstream{ - Ip: netIP.IP.To4().String(), - Port: ListenPort(), - }) - } - } - } - } - return upstreams -} diff --git a/internal/router/firewalla/firewalla.go b/internal/router/firewalla/firewalla.go new file mode 100644 index 0000000..fd4635f --- /dev/null +++ b/internal/router/firewalla/firewalla.go @@ -0,0 +1,110 @@ +package firewalla + +import ( + "fmt" + "os" + "os/exec" + "strings" + + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + + "github.com/Control-D-Inc/ctrld" + "github.com/kardianos/service" +) + +const ( + Name = "firewalla" + + firewallaDNSMasqConfigPath = "/home/pi/.firewalla/config/dnsmasq_local/ctrld" + firewallaConfigPostMainDir = "/home/pi/.firewalla/config/post_main.d" + firewallaCtrldInitScriptPath = "/home/pi/.firewalla/config/post_main.d/start_ctrld.sh" +) + +type Firewalla struct { + cfg *ctrld.Config +} + +// New returns a router.Router for configuring/setup/run ctrld on Firewalla routers. +func New(cfg *ctrld.Config) *Firewalla { + return &Firewalla{cfg: cfg} +} + +func (f *Firewalla) ConfigureService(_ *service.Config) error { + return nil +} + +func (f *Firewalla) Install(_ *service.Config) error { + // Writing startup script. + if err := writeFirewallStartupScript(); err != nil { + return fmt.Errorf("writing startup script: %w", err) + } + return nil +} + +func (f *Firewalla) Uninstall(_ *service.Config) error { + // Removing startup script. + if err := os.Remove(firewallaCtrldInitScriptPath); err != nil { + return fmt.Errorf("removing startup script: %w", err) + } + return nil +} + +func (f *Firewalla) PreRun() error { + return nil +} + +func (f *Firewalla) Configure() error { + f.cfg.Listener["0"].IP = "0.0.0.0" + f.cfg.Listener["0"].Port = 5354 + return nil +} + +func (f *Firewalla) Setup() error { + data, err := dnsmasq.FirewallaConfTmpl(dnsmasq.ConfigContentTmpl, f.cfg) + if err != nil { + return fmt.Errorf("generating dnsmasq config: %w", err) + } + if err := os.WriteFile(firewallaDNSMasqConfigPath, []byte(data), 0600); err != nil { + return fmt.Errorf("writing ctrld config: %w", err) + } + + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return fmt.Errorf("restartDNSMasq: %w", err) + } + + return nil +} + +func (f *Firewalla) Cleanup() error { + // Removing current config. + if err := os.Remove(firewallaDNSMasqConfigPath); err != nil { + return fmt.Errorf("removing ctrld config: %w", err) + } + + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return fmt.Errorf("restartDNSMasq: %w", err) + } + + return nil +} + +func writeFirewallStartupScript() error { + if err := os.MkdirAll(firewallaConfigPostMainDir, 0775); err != nil { + return err + } + exe, err := os.Executable() + if err != nil { + return err + } + // This is called when "ctrld start ..." runs, so recording + // the same command line arguments to use in startup script. + argStr := strings.Join(os.Args[1:], " ") + script := fmt.Sprintf("#!/bin/bash\n\nsudo %q %s\n", exe, argStr) + return os.WriteFile(firewallaCtrldInitScriptPath, []byte(script), 0755) +} + +func restartDNSMasq() error { + return exec.Command("systemctl", "restart", "firerouter_dns").Run() +} diff --git a/internal/router/merlin.go b/internal/router/merlin.go deleted file mode 100644 index 8e20d68..0000000 --- a/internal/router/merlin.go +++ /dev/null @@ -1,89 +0,0 @@ -package router - -import ( - "bytes" - "fmt" - "os" - "os/exec" - "strings" - "unicode" -) - -func setupMerlin() error { - buf, err := os.ReadFile(merlinDNSMasqPostConfPath) - // Already setup. - if bytes.Contains(buf, []byte(merlinDNSMasqPostConfMarker)) { - return nil - } - if err != nil && !os.IsNotExist(err) { - return err - } - - merlinDNSMasqPostConf, err := dnsMasqConf() - if err != nil { - return err - } - data := strings.Join([]string{ - merlinDNSMasqPostConf, - "\n", - merlinDNSMasqPostConfMarker, - "\n", - string(buf), - }, "\n") - // Write dnsmasq post conf file. - if err := os.WriteFile(merlinDNSMasqPostConfPath, []byte(data), 0750); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - - if err := nvramSetKV(nvramSetupKV(), nvramCtrldSetupKey); err != nil { - return err - } - - return nil -} - -func cleanupMerlin() error { - // Restore old configs. - if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil { - return err - } - buf, err := os.ReadFile(merlinDNSMasqPostConfPath) - if err != nil && !os.IsNotExist(err) { - return err - } - // Restore dnsmasq post conf file. - if err := os.WriteFile(merlinDNSMasqPostConfPath, merlinParsePostConf(buf), 0750); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func postInstallMerlin() error { - return nil -} - -func merlinRestartDNSMasq() error { - if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil { - return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err) - } - return nil -} - -func merlinParsePostConf(buf []byte) []byte { - if len(buf) == 0 { - return nil - } - parts := bytes.Split(buf, []byte(merlinDNSMasqPostConfMarker)) - if len(parts) != 1 { - return bytes.TrimLeftFunc(parts[1], unicode.IsSpace) - } - return buf -} diff --git a/internal/router/merlin/merlin.go b/internal/router/merlin/merlin.go new file mode 100644 index 0000000..9e84298 --- /dev/null +++ b/internal/router/merlin/merlin.go @@ -0,0 +1,133 @@ +package merlin + +import ( + "bytes" + "fmt" + "os" + "os/exec" + "strings" + "unicode" + + "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + "github.com/Control-D-Inc/ctrld/internal/router/ntp" + "github.com/Control-D-Inc/ctrld/internal/router/nvram" +) + +const Name = "merlin" + +var nvramKvMap = map[string]string{ + "dnspriv_enable": "0", // Ensure Merlin native DoT disabled. +} + +type Merlin struct { + cfg *ctrld.Config +} + +// New returns a router.Router for configuring/setup/run ctrld on Merlin routers. +func New(cfg *ctrld.Config) *Merlin { + return &Merlin{cfg: cfg} +} + +func (m *Merlin) ConfigureService(config *service.Config) error { + return nil +} + +func (m *Merlin) Install(_ *service.Config) error { + return nil +} + +func (m *Merlin) Uninstall(_ *service.Config) error { + return nil +} + +func (m *Merlin) PreRun() error { + _ = m.Cleanup() + return ntp.Wait() +} + +func (m *Merlin) Configure() error { + m.cfg.Listener["0"].IP = "127.0.0.1" + m.cfg.Listener["0"].Port = 5354 + return nil +} + +func (m *Merlin) Setup() error { + buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath) + // Already setup. + if bytes.Contains(buf, []byte(dnsmasq.MerlinPostConfMarker)) { + return nil + } + if err != nil && !os.IsNotExist(err) { + return err + } + + data, err := dnsmasq.ConfTmpl(dnsmasq.MerlinPostConfTmpl, m.cfg) + if err != nil { + return err + } + data = strings.Join([]string{ + data, + "\n", + dnsmasq.MerlinPostConfMarker, + "\n", + string(buf), + }, "\n") + // Write dnsmasq post conf file. + if err := os.WriteFile(dnsmasq.MerlinPostConfPath, []byte(data), 0750); err != nil { + return err + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + + if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil { + return err + } + + return nil +} + +func (m *Merlin) Cleanup() error { + if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { + // Restore old configs. + if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { + return err + } + } + + buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath) + if err != nil && !os.IsNotExist(err) { + return err + } + // Restore dnsmasq post conf file. + if err := os.WriteFile(dnsmasq.MerlinPostConfPath, merlinParsePostConf(buf), 0750); err != nil { + return err + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func restartDNSMasq() error { + if out, err := exec.Command("service", "restart_dnsmasq").CombinedOutput(); err != nil { + return fmt.Errorf("restart_dnsmasq: %s, %w", string(out), err) + } + return nil +} + +func merlinParsePostConf(buf []byte) []byte { + if len(buf) == 0 { + return nil + } + parts := bytes.Split(buf, []byte(dnsmasq.MerlinPostConfMarker)) + if len(parts) != 1 { + return bytes.TrimLeftFunc(parts[1], unicode.IsSpace) + } + return buf +} diff --git a/internal/router/merlin_test.go b/internal/router/merlin/merlin_test.go similarity index 83% rename from internal/router/merlin_test.go rename to internal/router/merlin/merlin_test.go index e1715af..057628c 100644 --- a/internal/router/merlin_test.go +++ b/internal/router/merlin/merlin_test.go @@ -1,17 +1,19 @@ -package router +package merlin import ( "bytes" "strings" "testing" + + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" ) func Test_merlinParsePostConf(t *testing.T) { origContent := "# foo" data := strings.Join([]string{ - merlinDNSMasqPostConfTmpl, + dnsmasq.MerlinPostConfTmpl, "\n", - merlinDNSMasqPostConfMarker, + dnsmasq.MerlinPostConfMarker, "\n", }, "\n") diff --git a/internal/router/ntp/ntp.go b/internal/router/ntp/ntp.go new file mode 100644 index 0000000..9854fcf --- /dev/null +++ b/internal/router/ntp/ntp.go @@ -0,0 +1,26 @@ +package ntp + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/Control-D-Inc/ctrld/internal/router/nvram" + "tailscale.com/logtail/backoff" +) + +func Wait() error { + // Wait until `ntp_ready=1` set. + b := backoff.NewBackoff("ntp.Wait", func(format string, args ...any) {}, 10*time.Second) + for { + out, err := nvram.Run("get", "ntp_ready") + if err != nil { + return fmt.Errorf("PreStart: nvram: %w", err) + } + if out == "1" { + return nil + } + b.BackOff(context.Background(), errors.New("ntp not ready")) + } +} diff --git a/internal/router/nvram.go b/internal/router/nvram.go deleted file mode 100644 index de3400e..0000000 --- a/internal/router/nvram.go +++ /dev/null @@ -1,110 +0,0 @@ -package router - -import ( - "bytes" - "fmt" - "os/exec" - "strings" -) - -func nvram(args ...string) (string, error) { - cmd := exec.Command("nvram", args...) - var stdout, stderr bytes.Buffer - cmd.Stdout = &stdout - cmd.Stderr = &stderr - if err := cmd.Run(); err != nil { - return "", fmt.Errorf("%s:%w", stderr.String(), err) - } - return strings.TrimSpace(stdout.String()), nil -} - -/* -NOTE: - - For Openwrt, DNSSEC is not included in default dnsmasq (require dnsmasq-full). - - For Merlin, DNSSEC is configured during postconf script (see merlinDNSMasqPostConfTmpl). - - For Ubios UDM Pro/Dream Machine, DNSSEC is not included in their dnsmasq package: - +https://community.ui.com/questions/Implement-DNSSEC-into-UniFi/951c72b0-4d88-4c86-9174-45417bd2f9ca - +https://community.ui.com/questions/Enable-DNSSEC-for-Unifi-Dream-Machine-FW-updates/e68e367c-d09b-4459-9444-18908f7c1ea1 -*/ -func nvramSetupKV() map[string]string { - switch Name() { - case DDWrt: - return map[string]string{ - "dns_dnsmasq": "1", // Make dnsmasq running but disable DNS ability, ctrld will replace it. - "dnsmasq_options": "", // Configuration of dnsmasq set by ctrld, filled by setupDDWrt. - "dns_crypt": "0", // Disable DNSCrypt. - "dnssec": "0", // Disable DNSSEC. - } - case Merlin: - return map[string]string{ - "dnspriv_enable": "0", // Ensure Merlin native DoT disabled. - } - case Tomato: - return map[string]string{ - "dnsmasq_custom": "", // Configuration of dnsmasq set by ctrld, filled by setupTomato. - "dnscrypt_proxy": "0", // Disable DNSCrypt. - "dnssec_enable": "0", // Disable DNSSEC. - "stubby_proxy": "0", // Disable Stubby - } - } - return nil -} - -func nvramInstallKV() map[string]string { - switch Name() { - case Tomato: - return map[string]string{ - tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method. - } - } - return nil -} - -func nvramSetKV(m map[string]string, setupKey string) error { - // Backup current value, store ctrld's configs. - for key, value := range m { - old, err := nvram("get", key) - if err != nil { - return fmt.Errorf("%s: %w", old, err) - } - if out, err := nvram("set", nvramCtrldKeyPrefix+key+"="+old); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - if out, err := nvram("set", key+"="+value); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - } - - if out, err := nvram("set", setupKey+"=1"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - // Commit. - if out, err := nvram("commit"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - return nil -} - -func nvramRestore(m map[string]string, setupKey string) error { - // Restore old configs. - for key := range m { - ctrldKey := nvramCtrldKeyPrefix + key - old, err := nvram("get", ctrldKey) - if err != nil { - return fmt.Errorf("%s: %w", old, err) - } - _, _ = nvram("unset", ctrldKey) - if out, err := nvram("set", key+"="+old); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - } - - if out, err := nvram("unset", setupKey); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - // Commit. - if out, err := nvram("commit"); err != nil { - return fmt.Errorf("%s: %w", out, err) - } - return nil -} diff --git a/internal/router/nvram/nvram.go b/internal/router/nvram/nvram.go new file mode 100644 index 0000000..e76c017 --- /dev/null +++ b/internal/router/nvram/nvram.go @@ -0,0 +1,89 @@ +package nvram + +import ( + "bytes" + "fmt" + "os/exec" + "strings" +) + +const ( + CtrldKeyPrefix = "ctrld_" + CtrldSetupKey = "ctrld_setup" + CtrldInstallKey = "ctrld_install" + RCStartupKey = "rc_startup" +) + +// Run runs the given nvram command. +func Run(args ...string) (string, error) { + cmd := exec.Command("nvram", args...) + var stdout, stderr bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = &stderr + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("%s:%w", stderr.String(), err) + } + return strings.TrimSpace(stdout.String()), nil +} + +/* +NOTE: + - For Openwrt, DNSSEC is not included in default dnsmasq (require dnsmasq-full). + - For Merlin, DNSSEC is configured during postconf script (see merlinDNSMasqPostConfTmpl). + - For Ubios UDM Pro/Dream Machine, DNSSEC is not included in their dnsmasq package: + +https://community.ui.com/questions/Implement-DNSSEC-into-UniFi/951c72b0-4d88-4c86-9174-45417bd2f9ca + +https://community.ui.com/questions/Enable-DNSSEC-for-Unifi-Dream-Machine-FW-updates/e68e367c-d09b-4459-9444-18908f7c1ea1 +*/ + +// SetKV writes the given key/value from map to nvram. +// The given setupKey is set to 1 to indicates key/value set. +func SetKV(m map[string]string, setupKey string) error { + // Backup current value, store ctrld's configs. + for key, value := range m { + old, err := Run("get", key) + if err != nil { + return fmt.Errorf("%s: %w", old, err) + } + if out, err := Run("set", CtrldKeyPrefix+key+"="+old); err != nil { + return fmt.Errorf("%s: %w", out, err) + } + if out, err := Run("set", key+"="+value); err != nil { + return fmt.Errorf("%s: %w", out, err) + } + } + + if out, err := Run("set", setupKey+"=1"); err != nil { + return fmt.Errorf("%s: %w", out, err) + } + // Commit. + if out, err := Run("commit"); err != nil { + return fmt.Errorf("%s: %w", out, err) + } + return nil +} + +// Restore restores the old value of given key from map m. +// The given setupKey is set to 0 to indicates key/value restored. +func Restore(m map[string]string, setupKey string) error { + // Restore old configs. + for key := range m { + ctrldKey := CtrldKeyPrefix + key + old, err := Run("get", ctrldKey) + if err != nil { + return fmt.Errorf("%s: %w", old, err) + } + _, _ = Run("unset", ctrldKey) + if out, err := Run("set", key+"="+old); err != nil { + return fmt.Errorf("%s: %w", out, err) + } + } + + if out, err := Run("unset", setupKey); err != nil { + return fmt.Errorf("%s: %w", out, err) + } + // Commit. + if out, err := Run("commit"); err != nil { + return fmt.Errorf("%s: %w", out, err) + } + return nil +} diff --git a/internal/router/openwrt.go b/internal/router/openwrt/openwrt.go similarity index 54% rename from internal/router/openwrt.go rename to internal/router/openwrt/openwrt.go index afc25ae..bd08b12 100644 --- a/internal/router/openwrt.go +++ b/internal/router/openwrt/openwrt.go @@ -1,4 +1,4 @@ -package router +package openwrt import ( "bytes" @@ -7,42 +7,64 @@ import ( "os" "os/exec" "strings" + + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + + "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld" +) + +const ( + Name = "openwrt" + openwrtDNSMasqConfigPath = "/tmp/dnsmasq.d/ctrld.conf" ) var errUCIEntryNotFound = errors.New("uci: Entry not found") -const openwrtDNSMasqConfigPath = "/tmp/dnsmasq.d/ctrld.conf" - -// IsGLiNet reports whether the router is an GL.iNet router. -func IsGLiNet() bool { - if Name() != OpenWrt { - return false - } - buf, _ := os.ReadFile("/proc/version") - // The output of /proc/version contains "(glinet@glinet)". - return bytes.Contains(buf, []byte(" (glinet")) +type Openwrt struct { + cfg *ctrld.Config } -// IsOldOpenwrt reports whether the router is an "old" version of Openwrt, -// aka versions which don't have "service" command. -func IsOldOpenwrt() bool { - if Name() != OpenWrt { - return false - } - cmd, _ := exec.LookPath("service") - return cmd == "" +// New returns a router.Router for configuring/setup/run ctrld on Openwrt routers. +func New(cfg *ctrld.Config) *Openwrt { + return &Openwrt{cfg: cfg} } -func setupOpenWrt() error { +func (o *Openwrt) ConfigureService(svc *service.Config) error { + svc.Option["SysvScript"] = openWrtScript + return nil +} + +func (o *Openwrt) Install(config *service.Config) error { + return exec.Command("/etc/init.d/ctrld", "enable").Run() +} + +func (o *Openwrt) Uninstall(config *service.Config) error { + return nil +} + +func (o *Openwrt) PreRun() error { + return nil +} + +func (o *Openwrt) Configure() error { + o.cfg.Listener["0"].IP = "127.0.0.1" + o.cfg.Listener["0"].Port = 5354 + return nil +} + +func (o *Openwrt) Setup() error { // Delete dnsmasq port if set. if _, err := uci("delete", "dhcp.@dnsmasq[0].port"); err != nil && !errors.Is(err, errUCIEntryNotFound) { return err } - dnsMasqConfigContent, err := dnsMasqConf() + + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, o.cfg) if err != nil { return err } - if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { + if err := os.WriteFile(openwrtDNSMasqConfigPath, []byte(data), 0600); err != nil { return err } // Commit. @@ -56,7 +78,7 @@ func setupOpenWrt() error { return nil } -func cleanupOpenWrt() error { +func (o *Openwrt) Cleanup() error { // Remove the custom dnsmasq config if err := os.Remove(openwrtDNSMasqConfigPath); err != nil { return err @@ -68,8 +90,11 @@ func cleanupOpenWrt() error { return nil } -func postInstallOpenWrt() error { - return exec.Command("/etc/init.d/ctrld", "enable").Run() +func restartDNSMasq() error { + if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil { + return fmt.Errorf("%s: %w", string(out), err) + } + return nil } func uci(args ...string) (string, error) { @@ -85,10 +110,3 @@ func uci(args ...string) (string, error) { } return strings.TrimSpace(stdout.String()), nil } - -func openwrtRestartDNSMasq() error { - if out, err := exec.Command("/etc/init.d/dnsmasq", "restart").CombinedOutput(); err != nil { - return fmt.Errorf("%s: %w", string(out), err) - } - return nil -} diff --git a/internal/router/procd.go b/internal/router/openwrt/procd.go similarity index 97% rename from internal/router/procd.go rename to internal/router/openwrt/procd.go index d363f39..8e74461 100644 --- a/internal/router/procd.go +++ b/internal/router/openwrt/procd.go @@ -1,4 +1,4 @@ -package router +package openwrt const openWrtScript = `#!/bin/sh /etc/rc.common USE_PROCD=1 diff --git a/internal/router/pfsense.go b/internal/router/pfsense/pfsense.go similarity index 55% rename from internal/router/pfsense.go rename to internal/router/pfsense/pfsense.go index 3818a58..d724c23 100644 --- a/internal/router/pfsense.go +++ b/internal/router/pfsense/pfsense.go @@ -1,4 +1,4 @@ -package router +package pfsense import ( "fmt" @@ -6,46 +6,18 @@ import ( "os/exec" "path/filepath" + "github.com/Control-D-Inc/ctrld" "github.com/kardianos/service" ) const ( + Name = "pfsens" + rcPath = "/usr/local/etc/rc.d" unboundRcPath = rcPath + "/unbound" dnsmasqRcPath = rcPath + "/dnsmasq" ) -func setupPfsense() error { - // If Pfsense is in DNS Resolver mode, ensure no unbound processes running. - _ = exec.Command("killall", "unbound").Run() - - // If Pfsense is in DNS Forwarder mode, ensure no dnsmasq processes running. - _ = exec.Command("killall", "dnsmasq").Run() - return nil -} - -func cleanupPfsense(svc *service.Config) error { - if err := os.Remove(filepath.Join(rcPath, svc.Name+".sh")); err != nil { - return fmt.Errorf("os.Remove: %w", err) - } - _ = exec.Command(unboundRcPath, "onerestart").Run() - _ = exec.Command(dnsmasqRcPath, "onerestart").Run() - - return nil -} - -func postInstallPfsense(svc *service.Config) error { - // pfsense need ".sh" extension for script to be run at boot. - // See: https://docs.netgate.com/pfsense/en/latest/development/boot-commands.html#shell-script-option - oldname := filepath.Join(rcPath, svc.Name) - newname := filepath.Join(rcPath, svc.Name+".sh") - _ = os.Remove(newname) - if err := os.Symlink(oldname, newname); err != nil { - return fmt.Errorf("os.Symlink: %w", err) - } - return nil -} - const pfsenseInitScript = `#!/bin/sh # PROVIDE: {{.Name}} @@ -64,3 +36,56 @@ command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}" run_rc_command "$1" ` + +type Pfsense struct { + cfg *ctrld.Config + svcName string +} + +// New returns a router.Router for configuring/setup/run ctrld on Pfsense routers. +func New(cfg *ctrld.Config) *Pfsense { + return &Pfsense{cfg: cfg} +} + +func (p *Pfsense) ConfigureService(svc *service.Config) error { + svc.Option["SysvScript"] = pfsenseInitScript + p.svcName = svc.Name + return nil +} + +func (p *Pfsense) Install(config *service.Config) error { + return nil +} + +func (p *Pfsense) Uninstall(config *service.Config) error { + return nil +} + +func (p *Pfsense) PreRun() error { + return nil +} + +func (p *Pfsense) Configure() error { + p.cfg.Listener["0"].IP = "127.0.0.1" + p.cfg.Listener["0"].Port = 53 + return nil +} + +func (p *Pfsense) Setup() error { + // If Pfsense is in DNS Resolver mode, ensure no unbound processes running. + _ = exec.Command("killall", "unbound").Run() + + // If Pfsense is in DNS Forwarder mode, ensure no dnsmasq processes running. + _ = exec.Command("killall", "dnsmasq").Run() + return nil +} + +func (p *Pfsense) Cleanup() error { + if err := os.Remove(filepath.Join(rcPath, p.svcName+".sh")); err != nil { + return fmt.Errorf("os.Remove: %w", err) + } + _ = exec.Command(unboundRcPath, "onerestart").Run() + _ = exec.Command(dnsmasqRcPath, "onerestart").Run() + + return nil +} diff --git a/internal/router/router.go b/internal/router/router.go index b4575b0..257e3b4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -2,34 +2,90 @@ package router import ( "bytes" - "context" - "errors" - "fmt" "os" "os/exec" "sync/atomic" - "time" "github.com/kardianos/service" - "tailscale.com/logtail/backoff" "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" + "github.com/Control-D-Inc/ctrld/internal/router/edgeos" + "github.com/Control-D-Inc/ctrld/internal/router/firewalla" + "github.com/Control-D-Inc/ctrld/internal/router/merlin" + "github.com/Control-D-Inc/ctrld/internal/router/openwrt" + "github.com/Control-D-Inc/ctrld/internal/router/pfsense" + "github.com/Control-D-Inc/ctrld/internal/router/synology" + "github.com/Control-D-Inc/ctrld/internal/router/tomato" + "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) -const ( - DDWrt = "ddwrt" - EdgeOS = "edgeos" - Firewalla = "firewalla" - Merlin = "merlin" - OpenWrt = "openwrt" - Pfsense = "pfsense" - Synology = "synology" - Tomato = "tomato" - Ubios = "ubios" -) +// Service is the interface to manage ctrld service on router. +type Service interface { + ConfigureService(*service.Config) error + Install(*service.Config) error + Uninstall(*service.Config) error +} -// ErrNotSupported reports the current router is not supported error. -var ErrNotSupported = errors.New("unsupported platform") +// Config is the interface to manage ctrld config on router. +type Config interface { + Configure() error +} + +// Router is the interface for managing ctrld running on router. +type Router interface { + Service + Config + + PreRun() error + Setup() error + Cleanup() error +} + +// New returns new Router interface. +func New(cfg *ctrld.Config) Router { + switch Name() { + case ddwrt.Name: + return ddwrt.New(cfg) + case merlin.Name: + return merlin.New(cfg) + case openwrt.Name: + return openwrt.New(cfg) + case edgeos.Name: + return edgeos.New(cfg) + case ubios.Name: + return ubios.New(cfg) + case synology.Name: + return synology.New(cfg) + case tomato.Name: + return tomato.New(cfg) + case pfsense.Name: + return pfsense.New(cfg) + case firewalla.Name: + return firewalla.New(cfg) + } + return NewDummyRouter() +} + +// IsGLiNet reports whether the router is an GL.iNet router. +func IsGLiNet() bool { + if Name() != openwrt.Name { + return false + } + buf, _ := os.ReadFile("/proc/version") + // The output of /proc/version contains "(glinet@glinet)". + return bytes.Contains(buf, []byte(" (glinet")) +} + +// IsOldOpenwrt reports whether the router is an "old" version of Openwrt, +// aka versions which don't have "service" command. +func IsOldOpenwrt() bool { + if Name() != openwrt.Name { + return false + } + cmd, _ := exec.LookPath("service") + return cmd == "" +} var routerPlatform atomic.Pointer[router] @@ -41,15 +97,15 @@ type router struct { // IsSupported reports whether the given platform is supported by ctrld. func IsSupported(platform string) bool { switch platform { - case DDWrt, - EdgeOS, - Firewalla, - Merlin, - OpenWrt, - Pfsense, - Synology, - Tomato, - Ubios: + case ddwrt.Name, + edgeos.Name, + firewalla.Name, + merlin.Name, + openwrt.Name, + pfsense.Name, + synology.Name, + tomato.Name, + ubios.Name: return true } return false @@ -58,193 +114,18 @@ func IsSupported(platform string) bool { // SupportedPlatforms return all platforms that can be configured to run with ctrld. func SupportedPlatforms() []string { return []string{ - DDWrt, - EdgeOS, - Firewalla, - Merlin, - OpenWrt, - Pfsense, - Synology, - Tomato, - Ubios, + ddwrt.Name, + edgeos.Name, + firewalla.Name, + merlin.Name, + openwrt.Name, + pfsense.Name, + synology.Name, + tomato.Name, + ubios.Name, } } -var configureFunc = map[string]func() error{ - DDWrt: setupDDWrt, - EdgeOS: setupEdgeOS, - Firewalla: setupFirewalla, - Merlin: setupMerlin, - OpenWrt: setupOpenWrt, - Pfsense: setupPfsense, - Synology: setupSynology, - Tomato: setupTomato, - Ubios: setupUbiOS, -} - -// Configure configures things for running ctrld on the router. -func Configure(c *ctrld.Config) error { - name := Name() - switch name { - case DDWrt, - EdgeOS, - Firewalla, - Merlin, - OpenWrt, - Pfsense, - Synology, - Tomato, - Ubios: - if c.HasUpstreamSendClientInfo() { - r := routerPlatform.Load() - r.sendClientInfo = true - } - configure := configureFunc[name] - if err := configure(); err != nil { - return err - } - return nil - default: - return ErrNotSupported - } -} - -// ConfigureService performs necessary setup for running ctrld as a service on router. -func ConfigureService(sc *service.Config) error { - name := Name() - switch name { - case DDWrt: - if !ddwrtJff2Enabled() { - return errDdwrtJffs2NotEnabled - } - case OpenWrt: - sc.Option["SysvScript"] = openWrtScript - case Pfsense: - sc.Option["SysvScript"] = pfsenseInitScript - case EdgeOS, Firewalla, Merlin, Synology, Tomato, Ubios: - } - return nil -} - -// PreRun blocks until the router is ready for running ctrld. -func PreRun(svc *service.Config) (err error) { - // On some routers, NTP may out of sync, so waiting for it to be ready. - switch Name() { - case DDWrt, Merlin, Tomato: - // Cleanup router to ensure valid DNS for NTP synchronization. - _ = Cleanup(svc) - - // Wait until `ntp_ready=1` set. - b := backoff.NewBackoff("PreRun", func(format string, args ...any) {}, 10*time.Second) - for { - out, err := nvram("get", "ntp_ready") - if err != nil { - return fmt.Errorf("PreStart: nvram: %w", err) - } - if out == "1" { - return nil - } - b.BackOff(context.Background(), errors.New("ntp not ready")) - } - default: - return nil - } -} - -// PostInstall performs task after installing ctrld on router. -func PostInstall(svc *service.Config) error { - name := Name() - switch name { - case DDWrt: - return postInstallDDWrt() - case EdgeOS: - return postInstallEdgeOS() - case Firewalla: - return postInstallFirewalla() - case Merlin: - return postInstallMerlin() - case OpenWrt: - return postInstallOpenWrt() - case Pfsense: - return postInstallPfsense(svc) - case Synology: - return postInstallSynology() - case Tomato: - return postInstallTomato() - case Ubios: - return postInstallUbiOS() - } - return nil -} - -// PostUninstall performs task after uninstalling ctrld on router. -func PostUninstall(svc *service.Config) error { - name := Name() - switch name { - case DDWrt: - case EdgeOS: - case Firewalla: - return postUninstallFirewalla() - case Merlin: - case OpenWrt: - case Pfsense: - case Synology: - case Tomato: - case Ubios: - } - return nil -} - -// Cleanup cleans ctrld setup on the router. -func Cleanup(svc *service.Config) error { - name := Name() - switch name { - case DDWrt: - return cleanupDDWrt() - case EdgeOS: - return cleanupEdgeOS() - case Firewalla: - return cleanupFirewalla() - case Merlin: - return cleanupMerlin() - case OpenWrt: - return cleanupOpenWrt() - case Pfsense: - return cleanupPfsense(svc) - case Synology: - return cleanupSynology() - case Tomato: - return cleanupTomato() - case Ubios: - return cleanupUbiOS() - } - return nil -} - -// ListenIP returns the listener IP of ctrld on router. -func ListenIP() string { - name := Name() - switch name { - case Firewalla: - // Firewalla excepts 127.0.0.1 in all interfaces config. So we need to listen on all interfaces, - // making dnsmasq to be able to forward DNS query to specific interface based on VLAN config. - return "0.0.0.0" - } - return "127.0.0.1" -} - -// ListenPort returns the listener port of ctrld on router. -func ListenPort() int { - name := Name() - switch name { - case EdgeOS, DDWrt, Firewalla, Merlin, OpenWrt, Synology, Tomato, Ubios: - return 5354 - case Pfsense: - // On pfsense, we run ctrld as DNS resolver. - } - return 53 -} - // Name returns name of the router platform. func Name() string { if r := routerPlatform.Load(); r != nil { @@ -259,27 +140,25 @@ func Name() string { func distroName() string { switch { case bytes.HasPrefix(unameO(), []byte("DD-WRT")): - return DDWrt + return ddwrt.Name case bytes.HasPrefix(unameO(), []byte("ASUSWRT-Merlin")): - return Merlin + return merlin.Name case haveFile("/etc/openwrt_version"): - return OpenWrt + return openwrt.Name case haveDir("/data/unifi"): - return Ubios + return ubios.Name case bytes.HasPrefix(unameU(), []byte("synology")): - return Synology + return synology.Name case bytes.HasPrefix(unameO(), []byte("Tomato")): - return Tomato + return tomato.Name case haveDir("/config/scripts/post-config.d"): - checkUSG() - return EdgeOS + return edgeos.Name case haveFile("/etc/ubnt/init/vyatta-router"): - checkUSG() - return EdgeOS // For 2.x + return edgeos.Name // For 2.x case isPfsense(): - return Pfsense + return pfsense.Name case haveFile("/etc/firewalla_release"): - return Firewalla + return firewalla.Name } return "" } @@ -308,8 +187,3 @@ func isPfsense() bool { b, err := os.ReadFile("/etc/platform") return err == nil && bytes.HasPrefix(b, []byte("pfSense")) } - -func checkUSG() { - out, _ := exec.Command("mca-cli-op", "info").Output() - isUSG = bytes.Contains(out, []byte("UniFi-Gateway-")) -} diff --git a/internal/router/service.go b/internal/router/service.go index d9476e9..3333964 100644 --- a/internal/router/service.go +++ b/internal/router/service.go @@ -6,13 +6,18 @@ import ( "os/exec" "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" + "github.com/Control-D-Inc/ctrld/internal/router/merlin" + "github.com/Control-D-Inc/ctrld/internal/router/tomato" + "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) func init() { systems := []service.System{ &linuxSystemService{ name: "ddwrt", - detect: func() bool { return Name() == DDWrt }, + detect: func() bool { return Name() == ddwrt.Name }, interactive: func() bool { is, _ := isInteractive() return is @@ -21,7 +26,7 @@ func init() { }, &linuxSystemService{ name: "merlin", - detect: func() bool { return Name() == Merlin }, + detect: func() bool { return Name() == merlin.Name }, interactive: func() bool { is, _ := isInteractive() return is @@ -31,7 +36,7 @@ func init() { &linuxSystemService{ name: "ubios", detect: func() bool { - if Name() != Ubios { + if Name() != ubios.Name { return false } out, err := exec.Command("ubnt-device-info", "firmware").CombinedOutput() @@ -50,7 +55,7 @@ func init() { }, &linuxSystemService{ name: "tomato", - detect: func() bool { return Name() == Tomato }, + detect: func() bool { return Name() == tomato.Name }, interactive: func() bool { is, _ := isInteractive() return is diff --git a/internal/router/service_ddwrt.go b/internal/router/service_ddwrt.go index ac177f9..3e8b9bf 100644 --- a/internal/router/service_ddwrt.go +++ b/internal/router/service_ddwrt.go @@ -12,6 +12,8 @@ import ( "text/template" "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld/internal/router/nvram" ) type ddwrtSvc struct { @@ -94,19 +96,19 @@ func (s *ddwrtSvc) Install() error { return err } s.rcStartup = sb.String() - curVal, err := nvram("get", nvramRCStartupKey) + curVal, err := nvram.Run("get", nvram.RCStartupKey) if err != nil { return err } - if _, err := nvram("set", nvramCtrldKeyPrefix+nvramRCStartupKey+"="+curVal); err != nil { + if _, err := nvram.Run("set", nvram.CtrldKeyPrefix+nvram.RCStartupKey+"="+curVal); err != nil { return err } val := strings.Join([]string{curVal, s.rcStartup + " &", fmt.Sprintf(`echo $! > "/tmp/%s.pid"`, s.Config.Name)}, "\n") - if _, err := nvram("set", nvramRCStartupKey+"="+val); err != nil { + if _, err := nvram.Run("set", nvram.RCStartupKey+"="+val); err != nil { return err } - if out, err := nvram("commit"); err != nil { + if out, err := nvram.Run("commit"); err != nil { return fmt.Errorf("%s: %w", out, err) } @@ -118,16 +120,16 @@ func (s *ddwrtSvc) Uninstall() error { return err } - ctrldStartupKey := nvramCtrldKeyPrefix + nvramRCStartupKey - rcStartup, err := nvram("get", ctrldStartupKey) + ctrldStartupKey := nvram.CtrldKeyPrefix + nvram.RCStartupKey + rcStartup, err := nvram.Run("get", ctrldStartupKey) if err != nil { return err } - _, _ = nvram("unset", ctrldStartupKey) - if _, err := nvram("set", nvramRCStartupKey+"="+rcStartup); err != nil { + _, _ = nvram.Run("unset", ctrldStartupKey) + if _, err := nvram.Run("set", nvram.RCStartupKey+"="+rcStartup); err != nil { return err } - if out, err := nvram("commit"); err != nil { + if out, err := nvram.Run("commit"); err != nil { return fmt.Errorf("%s: %w", out, err) } diff --git a/internal/router/service_merlin.go b/internal/router/service_merlin.go index 3878c71..9273eca 100644 --- a/internal/router/service_merlin.go +++ b/internal/router/service_merlin.go @@ -13,6 +13,8 @@ import ( "text/template" "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld/internal/router/nvram" ) const ( @@ -67,10 +69,10 @@ func (s *merlinSvc) Install() error { if !strings.HasPrefix(exePath, "/jffs/") { return errors.New("could not install service outside /jffs") } - if _, err := nvram("set", "jffs2_scripts=1"); err != nil { + if _, err := nvram.Run("set", "jffs2_scripts=1"); err != nil { return err } - if _, err := nvram("commit"); err != nil { + if _, err := nvram.Run("commit"); err != nil { return err } diff --git a/internal/router/service_tomato.go b/internal/router/service_tomato.go index 8b7590c..aa96d4b 100644 --- a/internal/router/service_tomato.go +++ b/internal/router/service_tomato.go @@ -12,6 +12,8 @@ import ( "text/template" "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld/internal/router/nvram" ) const tomatoNvramScriptWanupKey = "script_wanup" @@ -63,10 +65,10 @@ func (s *tomatoSvc) Install() error { if !strings.HasPrefix(exePath, "/jffs/") { return errors.New("could not install service outside /jffs") } - if _, err := nvram("set", "jffs2_on=1"); err != nil { + if _, err := nvram.Run("set", "jffs2_on=1"); err != nil { return err } - if _, err := nvram("commit"); err != nil { + if _, err := nvram.Run("commit"); err != nil { return err } @@ -97,13 +99,15 @@ func (s *tomatoSvc) Install() error { return fmt.Errorf("os.Chmod: startup script: %w", err) } - nvramKvMap := nvramInstallKV() - old, err := nvram("get", tomatoNvramScriptWanupKey) + nvramKvMap := map[string]string{ + tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method. + } + old, err := nvram.Run("get", tomatoNvramScriptWanupKey) if err != nil { return fmt.Errorf("nvram: %w", err) } nvramKvMap[tomatoNvramScriptWanupKey] = strings.Join([]string{old, s.configPath() + " start"}, "\n") - if err := nvramSetKV(nvramKvMap, nvramCtrldInstallKey); err != nil { + if err := nvram.SetKV(nvramKvMap, nvram.CtrldInstallKey); err != nil { return err } return nil @@ -113,8 +117,11 @@ func (s *tomatoSvc) Uninstall() error { if err := os.Remove(s.configPath()); err != nil { return fmt.Errorf("os.Remove: %w", err) } + nvramKvMap := map[string]string{ + tomatoNvramScriptWanupKey: "", // script to start ctrld, filled by tomatoSvc.Install method. + } // Restore old configs. - if err := nvramRestore(nvramInstallKV(), nvramCtrldInstallKey); err != nil { + if err := nvram.Restore(nvramKvMap, nvram.CtrldInstallKey); err != nil { return err } return nil diff --git a/internal/router/service_ubios.go b/internal/router/service_ubios.go index 5c4d99d..0b49cd2 100644 --- a/internal/router/service_ubios.go +++ b/internal/router/service_ubios.go @@ -18,6 +18,9 @@ import ( // This is a copy of https://github.com/kardianos/service/blob/v1.2.1/service_sysv_linux.go, // with modification for supporting ubios v1 init system. +// Keep in sync with ubios.ubiosDNSMasqConfigPath +const ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf" + type ubiosSvc struct { i service.Interface platform string diff --git a/internal/router/synology.go b/internal/router/synology.go deleted file mode 100644 index 8c1d1d6..0000000 --- a/internal/router/synology.go +++ /dev/null @@ -1,55 +0,0 @@ -package router - -import ( - "fmt" - "os" - "os/exec" -) - -const ( - synologyDNSMasqConfigPath = "/etc/dhcpd/dhcpd-zzz-ctrld.conf" - synologyDhcpdInfoPath = "/etc/dhcpd/dhcpd-zzz-ctrld.info" -) - -func setupSynology() error { - dnsMasqConfigContent, err := dnsMasqConf() - if err != nil { - return err - } - if err := os.WriteFile(synologyDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { - return err - } - if err := os.WriteFile(synologyDhcpdInfoPath, []byte(`enable="yes"`), 0600); err != nil { - return err - } - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func cleanupSynology() error { - // Remove the custom config files. - for _, f := range []string{synologyDNSMasqConfigPath, synologyDhcpdInfoPath} { - if err := os.Remove(f); err != nil { - return err - } - } - - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func postInstallSynology() error { - return nil -} - -func synologyRestartDNSMasq() error { - if out, err := exec.Command("/etc/rc.network", "nat-restart-dhcp").CombinedOutput(); err != nil { - return fmt.Errorf("synologyRestartDNSMasq: %s - %w", string(out), err) - } - return nil -} diff --git a/internal/router/synology/synology.go b/internal/router/synology/synology.go new file mode 100644 index 0000000..e1d51bd --- /dev/null +++ b/internal/router/synology/synology.go @@ -0,0 +1,88 @@ +package synology + +import ( + "fmt" + "os" + "os/exec" + + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + + "github.com/Control-D-Inc/ctrld" + "github.com/kardianos/service" +) + +const ( + Name = "synology" + + synologyDNSMasqConfigPath = "/etc/dhcpd/dhcpd-zzz-ctrld.conf" + synologyDhcpdInfoPath = "/etc/dhcpd/dhcpd-zzz-ctrld.info" +) + +type Synology struct { + cfg *ctrld.Config +} + +// New returns a router.Router for configuring/setup/run ctrld on Ubios routers. +func New(cfg *ctrld.Config) *Synology { + return &Synology{cfg: cfg} +} + +func (s *Synology) ConfigureService(config *service.Config) error { + return nil +} + +func (s *Synology) Install(_ *service.Config) error { + return nil +} + +func (s *Synology) Uninstall(_ *service.Config) error { + return nil +} + +func (s *Synology) PreRun() error { + return nil +} + +func (s *Synology) Configure() error { + s.cfg.Listener["0"].IP = "127.0.0.1" + s.cfg.Listener["0"].Port = 5354 + return nil +} + +func (s *Synology) Setup() error { + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, s.cfg) + if err != nil { + return err + } + if err := os.WriteFile(synologyDNSMasqConfigPath, []byte(data), 0600); err != nil { + return err + } + if err := os.WriteFile(synologyDhcpdInfoPath, []byte(`enable="yes"`), 0600); err != nil { + return err + } + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func (s *Synology) Cleanup() error { + // Remove the custom config files. + for _, f := range []string{synologyDNSMasqConfigPath, synologyDhcpdInfoPath} { + if err := os.Remove(f); err != nil { + return err + } + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func restartDNSMasq() error { + if out, err := exec.Command("/etc/rc.network", "nat-restart-dhcp").CombinedOutput(); err != nil { + return fmt.Errorf("synologyRestartDNSMasq: %s - %w", string(out), err) + } + return nil +} diff --git a/internal/router/tomato.go b/internal/router/tomato.go deleted file mode 100644 index 945e992..0000000 --- a/internal/router/tomato.go +++ /dev/null @@ -1,82 +0,0 @@ -package router - -import ( - "fmt" - "os/exec" -) - -const ( - tomatoDnsCryptProxySvcName = "dnscrypt-proxy" - tomatoStubbySvcName = "stubby" - tomatoDNSMasqSvcName = "dnsmasq" -) - -func setupTomato() error { - // Already setup. - if val, _ := nvram("get", nvramCtrldSetupKey); val == "1" { - return nil - } - - data, err := dnsMasqConf() - if err != nil { - return err - } - - nvramKvMap := nvramSetupKV() - nvramKvMap["dnsmasq_custom"] = data - if err := nvramSetKV(nvramKvMap, nvramCtrldSetupKey); err != nil { - return err - } - - // Restart dnscrypt-proxy service. - if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil { - return err - } - // Restart stubby service. - if err := tomatoRestartService(tomatoStubbySvcName); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func postInstallTomato() error { - return nil -} - -func cleanupTomato() error { - // Restore old configs. - if err := nvramRestore(nvramSetupKV(), nvramCtrldSetupKey); err != nil { - return err - } - // Restart dnscrypt-proxy service. - if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil { - return err - } - // Restart stubby service. - if err := tomatoRestartService(tomatoStubbySvcName); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func tomatoRestartService(name string) error { - return tomatoRestartServiceWithKill(name, false) -} - -func tomatoRestartServiceWithKill(name string, killBeforeRestart bool) error { - if killBeforeRestart { - _, _ = exec.Command("killall", name).CombinedOutput() - } - if out, err := exec.Command("service", name, "restart").CombinedOutput(); err != nil { - return fmt.Errorf("service restart %s: %s, %w", name, string(out), err) - } - return nil -} diff --git a/internal/router/tomato/tomato.go b/internal/router/tomato/tomato.go new file mode 100644 index 0000000..937f8ba --- /dev/null +++ b/internal/router/tomato/tomato.go @@ -0,0 +1,131 @@ +package tomato + +import ( + "fmt" + "os/exec" + + "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + "github.com/Control-D-Inc/ctrld/internal/router/ntp" + "github.com/Control-D-Inc/ctrld/internal/router/nvram" + "github.com/kardianos/service" +) + +const ( + Name = "freshtomato" + + tomatoDnsCryptProxySvcName = "dnscrypt-proxy" + tomatoStubbySvcName = "stubby" + tomatoDNSMasqSvcName = "dnsmasq" +) + +var nvramKvMap = map[string]string{ + "dnsmasq_custom": "", // Configuration of dnsmasq set by ctrld, filled by setupTomato. + "dnscrypt_proxy": "0", // Disable DNSCrypt. + "dnssec_enable": "0", // Disable DNSSEC. + "stubby_proxy": "0", // Disable Stubby +} + +type FreshTomato struct { + cfg *ctrld.Config +} + +// New returns a router.Router for configuring/setup/run ctrld on Ubios routers. +func New(cfg *ctrld.Config) *FreshTomato { + return &FreshTomato{cfg: cfg} +} + +func (f *FreshTomato) ConfigureService(config *service.Config) error { + return nil +} + +func (f *FreshTomato) Install(_ *service.Config) error { + return nil +} + +func (f *FreshTomato) Uninstall(_ *service.Config) error { + return nil +} + +func (f *FreshTomato) PreRun() error { + _ = f.Cleanup() + return ntp.Wait() +} + +func (f *FreshTomato) Configure() error { + f.cfg.Listener["0"].IP = "127.0.0.1" + f.cfg.Listener["0"].Port = 5354 + return nil +} + +func (f *FreshTomato) Setup() error { + // Already setup. + if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { + return nil + } + + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, f.cfg) + if err != nil { + return err + } + nvramKvMap["dnsmasq_custom"] = data + if err := nvram.SetKV(nvramKvMap, nvram.CtrldSetupKey); err != nil { + return err + } + + // Restart dnscrypt-proxy service. + if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil { + return err + } + // Restart stubby service. + if err := tomatoRestartService(tomatoStubbySvcName); err != nil { + return err + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func (f *FreshTomato) Cleanup() error { + if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { + nvramKvMap["dnsmasq_custom"] = "" + // Restore old configs. + if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { + return err + } + } + + // Restart dnscrypt-proxy service. + if err := tomatoRestartServiceWithKill(tomatoDnsCryptProxySvcName, true); err != nil { + return err + } + // Restart stubby service. + if err := tomatoRestartService(tomatoStubbySvcName); err != nil { + return err + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func tomatoRestartService(name string) error { + return tomatoRestartServiceWithKill(name, false) +} + +func tomatoRestartServiceWithKill(name string, killBeforeRestart bool) error { + if killBeforeRestart { + _, _ = exec.Command("killall", name).CombinedOutput() + } + if out, err := exec.Command("service", name, "restart").CombinedOutput(); err != nil { + return fmt.Errorf("service restart %s: %s, %w", name, string(out), err) + } + return nil +} + +func restartDNSMasq() error { + return tomatoRestartService(tomatoDNSMasqSvcName) +} diff --git a/internal/router/ubios.go b/internal/router/ubios.go deleted file mode 100644 index 48e5d41..0000000 --- a/internal/router/ubios.go +++ /dev/null @@ -1,73 +0,0 @@ -package router - -import ( - "bytes" - "fmt" - "os" - "strconv" -) - -var errContentFilteringEnabled = fmt.Errorf(`the "Content Filtering" feature" is enabled, which is conflicted with ctrld.\n -To disable it, folowing instruction here: %s`, toggleContentFilteringLink) - -const ( - ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf" - toggleContentFilteringLink = "https://community.ui.com/questions/UDM-Pro-disable-enable-DNS-filtering/e2cc4060-e56a-4139-b200-62d7f773ff8f" -) - -func setupUbiOS() error { - // Disable dnsmasq as DNS server. - dnsMasqConfigContent, err := dnsMasqConf() - if err != nil { - return err - } - if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(dnsMasqConfigContent), 0600); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func cleanupUbiOS() error { - // Remove the custom dnsmasq config - if err := os.Remove(ubiosDNSMasqConfigPath); err != nil { - return err - } - // Restart dnsmasq service. - if err := restartDNSMasq(); err != nil { - return err - } - return nil -} - -func postInstallUbiOS() error { - // See comment in postInstallEdgeOS. - if contentFilteringEnabled() { - return errContentFilteringEnabled - } - return nil -} - -func ubiosRestartDNSMasq() error { - buf, err := os.ReadFile("/run/dnsmasq.pid") - if err != nil { - return err - } - pid, err := strconv.ParseUint(string(bytes.TrimSpace(buf)), 10, 64) - if err != nil { - return err - } - proc, err := os.FindProcess(int(pid)) - if err != nil { - return err - } - return proc.Kill() -} - -func contentFilteringEnabled() bool { - st, err := os.Stat("/run/dnsfilter/dnsfilter") - return err == nil && !st.IsDir() -} diff --git a/internal/router/ubios/ubios.go b/internal/router/ubios/ubios.go new file mode 100644 index 0000000..61ab8e0 --- /dev/null +++ b/internal/router/ubios/ubios.go @@ -0,0 +1,96 @@ +package ubios + +import ( + "bytes" + "os" + "strconv" + + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + + "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/router/edgeos" + "github.com/kardianos/service" +) + +const ( + Name = "ubios" + ubiosDNSMasqConfigPath = "/run/dnsmasq.conf.d/zzzctrld.conf" +) + +type Ubios struct { + cfg *ctrld.Config +} + +// New returns a router.Router for configuring/setup/run ctrld on Ubios routers. +func New(cfg *ctrld.Config) *Ubios { + return &Ubios{cfg: cfg} +} + +func (u *Ubios) ConfigureService(config *service.Config) error { + return nil +} + +func (u *Ubios) Install(config *service.Config) error { + // See comment in (*edgeos.EdgeOS).Install method. + if edgeos.ContentFilteringEnabled() { + return edgeos.ErrContentFilteringEnabled + } + return nil +} + +func (u *Ubios) Uninstall(_ *service.Config) error { + return nil +} + +func (u *Ubios) PreRun() error { + return nil +} + +func (u *Ubios) Configure() error { + u.cfg.Listener["0"].IP = "127.0.0.1" + u.cfg.Listener["0"].Port = 5354 + return nil +} + +func (u *Ubios) Setup() error { + data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, u.cfg) + if err != nil { + return err + } + if err := os.WriteFile(ubiosDNSMasqConfigPath, []byte(data), 0600); err != nil { + return err + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func (u *Ubios) Cleanup() error { + // Remove the custom dnsmasq config + if err := os.Remove(ubiosDNSMasqConfigPath); err != nil { + return err + } + // Restart dnsmasq service. + if err := restartDNSMasq(); err != nil { + return err + } + return nil +} + +func restartDNSMasq() error { + buf, err := os.ReadFile("/run/dnsmasq.pid") + if err != nil { + return err + } + pid, err := strconv.ParseUint(string(bytes.TrimSpace(buf)), 10, 64) + if err != nil { + return err + } + proc, err := os.FindProcess(int(pid)) + if err != nil { + return err + } + return proc.Kill() +} From d43e50ee2d98da882dc36ee15fecb119d01d1840 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 29 Jun 2023 22:44:58 +0700 Subject: [PATCH 27/84] cmd/ctrld: produce better message when "ctrd start" failed The current error message is not much helpful, not all users are able to investigate system log file to find the reason. Instead, gathering the log output of "ctrld run" command, and if error happens or self-check failed, print the log to users. --- cmd/ctrld/cli.go | 50 +++++++++++++++++++++++++++++++++++++++++------ cmd/ctrld/prog.go | 40 +++++++++++++++++++++++++++++++++---- 2 files changed, 80 insertions(+), 10 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index f3cfe01..5a8999e 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -120,16 +120,24 @@ func initCLI() { initConsoleLogging() }, Run: func(cmd *cobra.Command, args []string) { - if daemon && runtime.GOOS == "windows" { - mainLog.Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.") - } - waitCh := make(chan struct{}) stopCh := make(chan struct{}) p := &prog{ waitCh: waitCh, stopCh: stopCh, } + sockPath := filepath.Join(homedir, ctrldLogUnixSock) + if addr, err := net.ResolveUnixAddr("unix", sockPath); err == nil { + if conn, err := net.Dial(addr.Network(), addr.String()); err == nil { + consoleWriter.Out = io.MultiWriter(os.Stdout, conn) + p.logConn = conn + } + } + + if daemon && runtime.GOOS == "windows" { + mainLog.Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.") + } + if !daemon { // We need to call s.Run() as soon as possible to response to the OS manager, so it // can see ctrld is running and don't mark ctrld as failed service. @@ -309,12 +317,36 @@ func initCLI() { if configPath != "" { v.SetConfigFile(configPath) } + + // A buffer channel to gather log output from runCmd and report + // to user in case self-check process failed. + runCmdLogCh := make(chan string, 256) if dir, err := userHomeDir(); err == nil { setWorkingDirectory(sc, dir) if configPath == "" && writeDefaultConfig { defaultConfigFile = filepath.Join(dir, defaultConfigFile) } sc.Arguments = append(sc.Arguments, "--homedir="+dir) + sockPath := filepath.Join(dir, ctrldLogUnixSock) + _ = os.Remove(sockPath) + go func() { + defer func() { + close(runCmdLogCh) + _ = os.Remove(sockPath) + }() + if conn := runLogServer(sockPath); conn != nil { + // Enough buffer for log message, we don't produce + // such long log message, but just in case. + buf := make([]byte, 1024) + for { + n, err := conn.Read(buf) + if err != nil { + return + } + runCmdLogCh <- string(buf[:n]) + } + } + }() } tryReadingConfig(writeDefaultConfig) @@ -370,13 +402,19 @@ func initCLI() { case service.StatusRunning: mainLog.Notice().Msg("Service started") default: - mainLog.Error().Msg("Service did not start, please check system/service log for details error") + marker := bytes.Repeat([]byte("="), 32) + mainLog.Error().Msg("ctrld service may not have started due to an error or misconfiguration, service log:") + _, _ = mainLog.Write(marker) + for msg := range runCmdLogCh { + _, _ = mainLog.Write([]byte(msg)) + } + _, _ = mainLog.Write(marker) uninstall(p, s) os.Exit(1) } // On Linux, Darwin, Freebsd, ctrld set DNS on startup, because the DNS setting could be // reset after rebooting. On windows, we only need to set once here. See prog.preRun in - // prog_*.go file for dedicated code on each platforms. + // prog_*.go file for dedicated code on each platform. if runtime.GOOS == "windows" { p.setDNS() } diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index cf062b5..e53436f 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -22,7 +22,10 @@ import ( "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) -const defaultSemaphoreCap = 256 +const ( + defaultSemaphoreCap = 256 + ctrldLogUnixSock = "ctrld_start.sock" +) var logf = func(format string, args ...any) { mainLog.Debug().Msgf(format, args...) @@ -39,9 +42,10 @@ var svcConfig = &service.Config{ var useSystemdResolved = false type prog struct { - mu sync.Mutex - waitCh chan struct{} - stopCh chan struct{} + mu sync.Mutex + waitCh chan struct{} + stopCh chan struct{} + logConn net.Conn cfg *ctrld.Config cache dnscache.Cacher @@ -174,6 +178,12 @@ func (p *prog) run() { for _, f := range p.onStarted { f() } + // Stop writing log to unix socket. + consoleWriter.Out = os.Stdout + initLoggingWithBackup(false) + if p.logConn != nil { + _ = p.logConn.Close() + } wg.Wait() } @@ -301,3 +311,25 @@ func randomLocalIP() string { n := rand.Intn(254-2) + 2 return fmt.Sprintf("127.0.0.%d", n) } + +// runLogServer starts a unix listener, use by startCmd to gather log from runCmd. +func runLogServer(sockPath string) net.Conn { + addr, err := net.ResolveUnixAddr("unix", sockPath) + if err != nil { + mainLog.Warn().Err(err).Msg("invalid log sock path") + return nil + } + ln, err := net.ListenUnix("unix", addr) + if err != nil { + mainLog.Warn().Err(err).Msg("could not listen log socket") + return nil + } + defer ln.Close() + + server, err := ln.Accept() + if err != nil { + mainLog.Warn().Err(err).Msg("could not accept connection") + return nil + } + return server +} From de32dd8ba43f2dba05d1d864c2a618b57e7277f5 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 29 Jun 2023 23:52:52 +0700 Subject: [PATCH 28/84] cmd/ctrld: better error message for parsing/validation error --- cmd/ctrld/cli.go | 65 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 59 insertions(+), 6 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 5a8999e..1ac9893 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -13,6 +13,7 @@ import ( "os" "os/exec" "path/filepath" + "reflect" "runtime" "strconv" "strings" @@ -203,9 +204,7 @@ func initCLI() { initLoggingWithBackup(false) } - if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil { - mainLog.Fatal().Msgf("invalid config: %v", err) - } + validateConfig(&cfg) initCache() if daemon { @@ -359,9 +358,7 @@ func initCLI() { processCDFlags(p) - if err := ctrld.ValidateConfig(validator.New(), &cfg); err != nil { - mainLog.Fatal().Msgf("invalid config: %v", err) - } + validateConfig(&cfg) // Explicitly passing config, so on system where home directory could not be obtained, // or sub-process env is different with the parent, we still behave correctly and use @@ -704,6 +701,17 @@ func readConfigFile(writeDefaultConfig bool) bool { defaultConfigWritten = true return false } + + if _, ok := err.(viper.ConfigParseError); ok { + if f, _ := os.Open(v.ConfigFileUsed()); f != nil { + var i any + if err, ok := toml.NewDecoder(f).Decode(&i).(*toml.DecodeError); ok { + row, col := err.Position() + mainLog.Fatal().Msgf("failed to decode config file at line: %d, column: %d, error: %v", row, col, err) + } + } + } + // Otherwise, report fatal error and exit. mainLog.Fatal().Msgf("failed to decode config file: %v", err) return false @@ -1058,3 +1066,48 @@ func uninstall(p *prog, s service.Service) { return } } + +func validateConfig(cfg *ctrld.Config) { + err := ctrld.ValidateConfig(validator.New(), cfg) + if err == nil { + return + } + var ve validator.ValidationErrors + if errors.As(err, &ve) { + for _, fe := range ve { + mainLog.Error().Msgf("invalid config: %s: %s", fe.Namespace(), fieldErrorMsg(fe)) + } + } + os.Exit(1) +} + +func fieldErrorMsg(fe validator.FieldError) string { + switch fe.Tag() { + case "oneof": + return fmt.Sprintf("must be one of: %q", fe.Param()) + case "min": + if fe.Kind() == reflect.Map || fe.Kind() == reflect.Slice { + return fmt.Sprintf("must define at least %s element", fe.Param()) + } + return fmt.Sprintf("minimum value: %q", fe.Param()) + case "len": + if fe.Kind() == reflect.Slice { + return fmt.Sprintf("must have at least %s element", fe.Param()) + } + return fmt.Sprintf("minimum len: %q", fe.Param()) + case "gte": + return fmt.Sprintf("must be greater than or equal to: %s", fe.Param()) + case "cidr": + return fmt.Sprintf("invalid value: %s", fe.Value()) + case "required_unless", "required": + return fmt.Sprintf("value is required") + case "dnsrcode": + return fmt.Sprintf("invalid DNS rcode value: %s", fe.Value()) + case "ipstack": + ipStacks := []string{ctrld.IpStackV4, ctrld.IpStackV6, ctrld.IpStackSplit, ctrld.IpStackBoth} + return fmt.Sprintf("must be one of: %q", strings.Join(ipStacks, " ")) + case "iporempty": + return fmt.Sprintf("invalid IP format: %s", fe.Value()) + } + return "" +} From 6c2996a921c9c6db7a29821d71cc487206bf30b9 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 6 Jul 2023 00:40:50 +0700 Subject: [PATCH 29/84] cmd/ctrld: use sysv service wrapper for "unix-systemv" platform --- cmd/ctrld/service.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cmd/ctrld/service.go b/cmd/ctrld/service.go index adf0a28..2865e62 100644 --- a/cmd/ctrld/service.go +++ b/cmd/ctrld/service.go @@ -22,7 +22,9 @@ func newService(i service.Interface, c *service.Config) (service.Service, error) switch { case router.IsOldOpenwrt(): return &procd{&sysV{s}}, nil - case router.IsGLiNet(): // TODO: unify for other SysV system. + case router.IsGLiNet(): + return &sysV{s}, nil + case s.Platform() == "unix-systemv": return &sysV{s}, nil } return s, nil From ab1d7fd7969cc8f7496a840e3d44fbc29cdac98c Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 7 Jul 2023 01:09:45 +0700 Subject: [PATCH 30/84] cmd/ctrld: lower status string before checking Depending on system, the output of `/etc/init.d/ctrld status` can be either "Running" or "running", we must do in-sensitive comparison to get the right status of ctrld. --- cmd/ctrld/service.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/ctrld/service.go b/cmd/ctrld/service.go index 2865e62..5f6eeb2 100644 --- a/cmd/ctrld/service.go +++ b/cmd/ctrld/service.go @@ -131,7 +131,8 @@ func unixSystemVServiceStatus() (service.Status, error) { if err != nil { return service.StatusUnknown, nil } - switch string(bytes.TrimSpace(out)) { + + switch string(bytes.ToLower(bytes.TrimSpace(out))) { case "running": return service.StatusRunning, nil default: From 3f3c1d6d78c2e77b2bf21c38fc476f93156470e1 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 6 Jul 2023 23:42:04 +0700 Subject: [PATCH 31/84] Fix Ping upstream cause ctrld crash dohTransport returns a http.RoundTripper. When pinging upstream, we do it both for doh and doh3, and checking whether the transport is nil before performing the check. However, dohTransport returns a concrete *http.Transport. Thus dohTransport will always return a non-nil http.Roundtripper, causing invalid memory dereference when upstream is configured to use doh3. Performing ping upstream separately will fix the issue. --- config.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/config.go b/config.go index cb51e97..9c553b2 100644 --- a/config.go +++ b/config.go @@ -407,8 +407,12 @@ func (uc *UpstreamConfig) Ping() { } for _, typ := range []uint16{dns.TypeA, dns.TypeAAAA} { - ping(uc.dohTransport(typ)) - ping(uc.doh3Transport(typ)) + switch uc.Type { + case ResolverTypeDOH: + ping(uc.dohTransport(typ)) + case ResolverTypeDOH3: + ping(uc.doh3Transport(typ)) + } } } From 7af59ee589216359abd28dcdabd6f962f283e55a Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 7 Jul 2023 21:07:26 +0700 Subject: [PATCH 32/84] all: rework fetching/generating config in cd mode Config fetching/generating in cd mode is currently weird, error prone, and easy for user to break ctrld when using custom config. This commit reworks the flow: - Fetching config from Control D API. - No custom config, use the current default config. - If custom config presents, but there's no listener, use 0.0.0.0:53. - Try listening on current ip+port config, if ok, ctrld could be a direct listener with current setup, moving on. - If failed, trying 127.0.0.1:53. - If failed, trying current ip + port 5354 - If still failed, pick a random ip:port pair, retry until listening ok. With this flow, thing is more predictable/stable, and help removing the Config interface for router. --- cmd/ctrld/cli.go | 378 +++++++++++++++++++------ cmd/ctrld/cli_router.go | 100 ------- cmd/ctrld/cli_router_others.go | 5 - cmd/ctrld/conn.go | 51 ++++ cmd/ctrld/dns_proxy.go | 15 +- cmd/ctrld/main.go | 2 - cmd/ctrld/prog.go | 64 ++--- config.go | 21 ++ config_test.go | 15 + internal/dns/nm.go | 2 +- internal/router/ddwrt/ddwrt.go | 6 - internal/router/dnsmasq/dnsmasq.go | 14 +- internal/router/dummy.go | 4 - internal/router/edgeos/edgeos.go | 6 - internal/router/firewalla/firewalla.go | 6 - internal/router/merlin/merlin.go | 6 - internal/router/openwrt/openwrt.go | 6 - internal/router/pfsense/pfsense.go | 23 +- internal/router/router.go | 40 +-- internal/router/synology/synology.go | 6 - internal/router/tomato/tomato.go | 6 - internal/router/ubios/ubios.go | 6 - testhelper/config.go | 4 + 23 files changed, 442 insertions(+), 344 deletions(-) delete mode 100644 cmd/ctrld/cli_router.go delete mode 100644 cmd/ctrld/cli_router_others.go create mode 100644 cmd/ctrld/conn.go diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 1ac9893..e6c40f3 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -15,17 +15,21 @@ import ( "path/filepath" "reflect" "runtime" + "sort" "strconv" "strings" + "sync" "time" "github.com/cuonglm/osinfo" + "github.com/fsnotify/fsnotify" "github.com/go-playground/validator/v10" "github.com/kardianos/service" "github.com/miekg/dns" "github.com/pelletier/go-toml/v2" "github.com/rs/zerolog" "github.com/spf13/cobra" + "github.com/spf13/pflag" "github.com/spf13/viper" "tailscale.com/logtail/backoff" "tailscale.com/net/interfaces" @@ -36,6 +40,7 @@ import ( ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" "github.com/Control-D-Inc/ctrld/internal/router" "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" + "github.com/Control-D-Inc/ctrld/internal/router/firewalla" "github.com/Control-D-Inc/ctrld/internal/router/merlin" "github.com/Control-D-Inc/ctrld/internal/router/tomato" ) @@ -126,12 +131,14 @@ func initCLI() { p := &prog{ waitCh: waitCh, stopCh: stopCh, + cfg: &cfg, } sockPath := filepath.Join(homedir, ctrldLogUnixSock) if addr, err := net.ResolveUnixAddr("unix", sockPath); err == nil { if conn, err := net.Dial(addr.Network(), addr.String()); err == nil { - consoleWriter.Out = io.MultiWriter(os.Stdout, conn) - p.logConn = conn + lc := &logConn{conn: conn} + consoleWriter.Out = io.MultiWriter(os.Stdout, lc) + p.logConn = lc } } @@ -177,10 +184,7 @@ func initCLI() { mainLog.Fatal().Msg("network is not up yet") } - p.router = router.NewDummyRouter() - if setupRouter { - p.router = router.New(&cfg) - } + p.router = router.New(&cfg) // Processing --cd flag require connecting to ControlD API, which needs valid // time for validating server certificate. Some routers need NTP synchronization @@ -190,7 +194,22 @@ func initCLI() { } oldLogPath := cfg.Service.LogPath - processCDFlags(p) + if cdUID != "" { + processCDFlags() + } + + updateListenerConfig() + + if cdUID != "" { + processLogAndCacheFlags() + } + + if err := writeConfigFile(); err != nil { + mainLog.Fatal().Err(err).Msg("failed to write config file") + } else { + mainLog.Info().Msg("writing config file to: " + defaultConfigFile) + } + 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) @@ -229,24 +248,38 @@ func initCLI() { os.Exit(0) } - if setupRouter { - switch platform := router.Name(); { - case platform == ddwrt.Name: - rootCertPool = certs.CACertPool() - fallthrough - case platform != "": - if !router.IsSupported(platform) { - unsupportedPlatformHelp(cmd) - os.Exit(1) + p.onStarted = append(p.onStarted, func() { + for _, lc := range p.cfg.Listener { + if shouldAllocateLoopbackIP(lc.IP) { + if err := allocateIP(lc.IP); err != nil { + mainLog.Error().Err(err).Msgf("could not allocate IP: %s", lc.IP) + } } + } + }) + p.onStopped = append(p.onStopped, func() { + for _, lc := range p.cfg.Listener { + if shouldAllocateLoopbackIP(lc.IP) { + if err := deAllocateIP(lc.IP); err != nil { + mainLog.Error().Err(err).Msgf("could not de-allocate IP: %s", lc.IP) + } + } + } + }) + if platform := router.Name(); platform != "" { + if platform == ddwrt.Name { + rootCertPool = certs.CACertPool() + } + // Perform router setup/cleanup if ctrld could not be direct listener. + if !couldBeDirectListener(cfg.FirstListener()) { p.onStarted = append(p.onStarted, func() { - mainLog.Debug().Msg("Router setup") + mainLog.Debug().Msg("router setup") if err := p.router.Setup(); err != nil { mainLog.Error().Err(err).Msg("could not configure router") } }) p.onStopped = append(p.onStopped, func() { - mainLog.Debug().Msg("Router cleanup") + mainLog.Debug().Msg("router cleanup") if err := p.router.Cleanup(); err != nil { mainLog.Error().Err(err).Msg("could not cleanup router") } @@ -278,8 +311,6 @@ func initCLI() { _ = runCmd.Flags().MarkHidden("homedir") runCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) _ = runCmd.Flags().MarkHidden("iface") - runCmd.Flags().BoolVarP(&setupRouter, "router", "", false, `setup for running on router platforms`) - _ = runCmd.Flags().MarkHidden("router") rootCmd.AddCommand(runCmd) @@ -301,11 +332,10 @@ func initCLI() { setDependencies(sc) sc.Arguments = append([]string{"run"}, osArgs...) - p := &prog{router: router.NewDummyRouter()} - if setupRouter { - p.router = router.New(&cfg) + p := &prog{ + router: router.New(&cfg), + cfg: &cfg, } - if err := p.router.ConfigureService(sc); err != nil { mainLog.Fatal().Err(err).Msg("failed to configure service on router") } @@ -356,10 +386,6 @@ func initCLI() { initLogging() - processCDFlags(p) - - validateConfig(&cfg) - // Explicitly passing config, so on system where home directory could not be obtained, // or sub-process env is different with the parent, we still behave correctly and use // the expected config file. @@ -373,8 +399,10 @@ func initCLI() { return } - mainLog.Debug().Msg("cleaning up router before installing") - _ = p.router.Cleanup() + if router.Name() != "" && !couldBeDirectListener(cfg.FirstListener()) { + mainLog.Debug().Msg("cleaning up router before installing") + _ = p.router.Cleanup() + } tasks := []task{ {s.Stop, false}, @@ -431,8 +459,36 @@ func initCLI() { startCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain") _ = startCmd.Flags().MarkHidden("dev") startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) - startCmd.Flags().BoolVarP(&setupRouter, "router", "", false, `setup for running on router platforms`) - _ = startCmd.Flags().MarkHidden("router") + + routerCmd := &cobra.Command{ + Use: "setup", + PreRun: func(cmd *cobra.Command, args []string) { + initConsoleLogging() + }, + Run: func(cmd *cobra.Command, _ []string) { + exe, err := os.Executable() + if err != nil { + mainLog.Fatal().Msgf("could not find executable path: %v", err) + os.Exit(1) + } + flags := make([]string, 0) + cmd.Flags().Visit(func(flag *pflag.Flag) { + flags = append(flags, fmt.Sprintf("--%s=%s", flag.Name, flag.Value)) + }) + cmdArgs := []string{"start"} + cmdArgs = append(cmdArgs, flags...) + command := exec.Command(exe, cmdArgs...) + command.Stdout = os.Stdout + command.Stderr = os.Stderr + command.Stdin = os.Stdin + if err := command.Run(); err != nil { + mainLog.Fatal().Msg(err.Error()) + } + }, + } + routerCmd.Flags().AddFlagSet(startCmd.Flags()) + routerCmd.Hidden = true + rootCmd.AddCommand(routerCmd) stopCmd := &cobra.Command{ PreRun: func(cmd *cobra.Command, args []string) { @@ -779,10 +835,7 @@ func processNoConfigFlags(noConfigStart bool) { v.Set("upstream", upstream) } -func processCDFlags(p *prog) { - if cdUID == "" { - return - } +func processCDFlags() { logger := mainLog.With().Str("mode", "cd").Logger() logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID) resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev) @@ -817,48 +870,15 @@ func processCDFlags(p *prog) { } logger.Info().Msg("generating ctrld config from Control-D configuration") - cfg = ctrld.Config{Listener: map[string]*ctrld.ListenerConfig{ - "0": {Port: 53}, - }} + cfg = ctrld.Config{} + + // Fetch config, unmarshal to cfg. if resolverConfig.Ctrld.CustomConfig != "" { logger.Info().Msg("using defined custom config of Control-D resolver") readBase64Config(resolverConfig.Ctrld.CustomConfig) if err := v.Unmarshal(&cfg); err != nil { mainLog.Fatal().Msgf("failed to unmarshal config: %v", err) } - for _, listener := range cfg.Listener { - if listener.IP == "" { - listener.IP = randomLocalIP() - } - if listener.Port == 0 { - listener.Port = 53 - } - } - switch { - case setupRouter: - if lc := cfg.Listener["0"]; lc != nil && lc.IP == "" { - if err := p.router.Configure(); err != nil { - mainLog.Fatal().Err(err).Msg("failed to change ctrld config for router") - } - } - case useSystemdResolved: - if lc := cfg.Listener["0"]; lc != nil { - if ip := net.ParseIP(lc.IP); ip != nil && ip.IsLoopback() { - mainLog.Warn().Msg("using loopback interface do not work with systemd-resolved") - // 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() - mainLog.Warn().Msgf("use %s as listener address", lc.IP) - } - } - } - } - } - } } else { cfg.Network = make(map[string]*ctrld.NetworkConfig) cfg.Network["0"] = &ctrld.NetworkConfig{ @@ -877,27 +897,18 @@ func processCDFlags(p *prog) { } cfg.Listener = make(map[string]*ctrld.ListenerConfig) lc := &ctrld.ListenerConfig{ - IP: "127.0.0.1", - Port: 53, Policy: &ctrld.ListenerPolicyConfig{ Name: "My Policy", Rules: rules, }, } cfg.Listener["0"] = lc - if setupRouter { - if err := p.router.Configure(); err != nil { - mainLog.Fatal().Err(err).Msg("failed to change ctrld config for router") - } - } } - - processLogAndCacheFlags() - - if err := writeConfigFile(); err != nil { - logger.Fatal().Err(err).Msg("failed to write config file") - } else { - logger.Info().Msg("writing config file to: " + defaultConfigFile) + // Set default value. + if len(cfg.Listener) == 0 { + cfg.Listener = map[string]*ctrld.ListenerConfig{ + "0": {IP: "", Port: 0}, + } } } @@ -924,9 +935,11 @@ func processListenFlag() { func processLogAndCacheFlags() { if logPath != "" { - cfg.Service.LogLevel = "debug" cfg.Service.LogPath = logPath } + if logPath != "" && cfg.Service.LogLevel == "" { + cfg.Service.LogLevel = "debug" + } if cacheSize != 0 { cfg.Service.CacheEnable = true @@ -979,8 +992,35 @@ func selfCheckStatus(status service.Status, domain string) service.Status { maxAttempts := 20 mainLog.Debug().Msg("Performing self-check") + var ( + lcChanged map[string]*ctrld.ListenerConfig + mu sync.Mutex + ) + curCfg := cfg + watcher, err := fsnotify.NewWatcher() + if err != nil { + mainLog.Error().Err(err).Msg("could not watch config change") + return service.StatusUnknown + } + defer watcher.Close() + + 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++ { - lc := cfg.Listener["0"] + mu.Lock() + if lcChanged != nil { + curCfg.Listener = lcChanged + } + mu.Unlock() + lc := curCfg.FirstListener() + m := new(dns.Msg) m.SetQuestion(domain+".", dns.TypeA) m.RecursionDesired = true @@ -995,10 +1035,6 @@ func selfCheckStatus(status service.Status, domain string) service.Status { return service.StatusUnknown } -func unsupportedPlatformHelp(cmd *cobra.Command) { - mainLog.Error().Msg("Unsupported or incorrectly chosen router platform. Please open an issue and provide all relevant information: https://github.com/Control-D-Inc/ctrld/issues/new") -} - func userHomeDir() (string, error) { switch router.Name() { case ddwrt.Name, merlin.Name, tomato.Name: @@ -1111,3 +1147,161 @@ func fieldErrorMsg(fe validator.FieldError) string { } return "" } + +// couldBeDirectListener reports whether ctrld can be a direct listener on port 53. +// It returns true only if ctrld can listen on port 53 for all interfaces. That means +// there's no other software listening on port 53. +// +// If someone listening on port 53, or ctrld could only listen on port 53 for a specific +// interface, ctrld could only be configured as a DNS forwarder. +func couldBeDirectListener(lc *ctrld.ListenerConfig) bool { + if lc == nil || lc.Port != 53 { + return false + } + switch lc.IP { + case "", "::", "0.0.0.0": + return true + default: + return false + } + +} + +func isLoopback(ipStr string) bool { + ip := net.ParseIP(ipStr) + if ip == nil { + return false + } + return ip.IsLoopback() +} + +func shouldAllocateLoopbackIP(ipStr string) bool { + ip := net.ParseIP(ipStr) + if ip == nil || ip.To4() == nil { + return false + } + return ip.IsLoopback() && ip.String() != "127.0.0.1" +} + +// updateListenerConfig updates the config for listeners if not defined, +// or defined but invalid to be used, e.g: using loopback address other +// than 127.0.0.1 with sytemd-resolved. +func updateListenerConfig() { + for _, listener := range cfg.Listener { + if listener.IP == "" { + listener.IP = "0.0.0.0" + } + if listener.Port == 0 { + listener.Port = 53 + } + } + + var closers []io.Closer + defer func() { + for _, closer := range closers { + _ = closer.Close() + } + }() + // listenOk reports whether we can listen on udp/tcp of given address. + // Created listeners will be kept in listeners slice above, and close + // before function finished. + listenOk := func(addr string) bool { + udpLn, udpErr := net.ListenPacket("udp", addr) + if udpLn != nil { + closers = append(closers, udpLn) + } + tcpLn, tcpErr := net.Listen("tcp", addr) + if tcpLn != nil { + closers = append(closers, tcpLn) + } + return udpErr == nil && tcpErr == nil + } + + listeners := make([]int, 0, len(cfg.Listener)) + for k := range cfg.Listener { + n, err := strconv.Atoi(k) + if err != nil { + continue + } + listeners = append(listeners, n) + } + sort.Ints(listeners) + + for _, n := range listeners { + listener := cfg.Listener[strconv.Itoa(n)] + oldIP := listener.IP + // Check if we could listen on the current IP + Port, if not, try following thing, pick first one success: + // - Try 127.0.0.1:53 + // - Pick a random port until success. + localhostIP := func(ipStr string) string { + if ip := net.ParseIP(ipStr); ip != nil && ip.To4() == nil { + return "::1" + } + return "127.0.0.1" + } + + // On firewalla, we don't need to check localhost, because the lo interface is excluded in dnsmasq + // config, so we can always listen on localhost port 53, but no traffic could be routed there. + tryLocalhost := !isLoopback(listener.IP) && router.Name() != firewalla.Name + tryPort5354 := true + attempts := 0 + maxAttempts := 10 + for { + if attempts == maxAttempts { + mainLog.Fatal().Msg("could not find available listen ip and port") + } + addr := net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)) + if listenOk(addr) { + break + } + if tryLocalhost { + tryLocalhost = false + listener.IP = localhostIP(listener.IP) + listener.Port = 53 + mainLog.Warn().Msgf("could not listen on address: %s, trying localhost: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + continue + } + if tryPort5354 { + tryPort5354 = false + listener.IP = oldIP + listener.Port = 5354 + mainLog.Warn().Msgf("could not listen on address: %s, trying port 5354", addr) + continue + } + listener.IP = randomLocalIP() + listener.Port = randomPort() + mainLog.Warn().Msgf("could not listen on address: %s, pick a random ip+port", addr) + attempts++ + } + } + + // Specific case for systemd-resolved. + if useSystemdResolved { + if listener := cfg.FirstListener(); listener != nil && listener.Port == 53 { + // systemd-resolved does not allow forwarding DNS queries from 127.0.0.53 to loopback + // ip address, other than "127.0.0.1", so trying to listen on default route interface + // address instead. + if ip := net.ParseIP(listener.IP); ip != nil && ip.IsLoopback() && ip.String() != "127.0.0.1" { + mainLog.Warn().Msg("using loopback interface do not work with systemd-resolved") + found := false + 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 { + addr := net.JoinHostPort(netIP.IP.String(), strconv.Itoa(listener.Port)) + if listenOk(addr) { + found = true + listener.IP = netIP.IP.String() + mainLog.Warn().Msgf("use %s as listener address", listener.IP) + break + } + } + } + } + if !found { + mainLog.Fatal().Msgf("could not use %q as DNS nameserver with systemd resolved", listener.IP) + } + } + } + } +} diff --git a/cmd/ctrld/cli_router.go b/cmd/ctrld/cli_router.go deleted file mode 100644 index 7688e9a..0000000 --- a/cmd/ctrld/cli_router.go +++ /dev/null @@ -1,100 +0,0 @@ -//go:build linux || freebsd - -package main - -import ( - "os" - "os/exec" - "strings" - - "github.com/spf13/cobra" - - "github.com/Control-D-Inc/ctrld/internal/router" -) - -func initRouterCLI() { - validArgs := append(router.SupportedPlatforms(), "auto") - var b strings.Builder - b.WriteString("Auto-setup Control D on a router.\n\nSupported platforms:\n\n") - for _, arg := range validArgs { - b.WriteString(" ₒ ") - b.WriteString(arg) - if arg == "auto" { - b.WriteString(" - detect the platform you are running on") - } - b.WriteString("\n") - } - - routerCmd := &cobra.Command{ - Use: "setup", - Short: b.String(), - PreRun: func(cmd *cobra.Command, args []string) { - initConsoleLogging() - }, - Run: func(cmd *cobra.Command, args []string) { - if len(args) == 0 { - _ = cmd.Help() - return - } - if len(args) != 1 { - _ = cmd.Help() - return - } - platform := args[0] - if platform == "auto" { - platform = router.Name() - } - if !router.IsSupported(platform) { - unsupportedPlatformHelp(cmd) - os.Exit(1) - } - - exe, err := os.Executable() - if err != nil { - mainLog.Fatal().Msgf("could not find executable path: %v", err) - os.Exit(1) - } - - cmdArgs := []string{"start"} - cmdArgs = append(cmdArgs, osArgs(platform)...) - cmdArgs = append(cmdArgs, "--router") - command := exec.Command(exe, cmdArgs...) - command.Stdout = os.Stdout - command.Stderr = os.Stderr - command.Stdin = os.Stdin - if err := command.Run(); err != nil { - mainLog.Fatal().Msg(err.Error()) - } - }, - } - // Keep these flags in sync with startCmd, except for "--router". - routerCmd.Flags().StringVarP(&configPath, "config", "c", "", "Path to config file") - routerCmd.Flags().StringVarP(&configBase64, "base64_config", "", "", "Base64 encoded config") - routerCmd.Flags().StringVarP(&listenAddress, "listen", "", "", "Listener address and port, in format: address:port") - routerCmd.Flags().StringVarP(&primaryUpstream, "primary_upstream", "", "", "Primary upstream endpoint") - routerCmd.Flags().StringVarP(&secondaryUpstream, "secondary_upstream", "", "", "Secondary upstream endpoint") - routerCmd.Flags().StringSliceVarP(&domains, "domains", "", nil, "List of domain to apply in a split DNS policy") - routerCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file") - routerCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") - routerCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid") - routerCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain") - _ = routerCmd.Flags().MarkHidden("dev") - routerCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) - - tmpl := routerCmd.UsageTemplate() - tmpl = strings.Replace(tmpl, "{{.UseLine}}", "{{.UseLine}} [platform]", 1) - routerCmd.SetUsageTemplate(tmpl) - rootCmd.AddCommand(routerCmd) -} - -func osArgs(platform string) []string { - args := os.Args[2:] - n := 0 - for _, x := range args { - if x != platform && x != "auto" { - args[n] = x - n++ - } - } - return args[:n] -} diff --git a/cmd/ctrld/cli_router_others.go b/cmd/ctrld/cli_router_others.go deleted file mode 100644 index 4934b5c..0000000 --- a/cmd/ctrld/cli_router_others.go +++ /dev/null @@ -1,5 +0,0 @@ -//go:build !linux && !freebsd - -package main - -func initRouterCLI() {} diff --git a/cmd/ctrld/conn.go b/cmd/ctrld/conn.go new file mode 100644 index 0000000..a627935 --- /dev/null +++ b/cmd/ctrld/conn.go @@ -0,0 +1,51 @@ +package main + +import ( + "net" + "time" +) + +// logConn wraps a net.Conn, override the Write behavior. +// runCmd uses this wrapper, so as long as startCmd finished, +// ctrld log won't be flushed with un-necessary write errors. +type logConn struct { + conn net.Conn +} + +func (lc *logConn) Read(b []byte) (n int, err error) { + return lc.conn.Read(b) +} + +func (lc *logConn) Close() error { + return lc.conn.Close() +} + +func (lc *logConn) LocalAddr() net.Addr { + return lc.conn.LocalAddr() +} + +func (lc *logConn) RemoteAddr() net.Addr { + return lc.conn.RemoteAddr() +} + +func (lc *logConn) SetDeadline(t time.Time) error { + return lc.conn.SetDeadline(t) +} + +func (lc *logConn) SetReadDeadline(t time.Time) error { + return lc.conn.SetReadDeadline(t) +} + +func (lc *logConn) SetWriteDeadline(t time.Time) error { + return lc.conn.SetWriteDeadline(t) +} + +func (lc *logConn) Write(b []byte) (int, error) { + // Write performs writes with underlying net.Conn, ignore any errors happen. + // "ctrld run" command use this wrapper to report errors to "ctrld start". + // If no error occurred, "ctrld start" may finish before "ctrld run" attempt + // to close the connection, so ignore errors conservatively here, prevent + // un-necessary error "write to closed connection" flushed to ctrld log. + _, _ = lc.conn.Write(b) + return len(b), nil +} diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index fad8e9c..52a5fb1 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -93,7 +93,7 @@ func (p *prog) serveDNS(listenerNum string) error { }) } g.Go(func() error { - s, errCh := runDNSServer(dnsListenAddress(listenerNum, listenerConfig), proto, handler) + s, errCh := runDNSServer(dnsListenAddress(listenerConfig), proto, handler) defer s.Shutdown() if listenerConfig.Port == 0 { switch s.Net { @@ -400,12 +400,13 @@ func needLocalIPv6Listener() bool { return ctrldnet.SupportsIPv6ListenLocal() && runtime.GOOS == "windows" } -func dnsListenAddress(lcNum string, lc *ctrld.ListenerConfig) string { - addr := net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)) - // If we are inside container and the listener address is localhost, - // Change it to 0.0.0.0:53, so user can expose the port to outside. - if addr == "127.0.0.1:53" && cdUID != "" && inContainer() { - return "0.0.0.0:53" +func dnsListenAddress(lc *ctrld.ListenerConfig) string { + // If we are inside container and the listener loopback address, change + // the address to something like 0.0.0.0:53, so user can expose the port to outside. + if inContainer() { + if ip := net.ParseIP(lc.IP); ip != nil && ip.IsLoopback() { + return net.JoinHostPort("0.0.0.0", strconv.Itoa(lc.Port)) + } } return net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)) } diff --git a/cmd/ctrld/main.go b/cmd/ctrld/main.go index 9f6ec60..2573f6e 100644 --- a/cmd/ctrld/main.go +++ b/cmd/ctrld/main.go @@ -30,7 +30,6 @@ var ( cdDev bool iface string ifaceStartStop string - setupRouter bool mainLog = zerolog.New(io.Discard) consoleWriter zerolog.ConsoleWriter @@ -39,7 +38,6 @@ var ( func main() { ctrld.InitConfig(v, "ctrld") initCLI() - initRouterCLI() if err := rootCmd.Execute(); err != nil { mainLog.Error().Msg(err.Error()) os.Exit(1) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index e53436f..e94e0e9 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -16,10 +16,7 @@ import ( "github.com/Control-D-Inc/ctrld/internal/clientinfo" "github.com/Control-D-Inc/ctrld/internal/dnscache" "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" "github.com/Control-D-Inc/ctrld/internal/router/firewalla" - "github.com/Control-D-Inc/ctrld/internal/router/openwrt" - "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) const ( @@ -221,16 +218,7 @@ func (p *prog) deAllocateIP() error { } func (p *prog) setDNS() { - switch router.Name() { - case ddwrt.Name, openwrt.Name, ubios.Name: - // On router, ctrld run as a DNS forwarder, it does not have to change system DNS. - // Except for: - // + EdgeOS, which /etc/resolv.conf could be managed by vyatta_update_resolv.pl script. - // + Merlin/Tomato, which has WAN DNS setup on boot for NTP. - // + Synology, which /etc/resolv.conf is not configured to point to localhost. - return - } - if cfg.Listener == nil || cfg.Listener["0"] == nil { + if cfg.Listener == nil { return } if iface == "" { @@ -239,6 +227,10 @@ func (p *prog) setDNS() { if iface == "auto" { iface = defaultIfaceName() } + lc := cfg.FirstListener() + if lc == nil { + return + } logger := mainLog.With().Str("iface", iface).Logger() netIface, err := netInterface(iface) if err != nil { @@ -250,23 +242,29 @@ func (p *prog) setDNS() { return } logger.Debug().Msg("setting DNS for interface") - ns := cfg.Listener["0"].IP - if router.Name() == firewalla.Name && (ns == "127.0.0.1" || ns == "0.0.0.0" || ns == "") { + ns := lc.IP + ifaceName := defaultIfaceName() + isFirewalla := router.Name() == firewalla.Name + if isFirewalla { // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. - if ns == "127.0.0.1" { - logger.Warn().Msg("127.0.0.1 as DNS server won't work on Firewalla") - } else { - logger.Warn().Msgf("%q could not be used as DNS server", ns) + ifaceName = "br0" + logger.Warn().Msg("using br0 interface IP address as DNS server") + } + if couldBeDirectListener(lc) { + // If ctrld is direct listener, use 127.0.0.1 as nameserver. + ns = "127.0.0.1" + } else if lc.Port != 53 { + logger.Warn().Msg("ctrld is not running on port 53, use default route interface as DNS server") + netIface, err := net.InterfaceByName(ifaceName) + if err != nil { + mainLog.Fatal().Err(err).Msg("failed to get default route interface") } - if netIface, err := net.InterfaceByName("br0"); err == nil { - addrs, _ := netIface.Addrs() - for _, addr := range addrs { - if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { - logger.Warn().Msg("using br0 interface IP address as DNS server") - ns = netIP.IP.To4().String() - break - } + addrs, _ := netIface.Addrs() + for _, addr := range addrs { + if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { + ns = netIP.IP.To4().String() + break } } } @@ -278,11 +276,6 @@ func (p *prog) setDNS() { } func (p *prog) resetDNS() { - switch router.Name() { - case ddwrt.Name, openwrt.Name, ubios.Name: - // See comment in p.setDNS method. - return - } if iface == "" { return } @@ -312,6 +305,13 @@ func randomLocalIP() string { return fmt.Sprintf("127.0.0.%d", n) } +func randomPort() int { + max := 1<<16 - 1 + min := 1025 + n := rand.Intn(max-min) + min + return n +} + // runLogServer starts a unix listener, use by startCmd to gather log from runCmd. func runLogServer(sockPath string) net.Conn { addr, err := net.ResolveUnixAddr("unix", sockPath) diff --git a/config.go b/config.go index 9c553b2..afc1e49 100644 --- a/config.go +++ b/config.go @@ -12,6 +12,8 @@ import ( "net/url" "os" "runtime" + "sort" + "strconv" "strings" "sync" "sync/atomic" @@ -123,6 +125,25 @@ func (c *Config) HasUpstreamSendClientInfo() bool { return false } +// FirstListener returns the first listener config of current config. Listeners are sorted numerically. +// +// It panics if Config has no listeners configured. +func (c *Config) FirstListener() *ListenerConfig { + listeners := make([]int, 0, len(c.Listener)) + for k := range c.Listener { + n, err := strconv.Atoi(k) + if err != nil { + continue + } + listeners = append(listeners, n) + } + if len(listeners) == 0 { + panic("missing listener config") + } + sort.Ints(listeners) + return c.Listener[strconv.Itoa(listeners[0])] +} + // ServiceConfig specifies the general ctrld config. type ServiceConfig struct { LogLevel string `mapstructure:"log_level" toml:"log_level,omitempty"` diff --git a/config_test.go b/config_test.go index 27f9d40..3591327 100644 --- a/config_test.go +++ b/config_test.go @@ -1,6 +1,7 @@ package ctrld_test import ( + "strings" "testing" "github.com/go-playground/validator/v10" @@ -56,6 +57,20 @@ func TestLoadDefaultConfig(t *testing.T) { assert.Len(t, cfg.Upstream, 2) } +func TestConfigOverride(t *testing.T) { + v := viper.NewWithOptions(viper.KeyDelimiter("::")) + ctrld.InitConfig(v, "test_load_config") + v.SetConfigType("toml") + require.NoError(t, v.ReadConfig(strings.NewReader(testhelper.SampleConfigStr(t)))) + cfg := ctrld.Config{Listener: map[string]*ctrld.ListenerConfig{ + "0": {IP: "127.0.0.1", Port: 53}, + }} + require.NoError(t, v.Unmarshal(&cfg)) + + assert.Equal(t, "10.10.42.69", cfg.Listener["1"].IP) + assert.Equal(t, 1337, cfg.Listener["1"].Port) +} + func TestConfigValidation(t *testing.T) { tests := []struct { name string diff --git a/internal/dns/nm.go b/internal/dns/nm.go index a8ea923..b8bc0c7 100644 --- a/internal/dns/nm.go +++ b/internal/dns/nm.go @@ -267,5 +267,5 @@ func (m *nmManager) Close() error { } func (m *nmManager) Mode() string { - return "network-maanger" + return "network-manager" } diff --git a/internal/router/ddwrt/ddwrt.go b/internal/router/ddwrt/ddwrt.go index cf45b30..f2cffdc 100644 --- a/internal/router/ddwrt/ddwrt.go +++ b/internal/router/ddwrt/ddwrt.go @@ -57,12 +57,6 @@ func (d *Ddwrt) PreRun() error { return ntp.Wait() } -func (d *Ddwrt) Configure() error { - d.cfg.Listener["0"].IP = "127.0.0.1" - d.cfg.Listener["0"].Port = 5354 - return nil -} - func (d *Ddwrt) Setup() error { // Already setup. if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { diff --git a/internal/router/dnsmasq/dnsmasq.go b/internal/router/dnsmasq/dnsmasq.go index 6089c43..0b051aa 100644 --- a/internal/router/dnsmasq/dnsmasq.go +++ b/internal/router/dnsmasq/dnsmasq.go @@ -1,6 +1,7 @@ package dnsmasq import ( + "errors" "html/template" "net" "path/filepath" @@ -60,15 +61,20 @@ type Upstream struct { } func ConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) { - upstreams := make([]Upstream, 0, len(cfg.Listener)) - for _, listener := range cfg.Listener { - upstreams = append(upstreams, Upstream{Ip: listener.IP, Port: listener.Port}) + listener := cfg.FirstListener() + if listener == nil { + return "", errors.New("missing listener") } + ip := listener.IP + if ip == "0.0.0.0" || ip == "::" || ip == "" { + ip = "127.0.0.1" + } + upstreams := []Upstream{{Ip: ip, Port: listener.Port}} return confTmpl(tmplText, upstreams, cfg.HasUpstreamSendClientInfo()) } func FirewallaConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) { - if lc := cfg.Listener["0"]; lc != nil && lc.IP == "0.0.0.0" { + if lc := cfg.FirstListener(); lc != nil && (lc.IP == "0.0.0.0" || lc.IP == "") { return confTmpl(tmplText, firewallaUpstreams(lc.Port), cfg.HasUpstreamSendClientInfo()) } return ConfTmpl(tmplText, cfg) diff --git a/internal/router/dummy.go b/internal/router/dummy.go index 71d7a82..dea54e0 100644 --- a/internal/router/dummy.go +++ b/internal/router/dummy.go @@ -4,10 +4,6 @@ import "github.com/kardianos/service" type dummy struct{} -func NewDummyRouter() Router { - return &dummy{} -} - func (d *dummy) ConfigureService(_ *service.Config) error { return nil } diff --git a/internal/router/edgeos/edgeos.go b/internal/router/edgeos/edgeos.go index c6b1e3c..f84e3b1 100644 --- a/internal/router/edgeos/edgeos.go +++ b/internal/router/edgeos/edgeos.go @@ -61,12 +61,6 @@ func (e *EdgeOS) PreRun() error { return nil } -func (e *EdgeOS) Configure() error { - e.cfg.Listener["0"].IP = "127.0.0.1" - e.cfg.Listener["0"].Port = 5354 - return nil -} - func (e *EdgeOS) Setup() error { if e.isUSG { return e.setupUSG() diff --git a/internal/router/firewalla/firewalla.go b/internal/router/firewalla/firewalla.go index fd4635f..4e177ed 100644 --- a/internal/router/firewalla/firewalla.go +++ b/internal/router/firewalla/firewalla.go @@ -53,12 +53,6 @@ func (f *Firewalla) PreRun() error { return nil } -func (f *Firewalla) Configure() error { - f.cfg.Listener["0"].IP = "0.0.0.0" - f.cfg.Listener["0"].Port = 5354 - return nil -} - func (f *Firewalla) Setup() error { data, err := dnsmasq.FirewallaConfTmpl(dnsmasq.ConfigContentTmpl, f.cfg) if err != nil { diff --git a/internal/router/merlin/merlin.go b/internal/router/merlin/merlin.go index 9e84298..18b07c5 100644 --- a/internal/router/merlin/merlin.go +++ b/internal/router/merlin/merlin.go @@ -48,12 +48,6 @@ func (m *Merlin) PreRun() error { return ntp.Wait() } -func (m *Merlin) Configure() error { - m.cfg.Listener["0"].IP = "127.0.0.1" - m.cfg.Listener["0"].Port = 5354 - return nil -} - func (m *Merlin) Setup() error { buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath) // Already setup. diff --git a/internal/router/openwrt/openwrt.go b/internal/router/openwrt/openwrt.go index bd08b12..1c8860d 100644 --- a/internal/router/openwrt/openwrt.go +++ b/internal/router/openwrt/openwrt.go @@ -48,12 +48,6 @@ func (o *Openwrt) PreRun() error { return nil } -func (o *Openwrt) Configure() error { - o.cfg.Listener["0"].IP = "127.0.0.1" - o.cfg.Listener["0"].Port = 5354 - return nil -} - func (o *Openwrt) Setup() error { // Delete dnsmasq port if set. if _, err := uci("delete", "dhcp.@dnsmasq[0].port"); err != nil && !errors.Is(err, errUCIEntryNotFound) { diff --git a/internal/router/pfsense/pfsense.go b/internal/router/pfsense/pfsense.go index d724c23..ec2b890 100644 --- a/internal/router/pfsense/pfsense.go +++ b/internal/router/pfsense/pfsense.go @@ -54,6 +54,14 @@ func (p *Pfsense) ConfigureService(svc *service.Config) error { } func (p *Pfsense) Install(config *service.Config) error { + // pfsense need ".sh" extension for script to be run at boot. + // See: https://docs.netgate.com/pfsense/en/latest/development/boot-commands.html#shell-script-option + oldname := filepath.Join(rcPath, p.svcName) + newname := filepath.Join(rcPath, p.svcName+".sh") + _ = os.Remove(newname) + if err := os.Symlink(oldname, newname); err != nil { + return fmt.Errorf("os.Symlink: %w", err) + } return nil } @@ -62,16 +70,7 @@ func (p *Pfsense) Uninstall(config *service.Config) error { } func (p *Pfsense) PreRun() error { - return nil -} - -func (p *Pfsense) Configure() error { - p.cfg.Listener["0"].IP = "127.0.0.1" - p.cfg.Listener["0"].Port = 53 - return nil -} - -func (p *Pfsense) Setup() error { + // TODO: remove this hacky solution. // If Pfsense is in DNS Resolver mode, ensure no unbound processes running. _ = exec.Command("killall", "unbound").Run() @@ -80,6 +79,10 @@ func (p *Pfsense) Setup() error { return nil } +func (p *Pfsense) Setup() error { + return nil +} + func (p *Pfsense) Cleanup() error { if err := os.Remove(filepath.Join(rcPath, p.svcName+".sh")); err != nil { return fmt.Errorf("os.Remove: %w", err) diff --git a/internal/router/router.go b/internal/router/router.go index 257e3b4..839151e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -27,15 +27,9 @@ type Service interface { Uninstall(*service.Config) error } -// Config is the interface to manage ctrld config on router. -type Config interface { - Configure() error -} - // Router is the interface for managing ctrld running on router. type Router interface { Service - Config PreRun() error Setup() error @@ -64,7 +58,7 @@ func New(cfg *ctrld.Config) Router { case firewalla.Name: return firewalla.New(cfg) } - return NewDummyRouter() + return &dummy{} } // IsGLiNet reports whether the router is an GL.iNet router. @@ -94,38 +88,6 @@ type router struct { sendClientInfo bool } -// IsSupported reports whether the given platform is supported by ctrld. -func IsSupported(platform string) bool { - switch platform { - case ddwrt.Name, - edgeos.Name, - firewalla.Name, - merlin.Name, - openwrt.Name, - pfsense.Name, - synology.Name, - tomato.Name, - ubios.Name: - return true - } - return false -} - -// SupportedPlatforms return all platforms that can be configured to run with ctrld. -func SupportedPlatforms() []string { - return []string{ - ddwrt.Name, - edgeos.Name, - firewalla.Name, - merlin.Name, - openwrt.Name, - pfsense.Name, - synology.Name, - tomato.Name, - ubios.Name, - } -} - // Name returns name of the router platform. func Name() string { if r := routerPlatform.Load(); r != nil { diff --git a/internal/router/synology/synology.go b/internal/router/synology/synology.go index e1d51bd..78551e4 100644 --- a/internal/router/synology/synology.go +++ b/internal/router/synology/synology.go @@ -43,12 +43,6 @@ func (s *Synology) PreRun() error { return nil } -func (s *Synology) Configure() error { - s.cfg.Listener["0"].IP = "127.0.0.1" - s.cfg.Listener["0"].Port = 5354 - return nil -} - func (s *Synology) Setup() error { data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, s.cfg) if err != nil { diff --git a/internal/router/tomato/tomato.go b/internal/router/tomato/tomato.go index 937f8ba..4c1824d 100644 --- a/internal/router/tomato/tomato.go +++ b/internal/router/tomato/tomato.go @@ -52,12 +52,6 @@ func (f *FreshTomato) PreRun() error { return ntp.Wait() } -func (f *FreshTomato) Configure() error { - f.cfg.Listener["0"].IP = "127.0.0.1" - f.cfg.Listener["0"].Port = 5354 - return nil -} - func (f *FreshTomato) Setup() error { // Already setup. if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { diff --git a/internal/router/ubios/ubios.go b/internal/router/ubios/ubios.go index 61ab8e0..06194d9 100644 --- a/internal/router/ubios/ubios.go +++ b/internal/router/ubios/ubios.go @@ -46,12 +46,6 @@ func (u *Ubios) PreRun() error { return nil } -func (u *Ubios) Configure() error { - u.cfg.Listener["0"].IP = "127.0.0.1" - u.cfg.Listener["0"].Port = 5354 - return nil -} - func (u *Ubios) Setup() error { data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, u.cfg) if err != nil { diff --git a/testhelper/config.go b/testhelper/config.go index 0b739f0..5c2e5f4 100644 --- a/testhelper/config.go +++ b/testhelper/config.go @@ -19,6 +19,10 @@ func SampleConfig(t *testing.T) *ctrld.Config { return &cfg } +func SampleConfigStr(t *testing.T) string { + return sampleConfigContent +} + var sampleConfigContent = ` [service] log_level = "info" From a4edf266f0d7bad437d3913d64110a012ac93de1 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 11 Jul 2023 00:24:54 +0700 Subject: [PATCH 33/84] all: workaround problem with EdgeOS dnsmasq config --- cmd/ctrld/prog.go | 27 ++++++++++------ cmd/ctrld/prog_linux.go | 1 - internal/router/dnsmasq/conf.go | 30 ++++++++++++++++++ internal/router/dnsmasq/conf_test.go | 46 ++++++++++++++++++++++++++++ 4 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 internal/router/dnsmasq/conf.go create mode 100644 internal/router/dnsmasq/conf_test.go diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index e94e0e9..83e71ae 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -16,6 +16,8 @@ import ( "github.com/Control-D-Inc/ctrld/internal/clientinfo" "github.com/Control-D-Inc/ctrld/internal/dnscache" "github.com/Control-D-Inc/ctrld/internal/router" + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + "github.com/Control-D-Inc/ctrld/internal/router/edgeos" "github.com/Control-D-Inc/ctrld/internal/router/firewalla" ) @@ -243,19 +245,26 @@ func (p *prog) setDNS() { } logger.Debug().Msg("setting DNS for interface") ns := lc.IP - ifaceName := defaultIfaceName() - isFirewalla := router.Name() == firewalla.Name - if isFirewalla { - // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. - // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. - ifaceName = "br0" - logger.Warn().Msg("using br0 interface IP address as DNS server") - } if couldBeDirectListener(lc) { // If ctrld is direct listener, use 127.0.0.1 as nameserver. ns = "127.0.0.1" } else if lc.Port != 53 { - logger.Warn().Msg("ctrld is not running on port 53, use default route interface as DNS server") + ifaceName := defaultIfaceName() + switch router.Name() { + case firewalla.Name: + // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. + // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. + ifaceName = "br0" + logger.Warn().Msg("using br0 interface IP address as DNS server") + case edgeos.Name: + // On EdgeOS, dnsmasq is run with "--local-service", so we need to get + // the proper interface from dnsmasq config. + if name, _ := dnsmasq.InterfaceNameFromConfig("/etc/dnsmasq.conf"); name != "" { + ifaceName = name + logger.Warn().Msgf("using %s interface IP address as DNS server", ifaceName) + } + } + logger.Warn().Msg("ctrld is not running on port 53, use interface %s IP as DNS server") netIface, err := net.InterfaceByName(ifaceName) if err != nil { mainLog.Fatal().Err(err).Msg("failed to get default route interface") diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 0748b51..a3f2823 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -34,7 +34,6 @@ func setDependencies(svc *service.Config) { svc.Dependencies = append(svc.Dependencies, "Wants=vyatta-dhcpd.service") svc.Dependencies = append(svc.Dependencies, "After=vyatta-dhcpd.service") svc.Dependencies = append(svc.Dependencies, "Wants=dnsmasq.service") - svc.Dependencies = append(svc.Dependencies, "After=dnsmasq.service") } } diff --git a/internal/router/dnsmasq/conf.go b/internal/router/dnsmasq/conf.go new file mode 100644 index 0000000..b168042 --- /dev/null +++ b/internal/router/dnsmasq/conf.go @@ -0,0 +1,30 @@ +package dnsmasq + +import ( + "bufio" + "bytes" + "errors" + "io" + "os" + "strings" +) + +func InterfaceNameFromConfig(filename string) (string, error) { + buf, err := os.ReadFile(filename) + if err != nil { + return "", err + } + return interfaceNameFromReader(bytes.NewReader(buf)) +} + +func interfaceNameFromReader(r io.Reader) (string, error) { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + after, found := strings.CutPrefix(line, "interface=") + if found { + return after, nil + } + } + return "", errors.New("not found") +} diff --git a/internal/router/dnsmasq/conf_test.go b/internal/router/dnsmasq/conf_test.go new file mode 100644 index 0000000..99a0710 --- /dev/null +++ b/internal/router/dnsmasq/conf_test.go @@ -0,0 +1,46 @@ +package dnsmasq + +import ( + "strings" + "testing" +) + +func Test_interfaceNameFromReader(t *testing.T) { + tests := []struct { + name string + in string + wantIface string + }{ + { + "good", + `interface=lo`, + "lo", + }, + { + "multiple", + `interface=lo +interface=eth0 +`, + "lo", + }, + { + "no iface", + `cache-size=100`, + "", + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + ifaceName, err := interfaceNameFromReader(strings.NewReader(tc.in)) + if tc.wantIface != "" && err != nil { + t.Errorf("unexpected error: %v", err) + return + } + if tc.wantIface != ifaceName { + t.Errorf("mismatched, want: %q, got: %q", tc.wantIface, ifaceName) + } + }) + } +} From dc61fd255477348091238d03fe15d6bf9fbc0bc0 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 11 Jul 2023 18:23:09 +0700 Subject: [PATCH 34/84] all: update handling of local config For local config, we don't want to alter what user explicitly set, and only try filling in missing value. While at it, also remove the dnsmasq port delete on openwrt, we don't need that hack anymore. --- cmd/ctrld/cli.go | 98 ++++++++++++++++++++++++++---- cmd/ctrld/prog.go | 2 +- internal/router/openwrt/openwrt.go | 5 -- 3 files changed, 87 insertions(+), 18 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index e6c40f3..6876fc2 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -996,7 +996,13 @@ func selfCheckStatus(status service.Status, domain string) service.Status { lcChanged map[string]*ctrld.ListenerConfig mu sync.Mutex ) - curCfg := cfg + + if err := v.ReadInConfig(); err != nil { + mainLog.Fatal().Err(err).Msg("failed to read new config") + } + if err := v.Unmarshal(&cfg); err != nil { + mainLog.Fatal().Err(err).Msg("failed to update new config") + } watcher, err := fsnotify.NewWatcher() if err != nil { mainLog.Error().Err(err).Msg("could not watch config change") @@ -1016,10 +1022,10 @@ func selfCheckStatus(status service.Status, domain string) service.Status { for i := 0; i < maxAttempts; i++ { mu.Lock() if lcChanged != nil { - curCfg.Listener = lcChanged + cfg.Listener = lcChanged } mu.Unlock() - lc := curCfg.FirstListener() + lc := cfg.FirstListener() m := new(dns.Msg) m.SetQuestion(domain+".", dns.TypeA) @@ -1183,16 +1189,31 @@ func shouldAllocateLoopbackIP(ipStr string) bool { return ip.IsLoopback() && ip.String() != "127.0.0.1" } +type listenerConfigCheck struct { + IP bool + Port bool +} + // updateListenerConfig updates the config for listeners if not defined, // or defined but invalid to be used, e.g: using loopback address other // than 127.0.0.1 with sytemd-resolved. func updateListenerConfig() { - for _, listener := range cfg.Listener { + lcc := make(map[string]*listenerConfigCheck) + cdMode := cdUID != "" + for n, listener := range cfg.Listener { + lcc[n] = &listenerConfigCheck{} if listener.IP == "" { listener.IP = "0.0.0.0" + lcc[n].IP = true } if listener.Port == 0 { listener.Port = 53 + lcc[n].Port = true + } + // In cd mode, we always try to pick an ip:port pair to work. + if cdMode { + lcc[n].IP = true + lcc[n].Port = true } } @@ -1229,7 +1250,10 @@ func updateListenerConfig() { for _, n := range listeners { listener := cfg.Listener[strconv.Itoa(n)] + check := lcc[strconv.Itoa(n)] oldIP := listener.IP + oldPort := listener.Port + // Check if we could listen on the current IP + Port, if not, try following thing, pick first one success: // - Try 127.0.0.1:53 // - Pick a random port until success. @@ -1243,6 +1267,8 @@ func updateListenerConfig() { // On firewalla, we don't need to check localhost, because the lo interface is excluded in dnsmasq // config, so we can always listen on localhost port 53, but no traffic could be routed there. tryLocalhost := !isLoopback(listener.IP) && router.Name() != firewalla.Name + tryAllPort53 := true + tryOldIPPort5354 := true tryPort5354 := true attempts := 0 maxAttempts := 10 @@ -1254,22 +1280,70 @@ func updateListenerConfig() { if listenOk(addr) { break } + if !check.IP && !check.Port { + mainLog.Fatal().Msgf("failed to listen on: %s", addr) + } + if tryAllPort53 { + tryAllPort53 = false + if check.IP { + listener.IP = "0.0.0.0" + } + if check.Port { + listener.Port = 53 + } + if check.IP { + mainLog.Warn().Msgf("could not listen on address: %s, trying: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + } + continue + } if tryLocalhost { tryLocalhost = false - listener.IP = localhostIP(listener.IP) - listener.Port = 53 - mainLog.Warn().Msgf("could not listen on address: %s, trying localhost: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + if check.IP { + listener.IP = localhostIP(listener.IP) + } + if check.Port { + listener.Port = 53 + } + if check.IP { + mainLog.Warn().Msgf("could not listen on address: %s, trying localhost: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + } + continue + } + if tryOldIPPort5354 { + tryOldIPPort5354 = false + if check.IP { + listener.IP = oldIP + } + if check.Port { + listener.Port = 5354 + } + mainLog.Warn().Msgf("could not listen on address: %s, trying current ip with port 5354", addr) continue } if tryPort5354 { tryPort5354 = false - listener.IP = oldIP - listener.Port = 5354 - mainLog.Warn().Msgf("could not listen on address: %s, trying port 5354", addr) + if check.IP { + listener.IP = "0.0.0.0" + } + if check.Port { + listener.Port = 5354 + } + mainLog.Warn().Msgf("could not listen on address: %s, trying 0.0.0.0:5354", addr) continue } - listener.IP = randomLocalIP() - listener.Port = randomPort() + if check.IP { + listener.IP = randomLocalIP() + } else { + listener.IP = oldIP + } + if check.Port { + listener.Port = randomPort() + } else { + listener.Port = oldPort + } + if listener.IP == oldIP && listener.Port == oldPort { + mainLog.Fatal().Msgf("could not listener on: %s", net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + } mainLog.Warn().Msgf("could not listen on address: %s, pick a random ip+port", addr) attempts++ } diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 83e71ae..394b4a0 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -264,7 +264,7 @@ func (p *prog) setDNS() { logger.Warn().Msgf("using %s interface IP address as DNS server", ifaceName) } } - logger.Warn().Msg("ctrld is not running on port 53, use interface %s IP as DNS server") + logger.Warn().Msgf("ctrld is not running on port 53, use interface %s IP as DNS server", ifaceName) netIface, err := net.InterfaceByName(ifaceName) if err != nil { mainLog.Fatal().Err(err).Msg("failed to get default route interface") diff --git a/internal/router/openwrt/openwrt.go b/internal/router/openwrt/openwrt.go index 1c8860d..1d8de34 100644 --- a/internal/router/openwrt/openwrt.go +++ b/internal/router/openwrt/openwrt.go @@ -49,11 +49,6 @@ func (o *Openwrt) PreRun() error { } func (o *Openwrt) Setup() error { - // Delete dnsmasq port if set. - if _, err := uci("delete", "dhcp.@dnsmasq[0].port"); err != nil && !errors.Is(err, errUCIEntryNotFound) { - return err - } - data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, o.cfg) if err != nil { return err From e65a71b2ae1405e784e5c3979fedb2e9d5fe1e20 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 14 Jul 2023 00:06:25 +0000 Subject: [PATCH 35/84] cmd/ctrld: do not try random local ip if IP is v4/v6 zero --- cmd/ctrld/cli.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 6876fc2..8a362f7 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -1253,6 +1253,7 @@ func updateListenerConfig() { check := lcc[strconv.Itoa(n)] oldIP := listener.IP oldPort := listener.Port + isZeroIP := listener.IP == "0.0.0.0" || listener.IP == "::" // Check if we could listen on the current IP + Port, if not, try following thing, pick first one success: // - Try 127.0.0.1:53 @@ -1331,7 +1332,7 @@ func updateListenerConfig() { mainLog.Warn().Msgf("could not listen on address: %s, trying 0.0.0.0:5354", addr) continue } - if check.IP { + if check.IP && !isZeroIP { // for "0.0.0.0" or "::", we only need to try new port. listener.IP = randomLocalIP() } else { listener.IP = oldIP From 28df551195e31f45cf760304c1f1f3edcebe6b77 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 14 Jul 2023 00:18:33 +0000 Subject: [PATCH 36/84] cmd/ctrld: prefix log with listener number when update listener config --- cmd/ctrld/cli.go | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 8a362f7..c14a4c7 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -1238,6 +1238,12 @@ func updateListenerConfig() { return udpErr == nil && tcpErr == nil } + logMsg := func(e *zerolog.Event, listenerNum int, format string, v ...any) { + e.MsgFunc(func() string { + return fmt.Sprintf("listener.%d %s", listenerNum, fmt.Sprintf(format, v...)) + }) + } + listeners := make([]int, 0, len(cfg.Listener)) for k := range cfg.Listener { n, err := strconv.Atoi(k) @@ -1275,14 +1281,14 @@ func updateListenerConfig() { maxAttempts := 10 for { if attempts == maxAttempts { - mainLog.Fatal().Msg("could not find available listen ip and port") + logMsg(mainLog.Fatal(), n, "could not find available listen ip and port") } addr := net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)) if listenOk(addr) { break } if !check.IP && !check.Port { - mainLog.Fatal().Msgf("failed to listen on: %s", addr) + logMsg(mainLog.Fatal(), n, "failed to listen on: %s", addr) } if tryAllPort53 { tryAllPort53 = false @@ -1293,7 +1299,7 @@ func updateListenerConfig() { listener.Port = 53 } if check.IP { - mainLog.Warn().Msgf("could not listen on address: %s, trying: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + logMsg(mainLog.Warn(), n, "could not listen on address: %s, trying: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) } continue } @@ -1306,7 +1312,7 @@ func updateListenerConfig() { listener.Port = 53 } if check.IP { - mainLog.Warn().Msgf("could not listen on address: %s, trying localhost: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + logMsg(mainLog.Warn(), n, "could not listen on address: %s, trying localhost: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) } continue } @@ -1318,7 +1324,7 @@ func updateListenerConfig() { if check.Port { listener.Port = 5354 } - mainLog.Warn().Msgf("could not listen on address: %s, trying current ip with port 5354", addr) + logMsg(mainLog.Warn(), n, "could not listen on address: %s, trying current ip with port 5354", addr) continue } if tryPort5354 { @@ -1329,7 +1335,7 @@ func updateListenerConfig() { if check.Port { listener.Port = 5354 } - mainLog.Warn().Msgf("could not listen on address: %s, trying 0.0.0.0:5354", addr) + logMsg(mainLog.Warn(), n, "could not listen on address: %s, trying 0.0.0.0:5354", addr) continue } if check.IP && !isZeroIP { // for "0.0.0.0" or "::", we only need to try new port. @@ -1343,9 +1349,9 @@ func updateListenerConfig() { listener.Port = oldPort } if listener.IP == oldIP && listener.Port == oldPort { - mainLog.Fatal().Msgf("could not listener on: %s", net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + logMsg(mainLog.Fatal(), n, "could not listener on: %s", net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) } - mainLog.Warn().Msgf("could not listen on address: %s, pick a random ip+port", addr) + logMsg(mainLog.Warn(), n, "could not listen on address: %s, pick a random ip+port", addr) attempts++ } } @@ -1353,11 +1359,12 @@ func updateListenerConfig() { // Specific case for systemd-resolved. if useSystemdResolved { if listener := cfg.FirstListener(); listener != nil && listener.Port == 53 { + n := listeners[0] // systemd-resolved does not allow forwarding DNS queries from 127.0.0.53 to loopback // ip address, other than "127.0.0.1", so trying to listen on default route interface // address instead. if ip := net.ParseIP(listener.IP); ip != nil && ip.IsLoopback() && ip.String() != "127.0.0.1" { - mainLog.Warn().Msg("using loopback interface do not work with systemd-resolved") + logMsg(mainLog.Warn(), n, "using loopback interface do not work with systemd-resolved") found := false if netIface, _ := net.InterfaceByName(defaultIfaceName()); netIface != nil { addrs, _ := netIface.Addrs() @@ -1367,14 +1374,14 @@ func updateListenerConfig() { if listenOk(addr) { found = true listener.IP = netIP.IP.String() - mainLog.Warn().Msgf("use %s as listener address", listener.IP) + logMsg(mainLog.Warn(), n, "use %s as listener address", listener.IP) break } } } } if !found { - mainLog.Fatal().Msgf("could not use %q as DNS nameserver with systemd resolved", listener.IP) + logMsg(mainLog.Fatal(), n, "could not use %q as DNS nameserver with systemd resolved", listener.IP) } } } From 48a780fc3e8f720327f4c551e85483aa4d1f0c5b Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 14 Jul 2023 11:43:32 +0000 Subject: [PATCH 37/84] cmd/ctrld: add workaround for default iface name on Ubios --- cmd/ctrld/cli.go | 5 +++++ cmd/ctrld/prog.go | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index c14a4c7..acbdb2d 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -43,6 +43,7 @@ import ( "github.com/Control-D-Inc/ctrld/internal/router/firewalla" "github.com/Control-D-Inc/ctrld/internal/router/merlin" "github.com/Control-D-Inc/ctrld/internal/router/tomato" + "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) var ( @@ -975,6 +976,10 @@ func defaultIfaceName() string { if oi := osinfo.New(); strings.Contains(oi.String(), "Microsoft") { return "lo" } + // Same as WSL case above. + if router.Name() == ubios.Name { + return "lo" + } mainLog.Fatal().Err(err).Msg("failed to get default route interface") } return dri diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 394b4a0..24fd9d9 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -249,7 +249,7 @@ func (p *prog) setDNS() { // If ctrld is direct listener, use 127.0.0.1 as nameserver. ns = "127.0.0.1" } else if lc.Port != 53 { - ifaceName := defaultIfaceName() + ifaceName := iface switch router.Name() { case firewalla.Name: // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. From fa3af372ab7276ca32f241b3c79d326492126d20 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 14 Jul 2023 15:59:21 +0000 Subject: [PATCH 38/84] Use ControlD anycast IP if no system DNS found --- resolver.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/resolver.go b/resolver.go index f61c9bc..2512434 100644 --- a/resolver.go +++ b/resolver.go @@ -29,6 +29,13 @@ const ( var bootstrapDNS = "76.76.2.0" var or = &osResolver{nameservers: nameservers()} +func init() { + if len(or.nameservers) == 0 { + // Add bootstrap DNS in case we did not find any. + or.nameservers = []string{net.JoinHostPort(bootstrapDNS, "53")} + } +} + // Resolver is the interface that wraps the basic DNS operations. // // Resolve resolves the DNS query, return the result and the corresponding error. From 3007cb86ec6cf1dfe331d39ec9d4a9ba2a80cb41 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 14 Jul 2023 16:36:46 +0000 Subject: [PATCH 39/84] cmd/ctrld: add control server/client via unix socket --- cmd/ctrld/cli.go | 10 ++++++ cmd/ctrld/control_client.go | 29 ++++++++++++++++ cmd/ctrld/control_server.go | 59 ++++++++++++++++++++++++++++++++ cmd/ctrld/control_server_test.go | 54 +++++++++++++++++++++++++++++ cmd/ctrld/prog.go | 12 +++++-- 5 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 cmd/ctrld/control_client.go create mode 100644 cmd/ctrld/control_server.go create mode 100644 cmd/ctrld/control_server_test.go diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index acbdb2d..bb8a8da 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -134,6 +134,11 @@ func initCLI() { stopCh: stopCh, cfg: &cfg, } + if homedir == "" { + if dir, err := userHomeDir(); err == nil { + homedir = dir + } + } sockPath := filepath.Join(homedir, ctrldLogUnixSock) if addr, err := net.ResolveUnixAddr("unix", sockPath); err == nil { if conn, err := net.Dial(addr.Network(), addr.String()); err == nil { @@ -186,6 +191,11 @@ func initCLI() { } p.router = router.New(&cfg) + cs, err := newControlServer(filepath.Join(homedir, ctrldControlUnixSock)) + if err != nil { + mainLog.Warn().Err(err).Msg("could not create control server") + } + p.cs = cs // Processing --cd flag require connecting to ControlD API, which needs valid // time for validating server certificate. Some routers need NTP synchronization diff --git a/cmd/ctrld/control_client.go b/cmd/ctrld/control_client.go new file mode 100644 index 0000000..0a94c99 --- /dev/null +++ b/cmd/ctrld/control_client.go @@ -0,0 +1,29 @@ +package main + +import ( + "context" + "io" + "net" + "net/http" + "time" +) + +type controlClient struct { + c *http.Client +} + +func newControlClient(addr string) *controlClient { + return &controlClient{c: &http.Client{ + Transport: &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + d := net.Dialer{} + return d.DialContext(ctx, "unix", addr) + }, + }, + Timeout: time.Second * 5, + }} +} + +func (c *controlClient) post(path string, data io.Reader) (*http.Response, error) { + return c.c.Post("http://unix"+path, contentTypeJson, data) +} diff --git a/cmd/ctrld/control_server.go b/cmd/ctrld/control_server.go new file mode 100644 index 0000000..437e4a8 --- /dev/null +++ b/cmd/ctrld/control_server.go @@ -0,0 +1,59 @@ +package main + +import ( + "context" + "net" + "net/http" + "os" + "time" +) + +const contentTypeJson = "application/json" + +type controlServer struct { + server *http.Server + mux *http.ServeMux + addr string +} + +func newControlServer(addr string) (*controlServer, error) { + mux := http.NewServeMux() + s := &controlServer{ + server: &http.Server{Handler: mux}, + mux: mux, + } + s.addr = addr + return s, nil +} + +func (s *controlServer) start() error { + _ = os.Remove(s.addr) + unixListener, err := net.Listen("unix", s.addr) + if err != nil { + return err + } + go s.server.Serve(unixListener) + return nil +} + +func (s *controlServer) stop() error { + _ = os.Remove(s.addr) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*2) + defer cancel() + return s.server.Shutdown(ctx) +} + +func (s *controlServer) register(pattern string, handler http.Handler) { + s.mux.Handle(pattern, jsonResponse(handler)) +} + +func (p *prog) registerControlServerHandler() { + // TODO: register handler here. +} + +func jsonResponse(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + next.ServeHTTP(w, r) + }) +} diff --git a/cmd/ctrld/control_server_test.go b/cmd/ctrld/control_server_test.go new file mode 100644 index 0000000..2bcd64a --- /dev/null +++ b/cmd/ctrld/control_server_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "bytes" + "io" + "net/http" + "os" + "testing" +) + +func TestControlServer(t *testing.T) { + f, err := os.CreateTemp("", "") + if err != nil { + t.Fatal(err) + } + defer os.Remove(f.Name()) + f.Close() + + s, err := newControlServer(f.Name()) + if err != nil { + t.Fatal(err) + } + pattern := "/ping" + respBody := []byte("pong") + s.register(pattern, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + _, _ = w.Write(respBody) + })) + if err := s.start(); err != nil { + t.Fatal(err) + } + + c := newControlClient(f.Name()) + resp, err := c.post(pattern, nil) + if err != nil { + t.Fatal(err) + } + defer resp.Body.Close() + if resp.StatusCode != 200 { + t.Fatalf("unepxected response code: %d", resp.StatusCode) + } + if ct := resp.Header.Get("content-type"); ct != contentTypeJson { + t.Fatalf("unexpected content type: %s", ct) + } + buf, err := io.ReadAll(resp.Body) + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(buf, respBody) { + t.Errorf("unexpected response body, want: %q, got: %q", string(respBody), string(buf)) + } + if err := s.stop(); err != nil { + t.Fatal(err) + } +} diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 24fd9d9..3298cd4 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -22,8 +22,9 @@ import ( ) const ( - defaultSemaphoreCap = 256 - ctrldLogUnixSock = "ctrld_start.sock" + defaultSemaphoreCap = 256 + ctrldLogUnixSock = "ctrld_start.sock" + ctrldControlUnixSock = "ctrld_control.sock" ) var logf = func(format string, args ...any) { @@ -45,6 +46,7 @@ type prog struct { waitCh chan struct{} stopCh chan struct{} logConn net.Conn + cs *controlServer cfg *ctrld.Config cache dnscache.Cacher @@ -183,6 +185,12 @@ func (p *prog) run() { if p.logConn != nil { _ = p.logConn.Close() } + if p.cs != nil { + p.registerControlServerHandler() + if err := p.cs.start(); err != nil { + mainLog.Warn().Err(err).Msg("could not start control server") + } + } wg.Wait() } From 76d2e2c2266d6416cde67a85c4d6985726012656 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 14 Jul 2023 16:53:17 +0000 Subject: [PATCH 40/84] Improving Mac discovery --- cmd/ctrld/cli.go | 3 + cmd/ctrld/dns_proxy.go | 31 ++- cmd/ctrld/dns_proxy_test.go | 4 +- cmd/ctrld/prog.go | 34 ++- config.go | 8 +- config_test.go | 26 ++ internal/clientinfo/arp.go | 29 +++ internal/clientinfo/arp_linux.go | 28 ++ internal/clientinfo/arp_test.go | 23 ++ internal/clientinfo/arp_unix.go | 30 +++ internal/clientinfo/arp_windows.go | 38 +++ internal/clientinfo/client_info.go | 324 ++++++++++++------------ internal/clientinfo/client_info_test.go | 80 ------ internal/clientinfo/dhcp.go | 256 +++++++++++++++++++ internal/clientinfo/dhcp_lease_files.go | 18 ++ internal/clientinfo/dhcp_test.go | 88 +++++++ internal/clientinfo/mdns.go | 163 ++++++++++++ internal/clientinfo/mdns_services.go | 70 +++++ internal/clientinfo/merlin.go | 67 +++++ internal/clientinfo/merlin_test.go | 82 ++++++ internal/clientinfo/ptr_lookup.go | 62 +++++ resolver.go | 56 +++- 22 files changed, 1229 insertions(+), 291 deletions(-) create mode 100644 internal/clientinfo/arp.go create mode 100644 internal/clientinfo/arp_linux.go create mode 100644 internal/clientinfo/arp_test.go create mode 100644 internal/clientinfo/arp_unix.go create mode 100644 internal/clientinfo/arp_windows.go create mode 100644 internal/clientinfo/dhcp.go create mode 100644 internal/clientinfo/dhcp_lease_files.go create mode 100644 internal/clientinfo/dhcp_test.go create mode 100644 internal/clientinfo/mdns.go create mode 100644 internal/clientinfo/mdns_services.go create mode 100644 internal/clientinfo/merlin.go create mode 100644 internal/clientinfo/merlin_test.go create mode 100644 internal/clientinfo/ptr_lookup.go diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index bb8a8da..6ed93e7 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -1138,6 +1138,7 @@ func validateConfig(cfg *ctrld.Config) { os.Exit(1) } +// NOTE: Add more case here once new validation tag is used in ctrld.Config struct. func fieldErrorMsg(fe validator.FieldError) string { switch fe.Tag() { case "oneof": @@ -1165,6 +1166,8 @@ func fieldErrorMsg(fe validator.FieldError) string { return fmt.Sprintf("must be one of: %q", strings.Join(ipStacks, " ")) case "iporempty": return fmt.Sprintf("invalid IP format: %s", fe.Value()) + case "file": + return fmt.Sprintf("filed does not exist: %s", fe.Value()) } return "" } diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 52a5fb1..b263aa7 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -55,7 +55,10 @@ func (p *prog) serveDNS(listenerNum string) error { q := m.Question[0] domain := canonicalName(q.Name) reqId := requestID() - remoteAddr := spoofRemoteAddr(w.RemoteAddr(), p.mt.GetClientInfoByMac(macFromMsg(m))) + remoteIP, _, _ := net.SplitHostPort(w.RemoteAddr().String()) + mac := macFromMsg(m) + ci := p.getClientInfo(remoteIP, mac) + remoteAddr := spoofRemoteAddr(w.RemoteAddr(), ci) fmtSrcToDest := fmtRemoteToLocal(listenerNum, remoteAddr.String(), w.LocalAddr().String()) t := time.Now() ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId) @@ -66,7 +69,7 @@ func (p *prog) serveDNS(listenerNum string) error { answer = new(dns.Msg) answer.SetRcode(m, dns.RcodeRefused) } else { - answer = p.proxy(ctx, upstreams, failoverRcodes, m) + answer = p.proxy(ctx, upstreams, failoverRcodes, m, ci) rtt := time.Since(t) ctrld.Log(ctx, mainLog.Debug(), "received response of %d bytes in %s", answer.Len(), rtt) } @@ -202,7 +205,7 @@ networkRules: return upstreams, matched } -func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []int, msg *dns.Msg) *dns.Msg { +func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []int, msg *dns.Msg, ci *ctrld.ClientInfo) *dns.Msg { var staleAnswer *dns.Msg serveStaleCache := p.cache != nil && p.cfg.Service.CacheServeStale upstreamConfigs := p.upstreamConfigsFromUpstreamNumbers(upstreams) @@ -245,12 +248,9 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i return dnsResolver.Resolve(resolveCtx, msg) } resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg { - if upstreamConfig.UpstreamSendClientInfo() { - ci := p.mt.GetClientInfoByMac(macFromMsg(msg)) - if ci != nil { - ctrld.Log(ctx, mainLog.Debug(), "including client info with the request") - ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, ci) - } + if upstreamConfig.UpstreamSendClientInfo() && ci != nil { + ctrld.Log(ctx, mainLog.Debug(), "including client info with the request") + ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, ci) } answer, err := resolve1(n, upstreamConfig, msg) if err != nil { @@ -510,3 +510,16 @@ func inContainer() bool { }) return ret } + +func (p *prog) getClientInfo(ip, mac string) *ctrld.ClientInfo { + ci := &ctrld.ClientInfo{} + if mac != "" { + ci.Mac = mac + ci.IP = p.ciTable.LookupIP(mac) + } else { + ci.IP = ip + ci.Mac = p.ciTable.LookupMac(ip) + } + ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac) + return ci +} diff --git a/cmd/ctrld/dns_proxy_test.go b/cmd/ctrld/dns_proxy_test.go index 2d29bc3..3245875 100644 --- a/cmd/ctrld/dns_proxy_test.go +++ b/cmd/ctrld/dns_proxy_test.go @@ -149,8 +149,8 @@ func TestCache(t *testing.T) { answer2.SetRcode(msg, dns.RcodeRefused) prog.cache.Add(dnscache.NewKey(msg, "upstream.0"), dnscache.NewValue(answer2, time.Now().Add(time.Minute))) - got1 := prog.proxy(context.Background(), []string{"upstream.1"}, nil, msg) - got2 := prog.proxy(context.Background(), []string{"upstream.0"}, nil, msg) + got1 := prog.proxy(context.Background(), []string{"upstream.1"}, nil, msg, nil) + got2 := prog.proxy(context.Background(), []string{"upstream.0"}, nil, msg, nil) assert.NotSame(t, got1, got2) assert.Equal(t, answer1.Rcode, got1.Rcode) assert.Equal(t, answer2.Rcode, got2.Rcode) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 3298cd4..9aa7db4 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -48,11 +48,11 @@ type prog struct { logConn net.Conn cs *controlServer - cfg *ctrld.Config - cache dnscache.Cacher - sema semaphore - mt *clientinfo.MacTable - router router.Router + cfg *ctrld.Config + cache dnscache.Cacher + sema semaphore + ciTable *clientinfo.Table + router router.Router started chan struct{} onStarted []func() @@ -106,24 +106,22 @@ func (p *prog) run() { uc.Init() if uc.BootstrapIP == "" { uc.SetupBootstrapIP() - mainLog.Info().Msgf("Bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs()) + mainLog.Info().Msgf("bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs()) } else { - mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("Using bootstrap IP for upstream.%s", n) + mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("using bootstrap IP for upstream.%s", n) } uc.SetCertPool(rootCertPool) go uc.Ping() } - p.mt = clientinfo.NewMacTable() - if p.cfg.HasUpstreamSendClientInfo() { - mainLog.Debug().Msg("Sending client info enabled") - if err := p.mt.Init(); err == nil { - mainLog.Debug().Msg("Start watching client info changes") - go p.mt.WatchLeaseFiles() - } else { - mainLog.Warn().Err(err).Msg("could not record client info") - } + p.ciTable = clientinfo.NewTable(&cfg) + if leaseFile := p.cfg.Service.DHCPLeaseFile; leaseFile != "" { + mainLog.Debug().Msgf("watching custom lease file: %s", leaseFile) + format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat) + p.ciTable.AddLeaseFile(leaseFile, format) } + p.ciTable.Init() + go p.ciTable.RefreshLoop(p.stopCh) go p.watchLinkState() for listenerNum := range p.cfg.Listener { @@ -136,7 +134,7 @@ func (p *prog) run() { mainLog.Warn().Msgf("no default upstream for: [listener.%s]", listenerNum) } addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)) - mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, addr) + mainLog.Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr) err := p.serveDNS(listenerNum) if err != nil && !defaultConfigWritten && cdUID == "" { mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) @@ -162,7 +160,7 @@ func (p *prog) run() { p.cfg.Service.AllocateIP = true p.mu.Unlock() p.preRun() - mainLog.Info().Msgf("Starting DNS server on listener.%s: %s", listenerNum, net.JoinHostPort(ip, strconv.Itoa(port))) + mainLog.Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, net.JoinHostPort(ip, strconv.Itoa(port))) if err := p.serveDNS(listenerNum); err != nil { mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) return diff --git a/config.go b/config.go index afc1e49..9e8f518 100644 --- a/config.go +++ b/config.go @@ -153,6 +153,12 @@ type ServiceConfig struct { CacheTTLOverride int `mapstructure:"cache_ttl_override" toml:"cache_ttl_override,omitempty"` CacheServeStale bool `mapstructure:"cache_serve_stale" toml:"cache_serve_stale,omitempty"` MaxConcurrentRequests *int `mapstructure:"max_concurrent_requests" toml:"max_concurrent_requests,omitempty" validate:"omitempty,gte=0"` + DHCPLeaseFile string `mapstructure:"dhcp_lease_file_path" toml:"dhcp_lease_file_path" validate:"omitempty,file"` + DHCPLeaseFileFormat string `mapstructure:"dhcp_lease_file_format" toml:"dhcp_lease_file_format" validate:"required_unless=DHCPLeaseFile '',omitempty,oneof=dnsmasq isc-dhcp"` + DiscoverMDNS *bool `mapstructure:"discover_mdns" toml:"discover_mdns,omitempty"` + DiscoverARP *bool `mapstructure:"discover_arp" toml:"discover_dhcp,omitempty"` + DiscoverDHCP *bool `mapstructure:"discover_dhcp" toml:"discover_dhcp,omitempty"` + DiscoverPtr *bool `mapstructure:"discover_ptr" toml:"discover_ptr,omitempty"` Daemon bool `mapstructure:"-" toml:"-"` AllocateIP bool `mapstructure:"-" toml:"-"` } @@ -316,7 +322,7 @@ func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) { uc.bootstrapIPs4 = append(uc.bootstrapIPs4, ip) } } - ProxyLog.Debug().Msgf("Bootstrap IPs: %v", uc.bootstrapIPs) + ProxyLog.Debug().Msgf("bootstrap IPs: %v", uc.bootstrapIPs) } // ReBootstrap re-setup the bootstrap IP and the transport. diff --git a/config_test.go b/config_test.go index 3591327..83a3386 100644 --- a/config_test.go +++ b/config_test.go @@ -1,6 +1,7 @@ package ctrld_test import ( + "os" "strings" "testing" @@ -91,6 +92,9 @@ func TestConfigValidation(t *testing.T) { {"invalid rules", configWithInvalidRules(t), true}, {"invalid dns rcodes", configWithInvalidRcodes(t), true}, {"invalid max concurrent requests", configWithInvalidMaxConcurrentRequests(t), true}, + {"non-existed lease file", configWithNonExistedLeaseFile(t), true}, + {"lease file format required if lease file exist", configWithExistedLeaseFile(t), true}, + {"invalid lease file format", configWithInvalidLeaseFileFormat(t), true}, } for _, tc := range tests { @@ -199,3 +203,25 @@ func configWithInvalidMaxConcurrentRequests(t *testing.T) *ctrld.Config { cfg.Service.MaxConcurrentRequests = &n return cfg } + +func configWithNonExistedLeaseFile(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Service.DHCPLeaseFile = "non-existed" + return cfg +} + +func configWithExistedLeaseFile(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + exe, err := os.Executable() + if err != nil { + t.Fatal(err) + } + cfg.Service.DHCPLeaseFile = exe + return cfg +} + +func configWithInvalidLeaseFileFormat(t *testing.T) *ctrld.Config { + cfg := defaultConfig(t) + cfg.Service.DHCPLeaseFileFormat = "invalid" + return cfg +} diff --git a/internal/clientinfo/arp.go b/internal/clientinfo/arp.go new file mode 100644 index 0000000..ef70031 --- /dev/null +++ b/internal/clientinfo/arp.go @@ -0,0 +1,29 @@ +package clientinfo + +import "sync" + +type arpDiscover struct { + mac sync.Map // ip => mac + ip sync.Map // mac => ip +} + +func (a *arpDiscover) refresh() error { + a.scan() + return nil +} + +func (a *arpDiscover) LookupIP(mac string) string { + val, ok := a.ip.Load(mac) + if !ok { + return "" + } + return val.(string) +} + +func (a *arpDiscover) LookupMac(ip string) string { + val, ok := a.mac.Load(ip) + if !ok { + return "" + } + return val.(string) +} diff --git a/internal/clientinfo/arp_linux.go b/internal/clientinfo/arp_linux.go new file mode 100644 index 0000000..3e48337 --- /dev/null +++ b/internal/clientinfo/arp_linux.go @@ -0,0 +1,28 @@ +package clientinfo + +import ( + "bufio" + "os" + "strings" +) + +const procNetArpFile = "/proc/net/arp" + +func (a *arpDiscover) scan() { + f, err := os.Open(procNetArpFile) + if err != nil { + return + } + defer f.Close() + + s := bufio.NewScanner(f) + s.Scan() // skip header + for s.Scan() { + line := s.Text() + fields := strings.Fields(line) + ip := fields[0] + mac := fields[3] + a.mac.Store(ip, mac) + a.ip.Store(mac, ip) + } +} diff --git a/internal/clientinfo/arp_test.go b/internal/clientinfo/arp_test.go new file mode 100644 index 0000000..08a75f8 --- /dev/null +++ b/internal/clientinfo/arp_test.go @@ -0,0 +1,23 @@ +package clientinfo + +import ( + "sync" + "testing" +) + +func TestArpScan(t *testing.T) { + a := &arpDiscover{} + a.scan() + + for _, table := range []*sync.Map{&a.mac, &a.ip} { + count := 0 + table.Range(func(key, value any) bool { + count++ + t.Logf("%s => %s", key, value) + return true + }) + if count == 0 { + t.Error("empty result from arp scan") + } + } +} diff --git a/internal/clientinfo/arp_unix.go b/internal/clientinfo/arp_unix.go new file mode 100644 index 0000000..f5d8f88 --- /dev/null +++ b/internal/clientinfo/arp_unix.go @@ -0,0 +1,30 @@ +//go:build !linux && !windows + +package clientinfo + +import ( + "os/exec" + "strings" +) + +func (a *arpDiscover) scan() { + data, err := exec.Command("arp", "-an").Output() + if err != nil { + return + } + + for _, line := range strings.Split(string(data), "\n") { + fields := strings.Fields(line) + if len(fields) <= 3 { + continue + } + + // trim brackets + ip := strings.ReplaceAll(fields[1], "(", "") + ip = strings.ReplaceAll(ip, ")", "") + + mac := fields[3] + a.mac.Store(ip, mac) + a.ip.Store(mac, ip) + } +} diff --git a/internal/clientinfo/arp_windows.go b/internal/clientinfo/arp_windows.go new file mode 100644 index 0000000..016b752 --- /dev/null +++ b/internal/clientinfo/arp_windows.go @@ -0,0 +1,38 @@ +package clientinfo + +import ( + "os/exec" + "strings" +) + +func (a *arpDiscover) scan() { + data, err := exec.Command("arp", "-a").Output() + if err != nil { + return + } + + header := false + for _, line := range strings.Split(string(data), "\n") { + if len(line) == 0 { + continue // empty lines + } + if line[0] != ' ' { + header = true // "Interface:" lines, next is header line. + continue + } + if header { + header = false // header lines + continue + } + + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + + ip := fields[0] + mac := strings.ReplaceAll(fields[1], "-", ":") + a.mac.Store(ip, mac) + a.ip.Store(mac, ip) + } +} diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 48b8678..79e1acd 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -1,211 +1,194 @@ package clientinfo import ( - "bufio" - "bytes" - "fmt" - "io" - "log" - "net" - "os" "strings" - "sync" "time" - "github.com/fsnotify/fsnotify" - "tailscale.com/util/lineread" - "github.com/Control-D-Inc/ctrld" ) -// clientInfoFiles specifies client info files and how to read them on supported platforms. -var clientInfoFiles = map[string]ctrld.LeaseFileFormat{ - "/tmp/dnsmasq.leases": ctrld.Dnsmasq, // ddwrt - "/tmp/dhcp.leases": ctrld.Dnsmasq, // openwrt - "/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // merlin - "/mnt/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDM Pro - "/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDR - "/etc/dhcpd/dhcpd-leases.log": ctrld.Dnsmasq, // Synology - "/tmp/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // Tomato - "/run/dnsmasq-dhcp.leases": ctrld.Dnsmasq, // EdgeOS - "/run/dhcpd.leases": ctrld.IscDhcpd, // EdgeOS - "/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense - "/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla +// IpResolver is the interface for retrieving IP from Mac. +type IpResolver interface { + LookupIP(mac string) string } -// NewMacTable returns new Mac table to record client information. -func NewMacTable() *MacTable { - return &MacTable{} +// MacResolver is the interface for retrieving Mac from IP. +type MacResolver interface { + LookupMac(ip string) string } -// MacTable records clients information by MAC address. -type MacTable struct { - mac sync.Map - watcher *fsnotify.Watcher +// HostnameByIpResolver is the interface for retrieving hostname from IP. +type HostnameByIpResolver interface { + LookupHostnameByIP(ip string) string } -// Init initializes recording client info. -func (mt *MacTable) Init() error { - watcher, err := fsnotify.NewWatcher() - if err != nil { - return err - } - mt.watcher = watcher - for file, format := range clientInfoFiles { - // Ignore errors for default lease files. - _ = mt.AddLeaseFile(file, format) - } - return nil +// HostnameByMacResolver is the interface for retrieving hostname from Mac. +type HostnameByMacResolver interface { + LookupHostnameByMac(mac string) string } -// AddLeaseFile adds given lease file for reading/watching clients info. -func (mt *MacTable) AddLeaseFile(name string, format ctrld.LeaseFileFormat) error { - if err := mt.readLeaseFile(name, format); err != nil { - return fmt.Errorf("could not read lease file: %w", err) - } - clientInfoFiles[name] = format - return mt.watcher.Add(name) +type HostnameResolver interface { + HostnameByIpResolver + HostnameByMacResolver } -// GetClientInfoByMac returns ClientInfo for the client associated with the given MAC address. -func (mt *MacTable) GetClientInfoByMac(mac string) *ctrld.ClientInfo { - if mac == "" { - return nil - } - val, ok := mt.mac.Load(mac) - if !ok { - return nil - } - return val.(*ctrld.ClientInfo) +type refresher interface { + refresh() error } -// WatchLeaseFiles watches changes happens in dnsmasq/dhcpd -// lease files, perform updating to mac table if necessary. -func (mt *MacTable) WatchLeaseFiles() { - if mt.watcher == nil { +type Table struct { + ipResolvers []IpResolver + macResolvers []MacResolver + hostnameResolvers []HostnameResolver + refreshers []refresher + + dhcp *dhcp + merlin *merlinDiscover + arp *arpDiscover + ptr *ptrDiscover + mdns *mdns + cfg *ctrld.Config +} + +func NewTable(cfg *ctrld.Config) *Table { + return &Table{cfg: cfg} +} + +func (t *Table) AddLeaseFile(name string, format ctrld.LeaseFileFormat) { + if !t.discoverDHCP() { return } + clientInfoFiles[name] = format +} + +func (t *Table) RefreshLoop(stopCh chan struct{}) { timer := time.NewTicker(time.Minute * 5) for { select { case <-timer.C: - for _, name := range mt.watcher.WatchList() { - format := clientInfoFiles[name] - if err := mt.readLeaseFile(name, format); err != nil { - ctrld.ProxyLog.Err(err).Str("file", name).Msg("failed to update lease file") - } + for _, r := range t.refreshers { + _ = r.refresh() } - case event, ok := <-mt.watcher.Events: - if !ok { - return - } - if event.Has(fsnotify.Write) { - format := clientInfoFiles[event.Name] - if err := mt.readLeaseFile(event.Name, format); err != nil && !os.IsNotExist(err) { - ctrld.ProxyLog.Err(err).Str("file", event.Name).Msg("leases file changed but failed to update client info") - } - } - case err, ok := <-mt.watcher.Errors: - if !ok { - return - } - ctrld.ProxyLog.Err(err).Msg("could not watch client info file") + case <-stopCh: + return } } } -// readLeaseFile reads the lease file with given format, saving client information to mac table. -func (mt *MacTable) readLeaseFile(name string, format ctrld.LeaseFileFormat) error { - switch format { - case ctrld.Dnsmasq: - return mt.dnsmasqReadClientInfoFile(name) - case ctrld.IscDhcpd: - return mt.iscDHCPReadClientInfoFile(name) +func (t *Table) Init() { + if t.discoverDHCP() || t.discoverARP() { + t.merlin = &merlinDiscover{} + if err := t.merlin.refresh(); err != nil { + ctrld.ProxyLog.Error().Err(err).Msg("could not init Merlin discover") + } else { + t.hostnameResolvers = append(t.hostnameResolvers, t.merlin) + t.refreshers = append(t.refreshers, t.merlin) + } + } + if t.discoverDHCP() { + t.dhcp = &dhcp{} + ctrld.ProxyLog.Debug().Msg("start dhcp discovery") + if err := t.dhcp.refresh(); err != nil { + ctrld.ProxyLog.Error().Err(err).Msg("could not init DHCP discover") + } else { + t.ipResolvers = append(t.ipResolvers, t.dhcp) + t.macResolvers = append(t.macResolvers, t.dhcp) + t.hostnameResolvers = append(t.hostnameResolvers, t.dhcp) + t.refreshers = append(t.refreshers, t.dhcp) + } + go t.dhcp.watchChanges() + } + if t.discoverARP() { + t.arp = &arpDiscover{} + ctrld.ProxyLog.Debug().Msg("start arp discovery") + if err := t.arp.refresh(); err != nil { + ctrld.ProxyLog.Error().Err(err).Msg("could not init ARP discover") + } else { + t.ipResolvers = append(t.ipResolvers, t.arp) + t.macResolvers = append(t.macResolvers, t.arp) + t.refreshers = append(t.refreshers, t.arp) + } + } + if t.discoverPTR() { + t.ptr = &ptrDiscover{resolver: ctrld.NewPrivateResolver()} + ctrld.ProxyLog.Debug().Msg("start ptr discovery") + if err := t.ptr.refresh(); err != nil { + ctrld.ProxyLog.Error().Err(err).Msg("could not init PTR discover") + } else { + t.hostnameResolvers = append(t.hostnameResolvers, t.ptr) + t.refreshers = append(t.refreshers, t.ptr) + } + } + if t.discoverMDNS() { + t.mdns = &mdns{} + ctrld.ProxyLog.Debug().Msg("start mdns discovery") + if err := t.mdns.init(); err != nil { + ctrld.ProxyLog.Error().Err(err).Msg("could not init mDNS discover") + } else { + t.hostnameResolvers = append(t.hostnameResolvers, t.mdns) + } } - return fmt.Errorf("unsupported format: %s, file: %s", format, name) } -// dnsmasqReadClientInfoFile populates mac table with client info reading from dnsmasq lease file. -func (mt *MacTable) dnsmasqReadClientInfoFile(name string) error { - f, err := os.Open(name) - if err != nil { - return err +func (t *Table) LookupIP(mac string) string { + for _, r := range t.ipResolvers { + if ip := r.LookupIP(mac); ip != "" { + return ip + } } - defer f.Close() - return mt.dnsmasqReadClientInfoReader(f) - + return "" } -// dnsmasqReadClientInfoReader likes ctrld.Dnsmasq, but reading from an io.Reader instead of file. -func (mt *MacTable) dnsmasqReadClientInfoReader(reader io.Reader) error { - return lineread.Reader(reader, func(line []byte) error { - fields := bytes.Fields(line) - if len(fields) < 4 { - return nil - } - mac := string(fields[1]) - if _, err := net.ParseMAC(mac); err != nil { - // The second field is not a mac, skip. - return nil - } - ip := normalizeIP(string(fields[2])) - if net.ParseIP(ip) == nil { - log.Printf("invalid ip address entry: %q", ip) - ip = "" - } - hostname := string(fields[3]) - mt.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname}) - return nil +func (t *Table) LookupMac(ip string) string { + t.arp.mac.Range(func(key, value any) bool { + return true }) -} - -// iscDHCPReadClientInfoFile populates mac table with client info reading from isc-dhcpd lease file. -func (mt *MacTable) iscDHCPReadClientInfoFile(name string) error { - f, err := os.Open(name) - if err != nil { - return err - } - defer f.Close() - return mt.iscDHCPReadClientInfoReader(f) -} - -// iscDHCPReadClientInfoReader likes ctrld.IscDhcpd, but reading from an io.Reader instead of file. -func (mt *MacTable) iscDHCPReadClientInfoReader(reader io.Reader) error { - s := bufio.NewScanner(reader) - var ip, mac, hostname string - for s.Scan() { - line := s.Text() - if strings.HasPrefix(line, "}") { - if mac != "" { - mt.mac.Store(mac, &ctrld.ClientInfo{Mac: mac, IP: ip, Hostname: hostname}) - ip, mac, hostname = "", "", "" - } - continue - } - fields := strings.Fields(line) - if len(fields) < 2 { - continue - } - switch fields[0] { - case "lease": - ip = normalizeIP(strings.ToLower(fields[1])) - if net.ParseIP(ip) == nil { - log.Printf("invalid ip address entry: %q", ip) - ip = "" - } - case "hardware": - if len(fields) >= 3 { - mac = strings.ToLower(strings.TrimRight(fields[2], ";")) - if _, err := net.ParseMAC(mac); err != nil { - // Invalid mac, skip. - mac = "" - } - } - case "client-hostname": - hostname = strings.Trim(fields[1], `";`) + for _, r := range t.macResolvers { + if mac := r.LookupMac(ip); mac != "" { + return mac } } - return nil + return "" +} + +func (t *Table) LookupHostname(ip, mac string) string { + for _, r := range t.hostnameResolvers { + if name := r.LookupHostnameByIP(ip); name != "" { + return name + } + if name := r.LookupHostnameByMac(mac); name != "" { + return name + } + } + return "" +} + +func (t *Table) discoverDHCP() bool { + if t.cfg.Service.DiscoverDHCP == nil { + return true + } + return *t.cfg.Service.DiscoverDHCP +} + +func (t *Table) discoverARP() bool { + if t.cfg.Service.DiscoverARP == nil { + return true + } + return *t.cfg.Service.DiscoverARP +} + +func (t *Table) discoverMDNS() bool { + if t.cfg.Service.DiscoverMDNS == nil { + return true + } + return *t.cfg.Service.DiscoverMDNS +} + +func (t *Table) discoverPTR() bool { + if t.cfg.Service.DiscoverPtr == nil { + return true + } + return *t.cfg.Service.DiscoverPtr } // normalizeIP normalizes the ip parsed from dnsmasq/dhcpd lease file. @@ -217,3 +200,10 @@ func normalizeIP(in string) string { } return in } + +func normalizeHostname(name string) string { + if before, _, found := strings.Cut(name, "."); found { + return before // remove ".local.", ".lan.", ... suffix + } + return strings.ToLower(name) +} diff --git a/internal/clientinfo/client_info_test.go b/internal/clientinfo/client_info_test.go index 2f2e092..79e5912 100644 --- a/internal/clientinfo/client_info_test.go +++ b/internal/clientinfo/client_info_test.go @@ -1,11 +1,7 @@ package clientinfo import ( - "io" - "strings" "testing" - - "github.com/Control-D-Inc/ctrld" ) func Test_normalizeIP(t *testing.T) { @@ -29,79 +25,3 @@ func Test_normalizeIP(t *testing.T) { }) } } - -func Test_readClientInfoReader(t *testing.T) { - mt := NewMacTable() - tests := []struct { - name string - in string - readFunc func(r io.Reader) error - mac string - }{ - { - "good dnsmasq", - `1683329857 e6:20:59:b8:c1:6d 192.168.1.186 * 01:e6:20:59:b8:c1:6d -`, - mt.dnsmasqReadClientInfoReader, - "e6:20:59:b8:c1:6d", - }, - { - "bad dnsmasq seen on UDMdream machine", - `1683329857 e6:20:59:b8:c1:6e 192.168.1.111 * 01:e6:20:59:b8:c1:6e -duid 00:01:00:01:2b:e4:2e:2c:52:52:14:26:dc:1c -1683322985 117442354 2600:4040:b0e6:b700::111 ASDASD 00:01:00:01:2a:d0:b9:81:00:07:32:4c:1c:07 -`, - mt.dnsmasqReadClientInfoReader, - "e6:20:59:b8:c1:6e", - }, - { - "isc-dhcpd good", - `lease 192.168.1.1 { - hardware ethernet 00:00:00:00:00:01; - client-hostname "host-1"; -} -`, - mt.iscDHCPReadClientInfoReader, - "00:00:00:00:00:01", - }, - { - "isc-dhcpd bad mac", - `lease 192.168.1.1 { - hardware ethernet invalid-mac; - client-hostname "host-1"; -} - -lease 192.168.1.2 { - hardware ethernet 00:00:00:00:00:02; - client-hostname "host-2"; -} -`, - mt.iscDHCPReadClientInfoReader, - "00:00:00:00:00:02", - }, - { - "", - `1685794060 00:00:00:00:00:04 192.168.0.209 cuonglm-ThinkPad-X1-Carbon-Gen-9 00:00:00:00:00:04 9`, - mt.dnsmasqReadClientInfoReader, - "00:00:00:00:00:04", - }, - } - - for _, tc := range tests { - t.Run(tc.name, func(t *testing.T) { - mt.mac.Delete(tc.mac) - if err := tc.readFunc(strings.NewReader(tc.in)); err != nil { - t.Errorf("readClientInfoReader() error = %v", err) - } - info, existed := mt.mac.Load(tc.mac) - if !existed { - t.Error("client info missing") - } - if ci, ok := info.(*ctrld.ClientInfo); ok && existed && ci.Mac != tc.mac { - t.Errorf("mac mismatched, got: %q, want: %q", ci.Mac, tc.mac) - } else { - t.Log(ci) - } - }) - } -} diff --git a/internal/clientinfo/dhcp.go b/internal/clientinfo/dhcp.go new file mode 100644 index 0000000..5ca28ec --- /dev/null +++ b/internal/clientinfo/dhcp.go @@ -0,0 +1,256 @@ +package clientinfo + +import ( + "bufio" + "bytes" + "fmt" + "io" + "net" + "net/netip" + "os" + "strings" + "sync" + + "github.com/Control-D-Inc/ctrld" + "tailscale.com/net/interfaces" + "tailscale.com/util/lineread" + + "github.com/fsnotify/fsnotify" +) + +type dhcp struct { + mac2name sync.Map // mac => name + ip2name sync.Map // ip => name + ip sync.Map // mac => ip + mac sync.Map // ip => mac + + watcher *fsnotify.Watcher +} + +func (d *dhcp) refresh() error { + watcher, err := fsnotify.NewWatcher() + if err != nil { + return err + } + d.addSelf() + d.watcher = watcher + for file, format := range clientInfoFiles { + // Ignore errors for default lease files. + _ = d.addLeaseFile(file, format) + } + return nil +} + +func (d *dhcp) watchChanges() { + if d.watcher == nil { + return + } + for { + select { + case event, ok := <-d.watcher.Events: + if !ok { + return + } + if event.Has(fsnotify.Write) { + format := clientInfoFiles[event.Name] + if err := d.readLeaseFile(event.Name, format); err != nil && !os.IsNotExist(err) { + ctrld.ProxyLog.Err(err).Str("file", event.Name).Msg("leases file changed but failed to update client info") + } + } + case err, ok := <-d.watcher.Errors: + if !ok { + return + } + ctrld.ProxyLog.Err(err).Msg("could not watch client info file") + } + } + +} + +func (d *dhcp) LookupIP(mac string) string { + val, ok := d.ip.Load(mac) + if !ok { + return "" + } + return val.(string) +} + +func (d *dhcp) LookupMac(ip string) string { + val, ok := d.mac.Load(ip) + if !ok { + return "" + } + return val.(string) +} + +func (d *dhcp) LookupHostnameByIP(ip string) string { + val, ok := d.ip2name.Load(ip) + if !ok { + return "" + } + return val.(string) +} + +func (d *dhcp) LookupHostnameByMac(mac string) string { + val, ok := d.mac2name.Load(mac) + if !ok { + return "" + } + return val.(string) +} + +// AddLeaseFile adds given lease file for reading/watching clients info. +func (d *dhcp) addLeaseFile(name string, format ctrld.LeaseFileFormat) error { + if d.watcher == nil { + return nil + } + if err := d.readLeaseFile(name, format); err != nil { + return fmt.Errorf("could not read lease file: %w", err) + } + clientInfoFiles[name] = format + return d.watcher.Add(name) +} + +// readLeaseFile reads the lease file with given format, saving client information to dhcp table. +func (d *dhcp) readLeaseFile(name string, format ctrld.LeaseFileFormat) error { + switch format { + case ctrld.Dnsmasq: + return d.dnsmasqReadClientInfoFile(name) + case ctrld.IscDhcpd: + return d.iscDHCPReadClientInfoFile(name) + } + return fmt.Errorf("unsupported format: %s, file: %s", format, name) +} + +// dnsmasqReadClientInfoFile populates dhcp table with client info reading from dnsmasq lease file. +func (d *dhcp) dnsmasqReadClientInfoFile(name string) error { + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + return d.dnsmasqReadClientInfoReader(f) + +} + +// dnsmasqReadClientInfoReader likes ctrld.Dnsmasq, but reading from an io.Reader instead of file. +func (d *dhcp) dnsmasqReadClientInfoReader(reader io.Reader) error { + return lineread.Reader(reader, func(line []byte) error { + fields := bytes.Fields(line) + if len(fields) < 4 { + return nil + } + + mac := string(fields[1]) + if _, err := net.ParseMAC(mac); err != nil { + // The second field is not a dhcp, skip. + return nil + } + ip := normalizeIP(string(fields[2])) + if net.ParseIP(ip) == nil { + ctrld.ProxyLog.Warn().Msgf("invalid ip address entry: %q", ip) + ip = "" + } + + d.mac.Store(ip, mac) + d.ip.Store(mac, ip) + hostname := string(fields[3]) + if hostname == "*" { + return nil + } + name := normalizeHostname(hostname) + d.mac2name.Store(mac, name) + d.ip2name.Store(ip, name) + return nil + }) +} + +// iscDHCPReadClientInfoFile populates dhcp table with client info reading from isc-dhcpd lease file. +func (d *dhcp) iscDHCPReadClientInfoFile(name string) error { + f, err := os.Open(name) + if err != nil { + return err + } + defer f.Close() + return d.iscDHCPReadClientInfoReader(f) +} + +// iscDHCPReadClientInfoReader likes ctrld.IscDhcpd, but reading from an io.Reader instead of file. +func (d *dhcp) iscDHCPReadClientInfoReader(reader io.Reader) error { + s := bufio.NewScanner(reader) + var ip, mac, hostname string + for s.Scan() { + line := s.Text() + if strings.HasPrefix(line, "}") { + d.mac.Store(ip, mac) + d.ip.Store(mac, ip) + if hostname != "" && hostname != "*" { + name := normalizeHostname(hostname) + d.mac2name.Store(mac, name) + d.ip2name.Store(ip, hostname) + ip, mac, hostname = "", "", "" + } + continue + } + fields := strings.Fields(line) + if len(fields) < 2 { + continue + } + switch fields[0] { + case "lease": + ip = normalizeIP(strings.ToLower(fields[1])) + if net.ParseIP(ip) == nil { + ctrld.ProxyLog.Warn().Msgf("invalid ip address entry: %q", ip) + ip = "" + } + case "hardware": + if len(fields) >= 3 { + mac = strings.ToLower(strings.TrimRight(fields[2], ";")) + if _, err := net.ParseMAC(mac); err != nil { + // Invalid dhcp, skip. + mac = "" + } + } + case "client-hostname": + hostname = strings.Trim(fields[1], `";`) + } + } + return nil +} + +// addSelf populates current host info to dhcp, so queries from +// the host itself can be attached with proper client info. +func (d *dhcp) addSelf() { + hostname, err := os.Hostname() + if err != nil { + ctrld.ProxyLog.Err(err).Msg("could not get hostname") + return + } + hostname = normalizeHostname(hostname) + d.ip2name.Store("127.0.0.1", hostname) + d.ip2name.Store("::1", hostname) + interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + mac := i.HardwareAddr.String() + // Skip loopback interfaces, info was stored above. + if mac == "" { + return + } + addrs, _ := i.Addrs() + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok { + continue + } + ip := ipNet.IP + d.mac.Store(ip.String(), mac) + d.ip.Store(mac, ip.String()) + if ip.To4() != nil { + d.mac.Store("127.0.0.1", mac) + } else { + d.mac.Store("::1", mac) + } + d.mac2name.Store(mac, hostname) + d.ip2name.Store(ip.String(), hostname) + } + }) +} diff --git a/internal/clientinfo/dhcp_lease_files.go b/internal/clientinfo/dhcp_lease_files.go new file mode 100644 index 0000000..4932a4b --- /dev/null +++ b/internal/clientinfo/dhcp_lease_files.go @@ -0,0 +1,18 @@ +package clientinfo + +import "github.com/Control-D-Inc/ctrld" + +// clientInfoFiles specifies client info files and how to read them on supported platforms. +var clientInfoFiles = map[string]ctrld.LeaseFileFormat{ + "/tmp/dnsmasq.leases": ctrld.Dnsmasq, // ddwrt + "/tmp/dhcp.leases": ctrld.Dnsmasq, // openwrt + "/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // merlin + "/mnt/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDM Pro + "/data/udapi-config/dnsmasq.lease": ctrld.Dnsmasq, // UDR + "/etc/dhcpd/dhcpd-leases.log": ctrld.Dnsmasq, // Synology + "/tmp/var/lib/misc/dnsmasq.leases": ctrld.Dnsmasq, // Tomato + "/run/dnsmasq-dhcp.leases": ctrld.Dnsmasq, // EdgeOS + "/run/dhcpd.leases": ctrld.IscDhcpd, // EdgeOS + "/var/dhcpd/var/db/dhcpd.leases": ctrld.IscDhcpd, // Pfsense + "/home/pi/.router/run/dhcp/dnsmasq.leases": ctrld.Dnsmasq, // Firewalla +} diff --git a/internal/clientinfo/dhcp_test.go b/internal/clientinfo/dhcp_test.go new file mode 100644 index 0000000..af3a168 --- /dev/null +++ b/internal/clientinfo/dhcp_test.go @@ -0,0 +1,88 @@ +package clientinfo + +import ( + "io" + "strings" + "testing" +) + +func Test_readClientInfoReader(t *testing.T) { + d := &dhcp{} + tests := []struct { + name string + in string + readFunc func(r io.Reader) error + mac string + hostname string + }{ + { + "good dnsmasq", + `1683329857 e6:20:59:b8:c1:6d 192.168.1.186 host1 01:e6:20:59:b8:c1:6d +`, + d.dnsmasqReadClientInfoReader, + "e6:20:59:b8:c1:6d", + "host1", + }, + { + "bad dnsmasq seen on UDMdream machine", + `1683329857 e6:20:59:b8:c1:6e 192.168.1.111 host1 01:e6:20:59:b8:c1:6e +duid 00:01:00:01:2b:e4:2e:2c:52:52:14:26:dc:1c +1683322985 117442354 2600:4040:b0e6:b700::111 ASDASD 00:01:00:01:2a:d0:b9:81:00:07:32:4c:1c:07 +`, + d.dnsmasqReadClientInfoReader, + "e6:20:59:b8:c1:6e", + "host1", + }, + { + "isc-dhcpd good", + `lease 192.168.1.1 { + hardware ethernet 00:00:00:00:00:01; + client-hostname "host-1"; +} +`, + d.iscDHCPReadClientInfoReader, + "00:00:00:00:00:01", + "host-1", + }, + { + "isc-dhcpd bad dhcp", + `lease 192.168.1.1 { + hardware ethernet invalid-dhcp; + client-hostname "host-1"; +} + +lease 192.168.1.2 { + hardware ethernet 00:00:00:00:00:02; + client-hostname "host-2"; +} +`, + d.iscDHCPReadClientInfoReader, + "00:00:00:00:00:02", + "host-2", + }, + { + "", + `1685794060 00:00:00:00:00:04 192.168.0.209 example 00:00:00:00:00:04 9`, + d.dnsmasqReadClientInfoReader, + "00:00:00:00:00:04", + "example", + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + d.mac2name.Delete(tc.mac) + if err := tc.readFunc(strings.NewReader(tc.in)); err != nil { + t.Errorf("readClientInfoReader() error = %v", err) + } + val, existed := d.mac2name.Load(tc.mac) + if !existed { + t.Error("client info missing") + } + hostname := val.(string) + if existed && hostname != tc.hostname { + t.Errorf("hostname mismatched, want: %q, got: %q", tc.hostname, hostname) + } + }) + } +} diff --git a/internal/clientinfo/mdns.go b/internal/clientinfo/mdns.go new file mode 100644 index 0000000..ce92d50 --- /dev/null +++ b/internal/clientinfo/mdns.go @@ -0,0 +1,163 @@ +package clientinfo + +import ( + "context" + "errors" + "net" + "sync" + "time" + + "github.com/miekg/dns" + "tailscale.com/logtail/backoff" + + "github.com/Control-D-Inc/ctrld" + ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" +) + +var ( + mdnsV4Addr = &net.UDPAddr{ + IP: net.ParseIP("224.0.0.251"), + Port: 5353, + } + mdnsV6Addr = &net.UDPAddr{ + IP: net.ParseIP("ff02::fb"), + Port: 5353, + } +) + +type mdns struct { + name sync.Map // ip => hostname +} + +func (m *mdns) LookupHostnameByIP(ip string) string { + val, ok := m.name.Load(ip) + if !ok { + return "" + } + return val.(string) +} + +func (m *mdns) LookupHostnameByMac(mac string) string { + return "" +} + +func (m *mdns) init() error { + ifaces, err := multicastInterfaces() + if err != nil { + return err + } + + v4ConnList := make([]*net.UDPConn, 0, len(ifaces)) + v6ConnList := make([]*net.UDPConn, 0, len(ifaces)) + for _, iface := range ifaces { + if iface.Flags&net.FlagLoopback != 0 { + continue + } + if conn, err := net.ListenMulticastUDP("udp4", &iface, mdnsV4Addr); err == nil { + v4ConnList = append(v4ConnList, conn) + go m.readLoop(conn) + } + if ctrldnet.IPv6Available(context.Background()) { + if conn, err := net.ListenMulticastUDP("udp6", &iface, mdnsV6Addr); err == nil { + v6ConnList = append(v6ConnList, conn) + go m.readLoop(conn) + } + } + } + + go func() { + bo := backoff.NewBackoff("mdns probe", func(format string, args ...any) {}, time.Second*30) + for { + err := m.probe(v4ConnList, v6ConnList) + if err != nil { + ctrld.ProxyLog.Warn().Err(err).Msg("error while probing mdns") + } + bo.BackOff(context.Background(), errors.New("mdns probe backoff")) + } + }() + + return nil +} + +func (m *mdns) readLoop(conn *net.UDPConn) { + defer conn.Close() + buf := make([]byte, dns.MaxMsgSize) + + for { + _ = conn.SetReadDeadline(time.Now().Add(time.Second * 30)) + n, _, err := conn.ReadFromUDP(buf) + if err != nil { + if err, ok := err.(*net.OpError); ok { + if err.Timeout() || err.Temporary() { + continue + } + ctrld.ProxyLog.Debug().Err(err).Msg("mdns readLoop error") + } + return + } + + var msg dns.Msg + if err := msg.Unpack(buf[:n]); err != nil { + continue + } + + var ip, name string + for _, answer := range msg.Answer { + switch ar := answer.(type) { + case *dns.A: + ip, name = ar.A.String(), ar.Hdr.Name + case *dns.AAAA: + ip, name = ar.AAAA.String(), ar.Hdr.Name + } + if ip != "" && name != "" { + name = normalizeHostname(name) + ctrld.ProxyLog.Debug().Msgf("Found hostname: %q, ip: %q via mdns", name, ip) + m.name.Store(ip, name) + } + } + } +} + +func (m *mdns) probe(v4connList, v6connList []*net.UDPConn) error { + msg := new(dns.Msg) + msg.Question = make([]dns.Question, len(services)) + for i, service := range services { + msg.Question[i] = dns.Question{ + Name: dns.CanonicalName(service), + Qtype: dns.TypePTR, + Qclass: dns.ClassINET, + } + } + + buf, err := msg.Pack() + if err != nil { + return err + } + do := func(connList []*net.UDPConn, remoteAddr net.Addr) error { + for _, conn := range connList { + _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 30)) + if _, err := conn.WriteTo(buf, remoteAddr); err != nil { + return err + } + } + return nil + } + return errors.Join(do(v4connList, mdnsV4Addr), do(v6connList, mdnsV6Addr)) +} + +func multicastInterfaces() ([]net.Interface, error) { + ifaces, err := net.Interfaces() + if err != nil { + return nil, err + } + interfaces := make([]net.Interface, 0, len(ifaces)) + for _, ifi := range ifaces { + if (ifi.Flags & net.FlagUp) == 0 { + continue + } + if (ifi.Flags & net.FlagMulticast) > 0 { + interfaces = append(interfaces, ifi) + } + } + return interfaces, nil +} diff --git a/internal/clientinfo/mdns_services.go b/internal/clientinfo/mdns_services.go new file mode 100644 index 0000000..d7869c8 --- /dev/null +++ b/internal/clientinfo/mdns_services.go @@ -0,0 +1,70 @@ +package clientinfo + +var services = [...]string{ + // From: https://jonathanmumm.com/tech-it/mdns-bonjour-bible-common-service-strings-for-various-vendors/ + "_afpovertcp._tcp.local.", + "_airdroid._tcp.local.", + "_airdrop._tcp.local.", + "_airplay._tcp.local.", + "_airport._tcp.local.", + "_amzn-wplay._tcp.local.", + "_sub._apple-mobdev2._tcp.local.", + "_apple-mobdev2._tcp.local.", + "_apple-sasl._tcp.local.", + "_atc._tcp.local.", + "_sketchmirror._tcp.local.", + "_bp2p._tcp.local.", + "_Friendly._sub._bp2p._tcp.local.", + "_invoke._sub._bp2p._tcp.local.", + "_webdav._sub._bp2p._tcp.local.", + "_device-info._tcp.local.", + "_distcc._tcp.local.", + "_dpap._tcp.local.", + "_eppc._tcp.local.", + "_esdevice._tcp.local.", + "_esfileshare._tcp.local.", + "_ftp._tcp.local.", + "_googlecast._tcp.local.", + "_googlezone._tcp.local.", + "_hap._tcp.local.", + "_homekit._tcp.local.", + "_home-sharing._tcp.local.", + "_http._tcp.local.", + "_hudson._tcp.local.", + "_ica-networking._tcp.local.", + "_print._sub._ipp._tcp.local.", + "_cups._sub._ipps._tcp.local.", + "_print._sub._ipps._tcp.local.", + "_jenkins._tcp.local.", + "_KeynoteControl._tcp.local.", + "_keynotepair._tcp.local.", + "_mediaremotetv._tcp.local.", + "_nfs._tcp.local.", + "_nvstream._tcp.local.", + "_androidtvremote._tcp.local.", + "_omnistate._tcp.local.", + "_photoshopserver._tcp.local.", + "_printer._tcp.local.", + "_raop._tcp.local.", + "_readynas._tcp.local.", + "_rfb._tcp.local.", + "_physicalweb._tcp.local.", + "_rsp._tcp.local.", + "_scanner._tcp.local.", + "_sftp-ssh._tcp.local.", + "_sleep-proxy._udp.local.", + "_smb._tcp.local.", + "_spotify-connect._tcp.local.", + "_ssh._tcp.local.", + "_teamviewer._tcp.local.", + "_telnet._tcp.local.", + "_touch-able._tcp.local.", + "_tunnel._tcp.local.", + "_webdav._tcp.local.", + "_webdav._tcp.local.", + "_workstation._tcp.local.", + "_xserveraid._tcp.local.", + + // Merlin + "_alexa._tcp", +} diff --git a/internal/clientinfo/merlin.go b/internal/clientinfo/merlin.go new file mode 100644 index 0000000..7e793ed --- /dev/null +++ b/internal/clientinfo/merlin.go @@ -0,0 +1,67 @@ +package clientinfo + +import ( + "strings" + "sync" + + "github.com/Control-D-Inc/ctrld/internal/router" + "github.com/Control-D-Inc/ctrld/internal/router/merlin" + + "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/router/nvram" +) + +const merlinNvramCustomClientListKey = "custom_clientlist" + +type merlinDiscover struct { + hostname sync.Map // mac => hostname +} + +func (m *merlinDiscover) refresh() error { + if router.Name() != merlin.Name { + return nil + } + out, err := nvram.Run("get", merlinNvramCustomClientListKey) + if err != nil { + return err + } + ctrld.ProxyLog.Debug().Msg("reading Merlin custom client list") + m.parseMerlinCustomClientList(out) + return nil +} + +func (m *merlinDiscover) LookupHostnameByIP(ip string) string { + return "" +} + +func (m *merlinDiscover) LookupHostnameByMac(mac string) string { + val, ok := m.hostname.Load(mac) + if !ok { + return "" + } + return val.(string) +} + +// "nvram get custom_clientlist" output: +// +// 00:00:00:00:00:01>0>4>>00:00:00:00:00:02>0>24>>... +// +// So to parse it, do the following steps: +// +// - Split by "<" => entries +// - For each entry, split by ">" => parts +// - Empty parts => skip +// - Empty parts[0] => skip empty hostname +// - Empty parts[1] => skip empty MAC +func (m *merlinDiscover) parseMerlinCustomClientList(data string) { + entries := strings.Split(data, "<") + for _, entry := range entries { + parts := strings.SplitN(string(entry), ">", 3) + if len(parts) < 2 || len(parts[0]) == 0 || len(parts[1]) == 0 { + continue + } + hostname := normalizeHostname(parts[0]) + mac := strings.ToLower(parts[1]) + m.hostname.Store(mac, hostname) + } +} diff --git a/internal/clientinfo/merlin_test.go b/internal/clientinfo/merlin_test.go new file mode 100644 index 0000000..0437035 --- /dev/null +++ b/internal/clientinfo/merlin_test.go @@ -0,0 +1,82 @@ +package clientinfo + +import ( + "testing" +) + +func TestParseMerlinCustomClientList(t *testing.T) { + tests := []struct { + name string + clientList string + macList []string + hostnameList []string + macNotPresentList []string + }{ + { + "normal", + "00:00:00:00:00:01>0>4>>", + []string{"00:00:00:00:00:01"}, + []string{"client1"}, + nil, + }, + { + "multiple clients", + "00:00:00:00:00:01>0>4>>00:00:00:00:00:02>0>24>>", + []string{"00:00:00:00:00:01", "00:00:00:00:00:02"}, + []string{"client1", "client2"}, + nil, + }, + { + "empty hostname", + "00:00:00:00:00:01>0>4>><>00:00:00:00:00:02>0>24>>", + []string{"00:00:00:00:00:01"}, + []string{"client1"}, + []string{"00:00:00:00:00:02"}, + }, + { + "empty dhcp", + "00:00:00:00:00:01>0>4>>>>", + []string{"00:00:00:00:00:01"}, + []string{"client1"}, + []string{""}, + }, + { + "invalid", + "qwerty", + nil, + nil, + nil, + }, + { + "empty", + "", + + nil, + nil, + nil, + }, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + m := &merlinDiscover{} + m.parseMerlinCustomClientList(tc.clientList) + for i, mac := range tc.macList { + val, ok := m.hostname.Load(mac) + if !ok { + t.Errorf("missing hostname: %s", mac) + } + hostname := val.(string) + if hostname != tc.hostnameList[i] { + t.Errorf("hostname mismatch, want: %q, got: %q", tc.hostnameList[i], hostname) + } + } + for _, mac := range tc.macNotPresentList { + if _, ok := m.hostname.Load(mac); ok { + t.Errorf("mac2name address %q should not be present", mac) + } + } + }) + } +} diff --git a/internal/clientinfo/ptr_lookup.go b/internal/clientinfo/ptr_lookup.go new file mode 100644 index 0000000..600b67c --- /dev/null +++ b/internal/clientinfo/ptr_lookup.go @@ -0,0 +1,62 @@ +package clientinfo + +import ( + "context" + "sync" + "time" + + "github.com/miekg/dns" + + "github.com/Control-D-Inc/ctrld" +) + +type ptrDiscover struct { + hostname sync.Map // ip => hostname + resolver ctrld.Resolver +} + +func (p *ptrDiscover) refresh() error { + p.hostname.Range(func(key, value any) bool { + ip := key.(string) + if name := p.lookupHostname(ip); name != "" { + p.hostname.Store(ip, name) + } + return true + }) + return nil +} + +func (p *ptrDiscover) LookupHostnameByIP(ip string) string { + if val, ok := p.hostname.Load(ip); ok { + return val.(string) + } + return p.lookupHostname(ip) +} +func (p *ptrDiscover) LookupHostnameByMac(mac string) string { + return "" +} + +func (p *ptrDiscover) lookupHostname(ip string) string { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + msg := new(dns.Msg) + addr, err := dns.ReverseAddr(ip) + if err != nil { + ctrld.ProxyLog.Error().Err(err).Msg("invalid ip address") + return "" + } + msg.SetQuestion(addr, dns.TypePTR) + ans, err := p.resolver.Resolve(ctx, msg) + if err != nil { + ctrld.ProxyLog.Error().Err(err).Msg("could not lookup IP") + return "" + } + for _, rr := range ans.Answer { + if ptr, ok := rr.(*dns.PTR); ok { + hostname := normalizeHostname(ptr.Ptr) + p.hostname.Store(ip, hostname) + return hostname + } + } + return "" +} diff --git a/resolver.go b/resolver.go index 2512434..2bee2d8 100644 --- a/resolver.go +++ b/resolver.go @@ -110,18 +110,6 @@ func (o *osResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error return nil, errors.Join(errs...) } -func newDialer(dnsAddress string) *net.Dialer { - return &net.Dialer{ - Resolver: &net.Resolver{ - PreferGo: true, - Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{} - return d.DialContext(ctx, network, dnsAddress) - }, - }, - } -} - type legacyResolver struct { uc *UpstreamConfig } @@ -149,6 +137,14 @@ func (r *legacyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, e return answer, err } +type dummyResolver struct{} + +func (d dummyResolver) Resolve(ctx context.Context, msg *dns.Msg) (*dns.Msg, error) { + ans := new(dns.Msg) + ans.SetReply(msg) + return ans, nil +} + // LookupIP looks up host using OS resolver. // It returns a slice of that host's IPv4 and IPv6 addresses. func LookupIP(domain string) []string { @@ -160,7 +156,7 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) if withBootstrapDNS { resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...) } - ProxyLog.Debug().Msgf("Resolving %q using bootstrap DNS %q", domain, resolver.nameservers) + ProxyLog.Debug().Msgf("resolving %q using bootstrap DNS %q", domain, resolver.nameservers) timeoutMs := 2000 if timeout > 0 && timeout < timeoutMs { timeoutMs = timeout @@ -230,7 +226,6 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) // NewBootstrapResolver returns an OS resolver, which use following nameservers: // -// - ControlD bootstrap DNS server. // - Gateway IP address (depends on OS). // - Input servers. func NewBootstrapResolver(servers ...string) Resolver { @@ -241,3 +236,36 @@ func NewBootstrapResolver(servers ...string) Resolver { } return resolver } + +// NewPrivateResolver returns an OS resolver, which includes only private DNS servers. +// This is useful for doing PTR lookup in LAN network. +func NewPrivateResolver() Resolver { + nss := nameservers() + n := 0 + for _, ns := range nss { + host, _, _ := net.SplitHostPort(ns) + ip := net.ParseIP(host) + if ip != nil && ip.IsPrivate() && !ip.IsLoopback() { + nss[n] = ns + n++ + } + } + nss = nss[:n] + if len(nss) == 0 { + return &dummyResolver{} + } + resolver := &osResolver{nameservers: nss} + return resolver +} + +func newDialer(dnsAddress string) *net.Dialer { + return &net.Dialer{ + Resolver: &net.Resolver{ + PreferGo: true, + Dial: func(ctx context.Context, network, address string) (net.Conn, error) { + d := net.Dialer{} + return d.DialContext(ctx, network, dnsAddress) + }, + }, + } +} From 0a7d3445f4498f78c7ffcc5dad156d8f01c4fb71 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 14 Jul 2023 17:07:03 +0000 Subject: [PATCH 41/84] all: use 127.0.0.1 as nameserver when ctrld is an upstream --- cmd/ctrld/cli.go | 34 ++++++---------- cmd/ctrld/os_linux.go | 2 +- cmd/ctrld/prog.go | 42 +++++-------------- cmd/ctrld/prog_linux.go | 8 +--- internal/router/router.go | 85 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 59 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 6ed93e7..91addb6 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -35,15 +35,9 @@ import ( "tailscale.com/net/interfaces" "github.com/Control-D-Inc/ctrld" - "github.com/Control-D-Inc/ctrld/internal/certs" "github.com/Control-D-Inc/ctrld/internal/controld" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" - "github.com/Control-D-Inc/ctrld/internal/router/firewalla" - "github.com/Control-D-Inc/ctrld/internal/router/merlin" - "github.com/Control-D-Inc/ctrld/internal/router/tomato" - "github.com/Control-D-Inc/ctrld/internal/router/ubios" ) var ( @@ -278,8 +272,8 @@ func initCLI() { } }) if platform := router.Name(); platform != "" { - if platform == ddwrt.Name { - rootCertPool = certs.CACertPool() + if cp := router.CertPool(); cp != nil { + rootCertPool = cp } // Perform router setup/cleanup if ctrld could not be direct listener. if !couldBeDirectListener(cfg.FirstListener()) { @@ -979,6 +973,9 @@ func netInterface(ifaceName string) (*net.Interface, error) { } func defaultIfaceName() string { + if ifaceName := router.DefaultInterfaceName(); ifaceName != "" { + return ifaceName + } dri, err := interfaces.DefaultRouteInterface() if err != nil { // On WSL 1, the route table does not have any default route. But the fact that @@ -986,10 +983,6 @@ func defaultIfaceName() string { if oi := osinfo.New(); strings.Contains(oi.String(), "Microsoft") { return "lo" } - // Same as WSL case above. - if router.Name() == ubios.Name { - return "lo" - } mainLog.Fatal().Err(err).Msg("failed to get default route interface") } return dri @@ -1057,19 +1050,18 @@ func selfCheckStatus(status service.Status, domain string) service.Status { } func userHomeDir() (string, error) { - switch router.Name() { - case ddwrt.Name, merlin.Name, tomato.Name: - exe, err := os.Executable() - if err != nil { - return "", err - } - return filepath.Dir(exe), nil + dir, err := router.HomeDir() + if err != nil { + return "", err + } + if dir != "" { + return dir, nil } // viper will expand for us. if runtime.GOOS == "windows" { return os.UserHomeDir() } - dir := "/etc/controld" + dir = "/etc/controld" if err := os.MkdirAll(dir, 0750); err != nil { return "", err } @@ -1291,7 +1283,7 @@ func updateListenerConfig() { // On firewalla, we don't need to check localhost, because the lo interface is excluded in dnsmasq // config, so we can always listen on localhost port 53, but no traffic could be routed there. - tryLocalhost := !isLoopback(listener.IP) && router.Name() != firewalla.Name + tryLocalhost := !isLoopback(listener.IP) && router.CanListenLocalhost() tryAllPort53 := true tryOldIPPort5354 := true tryPort5354 := true diff --git a/cmd/ctrld/os_linux.go b/cmd/ctrld/os_linux.go index 55f6e7c..94ab8d1 100644 --- a/cmd/ctrld/os_linux.go +++ b/cmd/ctrld/os_linux.go @@ -111,7 +111,7 @@ func resetDNS(iface *net.Interface) (err error) { } // Start systemd-networkd if present. if exe, _ := exec.LookPath("/lib/systemd/systemd-networkd"); exe != "" { - _ = exec.Command("systemctl", "restart", "systemd-networkd").Run() + _ = exec.Command("systemctl", "start", "systemd-networkd").Run() } if r, oerr := dns.NewOSConfigurator(logf, iface.Name); oerr == nil { _ = r.SetDNS(dns.OSConfig{}) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 9aa7db4..cf3b847 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -16,9 +16,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/clientinfo" "github.com/Control-D-Inc/ctrld/internal/dnscache" "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" - "github.com/Control-D-Inc/ctrld/internal/router/edgeos" - "github.com/Control-D-Inc/ctrld/internal/router/firewalla" ) const ( @@ -249,40 +246,23 @@ func (p *prog) setDNS() { logger.Error().Err(err).Msg("could not patch NetworkManager") return } + logger.Debug().Msg("setting DNS for interface") ns := lc.IP - if couldBeDirectListener(lc) { + switch { + case couldBeDirectListener(lc): // If ctrld is direct listener, use 127.0.0.1 as nameserver. ns = "127.0.0.1" - } else if lc.Port != 53 { - ifaceName := iface - switch router.Name() { - case firewalla.Name: - // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. - // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. - ifaceName = "br0" - logger.Warn().Msg("using br0 interface IP address as DNS server") - case edgeos.Name: - // On EdgeOS, dnsmasq is run with "--local-service", so we need to get - // the proper interface from dnsmasq config. - if name, _ := dnsmasq.InterfaceNameFromConfig("/etc/dnsmasq.conf"); name != "" { - ifaceName = name - logger.Warn().Msgf("using %s interface IP address as DNS server", ifaceName) - } - } - logger.Warn().Msgf("ctrld is not running on port 53, use interface %s IP as DNS server", ifaceName) - netIface, err := net.InterfaceByName(ifaceName) - if err != nil { - mainLog.Fatal().Err(err).Msg("failed to get default route interface") - } - addrs, _ := netIface.Addrs() - for _, addr := range addrs { - if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { - ns = netIP.IP.To4().String() - break - } + case lc.Port != 53: + ns = "127.0.0.1" + if resolver := router.LocalResolverIP(); resolver != "" { + ns = resolver } + default: + // If we ever reach here, it means ctrld is running on lc.IP port 53, + // so we could just use lc.IP as nameserver. } + if err := setDNS(netIface, []string{ns}); err != nil { logger.Error().Err(err).Msgf("could not set DNS for interface") return diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index a3f2823..58332bb 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -5,7 +5,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/dns" "github.com/Control-D-Inc/ctrld/internal/router" - "github.com/Control-D-Inc/ctrld/internal/router/edgeos" ) func init() { @@ -29,11 +28,8 @@ func setDependencies(svc *service.Config) { "Wants=systemd-networkd-wait-online.service", "After=systemd-networkd-wait-online.service", } - // On EdeOS, ctrld needs to start after vyatta-dhcpd, so it can read leases file. - if router.Name() == edgeos.Name { - svc.Dependencies = append(svc.Dependencies, "Wants=vyatta-dhcpd.service") - svc.Dependencies = append(svc.Dependencies, "After=vyatta-dhcpd.service") - svc.Dependencies = append(svc.Dependencies, "Wants=dnsmasq.service") + if routerDeps := router.ServiceDependencies(); len(routerDeps) > 0 { + svc.Dependencies = append(svc.Dependencies, routerDeps...) } } diff --git a/internal/router/router.go b/internal/router/router.go index 839151e..b500de6 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -2,14 +2,19 @@ package router import ( "bytes" + "crypto/x509" + "net" "os" "os/exec" + "path/filepath" "sync/atomic" "github.com/kardianos/service" "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/certs" "github.com/Control-D-Inc/ctrld/internal/router/ddwrt" + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" "github.com/Control-D-Inc/ctrld/internal/router/edgeos" "github.com/Control-D-Inc/ctrld/internal/router/firewalla" "github.com/Control-D-Inc/ctrld/internal/router/merlin" @@ -99,6 +104,86 @@ func Name() string { return r.name } +// DefaultInterfaceName returns the default interface name of the current router. +func DefaultInterfaceName() string { + switch Name() { + case ubios.Name: + return "lo" + } + return "" +} + +// LocalResolverIP returns the IP that could be used as nameserver in /etc/resolv.conf file. +func LocalResolverIP() string { + var iface string + switch Name() { + case edgeos.Name: + // On EdgeOS, dnsmasq is run with "--local-service", so we need to get + // the proper interface from dnsmasq config. + if name, _ := dnsmasq.InterfaceNameFromConfig("/etc/dnsmasq.conf"); name != "" { + iface = name + } + case firewalla.Name: + // On Firewalla, the lo interface is excluded in all dnsmasq settings of all interfaces. + // Thus, we use "br0" as the nameserver in /etc/resolv.conf file. + iface = "br0" + } + if netIface, _ := net.InterfaceByName(iface); netIface != nil { + addrs, _ := netIface.Addrs() + for _, addr := range addrs { + if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { + return netIP.IP.To4().String() + } + } + } + return "" +} + +// HomeDir returns the home directory of ctrld on current router. +func HomeDir() (string, error) { + switch Name() { + case ddwrt.Name, merlin.Name, tomato.Name: + exe, err := os.Executable() + if err != nil { + return "", err + } + return filepath.Dir(exe), nil + } + return "", nil +} + +// CertPool returns the system certificate pool of the current router. +func CertPool() *x509.CertPool { + if Name() == ddwrt.Name { + return certs.CACertPool() + } + return nil +} + +// CanListenLocalhost reports whether the ctrld can listen on localhost with current host. +func CanListenLocalhost() bool { + switch { + case Name() == firewalla.Name: + return false + default: + return true + } +} + +// ServiceDependencies returns list of dependencies that ctrld services needs on this router. +// See https://pkg.go.dev/github.com/kardianos/service#Config for list format. +func ServiceDependencies() []string { + if Name() == edgeos.Name { + // On EdeOS, ctrld needs to start after vyatta-dhcpd, so it can read leases file. + return []string{ + "Wants=vyatta-dhcpd.service", + "After=vyatta-dhcpd.service", + "Wants=dnsmasq.service", + } + } + return nil +} + func distroName() string { switch { case bytes.HasPrefix(unameO(), []byte("DD-WRT")): From 7b476e38be978ffe74e1e701f3834b64212144fe Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 17 Jul 2023 16:22:46 +0000 Subject: [PATCH 42/84] cmd/ctrld: do not spawn extra listener if conflicted in cd mode --- cmd/ctrld/prog.go | 38 ++------------------------------------ 1 file changed, 2 insertions(+), 36 deletions(-) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index cf3b847..cd42e77 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -1,14 +1,12 @@ package main import ( - "errors" "fmt" "math/rand" "net" "os" "strconv" "sync" - "syscall" "github.com/kardianos/service" @@ -28,8 +26,6 @@ var logf = func(format string, args ...any) { mainLog.Debug().Msgf(format, args...) } -var errWindowsAddrInUse = syscall.Errno(0x2740) - var svcConfig = &service.Config{ Name: "ctrld", DisplayName: "Control-D Helper Service", @@ -132,39 +128,9 @@ 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.serveDNS(listenerNum) - if err != nil && !defaultConfigWritten && cdUID == "" { - mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) - return + if err := p.serveDNS(listenerNum); err != nil { + mainLog.Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum) } - if err == nil { - return - } - - 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) - 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) - } - p.mu.Lock() - p.cfg.Service.AllocateIP = true - p.mu.Unlock() - p.preRun() - mainLog.Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, net.JoinHostPort(ip, strconv.Itoa(port))) - if err := p.serveDNS(listenerNum); err != nil { - mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) - return - } - } - } - mainLog.Fatal().Err(err).Msgf("Unable to start dns proxy on listener.%s", listenerNum) }(listenerNum) } From be0769e433287f450fe650fca879733f40b3c1a4 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 17 Jul 2023 16:29:18 +0000 Subject: [PATCH 43/84] cmd/ctrld: do not create config dir if not necessary --- cmd/ctrld/cli.go | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 91addb6..f043605 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -1063,12 +1063,25 @@ func userHomeDir() (string, error) { } dir = "/etc/controld" if err := os.MkdirAll(dir, 0750); err != nil { - return "", err + return os.UserHomeDir() // fallback to user home directory + } + if ok, _ := dirWritable(dir); !ok { + return os.UserHomeDir() } return dir, nil } func tryReadingConfig(writeDefaultConfig bool) { + // --config is specified. + if configPath != "" { + v.SetConfigFile(configPath) + readConfigFile(false) + return + } + // no config start or base64 config mode. + if !writeDefaultConfig { + return + } configs := []struct { name string written bool @@ -1080,7 +1093,7 @@ func tryReadingConfig(writeDefaultConfig bool) { dir, err := userHomeDir() if err != nil { - mainLog.Fatal().Msgf("failed to get config dir: %v", err) + mainLog.Fatal().Msgf("failed to get user home dir: %v", err) } for _, config := range configs { ctrld.SetConfigNameWithPath(v, config.name, dir) @@ -1397,3 +1410,12 @@ func updateListenerConfig() { } } } + +func dirWritable(dir string) (bool, error) { + f, err := os.CreateTemp(dir, "") + if err != nil { + return false, err + } + defer os.Remove(f.Name()) + return true, f.Close() +} From 4bf09120ffadb67d5a0b1be224ef063f8babd0a1 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 17 Jul 2023 18:00:51 +0000 Subject: [PATCH 44/84] cmd/ctrld: spawn RFC1918 listeners if listen on 127.0.0.1:53 --- cmd/ctrld/dns_proxy.go | 44 +++++++++++++++++++++++++++++++++--------- cmd/ctrld/prog.go | 2 +- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index b263aa7..0df6e61 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -7,6 +7,7 @@ import ( "fmt" "io" "net" + "net/netip" "os" "runtime" "strconv" @@ -17,6 +18,7 @@ import ( "github.com/miekg/dns" "go4.org/mem" "golang.org/x/sync/errgroup" + "tailscale.com/net/interfaces" "tailscale.com/util/lineread" "github.com/Control-D-Inc/ctrld" @@ -78,6 +80,7 @@ func (p *prog) serveDNS(listenerNum string) error { } }) + needRFC1918Listeners := listenerConfig.IP == "127.0.0.1" && listenerConfig.Port == 53 g, ctx := errgroup.WithContext(context.Background()) for _, proto := range []string{"udp", "tcp"} { proto := proto @@ -86,6 +89,7 @@ func (p *prog) serveDNS(listenerNum string) error { s, errCh := runDNSServer(net.JoinHostPort("::1", strconv.Itoa(listenerConfig.Port)), proto, handler) defer s.Shutdown() select { + case <-p.stopCh: case <-ctx.Done(): case err := <-errCh: // Local ipv6 listener should not terminate ctrld. @@ -95,17 +99,38 @@ func (p *prog) serveDNS(listenerNum string) error { return nil }) } + // When we spawn a listener on 127.0.0.1, also spawn listeners on the RFC1918 + // addresses of the machine. So ctrld could receive queries from LAN clients. + if needRFC1918Listeners { + g.Go(func() error { + interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + addrs, _ := i.Addrs() + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok || !ipNet.IP.IsPrivate() { + continue + } + func() { + listenAddr := net.JoinHostPort(ipNet.IP.String(), "53") + s, errCh := runDNSServer(listenAddr, proto, handler) + defer s.Shutdown() + select { + case <-p.stopCh: + case <-ctx.Done(): + case err := <-errCh: + // RFC1918 listener should not terminate ctrld. + // It's a workaround for a quirk on system with systemd-resolved. + mainLog.Warn().Err(err).Msgf("could not listen on %s: %s", proto, listenAddr) + } + }() + } + }) + return nil + }) + } g.Go(func() error { s, errCh := runDNSServer(dnsListenAddress(listenerConfig), proto, handler) defer s.Shutdown() - if listenerConfig.Port == 0 { - switch s.Net { - case "udp": - mainLog.Info().Msgf("Random port chosen for udp listener.%s: %s", listenerNum, s.PacketConn.LocalAddr()) - case "tcp": - mainLog.Info().Msgf("Random port chosen for tcp listener.%s: %s", listenerNum, s.Listener.Addr()) - } - } select { case err := <-errCh: return err @@ -113,11 +138,12 @@ func (p *prog) serveDNS(listenerNum string) error { p.started <- struct{}{} } select { + case <-p.stopCh: case <-ctx.Done(): - return nil case err := <-errCh: return err } + return nil }) } return g.Wait() diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index cd42e77..f75ae16 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -156,12 +156,12 @@ func (p *prog) run() { } func (p *prog) Stop(s service.Service) error { + mainLog.Info().Msg("Service stopped") close(p.stopCh) if err := p.deAllocateIP(); err != nil { mainLog.Error().Err(err).Msg("de-allocate ip failed") return err } - mainLog.Info().Msg("Service stopped") return nil } From 66cb7cc21dc3ffc285281af0ff6f71fcc7acca3c Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 17 Jul 2023 18:25:40 +0000 Subject: [PATCH 45/84] cmd/ctrld: general UX improvement --- cmd/ctrld/cli.go | 46 ++++++++++++++++++++++++++++++++++++++++++ cmd/ctrld/dns_proxy.go | 6 +++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index f043605..6370488 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -704,6 +704,52 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, stopCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`) stopCmdAlias.Flags().AddFlagSet(stopCmd.Flags()) rootCmd.AddCommand(stopCmdAlias) + + restartCmdAlias := &cobra.Command{ + PreRun: func(cmd *cobra.Command, args []string) { + initConsoleLogging() + checkHasElevatedPrivilege() + }, + Use: "restart", + Short: "Restart the ctrld service", + Run: func(cmd *cobra.Command, args []string) { + restartCmd.Run(cmd, args) + }, + } + rootCmd.AddCommand(restartCmdAlias) + + statusCmdAlias := &cobra.Command{ + Use: "status", + Short: "Show status of the ctrld service", + Args: cobra.NoArgs, + PreRun: func(cmd *cobra.Command, args []string) { + initConsoleLogging() + }, + Run: statusCmd.Run, + } + rootCmd.AddCommand(statusCmdAlias) + + uninstallCmdAlias := &cobra.Command{ + PreRun: func(cmd *cobra.Command, args []string) { + initConsoleLogging() + checkHasElevatedPrivilege() + }, + Use: "uninstall", + Short: "Stop and uninstall the ctrld service", + Long: `Stop and uninstall the ctrld service. + +NOTE: Uninstalling will set DNS to values provided by DHCP.`, + Run: func(cmd *cobra.Command, args []string) { + if !cmd.Flags().Changed("iface") { + os.Args = append(os.Args, "--iface="+ifaceStartStop) + } + iface = ifaceStartStop + uninstallCmd.Run(cmd, args) + }, + } + uninstallCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`) + uninstallCmdAlias.Flags().AddFlagSet(stopCmd.Flags()) + rootCmd.AddCommand(uninstallCmdAlias) } func writeConfigFile() error { diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 0df6e61..abacd85 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -168,7 +168,11 @@ func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *c ctrld.Log(ctx, mainLog.Info(), "query refused, %s does not match any network policy", addr.String()) return } - ctrld.Log(ctx, mainLog.Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams) + if matched { + ctrld.Log(ctx, mainLog.Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams) + } else { + ctrld.Log(ctx, mainLog.Info(), "no explicit policy matched, using default routing -> %v", upstreams) + } }() if lc.Policy == nil { From 9ed8e49a08efc9a0ed4d5733ee02a3633d42e3e9 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 17 Jul 2023 18:57:30 +0000 Subject: [PATCH 46/84] all: make router setup/cleanup works more generally --- cmd/ctrld/cli.go | 59 ++++++++++++++------------ cmd/ctrld/os_linux.go | 2 +- cmd/ctrld/prog.go | 12 +++++- config.go | 18 ++++++++ internal/router/ddwrt/ddwrt.go | 6 +++ internal/router/edgeos/edgeos.go | 6 +++ internal/router/firewalla/firewalla.go | 6 +++ internal/router/merlin/merlin.go | 6 +++ internal/router/openwrt/openwrt.go | 6 +++ internal/router/pfsense/pfsense.go | 10 +++-- internal/router/synology/synology.go | 6 +++ internal/router/tomato/tomato.go | 6 +++ internal/router/ubios/ubios.go | 6 +++ 13 files changed, 115 insertions(+), 34 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 6370488..2384de8 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -275,22 +275,19 @@ func initCLI() { if cp := router.CertPool(); cp != nil { rootCertPool = cp } - // Perform router setup/cleanup if ctrld could not be direct listener. - if !couldBeDirectListener(cfg.FirstListener()) { - p.onStarted = append(p.onStarted, func() { - mainLog.Debug().Msg("router setup") - if err := p.router.Setup(); err != nil { - mainLog.Error().Err(err).Msg("could not configure router") - } - }) - p.onStopped = append(p.onStopped, func() { - mainLog.Debug().Msg("router cleanup") - if err := p.router.Cleanup(); err != nil { - mainLog.Error().Err(err).Msg("could not cleanup router") - } - p.resetDNS() - }) - } + p.onStarted = append(p.onStarted, func() { + mainLog.Debug().Msg("router setup") + if err := p.router.Setup(); err != nil { + mainLog.Error().Err(err).Msg("could not configure router") + } + }) + p.onStopped = append(p.onStopped, func() { + mainLog.Debug().Msg("router cleanup") + if err := p.router.Cleanup(); err != nil { + mainLog.Error().Err(err).Msg("could not cleanup router") + } + p.resetDNS() + }) } close(waitCh) @@ -404,7 +401,7 @@ func initCLI() { return } - if router.Name() != "" && !couldBeDirectListener(cfg.FirstListener()) { + if router.Name() != "" { mainLog.Debug().Msg("cleaning up router before installing") _ = p.router.Cleanup() } @@ -504,15 +501,18 @@ func initCLI() { Short: "Stop the ctrld service", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - prog := &prog{} - s, err := newService(prog, svcConfig) + tryReadingConfig(false) + v.Unmarshal(&cfg) + p := &prog{router: router.New(&cfg)} + s, err := newService(p, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) return } initLogging() if doTasks([]task{{s.Stop, true}}) { - prog.resetDNS() + p.router.Cleanup() + p.resetDNS() mainLog.Notice().Msg("Service stopped") } }, @@ -591,7 +591,9 @@ func initCLI() { NOTE: Uninstalling will set DNS to values provided by DHCP.`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - p := &prog{} + tryReadingConfig(false) + v.Unmarshal(&cfg) + p := &prog{router: router.New(&cfg)} s, err := newService(p, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) @@ -1157,19 +1159,20 @@ func uninstall(p *prog, s service.Service) { } initLogging() if doTasks(tasks) { - r := router.New(&cfg) - if err := r.Uninstall(svcConfig); err != nil { + if err := p.router.ConfigureService(svcConfig); err != nil { + mainLog.Fatal().Err(err).Msg("could not configure service") + } + if err := p.router.Uninstall(svcConfig); err != nil { mainLog.Warn().Err(err).Msg("post uninstallation failed, please check system/service log for details error") return } - // Stop already reset DNS on router. - if router.Name() == "" { - p.resetDNS() + p.resetDNS() + if router.Name() != "" { + mainLog.Debug().Msg("Router cleanup") } - mainLog.Debug().Msg("Router cleanup") // Stop already did router.Cleanup and report any error if happens, // ignoring error here to prevent false positive. - _ = r.Cleanup() + _ = p.router.Cleanup() mainLog.Notice().Msg("Service uninstalled") return } diff --git a/cmd/ctrld/os_linux.go b/cmd/ctrld/os_linux.go index 94ab8d1..570cabc 100644 --- a/cmd/ctrld/os_linux.go +++ b/cmd/ctrld/os_linux.go @@ -147,7 +147,7 @@ func resetDNS(iface *net.Interface) (err error) { if ctrldnet.IPv6Available(ctx) { c := client6.NewClient() conversation, err := c.Exchange(iface.Name) - if err != nil { + if err != nil && !errAddrInUse(err) { mainLog.Debug().Err(err).Msg("could not exchange DHCPv6") } for _, packet := range conversation { diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index f75ae16..d22b7e2 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -1,12 +1,14 @@ package main import ( + "errors" "fmt" "math/rand" "net" "os" "strconv" "sync" + "syscall" "github.com/kardianos/service" @@ -216,7 +218,7 @@ func (p *prog) setDNS() { logger.Debug().Msg("setting DNS for interface") ns := lc.IP switch { - case couldBeDirectListener(lc): + case lc.IsDirectDnsListener(): // If ctrld is direct listener, use 127.0.0.1 as nameserver. ns = "127.0.0.1" case lc.Port != 53: @@ -294,3 +296,11 @@ func runLogServer(sockPath string) net.Conn { } return server } + +func errAddrInUse(err error) bool { + opErr, ok := err.(*net.OpError) + if !ok { + return false + } + return errors.Is(opErr.Err, syscall.EADDRINUSE) +} diff --git a/config.go b/config.go index 9e8f518..ff803c5 100644 --- a/config.go +++ b/config.go @@ -207,6 +207,24 @@ type ListenerConfig struct { Policy *ListenerPolicyConfig `mapstructure:"policy" toml:"policy,omitempty"` } +// IsDirectDnsListener reports whether ctrld can be a direct listener on port 53. +// It returns true only if ctrld can listen on port 53 for all interfaces. That means +// there's no other software listening on port 53. +// +// If someone listening on port 53, or ctrld could only listen on port 53 for a specific +// interface, ctrld could only be configured as a DNS forwarder. +func (lc *ListenerConfig) IsDirectDnsListener() bool { + if lc == nil || lc.Port != 53 { + return false + } + switch lc.IP { + case "", "::", "0.0.0.0": + return true + default: + return false + } +} + // ListenerPolicyConfig specifies the policy rules for ctrld to filter incoming requests. type ListenerPolicyConfig struct { Name string `mapstructure:"name" toml:"name,omitempty"` diff --git a/internal/router/ddwrt/ddwrt.go b/internal/router/ddwrt/ddwrt.go index f2cffdc..dc220ce 100644 --- a/internal/router/ddwrt/ddwrt.go +++ b/internal/router/ddwrt/ddwrt.go @@ -58,6 +58,9 @@ func (d *Ddwrt) PreRun() error { } func (d *Ddwrt) Setup() error { + if d.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Already setup. if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { return nil @@ -81,6 +84,9 @@ func (d *Ddwrt) Setup() error { } func (d *Ddwrt) Cleanup() error { + if d.cfg.FirstListener().IsDirectDnsListener() { + return nil + } if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { nvramKvMap["dnsmasq_options"] = "" // Restore old configs. diff --git a/internal/router/edgeos/edgeos.go b/internal/router/edgeos/edgeos.go index f84e3b1..014a594 100644 --- a/internal/router/edgeos/edgeos.go +++ b/internal/router/edgeos/edgeos.go @@ -62,6 +62,9 @@ func (e *EdgeOS) PreRun() error { } func (e *EdgeOS) Setup() error { + if e.cfg.FirstListener().IsDirectDnsListener() { + return nil + } if e.isUSG { return e.setupUSG() } @@ -69,6 +72,9 @@ func (e *EdgeOS) Setup() error { } func (e *EdgeOS) Cleanup() error { + if e.cfg.FirstListener().IsDirectDnsListener() { + return nil + } if e.isUSG { return e.cleanupUSG() } diff --git a/internal/router/firewalla/firewalla.go b/internal/router/firewalla/firewalla.go index 4e177ed..cdf6586 100644 --- a/internal/router/firewalla/firewalla.go +++ b/internal/router/firewalla/firewalla.go @@ -54,6 +54,9 @@ func (f *Firewalla) PreRun() error { } func (f *Firewalla) Setup() error { + if f.cfg.FirstListener().IsDirectDnsListener() { + return nil + } data, err := dnsmasq.FirewallaConfTmpl(dnsmasq.ConfigContentTmpl, f.cfg) if err != nil { return fmt.Errorf("generating dnsmasq config: %w", err) @@ -71,6 +74,9 @@ func (f *Firewalla) Setup() error { } func (f *Firewalla) Cleanup() error { + if f.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Removing current config. if err := os.Remove(firewallaDNSMasqConfigPath); err != nil { return fmt.Errorf("removing ctrld config: %w", err) diff --git a/internal/router/merlin/merlin.go b/internal/router/merlin/merlin.go index 18b07c5..19d14b3 100644 --- a/internal/router/merlin/merlin.go +++ b/internal/router/merlin/merlin.go @@ -49,6 +49,9 @@ func (m *Merlin) PreRun() error { } func (m *Merlin) Setup() error { + if m.cfg.FirstListener().IsDirectDnsListener() { + return nil + } buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath) // Already setup. if bytes.Contains(buf, []byte(dnsmasq.MerlinPostConfMarker)) { @@ -86,6 +89,9 @@ func (m *Merlin) Setup() error { } func (m *Merlin) Cleanup() error { + if m.cfg.FirstListener().IsDirectDnsListener() { + return nil + } if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { // Restore old configs. if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { diff --git a/internal/router/openwrt/openwrt.go b/internal/router/openwrt/openwrt.go index 1d8de34..83ea884 100644 --- a/internal/router/openwrt/openwrt.go +++ b/internal/router/openwrt/openwrt.go @@ -49,6 +49,9 @@ func (o *Openwrt) PreRun() error { } func (o *Openwrt) Setup() error { + if o.cfg.FirstListener().IsDirectDnsListener() { + return nil + } data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, o.cfg) if err != nil { return err @@ -68,6 +71,9 @@ func (o *Openwrt) Setup() error { } func (o *Openwrt) Cleanup() error { + if o.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Remove the custom dnsmasq config if err := os.Remove(openwrtDNSMasqConfigPath); err != nil { return err diff --git a/internal/router/pfsense/pfsense.go b/internal/router/pfsense/pfsense.go index ec2b890..1806ec7 100644 --- a/internal/router/pfsense/pfsense.go +++ b/internal/router/pfsense/pfsense.go @@ -66,6 +66,9 @@ func (p *Pfsense) Install(config *service.Config) error { } func (p *Pfsense) Uninstall(config *service.Config) error { + if err := os.Remove(filepath.Join(rcPath, p.svcName+".sh")); err != nil { + return fmt.Errorf("os.Remove: %w", err) + } return nil } @@ -84,11 +87,10 @@ func (p *Pfsense) Setup() error { } func (p *Pfsense) Cleanup() error { - if err := os.Remove(filepath.Join(rcPath, p.svcName+".sh")); err != nil { - return fmt.Errorf("os.Remove: %w", err) + if p.cfg.FirstListener().IsDirectDnsListener() { + _ = exec.Command(unboundRcPath, "onerestart").Run() + _ = exec.Command(dnsmasqRcPath, "onerestart").Run() } - _ = exec.Command(unboundRcPath, "onerestart").Run() - _ = exec.Command(dnsmasqRcPath, "onerestart").Run() return nil } diff --git a/internal/router/synology/synology.go b/internal/router/synology/synology.go index 78551e4..3ad0388 100644 --- a/internal/router/synology/synology.go +++ b/internal/router/synology/synology.go @@ -44,6 +44,9 @@ func (s *Synology) PreRun() error { } func (s *Synology) Setup() error { + if s.cfg.FirstListener().IsDirectDnsListener() { + return nil + } data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, s.cfg) if err != nil { return err @@ -61,6 +64,9 @@ func (s *Synology) Setup() error { } func (s *Synology) Cleanup() error { + if s.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Remove the custom config files. for _, f := range []string{synologyDNSMasqConfigPath, synologyDhcpdInfoPath} { if err := os.Remove(f); err != nil { diff --git a/internal/router/tomato/tomato.go b/internal/router/tomato/tomato.go index 4c1824d..40a70e5 100644 --- a/internal/router/tomato/tomato.go +++ b/internal/router/tomato/tomato.go @@ -53,6 +53,9 @@ func (f *FreshTomato) PreRun() error { } func (f *FreshTomato) Setup() error { + if f.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Already setup. if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { return nil @@ -83,6 +86,9 @@ func (f *FreshTomato) Setup() error { } func (f *FreshTomato) Cleanup() error { + if f.cfg.FirstListener().IsDirectDnsListener() { + return nil + } if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { nvramKvMap["dnsmasq_custom"] = "" // Restore old configs. diff --git a/internal/router/ubios/ubios.go b/internal/router/ubios/ubios.go index 06194d9..b0762db 100644 --- a/internal/router/ubios/ubios.go +++ b/internal/router/ubios/ubios.go @@ -47,6 +47,9 @@ func (u *Ubios) PreRun() error { } func (u *Ubios) Setup() error { + if u.cfg.FirstListener().IsDirectDnsListener() { + return nil + } data, err := dnsmasq.ConfTmpl(dnsmasq.ConfigContentTmpl, u.cfg) if err != nil { return err @@ -62,6 +65,9 @@ func (u *Ubios) Setup() error { } func (u *Ubios) Cleanup() error { + if u.cfg.FirstListener().IsDirectDnsListener() { + return nil + } // Remove the custom dnsmasq config if err := os.Remove(ubiosDNSMasqConfigPath); err != nil { return err From 2cd063ebd67306c3fb388bdb349bfde2a05bca1a Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 18 Jul 2023 18:07:46 +0700 Subject: [PATCH 47/84] cmd/ctrld: do client info table init in separated goroutine So it won't cause the listener take more times to be ready. --- cmd/ctrld/prog.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index d22b7e2..4e011b2 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -115,8 +115,11 @@ func (p *prog) run() { format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat) p.ciTable.AddLeaseFile(leaseFile, format) } - p.ciTable.Init() - go p.ciTable.RefreshLoop(p.stopCh) + + go func() { + p.ciTable.Init() + p.ciTable.RefreshLoop(p.stopCh) + }() go p.watchLinkState() for listenerNum := range p.cfg.Listener { From cacd9575948326181750fdbec68eda7ee381e3e4 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 18 Jul 2023 19:00:08 +0700 Subject: [PATCH 48/84] internal/clientinfo: do not lower case hostname --- internal/clientinfo/client_info.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 79e1acd..a026d1d 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -205,5 +205,5 @@ func normalizeHostname(name string) string { if before, _, found := strings.Cut(name, "."); found { return before // remove ".local.", ".lan.", ... suffix } - return strings.ToLower(name) + return name } From 59a895bfe2100aabb4d83cc5f4c8904e83e542bf Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 18 Jul 2023 19:12:29 +0700 Subject: [PATCH 49/84] internal/clientinfo: improving mdns discovery - Prevent duplicated log message. - Distinguish in case of create/update hostname. - Stop probing if network is unreachable or invalid. --- internal/clientinfo/client_info.go | 6 ++- internal/clientinfo/mdns.go | 82 ++++++++++++++++++++---------- 2 files changed, 59 insertions(+), 29 deletions(-) diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index a026d1d..ad17121 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -48,10 +48,11 @@ type Table struct { ptr *ptrDiscover mdns *mdns cfg *ctrld.Config + quitCh chan struct{} } func NewTable(cfg *ctrld.Config) *Table { - return &Table{cfg: cfg} + return &Table{cfg: cfg, quitCh: make(chan struct{})} } func (t *Table) AddLeaseFile(name string, format ctrld.LeaseFileFormat) { @@ -70,6 +71,7 @@ func (t *Table) RefreshLoop(stopCh chan struct{}) { _ = r.refresh() } case <-stopCh: + close(t.quitCh) return } } @@ -122,7 +124,7 @@ func (t *Table) Init() { if t.discoverMDNS() { t.mdns = &mdns{} ctrld.ProxyLog.Debug().Msg("start mdns discovery") - if err := t.mdns.init(); err != nil { + if err := t.mdns.init(t.quitCh); err != nil { ctrld.ProxyLog.Error().Err(err).Msg("could not init mDNS discover") } else { t.hostnameResolvers = append(t.hostnameResolvers, t.mdns) diff --git a/internal/clientinfo/mdns.go b/internal/clientinfo/mdns.go index ce92d50..ac34713 100644 --- a/internal/clientinfo/mdns.go +++ b/internal/clientinfo/mdns.go @@ -4,7 +4,9 @@ import ( "context" "errors" "net" + "os" "sync" + "syscall" "time" "github.com/miekg/dns" @@ -41,7 +43,7 @@ func (m *mdns) LookupHostnameByMac(mac string) string { return "" } -func (m *mdns) init() error { +func (m *mdns) init(quitCh chan struct{}) error { ifaces, err := multicastInterfaces() if err != nil { return err @@ -65,20 +67,35 @@ func (m *mdns) init() error { } } - go func() { - bo := backoff.NewBackoff("mdns probe", func(format string, args ...any) {}, time.Second*30) - for { - err := m.probe(v4ConnList, v6ConnList) - if err != nil { - ctrld.ProxyLog.Warn().Err(err).Msg("error while probing mdns") - } - bo.BackOff(context.Background(), errors.New("mdns probe backoff")) - } - }() + go m.probeLoop(v4ConnList, mdnsV4Addr, quitCh) + go m.probeLoop(v6ConnList, mdnsV6Addr, quitCh) return nil } +func (m *mdns) probeLoop(conns []*net.UDPConn, remoteAddr net.Addr, quitCh chan struct{}) { + bo := backoff.NewBackoff("mdns probe", func(format string, args ...any) {}, time.Second*30) + for { + err := m.probe(conns, remoteAddr, quitCh) + if isErrNetUnreachableOrInvalid(err) { + ctrld.ProxyLog.Warn().Msgf("stop probing %q: network unreachable or invalid", remoteAddr) + break + } + if err != nil { + ctrld.ProxyLog.Warn().Err(err).Msg("error while probing mdns") + bo.BackOff(context.Background(), errors.New("mdns probe backoff")) + } + select { + case <-quitCh: + break + } + } + <-quitCh + for _, conn := range conns { + _ = conn.Close() + } +} + func (m *mdns) readLoop(conn *net.UDPConn) { defer conn.Close() buf := make([]byte, dns.MaxMsgSize) @@ -87,12 +104,10 @@ func (m *mdns) readLoop(conn *net.UDPConn) { _ = conn.SetReadDeadline(time.Now().Add(time.Second * 30)) n, _, err := conn.ReadFromUDP(buf) if err != nil { - if err, ok := err.(*net.OpError); ok { - if err.Timeout() || err.Temporary() { - continue - } - ctrld.ProxyLog.Debug().Err(err).Msg("mdns readLoop error") + if err, ok := err.(*net.OpError); ok && (err.Timeout() || err.Temporary()) { + continue } + ctrld.ProxyLog.Debug().Err(err).Msg("mdns readLoop error") return } @@ -111,14 +126,22 @@ func (m *mdns) readLoop(conn *net.UDPConn) { } if ip != "" && name != "" { name = normalizeHostname(name) - ctrld.ProxyLog.Debug().Msgf("Found hostname: %q, ip: %q via mdns", name, ip) - m.name.Store(ip, name) + if val, loaded := m.name.LoadOrStore(ip, name); !loaded { + ctrld.ProxyLog.Debug().Msgf("found hostname: %q, ip: %q via mdns", name, ip) + } else { + old := val.(string) + if old != name { + ctrld.ProxyLog.Debug().Msgf("update hostname: %q, ip: %q, old: %q via mdns", name, ip, old) + m.name.Store(ip, name) + } + } + ip, name = "", "" } } } } -func (m *mdns) probe(v4connList, v6connList []*net.UDPConn) error { +func (m *mdns) probe(conns []*net.UDPConn, remoteAddr net.Addr, quitCh chan struct{}) error { msg := new(dns.Msg) msg.Question = make([]dns.Question, len(services)) for i, service := range services { @@ -133,16 +156,13 @@ func (m *mdns) probe(v4connList, v6connList []*net.UDPConn) error { if err != nil { return err } - do := func(connList []*net.UDPConn, remoteAddr net.Addr) error { - for _, conn := range connList { - _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 30)) - if _, err := conn.WriteTo(buf, remoteAddr); err != nil { - return err - } + for _, conn := range conns { + _ = conn.SetWriteDeadline(time.Now().Add(time.Second * 30)) + if _, werr := conn.WriteTo(buf, remoteAddr); werr != nil { + err = werr } - return nil } - return errors.Join(do(v4connList, mdnsV4Addr), do(v6connList, mdnsV6Addr)) + return err } func multicastInterfaces() ([]net.Interface, error) { @@ -161,3 +181,11 @@ func multicastInterfaces() ([]net.Interface, error) { } return interfaces, nil } + +func isErrNetUnreachableOrInvalid(err error) bool { + var se *os.SyscallError + if errors.As(err, &se) { + return se.Err == syscall.ENETUNREACH || se.Err == syscall.EINVAL + } + return false +} From d6768c4c399a64549aeb222dc26578a246858343 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 18 Jul 2023 20:52:51 +0700 Subject: [PATCH 50/84] internal/clientinfo: use default route IP as self client info --- cmd/ctrld/dns_proxy.go | 3 +++ cmd/ctrld/prog.go | 30 +++++++++++++++++++++++++++++- internal/clientinfo/client_info.go | 11 ++++++++--- internal/clientinfo/dhcp.go | 9 +++++++++ 4 files changed, 49 insertions(+), 4 deletions(-) diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index abacd85..0586151 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -549,6 +549,9 @@ func (p *prog) getClientInfo(ip, mac string) *ctrld.ClientInfo { } else { ci.IP = ip ci.Mac = p.ciTable.LookupMac(ip) + if ip == "127.0.0.1" || ip == "::1" { + ci.IP = p.ciTable.LookupIP(ci.Mac) + } } ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac) return ci diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 4e011b2..dd0b5f0 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -11,6 +11,7 @@ import ( "syscall" "github.com/kardianos/service" + "tailscale.com/net/interfaces" "github.com/Control-D-Inc/ctrld" "github.com/Control-D-Inc/ctrld/internal/clientinfo" @@ -109,7 +110,7 @@ func (p *prog) run() { go uc.Ping() } - p.ciTable = clientinfo.NewTable(&cfg) + p.ciTable = clientinfo.NewTable(&cfg, defaultRouteIP()) if leaseFile := p.cfg.Service.DHCPLeaseFile; leaseFile != "" { mainLog.Debug().Msgf("watching custom lease file: %s", leaseFile) format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat) @@ -307,3 +308,30 @@ func errAddrInUse(err error) bool { } return errors.Is(opErr.Err, syscall.EADDRINUSE) } + +// defaultRouteIP returns IP string of the default route if present, prefer IPv4 over IPv6. +func defaultRouteIP() string { + if dr, err := interfaces.DefaultRoute(); err == nil { + if netIface, err := netInterface(dr.InterfaceName); err == nil { + addrs, _ := netIface.Addrs() + do := func(v4 bool) net.IP { + for _, addr := range addrs { + if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.IsPrivate() { + if v4 { + return netIP.IP.To4() + } + return netIP.IP + } + } + return nil + } + if ip := do(true); ip != nil { + return ip.String() + } + if ip := do(false); ip != nil { + return ip.String() + } + } + } + return "" +} diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index ad17121..6d220e1 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -49,10 +49,15 @@ type Table struct { mdns *mdns cfg *ctrld.Config quitCh chan struct{} + selfIP string } -func NewTable(cfg *ctrld.Config) *Table { - return &Table{cfg: cfg, quitCh: make(chan struct{})} +func NewTable(cfg *ctrld.Config, selfIP string) *Table { + return &Table{ + cfg: cfg, + quitCh: make(chan struct{}), + selfIP: selfIP, + } } func (t *Table) AddLeaseFile(name string, format ctrld.LeaseFileFormat) { @@ -88,7 +93,7 @@ func (t *Table) Init() { } } if t.discoverDHCP() { - t.dhcp = &dhcp{} + t.dhcp = &dhcp{selfIP: t.selfIP} ctrld.ProxyLog.Debug().Msg("start dhcp discovery") if err := t.dhcp.refresh(); err != nil { ctrld.ProxyLog.Error().Err(err).Msg("could not init DHCP discover") diff --git a/internal/clientinfo/dhcp.go b/internal/clientinfo/dhcp.go index 5ca28ec..1aca295 100644 --- a/internal/clientinfo/dhcp.go +++ b/internal/clientinfo/dhcp.go @@ -25,6 +25,7 @@ type dhcp struct { mac sync.Map // ip => mac watcher *fsnotify.Watcher + selfIP string } func (d *dhcp) refresh() error { @@ -229,6 +230,7 @@ func (d *dhcp) addSelf() { hostname = normalizeHostname(hostname) d.ip2name.Store("127.0.0.1", hostname) d.ip2name.Store("::1", hostname) + found := false interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { mac := i.HardwareAddr.String() // Skip loopback interfaces, info was stored above. @@ -237,6 +239,9 @@ func (d *dhcp) addSelf() { } addrs, _ := i.Addrs() for _, addr := range addrs { + if found { + return + } ipNet, ok := addr.(*net.IPNet) if !ok { continue @@ -251,6 +256,10 @@ func (d *dhcp) addSelf() { } d.mac2name.Store(mac, hostname) d.ip2name.Store(ip.String(), hostname) + // If we have self IP set, and this IP is it, use this IP only. + if ip.String() == d.selfIP { + found = true + } } }) } From 2cd8b7e021b3deaecf7ff74f8cede0ebecabaf64 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 18 Jul 2023 21:15:09 +0700 Subject: [PATCH 51/84] internal/clientinfo: remove dhcp from refresher list dhcp lease files are watched separately using fsnotify, it does not need to be in refresher list. --- internal/clientinfo/client_info.go | 3 +-- internal/clientinfo/dhcp.go | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 6d220e1..4dd757b 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -95,13 +95,12 @@ func (t *Table) Init() { if t.discoverDHCP() { t.dhcp = &dhcp{selfIP: t.selfIP} ctrld.ProxyLog.Debug().Msg("start dhcp discovery") - if err := t.dhcp.refresh(); err != nil { + if err := t.dhcp.init(); err != nil { ctrld.ProxyLog.Error().Err(err).Msg("could not init DHCP discover") } else { t.ipResolvers = append(t.ipResolvers, t.dhcp) t.macResolvers = append(t.macResolvers, t.dhcp) t.hostnameResolvers = append(t.hostnameResolvers, t.dhcp) - t.refreshers = append(t.refreshers, t.dhcp) } go t.dhcp.watchChanges() } diff --git a/internal/clientinfo/dhcp.go b/internal/clientinfo/dhcp.go index 1aca295..9ddc2ed 100644 --- a/internal/clientinfo/dhcp.go +++ b/internal/clientinfo/dhcp.go @@ -28,7 +28,7 @@ type dhcp struct { selfIP string } -func (d *dhcp) refresh() error { +func (d *dhcp) init() error { watcher, err := fsnotify.NewWatcher() if err != nil { return err From e43b2b55304b6c2b1af0b767137d08d14ac22cfc Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 19 Jul 2023 09:08:02 +0700 Subject: [PATCH 52/84] internal/clientinfo: add doc comments for mdns operations While at it, also remove un-used channel argument of probe function. --- internal/clientinfo/mdns.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/internal/clientinfo/mdns.go b/internal/clientinfo/mdns.go index ac34713..e2a9588 100644 --- a/internal/clientinfo/mdns.go +++ b/internal/clientinfo/mdns.go @@ -73,10 +73,11 @@ func (m *mdns) init(quitCh chan struct{}) error { return nil } +// probeLoop performs mdns probe actively to get hostname updates. func (m *mdns) probeLoop(conns []*net.UDPConn, remoteAddr net.Addr, quitCh chan struct{}) { bo := backoff.NewBackoff("mdns probe", func(format string, args ...any) {}, time.Second*30) for { - err := m.probe(conns, remoteAddr, quitCh) + err := m.probe(conns, remoteAddr) if isErrNetUnreachableOrInvalid(err) { ctrld.ProxyLog.Warn().Msgf("stop probing %q: network unreachable or invalid", remoteAddr) break @@ -96,6 +97,7 @@ func (m *mdns) probeLoop(conns []*net.UDPConn, remoteAddr net.Addr, quitCh chan } } +// readLoop reads from mdns connection, save/update any hostnames found. func (m *mdns) readLoop(conn *net.UDPConn) { defer conn.Close() buf := make([]byte, dns.MaxMsgSize) @@ -141,7 +143,8 @@ func (m *mdns) readLoop(conn *net.UDPConn) { } } -func (m *mdns) probe(conns []*net.UDPConn, remoteAddr net.Addr, quitCh chan struct{}) error { +// probe performs mdns queries with known services. +func (m *mdns) probe(conns []*net.UDPConn, remoteAddr net.Addr) error { msg := new(dns.Msg) msg.Question = make([]dns.Question, len(services)) for i, service := range services { From 7ccecdd9f75a791ed091580965c68302230804ce Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 19 Jul 2023 02:14:55 +0000 Subject: [PATCH 53/84] cmd/ctrld: add more debugging information when self-check failed --- cmd/ctrld/cli.go | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 2384de8..a935a2e 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -1075,6 +1075,10 @@ func selfCheckStatus(status service.Status, domain string) service.Status { } }) v.WatchConfig() + var ( + lastAnswer *dns.Msg + lastErr error + ) for i := 0; i < maxAttempts; i++ { mu.Lock() if lcChanged != nil { @@ -1091,9 +1095,25 @@ func selfCheckStatus(status service.Status, domain string) service.Status { mainLog.Debug().Msgf("self-check against %q succeeded", domain) return status } + lastAnswer = r + lastErr = err bo.BackOff(ctx, fmt.Errorf("ExchangeContext: %w", err)) } mainLog.Debug().Msgf("self-check against %q failed", domain) + lc := cfg.FirstListener() + addr := net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)) + marker := strings.Repeat("=", 32) + mainLog.Debug().Msg(marker) + mainLog.Debug().Msgf("listener address : %s", addr) + mainLog.Debug().Msgf("last error : %v", lastErr) + if lastAnswer != nil { + mainLog.Debug().Msgf("last answer from ctrld :") + mainLog.Debug().Msg(marker) + for _, s := range strings.Split(lastAnswer.String(), "\n") { + mainLog.Debug().Msgf("%s", s) + } + mainLog.Debug().Msg(marker) + } return service.StatusUnknown } From 61b6431b6e92ccd86b43390793f1e291f322b8c3 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 21 Jul 2023 14:04:58 +0000 Subject: [PATCH 54/84] cmd/ctrld: trim os version on freebsd --- cmd/ctrld/cli.go | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index a935a2e..e7f64ca 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -176,8 +176,7 @@ func initCLI() { initLogging() mainLog.Info().Msgf("starting ctrld %s", curVersion()) - oi := osinfo.New() - mainLog.Info().Msgf("os: %s", oi.String()) + mainLog.Info().Msgf("os: %s", osVersion()) // Wait for network up. if !ctrldnet.Up() { @@ -1488,3 +1487,13 @@ func dirWritable(dir string) (bool, error) { defer os.Remove(f.Name()) return true, f.Close() } + +func osVersion() string { + oi := osinfo.New() + if runtime.GOOS == "freebsd" { + if ver, _, found := strings.Cut(oi.String(), ":"); found { + return ver + } + } + return oi.String() +} From 437fb1b16d548e158bc2eff96c7e79c0967b1f60 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 21 Jul 2023 16:28:08 +0000 Subject: [PATCH 55/84] all: add clients list command to debug Mac discovery --- cmd/ctrld/cli.go | 63 ++++++++++++++++++ cmd/ctrld/control_server.go | 18 ++++- go.mod | 5 +- go.sum | 8 +++ internal/clientinfo/arp.go | 17 +++++ internal/clientinfo/client_info.go | 102 ++++++++++++++++++++++++++++- internal/clientinfo/dhcp.go | 17 +++++ internal/clientinfo/mdns.go | 4 ++ internal/clientinfo/merlin.go | 4 ++ internal/clientinfo/ptr_lookup.go | 4 ++ 10 files changed, 236 insertions(+), 6 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index e7f64ca..00d964d 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -5,6 +5,7 @@ import ( "context" "crypto/x509" "encoding/base64" + "encoding/json" "errors" "fmt" "io" @@ -26,6 +27,7 @@ import ( "github.com/go-playground/validator/v10" "github.com/kardianos/service" "github.com/miekg/dns" + "github.com/olekukonko/tablewriter" "github.com/pelletier/go-toml/v2" "github.com/rs/zerolog" "github.com/spf13/cobra" @@ -35,6 +37,7 @@ import ( "tailscale.com/net/interfaces" "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/clientinfo" "github.com/Control-D-Inc/ctrld/internal/controld" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" "github.com/Control-D-Inc/ctrld/internal/router" @@ -751,6 +754,66 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, uninstallCmdAlias.Flags().StringVarP(&ifaceStartStop, "iface", "", "auto", `Reset DNS setting for iface, "auto" means the default interface gateway`) uninstallCmdAlias.Flags().AddFlagSet(stopCmd.Flags()) rootCmd.AddCommand(uninstallCmdAlias) + + listClientsCmd := &cobra.Command{ + Use: "list", + Short: "List clients that ctrld discovered", + Args: cobra.NoArgs, + PreRun: func(cmd *cobra.Command, args []string) { + initConsoleLogging() + checkHasElevatedPrivilege() + }, + Run: func(cmd *cobra.Command, args []string) { + dir, err := userHomeDir() + if err != nil { + mainLog.Fatal().Err(err).Msg("failed to find ctrld home dir") + } + cc := newControlClient(filepath.Join(dir, ctrldControlUnixSock)) + resp, err := cc.post(listClientsPath, nil) + if err != nil { + mainLog.Fatal().Err(err).Msg("failed to get clients list") + } + defer resp.Body.Close() + + var clients []*clientinfo.Client + if err := json.NewDecoder(resp.Body).Decode(&clients); err != nil { + mainLog.Fatal().Err(err).Msg("failed to decode clients list result") + } + map2Slice := func(m map[string]struct{}) []string { + s := make([]string, 0, len(m)) + for k := range m { + s = append(s, k) + } + sort.Strings(s) + return s + } + data := make([][]string, len(clients)) + for i, c := range clients { + row := []string{ + c.IP.String(), + c.Hostname, + c.Mac, + strings.Join(map2Slice(c.Source), ","), + } + data[i] = row + } + table := tablewriter.NewWriter(os.Stdout) + table.SetHeader([]string{"IP", "Hostname", "Mac", "Discovered"}) + table.SetAutoFormatHeaders(false) + table.AppendBulk(data) + table.Render() + }, + } + clientsCmd := &cobra.Command{ + Use: "clients", + Short: "Manage clients", + Args: cobra.OnlyValidArgs, + ValidArgs: []string{ + listClientsCmd.Use, + }, + } + clientsCmd.AddCommand(listClientsCmd) + rootCmd.AddCommand(clientsCmd) } func writeConfigFile() error { diff --git a/cmd/ctrld/control_server.go b/cmd/ctrld/control_server.go index 437e4a8..a1681a7 100644 --- a/cmd/ctrld/control_server.go +++ b/cmd/ctrld/control_server.go @@ -2,13 +2,18 @@ package main import ( "context" + "encoding/json" "net" "net/http" "os" + "sort" "time" ) -const contentTypeJson = "application/json" +const ( + contentTypeJson = "application/json" + listClientsPath = "/clients" +) type controlServer struct { server *http.Server @@ -48,7 +53,16 @@ func (s *controlServer) register(pattern string, handler http.Handler) { } func (p *prog) registerControlServerHandler() { - // TODO: register handler here. + p.cs.mux.Handle(listClientsPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + clients := p.ciTable.ListClients() + sort.Slice(clients, func(i, j int) bool { + return clients[i].IP.Less(clients[j].IP) + }) + if err := json.NewEncoder(w).Encode(&clients); err != nil { + http.Error(w, err.Error(), http.StatusInternalServerError) + return + } + })) } func jsonResponse(next http.Handler) http.Handler { diff --git a/go.mod b/go.mod index 6024239..1229987 100644 --- a/go.mod +++ b/go.mod @@ -15,10 +15,12 @@ require ( github.com/josharian/native v1.1.1-0.20230202152459-5c7d0dd6ab86 github.com/kardianos/service v1.2.1 github.com/miekg/dns v1.1.55 + github.com/olekukonko/tablewriter v0.0.5 github.com/pelletier/go-toml/v2 v2.0.8 github.com/quic-go/quic-go v0.32.0 github.com/rs/zerolog v1.28.0 github.com/spf13/cobra v1.7.0 + github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.16.0 github.com/stretchr/testify v1.8.3 github.com/vishvananda/netlink v1.2.1-beta.2 @@ -48,6 +50,7 @@ require ( github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.18 // indirect + github.com/mattn/go-runewidth v0.0.14 // indirect github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 // indirect @@ -60,11 +63,11 @@ require ( github.com/quic-go/qtls-go1-18 v0.2.0 // indirect github.com/quic-go/qtls-go1-19 v0.2.0 // indirect github.com/quic-go/qtls-go1-20 v0.1.0 // indirect + github.com/rivo/uniseg v0.4.4 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/spf13/afero v1.9.5 // indirect github.com/spf13/cast v1.5.1 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/subosito/gotenv v1.4.2 // indirect github.com/u-root/uio v0.0.0-20230305220412-3e8cd9d6bf63 // indirect github.com/vishvananda/netns v0.0.4 // indirect diff --git a/go.sum b/go.sum index be89ee2..bdd9bef 100644 --- a/go.sum +++ b/go.sum @@ -193,6 +193,9 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.18 h1:DOKFKCQ7FNG2L1rbrmstDN4QVRdS89Nkh85u68Uwp98= github.com/mattn/go-isatty v0.0.18/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= +github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE= github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y= github.com/mdlayher/netlink v1.7.2 h1:/UtM3ofJap7Vl4QWCPDGXY8d3GIY2UGSDbK+QWmY8/g= @@ -206,6 +209,8 @@ github.com/miekg/dns v1.1.55 h1:GoQ4hpsj0nFLYe+bWiCToyrBEJXkQfOOIvFGFy0lEgo= github.com/miekg/dns v1.1.55/go.mod h1:uInx36IzPl7FYnDcMeVWxj9byh7DutNykX4G9Sj60FY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.2.0 h1:3ZNA3L1c5FYDFTTxbFeVGGD8jYvjYauHD30YgLxVsNI= github.com/onsi/ginkgo/v2 v2.2.0/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= github.com/onsi/gomega v1.20.1 h1:PA/3qinGoukvymdIDV8pii6tiZgC8kbmJO6Z5+b002Q= @@ -230,6 +235,9 @@ github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV5 github.com/quic-go/qtls-go1-20 v0.1.0/go.mod h1:JKtK6mjbAVcUTN/9jZpvLbGxvdWIKS8uT7EiStoU1SM= github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA= github.com/quic-go/quic-go v0.32.0/go.mod h1:/fCsKANhQIeD5l76c2JFU+07gVE3KaA0FP+0zMWwfwo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= +github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= diff --git a/internal/clientinfo/arp.go b/internal/clientinfo/arp.go index ef70031..8429b56 100644 --- a/internal/clientinfo/arp.go +++ b/internal/clientinfo/arp.go @@ -27,3 +27,20 @@ func (a *arpDiscover) LookupMac(ip string) string { } return val.(string) } + +func (a *arpDiscover) String() string { + return "arp" +} + +func (a *arpDiscover) List() []string { + var ips []string + a.ip.Range(func(key, value any) bool { + ips = append(ips, value.(string)) + return true + }) + a.mac.Range(func(key, value any) bool { + ips = append(ips, key.(string)) + return true + }) + return ips +} diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 4dd757b..55fe0b1 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -1,6 +1,8 @@ package clientinfo import ( + "fmt" + "net/netip" "strings" "time" @@ -9,25 +11,35 @@ import ( // IpResolver is the interface for retrieving IP from Mac. type IpResolver interface { + fmt.Stringer + // LookupIP returns ip of the device with given mac. LookupIP(mac string) string + // List returns list of ip known by the resolver. + List() []string } // MacResolver is the interface for retrieving Mac from IP. type MacResolver interface { + fmt.Stringer + // LookupMac returns mac of the device with given ip. LookupMac(ip string) string } // HostnameByIpResolver is the interface for retrieving hostname from IP. type HostnameByIpResolver interface { + // LookupHostnameByIP returns hostname of the given ip. LookupHostnameByIP(ip string) string } // HostnameByMacResolver is the interface for retrieving hostname from Mac. type HostnameByMacResolver interface { + // LookupHostnameByMac returns hostname of the device with given mac. LookupHostnameByMac(mac string) string } +// HostnameResolver is the interface for retrieving hostname from either IP or Mac. type HostnameResolver interface { + fmt.Stringer HostnameByIpResolver HostnameByMacResolver } @@ -36,6 +48,13 @@ type refresher interface { refresh() error } +type Client struct { + IP netip.Addr + Mac string + Hostname string + Source map[string]struct{} +} + type Table struct { ipResolvers []IpResolver macResolvers []MacResolver @@ -146,9 +165,6 @@ func (t *Table) LookupIP(mac string) string { } func (t *Table) LookupMac(ip string) string { - t.arp.mac.Range(func(key, value any) bool { - return true - }) for _, r := range t.macResolvers { if mac := r.LookupMac(ip); mac != "" { return mac @@ -169,6 +185,86 @@ func (t *Table) LookupHostname(ip, mac string) string { return "" } +type macEntry struct { + mac string + src string +} + +type hostnameEntry struct { + name string + src string +} + +func (t *Table) lookupMacAll(ip string) []*macEntry { + var res []*macEntry + for _, r := range t.macResolvers { + res = append(res, &macEntry{mac: r.LookupMac(ip), src: r.String()}) + } + return res +} + +func (t *Table) lookupHostnameAll(ip, mac string) []*hostnameEntry { + var res []*hostnameEntry + for _, r := range t.hostnameResolvers { + src := r.String() + if name := r.LookupHostnameByIP(ip); name != "" { + res = append(res, &hostnameEntry{name: name, src: src}) + continue + } + if name := r.LookupHostnameByMac(mac); name != "" { + res = append(res, &hostnameEntry{name: name, src: src}) + continue + } + } + return res +} + +// ListClients returns list of clients discovered by ctrld. +func (t *Table) ListClients() []*Client { + for _, r := range t.refreshers { + _ = r.refresh() + } + ipMap := make(map[string]*Client) + for _, ir := range t.ipResolvers { + for _, ip := range ir.List() { + c, ok := ipMap[ip] + if !ok { + c = &Client{ + IP: netip.MustParseAddr(ip), + Source: map[string]struct{}{ir.String(): {}}, + } + ipMap[ip] = c + } else { + c.Source[ir.String()] = struct{}{} + } + } + } + for ip := range ipMap { + c := ipMap[ip] + for _, e := range t.lookupMacAll(ip) { + if c.Mac == "" && e.mac != "" { + c.Mac = e.mac + } + if e.mac != "" { + c.Source[e.src] = struct{}{} + } + } + for _, e := range t.lookupHostnameAll(ip, c.Mac) { + if c.Hostname == "" && e.name != "" { + c.Hostname = e.name + } + if e.name != "" { + c.Source[e.src] = struct{}{} + } + } + } + clients := make([]*Client, 0, len(ipMap)) + for _, c := range ipMap { + clients = append(clients, c) + } + return clients +} + func (t *Table) discoverDHCP() bool { if t.cfg.Service.DiscoverDHCP == nil { return true diff --git a/internal/clientinfo/dhcp.go b/internal/clientinfo/dhcp.go index 9ddc2ed..bcad26a 100644 --- a/internal/clientinfo/dhcp.go +++ b/internal/clientinfo/dhcp.go @@ -100,6 +100,23 @@ func (d *dhcp) LookupHostnameByMac(mac string) string { return val.(string) } +func (d *dhcp) String() string { + return "dhcp" +} + +func (d *dhcp) List() []string { + var ips []string + d.ip.Range(func(key, value any) bool { + ips = append(ips, value.(string)) + return true + }) + d.mac.Range(func(key, value any) bool { + ips = append(ips, key.(string)) + return true + }) + return ips +} + // AddLeaseFile adds given lease file for reading/watching clients info. func (d *dhcp) addLeaseFile(name string, format ctrld.LeaseFileFormat) error { if d.watcher == nil { diff --git a/internal/clientinfo/mdns.go b/internal/clientinfo/mdns.go index e2a9588..59ef7eb 100644 --- a/internal/clientinfo/mdns.go +++ b/internal/clientinfo/mdns.go @@ -43,6 +43,10 @@ func (m *mdns) LookupHostnameByMac(mac string) string { return "" } +func (m *mdns) String() string { + return "mdns" +} + func (m *mdns) init(quitCh chan struct{}) error { ifaces, err := multicastInterfaces() if err != nil { diff --git a/internal/clientinfo/merlin.go b/internal/clientinfo/merlin.go index 7e793ed..71c570c 100644 --- a/internal/clientinfo/merlin.go +++ b/internal/clientinfo/merlin.go @@ -65,3 +65,7 @@ func (m *merlinDiscover) parseMerlinCustomClientList(data string) { m.hostname.Store(mac, hostname) } } + +func (m *merlinDiscover) String() string { + return "merlin" +} diff --git a/internal/clientinfo/ptr_lookup.go b/internal/clientinfo/ptr_lookup.go index 600b67c..0de3f1a 100644 --- a/internal/clientinfo/ptr_lookup.go +++ b/internal/clientinfo/ptr_lookup.go @@ -36,6 +36,10 @@ func (p *ptrDiscover) LookupHostnameByMac(mac string) string { return "" } +func (p *ptrDiscover) String() string { + return "ptr" +} + func (p *ptrDiscover) lookupHostname(ip string) string { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() From 6be80e482736eaea5b44adc681218903949a8b4a Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 21 Jul 2023 16:31:02 +0000 Subject: [PATCH 56/84] internal/router: generalize freebsd-like router support --- cmd/ctrld/cli.go | 16 ++-- internal/router/dummy.go | 33 ------- internal/router/os_freebsd.go | 142 +++++++++++++++++++++++++++++ internal/router/os_others.go | 41 +++++++++ internal/router/pfsense/pfsense.go | 96 ------------------- internal/router/router.go | 18 ++-- 6 files changed, 203 insertions(+), 143 deletions(-) delete mode 100644 internal/router/dummy.go create mode 100644 internal/router/os_freebsd.go create mode 100644 internal/router/os_others.go delete mode 100644 internal/router/pfsense/pfsense.go diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 00d964d..90c5484 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -186,7 +186,7 @@ func initCLI() { mainLog.Fatal().Msg("network is not up yet") } - p.router = router.New(&cfg) + p.router = router.New(&cfg, cdUID != "") cs, err := newControlServer(filepath.Join(homedir, ctrldControlUnixSock)) if err != nil { mainLog.Warn().Err(err).Msg("could not create control server") @@ -337,7 +337,7 @@ func initCLI() { sc.Arguments = append([]string{"run"}, osArgs...) p := &prog{ - router: router.New(&cfg), + router: router.New(&cfg, cdUID != ""), cfg: &cfg, } if err := p.router.ConfigureService(sc); err != nil { @@ -503,9 +503,9 @@ func initCLI() { Short: "Stop the ctrld service", Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - tryReadingConfig(false) + readConfig(false) v.Unmarshal(&cfg) - p := &prog{router: router.New(&cfg)} + p := &prog{router: router.New(&cfg, cdUID != "")} s, err := newService(p, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) @@ -593,9 +593,9 @@ func initCLI() { NOTE: Uninstalling will set DNS to values provided by DHCP.`, Args: cobra.NoArgs, Run: func(cmd *cobra.Command, args []string) { - tryReadingConfig(false) + readConfig(false) v.Unmarshal(&cfg) - p := &prog{router: router.New(&cfg)} + p := &prog{router: router.New(&cfg, cdUID != "")} s, err := newService(p, svcConfig) if err != nil { mainLog.Error().Msg(err.Error()) @@ -1212,6 +1212,10 @@ func tryReadingConfig(writeDefaultConfig bool) { if !writeDefaultConfig { return } + readConfig(writeDefaultConfig) +} + +func readConfig(writeDefaultConfig bool) { configs := []struct { name string written bool diff --git a/internal/router/dummy.go b/internal/router/dummy.go deleted file mode 100644 index dea54e0..0000000 --- a/internal/router/dummy.go +++ /dev/null @@ -1,33 +0,0 @@ -package router - -import "github.com/kardianos/service" - -type dummy struct{} - -func (d *dummy) ConfigureService(_ *service.Config) error { - return nil -} - -func (d *dummy) Install(_ *service.Config) error { - return nil -} - -func (d *dummy) Uninstall(_ *service.Config) error { - return nil -} - -func (d *dummy) PreRun() error { - return nil -} - -func (d *dummy) Configure() error { - return nil -} - -func (d *dummy) Setup() error { - return nil -} - -func (d *dummy) Cleanup() error { - return nil -} diff --git a/internal/router/os_freebsd.go b/internal/router/os_freebsd.go new file mode 100644 index 0000000..9d9b738 --- /dev/null +++ b/internal/router/os_freebsd.go @@ -0,0 +1,142 @@ +package router + +import ( + "fmt" + "net" + "os" + "os/exec" + "path/filepath" + "text/template" + + "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld" +) + +const ( + osName = "freebsd" + rcPath = "/usr/local/etc/rc.d" + rcConfPath = "/etc/rc.conf.d/" + unboundRcPath = rcPath + "/unbound" + dnsmasqRcPath = rcPath + "/dnsmasq" +) + +func newOsRouter(cfg *ctrld.Config, cdMode bool) Router { + return &osRouter{cfg: cfg, cdMode: cdMode} +} + +type osRouter struct { + cfg *ctrld.Config + svcName string + // cdMode indicates whether the router will configure ctrld in cd mode (aka --cd=). + // When ctrld is running on freebsd-like routers, and there's process running on port 53 + // in cd mode, ctrld will attempt to kill the process and become direct listener. + // See details implemenation in osRouter.PreRun method. + cdMode bool +} + +func (or *osRouter) ConfigureService(svc *service.Config) error { + svc.Option["SysvScript"] = bsdInitScript + or.svcName = svc.Name + rcFile := filepath.Join(rcConfPath, or.svcName) + var to = &struct { + Name string + }{ + or.svcName, + } + + f, err := os.Create(rcFile) + if err != nil { + return fmt.Errorf("os.Create: %w", err) + } + defer f.Close() + if err := template.Must(template.New("").Parse(rcConfTmpl)).Execute(f, to); err != nil { + return err + } + return f.Close() +} + +func (or *osRouter) Install(_ *service.Config) error { + if isPfsense() { + // pfsense need ".sh" extension for script to be run at boot. + // See: https://docs.netgate.com/pfsense/en/latest/development/boot-commands.html#shell-script-option + oldname := filepath.Join(rcPath, or.svcName) + newname := filepath.Join(rcPath, or.svcName+".sh") + _ = os.Remove(newname) + if err := os.Symlink(oldname, newname); err != nil { + return fmt.Errorf("os.Symlink: %w", err) + } + } + return nil +} + +func (or *osRouter) Uninstall(_ *service.Config) error { + rcFiles := []string{filepath.Join(rcConfPath, or.svcName)} + if isPfsense() { + rcFiles = append(rcFiles, filepath.Join(rcPath, or.svcName+".sh")) + } + for _, filename := range rcFiles { + if err := os.Remove(filename); err != nil { + return fmt.Errorf("os.Remove: %w", err) + } + } + + return nil +} + +func (or *osRouter) PreRun() error { + if or.cdMode { + addr := "0.0.0.0:53" + udpLn, udpErr := net.ListenPacket("udp", addr) + if udpLn != nil { + udpLn.Close() + } + tcpLn, tcpErr := net.Listen("tcp", addr) + if tcpLn != nil { + tcpLn.Close() + } + // If we could not listen on :53 for any reason, try killing unbound/dnsmasq, become direct listener + if udpErr != nil || tcpErr != nil { + _ = exec.Command("killall", "unbound").Run() + _ = exec.Command("killall", "dnsmasq").Run() + } + } + return nil +} + +func (or *osRouter) Setup() error { + return nil +} + +func (or *osRouter) Cleanup() error { + if or.cfg.FirstListener().IsDirectDnsListener() { + _ = exec.Command(unboundRcPath, "onerestart").Run() + _ = exec.Command(dnsmasqRcPath, "onerestart").Run() + } + return nil +} + +const bsdInitScript = `#!/bin/sh + +# PROVIDE: {{.Name}} +# REQUIRE: SERVERS +# REQUIRE: unbound dnsmasq securelevel +# KEYWORD: shutdown + +. /etc/rc.subr + +name="{{.Name}}" +rcvar="${name}_enable" +{{.Name}}_env="IS_DAEMON=1" +pidfile="/var/run/${name}.pid" +command="/usr/sbin/daemon" +daemon_args="-P ${pidfile} -r -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}" +command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}" + +load_rc_config "${name}" +run_rc_command "$1" +` + +var rcConfTmpl = `# {{.Name}} +{{.Name}}_enable="YES" +` diff --git a/internal/router/os_others.go b/internal/router/os_others.go new file mode 100644 index 0000000..52b41e4 --- /dev/null +++ b/internal/router/os_others.go @@ -0,0 +1,41 @@ +//go:build !freebsd + +package router + +import ( + "github.com/kardianos/service" + + "github.com/Control-D-Inc/ctrld" +) + +const osName = "" + +func newOsRouter(cfg *ctrld.Config, cdMode bool) Router { + return &osRouter{} +} + +type osRouter struct{} + +func (d *osRouter) ConfigureService(_ *service.Config) error { + return nil +} + +func (d *osRouter) Install(_ *service.Config) error { + return nil +} + +func (d *osRouter) Uninstall(_ *service.Config) error { + return nil +} + +func (d *osRouter) PreRun() error { + return nil +} + +func (d *osRouter) Setup() error { + return nil +} + +func (d *osRouter) Cleanup() error { + return nil +} diff --git a/internal/router/pfsense/pfsense.go b/internal/router/pfsense/pfsense.go deleted file mode 100644 index 1806ec7..0000000 --- a/internal/router/pfsense/pfsense.go +++ /dev/null @@ -1,96 +0,0 @@ -package pfsense - -import ( - "fmt" - "os" - "os/exec" - "path/filepath" - - "github.com/Control-D-Inc/ctrld" - "github.com/kardianos/service" -) - -const ( - Name = "pfsens" - - rcPath = "/usr/local/etc/rc.d" - unboundRcPath = rcPath + "/unbound" - dnsmasqRcPath = rcPath + "/dnsmasq" -) - -const pfsenseInitScript = `#!/bin/sh - -# PROVIDE: {{.Name}} -# REQUIRE: SERVERS -# REQUIRE: unbound dnsmasq securelevel -# KEYWORD: shutdown - -. /etc/rc.subr - -name="{{.Name}}" -{{.Name}}_env="IS_DAEMON=1" -pidfile="/var/run/${name}.pid" -command="/usr/sbin/daemon" -daemon_args="-P ${pidfile} -r -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}" -command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}" - -run_rc_command "$1" -` - -type Pfsense struct { - cfg *ctrld.Config - svcName string -} - -// New returns a router.Router for configuring/setup/run ctrld on Pfsense routers. -func New(cfg *ctrld.Config) *Pfsense { - return &Pfsense{cfg: cfg} -} - -func (p *Pfsense) ConfigureService(svc *service.Config) error { - svc.Option["SysvScript"] = pfsenseInitScript - p.svcName = svc.Name - return nil -} - -func (p *Pfsense) Install(config *service.Config) error { - // pfsense need ".sh" extension for script to be run at boot. - // See: https://docs.netgate.com/pfsense/en/latest/development/boot-commands.html#shell-script-option - oldname := filepath.Join(rcPath, p.svcName) - newname := filepath.Join(rcPath, p.svcName+".sh") - _ = os.Remove(newname) - if err := os.Symlink(oldname, newname); err != nil { - return fmt.Errorf("os.Symlink: %w", err) - } - return nil -} - -func (p *Pfsense) Uninstall(config *service.Config) error { - if err := os.Remove(filepath.Join(rcPath, p.svcName+".sh")); err != nil { - return fmt.Errorf("os.Remove: %w", err) - } - return nil -} - -func (p *Pfsense) PreRun() error { - // TODO: remove this hacky solution. - // If Pfsense is in DNS Resolver mode, ensure no unbound processes running. - _ = exec.Command("killall", "unbound").Run() - - // If Pfsense is in DNS Forwarder mode, ensure no dnsmasq processes running. - _ = exec.Command("killall", "dnsmasq").Run() - return nil -} - -func (p *Pfsense) Setup() error { - return nil -} - -func (p *Pfsense) Cleanup() error { - if p.cfg.FirstListener().IsDirectDnsListener() { - _ = exec.Command(unboundRcPath, "onerestart").Run() - _ = exec.Command(dnsmasqRcPath, "onerestart").Run() - } - - return nil -} diff --git a/internal/router/router.go b/internal/router/router.go index b500de6..6cae16e 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -19,7 +19,6 @@ import ( "github.com/Control-D-Inc/ctrld/internal/router/firewalla" "github.com/Control-D-Inc/ctrld/internal/router/merlin" "github.com/Control-D-Inc/ctrld/internal/router/openwrt" - "github.com/Control-D-Inc/ctrld/internal/router/pfsense" "github.com/Control-D-Inc/ctrld/internal/router/synology" "github.com/Control-D-Inc/ctrld/internal/router/tomato" "github.com/Control-D-Inc/ctrld/internal/router/ubios" @@ -27,8 +26,11 @@ import ( // Service is the interface to manage ctrld service on router. type Service interface { + // ConfigureService performs works for installing ctrla as a service on router. ConfigureService(*service.Config) error + // Install performs necessary works after service.Install done. Install(*service.Config) error + // Uninstall performs necessary works after service.Uninstallation done. Uninstall(*service.Config) error } @@ -36,13 +38,17 @@ type Service interface { type Router interface { Service + // PreRun performs works need to be done before ctrld being run on router. + // Implementation should only return if the pre-condition was met (e.g: ntp synced). PreRun() error + // Setup configures ctrld to be run on the router. Setup() error + // Cleanup cleans up works setup on router by ctrld. Cleanup() error } // New returns new Router interface. -func New(cfg *ctrld.Config) Router { +func New(cfg *ctrld.Config, cdMode bool) Router { switch Name() { case ddwrt.Name: return ddwrt.New(cfg) @@ -58,12 +64,10 @@ func New(cfg *ctrld.Config) Router { return synology.New(cfg) case tomato.Name: return tomato.New(cfg) - case pfsense.Name: - return pfsense.New(cfg) case firewalla.Name: return firewalla.New(cfg) } - return &dummy{} + return newOsRouter(cfg, cdMode) } // IsGLiNet reports whether the router is an GL.iNet router. @@ -202,12 +206,10 @@ func distroName() string { return edgeos.Name case haveFile("/etc/ubnt/init/vyatta-router"): return edgeos.Name // For 2.x - case isPfsense(): - return pfsense.Name case haveFile("/etc/firewalla_release"): return firewalla.Name } - return "" + return osName } func haveFile(file string) bool { From 6b43639be553b945f99a7de830b46ec5fc185173 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 21 Jul 2023 16:35:28 +0000 Subject: [PATCH 57/84] cmd/ctrld: wait until ctrld listener ready to do self-check --- cmd/ctrld/cli.go | 39 ++++++++++++++++++++++++++++++++++--- cmd/ctrld/control_server.go | 9 +++++++++ cmd/ctrld/prog.go | 10 +++++++--- 3 files changed, 52 insertions(+), 6 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 90c5484..8484677 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -10,6 +10,7 @@ import ( "fmt" "io" "net" + "net/http" "net/netip" "os" "os/exec" @@ -1103,13 +1104,45 @@ func selfCheckStatus(status service.Status, domain string) service.Status { // Nothing to do, return the status as-is. return status } - c := new(dns.Client) + dir, err := userHomeDir() + if err != nil { + mainLog.Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory") + return service.StatusUnknown + } + bo := backoff.NewBackoff("self-check", logf, 10*time.Second) - bo.LogLongerThan = 500 * time.Millisecond + bo.LogLongerThan = 10 * time.Second ctx := context.Background() maxAttempts := 20 - mainLog.Debug().Msg("Performing self-check") + mainLog.Debug().Msg("waiting for ctrld listener to be ready") + cc := newControlClient(filepath.Join(dir, ctrldControlUnixSock)) + + // The socket control server may not start yet, so attempt to ping + // it until we got a response, or maxAttempts reached. + for i := 0; i < maxAttempts; i++ { + if _, err := cc.post("/", nil); err != nil { + bo.BackOff(ctx, err) + continue + } + break + } + resp, err := cc.post(startedPath, nil) + if err != nil { + mainLog.Error().Err(err).Msg("failed to connect to control server") + return service.StatusUnknown + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + mainLog.Error().Msg("ctrld listener is not ready") + return service.StatusUnknown + } + + mainLog.Debug().Msg("ctrld listener is ready") + mainLog.Debug().Msg("performing self-check") + bo = backoff.NewBackoff("self-check", logf, 10*time.Second) + bo.LogLongerThan = 500 * time.Millisecond + c := new(dns.Client) var ( lcChanged map[string]*ctrld.ListenerConfig mu sync.Mutex diff --git a/cmd/ctrld/control_server.go b/cmd/ctrld/control_server.go index a1681a7..c779a18 100644 --- a/cmd/ctrld/control_server.go +++ b/cmd/ctrld/control_server.go @@ -13,6 +13,7 @@ import ( const ( contentTypeJson = "application/json" listClientsPath = "/clients" + startedPath = "/started" ) type controlServer struct { @@ -63,6 +64,14 @@ func (p *prog) registerControlServerHandler() { return } })) + p.cs.mux.Handle(startedPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + select { + case <-p.onStartedDone: + w.WriteHeader(http.StatusOK) + case <-time.After(10 * time.Second): + w.WriteHeader(http.StatusRequestTimeout) + } + })) } func jsonResponse(next http.Handler) http.Handler { diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index dd0b5f0..50d49f8 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -50,9 +50,10 @@ type prog struct { ciTable *clientinfo.Table router router.Router - started chan struct{} - onStarted []func() - onStopped []func() + started chan struct{} + onStartedDone chan struct{} + onStarted []func() + onStopped []func() } func (p *prog) Start(s service.Service) error { @@ -67,6 +68,7 @@ func (p *prog) run() { p.preRun() numListeners := len(p.cfg.Listener) p.started = make(chan struct{}, numListeners) + p.onStartedDone = make(chan struct{}) if p.cfg.Service.CacheEnable { cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize) if err != nil { @@ -146,6 +148,8 @@ func (p *prog) run() { for _, f := range p.onStarted { f() } + close(p.onStartedDone) + // Stop writing log to unix socket. consoleWriter.Out = os.Stdout initLoggingWithBackup(false) From 28f32bd7e5a92e3b9542bfabd6a4760db55a3a0e Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 21 Jul 2023 16:41:58 +0000 Subject: [PATCH 58/84] cmd/ctrld: use controlServer register method --- cmd/ctrld/control_server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/ctrld/control_server.go b/cmd/ctrld/control_server.go index c779a18..20dec83 100644 --- a/cmd/ctrld/control_server.go +++ b/cmd/ctrld/control_server.go @@ -54,7 +54,7 @@ func (s *controlServer) register(pattern string, handler http.Handler) { } func (p *prog) registerControlServerHandler() { - p.cs.mux.Handle(listClientsPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + p.cs.register(listClientsPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { clients := p.ciTable.ListClients() sort.Slice(clients, func(i, j int) bool { return clients[i].IP.Less(clients[j].IP) @@ -64,7 +64,7 @@ func (p *prog) registerControlServerHandler() { return } })) - p.cs.mux.Handle(startedPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { + p.cs.register(startedPath, http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) { select { case <-p.onStartedDone: w.WriteHeader(http.StatusOK) From 12c8ab696fc034f65bf4f1a53f4bc3c0b7efefa9 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 25 Jul 2023 00:28:26 +0000 Subject: [PATCH 59/84] cmd/ctrld: use RFC1918 addresses as nameservers if required --- cmd/ctrld/dns_proxy.go | 57 +++++++++++++++++++++++++----------------- cmd/ctrld/prog.go | 6 ++++- 2 files changed, 39 insertions(+), 24 deletions(-) diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index 0586151..ba0c3be 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -80,7 +80,6 @@ func (p *prog) serveDNS(listenerNum string) error { } }) - needRFC1918Listeners := listenerConfig.IP == "127.0.0.1" && listenerConfig.Port == 53 g, ctx := errgroup.WithContext(context.Background()) for _, proto := range []string{"udp", "tcp"} { proto := proto @@ -101,30 +100,23 @@ func (p *prog) serveDNS(listenerNum string) error { } // When we spawn a listener on 127.0.0.1, also spawn listeners on the RFC1918 // addresses of the machine. So ctrld could receive queries from LAN clients. - if needRFC1918Listeners { + if needRFC1918Listeners(listenerConfig) { g.Go(func() error { - interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { - addrs, _ := i.Addrs() - for _, addr := range addrs { - ipNet, ok := addr.(*net.IPNet) - if !ok || !ipNet.IP.IsPrivate() { - continue + for _, addr := range rfc1918Addresses() { + func() { + listenAddr := net.JoinHostPort(addr, strconv.Itoa(listenerConfig.Port)) + s, errCh := runDNSServer(listenAddr, proto, handler) + defer s.Shutdown() + select { + case <-p.stopCh: + case <-ctx.Done(): + case err := <-errCh: + // RFC1918 listener should not terminate ctrld. + // It's a workaround for a quirk on system with systemd-resolved. + mainLog.Warn().Err(err).Msgf("could not listen on %s: %s", proto, listenAddr) } - func() { - listenAddr := net.JoinHostPort(ipNet.IP.String(), "53") - s, errCh := runDNSServer(listenAddr, proto, handler) - defer s.Shutdown() - select { - case <-p.stopCh: - case <-ctx.Done(): - case err := <-errCh: - // RFC1918 listener should not terminate ctrld. - // It's a workaround for a quirk on system with systemd-resolved. - mainLog.Warn().Err(err).Msgf("could not listen on %s: %s", proto, listenAddr) - } - }() - } - }) + }() + } return nil }) } @@ -556,3 +548,22 @@ func (p *prog) getClientInfo(ip, mac string) *ctrld.ClientInfo { ci.Hostname = p.ciTable.LookupHostname(ci.IP, ci.Mac) return ci } + +func needRFC1918Listeners(lc *ctrld.ListenerConfig) bool { + return lc.IP == "127.0.0.1" && lc.Port == 53 +} + +func rfc1918Addresses() []string { + var res []string + interfaces.ForeachInterface(func(i interfaces.Interface, prefixes []netip.Prefix) { + addrs, _ := i.Addrs() + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok || !ipNet.IP.IsPrivate() { + continue + } + res = append(res, ipNet.IP.String()) + } + }) + return res +} diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 50d49f8..dce7c03 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -239,7 +239,11 @@ func (p *prog) setDNS() { // so we could just use lc.IP as nameserver. } - if err := setDNS(netIface, []string{ns}); err != nil { + nameservers := []string{ns} + if needRFC1918Listeners(lc) { + nameservers = append(nameservers, rfc1918Addresses()...) + } + if err := setDNS(netIface, nameservers); err != nil { logger.Error().Err(err).Msgf("could not set DNS for interface") return } From 59dc74ffbbbfe404b414b00dd6043af9133ecd23 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 25 Jul 2023 20:28:30 +0000 Subject: [PATCH 60/84] internal: record correct interfaces for queries from router on Firewalla --- internal/clientinfo/dhcp.go | 23 +++++++++++-- internal/router/dnsmasq/dnsmasq.go | 52 ++++++++++++++++++------------ internal/router/router.go | 10 ++++++ 3 files changed, 62 insertions(+), 23 deletions(-) diff --git a/internal/clientinfo/dhcp.go b/internal/clientinfo/dhcp.go index bcad26a..ad423bd 100644 --- a/internal/clientinfo/dhcp.go +++ b/internal/clientinfo/dhcp.go @@ -11,11 +11,12 @@ import ( "strings" "sync" - "github.com/Control-D-Inc/ctrld" + "github.com/fsnotify/fsnotify" "tailscale.com/net/interfaces" "tailscale.com/util/lineread" - "github.com/fsnotify/fsnotify" + "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/router" ) type dhcp struct { @@ -279,4 +280,22 @@ func (d *dhcp) addSelf() { } } }) + for _, netIface := range router.SelfInterfaces() { + mac := netIface.HardwareAddr.String() + if mac == "" { + return + } + d.mac2name.Store(mac, hostname) + addrs, _ := netIface.Addrs() + for _, addr := range addrs { + ipNet, ok := addr.(*net.IPNet) + if !ok { + continue + } + ip := ipNet.IP + d.mac.LoadOrStore(ip.String(), mac) + d.ip.LoadOrStore(mac, ip.String()) + d.ip2name.Store(ip.String(), hostname) + } + } } diff --git a/internal/router/dnsmasq/dnsmasq.go b/internal/router/dnsmasq/dnsmasq.go index 0b051aa..a25f564 100644 --- a/internal/router/dnsmasq/dnsmasq.go +++ b/internal/router/dnsmasq/dnsmasq.go @@ -13,7 +13,7 @@ import ( const ConfigContentTmpl = `# GENERATED BY ctrld - DO NOT MODIFY no-resolv {{- range .Upstreams}} -server={{ .Ip }}#{{ .Port }} +server={{ .IP }}#{{ .Port }} {{- end}} {{- if .SendClientInfo}} add-mac @@ -36,7 +36,7 @@ if [ -n "$pid" ] && [ -f "/proc/${pid}/cmdline" ]; then # use ctrld as upstream pc_delete "server=" "$config_file" {{- range .Upstreams}} - pc_append "server={{ .Ip }}#{{ .Port }}" "$config_file" + pc_append "server={{ .IP }}#{{ .Port }}" "$config_file" {{- end}} {{- if .SendClientInfo}} pc_append "add-mac" "$config_file" # add client mac @@ -56,7 +56,7 @@ fi ` type Upstream struct { - Ip string + IP string Port int } @@ -69,7 +69,7 @@ func ConfTmpl(tmplText string, cfg *ctrld.Config) (string, error) { if ip == "0.0.0.0" || ip == "::" || ip == "" { ip = "127.0.0.1" } - upstreams := []Upstream{{Ip: ip, Port: listener.Port}} + upstreams := []Upstream{{IP: ip, Port: listener.Port}} return confTmpl(tmplText, upstreams, cfg.HasUpstreamSendClientInfo()) } @@ -97,25 +97,35 @@ func confTmpl(tmplText string, upstreams []Upstream, sendClientInfo bool) (strin } func firewallaUpstreams(port int) []Upstream { - matches, err := filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf") - if err != nil { - return nil - } - upstreams := make([]Upstream, 0, len(matches)) - for _, match := range matches { - // Trim prefix and suffix to get the iface name only. - ifaceName := strings.TrimSuffix(strings.TrimPrefix(match, "/home/pi/firerouter/etc/dnsmasq.dns."), ".conf") - if netIface, _ := net.InterfaceByName(ifaceName); netIface != nil { - addrs, _ := netIface.Addrs() - for _, addr := range addrs { - if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { - upstreams = append(upstreams, Upstream{ - Ip: netIP.IP.To4().String(), - Port: port, - }) - } + ifaces := FirewallaSelfInterfaces() + upstreams := make([]Upstream, 0, len(ifaces)) + for _, netIface := range ifaces { + addrs, _ := netIface.Addrs() + for _, addr := range addrs { + if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { + upstreams = append(upstreams, Upstream{ + IP: netIP.IP.To4().String(), + Port: port, + }) } } } return upstreams } + +// FirewallaSelfInterfaces returns list of interfaces that will be configured with default dnsmasq setup on Firewalla. +func FirewallaSelfInterfaces() []*net.Interface { + matches, err := filepath.Glob("/home/pi/firerouter/etc/dnsmasq.dns.*.conf") + if err != nil { + return nil + } + ifaces := make([]*net.Interface, 0, len(matches)) + for _, match := range matches { + // Trim prefix and suffix to get the iface name only. + ifaceName := strings.TrimSuffix(strings.TrimPrefix(match, "/home/pi/firerouter/etc/dnsmasq.dns."), ".conf") + if netIface, _ := net.InterfaceByName(ifaceName); netIface != nil { + ifaces = append(ifaces, netIface) + } + } + return ifaces +} diff --git a/internal/router/router.go b/internal/router/router.go index 6cae16e..90882c9 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -188,6 +188,16 @@ func ServiceDependencies() []string { return nil } +// SelfInterfaces return list of *net.Interface that will be source of requests from router itself. +func SelfInterfaces() []*net.Interface { + switch Name() { + case firewalla.Name: + return dnsmasq.FirewallaSelfInterfaces() + default: + return nil + } +} + func distroName() string { switch { case bytes.HasPrefix(unameO(), []byte("DD-WRT")): From 19bc44a7f324b0a35f6ae476af1b300b325e62d3 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 26 Jul 2023 16:48:17 +0000 Subject: [PATCH 61/84] all: prevent data race when accessing zerolog.Logger --- cmd/ctrld/cli.go | 203 +++++++++++++++-------------- cmd/ctrld/dns_proxy.go | 38 +++--- cmd/ctrld/main.go | 26 ++-- cmd/ctrld/main_test.go | 3 +- cmd/ctrld/net_darwin.go | 2 +- cmd/ctrld/netlink_linux.go | 4 +- cmd/ctrld/network_manager_linux.go | 18 +-- cmd/ctrld/os_darwin.go | 8 +- cmd/ctrld/os_freebsd.go | 12 +- cmd/ctrld/os_linux.go | 16 +-- cmd/ctrld/os_windows.go | 12 +- cmd/ctrld/prog.go | 34 ++--- cmd/ctrld/service.go | 6 +- config.go | 10 +- config_quic.go | 4 +- doh.go | 2 +- internal/clientinfo/client_info.go | 18 +-- internal/clientinfo/dhcp.go | 10 +- internal/clientinfo/mdns.go | 10 +- internal/clientinfo/merlin.go | 2 +- internal/clientinfo/ptr_lookup.go | 4 +- internal/controld/config.go | 4 +- log.go | 10 ++ resolver.go | 8 +- 24 files changed, 242 insertions(+), 222 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 8484677..0774f0b 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -147,7 +147,7 @@ func initCLI() { } if daemon && runtime.GOOS == "windows" { - mainLog.Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.") + mainLog.Load().Fatal().Msg("Cannot run in daemon mode. Please install a Windows service.") } if !daemon { @@ -156,10 +156,10 @@ func initCLI() { go func() { s, err := newService(p, svcConfig) if err != nil { - mainLog.Fatal().Err(err).Msg("failed create new service") + mainLog.Load().Fatal().Err(err).Msg("failed create new service") } if err := s.Run(); err != nil { - mainLog.Error().Err(err).Msg("failed to start service") + mainLog.Load().Error().Err(err).Msg("failed to start service") } }() } @@ -170,7 +170,7 @@ func initCLI() { readBase64Config(configBase64) processNoConfigFlags(noConfigStart) if err := v.Unmarshal(&cfg); err != nil { - mainLog.Fatal().Msgf("failed to unmarshal config: %v", err) + mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err) } processLogAndCacheFlags() @@ -179,18 +179,18 @@ func initCLI() { // so it's able to log information in processCDFlags. initLogging() - mainLog.Info().Msgf("starting ctrld %s", curVersion()) - mainLog.Info().Msgf("os: %s", osVersion()) + mainLog.Load().Info().Msgf("starting ctrld %s", curVersion()) + mainLog.Load().Info().Msgf("os: %s", osVersion()) // Wait for network up. if !ctrldnet.Up() { - mainLog.Fatal().Msg("network is not up yet") + mainLog.Load().Fatal().Msg("network is not up yet") } p.router = router.New(&cfg, cdUID != "") cs, err := newControlServer(filepath.Join(homedir, ctrldControlUnixSock)) if err != nil { - mainLog.Warn().Err(err).Msg("could not create control server") + mainLog.Load().Warn().Err(err).Msg("could not create control server") } p.cs = cs @@ -198,7 +198,7 @@ func initCLI() { // time for validating server certificate. Some routers need NTP synchronization // to set the current time, so this check must happen before processCDFlags. if err := p.router.PreRun(); err != nil { - mainLog.Fatal().Err(err).Msg("failed to perform router pre-run check") + mainLog.Load().Fatal().Err(err).Msg("failed to perform router pre-run check") } oldLogPath := cfg.Service.LogPath @@ -213,19 +213,20 @@ func initCLI() { } if err := writeConfigFile(); err != nil { - mainLog.Fatal().Err(err).Msg("failed to write config file") + mainLog.Load().Fatal().Err(err).Msg("failed to write config file") } else { - mainLog.Info().Msg("writing config file to: " + defaultConfigFile) + mainLog.Load().Info().Msg("writing config file to: " + defaultConfigFile) } 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) + l := zerolog.New(io.Discard) + mainLog.Store(&l) // 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") + mainLog.Load().Warn().Err(err).Msg("could not copy old log file") } } initLoggingWithBackup(false) @@ -237,22 +238,22 @@ func initCLI() { if daemon { exe, err := os.Executable() if err != nil { - mainLog.Error().Err(err).Msg("failed to find the binary") + mainLog.Load().Error().Err(err).Msg("failed to find the binary") os.Exit(1) } curDir, err := os.Getwd() if err != nil { - mainLog.Error().Err(err).Msg("failed to get current working directory") + mainLog.Load().Error().Err(err).Msg("failed to get current working directory") os.Exit(1) } // If running as daemon, re-run the command in background, with daemon off. cmd := exec.Command(exe, append(os.Args[1:], "-d=false")...) cmd.Dir = curDir if err := cmd.Start(); err != nil { - mainLog.Error().Err(err).Msg("failed to start process as daemon") + mainLog.Load().Error().Err(err).Msg("failed to start process as daemon") os.Exit(1) } - mainLog.Info().Int("pid", cmd.Process.Pid).Msg("DNS proxy started") + mainLog.Load().Info().Int("pid", cmd.Process.Pid).Msg("DNS proxy started") os.Exit(0) } @@ -260,7 +261,7 @@ func initCLI() { for _, lc := range p.cfg.Listener { if shouldAllocateLoopbackIP(lc.IP) { if err := allocateIP(lc.IP); err != nil { - mainLog.Error().Err(err).Msgf("could not allocate IP: %s", lc.IP) + mainLog.Load().Error().Err(err).Msgf("could not allocate IP: %s", lc.IP) } } } @@ -269,7 +270,7 @@ func initCLI() { for _, lc := range p.cfg.Listener { if shouldAllocateLoopbackIP(lc.IP) { if err := deAllocateIP(lc.IP); err != nil { - mainLog.Error().Err(err).Msgf("could not de-allocate IP: %s", lc.IP) + mainLog.Load().Error().Err(err).Msgf("could not de-allocate IP: %s", lc.IP) } } } @@ -279,15 +280,15 @@ func initCLI() { rootCertPool = cp } p.onStarted = append(p.onStarted, func() { - mainLog.Debug().Msg("router setup") + mainLog.Load().Debug().Msg("router setup") if err := p.router.Setup(); err != nil { - mainLog.Error().Err(err).Msg("could not configure router") + mainLog.Load().Error().Err(err).Msg("could not configure router") } }) p.onStopped = append(p.onStopped, func() { - mainLog.Debug().Msg("router cleanup") + mainLog.Load().Debug().Msg("router cleanup") if err := p.router.Cleanup(); err != nil { - mainLog.Error().Err(err).Msg("could not cleanup router") + mainLog.Load().Error().Err(err).Msg("could not cleanup router") } p.resetDNS() }) @@ -342,7 +343,7 @@ func initCLI() { cfg: &cfg, } if err := p.router.ConfigureService(sc); err != nil { - mainLog.Fatal().Err(err).Msg("failed to configure service on router") + mainLog.Load().Fatal().Err(err).Msg("failed to configure service on router") } // No config path, generating config in HOME directory. @@ -386,7 +387,7 @@ func initCLI() { tryReadingConfig(writeDefaultConfig) if err := v.Unmarshal(&cfg); err != nil { - mainLog.Fatal().Msgf("failed to unmarshal config: %v", err) + mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err) } initLogging() @@ -400,12 +401,12 @@ func initCLI() { s, err := newService(p, sc) if err != nil { - mainLog.Error().Msg(err.Error()) + mainLog.Load().Error().Msg(err.Error()) return } if router.Name() != "" { - mainLog.Debug().Msg("cleaning up router before installing") + mainLog.Load().Debug().Msg("cleaning up router before installing") _ = p.router.Cleanup() } @@ -417,12 +418,12 @@ func initCLI() { } if doTasks(tasks) { if err := p.router.Install(sc); err != nil { - mainLog.Warn().Err(err).Msg("post installation failed, please check system/service log for details error") + mainLog.Load().Warn().Err(err).Msg("post installation failed, please check system/service log for details error") return } status, err := s.Status() if err != nil { - mainLog.Warn().Err(err).Msg("could not get service status") + mainLog.Load().Warn().Err(err).Msg("could not get service status") return } @@ -430,15 +431,15 @@ func initCLI() { status = selfCheckStatus(status, domain) switch status { case service.StatusRunning: - mainLog.Notice().Msg("Service started") + mainLog.Load().Notice().Msg("Service started") default: marker := bytes.Repeat([]byte("="), 32) - mainLog.Error().Msg("ctrld service may not have started due to an error or misconfiguration, service log:") - _, _ = mainLog.Write(marker) + mainLog.Load().Error().Msg("ctrld service may not have started due to an error or misconfiguration, service log:") + _, _ = mainLog.Load().Write(marker) for msg := range runCmdLogCh { - _, _ = mainLog.Write([]byte(msg)) + _, _ = mainLog.Load().Write([]byte(msg)) } - _, _ = mainLog.Write(marker) + _, _ = mainLog.Load().Write(marker) uninstall(p, s) os.Exit(1) } @@ -473,7 +474,7 @@ func initCLI() { Run: func(cmd *cobra.Command, _ []string) { exe, err := os.Executable() if err != nil { - mainLog.Fatal().Msgf("could not find executable path: %v", err) + mainLog.Load().Fatal().Msgf("could not find executable path: %v", err) os.Exit(1) } flags := make([]string, 0) @@ -487,7 +488,7 @@ func initCLI() { command.Stderr = os.Stderr command.Stdin = os.Stdin if err := command.Run(); err != nil { - mainLog.Fatal().Msg(err.Error()) + mainLog.Load().Fatal().Msg(err.Error()) } }, } @@ -509,14 +510,14 @@ func initCLI() { p := &prog{router: router.New(&cfg, cdUID != "")} s, err := newService(p, svcConfig) if err != nil { - mainLog.Error().Msg(err.Error()) + mainLog.Load().Error().Msg(err.Error()) return } initLogging() if doTasks([]task{{s.Stop, true}}) { p.router.Cleanup() p.resetDNS() - mainLog.Notice().Msg("Service stopped") + mainLog.Load().Notice().Msg("Service stopped") } }, } @@ -533,12 +534,12 @@ func initCLI() { Run: func(cmd *cobra.Command, args []string) { s, err := newService(&prog{}, svcConfig) if err != nil { - mainLog.Error().Msg(err.Error()) + mainLog.Load().Error().Msg(err.Error()) return } initLogging() if doTasks([]task{{s.Restart, true}}) { - mainLog.Notice().Msg("Service restarted") + mainLog.Load().Notice().Msg("Service restarted") } }, } @@ -553,23 +554,23 @@ func initCLI() { Run: func(cmd *cobra.Command, args []string) { s, err := newService(&prog{}, svcConfig) if err != nil { - mainLog.Error().Msg(err.Error()) + mainLog.Load().Error().Msg(err.Error()) return } status, err := s.Status() if err != nil { - mainLog.Error().Msg(err.Error()) + mainLog.Load().Error().Msg(err.Error()) os.Exit(1) } switch status { case service.StatusUnknown: - mainLog.Notice().Msg("Unknown status") + mainLog.Load().Notice().Msg("Unknown status") os.Exit(2) case service.StatusRunning: - mainLog.Notice().Msg("Service is running") + mainLog.Load().Notice().Msg("Service is running") os.Exit(0) case service.StatusStopped: - mainLog.Notice().Msg("Service is stopped") + mainLog.Load().Notice().Msg("Service is stopped") os.Exit(1) } }, @@ -599,7 +600,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, p := &prog{router: router.New(&cfg, cdUID != "")} s, err := newService(p, svcConfig) if err != nil { - mainLog.Error().Msg(err.Error()) + mainLog.Load().Error().Msg(err.Error()) return } if iface == "" { @@ -639,7 +640,7 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, println() }) if err != nil { - mainLog.Error().Msg(err.Error()) + mainLog.Load().Error().Msg(err.Error()) } }, } @@ -767,18 +768,18 @@ NOTE: Uninstalling will set DNS to values provided by DHCP.`, Run: func(cmd *cobra.Command, args []string) { dir, err := userHomeDir() if err != nil { - mainLog.Fatal().Err(err).Msg("failed to find ctrld home dir") + mainLog.Load().Fatal().Err(err).Msg("failed to find ctrld home dir") } cc := newControlClient(filepath.Join(dir, ctrldControlUnixSock)) resp, err := cc.post(listClientsPath, nil) if err != nil { - mainLog.Fatal().Err(err).Msg("failed to get clients list") + mainLog.Load().Fatal().Err(err).Msg("failed to get clients list") } defer resp.Body.Close() var clients []*clientinfo.Client if err := json.NewDecoder(resp.Body).Decode(&clients); err != nil { - mainLog.Fatal().Err(err).Msg("failed to decode clients list result") + mainLog.Load().Fatal().Err(err).Msg("failed to decode clients list result") } map2Slice := func(m map[string]struct{}) []string { s := make([]string, 0, len(m)) @@ -847,7 +848,7 @@ func readConfigFile(writeDefaultConfig bool) bool { // If err == nil, there's a config supplied via `--config`, no default config written. err := v.ReadInConfig() if err == nil { - mainLog.Info().Msg("loading config file from: " + v.ConfigFileUsed()) + mainLog.Load().Info().Msg("loading config file from: " + v.ConfigFileUsed()) defaultConfigFile = v.ConfigFileUsed() return true } @@ -859,16 +860,16 @@ func readConfigFile(writeDefaultConfig bool) bool { // If error is viper.ConfigFileNotFoundError, write default config. if _, ok := err.(viper.ConfigFileNotFoundError); ok { if err := v.Unmarshal(&cfg); err != nil { - mainLog.Fatal().Msgf("failed to unmarshal default config: %v", err) + mainLog.Load().Fatal().Msgf("failed to unmarshal default config: %v", err) } if err := writeConfigFile(); err != nil { - mainLog.Fatal().Msgf("failed to write default config file: %v", err) + mainLog.Load().Fatal().Msgf("failed to write default config file: %v", err) } else { fp, err := filepath.Abs(defaultConfigFile) if err != nil { - mainLog.Fatal().Msgf("failed to get default config file path: %v", err) + mainLog.Load().Fatal().Msgf("failed to get default config file path: %v", err) } - mainLog.Info().Msg("writing default config file to: " + fp) + mainLog.Load().Info().Msg("writing default config file to: " + fp) } defaultConfigWritten = true return false @@ -879,13 +880,13 @@ func readConfigFile(writeDefaultConfig bool) bool { var i any if err, ok := toml.NewDecoder(f).Decode(&i).(*toml.DecodeError); ok { row, col := err.Position() - mainLog.Fatal().Msgf("failed to decode config file at line: %d, column: %d, error: %v", row, col, err) + mainLog.Load().Fatal().Msgf("failed to decode config file at line: %d, column: %d, error: %v", row, col, err) } } } // Otherwise, report fatal error and exit. - mainLog.Fatal().Msgf("failed to decode config file: %v", err) + mainLog.Load().Fatal().Msgf("failed to decode config file: %v", err) return false } @@ -895,7 +896,7 @@ func readBase64Config(configBase64 string) { } configStr, err := base64.StdEncoding.DecodeString(configBase64) if err != nil { - mainLog.Fatal().Msgf("invalid base64 config: %v", err) + mainLog.Load().Fatal().Msgf("invalid base64 config: %v", err) } // readBase64Config is called when: @@ -907,7 +908,7 @@ func readBase64Config(configBase64 string) { v = viper.NewWithOptions(viper.KeyDelimiter("::")) v.SetConfigType("toml") if err := v.ReadConfig(bytes.NewReader(configStr)); err != nil { - mainLog.Fatal().Msgf("failed to read base64 config: %v", err) + mainLog.Load().Fatal().Msgf("failed to read base64 config: %v", err) } } @@ -916,7 +917,7 @@ func processNoConfigFlags(noConfigStart bool) { return } if listenAddress == "" || primaryUpstream == "" { - mainLog.Fatal().Msg(`"listen" and "primary_upstream" flags must be set in no config mode`) + mainLog.Load().Fatal().Msg(`"listen" and "primary_upstream" flags must be set in no config mode`) } processListenFlag() @@ -952,7 +953,7 @@ func processNoConfigFlags(noConfigStart bool) { } func processCDFlags() { - logger := mainLog.With().Str("mode", "cd").Logger() + logger := mainLog.Load().With().Str("mode", "cd").Logger() logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID) resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev) if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode { @@ -993,7 +994,7 @@ func processCDFlags() { logger.Info().Msg("using defined custom config of Control-D resolver") readBase64Config(resolverConfig.Ctrld.CustomConfig) if err := v.Unmarshal(&cfg); err != nil { - mainLog.Fatal().Msgf("failed to unmarshal config: %v", err) + mainLog.Load().Fatal().Msgf("failed to unmarshal config: %v", err) } } else { cfg.Network = make(map[string]*ctrld.NetworkConfig) @@ -1034,11 +1035,11 @@ func processListenFlag() { } host, portStr, err := net.SplitHostPort(listenAddress) if err != nil { - mainLog.Fatal().Msgf("invalid listener address: %v", err) + mainLog.Load().Fatal().Msgf("invalid listener address: %v", err) } port, err := strconv.Atoi(portStr) if err != nil { - mainLog.Fatal().Msgf("invalid port number: %v", err) + mainLog.Load().Fatal().Msgf("invalid port number: %v", err) } lc := &ctrld.ListenerConfig{ IP: host, @@ -1094,7 +1095,7 @@ func defaultIfaceName() string { if oi := osinfo.New(); strings.Contains(oi.String(), "Microsoft") { return "lo" } - mainLog.Fatal().Err(err).Msg("failed to get default route interface") + mainLog.Load().Fatal().Err(err).Msg("failed to get default route interface") } return dri } @@ -1106,7 +1107,7 @@ func selfCheckStatus(status service.Status, domain string) service.Status { } dir, err := userHomeDir() if err != nil { - mainLog.Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory") + mainLog.Load().Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory") return service.StatusUnknown } @@ -1115,7 +1116,7 @@ func selfCheckStatus(status service.Status, domain string) service.Status { ctx := context.Background() maxAttempts := 20 - mainLog.Debug().Msg("waiting for ctrld listener to be ready") + mainLog.Load().Debug().Msg("waiting for ctrld listener to be ready") cc := newControlClient(filepath.Join(dir, ctrldControlUnixSock)) // The socket control server may not start yet, so attempt to ping @@ -1129,17 +1130,17 @@ func selfCheckStatus(status service.Status, domain string) service.Status { } resp, err := cc.post(startedPath, nil) if err != nil { - mainLog.Error().Err(err).Msg("failed to connect to control server") + mainLog.Load().Error().Err(err).Msg("failed to connect to control server") return service.StatusUnknown } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - mainLog.Error().Msg("ctrld listener is not ready") + mainLog.Load().Error().Msg("ctrld listener is not ready") return service.StatusUnknown } - mainLog.Debug().Msg("ctrld listener is ready") - mainLog.Debug().Msg("performing self-check") + mainLog.Load().Debug().Msg("ctrld listener is ready") + mainLog.Load().Debug().Msg("performing self-check") bo = backoff.NewBackoff("self-check", logf, 10*time.Second) bo.LogLongerThan = 500 * time.Millisecond c := new(dns.Client) @@ -1149,14 +1150,14 @@ func selfCheckStatus(status service.Status, domain string) service.Status { ) if err := v.ReadInConfig(); err != nil { - mainLog.Fatal().Err(err).Msg("failed to read new config") + mainLog.Load().Fatal().Err(err).Msg("failed to read new config") } if err := v.Unmarshal(&cfg); err != nil { - mainLog.Fatal().Err(err).Msg("failed to update new config") + mainLog.Load().Fatal().Err(err).Msg("failed to update new config") } watcher, err := fsnotify.NewWatcher() if err != nil { - mainLog.Error().Err(err).Msg("could not watch config change") + mainLog.Load().Error().Err(err).Msg("could not watch config change") return service.StatusUnknown } defer watcher.Close() @@ -1165,7 +1166,7 @@ func selfCheckStatus(status service.Status, domain string) service.Status { mu.Lock() defer mu.Unlock() if err := v.UnmarshalKey("listener", &lcChanged); err != nil { - mainLog.Error().Msgf("failed to unmarshal listener config: %v", err) + mainLog.Load().Error().Msgf("failed to unmarshal listener config: %v", err) return } }) @@ -1187,27 +1188,27 @@ func selfCheckStatus(status service.Status, domain string) service.Status { m.RecursionDesired = true r, _, err := c.ExchangeContext(ctx, m, net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port))) if r != nil && r.Rcode == dns.RcodeSuccess && len(r.Answer) > 0 { - mainLog.Debug().Msgf("self-check against %q succeeded", domain) + mainLog.Load().Debug().Msgf("self-check against %q succeeded", domain) return status } lastAnswer = r lastErr = err bo.BackOff(ctx, fmt.Errorf("ExchangeContext: %w", err)) } - mainLog.Debug().Msgf("self-check against %q failed", domain) + mainLog.Load().Debug().Msgf("self-check against %q failed", domain) lc := cfg.FirstListener() addr := net.JoinHostPort(lc.IP, strconv.Itoa(lc.Port)) marker := strings.Repeat("=", 32) - mainLog.Debug().Msg(marker) - mainLog.Debug().Msgf("listener address : %s", addr) - mainLog.Debug().Msgf("last error : %v", lastErr) + mainLog.Load().Debug().Msg(marker) + mainLog.Load().Debug().Msgf("listener address : %s", addr) + mainLog.Load().Debug().Msgf("last error : %v", lastErr) if lastAnswer != nil { - mainLog.Debug().Msgf("last answer from ctrld :") - mainLog.Debug().Msg(marker) + mainLog.Load().Debug().Msgf("last answer from ctrld :") + mainLog.Load().Debug().Msg(marker) for _, s := range strings.Split(lastAnswer.String(), "\n") { - mainLog.Debug().Msgf("%s", s) + mainLog.Load().Debug().Msgf("%s", s) } - mainLog.Debug().Msg(marker) + mainLog.Load().Debug().Msg(marker) } return service.StatusUnknown } @@ -1260,7 +1261,7 @@ func readConfig(writeDefaultConfig bool) { dir, err := userHomeDir() if err != nil { - mainLog.Fatal().Msgf("failed to get user home dir: %v", err) + mainLog.Load().Fatal().Msgf("failed to get user home dir: %v", err) } for _, config := range configs { ctrld.SetConfigNameWithPath(v, config.name, dir) @@ -1279,20 +1280,20 @@ func uninstall(p *prog, s service.Service) { initLogging() if doTasks(tasks) { if err := p.router.ConfigureService(svcConfig); err != nil { - mainLog.Fatal().Err(err).Msg("could not configure service") + mainLog.Load().Fatal().Err(err).Msg("could not configure service") } if err := p.router.Uninstall(svcConfig); err != nil { - mainLog.Warn().Err(err).Msg("post uninstallation failed, please check system/service log for details error") + mainLog.Load().Warn().Err(err).Msg("post uninstallation failed, please check system/service log for details error") return } p.resetDNS() if router.Name() != "" { - mainLog.Debug().Msg("Router cleanup") + mainLog.Load().Debug().Msg("Router cleanup") } // Stop already did router.Cleanup and report any error if happens, // ignoring error here to prevent false positive. _ = p.router.Cleanup() - mainLog.Notice().Msg("Service uninstalled") + mainLog.Load().Notice().Msg("Service uninstalled") return } } @@ -1305,7 +1306,7 @@ func validateConfig(cfg *ctrld.Config) { var ve validator.ValidationErrors if errors.As(err, &ve) { for _, fe := range ve { - mainLog.Error().Msgf("invalid config: %s: %s", fe.Namespace(), fieldErrorMsg(fe)) + mainLog.Load().Error().Msgf("invalid config: %s: %s", fe.Namespace(), fieldErrorMsg(fe)) } } os.Exit(1) @@ -1472,14 +1473,14 @@ func updateListenerConfig() { maxAttempts := 10 for { if attempts == maxAttempts { - logMsg(mainLog.Fatal(), n, "could not find available listen ip and port") + logMsg(mainLog.Load().Fatal(), n, "could not find available listen ip and port") } addr := net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)) if listenOk(addr) { break } if !check.IP && !check.Port { - logMsg(mainLog.Fatal(), n, "failed to listen on: %s", addr) + logMsg(mainLog.Load().Fatal(), n, "failed to listen on: %s", addr) } if tryAllPort53 { tryAllPort53 = false @@ -1490,7 +1491,7 @@ func updateListenerConfig() { listener.Port = 53 } if check.IP { - logMsg(mainLog.Warn(), n, "could not listen on address: %s, trying: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, trying: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) } continue } @@ -1503,7 +1504,7 @@ func updateListenerConfig() { listener.Port = 53 } if check.IP { - logMsg(mainLog.Warn(), n, "could not listen on address: %s, trying localhost: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, trying localhost: %s", addr, net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) } continue } @@ -1515,7 +1516,7 @@ func updateListenerConfig() { if check.Port { listener.Port = 5354 } - logMsg(mainLog.Warn(), n, "could not listen on address: %s, trying current ip with port 5354", addr) + logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, trying current ip with port 5354", addr) continue } if tryPort5354 { @@ -1526,7 +1527,7 @@ func updateListenerConfig() { if check.Port { listener.Port = 5354 } - logMsg(mainLog.Warn(), n, "could not listen on address: %s, trying 0.0.0.0:5354", addr) + logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, trying 0.0.0.0:5354", addr) continue } if check.IP && !isZeroIP { // for "0.0.0.0" or "::", we only need to try new port. @@ -1540,9 +1541,9 @@ func updateListenerConfig() { listener.Port = oldPort } if listener.IP == oldIP && listener.Port == oldPort { - logMsg(mainLog.Fatal(), n, "could not listener on: %s", net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + logMsg(mainLog.Load().Fatal(), n, "could not listener on: %s", net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) } - logMsg(mainLog.Warn(), n, "could not listen on address: %s, pick a random ip+port", addr) + logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, pick a random ip+port", addr) attempts++ } } @@ -1555,7 +1556,7 @@ func updateListenerConfig() { // ip address, other than "127.0.0.1", so trying to listen on default route interface // address instead. if ip := net.ParseIP(listener.IP); ip != nil && ip.IsLoopback() && ip.String() != "127.0.0.1" { - logMsg(mainLog.Warn(), n, "using loopback interface do not work with systemd-resolved") + logMsg(mainLog.Load().Warn(), n, "using loopback interface do not work with systemd-resolved") found := false if netIface, _ := net.InterfaceByName(defaultIfaceName()); netIface != nil { addrs, _ := netIface.Addrs() @@ -1565,14 +1566,14 @@ func updateListenerConfig() { if listenOk(addr) { found = true listener.IP = netIP.IP.String() - logMsg(mainLog.Warn(), n, "use %s as listener address", listener.IP) + logMsg(mainLog.Load().Warn(), n, "use %s as listener address", listener.IP) break } } } } if !found { - logMsg(mainLog.Fatal(), n, "could not use %q as DNS nameserver with systemd resolved", listener.IP) + logMsg(mainLog.Load().Fatal(), n, "could not use %q as DNS nameserver with systemd resolved", listener.IP) } } } diff --git a/cmd/ctrld/dns_proxy.go b/cmd/ctrld/dns_proxy.go index ba0c3be..81a373e 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/ctrld/dns_proxy.go @@ -44,7 +44,7 @@ func (p *prog) serveDNS(listenerNum string) error { listenerConfig := p.cfg.Listener[listenerNum] // make sure ip is allocated if allocErr := p.allocateIP(listenerConfig.IP); allocErr != nil { - mainLog.Error().Err(allocErr).Str("ip", listenerConfig.IP).Msg("serveUDP: failed to allocate listen ip") + mainLog.Load().Error().Err(allocErr).Str("ip", listenerConfig.IP).Msg("serveUDP: failed to allocate listen ip") return allocErr } var failoverRcodes []int @@ -64,7 +64,7 @@ func (p *prog) serveDNS(listenerNum string) error { fmtSrcToDest := fmtRemoteToLocal(listenerNum, remoteAddr.String(), w.LocalAddr().String()) t := time.Now() ctx := context.WithValue(context.Background(), ctrld.ReqIdCtxKey{}, reqId) - ctrld.Log(ctx, mainLog.Debug(), "%s received query: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain) + ctrld.Log(ctx, mainLog.Load().Debug(), "%s received query: %s %s", fmtSrcToDest, dns.TypeToString[q.Qtype], domain) upstreams, matched := p.upstreamFor(ctx, listenerNum, listenerConfig, remoteAddr, domain) var answer *dns.Msg if !matched && listenerConfig.Restricted { @@ -73,10 +73,10 @@ func (p *prog) serveDNS(listenerNum string) error { } else { answer = p.proxy(ctx, upstreams, failoverRcodes, m, ci) rtt := time.Since(t) - ctrld.Log(ctx, mainLog.Debug(), "received response of %d bytes in %s", answer.Len(), rtt) + ctrld.Log(ctx, mainLog.Load().Debug(), "received response of %d bytes in %s", answer.Len(), rtt) } if err := w.WriteMsg(answer); err != nil { - ctrld.Log(ctx, mainLog.Error().Err(err), "serveUDP: failed to send DNS response to client") + ctrld.Log(ctx, mainLog.Load().Error().Err(err), "serveUDP: failed to send DNS response to client") } }) @@ -93,7 +93,7 @@ func (p *prog) serveDNS(listenerNum string) error { case err := <-errCh: // Local ipv6 listener should not terminate ctrld. // It's a workaround for a quirk on Windows. - mainLog.Warn().Err(err).Msg("local ipv6 listener failed") + mainLog.Load().Warn().Err(err).Msg("local ipv6 listener failed") } return nil }) @@ -113,7 +113,7 @@ func (p *prog) serveDNS(listenerNum string) error { case err := <-errCh: // RFC1918 listener should not terminate ctrld. // It's a workaround for a quirk on system with systemd-resolved. - mainLog.Warn().Err(err).Msgf("could not listen on %s: %s", proto, listenAddr) + mainLog.Load().Warn().Err(err).Msgf("could not listen on %s: %s", proto, listenAddr) } }() } @@ -157,13 +157,13 @@ func (p *prog) upstreamFor(ctx context.Context, defaultUpstreamNum string, lc *c defer func() { if !matched && lc.Restricted { - ctrld.Log(ctx, mainLog.Info(), "query refused, %s does not match any network policy", addr.String()) + ctrld.Log(ctx, mainLog.Load().Info(), "query refused, %s does not match any network policy", addr.String()) return } if matched { - ctrld.Log(ctx, mainLog.Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams) + ctrld.Log(ctx, mainLog.Load().Info(), "%s, %s, %s -> %v", matchedPolicy, matchedNetwork, matchedRule, upstreams) } else { - ctrld.Log(ctx, mainLog.Info(), "no explicit policy matched, using default routing -> %v", upstreams) + ctrld.Log(ctx, mainLog.Load().Info(), "no explicit policy matched, using default routing -> %v", upstreams) } }() @@ -246,7 +246,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i answer.SetRcode(msg, answer.Rcode) now := time.Now() if cachedValue.Expire.After(now) { - ctrld.Log(ctx, mainLog.Debug(), "hit cached response") + ctrld.Log(ctx, mainLog.Load().Debug(), "hit cached response") setCachedAnswerTTL(answer, now, cachedValue.Expire) return answer } @@ -254,10 +254,10 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i } } resolve1 := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) (*dns.Msg, error) { - ctrld.Log(ctx, mainLog.Debug(), "sending query to %s: %s", upstreams[n], upstreamConfig.Name) + ctrld.Log(ctx, mainLog.Load().Debug(), "sending query to %s: %s", upstreams[n], upstreamConfig.Name) dnsResolver, err := ctrld.NewResolver(upstreamConfig) if err != nil { - ctrld.Log(ctx, mainLog.Error().Err(err), "failed to create resolver") + ctrld.Log(ctx, mainLog.Load().Error().Err(err), "failed to create resolver") return nil, err } resolveCtx, cancel := context.WithCancel(ctx) @@ -271,12 +271,12 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i } resolve := func(n int, upstreamConfig *ctrld.UpstreamConfig, msg *dns.Msg) *dns.Msg { if upstreamConfig.UpstreamSendClientInfo() && ci != nil { - ctrld.Log(ctx, mainLog.Debug(), "including client info with the request") + ctrld.Log(ctx, mainLog.Load().Debug(), "including client info with the request") ctx = context.WithValue(ctx, ctrld.ClientInfoCtxKey{}, ci) } answer, err := resolve1(n, upstreamConfig, msg) if err != nil { - ctrld.Log(ctx, mainLog.Error().Err(err), "failed to resolve query") + ctrld.Log(ctx, mainLog.Load().Error().Err(err), "failed to resolve query") return nil } return answer @@ -288,7 +288,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i answer := resolve(n, upstreamConfig, msg) if answer == nil { if serveStaleCache && staleAnswer != nil { - ctrld.Log(ctx, mainLog.Debug(), "serving stale cached response") + ctrld.Log(ctx, mainLog.Load().Debug(), "serving stale cached response") now := time.Now() setCachedAnswerTTL(staleAnswer, now, now.Add(staleTTL)) return staleAnswer @@ -296,7 +296,7 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i continue } if answer.Rcode != dns.RcodeSuccess && len(upstreamConfigs) > 1 && containRcode(failoverRcodes, answer.Rcode) { - ctrld.Log(ctx, mainLog.Debug(), "failover rcode matched, process to next upstream") + ctrld.Log(ctx, mainLog.Load().Debug(), "failover rcode matched, process to next upstream") continue } @@ -312,11 +312,11 @@ func (p *prog) proxy(ctx context.Context, upstreams []string, failoverRcodes []i } setCachedAnswerTTL(answer, now, expired) p.cache.Add(dnscache.NewKey(msg, upstreams[n]), dnscache.NewValue(answer, expired)) - ctrld.Log(ctx, mainLog.Debug(), "add cached response") + ctrld.Log(ctx, mainLog.Load().Debug(), "add cached response") } return answer } - ctrld.Log(ctx, mainLog.Error(), "all upstreams failed") + ctrld.Log(ctx, mainLog.Load().Error(), "all upstreams failed") answer := new(dns.Msg) answer.SetRcode(msg, dns.RcodeServerFailure) return answer @@ -490,7 +490,7 @@ func runDNSServer(addr, network string, handler dns.Handler) (*dns.Server, <-cha defer close(errCh) if err := s.ListenAndServe(); err != nil { waitLock.Unlock() - mainLog.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 } }() diff --git a/cmd/ctrld/main.go b/cmd/ctrld/main.go index 2573f6e..75e7d2b 100644 --- a/cmd/ctrld/main.go +++ b/cmd/ctrld/main.go @@ -4,6 +4,7 @@ import ( "io" "os" "path/filepath" + "sync/atomic" "time" "github.com/kardianos/service" @@ -31,15 +32,20 @@ var ( iface string ifaceStartStop string - mainLog = zerolog.New(io.Discard) + mainLog atomic.Pointer[zerolog.Logger] consoleWriter zerolog.ConsoleWriter ) +func init() { + l := zerolog.New(io.Discard) + mainLog.Store(&l) +} + func main() { ctrld.InitConfig(v, "ctrld") initCLI() if err := rootCmd.Execute(); err != nil { - mainLog.Error().Msg(err.Error()) + mainLog.Load().Error().Msg(err.Error()) os.Exit(1) } } @@ -63,7 +69,8 @@ func initConsoleLogging() { w.TimeFormat = time.StampMilli }) multi := zerolog.MultiLevelWriter(consoleWriter) - mainLog = mainLog.Output(multi).With().Timestamp().Logger() + l := mainLog.Load().Output(multi).With().Timestamp().Logger() + mainLog.Store(&l) switch { case silent: zerolog.SetGlobalLevel(zerolog.NoLevel) @@ -92,7 +99,7 @@ func initLoggingWithBackup(doBackup bool) { if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" { // Create parent directory if necessary. if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil { - mainLog.Error().Msgf("failed to create log path: %v", err) + mainLog.Load().Error().Msgf("failed to create log path: %v", err) os.Exit(1) } @@ -101,7 +108,7 @@ func initLoggingWithBackup(doBackup bool) { 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) + mainLog.Load().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 @@ -109,16 +116,17 @@ func initLoggingWithBackup(doBackup bool) { } logFile, err := os.OpenFile(logFilePath, flags, os.FileMode(0o600)) if err != nil { - mainLog.Error().Msgf("failed to create log file: %v", err) + mainLog.Load().Error().Msgf("failed to create log file: %v", err) os.Exit(1) } writers = append(writers, logFile) } writers = append(writers, consoleWriter) multi := zerolog.MultiLevelWriter(writers...) - mainLog = mainLog.Output(multi).With().Timestamp().Logger() + l := mainLog.Load().Output(multi).With().Timestamp().Logger() + mainLog.Store(&l) // TODO: find a better way. - ctrld.ProxyLog = mainLog + ctrld.ProxyLogger.Store(&l) zerolog.SetGlobalLevel(zerolog.NoticeLevel) logLevel := cfg.Service.LogLevel @@ -136,7 +144,7 @@ func initLoggingWithBackup(doBackup bool) { } level, err := zerolog.ParseLevel(logLevel) if err != nil { - mainLog.Warn().Err(err).Msg("could not set log level") + mainLog.Load().Warn().Err(err).Msg("could not set log level") return } zerolog.SetGlobalLevel(level) diff --git a/cmd/ctrld/main_test.go b/cmd/ctrld/main_test.go index 2a2e079..9654fb6 100644 --- a/cmd/ctrld/main_test.go +++ b/cmd/ctrld/main_test.go @@ -11,6 +11,7 @@ import ( var logOutput strings.Builder func TestMain(m *testing.M) { - mainLog = zerolog.New(&logOutput) + l := zerolog.New(&logOutput) + mainLog.Store(&l) os.Exit(m.Run()) } diff --git a/cmd/ctrld/net_darwin.go b/cmd/ctrld/net_darwin.go index 0939c85..f0f7e5a 100644 --- a/cmd/ctrld/net_darwin.go +++ b/cmd/ctrld/net_darwin.go @@ -17,7 +17,7 @@ func patchNetIfaceName(iface *net.Interface) error { if name := networkServiceName(iface.Name, bytes.NewReader(b)); name != "" { iface.Name = name - mainLog.Debug().Str("network_service", name).Msg("found network service name for interface") + mainLog.Load().Debug().Str("network_service", name).Msg("found network service name for interface") } return nil } diff --git a/cmd/ctrld/netlink_linux.go b/cmd/ctrld/netlink_linux.go index 86eb45b..7657fc3 100644 --- a/cmd/ctrld/netlink_linux.go +++ b/cmd/ctrld/netlink_linux.go @@ -10,7 +10,7 @@ func (p *prog) watchLinkState() { done := make(chan struct{}) defer close(done) if err := netlink.LinkSubscribe(ch, done); err != nil { - mainLog.Warn().Err(err).Msg("could not subscribe link") + mainLog.Load().Warn().Err(err).Msg("could not subscribe link") return } for lu := range ch { @@ -18,7 +18,7 @@ func (p *prog) watchLinkState() { continue } if lu.Change&unix.IFF_UP != 0 { - mainLog.Debug().Msgf("link state changed, re-bootstrapping") + mainLog.Load().Debug().Msgf("link state changed, re-bootstrapping") for _, uc := range p.cfg.Upstream { uc.ReBootstrap() } diff --git a/cmd/ctrld/network_manager_linux.go b/cmd/ctrld/network_manager_linux.go index fe00f3a..799c2dc 100644 --- a/cmd/ctrld/network_manager_linux.go +++ b/cmd/ctrld/network_manager_linux.go @@ -24,37 +24,37 @@ var networkManagerCtrldConfFile = filepath.Join(nmConfDir, nmCtrldConfFilename) func setupNetworkManager() error { if content, _ := os.ReadFile(nmCtrldConfContent); string(content) == nmCtrldConfContent { - mainLog.Debug().Msg("NetworkManager already setup, nothing to do") + mainLog.Load().Debug().Msg("NetworkManager already setup, nothing to do") return nil } err := os.WriteFile(networkManagerCtrldConfFile, []byte(nmCtrldConfContent), os.FileMode(0644)) if os.IsNotExist(err) { - mainLog.Debug().Msg("NetworkManager is not available") + mainLog.Load().Debug().Msg("NetworkManager is not available") return nil } if err != nil { - mainLog.Debug().Err(err).Msg("could not write NetworkManager ctrld config file") + mainLog.Load().Debug().Err(err).Msg("could not write NetworkManager ctrld config file") return err } reloadNetworkManager() - mainLog.Debug().Msg("setup NetworkManager done") + mainLog.Load().Debug().Msg("setup NetworkManager done") return nil } func restoreNetworkManager() error { err := os.Remove(networkManagerCtrldConfFile) if os.IsNotExist(err) { - mainLog.Debug().Msg("NetworkManager is not available") + mainLog.Load().Debug().Msg("NetworkManager is not available") return nil } if err != nil { - mainLog.Debug().Err(err).Msg("could not remove NetworkManager ctrld config file") + mainLog.Load().Debug().Err(err).Msg("could not remove NetworkManager ctrld config file") return err } reloadNetworkManager() - mainLog.Debug().Msg("restore NetworkManager done") + mainLog.Load().Debug().Msg("restore NetworkManager done") return nil } @@ -63,14 +63,14 @@ func reloadNetworkManager() { defer cancel() conn, err := dbus.NewSystemConnectionContext(ctx) if err != nil { - mainLog.Error().Err(err).Msg("could not create new system connection") + mainLog.Load().Error().Err(err).Msg("could not create new system connection") return } defer conn.Close() waitCh := make(chan string) if _, err := conn.ReloadUnitContext(ctx, nmSystemdUnitName, "ignore-dependencies", waitCh); err != nil { - mainLog.Debug().Err(err).Msg("could not reload NetworkManager") + mainLog.Load().Debug().Err(err).Msg("could not reload NetworkManager") } <-waitCh } diff --git a/cmd/ctrld/os_darwin.go b/cmd/ctrld/os_darwin.go index 04bc66b..ac872d8 100644 --- a/cmd/ctrld/os_darwin.go +++ b/cmd/ctrld/os_darwin.go @@ -12,7 +12,7 @@ import ( func allocateIP(ip string) error { cmd := exec.Command("ifconfig", "lo0", "alias", ip, "up") if err := cmd.Run(); err != nil { - mainLog.Error().Err(err).Msg("allocateIP failed") + mainLog.Load().Error().Err(err).Msg("allocateIP failed") return err } return nil @@ -21,7 +21,7 @@ func allocateIP(ip string) error { func deAllocateIP(ip string) error { cmd := exec.Command("ifconfig", "lo0", "-alias", ip) if err := cmd.Run(); err != nil { - mainLog.Error().Err(err).Msg("deAllocateIP failed") + mainLog.Load().Error().Err(err).Msg("deAllocateIP failed") return err } return nil @@ -36,7 +36,7 @@ func setDNS(iface *net.Interface, nameservers []string) error { args = append(args, nameservers...) if err := exec.Command(cmd, args...).Run(); err != nil { - mainLog.Error().Err(err).Msgf("setDNS failed, ips = %q", nameservers) + mainLog.Load().Error().Err(err).Msgf("setDNS failed, ips = %q", nameservers) return err } return nil @@ -48,7 +48,7 @@ func resetDNS(iface *net.Interface) error { args := []string{"-setdnsservers", iface.Name, "empty"} if err := exec.Command(cmd, args...).Run(); err != nil { - mainLog.Error().Err(err).Msgf("resetDNS failed") + mainLog.Load().Error().Err(err).Msgf("resetDNS failed") return err } return nil diff --git a/cmd/ctrld/os_freebsd.go b/cmd/ctrld/os_freebsd.go index da1a05a..5a1fa36 100644 --- a/cmd/ctrld/os_freebsd.go +++ b/cmd/ctrld/os_freebsd.go @@ -14,7 +14,7 @@ import ( func allocateIP(ip string) error { cmd := exec.Command("ifconfig", "lo0", ip, "alias") if err := cmd.Run(); err != nil { - mainLog.Error().Err(err).Msg("allocateIP failed") + mainLog.Load().Error().Err(err).Msg("allocateIP failed") return err } return nil @@ -23,7 +23,7 @@ func allocateIP(ip string) error { func deAllocateIP(ip string) error { cmd := exec.Command("ifconfig", "lo0", ip, "-alias") if err := cmd.Run(); err != nil { - mainLog.Error().Err(err).Msg("deAllocateIP failed") + mainLog.Load().Error().Err(err).Msg("deAllocateIP failed") return err } return nil @@ -33,7 +33,7 @@ func deAllocateIP(ip string) error { func setDNS(iface *net.Interface, nameservers []string) error { r, err := dns.NewOSConfigurator(logf, iface.Name) if err != nil { - mainLog.Error().Err(err).Msg("failed to create DNS OS configurator") + mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator") return err } @@ -43,7 +43,7 @@ func setDNS(iface *net.Interface, nameservers []string) error { } if err := r.SetDNS(dns.OSConfig{Nameservers: ns}); err != nil { - mainLog.Error().Err(err).Msg("failed to set DNS") + mainLog.Load().Error().Err(err).Msg("failed to set DNS") return err } return nil @@ -52,12 +52,12 @@ func setDNS(iface *net.Interface, nameservers []string) error { func resetDNS(iface *net.Interface) error { r, err := dns.NewOSConfigurator(logf, iface.Name) if err != nil { - mainLog.Error().Err(err).Msg("failed to create DNS OS configurator") + mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator") return err } if err := r.Close(); err != nil { - mainLog.Error().Err(err).Msg("failed to rollback DNS setting") + mainLog.Load().Error().Err(err).Msg("failed to rollback DNS setting") return err } return nil diff --git a/cmd/ctrld/os_linux.go b/cmd/ctrld/os_linux.go index 570cabc..e26e396 100644 --- a/cmd/ctrld/os_linux.go +++ b/cmd/ctrld/os_linux.go @@ -29,7 +29,7 @@ import ( func allocateIP(ip string) error { cmd := exec.Command("ip", "a", "add", ip+"/24", "dev", "lo") if out, err := cmd.CombinedOutput(); err != nil { - mainLog.Error().Err(err).Msgf("allocateIP failed: %s", string(out)) + mainLog.Load().Error().Err(err).Msgf("allocateIP failed: %s", string(out)) return err } return nil @@ -38,7 +38,7 @@ func allocateIP(ip string) error { func deAllocateIP(ip string) error { cmd := exec.Command("ip", "a", "del", ip+"/24", "dev", "lo") if err := cmd.Run(); err != nil { - mainLog.Error().Err(err).Msg("deAllocateIP failed") + mainLog.Load().Error().Err(err).Msg("deAllocateIP failed") return err } return nil @@ -50,7 +50,7 @@ const maxSetDNSAttempts = 5 func setDNS(iface *net.Interface, nameservers []string) error { r, err := dns.NewOSConfigurator(logf, iface.Name) if err != nil { - mainLog.Error().Err(err).Msg("failed to create DNS OS configurator") + mainLog.Load().Error().Err(err).Msg("failed to create DNS OS configurator") return err } @@ -69,7 +69,7 @@ func setDNS(iface *net.Interface, nameservers []string) error { if err := r.SetDNS(osConfig); err != nil { if strings.Contains(err.Error(), "Rejected send message") && strings.Contains(err.Error(), "org.freedesktop.network1.Manager") { - mainLog.Warn().Msg("Interfaces are managed by systemd-networkd, switch to systemd-resolve for setting DNS") + mainLog.Load().Warn().Msg("Interfaces are managed by systemd-networkd, switch to systemd-resolve for setting DNS") trySystemdResolve = true break } @@ -100,7 +100,7 @@ func setDNS(iface *net.Interface, nameservers []string) error { time.Sleep(time.Second) } } - mainLog.Debug().Msg("DNS was not set for some reason") + mainLog.Load().Debug().Msg("DNS was not set for some reason") return nil } @@ -116,7 +116,7 @@ func resetDNS(iface *net.Interface) (err error) { if r, oerr := dns.NewOSConfigurator(logf, iface.Name); oerr == nil { _ = r.SetDNS(dns.OSConfig{}) if err := r.Close(); err != nil { - mainLog.Error().Err(err).Msg("failed to rollback DNS setting") + mainLog.Load().Error().Err(err).Msg("failed to rollback DNS setting") return } err = nil @@ -148,13 +148,13 @@ func resetDNS(iface *net.Interface) (err error) { c := client6.NewClient() conversation, err := c.Exchange(iface.Name) if err != nil && !errAddrInUse(err) { - mainLog.Debug().Err(err).Msg("could not exchange DHCPv6") + mainLog.Load().Debug().Err(err).Msg("could not exchange DHCPv6") } for _, packet := range conversation { if packet.Type() == dhcpv6.MessageTypeReply { msg, err := packet.GetInnerMessage() if err != nil { - mainLog.Debug().Err(err).Msg("could not get inner DHCPv6 message") + mainLog.Load().Debug().Err(err).Msg("could not get inner DHCPv6 message") return nil } nameservers := msg.Options.DNS() diff --git a/cmd/ctrld/os_windows.go b/cmd/ctrld/os_windows.go index 8858027..f96e224 100644 --- a/cmd/ctrld/os_windows.go +++ b/cmd/ctrld/os_windows.go @@ -30,12 +30,12 @@ func setDNS(iface *net.Interface, nameservers []string) error { func resetDNS(iface *net.Interface) error { if ctrldnet.SupportsIPv6ListenLocal() { if output, err := netsh("interface", "ipv6", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp"); err != nil { - mainLog.Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output)) + mainLog.Load().Warn().Err(err).Msgf("failed to reset ipv6 DNS: %s", string(output)) } } output, err := netsh("interface", "ipv4", "set", "dnsserver", strconv.Itoa(iface.Index), "dhcp") if err != nil { - mainLog.Error().Err(err).Msgf("failed to reset ipv4 DNS: %s", string(output)) + mainLog.Load().Error().Err(err).Msgf("failed to reset ipv4 DNS: %s", string(output)) return err } return nil @@ -49,7 +49,7 @@ func setPrimaryDNS(iface *net.Interface, dns string) error { idx := strconv.Itoa(iface.Index) output, err := netsh("interface", ipVer, "set", "dnsserver", idx, "static", dns) if err != nil { - mainLog.Error().Err(err).Msgf("failed to set primary DNS: %s", string(output)) + mainLog.Load().Error().Err(err).Msgf("failed to set primary DNS: %s", string(output)) return err } if ipVer == "ipv4" && ctrldnet.SupportsIPv6ListenLocal() { @@ -67,7 +67,7 @@ func addSecondaryDNS(iface *net.Interface, dns string) error { } output, err := netsh("interface", ipVer, "add", "dns", strconv.Itoa(iface.Index), dns, "index=2") if err != nil { - mainLog.Warn().Err(err).Msgf("failed to add secondary DNS: %s", string(output)) + mainLog.Load().Warn().Err(err).Msgf("failed to add secondary DNS: %s", string(output)) } return nil } @@ -79,12 +79,12 @@ func netsh(args ...string) ([]byte, error) { func currentDNS(iface *net.Interface) []string { luid, err := winipcfg.LUIDFromIndex(uint32(iface.Index)) if err != nil { - mainLog.Error().Err(err).Msg("failed to get interface LUID") + mainLog.Load().Error().Err(err).Msg("failed to get interface LUID") return nil } nameservers, err := luid.DNS() if err != nil { - mainLog.Error().Err(err).Msg("failed to get interface DNS") + mainLog.Load().Error().Err(err).Msg("failed to get interface DNS") return nil } ns := make([]string, 0, len(nameservers)) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index dce7c03..4aff917 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -26,7 +26,7 @@ const ( ) var logf = func(format string, args ...any) { - mainLog.Debug().Msgf(format, args...) + mainLog.Load().Debug().Msgf(format, args...) } var svcConfig = &service.Config{ @@ -72,7 +72,7 @@ func (p *prog) run() { if p.cfg.Service.CacheEnable { cacher, err := dnscache.NewLRUCache(p.cfg.Service.CacheSize) if err != nil { - mainLog.Error().Err(err).Msg("failed to create cacher, caching is disabled") + mainLog.Load().Error().Err(err).Msg("failed to create cacher, caching is disabled") } else { p.cache = cacher } @@ -93,7 +93,7 @@ func (p *prog) run() { for _, cidr := range nc.Cidrs { _, ipNet, err := net.ParseCIDR(cidr) if err != nil { - mainLog.Error().Err(err).Str("network", nc.Name).Str("cidr", cidr).Msg("invalid cidr") + mainLog.Load().Error().Err(err).Str("network", nc.Name).Str("cidr", cidr).Msg("invalid cidr") continue } nc.IPNets = append(nc.IPNets, ipNet) @@ -104,9 +104,9 @@ func (p *prog) run() { uc.Init() if uc.BootstrapIP == "" { uc.SetupBootstrapIP() - mainLog.Info().Msgf("bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs()) + mainLog.Load().Info().Msgf("bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs()) } else { - mainLog.Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("using bootstrap IP for upstream.%s", n) + mainLog.Load().Info().Str("bootstrap_ip", uc.BootstrapIP).Msgf("using bootstrap IP for upstream.%s", n) } uc.SetCertPool(rootCertPool) go uc.Ping() @@ -114,7 +114,7 @@ func (p *prog) run() { p.ciTable = clientinfo.NewTable(&cfg, defaultRouteIP()) if leaseFile := p.cfg.Service.DHCPLeaseFile; leaseFile != "" { - mainLog.Debug().Msgf("watching custom lease file: %s", leaseFile) + mainLog.Load().Debug().Msgf("watching custom lease file: %s", leaseFile) format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat) p.ciTable.AddLeaseFile(leaseFile, format) } @@ -132,12 +132,12 @@ func (p *prog) run() { listenerConfig := p.cfg.Listener[listenerNum] upstreamConfig := p.cfg.Upstream[listenerNum] if upstreamConfig == nil { - mainLog.Warn().Msgf("no default upstream for: [listener.%s]", listenerNum) + mainLog.Load().Warn().Msgf("no default upstream for: [listener.%s]", listenerNum) } addr := net.JoinHostPort(listenerConfig.IP, strconv.Itoa(listenerConfig.Port)) - mainLog.Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr) + mainLog.Load().Info().Msgf("starting DNS server on listener.%s: %s", listenerNum, addr) if err := p.serveDNS(listenerNum); err != nil { - mainLog.Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum) + mainLog.Load().Fatal().Err(err).Msgf("unable to start dns proxy on listener.%s", listenerNum) } }(listenerNum) } @@ -159,17 +159,17 @@ func (p *prog) run() { if p.cs != nil { p.registerControlServerHandler() if err := p.cs.start(); err != nil { - mainLog.Warn().Err(err).Msg("could not start control server") + mainLog.Load().Warn().Err(err).Msg("could not start control server") } } wg.Wait() } func (p *prog) Stop(s service.Service) error { - mainLog.Info().Msg("Service stopped") + mainLog.Load().Info().Msg("Service stopped") close(p.stopCh) if err := p.deAllocateIP(); err != nil { - mainLog.Error().Err(err).Msg("de-allocate ip failed") + mainLog.Load().Error().Err(err).Msg("de-allocate ip failed") return err } return nil @@ -212,7 +212,7 @@ func (p *prog) setDNS() { if lc == nil { return } - logger := mainLog.With().Str("iface", iface).Logger() + logger := mainLog.Load().With().Str("iface", iface).Logger() netIface, err := netInterface(iface) if err != nil { logger.Error().Err(err).Msg("could not get interface") @@ -257,7 +257,7 @@ func (p *prog) resetDNS() { if iface == "auto" { iface = defaultIfaceName() } - logger := mainLog.With().Str("iface", iface).Logger() + logger := mainLog.Load().With().Str("iface", iface).Logger() netIface, err := netInterface(iface) if err != nil { logger.Error().Err(err).Msg("could not get interface") @@ -291,19 +291,19 @@ func randomPort() int { func runLogServer(sockPath string) net.Conn { addr, err := net.ResolveUnixAddr("unix", sockPath) if err != nil { - mainLog.Warn().Err(err).Msg("invalid log sock path") + mainLog.Load().Warn().Err(err).Msg("invalid log sock path") return nil } ln, err := net.ListenUnix("unix", addr) if err != nil { - mainLog.Warn().Err(err).Msg("could not listen log socket") + mainLog.Load().Warn().Err(err).Msg("could not listen log socket") return nil } defer ln.Close() server, err := ln.Accept() if err != nil { - mainLog.Warn().Err(err).Msg("could not accept connection") + mainLog.Load().Warn().Err(err).Msg("could not accept connection") return nil } return server diff --git a/cmd/ctrld/service.go b/cmd/ctrld/service.go index 5f6eeb2..dfec02e 100644 --- a/cmd/ctrld/service.go +++ b/cmd/ctrld/service.go @@ -105,7 +105,7 @@ func doTasks(tasks []task) bool { for _, task := range tasks { if err := task.f(); err != nil { if task.abortOnError { - mainLog.Error().Msg(errors.Join(prevErr, err).Error()) + mainLog.Load().Error().Msg(errors.Join(prevErr, err).Error()) return false } prevErr = err @@ -117,11 +117,11 @@ func doTasks(tasks []task) bool { func checkHasElevatedPrivilege() { ok, err := hasElevatedPrivilege() if err != nil { - mainLog.Error().Msgf("could not detect user privilege: %v", err) + mainLog.Load().Error().Msgf("could not detect user privilege: %v", err) return } if !ok { - mainLog.Error().Msg("Please relaunch process with admin/root privilege.") + mainLog.Load().Error().Msg("Please relaunch process with admin/root privilege.") os.Exit(1) } } diff --git a/config.go b/config.go index ff803c5..0019e00 100644 --- a/config.go +++ b/config.go @@ -330,7 +330,7 @@ func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) { if len(uc.bootstrapIPs) > 0 { break } - ProxyLog.Warn().Msg("could not resolve bootstrap IPs, retrying...") + ProxyLogger.Load().Warn().Msg("could not resolve bootstrap IPs, retrying...") b.BackOff(context.Background(), errors.New("no bootstrap IPs")) } for _, ip := range uc.bootstrapIPs { @@ -340,7 +340,7 @@ func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) { uc.bootstrapIPs4 = append(uc.bootstrapIPs4, ip) } } - ProxyLog.Debug().Msgf("bootstrap IPs: %v", uc.bootstrapIPs) + ProxyLogger.Load().Debug().Msgf("bootstrap IPs: %v", uc.bootstrapIPs) } // ReBootstrap re-setup the bootstrap IP and the transport. @@ -351,7 +351,7 @@ func (uc *UpstreamConfig) ReBootstrap() { return } _, _, _ = uc.g.Do("ReBootstrap", func() (any, error) { - ProxyLog.Debug().Msg("re-bootstrapping upstream ip") + ProxyLogger.Load().Debug().Msg("re-bootstrapping upstream ip") uc.rebootstrap.Store(true) return true, nil }) @@ -405,7 +405,7 @@ func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport { if uc.BootstrapIP != "" { dialer := net.Dialer{Timeout: dialerTimeout, KeepAlive: dialerTimeout} addr := net.JoinHostPort(uc.BootstrapIP, port) - Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", addr) + Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", addr) return dialer.DialContext(ctx, network, addr) } pd := &ctrldnet.ParallelDialer{} @@ -419,7 +419,7 @@ func (uc *UpstreamConfig) newDOHTransport(addrs []string) *http.Transport { if err != nil { return nil, err } - Log(ctx, ProxyLog.Debug(), "sending doh request to: %s", conn.RemoteAddr()) + Log(ctx, ProxyLogger.Load().Debug(), "sending doh request to: %s", conn.RemoteAddr()) return conn, nil } runtime.SetFinalizer(transport, func(transport *http.Transport) { diff --git a/config_quic.go b/config_quic.go index 32d338e..e953c72 100644 --- a/config_quic.go +++ b/config_quic.go @@ -48,7 +48,7 @@ func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper { // if we have a bootstrap ip set, use it to avoid DNS lookup if uc.BootstrapIP != "" { addr = net.JoinHostPort(uc.BootstrapIP, port) - ProxyLog.Debug().Msgf("sending doh3 request to: %s", addr) + ProxyLogger.Load().Debug().Msgf("sending doh3 request to: %s", addr) udpConn, err := net.ListenUDP("udp", nil) if err != nil { return nil, err @@ -68,7 +68,7 @@ func (uc *UpstreamConfig) newDOH3Transport(addrs []string) http.RoundTripper { if err != nil { return nil, err } - ProxyLog.Debug().Msgf("sending doh3 request to: %s", conn.RemoteAddr()) + ProxyLogger.Load().Debug().Msgf("sending doh3 request to: %s", conn.RemoteAddr()) return conn, err } return rt diff --git a/doh.go b/doh.go index f861f2f..5886881 100644 --- a/doh.go +++ b/doh.go @@ -110,5 +110,5 @@ func addHeader(ctx context.Context, req *http.Request, sendClientInfo bool) { } } } - Log(ctx, ProxyLog.Debug().Interface("header", req.Header), "sending request header") + Log(ctx, ProxyLogger.Load().Debug().Interface("header", req.Header), "sending request header") } diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 55fe0b1..f923883 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -105,7 +105,7 @@ func (t *Table) Init() { if t.discoverDHCP() || t.discoverARP() { t.merlin = &merlinDiscover{} if err := t.merlin.refresh(); err != nil { - ctrld.ProxyLog.Error().Err(err).Msg("could not init Merlin discover") + ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init Merlin discover") } else { t.hostnameResolvers = append(t.hostnameResolvers, t.merlin) t.refreshers = append(t.refreshers, t.merlin) @@ -113,9 +113,9 @@ func (t *Table) Init() { } if t.discoverDHCP() { t.dhcp = &dhcp{selfIP: t.selfIP} - ctrld.ProxyLog.Debug().Msg("start dhcp discovery") + ctrld.ProxyLogger.Load().Debug().Msg("start dhcp discovery") if err := t.dhcp.init(); err != nil { - ctrld.ProxyLog.Error().Err(err).Msg("could not init DHCP discover") + ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init DHCP discover") } else { t.ipResolvers = append(t.ipResolvers, t.dhcp) t.macResolvers = append(t.macResolvers, t.dhcp) @@ -125,9 +125,9 @@ func (t *Table) Init() { } if t.discoverARP() { t.arp = &arpDiscover{} - ctrld.ProxyLog.Debug().Msg("start arp discovery") + ctrld.ProxyLogger.Load().Debug().Msg("start arp discovery") if err := t.arp.refresh(); err != nil { - ctrld.ProxyLog.Error().Err(err).Msg("could not init ARP discover") + ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init ARP discover") } else { t.ipResolvers = append(t.ipResolvers, t.arp) t.macResolvers = append(t.macResolvers, t.arp) @@ -136,9 +136,9 @@ func (t *Table) Init() { } if t.discoverPTR() { t.ptr = &ptrDiscover{resolver: ctrld.NewPrivateResolver()} - ctrld.ProxyLog.Debug().Msg("start ptr discovery") + ctrld.ProxyLogger.Load().Debug().Msg("start ptr discovery") if err := t.ptr.refresh(); err != nil { - ctrld.ProxyLog.Error().Err(err).Msg("could not init PTR discover") + ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init PTR discover") } else { t.hostnameResolvers = append(t.hostnameResolvers, t.ptr) t.refreshers = append(t.refreshers, t.ptr) @@ -146,9 +146,9 @@ func (t *Table) Init() { } if t.discoverMDNS() { t.mdns = &mdns{} - ctrld.ProxyLog.Debug().Msg("start mdns discovery") + ctrld.ProxyLogger.Load().Debug().Msg("start mdns discovery") if err := t.mdns.init(t.quitCh); err != nil { - ctrld.ProxyLog.Error().Err(err).Msg("could not init mDNS discover") + ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not init mDNS discover") } else { t.hostnameResolvers = append(t.hostnameResolvers, t.mdns) } diff --git a/internal/clientinfo/dhcp.go b/internal/clientinfo/dhcp.go index ad423bd..a91bdb9 100644 --- a/internal/clientinfo/dhcp.go +++ b/internal/clientinfo/dhcp.go @@ -56,14 +56,14 @@ func (d *dhcp) watchChanges() { if event.Has(fsnotify.Write) { format := clientInfoFiles[event.Name] if err := d.readLeaseFile(event.Name, format); err != nil && !os.IsNotExist(err) { - ctrld.ProxyLog.Err(err).Str("file", event.Name).Msg("leases file changed but failed to update client info") + ctrld.ProxyLogger.Load().Err(err).Str("file", event.Name).Msg("leases file changed but failed to update client info") } } case err, ok := <-d.watcher.Errors: if !ok { return } - ctrld.ProxyLog.Err(err).Msg("could not watch client info file") + ctrld.ProxyLogger.Load().Err(err).Msg("could not watch client info file") } } @@ -167,7 +167,7 @@ func (d *dhcp) dnsmasqReadClientInfoReader(reader io.Reader) error { } ip := normalizeIP(string(fields[2])) if net.ParseIP(ip) == nil { - ctrld.ProxyLog.Warn().Msgf("invalid ip address entry: %q", ip) + ctrld.ProxyLogger.Load().Warn().Msgf("invalid ip address entry: %q", ip) ip = "" } @@ -219,7 +219,7 @@ func (d *dhcp) iscDHCPReadClientInfoReader(reader io.Reader) error { case "lease": ip = normalizeIP(strings.ToLower(fields[1])) if net.ParseIP(ip) == nil { - ctrld.ProxyLog.Warn().Msgf("invalid ip address entry: %q", ip) + ctrld.ProxyLogger.Load().Warn().Msgf("invalid ip address entry: %q", ip) ip = "" } case "hardware": @@ -242,7 +242,7 @@ func (d *dhcp) iscDHCPReadClientInfoReader(reader io.Reader) error { func (d *dhcp) addSelf() { hostname, err := os.Hostname() if err != nil { - ctrld.ProxyLog.Err(err).Msg("could not get hostname") + ctrld.ProxyLogger.Load().Err(err).Msg("could not get hostname") return } hostname = normalizeHostname(hostname) diff --git a/internal/clientinfo/mdns.go b/internal/clientinfo/mdns.go index 59ef7eb..1e99fe6 100644 --- a/internal/clientinfo/mdns.go +++ b/internal/clientinfo/mdns.go @@ -83,11 +83,11 @@ func (m *mdns) probeLoop(conns []*net.UDPConn, remoteAddr net.Addr, quitCh chan for { err := m.probe(conns, remoteAddr) if isErrNetUnreachableOrInvalid(err) { - ctrld.ProxyLog.Warn().Msgf("stop probing %q: network unreachable or invalid", remoteAddr) + ctrld.ProxyLogger.Load().Warn().Msgf("stop probing %q: network unreachable or invalid", remoteAddr) break } if err != nil { - ctrld.ProxyLog.Warn().Err(err).Msg("error while probing mdns") + ctrld.ProxyLogger.Load().Warn().Err(err).Msg("error while probing mdns") bo.BackOff(context.Background(), errors.New("mdns probe backoff")) } select { @@ -113,7 +113,7 @@ func (m *mdns) readLoop(conn *net.UDPConn) { if err, ok := err.(*net.OpError); ok && (err.Timeout() || err.Temporary()) { continue } - ctrld.ProxyLog.Debug().Err(err).Msg("mdns readLoop error") + ctrld.ProxyLogger.Load().Debug().Err(err).Msg("mdns readLoop error") return } @@ -133,11 +133,11 @@ func (m *mdns) readLoop(conn *net.UDPConn) { if ip != "" && name != "" { name = normalizeHostname(name) if val, loaded := m.name.LoadOrStore(ip, name); !loaded { - ctrld.ProxyLog.Debug().Msgf("found hostname: %q, ip: %q via mdns", name, ip) + ctrld.ProxyLogger.Load().Debug().Msgf("found hostname: %q, ip: %q via mdns", name, ip) } else { old := val.(string) if old != name { - ctrld.ProxyLog.Debug().Msgf("update hostname: %q, ip: %q, old: %q via mdns", name, ip, old) + ctrld.ProxyLogger.Load().Debug().Msgf("update hostname: %q, ip: %q, old: %q via mdns", name, ip, old) m.name.Store(ip, name) } } diff --git a/internal/clientinfo/merlin.go b/internal/clientinfo/merlin.go index 71c570c..8a39398 100644 --- a/internal/clientinfo/merlin.go +++ b/internal/clientinfo/merlin.go @@ -25,7 +25,7 @@ func (m *merlinDiscover) refresh() error { if err != nil { return err } - ctrld.ProxyLog.Debug().Msg("reading Merlin custom client list") + ctrld.ProxyLogger.Load().Debug().Msg("reading Merlin custom client list") m.parseMerlinCustomClientList(out) return nil } diff --git a/internal/clientinfo/ptr_lookup.go b/internal/clientinfo/ptr_lookup.go index 0de3f1a..29526fa 100644 --- a/internal/clientinfo/ptr_lookup.go +++ b/internal/clientinfo/ptr_lookup.go @@ -46,13 +46,13 @@ func (p *ptrDiscover) lookupHostname(ip string) string { msg := new(dns.Msg) addr, err := dns.ReverseAddr(ip) if err != nil { - ctrld.ProxyLog.Error().Err(err).Msg("invalid ip address") + ctrld.ProxyLogger.Load().Error().Err(err).Msg("invalid ip address") return "" } msg.SetQuestion(addr, dns.TypePTR) ans, err := p.resolver.Resolve(ctx, msg) if err != nil { - ctrld.ProxyLog.Error().Err(err).Msg("could not lookup IP") + ctrld.ProxyLogger.Load().Error().Err(err).Msg("could not lookup IP") return "" } for _, rr := range ans.Answer { diff --git a/internal/controld/config.go b/internal/controld/config.go index 6bc5544..852aa8a 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -80,10 +80,10 @@ func FetchResolverConfig(uid, version string, cdDev bool) (*ResolverConfig, erro } ips := ctrld.LookupIP(apiDomain) if len(ips) == 0 { - ctrld.ProxyLog.Warn().Msgf("No IPs found for %s, connecting to %s", apiDomain, addr) + ctrld.ProxyLogger.Load().Warn().Msgf("No IPs found for %s, connecting to %s", apiDomain, addr) return ctrldnet.Dialer.DialContext(ctx, network, addr) } - ctrld.ProxyLog.Debug().Msgf("API IPs: %v", ips) + ctrld.ProxyLogger.Load().Debug().Msgf("API IPs: %v", ips) _, port, _ := net.SplitHostPort(addr) addrs := make([]string, len(ips)) for i := range ips { diff --git a/log.go b/log.go index a4689b3..c521163 100644 --- a/log.go +++ b/log.go @@ -4,14 +4,24 @@ import ( "context" "fmt" "io" + "sync/atomic" "github.com/rs/zerolog" ) +func init() { + l := zerolog.New(io.Discard) + ProxyLogger.Store(&l) +} + // ProxyLog emits the log record for proxy operations. // The caller should set it only once. +// DEPRECATED: use ProxyLogger instead. var ProxyLog = zerolog.New(io.Discard) +// ProxyLogger emits the log record for proxy operations. +var ProxyLogger atomic.Pointer[zerolog.Logger] + // ReqIdCtxKey is the context.Context key for a request id. type ReqIdCtxKey struct{} diff --git a/resolver.go b/resolver.go index 2bee2d8..297d796 100644 --- a/resolver.go +++ b/resolver.go @@ -156,7 +156,7 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) if withBootstrapDNS { resolver.nameservers = append([]string{net.JoinHostPort(bootstrapDNS, "53")}, resolver.nameservers...) } - ProxyLog.Debug().Msgf("resolving %q using bootstrap DNS %q", domain, resolver.nameservers) + ProxyLogger.Load().Debug().Msgf("resolving %q using bootstrap DNS %q", domain, resolver.nameservers) timeoutMs := 2000 if timeout > 0 && timeout < timeoutMs { timeoutMs = timeout @@ -199,15 +199,15 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) r, err := resolver.Resolve(ctx, m) if err != nil { - ProxyLog.Error().Err(err).Msgf("could not lookup %q record for domain %q", dns.TypeToString[dnsType], domain) + ProxyLogger.Load().Error().Err(err).Msgf("could not lookup %q record for domain %q", dns.TypeToString[dnsType], domain) return } if r.Rcode != dns.RcodeSuccess { - ProxyLog.Error().Msgf("could not resolve domain %q, return code: %s", domain, dns.RcodeToString[r.Rcode]) + ProxyLogger.Load().Error().Msgf("could not resolve domain %q, return code: %s", domain, dns.RcodeToString[r.Rcode]) return } if len(r.Answer) == 0 { - ProxyLog.Error().Msg("no answer from OS resolver") + ProxyLogger.Load().Error().Msg("no answer from OS resolver") return } target := targetDomain(r.Answer) From 72d2f4e7e3552b6e37af56b852c0a50e7eeb5509 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 26 Jul 2023 18:23:15 +0700 Subject: [PATCH 62/84] internal/controld: add support for parsing client id from raw UID --- internal/controld/config.go | 23 +++++++++++++++++--- internal/controld/config_test.go | 29 ++++++++++++------------- internal/controld/controld_test.go | 34 ++++++++++++++++++++++++++++++ 3 files changed, 67 insertions(+), 19 deletions(-) create mode 100644 internal/controld/controld_test.go diff --git a/internal/controld/config.go b/internal/controld/config.go index 852aa8a..320fd4c 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -8,6 +8,7 @@ import ( "fmt" "net" "net/http" + "strings" "time" "github.com/Control-D-Inc/ctrld" @@ -53,12 +54,18 @@ func (u UtilityErrorResponse) Error() string { } type utilityRequest struct { - UID string `json:"uid"` + UID string `json:"uid"` + ClientID string `json:"client_id,omitempty"` } // FetchResolverConfig fetch Control D config for given uid. -func FetchResolverConfig(uid, version string, cdDev bool) (*ResolverConfig, error) { - body, _ := json.Marshal(utilityRequest{UID: uid}) +func FetchResolverConfig(rawUID, version string, cdDev bool) (*ResolverConfig, error) { + uid, clientID := ParseRawUID(rawUID) + uReq := utilityRequest{UID: uid} + if clientID != "" { + uReq.ClientID = clientID + } + body, _ := json.Marshal(uReq) apiUrl := resolverDataURLCom if cdDev { apiUrl = resolverDataURLDev @@ -120,3 +127,13 @@ func FetchResolverConfig(uid, version string, cdDev bool) (*ResolverConfig, erro } return &ur.Body.Resolver, nil } + +// ParseRawUID parse the input raw UID, returning real UID and ClientID. +// The raw UID can have 2 forms: +// +// - +// - / +func ParseRawUID(rawUID string) (string, string) { + uid, clientID, _ := strings.Cut(rawUID, "/") + return uid, clientID +} diff --git a/internal/controld/config_test.go b/internal/controld/config_test.go index 2c00247..b266142 100644 --- a/internal/controld/config_test.go +++ b/internal/controld/config_test.go @@ -1,34 +1,31 @@ -//go:build controld - package controld import ( "testing" "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" ) -func TestFetchResolverConfig(t *testing.T) { +func Test_parseUID(t *testing.T) { tests := []struct { - name string - uid string - dev bool - wantErr bool + name string + uid string + wantUID string + wantClientID string }{ - {"valid com", "p2", false, false}, - {"valid dev", "p2", true, false}, - {"invalid uid", "abcd1234", false, true}, + {"empty", "", "", ""}, + {"only uid", "abcd1234", "abcd1234", ""}, + {"with client id", "abcd1234/clientID", "abcd1234", "clientID"}, + {"with empty clientID", "abcd1234/", "abcd1234", ""}, } + for _, tc := range tests { tc := tc t.Run(tc.name, func(t *testing.T) { t.Parallel() - got, err := FetchResolverConfig(tc.uid, "dev-test", tc.dev) - require.False(t, (err != nil) != tc.wantErr, err) - if !tc.wantErr { - assert.NotEmpty(t, got.DOH) - } + gotUID, gotClientID := ParseRawUID(tc.uid) + assert.Equal(t, tc.wantUID, gotUID) + assert.Equal(t, tc.wantClientID, gotClientID) }) } } diff --git a/internal/controld/controld_test.go b/internal/controld/controld_test.go new file mode 100644 index 0000000..2c00247 --- /dev/null +++ b/internal/controld/controld_test.go @@ -0,0 +1,34 @@ +//go:build controld + +package controld + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFetchResolverConfig(t *testing.T) { + tests := []struct { + name string + uid string + dev bool + wantErr bool + }{ + {"valid com", "p2", false, false}, + {"valid dev", "p2", true, false}, + {"invalid uid", "abcd1234", false, true}, + } + for _, tc := range tests { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + got, err := FetchResolverConfig(tc.uid, "dev-test", tc.dev) + require.False(t, (err != nil) != tc.wantErr, err) + if !tc.wantErr { + assert.NotEmpty(t, got.DOH) + } + }) + } +} From 39a2cab0513dfdc4a081d3afc6f7f6bad92f98be Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 26 Jul 2023 18:25:55 +0700 Subject: [PATCH 63/84] internal/clientinfo: only do self discover with client id While at it, also ensure that client info table was initialized before doing any lookup. --- cmd/ctrld/prog.go | 2 +- internal/clientinfo/client_info.go | 24 +++++++++++++++++++++++- 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 4aff917..47e3b92 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -112,7 +112,7 @@ func (p *prog) run() { go uc.Ping() } - p.ciTable = clientinfo.NewTable(&cfg, defaultRouteIP()) + p.ciTable = clientinfo.NewTable(&cfg, defaultRouteIP(), cdUID) if leaseFile := p.cfg.Service.DHCPLeaseFile; leaseFile != "" { mainLog.Load().Debug().Msgf("watching custom lease file: %s", leaseFile) format := ctrld.LeaseFileFormat(p.cfg.Service.DHCPLeaseFileFormat) diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index f923883..4d6d065 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -4,9 +4,11 @@ import ( "fmt" "net/netip" "strings" + "sync" "time" "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/controld" ) // IpResolver is the interface for retrieving IP from Mac. @@ -60,6 +62,7 @@ type Table struct { macResolvers []MacResolver hostnameResolvers []HostnameResolver refreshers []refresher + initOnce sync.Once dhcp *dhcp merlin *merlinDiscover @@ -69,13 +72,15 @@ type Table struct { cfg *ctrld.Config quitCh chan struct{} selfIP string + cdUID string } -func NewTable(cfg *ctrld.Config, selfIP string) *Table { +func NewTable(cfg *ctrld.Config, selfIP, cdUID string) *Table { return &Table{ cfg: cfg, quitCh: make(chan struct{}), selfIP: selfIP, + cdUID: cdUID, } } @@ -88,6 +93,7 @@ func (t *Table) AddLeaseFile(name string, format ctrld.LeaseFileFormat) { func (t *Table) RefreshLoop(stopCh chan struct{}) { timer := time.NewTicker(time.Minute * 5) + defer timer.Stop() for { select { case <-timer.C: @@ -102,6 +108,19 @@ func (t *Table) RefreshLoop(stopCh chan struct{}) { } func (t *Table) Init() { + t.initOnce.Do(t.init) +} + +func (t *Table) init() { + if _, clientID := controld.ParseRawUID(t.cdUID); clientID != "" { + ctrld.ProxyLogger.Load().Debug().Msg("start self discovery") + t.dhcp = &dhcp{selfIP: t.selfIP} + t.dhcp.addSelf() + t.ipResolvers = append(t.ipResolvers, t.dhcp) + t.macResolvers = append(t.macResolvers, t.dhcp) + t.hostnameResolvers = append(t.hostnameResolvers, t.dhcp) + return + } if t.discoverDHCP() || t.discoverARP() { t.merlin = &merlinDiscover{} if err := t.merlin.refresh(); err != nil { @@ -156,6 +175,7 @@ func (t *Table) Init() { } func (t *Table) LookupIP(mac string) string { + t.initOnce.Do(t.init) for _, r := range t.ipResolvers { if ip := r.LookupIP(mac); ip != "" { return ip @@ -165,6 +185,7 @@ func (t *Table) LookupIP(mac string) string { } func (t *Table) LookupMac(ip string) string { + t.initOnce.Do(t.init) for _, r := range t.macResolvers { if mac := r.LookupMac(ip); mac != "" { return mac @@ -174,6 +195,7 @@ func (t *Table) LookupMac(ip string) string { } func (t *Table) LookupHostname(ip, mac string) string { + t.initOnce.Do(t.init) for _, r := range t.hostnameResolvers { if name := r.LookupHostnameByIP(ip); name != "" { return name From 6e27f877ffc942f9d8787fae520b95bad7674a67 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 27 Jul 2023 20:39:40 +0000 Subject: [PATCH 64/84] internal/clientinfo: use ptr cache when listing clients --- cmd/ctrld/control_client.go | 2 +- internal/clientinfo/client_info.go | 8 ++++++++ internal/clientinfo/ptr_lookup.go | 7 +++++++ 3 files changed, 16 insertions(+), 1 deletion(-) diff --git a/cmd/ctrld/control_client.go b/cmd/ctrld/control_client.go index 0a94c99..8a41193 100644 --- a/cmd/ctrld/control_client.go +++ b/cmd/ctrld/control_client.go @@ -20,7 +20,7 @@ func newControlClient(addr string) *controlClient { return d.DialContext(ctx, "unix", addr) }, }, - Timeout: time.Second * 5, + Timeout: time.Second * 30, }} } diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index 4d6d065..f648ac9 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -229,6 +229,14 @@ func (t *Table) lookupHostnameAll(ip, mac string) []*hostnameEntry { var res []*hostnameEntry for _, r := range t.hostnameResolvers { src := r.String() + // For ptrDiscover, lookup hostname may block due to server unavailable, + // so only lookup from cache to prevent timeout reached. + if ptrResolver, ok := r.(*ptrDiscover); ok { + if name := ptrResolver.lookupHostnameFromCache(ip); name != "" { + res = append(res, &hostnameEntry{name: name, src: src}) + } + continue + } if name := r.LookupHostnameByIP(ip); name != "" { res = append(res, &hostnameEntry{name: name, src: src}) continue diff --git a/internal/clientinfo/ptr_lookup.go b/internal/clientinfo/ptr_lookup.go index 29526fa..ba76b12 100644 --- a/internal/clientinfo/ptr_lookup.go +++ b/internal/clientinfo/ptr_lookup.go @@ -40,6 +40,13 @@ func (p *ptrDiscover) String() string { return "ptr" } +func (p *ptrDiscover) lookupHostnameFromCache(ip string) string { + if val, ok := p.hostname.Load(ip); ok { + return val.(string) + } + return "" +} + func (p *ptrDiscover) lookupHostname(ip string) string { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() From 82d887f52d61e0f4b91b2d756589087bc45bb1e0 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 27 Jul 2023 22:28:15 +0000 Subject: [PATCH 65/84] cmd/ctrld: preserve OS error when updating listener config --- cmd/ctrld/cli.go | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 0774f0b..55fcaea 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -1415,10 +1415,10 @@ func updateListenerConfig() { _ = closer.Close() } }() - // listenOk reports whether we can listen on udp/tcp of given address. + // tryListen attempts to listen on given udp and tcp address. // Created listeners will be kept in listeners slice above, and close // before function finished. - listenOk := func(addr string) bool { + tryListen := func(addr string) error { udpLn, udpErr := net.ListenPacket("udp", addr) if udpLn != nil { closers = append(closers, udpLn) @@ -1427,7 +1427,7 @@ func updateListenerConfig() { if tcpLn != nil { closers = append(closers, tcpLn) } - return udpErr == nil && tcpErr == nil + return errors.Join(udpErr, tcpErr) } logMsg := func(e *zerolog.Event, listenerNum int, format string, v ...any) { @@ -1476,11 +1476,12 @@ func updateListenerConfig() { logMsg(mainLog.Load().Fatal(), n, "could not find available listen ip and port") } addr := net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)) - if listenOk(addr) { + err := tryListen(addr) + if err == nil { break } if !check.IP && !check.Port { - logMsg(mainLog.Load().Fatal(), n, "failed to listen on: %s", addr) + logMsg(mainLog.Load().Fatal(), n, "failed to listen: %v", err) } if tryAllPort53 { tryAllPort53 = false @@ -1541,7 +1542,7 @@ func updateListenerConfig() { listener.Port = oldPort } if listener.IP == oldIP && listener.Port == oldPort { - logMsg(mainLog.Load().Fatal(), n, "could not listener on: %s", net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port))) + logMsg(mainLog.Load().Fatal(), n, "could not listener on %s: %v", net.JoinHostPort(listener.IP, strconv.Itoa(listener.Port)), err) } logMsg(mainLog.Load().Warn(), n, "could not listen on address: %s, pick a random ip+port", addr) attempts++ @@ -1563,7 +1564,7 @@ func updateListenerConfig() { for _, addr := range addrs { if netIP, ok := addr.(*net.IPNet); ok && netIP.IP.To4() != nil { addr := net.JoinHostPort(netIP.IP.String(), strconv.Itoa(listener.Port)) - if listenOk(addr) { + if err := tryListen(addr); err == nil { found = true listener.IP = netIP.IP.String() logMsg(mainLog.Load().Warn(), n, "use %s as listener address", listener.IP) From c27189655166d3a59856505fb2c26f06b7028f97 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 27 Jul 2023 23:05:27 +0000 Subject: [PATCH 66/84] all: add support for provision token --- cmd/ctrld/cli.go | 78 +++++++++++++++++++++++++++++++++---- cmd/ctrld/main.go | 1 + config.go | 19 +++++++++ internal/controld/config.go | 27 +++++++++++-- 4 files changed, 114 insertions(+), 11 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 55fcaea..2369323 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -202,6 +202,9 @@ func initCLI() { } oldLogPath := cfg.Service.LogPath + if uid := cdUIDFromProvToken(); uid != "" { + cdUID = uid + } if cdUID != "" { processCDFlags() } @@ -311,6 +314,7 @@ func initCLI() { runCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file") runCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") runCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid") + runCmd.Flags().StringVarP(&cdOrg, "cd-org", "", "", "Control D provision token") runCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain") _ = runCmd.Flags().MarkHidden("dev") runCmd.Flags().StringVarP(&homedir, "homedir", "", "", "") @@ -337,6 +341,12 @@ func initCLI() { } setDependencies(sc) sc.Arguments = append([]string{"run"}, osArgs...) + if uid := cdUIDFromProvToken(); uid != "" { + cdUID = uid + removeProvTokenFromArgs(sc) + // Pass --cd flag to "ctrld run" command, so the provision token takes no effect. + sc.Arguments = append(sc.Arguments, "--cd="+cdUID) + } p := &prog{ router: router.New(&cfg, cdUID != ""), @@ -427,8 +437,7 @@ func initCLI() { return } - domain := cfg.Upstream["0"].VerifyDomain() - status = selfCheckStatus(status, domain) + status = selfCheckStatus(status) switch status { case service.StatusRunning: mainLog.Load().Notice().Msg("Service started") @@ -462,6 +471,7 @@ func initCLI() { startCmd.Flags().StringVarP(&logPath, "log", "", "", "Path to log file") startCmd.Flags().IntVarP(&cacheSize, "cache_size", "", 0, "Enable cache with size items") startCmd.Flags().StringVarP(&cdUID, "cd", "", "", "Control D resolver uid") + startCmd.Flags().StringVarP(&cdOrg, "cd-org", "", "", "Control D provision token") startCmd.Flags().BoolVarP(&cdDev, "dev", "", false, "Use Control D dev resolver/domain") _ = startCmd.Flags().MarkHidden("dev") startCmd.Flags().StringVarP(&iface, "iface", "", "", `Update DNS setting for iface, "auto" means the default interface gateway`) @@ -1100,11 +1110,7 @@ func defaultIfaceName() string { return dri } -func selfCheckStatus(status service.Status, domain string) service.Status { - if domain == "" { - // Nothing to do, return the status as-is. - return status - } +func selfCheckStatus(status service.Status) service.Status { dir, err := userHomeDir() if err != nil { mainLog.Load().Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory") @@ -1146,6 +1152,7 @@ func selfCheckStatus(status service.Status, domain string) service.Status { c := new(dns.Client) var ( lcChanged map[string]*ctrld.ListenerConfig + ucChanged map[string]*ctrld.UpstreamConfig mu sync.Mutex ) @@ -1155,6 +1162,11 @@ func selfCheckStatus(status service.Status, domain string) service.Status { 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 status + } watcher, err := fsnotify.NewWatcher() if err != nil { mainLog.Load().Error().Err(err).Msg("could not watch config change") @@ -1169,6 +1181,10 @@ func selfCheckStatus(status service.Status, domain string) service.Status { 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 ( @@ -1180,8 +1196,15 @@ func selfCheckStatus(status service.Status, domain string) service.Status { if lcChanged != nil { cfg.Listener = lcChanged } + if ucChanged != nil { + cfg.Upstream = ucChanged + } mu.Unlock() lc := cfg.FirstListener() + domain = cfg.FirstUpstream().VerifyDomain() + if domain == "" { + continue + } m := new(dns.Msg) m.SetQuestion(domain+".", dns.TypeA) @@ -1599,3 +1622,44 @@ func osVersion() string { } return oi.String() } + +// cdUIDFromProvToken fetch UID from ControlD API using provision token. +func cdUIDFromProvToken() string { + // --cd flag supersedes --cd-org, ignore it if both are supplied. + if cdUID != "" { + return "" + } + // --cd-org is empty, nothing to do. + if cdOrg == "" { + return "" + } + // Process provision token if provided. + resolverConfig, err := controld.FetchResolverUID(cdOrg, rootCmd.Version, cdDev) + if err != nil { + mainLog.Load().Fatal().Err(err).Msgf("failed to fetch resolver uid with provision token: %s", cdOrg) + } + return resolverConfig.UID +} + +// removeProvTokenFromArgs removes the --cd-org from command line arguments. +func removeProvTokenFromArgs(sc *service.Config) { + a := sc.Arguments[:0] + skip := false + for _, x := range sc.Arguments { + if skip { + skip = false + continue + } + // For "--cd-org XXX", skip it and mark next arg skipped. + if x == "--cd-org" { + skip = true + continue + } + // For "--cd-org=XXX", just skip it. + if strings.HasPrefix(x, "--cd-org=") { + continue + } + a = append(a, x) + } + sc.Arguments = a +} diff --git a/cmd/ctrld/main.go b/cmd/ctrld/main.go index 75e7d2b..80160ec 100644 --- a/cmd/ctrld/main.go +++ b/cmd/ctrld/main.go @@ -28,6 +28,7 @@ var ( verbose int silent bool cdUID string + cdOrg string cdDev bool iface string ifaceStartStop string diff --git a/config.go b/config.go index 0019e00..3e2efe4 100644 --- a/config.go +++ b/config.go @@ -144,6 +144,25 @@ func (c *Config) FirstListener() *ListenerConfig { return c.Listener[strconv.Itoa(listeners[0])] } +// FirstUpstream returns the first upstream of current config. Upstreams are sorted numerically. +// +// It panics if Config has no upstreams configured. +func (c *Config) FirstUpstream() *UpstreamConfig { + upstreams := make([]int, 0, len(c.Upstream)) + for k := range c.Upstream { + n, err := strconv.Atoi(k) + if err != nil { + continue + } + upstreams = append(upstreams, n) + } + if len(upstreams) == 0 { + panic("missing listener config") + } + sort.Ints(upstreams) + return c.Upstream[strconv.Itoa(upstreams[0])] +} + // ServiceConfig specifies the general ctrld config. type ServiceConfig struct { LogLevel string `mapstructure:"log_level" toml:"log_level,omitempty"` diff --git a/internal/controld/config.go b/internal/controld/config.go index 320fd4c..4e4bc2e 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -6,8 +6,10 @@ import ( "crypto/tls" "encoding/json" "fmt" + "io" "net" "net/http" + "os" "strings" "time" @@ -33,6 +35,7 @@ type ResolverConfig struct { CustomConfig string `json:"custom_config"` } `json:"ctrld"` Exclude []string `json:"exclude"` + UID string `json:"uid"` } type utilityResponse struct { @@ -58,19 +61,35 @@ type utilityRequest struct { ClientID string `json:"client_id,omitempty"` } +type utilityOrgRequest struct { + ProvToken string `json:"prov_token"` + Hostname string `json:"hostname"` +} + // FetchResolverConfig fetch Control D config for given uid. func FetchResolverConfig(rawUID, version string, cdDev bool) (*ResolverConfig, error) { uid, clientID := ParseRawUID(rawUID) - uReq := utilityRequest{UID: uid} + req := utilityRequest{UID: uid} if clientID != "" { - uReq.ClientID = clientID + req.ClientID = clientID } - body, _ := json.Marshal(uReq) + body, _ := json.Marshal(req) + return postUtilityAPI(version, cdDev, bytes.NewReader(body)) +} + +// FetchResolverUID fetch resolver uid from provision token. +func FetchResolverUID(pt, version string, cdDev bool) (*ResolverConfig, error) { + hostname, _ := os.Hostname() + body, _ := json.Marshal(utilityOrgRequest{ProvToken: pt, Hostname: hostname}) + return postUtilityAPI(version, cdDev, bytes.NewReader(body)) +} + +func postUtilityAPI(version string, cdDev bool, body io.Reader) (*ResolverConfig, error) { apiUrl := resolverDataURLCom if cdDev { apiUrl = resolverDataURLDev } - req, err := http.NewRequest("POST", apiUrl, bytes.NewReader(body)) + req, err := http.NewRequest("POST", apiUrl, body) if err != nil { return nil, fmt.Errorf("http.NewRequest: %w", err) } From 774f07dd7f6f2451363ccee42bdfb365cb0fdb19 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 28 Jul 2023 18:57:13 +0000 Subject: [PATCH 67/84] internal/router: only do cleanup in cd mode on freebsd --- cmd/ctrld/cli.go | 4 ++-- internal/router/os_freebsd.go | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 2369323..e21868d 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -283,13 +283,13 @@ func initCLI() { rootCertPool = cp } p.onStarted = append(p.onStarted, func() { - mainLog.Load().Debug().Msg("router setup") + mainLog.Load().Debug().Msg("router setup on start") if err := p.router.Setup(); err != nil { mainLog.Load().Error().Err(err).Msg("could not configure router") } }) p.onStopped = append(p.onStopped, func() { - mainLog.Load().Debug().Msg("router cleanup") + mainLog.Load().Debug().Msg("router cleanup on stop") if err := p.router.Cleanup(); err != nil { mainLog.Load().Error().Err(err).Msg("could not cleanup router") } diff --git a/internal/router/os_freebsd.go b/internal/router/os_freebsd.go index 9d9b738..a84fcaa 100644 --- a/internal/router/os_freebsd.go +++ b/internal/router/os_freebsd.go @@ -109,7 +109,7 @@ func (or *osRouter) Setup() error { } func (or *osRouter) Cleanup() error { - if or.cfg.FirstListener().IsDirectDnsListener() { + if or.cdMode { _ = exec.Command(unboundRcPath, "onerestart").Run() _ = exec.Command(dnsmasqRcPath, "onerestart").Run() } @@ -129,10 +129,22 @@ name="{{.Name}}" rcvar="${name}_enable" {{.Name}}_env="IS_DAEMON=1" pidfile="/var/run/${name}.pid" +child_pidfile="/var/run/${name}_child.pid" command="/usr/sbin/daemon" -daemon_args="-P ${pidfile} -r -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}" +daemon_args="-P ${pidfile} -p ${child_pidfile} -t \"${name}: daemon\"{{if .WorkingDirectory}} -c {{.WorkingDirectory}}{{end}}" command_args="${daemon_args} {{.Path}}{{range .Arguments}} {{.}}{{end}}" +stop_cmd="ctrld_stop" + +ctrld_stop() { + pid=$(cat ${pidfile}) + child_pid=$(cat ${child_pidfile}) + if [ -e "${child_pidfile}" ]; then + kill -s TERM "${child_pid}" + wait_for_pids "${child_pid}" "${pidfile}" + fi +} + load_rc_config "${name}" run_rc_command "$1" ` From 0dee7518c4381e13ce87c1f4aa9ae933b4314ea1 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Mon, 31 Jul 2023 22:07:09 +0000 Subject: [PATCH 68/84] cmd/ctrld: validate UID during start command --- cmd/ctrld/cli.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index e21868d..e24911a 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -341,7 +341,11 @@ func initCLI() { } setDependencies(sc) sc.Arguments = append([]string{"run"}, osArgs...) - if uid := cdUIDFromProvToken(); uid != "" { + if cdUID != "" { + if _, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev); err != nil { + mainLog.Load().Fatal().Err(err).Msgf("failed to fetch resolver uid: %s", cdUID) + } + } else if uid := cdUIDFromProvToken(); uid != "" { cdUID = uid removeProvTokenFromArgs(sc) // Pass --cd flag to "ctrld run" command, so the provision token takes no effect. From e1d078a2c37bd20ead183fc51b0e8b57103f2a6b Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 1 Aug 2023 04:10:20 +0000 Subject: [PATCH 69/84] Ignoring RFC 1918 addresses for ControlD upstream --- config.go | 16 +++++++++++++++- resolver.go | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/config.go b/config.go index 3e2efe4..70b165b 100644 --- a/config.go +++ b/config.go @@ -343,9 +343,23 @@ func (uc *UpstreamConfig) SetupBootstrapIP() { // SetupBootstrapIP manually find all available IPs of the upstream. // The first usable IP will be used as bootstrap IP of the upstream. func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) { - b := backoff.NewBackoff("setupBootstrapIP", func(format string, args ...any) {}, 2*time.Second) + b := backoff.NewBackoff("setupBootstrapIP", func(format string, args ...any) {}, 10*time.Second) + isControlD := uc.isControlD() for { uc.bootstrapIPs = lookupIP(uc.Domain, uc.Timeout, withBootstrapDNS) + // For ControlD upstream, the bootstrap IPs could not be RFC 1918 addresses, + // filtering them out here to prevent weird behavior. + if isControlD { + n := 0 + for _, ip := range uc.bootstrapIPs { + netIP := net.ParseIP(ip) + if netIP != nil && !netIP.IsPrivate() { + uc.bootstrapIPs[n] = ip + n++ + } + } + uc.bootstrapIPs = uc.bootstrapIPs[:n] + } if len(uc.bootstrapIPs) > 0 { break } diff --git a/resolver.go b/resolver.go index 297d796..d2586ec 100644 --- a/resolver.go +++ b/resolver.go @@ -177,12 +177,12 @@ func lookupIP(domain string, timeout int, withBootstrapDNS bool) (ips []string) ipFromRecord := func(record dns.RR, target string) string { switch ar := record.(type) { case *dns.A: - if ar.Hdr.Name != target { + if ar.Hdr.Name != target || len(ar.A) == 0 { return "" } return ar.A.String() case *dns.AAAA: - if ar.Hdr.Name != target { + if ar.Hdr.Name != target || len(ar.AAAA) == 0 { return "" } return ar.AAAA.String() From 8496adc63863e203660ac2a51dc5b343c52a371b Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 1 Aug 2023 23:27:25 +0000 Subject: [PATCH 70/84] cmd/ctrld: make self-check process more resilient --- cmd/ctrld/cli.go | 54 ++++++++++++++++++++++++++++++++++++----------- cmd/ctrld/prog.go | 39 ++++++++++++++++++++++++++++++---- 2 files changed, 77 insertions(+), 16 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index e24911a..f9543aa 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -435,13 +435,8 @@ func initCLI() { mainLog.Load().Warn().Err(err).Msg("post installation failed, please check system/service log for details error") return } - status, err := s.Status() - if err != nil { - mainLog.Load().Warn().Err(err).Msg("could not get service status") - return - } - status = selfCheckStatus(status) + status := selfCheckStatus(s) switch status { case service.StatusRunning: mainLog.Load().Notice().Msg("Service started") @@ -969,7 +964,19 @@ func processNoConfigFlags(noConfigStart bool) { func processCDFlags() { logger := mainLog.Load().With().Str("mode", "cd").Logger() logger.Info().Msgf("fetching Controld D configuration from API: %s", cdUID) + bo := backoff.NewBackoff("processCDFlags", logf, 30*time.Second) + bo.LogLongerThan = 30 * time.Second + ctx := context.Background() resolverConfig, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev) + for { + if errUrlNetworkError(err) { + bo.BackOff(ctx, err) + logger.Warn().Msg("could not fetch resolver using bootstrap DNS, retrying...") + resolverConfig, err = controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev) + continue + } + break + } if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode { s, err := newService(&prog{}, svcConfig) if err != nil { @@ -1114,7 +1121,16 @@ func defaultIfaceName() string { return dri } -func selfCheckStatus(status service.Status) service.Status { +func selfCheckStatus(s service.Service) service.Status { + status, err := s.Status() + if err != nil { + mainLog.Load().Warn().Err(err).Msg("could not get service status") + return status + } + // If ctrld is not running, do nothing, just return the status as-is. + if status != service.StatusRunning { + return status + } dir, err := userHomeDir() if err != nil { mainLog.Load().Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory") @@ -1124,17 +1140,30 @@ func selfCheckStatus(status service.Status) service.Status { bo := backoff.NewBackoff("self-check", logf, 10*time.Second) bo.LogLongerThan = 10 * time.Second ctx := context.Background() - maxAttempts := 20 mainLog.Load().Debug().Msg("waiting for ctrld listener to be ready") cc := newControlClient(filepath.Join(dir, ctrldControlUnixSock)) // The socket control server may not start yet, so attempt to ping - // it until we got a response, or maxAttempts reached. - for i := 0; i < maxAttempts; i++ { + // it until we got a response. For each iteration, check ctrld status + // to make sure ctrld is still running. + for { + curStatus, err := s.Status() + if err != nil { + mainLog.Load().Warn().Err(err).Msg("could not get service status while doing self-check") + return status + } + if curStatus != service.StatusRunning { + return curStatus + } if _, err := cc.post("/", nil); err != nil { - bo.BackOff(ctx, err) - continue + // Do not count attempt if the server is not ready yet. + if errUrlConnRefused(err) { + bo.BackOff(ctx, err) + continue + } + mainLog.Load().Warn().Err(err).Msg("could not ping socket control server") + return service.StatusUnknown } break } @@ -1153,6 +1182,7 @@ func selfCheckStatus(status service.Status) service.Status { mainLog.Load().Debug().Msg("performing self-check") bo = backoff.NewBackoff("self-check", logf, 10*time.Second) bo.LogLongerThan = 500 * time.Millisecond + maxAttempts := 20 c := new(dns.Client) var ( lcChanged map[string]*ctrld.ListenerConfig diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 47e3b92..530a7c2 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -5,6 +5,7 @@ import ( "fmt" "math/rand" "net" + "net/url" "os" "strconv" "sync" @@ -310,11 +311,41 @@ func runLogServer(sockPath string) net.Conn { } func errAddrInUse(err error) bool { - opErr, ok := err.(*net.OpError) - if !ok { - return false + var opErr *net.OpError + if errors.As(err, &opErr) { + return errors.Is(opErr.Err, syscall.EADDRINUSE) } - return errors.Is(opErr.Err, syscall.EADDRINUSE) + return false +} + +func errUrlConnRefused(err error) bool { + var urlErr *url.Error + if errors.As(err, &urlErr) { + var opErr *net.OpError + if errors.As(urlErr.Err, &opErr) { + return errors.Is(opErr.Err, syscall.ECONNREFUSED) + } + } + return false +} + +func errUrlNetworkError(err error) bool { + var urlErr *url.Error + if errors.As(err, &urlErr) { + var opErr *net.OpError + if errors.As(urlErr.Err, &opErr) { + if opErr.Temporary() { + return true + } + switch { + case errors.Is(opErr.Err, syscall.ECONNREFUSED), + errors.Is(opErr.Err, syscall.EINVAL), + errors.Is(opErr.Err, syscall.ENETUNREACH): + return true + } + } + } + return false } // defaultRouteIP returns IP string of the default route if present, prefer IPv4 over IPv6. From d3d2ed539f4e5d550a82e8b225257d797d737f16 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 2 Aug 2023 10:47:07 +0700 Subject: [PATCH 71/84] cmd/ctrld: correct syscall.Errno for Windows On Windows, the syscall error numbers are different, so correct the value so we can detect right errors we want. --- cmd/ctrld/prog.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 530a7c2..6c16a13 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -313,17 +313,25 @@ func runLogServer(sockPath string) net.Conn { func errAddrInUse(err error) bool { var opErr *net.OpError if errors.As(err, &opErr) { - return errors.Is(opErr.Err, syscall.EADDRINUSE) + return errors.Is(opErr.Err, syscall.EADDRINUSE) || errors.Is(opErr.Err, windowsEADDRINUSE) } return false } +// https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2 +var ( + windowsECONNREFUSED = syscall.Errno(10061) + windowsENETUNREACH = syscall.Errno(10051) + windowsEINVAL = syscall.Errno(10022) + windowsEADDRINUSE = syscall.Errno(10048) +) + func errUrlConnRefused(err error) bool { var urlErr *url.Error if errors.As(err, &urlErr) { var opErr *net.OpError if errors.As(urlErr.Err, &opErr) { - return errors.Is(opErr.Err, syscall.ECONNREFUSED) + return errors.Is(opErr.Err, syscall.ECONNREFUSED) || errors.Is(opErr.Err, windowsECONNREFUSED) } } return false @@ -340,7 +348,10 @@ func errUrlNetworkError(err error) bool { switch { case errors.Is(opErr.Err, syscall.ECONNREFUSED), errors.Is(opErr.Err, syscall.EINVAL), - errors.Is(opErr.Err, syscall.ENETUNREACH): + errors.Is(opErr.Err, syscall.ENETUNREACH), + errors.Is(opErr.Err, windowsENETUNREACH), + errors.Is(opErr.Err, windowsEINVAL), + errors.Is(opErr.Err, windowsECONNREFUSED): return true } } From 46509be8a0826dd84f6a91df9b4132925b4a9abf Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 2 Aug 2023 10:52:55 +0700 Subject: [PATCH 72/84] cmd/ctrld: start service before restart on Windows On Windows, calling s.Restart will fail if service is not running, ensure ctrld is started before calling restart. --- cmd/ctrld/cli.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index f9543aa..95ccb49 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -453,7 +453,7 @@ func initCLI() { } // On Linux, Darwin, Freebsd, ctrld set DNS on startup, because the DNS setting could be // reset after rebooting. On windows, we only need to set once here. See prog.preRun in - // prog_*.go file for dedicated code on each platform. + // prog_*.go file for dedicated code on each platform. (1) if runtime.GOOS == "windows" { p.setDNS() } @@ -525,7 +525,10 @@ func initCLI() { initLogging() if doTasks([]task{{s.Stop, true}}) { p.router.Cleanup() - p.resetDNS() + // See comment (1) in startCmd. + if runtime.GOOS != "windows" { + p.resetDNS() + } mainLog.Load().Notice().Msg("Service stopped") } }, @@ -547,7 +550,15 @@ func initCLI() { return } initLogging() - if doTasks([]task{{s.Restart, true}}) { + tasks := []task{{s.Restart, true}} + // On Windows, s.Restart will return error unless service is running. + if runtime.GOOS == "windows" { + tasks = []task{ + {s.Start, false}, + {s.Restart, true}, + } + } + if doTasks(tasks) { mainLog.Load().Notice().Msg("Service restarted") } }, From e5389ffecb12555bdeda3bd9ae9c3e9a73fa5570 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 4 Aug 2023 20:03:15 +0000 Subject: [PATCH 73/84] internal/clientinfo: use all possible source IP for listing clients --- internal/clientinfo/client_info.go | 11 ++++++++--- internal/clientinfo/mdns.go | 17 +++++++++++++++-- internal/clientinfo/ptr_lookup.go | 9 +++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/internal/clientinfo/client_info.go b/internal/clientinfo/client_info.go index f648ac9..9235ca9 100644 --- a/internal/clientinfo/client_info.go +++ b/internal/clientinfo/client_info.go @@ -16,8 +16,6 @@ type IpResolver interface { fmt.Stringer // LookupIP returns ip of the device with given mac. LookupIP(mac string) string - // List returns list of ip known by the resolver. - List() []string } // MacResolver is the interface for retrieving Mac from IP. @@ -50,6 +48,12 @@ type refresher interface { refresh() error } +type ipLister interface { + fmt.Stringer + // List returns list of ip known by the resolver. + List() []string +} + type Client struct { IP netip.Addr Mac string @@ -255,7 +259,8 @@ func (t *Table) ListClients() []*Client { _ = r.refresh() } ipMap := make(map[string]*Client) - for _, ir := range t.ipResolvers { + il := []ipLister{t.dhcp, t.arp, t.ptr, t.mdns} + for _, ir := range il { for _, ip := range ir.List() { c, ok := ipMap[ip] if !ok { diff --git a/internal/clientinfo/mdns.go b/internal/clientinfo/mdns.go index 1e99fe6..5aed579 100644 --- a/internal/clientinfo/mdns.go +++ b/internal/clientinfo/mdns.go @@ -47,6 +47,15 @@ func (m *mdns) String() string { return "mdns" } +func (m *mdns) List() []string { + var ips []string + m.name.Range(func(key, value any) bool { + ips = append(ips, key.(string)) + return true + }) + return ips +} + func (m *mdns) init(quitCh chan struct{}) error { ifaces, err := multicastInterfaces() if err != nil { @@ -123,8 +132,11 @@ func (m *mdns) readLoop(conn *net.UDPConn) { } var ip, name string - for _, answer := range msg.Answer { - switch ar := answer.(type) { + rrs := make([]dns.RR, 0, len(msg.Answer)+len(msg.Extra)) + rrs = append(rrs, msg.Answer...) + rrs = append(rrs, msg.Extra...) + for _, rr := range rrs { + switch ar := rr.(type) { case *dns.A: ip, name = ar.A.String(), ar.Hdr.Name case *dns.AAAA: @@ -151,6 +163,7 @@ func (m *mdns) readLoop(conn *net.UDPConn) { func (m *mdns) probe(conns []*net.UDPConn, remoteAddr net.Addr) error { msg := new(dns.Msg) msg.Question = make([]dns.Question, len(services)) + msg.Compress = true for i, service := range services { msg.Question[i] = dns.Question{ Name: dns.CanonicalName(service), diff --git a/internal/clientinfo/ptr_lookup.go b/internal/clientinfo/ptr_lookup.go index ba76b12..9c02fa1 100644 --- a/internal/clientinfo/ptr_lookup.go +++ b/internal/clientinfo/ptr_lookup.go @@ -40,6 +40,15 @@ func (p *ptrDiscover) String() string { return "ptr" } +func (p *ptrDiscover) List() []string { + var ips []string + p.hostname.Range(func(key, value any) bool { + ips = append(ips, key.(string)) + return true + }) + return ips +} + func (p *ptrDiscover) lookupHostnameFromCache(ip string) string { if val, ok := p.hostname.Load(ip); ok { return val.(string) From 46e8d4fad70fda9c9d5f0a634100629108fb3b6a Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 4 Aug 2023 20:05:51 +0000 Subject: [PATCH 74/84] cmd/ctrld: prevent race condition when ping socket control server --- cmd/ctrld/cli.go | 15 ++++++--------- cmd/ctrld/control_server.go | 3 +++ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 95ccb49..95e5c30 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -1167,16 +1167,13 @@ func selfCheckStatus(s service.Service) service.Status { if curStatus != service.StatusRunning { return curStatus } - if _, err := cc.post("/", nil); err != nil { - // Do not count attempt if the server is not ready yet. - if errUrlConnRefused(err) { - bo.BackOff(ctx, err) - continue - } - mainLog.Load().Warn().Err(err).Msg("could not ping socket control server") - return service.StatusUnknown + if _, err := cc.post("/", nil); err == nil { + // Server was started, stop pinging. + break } - break + // The socket control server is not ready yet, backoff for waiting it to be ready. + bo.BackOff(ctx, err) + continue } resp, err := cc.post(startedPath, nil) if err != nil { diff --git a/cmd/ctrld/control_server.go b/cmd/ctrld/control_server.go index 20dec83..5118113 100644 --- a/cmd/ctrld/control_server.go +++ b/cmd/ctrld/control_server.go @@ -35,6 +35,9 @@ func newControlServer(addr string) (*controlServer, error) { func (s *controlServer) start() error { _ = os.Remove(s.addr) unixListener, err := net.Listen("unix", s.addr) + if l, ok := unixListener.(*net.UnixListener); ok { + l.SetUnlinkOnClose(true) + } if err != nil { return err } From 125b4b6077c4fc835306556c408128152e8897bb Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Sat, 5 Aug 2023 01:34:31 +0000 Subject: [PATCH 75/84] cmd/ctrld: wait ctrld started during restart command --- cmd/ctrld/cli.go | 74 ++++++++++++++++++++----------- internal/router/service_ddwrt.go | 2 +- internal/router/service_merlin.go | 2 +- internal/router/service_tomato.go | 2 +- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 95e5c30..7a4d5aa 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -559,6 +559,15 @@ func initCLI() { } } if doTasks(tasks) { + dir, err := userHomeDir() + if err != nil { + mainLog.Load().Warn().Err(err).Msg("Service was restarted, but could not ping the control server") + return + } + if cc := newSocketControlClient(s, dir); cc == nil { + mainLog.Load().Notice().Msg("Service was not restarted") + os.Exit(1) + } mainLog.Load().Notice().Msg("Service restarted") } }, @@ -1147,34 +1156,12 @@ func selfCheckStatus(s service.Service) service.Status { mainLog.Load().Error().Err(err).Msg("failed to check ctrld listener status: could not get home directory") return service.StatusUnknown } - - bo := backoff.NewBackoff("self-check", logf, 10*time.Second) - bo.LogLongerThan = 10 * time.Second - ctx := context.Background() - mainLog.Load().Debug().Msg("waiting for ctrld listener to be ready") - cc := newControlClient(filepath.Join(dir, ctrldControlUnixSock)) - - // The socket control server may not start yet, so attempt to ping - // it until we got a response. For each iteration, check ctrld status - // to make sure ctrld is still running. - for { - curStatus, err := s.Status() - if err != nil { - mainLog.Load().Warn().Err(err).Msg("could not get service status while doing self-check") - return status - } - if curStatus != service.StatusRunning { - return curStatus - } - if _, err := cc.post("/", nil); err == nil { - // Server was started, stop pinging. - break - } - // The socket control server is not ready yet, backoff for waiting it to be ready. - bo.BackOff(ctx, err) - continue + cc := newSocketControlClient(s, dir) + if cc == nil { + return service.StatusUnknown } + resp, err := cc.post(startedPath, nil) if err != nil { mainLog.Load().Error().Err(err).Msg("failed to connect to control server") @@ -1188,8 +1175,9 @@ func selfCheckStatus(s service.Service) service.Status { mainLog.Load().Debug().Msg("ctrld listener is ready") mainLog.Load().Debug().Msg("performing self-check") - bo = backoff.NewBackoff("self-check", logf, 10*time.Second) + bo := backoff.NewBackoff("self-check", logf, 10*time.Second) bo.LogLongerThan = 500 * time.Millisecond + ctx := context.Background() maxAttempts := 20 c := new(dns.Client) var ( @@ -1705,3 +1693,35 @@ func removeProvTokenFromArgs(sc *service.Config) { } sc.Arguments = a } + +// newSocketControlClient returns new control client after control server was started. +func newSocketControlClient(s service.Service, dir string) *controlClient { + bo := backoff.NewBackoff("self-check", logf, 10*time.Second) + bo.LogLongerThan = 10 * time.Second + ctx := context.Background() + + cc := newControlClient(filepath.Join(dir, ctrldControlUnixSock)) + + // The socket control server may not start yet, so attempt to ping + // it until we got a response. For each iteration, check ctrld status + // to make sure ctrld is still running. + for { + curStatus, err := s.Status() + if err != nil { + mainLog.Load().Warn().Err(err).Msg("could not get service status while doing self-check") + return nil + } + if curStatus != service.StatusRunning { + return nil + } + if _, err := cc.post("/", nil); err == nil { + // Server was started, stop pinging. + break + } + // The socket control server is not ready yet, backoff for waiting it to be ready. + bo.BackOff(ctx, err) + continue + } + + return cc +} diff --git a/internal/router/service_ddwrt.go b/internal/router/service_ddwrt.go index 3e8b9bf..3217f8a 100644 --- a/internal/router/service_ddwrt.go +++ b/internal/router/service_ddwrt.go @@ -271,7 +271,7 @@ case "$1" in echo "failed to stop $name" exit 1 fi - exit 1 + exit 0 ;; restart) $0 stop diff --git a/internal/router/service_merlin.go b/internal/router/service_merlin.go index 9273eca..76ea938 100644 --- a/internal/router/service_merlin.go +++ b/internal/router/service_merlin.go @@ -304,7 +304,7 @@ case "$1" in logger -c "failed to stop $name" exit 1 fi - exit 1 + exit 0 ;; restart) $0 stop diff --git a/internal/router/service_tomato.go b/internal/router/service_tomato.go index aa96d4b..1a7151a 100644 --- a/internal/router/service_tomato.go +++ b/internal/router/service_tomato.go @@ -227,7 +227,7 @@ start() { stop() { if ! is_running; then elog "$NAME is not running." - exit 1 + exit 0 fi elog "Shutting down $NAME Services: " kill -SIGTERM "$(get_pid)" From 854a244ebb3c454c6358ff8f310f2c578d3f44b4 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 9 Aug 2023 02:45:38 +0000 Subject: [PATCH 76/84] Fix restart command when ctrld service was already stopped --- cmd/ctrld/cli.go | 11 +++---- cmd/ctrld/service.go | 10 ++++++ internal/router/synology/synology.go | 49 +++++++++++++++++++++++++++- 3 files changed, 62 insertions(+), 8 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 7a4d5aa..9396090 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -550,13 +550,10 @@ func initCLI() { return } initLogging() - tasks := []task{{s.Restart, true}} - // On Windows, s.Restart will return error unless service is running. - if runtime.GOOS == "windows" { - tasks = []task{ - {s.Start, false}, - {s.Restart, true}, - } + + tasks := []task{ + {s.Stop, false}, + {s.Start, true}, } if doTasks(tasks) { dir, err := userHomeDir() diff --git a/cmd/ctrld/service.go b/cmd/ctrld/service.go index dfec02e..28a70c6 100644 --- a/cmd/ctrld/service.go +++ b/cmd/ctrld/service.go @@ -63,6 +63,16 @@ func (s *sysV) Stop() error { return err } +func (s *sysV) Restart() error { + if !s.installed() { + return service.ErrNotInstalled + } + // We don't care about error returned by s.Stop, + // because the service may already be stopped. + _ = s.Stop() + return s.Start() +} + func (s *sysV) Status() (service.Status, error) { if !s.installed() { return service.StatusUnknown, service.ErrNotInstalled diff --git a/internal/router/synology/synology.go b/internal/router/synology/synology.go index 3ad0388..a367d4a 100644 --- a/internal/router/synology/synology.go +++ b/internal/router/synology/synology.go @@ -27,7 +27,8 @@ func New(cfg *ctrld.Config) *Synology { return &Synology{cfg: cfg} } -func (s *Synology) ConfigureService(config *service.Config) error { +func (s *Synology) ConfigureService(svc *service.Config) error { + svc.Option["UpstartScript"] = upstartScript return nil } @@ -86,3 +87,49 @@ func restartDNSMasq() error { } return nil } + +// Copied from https://github.com/kardianos/service/blob/6fe2824ee8248e776b0f8be39aaeff45a45a4f6c/service_upstart_linux.go#L232 +// With modification to wait for dhcpserver started before ctrld. + +// The upstart script should stop with an INT or the Go runtime will terminate +// the program before the Stop handler can run. +const upstartScript = `# {{.Description}} + +{{if .DisplayName}}description "{{.DisplayName}}"{{end}} + +{{if .HasKillStanza}}kill signal INT{{end}} +{{if .ChRoot}}chroot {{.ChRoot}}{{end}} +{{if .WorkingDirectory}}chdir {{.WorkingDirectory}}{{end}} +start on filesystem or runlevel [2345] +stop on runlevel [!2345] + +start on started dhcpserver + +{{if and .UserName .HasSetUIDStanza}}setuid {{.UserName}}{{end}} + +respawn +respawn limit 10 5 +umask 022 + +console none + +pre-start script + test -x {{.Path}} || { stop; exit 0; } +end script + +# Start +script + {{if .LogOutput}} + stdout_log="/var/log/{{.Name}}.out" + stderr_log="/var/log/{{.Name}}.err" + {{end}} + + if [ -f "/etc/sysconfig/{{.Name}}" ]; then + set -a + source /etc/sysconfig/{{.Name}} + set +a + fi + + exec {{if and .UserName (not .HasSetUIDStanza)}}sudo -E -u {{.UserName}} {{end}}{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}{{if .LogOutput}} >> $stdout_log 2>> $stderr_log{{end}} +end script +` From 5dd633695393d758f1e4bd6cc5cf96d631dc9a23 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Wed, 9 Aug 2023 16:11:51 +0000 Subject: [PATCH 77/84] internal/router/synology: define normal exit condition --- internal/router/synology/synology.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/router/synology/synology.go b/internal/router/synology/synology.go index a367d4a..fb69d8e 100644 --- a/internal/router/synology/synology.go +++ b/internal/router/synology/synology.go @@ -104,6 +104,7 @@ start on filesystem or runlevel [2345] stop on runlevel [!2345] start on started dhcpserver +normal exit 0 TERM HUP {{if and .UserName .HasSetUIDStanza}}setuid {{.UserName}}{{end}} From d292e03d1b060ee0fd882cc06afe30f816e0099b Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 10 Aug 2023 00:22:57 +0700 Subject: [PATCH 78/84] Satisfying staticcheck linter --- client_info.go | 2 +- cmd/ctrld/cli.go | 29 ++++------------------------- cmd/ctrld/prog.go | 11 ----------- internal/clientinfo/mdns.go | 6 ++---- internal/dns/direct.go | 3 ++- internal/dns/manager_linux.go | 2 ++ internal/router/os_freebsd.go | 6 ++++++ internal/router/router.go | 8 +------- 8 files changed, 18 insertions(+), 49 deletions(-) diff --git a/client_info.go b/client_info.go index acd7c5e..c4494f7 100644 --- a/client_info.go +++ b/client_info.go @@ -15,5 +15,5 @@ type LeaseFileFormat string const ( Dnsmasq LeaseFileFormat = "dnsmasq" - IscDhcpd = "isc-dhcpd" + IscDhcpd LeaseFileFormat = "isc-dhcpd" ) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 9396090..4a10928 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -50,10 +50,9 @@ var ( ) var ( - v = viper.NewWithOptions(viper.KeyDelimiter("::")) - defaultConfigWritten = false - defaultConfigFile = "ctrld.toml" - rootCertPool *x509.CertPool + v = viper.NewWithOptions(viper.KeyDelimiter("::")) + defaultConfigFile = "ctrld.toml" + rootCertPool *x509.CertPool ) var basicModeFlags = []string{"listen", "primary_upstream", "secondary_upstream", "domains"} @@ -897,7 +896,6 @@ func readConfigFile(writeDefaultConfig bool) bool { } mainLog.Load().Info().Msg("writing default config file to: " + fp) } - defaultConfigWritten = true return false } @@ -1382,7 +1380,7 @@ func fieldErrorMsg(fe validator.FieldError) string { case "cidr": return fmt.Sprintf("invalid value: %s", fe.Value()) case "required_unless", "required": - return fmt.Sprintf("value is required") + return "value is required" case "dnsrcode": return fmt.Sprintf("invalid DNS rcode value: %s", fe.Value()) case "ipstack": @@ -1396,25 +1394,6 @@ func fieldErrorMsg(fe validator.FieldError) string { return "" } -// couldBeDirectListener reports whether ctrld can be a direct listener on port 53. -// It returns true only if ctrld can listen on port 53 for all interfaces. That means -// there's no other software listening on port 53. -// -// If someone listening on port 53, or ctrld could only listen on port 53 for a specific -// interface, ctrld could only be configured as a DNS forwarder. -func couldBeDirectListener(lc *ctrld.ListenerConfig) bool { - if lc == nil || lc.Port != 53 { - return false - } - switch lc.IP { - case "", "::", "0.0.0.0": - return true - default: - return false - } - -} - func isLoopback(ipStr string) bool { ip := net.ParseIP(ipStr) if ip == nil { diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index 6c16a13..e955c92 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -326,17 +326,6 @@ var ( windowsEADDRINUSE = syscall.Errno(10048) ) -func errUrlConnRefused(err error) bool { - var urlErr *url.Error - if errors.As(err, &urlErr) { - var opErr *net.OpError - if errors.As(urlErr.Err, &opErr) { - return errors.Is(opErr.Err, syscall.ECONNREFUSED) || errors.Is(opErr.Err, windowsECONNREFUSED) - } - } - return false -} - func errUrlNetworkError(err error) bool { var urlErr *url.Error if errors.As(err, &urlErr) { diff --git a/internal/clientinfo/mdns.go b/internal/clientinfo/mdns.go index 5aed579..c9d97e5 100644 --- a/internal/clientinfo/mdns.go +++ b/internal/clientinfo/mdns.go @@ -98,11 +98,9 @@ func (m *mdns) probeLoop(conns []*net.UDPConn, remoteAddr net.Addr, quitCh chan if err != nil { ctrld.ProxyLogger.Load().Warn().Err(err).Msg("error while probing mdns") bo.BackOff(context.Background(), errors.New("mdns probe backoff")) + continue } - select { - case <-quitCh: - break - } + break } <-quitCh for _, conn := range conns { diff --git a/internal/dns/direct.go b/internal/dns/direct.go index e11be05..a825e6d 100644 --- a/internal/dns/direct.go +++ b/internal/dns/direct.go @@ -2,7 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. -//lint:file-ignore U1000 satisfy CI. +//lint:file-ignore U1000 Ignore, this file is forked from upstream code. +//lint:file-ignore ST1005 Ignore, this file is forked from upstream code. package dns diff --git a/internal/dns/manager_linux.go b/internal/dns/manager_linux.go index 1fa1650..2886090 100644 --- a/internal/dns/manager_linux.go +++ b/internal/dns/manager_linux.go @@ -2,6 +2,8 @@ // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. +//lint:file-ignore U1000 Ignore this file, it's a copy. + package dns import ( diff --git a/internal/router/os_freebsd.go b/internal/router/os_freebsd.go index a84fcaa..3b7c0a3 100644 --- a/internal/router/os_freebsd.go +++ b/internal/router/os_freebsd.go @@ -1,6 +1,7 @@ package router import ( + "bytes" "fmt" "net" "os" @@ -116,6 +117,11 @@ func (or *osRouter) Cleanup() error { return nil } +func isPfsense() bool { + b, err := os.ReadFile("/etc/platform") + return err == nil && bytes.HasPrefix(b, []byte("pfSense")) +} + const bsdInitScript = `#!/bin/sh # PROVIDE: {{.Name}} diff --git a/internal/router/router.go b/internal/router/router.go index 90882c9..ad3c641 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -93,8 +93,7 @@ func IsOldOpenwrt() bool { var routerPlatform atomic.Pointer[router] type router struct { - name string - sendClientInfo bool + name string } // Name returns name of the router platform. @@ -241,8 +240,3 @@ func unameU() []byte { out, _ := exec.Command("uname", "-u").Output() return out } - -func isPfsense() bool { - b, err := os.ReadFile("/etc/platform") - return err == nil && bytes.HasPrefix(b, []byte("pfSense")) -} From 32219e7d32c150d3832e7ee622dd2477f3af29e6 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 10 Aug 2023 20:28:29 +0000 Subject: [PATCH 79/84] internal/router: wait ntp synced on Synology --- internal/router/ddwrt/ddwrt.go | 2 +- internal/router/merlin/merlin.go | 2 +- internal/router/ntp/ntp.go | 24 +++++++- internal/router/synology/synology.go | 91 ++++++++++++---------------- internal/router/tomato/tomato.go | 2 +- 5 files changed, 65 insertions(+), 56 deletions(-) diff --git a/internal/router/ddwrt/ddwrt.go b/internal/router/ddwrt/ddwrt.go index dc220ce..9f2dc20 100644 --- a/internal/router/ddwrt/ddwrt.go +++ b/internal/router/ddwrt/ddwrt.go @@ -54,7 +54,7 @@ func (d *Ddwrt) Uninstall(_ *service.Config) error { func (d *Ddwrt) PreRun() error { _ = d.Cleanup() - return ntp.Wait() + return ntp.WaitNvram() } func (d *Ddwrt) Setup() error { diff --git a/internal/router/merlin/merlin.go b/internal/router/merlin/merlin.go index 19d14b3..5abf8f1 100644 --- a/internal/router/merlin/merlin.go +++ b/internal/router/merlin/merlin.go @@ -45,7 +45,7 @@ func (m *Merlin) Uninstall(_ *service.Config) error { func (m *Merlin) PreRun() error { _ = m.Cleanup() - return ntp.Wait() + return ntp.WaitNvram() } func (m *Merlin) Setup() error { diff --git a/internal/router/ntp/ntp.go b/internal/router/ntp/ntp.go index 9854fcf..a9a9409 100644 --- a/internal/router/ntp/ntp.go +++ b/internal/router/ntp/ntp.go @@ -1,16 +1,20 @@ package ntp import ( + "bytes" "context" "errors" "fmt" + "os/exec" "time" - "github.com/Control-D-Inc/ctrld/internal/router/nvram" "tailscale.com/logtail/backoff" + + "github.com/Control-D-Inc/ctrld/internal/router/nvram" ) -func Wait() error { +// WaitNvram waits NTP synced by checking "ntp_ready" value using nvram. +func WaitNvram() error { // Wait until `ntp_ready=1` set. b := backoff.NewBackoff("ntp.Wait", func(format string, args ...any) {}, 10*time.Second) for { @@ -24,3 +28,19 @@ func Wait() error { b.BackOff(context.Background(), errors.New("ntp not ready")) } } + +// WaitUpstart waits NTP synced by checking upstart task "ntpsync" is in "stop/waiting" state. +func WaitUpstart() error { + // Wait until `initctl status ntpsync` returns stop state. + b := backoff.NewBackoff("ntp.WaitUpstart", func(format string, args ...any) {}, 10*time.Second) + for { + out, err := exec.Command("initctl", "status", "ntpsync").CombinedOutput() + if err != nil { + return fmt.Errorf("exec.Command: %w", err) + } + if bytes.Contains(out, []byte("stop/waiting")) { + return nil + } + b.BackOff(context.Background(), errors.New("ntp not ready")) + } +} diff --git a/internal/router/synology/synology.go b/internal/router/synology/synology.go index fb69d8e..7933943 100644 --- a/internal/router/synology/synology.go +++ b/internal/router/synology/synology.go @@ -1,14 +1,21 @@ package synology import ( + "bytes" + "context" + "errors" "fmt" "os" "os/exec" + "strings" + "time" - "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + "github.com/kardianos/service" + "tailscale.com/logtail/backoff" "github.com/Control-D-Inc/ctrld" - "github.com/kardianos/service" + "github.com/Control-D-Inc/ctrld/internal/router/dnsmasq" + "github.com/Control-D-Inc/ctrld/internal/router/ntp" ) const ( @@ -19,16 +26,20 @@ const ( ) type Synology struct { - cfg *ctrld.Config + cfg *ctrld.Config + useUpstart bool } // New returns a router.Router for configuring/setup/run ctrld on Ubios routers. func New(cfg *ctrld.Config) *Synology { - return &Synology{cfg: cfg} + return &Synology{ + cfg: cfg, + useUpstart: service.Platform() == "linux-upstart", + } } func (s *Synology) ConfigureService(svc *service.Config) error { - svc.Option["UpstartScript"] = upstartScript + svc.Option["LogOutput"] = true return nil } @@ -41,6 +52,12 @@ func (s *Synology) Uninstall(_ *service.Config) error { } func (s *Synology) PreRun() error { + if s.useUpstart { + if err := ntp.WaitUpstart(); err != nil { + return err + } + return waitDhcpServer() + } return nil } @@ -88,49 +105,21 @@ func restartDNSMasq() error { return nil } -// Copied from https://github.com/kardianos/service/blob/6fe2824ee8248e776b0f8be39aaeff45a45a4f6c/service_upstart_linux.go#L232 -// With modification to wait for dhcpserver started before ctrld. - -// The upstart script should stop with an INT or the Go runtime will terminate -// the program before the Stop handler can run. -const upstartScript = `# {{.Description}} - -{{if .DisplayName}}description "{{.DisplayName}}"{{end}} - -{{if .HasKillStanza}}kill signal INT{{end}} -{{if .ChRoot}}chroot {{.ChRoot}}{{end}} -{{if .WorkingDirectory}}chdir {{.WorkingDirectory}}{{end}} -start on filesystem or runlevel [2345] -stop on runlevel [!2345] - -start on started dhcpserver -normal exit 0 TERM HUP - -{{if and .UserName .HasSetUIDStanza}}setuid {{.UserName}}{{end}} - -respawn -respawn limit 10 5 -umask 022 - -console none - -pre-start script - test -x {{.Path}} || { stop; exit 0; } -end script - -# Start -script - {{if .LogOutput}} - stdout_log="/var/log/{{.Name}}.out" - stderr_log="/var/log/{{.Name}}.err" - {{end}} - - if [ -f "/etc/sysconfig/{{.Name}}" ]; then - set -a - source /etc/sysconfig/{{.Name}} - set +a - fi - - exec {{if and .UserName (not .HasSetUIDStanza)}}sudo -E -u {{.UserName}} {{end}}{{.Path}}{{range .Arguments}} {{.|cmd}}{{end}}{{if .LogOutput}} >> $stdout_log 2>> $stderr_log{{end}} -end script -` +func waitDhcpServer() error { + // Wait until `initctl status dhcpserver` returns running state. + b := backoff.NewBackoff("waitDhcpServer", func(format string, args ...any) {}, 10*time.Second) + for { + out, err := exec.Command("initctl", "status", "dhcpserver").CombinedOutput() + if err != nil { + if strings.Contains(err.Error(), "Unknown job") { + // dhcpserver service does not exist. + return nil + } + return fmt.Errorf("exec.Command: %w", err) + } + if bytes.Contains(out, []byte("start/running")) { + return nil + } + b.BackOff(context.Background(), errors.New("ntp not ready")) + } +} diff --git a/internal/router/tomato/tomato.go b/internal/router/tomato/tomato.go index 40a70e5..cd8df70 100644 --- a/internal/router/tomato/tomato.go +++ b/internal/router/tomato/tomato.go @@ -49,7 +49,7 @@ func (f *FreshTomato) Uninstall(_ *service.Config) error { func (f *FreshTomato) PreRun() error { _ = f.Cleanup() - return ntp.Wait() + return ntp.WaitNvram() } func (f *FreshTomato) Setup() error { From ab8f07238801310dbeaaaac12d823fd6adacb637 Mon Sep 17 00:00:00 2001 From: Yegor Sak Date: Thu, 10 Aug 2023 20:54:47 +0000 Subject: [PATCH 80/84] Update README.md --- README.md | 87 +++++++++++++------------------------------------- docs/config.md | 47 +++++++++++++++++++++++++-- 2 files changed, 68 insertions(+), 66 deletions(-) diff --git a/README.md b/README.md index 9839d88..20f9047 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A highly configurable DNS forwarding proxy with support for: - Multiple network policy driven DNS query steering - Policy driven domain based "split horizon" DNS with wildcard support - Integrations with common router vendors and firmware +- LAN client discovery via DHCP, mDNS, and ARP ## TLDR Proxy legacy DNS traffic to secure DNS upstreams in highly configurable ways. @@ -51,6 +52,11 @@ Windows user and prefer Powershell (who doesn't)? No problem, execute this comma powershell -Command "(Invoke-WebRequest -Uri 'https://api.controld.com/dl' -UseBasicParsing).Content | Set-Content 'ctrld_install.bat'" && ctrld_install.bat ``` +Or you can pull and run a Docker container from [Docker Hub](https://hub.docker.com/r/controldns/ctrld) +``` +$ docker pull controldns/ctrld +``` + ## Download Manually Alternatively, if you know what you're doing you can download pre-compiled binaries from the [Releases](https://github.com/Control-D-Inc/ctrld/releases) section for the appropriate platform. @@ -67,19 +73,13 @@ or $ go install github.com/Control-D-Inc/ctrld/cmd/ctrld@latest ``` -## Docker +or ``` $ docker build -t controld/ctrld . -$ docker run -d --name=ctrld -p 53:53/tcp -p 53:53/udp controld/ctrld --cd=p2 -vv +$ docker run -d --name=ctrld -p 53:53/tcp -p 53:53/udp controld/ctrld --cd=RESOLVER_ID_GOES_HERE -vv ``` ------ -*NOTE* -When running inside container, and listener address is set to "127.0.0.1:53", `ctrld` will change -the listen address to "0.0.0.0:53", so users can expose the port to outside. - ----- # Usage The cli is self documenting, so free free to run `--help` on any sub-command to get specific usages. @@ -101,16 +101,10 @@ Available Commands: service Manage ctrld service start Quick start service and configure DNS on interface stop Quick stop service and remove DNS from interface - setup Auto-setup Control D on a router. - -Supported platforms: - - ₒ ddwrt - ₒ merlin - ₒ openwrt - ₒ ubios - ₒ auto - detect the platform you are running on - + restart Restart the ctrld service + status Show status of the ctrld service + uninstall Stop and uninstall the ctrld service + clients Manage clients Flags: -h, --help help for ctrld @@ -138,61 +132,30 @@ To start the server with default configuration, simply run: `./ctrld run`. This If `verify.controld.com` resolves, you're successfully using the default Control D upstream. From here, you can start editing the config file and go nuts with it. To enforce a new config, restart the server. ## Service Mode -To run the application in service mode on any Windows, MacOS or Linux distibution, simply run: `./ctrld start` as system/root user. This will create a generic `ctrld.toml` file in the **user home** directory (on Windows) or `/etc/controld/` (everywhere else), start the system service, and configure the listener on the default network interface. Service will start on OS boot. +To run the application in service mode on any Windows, MacOS, Linux distibution or supported router, simply run: `./ctrld start` as system/root user. This will create a generic `ctrld.toml` file in the **user home** directory (on Windows) or `/etc/controld/` (almost everywhere else), start the system service, and configure the listener on the default network interface. Service will start on OS boot. -In order to stop the service, and restore your DNS to original state, simply run `./ctrld stop`. If you wish to uninstall the service permanently, run `./ctrld service uninstall`. +When Control D upstreams are used, `ctrld` willl [relay your network topology](https://docs.controld.com/docs/device-clients) to Control D (LAN IPs, MAC addresses, and hostnames), and you will be able to see your LAN devices in the web panel, view analytics and apply unique profiles to them. -For granular control of the service, run the `service` command. Each sub-command has its own help section so you can see what arguments you can supply. +In order to stop the service, and restore your DNS to original state, simply run `./ctrld stop`. If you wish to stop and uninstall the service permanently, run `./ctrld uninstall`. -``` - Manage ctrld service - Usage: - ctrld service [command] - - Available Commands: - interfaces Manage network interfaces - restart Restart the ctrld service - start Start the ctrld service - status Show status of the ctrld service - stop Stop the ctrld service - uninstall Uninstall the ctrld service - - Flags: - -h, --help help for service - - Global Flags: - -v, --verbose count verbose log output, "-v" basic logging, "-vv" debug level logging - - Use "ctrld service [command] --help" for more information about a command. -``` - -## Router Mode +### Supported Routers You can run `ctrld` on any supported router, which will function similarly to the Service Mode mentioned above. The list of supported routers and firmware includes: - Asus Merlin - DD-WRT +- Firewalla - FreshTomato - GL.iNet - OpenWRT -- pfSense +- pfSense / OPNsense - Synology - Ubiquiti (UniFi, EdgeOS) -In order to start `ctrld` as a DNS provider, simply run `./ctrld setup auto` command. +`ctrld` will attempt to interface with dnsmasq whenever possible and set itself as the upstream, while running on port 5354. On FreeBSD based OSes, `ctrld` will terminate dnsmasq and unbound in order to be able to listen on port 53 directly. -In this mode, and when Control D upstreams are used, the router will [relay your network topology](https://docs.controld.com/docs/device-clients) to Control D (LAN IPs, MAC addresses, and hostnames), and you will be able to see your LAN devices in the web panel, view analytics and apply unique profiles to them. - ----- -*NOTE* - -`ctrld` will try leveraging default setup on routers, so changing routers default configuration would causes things won't work as expected (For example, changing dnsmasq configuration). - -Advanced users who want to play around, just run `ctrld` in [Service Mode](#service-mode) with custom configuration. - ----- ### Control D Auto Configuration -Application can be started with a specific resolver config, instead of the default one. Simply supply your Resolver ID with a `--cd` flag, when using the `run` (foreground) or `start` (service) or `setup` (router) modes. +Application can be started with a specific resolver config, instead of the default one. Simply supply your Resolver ID with a `--cd` flag, when using the `run` (foreground) or `start` (service) modes. The following command will start the application in foreground mode, using the free "p2" resolver, which blocks Ads & Trackers. @@ -206,14 +169,9 @@ Alternatively, you can use your own personal Control D Device resolver, and star ./ctrld start --cd abcd1234 ``` -You can do the same while starting in router mode: -```shell -./ctrld setup auto --cd abcd1234 -``` - -Once you run the above commands (in service or router modes only), the following things will happen: +Once you run the above commands (in service mode only), the following things will happen: - You resolver configuration will be fetched from the API, and config file templated with the resolver data -- Application will start as a service, and keep running (even after reboot) until you run the `stop` or `service uninstall` sub-commands +- Application will start as a service, and keep running (even after reboot) until you run the `stop` or `uninstall` sub-commands - Your default network interface will be updated to use the listener started by the service - All OS DNS queries will be sent to the listener @@ -274,4 +232,5 @@ See [Contribution Guideline](./docs/contributing.md) The following functionality is on the roadmap and will be available in future releases. - Prometheus metrics exporter - DNS intercept mode +- Direct listener mode - Support for more routers (let us know which ones) diff --git a/docs/config.md b/docs/config.md index c652afe..f2b5554 100644 --- a/docs/config.md +++ b/docs/config.md @@ -165,6 +165,49 @@ Tweaking this value depends on the capacity of your system. - Required: no - Default: 256 +### discover_mdns +Perform LAN client discovery using mDNS. This will spawn a listener on port 5353. + +- Type: boolean +- Required: no +- Default: true + +### discover_arp +Perform LAN client discovery using ARP. + +- Type: boolean +- Required: no +- Default: true + +### discover_dhcp +Perform LAN client discovery using DHCP leases files. Common file locations are auto-discovered. + +- Type: boolean +- Required: no +- Default: true + +### discover_ptr +Perform LAN client discovery using PTR queries. + +- Type: boolean +- Required: no +- Default: true + +### dhcp_lease_file_path +Relative or absolute path to a custom DHCP leases file location. + +- Type: string +- Required: no +- Default: "" + +### dhcp_lease_file_format +DHCP leases file format. + +- Type: string +- Required: no +- Valid values: `dnsmasq`, `isc-dhcp` +- Default: "" + ## Upstream The `[upstream]` section specifies the DNS upstream servers that `ctrld` will forward DNS requests to. @@ -316,14 +359,14 @@ IP address that serves the incoming requests. If `ip` is empty, ctrld will liste - Type: ip address string - Required: no -- Default: "" or "127.0.0.1" in [Router Mode](../README.md#router-mode). +- Default: "0.0.0.0" or RFC1918 addess or "127.0.0.1" (depending on platform) ### port Port number that the listener will listen on for incoming requests. If `port` is `0`, a random available port will be chosen. - Type: number - Required: no -- Default: 0 or 5354 in [Router Mode](../README.md#router-mode). +- Default: 0 or 53 or 5354 (depending on platform) ### restricted If set to `true` makes the listener `REFUSE` DNS queries from all source IP addresses that are not explicitly defined in the policy using a `network`. From 0c096d5f07c7bcff3cfbd7c716203ddd4db5e64d Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 11 Aug 2023 16:11:04 +0000 Subject: [PATCH 81/84] internal/router: make router.Cleanup idempotent On routers where we want to wait for NTP by checking nvram key. Before waiting, we clean up the router to ensure it's restored to original state. However, router.Cleanup is not idempotent, causing dnsmasq restarted. On tomato/ddwrt, restarting have no delay, and spawning new dnsmasq process immediately. On merlin, somehow it takes time to spawn new dnsmasq process, causing ctrld wrongly think there's no one listening on port 53. Fixing this by ensuring router.Cleanup is idempotent. While at it, also adding "ntp_done" to nvram key, which is now using on latest ddwrt. --- cmd/ctrld/os_linux.go | 10 ++++++++++ internal/router/ddwrt/ddwrt.go | 14 ++++++++------ internal/router/merlin/merlin.go | 19 ++++++++++++++----- internal/router/ntp/ntp.go | 15 +++++++++------ internal/router/tomato/tomato.go | 14 ++++++++------ 5 files changed, 49 insertions(+), 23 deletions(-) diff --git a/cmd/ctrld/os_linux.go b/cmd/ctrld/os_linux.go index e26e396..7a4efae 100644 --- a/cmd/ctrld/os_linux.go +++ b/cmd/ctrld/os_linux.go @@ -24,6 +24,8 @@ import ( "github.com/Control-D-Inc/ctrld/internal/resolvconffile" ) +const resolvConfBackupFailedMsg = "open /etc/resolv.pre-ctrld-backup.conf: read-only file system" + // allocate loopback ip // sudo ip a add 127.0.0.2/24 dev lo func allocateIP(ip string) error { @@ -73,6 +75,14 @@ func setDNS(iface *net.Interface, nameservers []string) error { trySystemdResolve = true break } + // This error happens on read-only file system, which causes ctrld failed to create backup + // for /etc/resolv.conf file. It is ok, because the DNS is still set anyway, and restore + // DNS will fallback to use DHCP if there's no backup /etc/resolv.conf file. + // The error format is controlled by us, so checking for error string is fine. + // See: ../../internal/dns/direct.go:L278 + if r.Mode() == "direct" && strings.Contains(err.Error(), resolvConfBackupFailedMsg) { + return nil + } return err } currentNS := currentDNS(iface) diff --git a/internal/router/ddwrt/ddwrt.go b/internal/router/ddwrt/ddwrt.go index 9f2dc20..edd7e6b 100644 --- a/internal/router/ddwrt/ddwrt.go +++ b/internal/router/ddwrt/ddwrt.go @@ -87,12 +87,14 @@ func (d *Ddwrt) Cleanup() error { if d.cfg.FirstListener().IsDirectDnsListener() { return nil } - if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { - nvramKvMap["dnsmasq_options"] = "" - // Restore old configs. - if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } + if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" { + return nil // was restored, nothing to do. + } + + nvramKvMap["dnsmasq_options"] = "" + // Restore old configs. + if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { + return err } // Restart dnsmasq service. diff --git a/internal/router/merlin/merlin.go b/internal/router/merlin/merlin.go index 5abf8f1..84ebd1c 100644 --- a/internal/router/merlin/merlin.go +++ b/internal/router/merlin/merlin.go @@ -52,6 +52,13 @@ func (m *Merlin) Setup() error { if m.cfg.FirstListener().IsDirectDnsListener() { return nil } + // Already setup. + if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { + return nil + } + if _, err := nvram.Run("set", nvram.CtrldSetupKey+"=1"); err != nil { + return err + } buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath) // Already setup. if bytes.Contains(buf, []byte(dnsmasq.MerlinPostConfMarker)) { @@ -92,11 +99,13 @@ func (m *Merlin) Cleanup() error { if m.cfg.FirstListener().IsDirectDnsListener() { return nil } - if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { - // Restore old configs. - if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } + if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" { + return nil // was restored, nothing to do. + } + + // Restore old configs. + if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { + return err } buf, err := os.ReadFile(dnsmasq.MerlinPostConfPath) diff --git a/internal/router/ntp/ntp.go b/internal/router/ntp/ntp.go index a9a9409..5c04a36 100644 --- a/internal/router/ntp/ntp.go +++ b/internal/router/ntp/ntp.go @@ -18,12 +18,15 @@ func WaitNvram() error { // Wait until `ntp_ready=1` set. b := backoff.NewBackoff("ntp.Wait", func(format string, args ...any) {}, 10*time.Second) for { - out, err := nvram.Run("get", "ntp_ready") - if err != nil { - return fmt.Errorf("PreStart: nvram: %w", err) - } - if out == "1" { - return nil + // ddwrt use "ntp_done": https://github.com/mirror/dd-wrt/blob/a08c693527ab3204bf7bebd408a7c9a83b6ede47/src/router/rc/ntp.c#L100 + for _, key := range []string{"ntp_ready", "ntp_done"} { + out, err := nvram.Run("get", key) + if err != nil { + return fmt.Errorf("PreStart: nvram: %w", err) + } + if out == "1" { + return nil + } } b.BackOff(context.Background(), errors.New("ntp not ready")) } diff --git a/internal/router/tomato/tomato.go b/internal/router/tomato/tomato.go index cd8df70..ee5f09b 100644 --- a/internal/router/tomato/tomato.go +++ b/internal/router/tomato/tomato.go @@ -89,12 +89,14 @@ func (f *FreshTomato) Cleanup() error { if f.cfg.FirstListener().IsDirectDnsListener() { return nil } - if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val == "1" { - nvramKvMap["dnsmasq_custom"] = "" - // Restore old configs. - if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { - return err - } + if val, _ := nvram.Run("get", nvram.CtrldSetupKey); val != "1" { + return nil // was restored, nothing to do. + } + + nvramKvMap["dnsmasq_custom"] = "" + // Restore old configs. + if err := nvram.Restore(nvramKvMap, nvram.CtrldSetupKey); err != nil { + return err } // Restart dnscrypt-proxy service. From 4896563e3c6a0a3b9ad5a62b24573b8a0de29e59 Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 11 Aug 2023 16:13:59 +0000 Subject: [PATCH 82/84] Various improvements and bug fixes - Watch more events for lease file changes - Improving network up detection by using bootstrap IPv6 along side IPv4 one. - Emitting log to notice user that ctrld is starting. - Using systemd wrapper to provide correct status. - Restoring DNS on stop on Windows. --- cmd/ctrld/cli.go | 13 +++---------- cmd/ctrld/prog.go | 14 ++++++++++++++ cmd/ctrld/prog_darwin.go | 11 ----------- cmd/ctrld/prog_freebsd.go | 6 ------ cmd/ctrld/prog_linux.go | 6 ------ cmd/ctrld/prog_others.go | 2 -- cmd/ctrld/service.go | 16 ++++++++++++++++ internal/clientinfo/dhcp.go | 2 +- internal/net/net.go | 37 +++++++++++++++++++++++++++++-------- 9 files changed, 63 insertions(+), 44 deletions(-) diff --git a/cmd/ctrld/cli.go b/cmd/ctrld/cli.go index 4a10928..8913b0f 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/ctrld/cli.go @@ -429,6 +429,7 @@ func initCLI() { {s.Install, false}, {s.Start, true}, } + mainLog.Load().Notice().Msg("Starting service") if doTasks(tasks) { if err := p.router.Install(sc); err != nil { mainLog.Load().Warn().Err(err).Msg("post installation failed, please check system/service log for details error") @@ -450,12 +451,7 @@ func initCLI() { uninstall(p, s) os.Exit(1) } - // On Linux, Darwin, Freebsd, ctrld set DNS on startup, because the DNS setting could be - // reset after rebooting. On windows, we only need to set once here. See prog.preRun in - // prog_*.go file for dedicated code on each platform. (1) - if runtime.GOOS == "windows" { - p.setDNS() - } + p.setDNS() } }, } @@ -524,10 +520,7 @@ func initCLI() { initLogging() if doTasks([]task{{s.Stop, true}}) { p.router.Cleanup() - // See comment (1) in startCmd. - if runtime.GOOS != "windows" { - p.resetDNS() - } + p.resetDNS() mainLog.Load().Notice().Msg("Service stopped") } }, diff --git a/cmd/ctrld/prog.go b/cmd/ctrld/prog.go index e955c92..fab10cc 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/ctrld/prog.go @@ -7,6 +7,7 @@ import ( "net" "net/url" "os" + "runtime" "strconv" "sync" "syscall" @@ -63,6 +64,19 @@ func (p *prog) Start(s service.Service) error { return nil } +func (p *prog) preRun() { + if !service.Interactive() { + p.setDNS() + } + if runtime.GOOS == "darwin" { + p.onStopped = append(p.onStopped, func() { + if !service.Interactive() { + p.resetDNS() + } + }) + } +} + func (p *prog) run() { // Wait the caller to signal that we can do our logic. <-p.waitCh diff --git a/cmd/ctrld/prog_darwin.go b/cmd/ctrld/prog_darwin.go index 4d9ad0a..1a6656d 100644 --- a/cmd/ctrld/prog_darwin.go +++ b/cmd/ctrld/prog_darwin.go @@ -4,17 +4,6 @@ import ( "github.com/kardianos/service" ) -func (p *prog) preRun() { - if !service.Interactive() { - p.setDNS() - } - p.onStopped = append(p.onStopped, func() { - if !service.Interactive() { - p.resetDNS() - } - }) -} - func setDependencies(svc *service.Config) {} func setWorkingDirectory(svc *service.Config, dir string) { diff --git a/cmd/ctrld/prog_freebsd.go b/cmd/ctrld/prog_freebsd.go index 63d8179..283e03c 100644 --- a/cmd/ctrld/prog_freebsd.go +++ b/cmd/ctrld/prog_freebsd.go @@ -6,12 +6,6 @@ import ( "github.com/kardianos/service" ) -func (p *prog) preRun() { - if !service.Interactive() { - p.setDNS() - } -} - func setDependencies(svc *service.Config) { // TODO(cuonglm): remove once https://github.com/kardianos/service/issues/359 fixed. _ = os.MkdirAll("/usr/local/etc/rc.d", 0755) diff --git a/cmd/ctrld/prog_linux.go b/cmd/ctrld/prog_linux.go index 58332bb..f14a054 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/ctrld/prog_linux.go @@ -13,12 +13,6 @@ func init() { } } -func (p *prog) preRun() { - if !service.Interactive() { - p.setDNS() - } -} - func setDependencies(svc *service.Config) { svc.Dependencies = []string{ "Wants=network-online.target", diff --git a/cmd/ctrld/prog_others.go b/cmd/ctrld/prog_others.go index 50fcf0d..7d70825 100644 --- a/cmd/ctrld/prog_others.go +++ b/cmd/ctrld/prog_others.go @@ -4,8 +4,6 @@ package main import "github.com/kardianos/service" -func (p *prog) preRun() {} - func setDependencies(svc *service.Config) {} func setWorkingDirectory(svc *service.Config, dir string) { diff --git a/cmd/ctrld/service.go b/cmd/ctrld/service.go index 28a70c6..263dfd8 100644 --- a/cmd/ctrld/service.go +++ b/cmd/ctrld/service.go @@ -26,6 +26,8 @@ func newService(i service.Interface, c *service.Config) (service.Service, error) return &sysV{s}, nil case s.Platform() == "unix-systemv": return &sysV{s}, nil + case s.Platform() == "linux-systemd": + return &systemd{s}, nil } return s, nil } @@ -105,6 +107,20 @@ func (s *procd) Status() (service.Status, error) { return service.StatusRunning, nil } +// procd wraps a service.Service, and provide status command to +// report the status correctly. +type systemd struct { + service.Service +} + +func (s *systemd) Status() (service.Status, error) { + out, _ := exec.Command("systemctl", "status", "ctrld").CombinedOutput() + if bytes.Contains(out, []byte("/FAILURE)")) { + return service.StatusStopped, nil + } + return s.Service.Status() +} + type task struct { f func() error abortOnError bool diff --git a/internal/clientinfo/dhcp.go b/internal/clientinfo/dhcp.go index a91bdb9..27e2bf4 100644 --- a/internal/clientinfo/dhcp.go +++ b/internal/clientinfo/dhcp.go @@ -53,7 +53,7 @@ func (d *dhcp) watchChanges() { if !ok { return } - if event.Has(fsnotify.Write) { + if event.Has(fsnotify.Write) || event.Has(fsnotify.Rename) || event.Has(fsnotify.Chmod) || event.Has(fsnotify.Remove) { format := clientInfoFiles[event.Name] if err := d.readLeaseFile(event.Name, format); err != nil && !os.IsNotExist(err) { ctrld.ProxyLogger.Load().Err(err).Str("file", event.Name).Msg("leases file changed but failed to update client info") diff --git a/internal/net/net.go b/internal/net/net.go index 9b47f2d..5f2c509 100644 --- a/internal/net/net.go +++ b/internal/net/net.go @@ -4,8 +4,11 @@ import ( "context" "errors" "net" + "os" + "os/signal" "sync" "sync/atomic" + "syscall" "time" "tailscale.com/logtail/backoff" @@ -13,17 +16,17 @@ import ( const ( controldIPv6Test = "ipv6.controld.io" - bootstrapDNS = "76.76.2.0:53" + v4BootstrapDNS = "76.76.2.0:53" + v6BootstrapDNS = "[2606:1a40::]:53" ) var Dialer = &net.Dialer{ Resolver: &net.Resolver{ PreferGo: true, Dial: func(ctx context.Context, network, address string) (net.Conn, error) { - d := net.Dialer{ - Timeout: 10 * time.Second, - } - return d.DialContext(ctx, "udp", bootstrapDNS) + d := ParallelDialer{} + d.Timeout = 10 * time.Second + return d.DialContext(ctx, "udp", []string{v4BootstrapDNS, v6BootstrapDNS}) }, }, } @@ -59,14 +62,32 @@ func supportListenIPv6Local() bool { } func probeStack() { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + go func() { + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + <-sigs + cancel() + }() + b := backoff.NewBackoff("probeStack", func(format string, args ...any) {}, 5*time.Second) for { - if _, err := probeStackDialer.Dial("udp", bootstrapDNS); err == nil { + if _, err := probeStackDialer.DialContext(ctx, "udp", v4BootstrapDNS); err == nil { hasNetworkUp = true break - } else { - b.BackOff(context.Background(), err) } + if _, err := probeStackDialer.DialContext(ctx, "udp", v6BootstrapDNS); err == nil { + hasNetworkUp = true + break + } + select { + case <-ctx.Done(): + return + default: + } + b.BackOff(context.Background(), errors.New("network is down")) } canListenIPv6Local = supportListenIPv6Local() } From 829e93c07966beaf6f45d75fd666643bcf1aeafe Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Tue, 15 Aug 2023 11:15:37 +0000 Subject: [PATCH 83/84] cmd: allow import/running ctrld as library --- cmd/{ctrld => cli}/cli.go | 2 +- cmd/{ctrld => cli}/cli_test.go | 2 +- cmd/{ctrld => cli}/conn.go | 2 +- cmd/{ctrld => cli}/control_client.go | 2 +- cmd/{ctrld => cli}/control_server.go | 2 +- cmd/{ctrld => cli}/control_server_test.go | 2 +- cmd/{ctrld => cli}/dns.go | 2 +- cmd/{ctrld => cli}/dns_proxy.go | 2 +- cmd/{ctrld => cli}/dns_proxy_test.go | 2 +- cmd/cli/main.go | 161 +++++++++++++++++++ cmd/{ctrld => cli}/main_test.go | 2 +- cmd/{ctrld => cli}/net_darwin.go | 2 +- cmd/{ctrld => cli}/net_darwin_test.go | 2 +- cmd/{ctrld => cli}/net_others.go | 2 +- cmd/{ctrld => cli}/netlink_linux.go | 2 +- cmd/{ctrld => cli}/netlink_others.go | 2 +- cmd/{ctrld => cli}/network_manager_linux.go | 2 +- cmd/{ctrld => cli}/network_manager_others.go | 2 +- cmd/{ctrld => cli}/os_darwin.go | 2 +- cmd/{ctrld => cli}/os_freebsd.go | 2 +- cmd/{ctrld => cli}/os_linux.go | 2 +- cmd/{ctrld => cli}/os_linux_test.go | 2 +- cmd/{ctrld => cli}/os_others.go | 2 +- cmd/{ctrld => cli}/os_windows.go | 2 +- cmd/{ctrld => cli}/prog.go | 2 +- cmd/{ctrld => cli}/prog_darwin.go | 2 +- cmd/{ctrld => cli}/prog_freebsd.go | 2 +- cmd/{ctrld => cli}/prog_linux.go | 2 +- cmd/{ctrld => cli}/prog_others.go | 2 +- cmd/{ctrld => cli}/sema.go | 2 +- cmd/{ctrld => cli}/service.go | 2 +- cmd/{ctrld => cli}/service_others.go | 2 +- cmd/{ctrld => cli}/service_windows.go | 2 +- cmd/ctrld/main.go | 158 +----------------- 34 files changed, 195 insertions(+), 188 deletions(-) rename cmd/{ctrld => cli}/cli.go (99%) rename cmd/{ctrld => cli}/cli_test.go (97%) rename cmd/{ctrld => cli}/conn.go (99%) rename cmd/{ctrld => cli}/control_client.go (97%) rename cmd/{ctrld => cli}/control_server.go (99%) rename cmd/{ctrld => cli}/control_server_test.go (98%) rename cmd/{ctrld => cli}/dns.go (86%) rename cmd/{ctrld => cli}/dns_proxy.go (99%) rename cmd/{ctrld => cli}/dns_proxy_test.go (99%) create mode 100644 cmd/cli/main.go rename cmd/{ctrld => cli}/main_test.go (93%) rename cmd/{ctrld => cli}/net_darwin.go (98%) rename cmd/{ctrld => cli}/net_darwin_test.go (98%) rename cmd/{ctrld => cli}/net_others.go (88%) rename cmd/{ctrld => cli}/netlink_linux.go (97%) rename cmd/{ctrld => cli}/netlink_others.go (80%) rename cmd/{ctrld => cli}/network_manager_linux.go (99%) rename cmd/{ctrld => cli}/network_manager_others.go (93%) rename cmd/{ctrld => cli}/os_darwin.go (99%) rename cmd/{ctrld => cli}/os_freebsd.go (99%) rename cmd/{ctrld => cli}/os_linux.go (99%) rename cmd/{ctrld => cli}/os_linux_test.go (97%) rename cmd/{ctrld => cli}/os_others.go (93%) rename cmd/{ctrld => cli}/os_windows.go (99%) rename cmd/{ctrld => cli}/prog.go (99%) rename cmd/{ctrld => cli}/prog_darwin.go (93%) rename cmd/{ctrld => cli}/prog_freebsd.go (95%) rename cmd/{ctrld => cli}/prog_linux.go (98%) rename cmd/{ctrld => cli}/prog_others.go (95%) rename cmd/{ctrld => cli}/sema.go (96%) rename cmd/{ctrld => cli}/service.go (99%) rename cmd/{ctrld => cli}/service_others.go (90%) rename cmd/{ctrld => cli}/service_windows.go (96%) diff --git a/cmd/ctrld/cli.go b/cmd/cli/cli.go similarity index 99% rename from cmd/ctrld/cli.go rename to cmd/cli/cli.go index 8913b0f..894efc0 100644 --- a/cmd/ctrld/cli.go +++ b/cmd/cli/cli.go @@ -1,4 +1,4 @@ -package main +package cli import ( "bytes" diff --git a/cmd/ctrld/cli_test.go b/cmd/cli/cli_test.go similarity index 97% rename from cmd/ctrld/cli_test.go rename to cmd/cli/cli_test.go index 23746b7..01f2586 100644 --- a/cmd/ctrld/cli_test.go +++ b/cmd/cli/cli_test.go @@ -1,4 +1,4 @@ -package main +package cli import ( "os" diff --git a/cmd/ctrld/conn.go b/cmd/cli/conn.go similarity index 99% rename from cmd/ctrld/conn.go rename to cmd/cli/conn.go index a627935..82e6468 100644 --- a/cmd/ctrld/conn.go +++ b/cmd/cli/conn.go @@ -1,4 +1,4 @@ -package main +package cli import ( "net" diff --git a/cmd/ctrld/control_client.go b/cmd/cli/control_client.go similarity index 97% rename from cmd/ctrld/control_client.go rename to cmd/cli/control_client.go index 8a41193..c626602 100644 --- a/cmd/ctrld/control_client.go +++ b/cmd/cli/control_client.go @@ -1,4 +1,4 @@ -package main +package cli import ( "context" diff --git a/cmd/ctrld/control_server.go b/cmd/cli/control_server.go similarity index 99% rename from cmd/ctrld/control_server.go rename to cmd/cli/control_server.go index 5118113..5f5ac51 100644 --- a/cmd/ctrld/control_server.go +++ b/cmd/cli/control_server.go @@ -1,4 +1,4 @@ -package main +package cli import ( "context" diff --git a/cmd/ctrld/control_server_test.go b/cmd/cli/control_server_test.go similarity index 98% rename from cmd/ctrld/control_server_test.go rename to cmd/cli/control_server_test.go index 2bcd64a..297b37d 100644 --- a/cmd/ctrld/control_server_test.go +++ b/cmd/cli/control_server_test.go @@ -1,4 +1,4 @@ -package main +package cli import ( "bytes" diff --git a/cmd/ctrld/dns.go b/cmd/cli/dns.go similarity index 86% rename from cmd/ctrld/dns.go rename to cmd/cli/dns.go index 770a630..cf9d779 100644 --- a/cmd/ctrld/dns.go +++ b/cmd/cli/dns.go @@ -1,4 +1,4 @@ -package main +package cli //lint:ignore U1000 use in os_linux.go type getDNS func(iface string) []string diff --git a/cmd/ctrld/dns_proxy.go b/cmd/cli/dns_proxy.go similarity index 99% rename from cmd/ctrld/dns_proxy.go rename to cmd/cli/dns_proxy.go index 81a373e..23ae03e 100644 --- a/cmd/ctrld/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -1,4 +1,4 @@ -package main +package cli import ( "context" diff --git a/cmd/ctrld/dns_proxy_test.go b/cmd/cli/dns_proxy_test.go similarity index 99% rename from cmd/ctrld/dns_proxy_test.go rename to cmd/cli/dns_proxy_test.go index 3245875..b7b0dbd 100644 --- a/cmd/ctrld/dns_proxy_test.go +++ b/cmd/cli/dns_proxy_test.go @@ -1,4 +1,4 @@ -package main +package cli import ( "context" diff --git a/cmd/cli/main.go b/cmd/cli/main.go new file mode 100644 index 0000000..e7376be --- /dev/null +++ b/cmd/cli/main.go @@ -0,0 +1,161 @@ +package cli + +import ( + "io" + "os" + "path/filepath" + "sync/atomic" + "time" + + "github.com/kardianos/service" + "github.com/rs/zerolog" + + "github.com/Control-D-Inc/ctrld" +) + +var ( + configPath string + configBase64 string + daemon bool + listenAddress string + primaryUpstream string + secondaryUpstream string + domains []string + logPath string + homedir string + cacheSize int + cfg ctrld.Config + verbose int + silent bool + cdUID string + cdOrg string + cdDev bool + iface string + ifaceStartStop string + + mainLog atomic.Pointer[zerolog.Logger] + consoleWriter zerolog.ConsoleWriter +) + +func init() { + l := zerolog.New(io.Discard) + mainLog.Store(&l) +} + +func Main() { + ctrld.InitConfig(v, "ctrld") + initCLI() + if err := rootCmd.Execute(); err != nil { + mainLog.Load().Error().Msg(err.Error()) + os.Exit(1) + } +} + +func normalizeLogFilePath(logFilePath string) string { + if logFilePath == "" || filepath.IsAbs(logFilePath) || service.Interactive() { + return logFilePath + } + if homedir != "" { + return filepath.Join(homedir, logFilePath) + } + dir, _ := userHomeDir() + if dir == "" { + return logFilePath + } + return filepath.Join(dir, logFilePath) +} + +func initConsoleLogging() { + consoleWriter = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) { + w.TimeFormat = time.StampMilli + }) + multi := zerolog.MultiLevelWriter(consoleWriter) + l := mainLog.Load().Output(multi).With().Timestamp().Logger() + mainLog.Store(&l) + switch { + case silent: + zerolog.SetGlobalLevel(zerolog.NoLevel) + case verbose == 1: + zerolog.SetGlobalLevel(zerolog.InfoLevel) + case verbose > 1: + zerolog.SetGlobalLevel(zerolog.DebugLevel) + default: + zerolog.SetGlobalLevel(zerolog.NoticeLevel) + } +} + +// initLogging initializes global logging setup. +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} + if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" { + // Create parent directory if necessary. + if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil { + mainLog.Load().Error().Msgf("failed to create log path: %v", err) + os.Exit(1) + } + + // Default open log file in append mode. + 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.Load().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, flags, os.FileMode(0o600)) + if err != nil { + mainLog.Load().Error().Msgf("failed to create log file: %v", err) + os.Exit(1) + } + writers = append(writers, logFile) + } + writers = append(writers, consoleWriter) + multi := zerolog.MultiLevelWriter(writers...) + l := mainLog.Load().Output(multi).With().Timestamp().Logger() + mainLog.Store(&l) + // TODO: find a better way. + ctrld.ProxyLogger.Store(&l) + + zerolog.SetGlobalLevel(zerolog.NoticeLevel) + logLevel := cfg.Service.LogLevel + switch { + case silent: + zerolog.SetGlobalLevel(zerolog.NoLevel) + return + case verbose == 1: + logLevel = "info" + case verbose > 1: + logLevel = "debug" + } + if logLevel == "" { + return + } + level, err := zerolog.ParseLevel(logLevel) + if err != nil { + mainLog.Load().Warn().Err(err).Msg("could not set log level") + return + } + zerolog.SetGlobalLevel(level) +} + +func initCache() { + if !cfg.Service.CacheEnable { + return + } + if cfg.Service.CacheSize == 0 { + cfg.Service.CacheSize = 4096 + } +} diff --git a/cmd/ctrld/main_test.go b/cmd/cli/main_test.go similarity index 93% rename from cmd/ctrld/main_test.go rename to cmd/cli/main_test.go index 9654fb6..6ed26c7 100644 --- a/cmd/ctrld/main_test.go +++ b/cmd/cli/main_test.go @@ -1,4 +1,4 @@ -package main +package cli import ( "os" diff --git a/cmd/ctrld/net_darwin.go b/cmd/cli/net_darwin.go similarity index 98% rename from cmd/ctrld/net_darwin.go rename to cmd/cli/net_darwin.go index f0f7e5a..f456327 100644 --- a/cmd/ctrld/net_darwin.go +++ b/cmd/cli/net_darwin.go @@ -1,4 +1,4 @@ -package main +package cli import ( "bufio" diff --git a/cmd/ctrld/net_darwin_test.go b/cmd/cli/net_darwin_test.go similarity index 98% rename from cmd/ctrld/net_darwin_test.go rename to cmd/cli/net_darwin_test.go index 7110d15..443a9d1 100644 --- a/cmd/ctrld/net_darwin_test.go +++ b/cmd/cli/net_darwin_test.go @@ -1,4 +1,4 @@ -package main +package cli import ( "strings" diff --git a/cmd/ctrld/net_others.go b/cmd/cli/net_others.go similarity index 88% rename from cmd/ctrld/net_others.go rename to cmd/cli/net_others.go index 9093395..2f7aec8 100644 --- a/cmd/ctrld/net_others.go +++ b/cmd/cli/net_others.go @@ -1,6 +1,6 @@ //go:build !darwin -package main +package cli import "net" diff --git a/cmd/ctrld/netlink_linux.go b/cmd/cli/netlink_linux.go similarity index 97% rename from cmd/ctrld/netlink_linux.go rename to cmd/cli/netlink_linux.go index 7657fc3..0faae84 100644 --- a/cmd/ctrld/netlink_linux.go +++ b/cmd/cli/netlink_linux.go @@ -1,4 +1,4 @@ -package main +package cli import ( "github.com/vishvananda/netlink" diff --git a/cmd/ctrld/netlink_others.go b/cmd/cli/netlink_others.go similarity index 80% rename from cmd/ctrld/netlink_others.go rename to cmd/cli/netlink_others.go index d069661..f0afd21 100644 --- a/cmd/ctrld/netlink_others.go +++ b/cmd/cli/netlink_others.go @@ -1,5 +1,5 @@ //go:build !linux -package main +package cli func (p *prog) watchLinkState() {} diff --git a/cmd/ctrld/network_manager_linux.go b/cmd/cli/network_manager_linux.go similarity index 99% rename from cmd/ctrld/network_manager_linux.go rename to cmd/cli/network_manager_linux.go index 799c2dc..5e7b540 100644 --- a/cmd/ctrld/network_manager_linux.go +++ b/cmd/cli/network_manager_linux.go @@ -1,4 +1,4 @@ -package main +package cli import ( "context" diff --git a/cmd/ctrld/network_manager_others.go b/cmd/cli/network_manager_others.go similarity index 93% rename from cmd/ctrld/network_manager_others.go rename to cmd/cli/network_manager_others.go index cd43bbc..323d2f2 100644 --- a/cmd/ctrld/network_manager_others.go +++ b/cmd/cli/network_manager_others.go @@ -1,6 +1,6 @@ //go:build !linux -package main +package cli func setupNetworkManager() error { reloadNetworkManager() diff --git a/cmd/ctrld/os_darwin.go b/cmd/cli/os_darwin.go similarity index 99% rename from cmd/ctrld/os_darwin.go rename to cmd/cli/os_darwin.go index ac872d8..5931819 100644 --- a/cmd/ctrld/os_darwin.go +++ b/cmd/cli/os_darwin.go @@ -1,4 +1,4 @@ -package main +package cli import ( "net" diff --git a/cmd/ctrld/os_freebsd.go b/cmd/cli/os_freebsd.go similarity index 99% rename from cmd/ctrld/os_freebsd.go rename to cmd/cli/os_freebsd.go index 5a1fa36..a6d6dde 100644 --- a/cmd/ctrld/os_freebsd.go +++ b/cmd/cli/os_freebsd.go @@ -1,4 +1,4 @@ -package main +package cli import ( "net" diff --git a/cmd/ctrld/os_linux.go b/cmd/cli/os_linux.go similarity index 99% rename from cmd/ctrld/os_linux.go rename to cmd/cli/os_linux.go index 7a4efae..004e863 100644 --- a/cmd/ctrld/os_linux.go +++ b/cmd/cli/os_linux.go @@ -1,4 +1,4 @@ -package main +package cli import ( "bufio" diff --git a/cmd/ctrld/os_linux_test.go b/cmd/cli/os_linux_test.go similarity index 97% rename from cmd/ctrld/os_linux_test.go rename to cmd/cli/os_linux_test.go index 671f1b4..694fb18 100644 --- a/cmd/ctrld/os_linux_test.go +++ b/cmd/cli/os_linux_test.go @@ -1,4 +1,4 @@ -package main +package cli import ( "reflect" diff --git a/cmd/ctrld/os_others.go b/cmd/cli/os_others.go similarity index 93% rename from cmd/ctrld/os_others.go rename to cmd/cli/os_others.go index 3807bcc..45edf0a 100644 --- a/cmd/ctrld/os_others.go +++ b/cmd/cli/os_others.go @@ -1,6 +1,6 @@ //go:build !linux && !darwin && !freebsd -package main +package cli // TODO(cuonglm): implement. func allocateIP(ip string) error { diff --git a/cmd/ctrld/os_windows.go b/cmd/cli/os_windows.go similarity index 99% rename from cmd/ctrld/os_windows.go rename to cmd/cli/os_windows.go index f96e224..a58411e 100644 --- a/cmd/ctrld/os_windows.go +++ b/cmd/cli/os_windows.go @@ -1,4 +1,4 @@ -package main +package cli import ( "errors" diff --git a/cmd/ctrld/prog.go b/cmd/cli/prog.go similarity index 99% rename from cmd/ctrld/prog.go rename to cmd/cli/prog.go index fab10cc..c3006ec 100644 --- a/cmd/ctrld/prog.go +++ b/cmd/cli/prog.go @@ -1,4 +1,4 @@ -package main +package cli import ( "errors" diff --git a/cmd/ctrld/prog_darwin.go b/cmd/cli/prog_darwin.go similarity index 93% rename from cmd/ctrld/prog_darwin.go rename to cmd/cli/prog_darwin.go index 1a6656d..9cd5786 100644 --- a/cmd/ctrld/prog_darwin.go +++ b/cmd/cli/prog_darwin.go @@ -1,4 +1,4 @@ -package main +package cli import ( "github.com/kardianos/service" diff --git a/cmd/ctrld/prog_freebsd.go b/cmd/cli/prog_freebsd.go similarity index 95% rename from cmd/ctrld/prog_freebsd.go rename to cmd/cli/prog_freebsd.go index 283e03c..93d737f 100644 --- a/cmd/ctrld/prog_freebsd.go +++ b/cmd/cli/prog_freebsd.go @@ -1,4 +1,4 @@ -package main +package cli import ( "os" diff --git a/cmd/ctrld/prog_linux.go b/cmd/cli/prog_linux.go similarity index 98% rename from cmd/ctrld/prog_linux.go rename to cmd/cli/prog_linux.go index f14a054..6f28083 100644 --- a/cmd/ctrld/prog_linux.go +++ b/cmd/cli/prog_linux.go @@ -1,4 +1,4 @@ -package main +package cli import ( "github.com/kardianos/service" diff --git a/cmd/ctrld/prog_others.go b/cmd/cli/prog_others.go similarity index 95% rename from cmd/ctrld/prog_others.go rename to cmd/cli/prog_others.go index 7d70825..92f3a9f 100644 --- a/cmd/ctrld/prog_others.go +++ b/cmd/cli/prog_others.go @@ -1,6 +1,6 @@ //go:build !linux && !freebsd && !darwin -package main +package cli import "github.com/kardianos/service" diff --git a/cmd/ctrld/sema.go b/cmd/cli/sema.go similarity index 96% rename from cmd/ctrld/sema.go rename to cmd/cli/sema.go index 8faa9d2..92b6ce0 100644 --- a/cmd/ctrld/sema.go +++ b/cmd/cli/sema.go @@ -1,4 +1,4 @@ -package main +package cli type semaphore interface { acquire() diff --git a/cmd/ctrld/service.go b/cmd/cli/service.go similarity index 99% rename from cmd/ctrld/service.go rename to cmd/cli/service.go index 263dfd8..c6ed68c 100644 --- a/cmd/ctrld/service.go +++ b/cmd/cli/service.go @@ -1,4 +1,4 @@ -package main +package cli import ( "bytes" diff --git a/cmd/ctrld/service_others.go b/cmd/cli/service_others.go similarity index 90% rename from cmd/ctrld/service_others.go rename to cmd/cli/service_others.go index 82a6ea3..e9522f4 100644 --- a/cmd/ctrld/service_others.go +++ b/cmd/cli/service_others.go @@ -1,6 +1,6 @@ //go:build !windows -package main +package cli import ( "os" diff --git a/cmd/ctrld/service_windows.go b/cmd/cli/service_windows.go similarity index 96% rename from cmd/ctrld/service_windows.go rename to cmd/cli/service_windows.go index 0ce8d3a..a1010a8 100644 --- a/cmd/ctrld/service_windows.go +++ b/cmd/cli/service_windows.go @@ -1,4 +1,4 @@ -package main +package cli import "golang.org/x/sys/windows" diff --git a/cmd/ctrld/main.go b/cmd/ctrld/main.go index 80160ec..af204ad 100644 --- a/cmd/ctrld/main.go +++ b/cmd/ctrld/main.go @@ -1,161 +1,7 @@ package main -import ( - "io" - "os" - "path/filepath" - "sync/atomic" - "time" - - "github.com/kardianos/service" - "github.com/rs/zerolog" - - "github.com/Control-D-Inc/ctrld" -) - -var ( - configPath string - configBase64 string - daemon bool - listenAddress string - primaryUpstream string - secondaryUpstream string - domains []string - logPath string - homedir string - cacheSize int - cfg ctrld.Config - verbose int - silent bool - cdUID string - cdOrg string - cdDev bool - iface string - ifaceStartStop string - - mainLog atomic.Pointer[zerolog.Logger] - consoleWriter zerolog.ConsoleWriter -) - -func init() { - l := zerolog.New(io.Discard) - mainLog.Store(&l) -} +import "github.com/Control-D-Inc/ctrld/cmd/cli" func main() { - ctrld.InitConfig(v, "ctrld") - initCLI() - if err := rootCmd.Execute(); err != nil { - mainLog.Load().Error().Msg(err.Error()) - os.Exit(1) - } -} - -func normalizeLogFilePath(logFilePath string) string { - if logFilePath == "" || filepath.IsAbs(logFilePath) || service.Interactive() { - return logFilePath - } - if homedir != "" { - return filepath.Join(homedir, logFilePath) - } - dir, _ := userHomeDir() - if dir == "" { - return logFilePath - } - return filepath.Join(dir, logFilePath) -} - -func initConsoleLogging() { - consoleWriter = zerolog.NewConsoleWriter(func(w *zerolog.ConsoleWriter) { - w.TimeFormat = time.StampMilli - }) - multi := zerolog.MultiLevelWriter(consoleWriter) - l := mainLog.Load().Output(multi).With().Timestamp().Logger() - mainLog.Store(&l) - switch { - case silent: - zerolog.SetGlobalLevel(zerolog.NoLevel) - case verbose == 1: - zerolog.SetGlobalLevel(zerolog.InfoLevel) - case verbose > 1: - zerolog.SetGlobalLevel(zerolog.DebugLevel) - default: - zerolog.SetGlobalLevel(zerolog.NoticeLevel) - } -} - -// initLogging initializes global logging setup. -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} - if logFilePath := normalizeLogFilePath(cfg.Service.LogPath); logFilePath != "" { - // Create parent directory if necessary. - if err := os.MkdirAll(filepath.Dir(logFilePath), 0750); err != nil { - mainLog.Load().Error().Msgf("failed to create log path: %v", err) - os.Exit(1) - } - - // Default open log file in append mode. - 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.Load().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, flags, os.FileMode(0o600)) - if err != nil { - mainLog.Load().Error().Msgf("failed to create log file: %v", err) - os.Exit(1) - } - writers = append(writers, logFile) - } - writers = append(writers, consoleWriter) - multi := zerolog.MultiLevelWriter(writers...) - l := mainLog.Load().Output(multi).With().Timestamp().Logger() - mainLog.Store(&l) - // TODO: find a better way. - ctrld.ProxyLogger.Store(&l) - - zerolog.SetGlobalLevel(zerolog.NoticeLevel) - logLevel := cfg.Service.LogLevel - switch { - case silent: - zerolog.SetGlobalLevel(zerolog.NoLevel) - return - case verbose == 1: - logLevel = "info" - case verbose > 1: - logLevel = "debug" - } - if logLevel == "" { - return - } - level, err := zerolog.ParseLevel(logLevel) - if err != nil { - mainLog.Load().Warn().Err(err).Msg("could not set log level") - return - } - zerolog.SetGlobalLevel(level) -} - -func initCache() { - if !cfg.Service.CacheEnable { - return - } - if cfg.Service.CacheSize == 0 { - cfg.Service.CacheSize = 4096 - } + cli.Main() } From 2bcba7b578e067612c1e534eaf0e50b084038a3e Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Thu, 10 Aug 2023 00:44:36 +0700 Subject: [PATCH 84/84] cmd/ctrld: workaround staticcheck complain on non-Linux OSes --- cmd/cli/prog.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index c3006ec..4169fb8 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -332,6 +332,8 @@ func errAddrInUse(err error) bool { return false } +var _ = errAddrInUse + // https://learn.microsoft.com/en-us/windows/win32/winsock/windows-sockets-error-codes-2 var ( windowsECONNREFUSED = syscall.Errno(10061)