mirror of
https://github.com/Control-D-Inc/ctrld.git
synced 2026-09-04 13:36:35 +02:00
all: implement self-uninstall ctrld based on REFUSED queries
This commit is contained in:
+34
-30
@@ -1104,36 +1104,9 @@ func run(appCallback *AppCallback, stopCh chan struct{}) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uninstallIfInvalidCdUID := func() {
|
cdLogger := mainLog.Load().With().Str("mode", "cd").Logger()
|
||||||
cdLogger := mainLog.Load().With().Str("mode", "cd").Logger()
|
_ = uninstallIfInvalidCdUID(err, cdLogger)
|
||||||
if uer, ok := err.(*controld.UtilityErrorResponse); ok && uer.ErrorField.Code == controld.InvalidConfigCode {
|
cdLogger.Fatal().Err(err).Msg("failed to fetch resolver config")
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2590,3 +2563,34 @@ func doValidateCdRemoteConfig(cdUID string) {
|
|||||||
}
|
}
|
||||||
v = oldV
|
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
|
||||||
|
}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
"tailscale.com/net/tsaddr"
|
"tailscale.com/net/tsaddr"
|
||||||
|
|
||||||
"github.com/Control-D-Inc/ctrld"
|
"github.com/Control-D-Inc/ctrld"
|
||||||
|
"github.com/Control-D-Inc/ctrld/internal/controld"
|
||||||
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
"github.com/Control-D-Inc/ctrld/internal/dnscache"
|
||||||
ctrldnet "github.com/Control-D-Inc/ctrld/internal/net"
|
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
|
// 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.
|
// This is also dns.EDNS0LOCALSTART, but define our own constant here for clarification.
|
||||||
EDNS0_OPTION_MAC = 0xFDE9
|
EDNS0_OPTION_MAC = 0xFDE9
|
||||||
|
|
||||||
|
// selfUninstallMaxQueries is number of REFUSED queries seen before checking for self-uninstallation.
|
||||||
|
selfUninstallMaxQueries = 32
|
||||||
)
|
)
|
||||||
|
|
||||||
var osUpstreamConfig = &ctrld.UpstreamConfig{
|
var osUpstreamConfig = &ctrld.UpstreamConfig{
|
||||||
@@ -143,6 +147,7 @@ func (p *prog) serveDNS(listenerNum string) error {
|
|||||||
failoverRcodes: failoverRcode,
|
failoverRcodes: failoverRcode,
|
||||||
ufr: ur,
|
ufr: ur,
|
||||||
})
|
})
|
||||||
|
go p.doSelfUninstall(pr.answer)
|
||||||
answer = pr.answer
|
answer = pr.answer
|
||||||
rtt := time.Since(t)
|
rtt := time.Since(t)
|
||||||
ctrld.Log(ctx, mainLog.Load().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)
|
||||||
@@ -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.
|
// queryFromSelf reports whether the input IP is from device running ctrld.
|
||||||
func queryFromSelf(ip string) bool {
|
func queryFromSelf(ip string) bool {
|
||||||
netIP := netip.MustParseAddr(ip)
|
netIP := netip.MustParseAddr(ip)
|
||||||
|
|||||||
@@ -89,6 +89,11 @@ type prog struct {
|
|||||||
ptrLoopGuard *loopGuard
|
ptrLoopGuard *loopGuard
|
||||||
lanLoopGuard *loopGuard
|
lanLoopGuard *loopGuard
|
||||||
|
|
||||||
|
selfUninstallMu sync.Mutex
|
||||||
|
refusedQueryCount int
|
||||||
|
canSelfUninstall bool
|
||||||
|
checkingSelfUninstall bool
|
||||||
|
|
||||||
loopMu sync.Mutex
|
loopMu sync.Mutex
|
||||||
loop map[string]bool
|
loop map[string]bool
|
||||||
|
|
||||||
@@ -221,9 +226,11 @@ func (p *prog) postRun() {
|
|||||||
func (p *prog) setupUpstream(cfg *ctrld.Config) {
|
func (p *prog) setupUpstream(cfg *ctrld.Config) {
|
||||||
localUpstreams := make([]string, 0, len(cfg.Upstream))
|
localUpstreams := make([]string, 0, len(cfg.Upstream))
|
||||||
ptrNameservers := make([]string, 0, len(cfg.Upstream))
|
ptrNameservers := make([]string, 0, len(cfg.Upstream))
|
||||||
|
isControlDUpstream := false
|
||||||
for n := range cfg.Upstream {
|
for n := range cfg.Upstream {
|
||||||
uc := cfg.Upstream[n]
|
uc := cfg.Upstream[n]
|
||||||
uc.Init()
|
uc.Init()
|
||||||
|
isControlDUpstream = isControlDUpstream || uc.IsControlD()
|
||||||
if uc.BootstrapIP == "" {
|
if uc.BootstrapIP == "" {
|
||||||
uc.SetupBootstrapIP()
|
uc.SetupBootstrapIP()
|
||||||
mainLog.Load().Info().Msgf("bootstrap IPs for upstream.%s: %q", n, uc.BootstrapIPs())
|
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)
|
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.localUpstreams = localUpstreams
|
||||||
p.ptrNameservers = ptrNameservers
|
p.ptrNameservers = ptrNameservers
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -316,7 +316,7 @@ func (uc *UpstreamConfig) Init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if uc.IPStack == "" {
|
if uc.IPStack == "" {
|
||||||
if uc.isControlD() {
|
if uc.IsControlD() {
|
||||||
uc.IPStack = IpStackSplit
|
uc.IPStack = IpStackSplit
|
||||||
} else {
|
} else {
|
||||||
uc.IPStack = IpStackBoth
|
uc.IPStack = IpStackBoth
|
||||||
@@ -354,7 +354,7 @@ func (uc *UpstreamConfig) UpstreamSendClientInfo() bool {
|
|||||||
}
|
}
|
||||||
switch uc.Type {
|
switch uc.Type {
|
||||||
case ResolverTypeDOH, ResolverTypeDOH3:
|
case ResolverTypeDOH, ResolverTypeDOH3:
|
||||||
if uc.isControlD() || uc.isNextDNS() {
|
if uc.IsControlD() || uc.isNextDNS() {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -401,7 +401,7 @@ func (uc *UpstreamConfig) UID() string {
|
|||||||
// The first usable IP will be used as bootstrap IP of the upstream.
|
// The first usable IP will be used as bootstrap IP of the upstream.
|
||||||
func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) {
|
func (uc *UpstreamConfig) setupBootstrapIP(withBootstrapDNS bool) {
|
||||||
b := backoff.NewBackoff("setupBootstrapIP", func(format string, args ...any) {}, 10*time.Second)
|
b := backoff.NewBackoff("setupBootstrapIP", func(format string, args ...any) {}, 10*time.Second)
|
||||||
isControlD := uc.isControlD()
|
isControlD := uc.IsControlD()
|
||||||
for {
|
for {
|
||||||
uc.bootstrapIPs = lookupIP(uc.Domain, uc.Timeout, withBootstrapDNS)
|
uc.bootstrapIPs = lookupIP(uc.Domain, uc.Timeout, withBootstrapDNS)
|
||||||
// For ControlD upstream, the bootstrap IPs could not be RFC 1918 addresses,
|
// For ControlD upstream, the bootstrap IPs could not be RFC 1918 addresses,
|
||||||
@@ -572,7 +572,8 @@ func (uc *UpstreamConfig) ping() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (uc *UpstreamConfig) isControlD() bool {
|
// IsControlD reports whether this is a ControlD upstream.
|
||||||
|
func (uc *UpstreamConfig) IsControlD() bool {
|
||||||
domain := uc.Domain
|
domain := uc.Domain
|
||||||
if domain == "" {
|
if domain == "" {
|
||||||
if u, err := url.Parse(uc.Endpoint); err == nil {
|
if u, err := url.Parse(uc.Endpoint); err == nil {
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ func addHeader(ctx context.Context, req *http.Request, uc *UpstreamConfig) {
|
|||||||
if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
|
if ci, ok := ctx.Value(ClientInfoCtxKey{}).(*ClientInfo); ok && ci != nil {
|
||||||
printed = ci.Mac != "" || ci.IP != "" || ci.Hostname != ""
|
printed = ci.Mac != "" || ci.IP != "" || ci.Hostname != ""
|
||||||
switch {
|
switch {
|
||||||
case uc.isControlD():
|
case uc.IsControlD():
|
||||||
dohHeader = newControlDHeaders(ci)
|
dohHeader = newControlDHeaders(ci)
|
||||||
case uc.isNextDNS():
|
case uc.isNextDNS():
|
||||||
dohHeader = newNextDNSHeaders(ci)
|
dohHeader = newNextDNSHeaders(ci)
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ const (
|
|||||||
apiDomainDev = "api.controld.dev"
|
apiDomainDev = "api.controld.dev"
|
||||||
resolverDataURLCom = "https://api.controld.com/utility"
|
resolverDataURLCom = "https://api.controld.com/utility"
|
||||||
resolverDataURLDev = "https://api.controld.dev/utility"
|
resolverDataURLDev = "https://api.controld.dev/utility"
|
||||||
InvalidConfigCode = 40401
|
InvalidConfigCode = 40402
|
||||||
)
|
)
|
||||||
|
|
||||||
// ResolverConfig represents Control D resolver data.
|
// ResolverConfig represents Control D resolver data.
|
||||||
|
|||||||
Reference in New Issue
Block a user