From 80cf79b9cb8fab881f1ce18631a5339979dea9df Mon Sep 17 00:00:00 2001 From: Cuong Manh Le Date: Fri, 19 Jul 2024 22:38:44 +0700 Subject: [PATCH] all: implement self-uninstall ctrld based on REFUSED queries --- cmd/cli/cli.go | 64 ++++++++++++++++++++----------------- cmd/cli/dns_proxy.go | 51 +++++++++++++++++++++++++++++ cmd/cli/prog.go | 11 +++++++ config.go | 9 +++--- doh.go | 2 +- internal/controld/config.go | 2 +- 6 files changed, 103 insertions(+), 36 deletions(-) diff --git a/cmd/cli/cli.go b/cmd/cli/cli.go index d8c892d..2c5307c 100644 --- a/cmd/cli/cli.go +++ b/cmd/cli/cli.go @@ -1104,36 +1104,9 @@ func run(appCallback *AppCallback, stopCh chan struct{}) { return } - uninstallIfInvalidCdUID := func() { - cdLogger := mainLog.Load().With().Str("mode", "cd").Logger() - if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode { - s, err := newService(&prog{}, svcConfig) - if err != nil { - cdLogger.Warn().Err(err).Msg("failed to create new service") - return - } - if netIface, _ := netInterface(iface); netIface != nil { - if err := restoreNetworkManager(); err != nil { - cdLogger.Error().Err(err).Msg("could not restore NetworkManager") - return - } - cdLogger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS for interface") - if err := resetDNS(netIface); err != nil { - cdLogger.Warn().Err(err).Msg("something went wrong while restoring DNS") - } else { - cdLogger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS successfully") - } - } - - tasks := []task{{s.Uninstall, true}} - if doTasks(tasks) { - cdLogger.Info().Msg("uninstalled service") - } - cdLogger.Fatal().Err(uer).Msg("failed to fetch resolver config") - return - } - } - uninstallIfInvalidCdUID() + cdLogger := mainLog.Load().With().Str("mode", "cd").Logger() + _ = uninstallIfInvalidCdUID(err, cdLogger) + cdLogger.Fatal().Err(err).Msg("failed to fetch resolver config") } } @@ -2590,3 +2563,34 @@ func doValidateCdRemoteConfig(cdUID string) { } v = oldV } + +// uninstallIfInvalidCdUID performs self-uninstallation if the ControlD device does not exist. +func uninstallIfInvalidCdUID(err error, logger zerolog.Logger) bool { + var uer *controld.UtilityErrorResponse + if errors.As(err, &uer) && uer.ErrorField.Code == controld.InvalidConfigCode { + s, err := newService(&prog{}, svcConfig) + if err != nil { + logger.Warn().Err(err).Msg("failed to create new service") + return false + } + if netIface, _ := netInterface(iface); netIface != nil { + if err := restoreNetworkManager(); err != nil { + logger.Error().Err(err).Msg("could not restore NetworkManager") + return false + } + logger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS for interface") + if err := resetDNS(netIface); err != nil { + logger.Warn().Err(err).Msg("something went wrong while restoring DNS") + } else { + logger.Debug().Str("iface", netIface.Name).Msg("Restoring DNS successfully") + } + } + + tasks := []task{{s.Uninstall, true}} + if doTasks(tasks) { + logger.Info().Msg("uninstalled service") + return true + } + } + return false +} diff --git a/cmd/cli/dns_proxy.go b/cmd/cli/dns_proxy.go index 9f95812..6e8c1fa 100644 --- a/cmd/cli/dns_proxy.go +++ b/cmd/cli/dns_proxy.go @@ -21,6 +21,7 @@ import ( "tailscale.com/net/tsaddr" "github.com/Control-D-Inc/ctrld" + "github.com/Control-D-Inc/ctrld/internal/controld" "github.com/Control-D-Inc/ctrld/internal/dnscache" ctrldnet "github.com/Control-D-Inc/ctrld/internal/net" ) @@ -32,6 +33,9 @@ const ( // https://thekelleys.org.uk/gitweb/?p=dnsmasq.git;a=blob;f=src/dns-protocol.h;h=76ac66a8c28317e9c121a74ab5fd0e20f6237dc8;hb=HEAD#l81 // This is also dns.EDNS0LOCALSTART, but define our own constant here for clarification. EDNS0_OPTION_MAC = 0xFDE9 + + // selfUninstallMaxQueries is number of REFUSED queries seen before checking for self-uninstallation. + selfUninstallMaxQueries = 32 ) var osUpstreamConfig = &ctrld.UpstreamConfig{ @@ -143,6 +147,7 @@ func (p *prog) serveDNS(listenerNum string) error { failoverRcodes: failoverRcode, ufr: ur, }) + go p.doSelfUninstall(pr.answer) answer = pr.answer rtt := time.Since(t) ctrld.Log(ctx, mainLog.Load().Debug(), "received response of %d bytes in %s", answer.Len(), rtt) @@ -836,6 +841,52 @@ func (p *prog) spoofLoopbackIpInClientInfo(ci *ctrld.ClientInfo) { } } +// doSelfUninstall performs self-uninstall if these condition met: +// +// - There is only 1 ControlD upstream in-use. +// - Number of refused queries seen so far equals to selfUninstallMaxQueries. +// - The cdUID is deleted. +func (p *prog) doSelfUninstall(answer *dns.Msg) { + if !p.canSelfUninstall || answer == nil || answer.Rcode != dns.RcodeRefused { + return + } + + p.selfUninstallMu.Lock() + defer p.selfUninstallMu.Unlock() + if p.checkingSelfUninstall { + return + } + + logger := mainLog.Load().With().Str("mode", "self-uninstall").Logger() + if p.refusedQueryCount > selfUninstallMaxQueries { + p.checkingSelfUninstall = true + _, err := controld.FetchResolverConfig(cdUID, rootCmd.Version, cdDev) + logger.Debug().Msg("maximum number of refused queries reached, checking device status") + if uninstallIfInvalidCdUID(err, logger) { + logger.Fatal().Msgf("service was uninstalled because device %q does not exist", cdUID) + } + if err != nil { + logger.Warn().Err(err).Msg("could not fetch resolver config") + } + // Cool-of period to prevent abusing the API. + go p.selfUninstallCoolOfPeriod() + return + } + p.refusedQueryCount++ +} + +// selfUninstallCoolOfPeriod waits for 30 minutes before +// calling API again for checking ControlD device status. +func (p *prog) selfUninstallCoolOfPeriod() { + t := time.NewTimer(time.Minute * 30) + defer t.Stop() + <-t.C + p.selfUninstallMu.Lock() + p.checkingSelfUninstall = false + p.refusedQueryCount = 0 + p.selfUninstallMu.Unlock() +} + // queryFromSelf reports whether the input IP is from device running ctrld. func queryFromSelf(ip string) bool { netIP := netip.MustParseAddr(ip) diff --git a/cmd/cli/prog.go b/cmd/cli/prog.go index 90fdfb2..239b044 100644 --- a/cmd/cli/prog.go +++ b/cmd/cli/prog.go @@ -89,6 +89,11 @@ type prog struct { ptrLoopGuard *loopGuard lanLoopGuard *loopGuard + selfUninstallMu sync.Mutex + refusedQueryCount int + canSelfUninstall bool + checkingSelfUninstall bool + loopMu sync.Mutex loop map[string]bool @@ -221,9 +226,11 @@ func (p *prog) postRun() { func (p *prog) setupUpstream(cfg *ctrld.Config) { localUpstreams := make([]string, 0, len(cfg.Upstream)) ptrNameservers := make([]string, 0, len(cfg.Upstream)) + isControlDUpstream := false for n := range cfg.Upstream { uc := cfg.Upstream[n] uc.Init() + isControlDUpstream = isControlDUpstream || uc.IsControlD() if uc.BootstrapIP == "" { uc.SetupBootstrapIP() mainLog.Load().Info().Msgf("bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs()) @@ -240,6 +247,10 @@ func (p *prog) setupUpstream(cfg *ctrld.Config) { ptrNameservers = append(ptrNameservers, uc.Endpoint) } } + // Self-uninstallation is ok If there is only 1 ControlD upstream, and no remote config. + if len(cfg.Upstream) == 1 && isControlDUpstream { + p.canSelfUninstall = true + } p.localUpstreams = localUpstreams p.ptrNameservers = ptrNameservers } diff --git a/config.go b/config.go index 8c99a8e..ae9f75b 100644 --- a/config.go +++ b/config.go @@ -316,7 +316,7 @@ func (uc *UpstreamConfig) Init() { } } if uc.IPStack == "" { - if uc.isControlD() { + if uc.IsControlD() { uc.IPStack = IpStackSplit } else { uc.IPStack = IpStackBoth @@ -354,7 +354,7 @@ func (uc *UpstreamConfig) UpstreamSendClientInfo() bool { } switch uc.Type { case ResolverTypeDOH, ResolverTypeDOH3: - if uc.isControlD() || uc.isNextDNS() { + if uc.IsControlD() || uc.isNextDNS() { return true } } @@ -401,7 +401,7 @@ func (uc *UpstreamConfig) UID() string { // 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) {}, 10*time.Second) - isControlD := uc.isControlD() + isControlD := uc.IsControlD() for { uc.bootstrapIPs = lookupIP(uc.Domain, uc.Timeout, withBootstrapDNS) // For ControlD upstream, the bootstrap IPs could not be RFC 1918 addresses, @@ -572,7 +572,8 @@ func (uc *UpstreamConfig) ping() error { return nil } -func (uc *UpstreamConfig) isControlD() bool { +// IsControlD reports whether this is a ControlD upstream. +func (uc *UpstreamConfig) IsControlD() bool { domain := uc.Domain if domain == "" { if u, err := url.Parse(uc.Endpoint); err == nil { diff --git a/doh.go b/doh.go index bddc583..d702995 100644 --- a/doh.go +++ b/doh.go @@ -147,7 +147,7 @@ func addHeader(ctx context.Context, req *http.Request, uc *UpstreamConfig) { if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil { printed = ci.Mac != "" || ci.IP != "" || ci.Hostname != "" switch { - case uc.isControlD(): + case uc.IsControlD(): dohHeader = newControlDHeaders(ci) case uc.isNextDNS(): dohHeader = newNextDNSHeaders(ci) diff --git a/internal/controld/config.go b/internal/controld/config.go index c095c0c..d2b564a 100644 --- a/internal/controld/config.go +++ b/internal/controld/config.go @@ -26,7 +26,7 @@ const ( apiDomainDev = "api.controld.dev" resolverDataURLCom = "https://api.controld.com/utility" resolverDataURLDev = "https://api.controld.dev/utility" - InvalidConfigCode = 40401 + InvalidConfigCode = 40402 ) // ResolverConfig represents Control D resolver data.