From b58ab8dc6086b84f8dcfe42c075be12e065e1bc9 Mon Sep 17 00:00:00 2001 From: PJ Date: Mon, 2 Oct 2023 12:53:35 +0200 Subject: [PATCH 1/3] autopilot: update ipfilter --- autopilot/contractor.go | 26 ++++++---- autopilot/hostfilter.go | 18 +++---- autopilot/ipfilter.go | 112 ++++++++++++++++++++++++++++------------ 3 files changed, 103 insertions(+), 53 deletions(-) diff --git a/autopilot/contractor.go b/autopilot/contractor.go index 406f72c75..4c9fcfcb6 100644 --- a/autopilot/contractor.go +++ b/autopilot/contractor.go @@ -258,8 +258,11 @@ func (c *contractor) performContractMaintenance(ctx context.Context, w Worker) ( c.cachedMinScore = minScore c.mu.Unlock() + // create a new ip filter + ipFilter := newIPFilter(c.logger) + // run checks - updatedSet, toArchive, toStopUsing, toRefresh, toRenew, err := c.runContractChecks(ctx, w, contracts, isInCurrentSet, minScore) + updatedSet, toArchive, toStopUsing, toRefresh, toRenew, err := c.runContractChecks(ctx, w, contracts, isInCurrentSet, minScore, ipFilter) if err != nil { return false, fmt.Errorf("failed to run contract checks, err: %v", err) } @@ -341,7 +344,7 @@ func (c *contractor) performContractMaintenance(ctx context.Context, w Worker) ( // check if we need to form contracts and add them to the contract set var formed []types.FileContractID if uint64(len(updatedSet)) < threshold { - formed, err = c.runContractFormations(ctx, w, hosts, usedHosts, state.cfg.Contracts.Amount-uint64(len(updatedSet)), &remaining, minScore) + formed, err = c.runContractFormations(ctx, w, hosts, usedHosts, state.cfg.Contracts.Amount-uint64(len(updatedSet)), &remaining, minScore, ipFilter) if err != nil { c.logger.Errorf("failed to form contracts, err: %v", err) // continue } else { @@ -583,7 +586,7 @@ func (c *contractor) performWalletMaintenance(ctx context.Context) error { return nil } -func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts []api.Contract, inCurrentSet map[types.FileContractID]struct{}, minScore float64) (toKeep []types.FileContractID, toArchive, toStopUsing map[types.FileContractID]string, toRefresh, toRenew []contractInfo, _ error) { +func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts []api.Contract, inCurrentSet map[types.FileContractID]struct{}, minScore float64, ipFilter *ipFilter) (toKeep []types.FileContractID, toArchive, toStopUsing map[types.FileContractID]string, toRefresh, toRenew []contractInfo, _ error) { if c.ap.isStopped() { return } @@ -591,6 +594,7 @@ func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts // convenience variables state := c.ap.State() + shouldFilter := !state.cfg.Hosts.AllowRedundantIPs // fetch consensus state cs, err := c.ap.bus.ConsensusState(ctx) @@ -618,9 +622,6 @@ func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts ) }() - // create a new ip filter - f := newIPFilter(c.logger) - // return variables toArchive = make(map[types.FileContractID]string) toStopUsing = make(map[types.FileContractID]string) @@ -710,6 +711,8 @@ func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts if contract.Revision == nil { if _, found := inCurrentSet[fcid]; !found || remainingKeepLeeway == 0 { toStopUsing[fcid] = errContractNoRevision.Error() + } else if shouldFilter && ipFilter.IsRedundantIP(contract.HostIP, contract.HostKey) { + toStopUsing[fcid] = fmt.Sprintf("%v; %v", errHostRedundantIP, errContractNoRevision) } else { toKeep = append(toKeep, fcid) remainingKeepLeeway-- // we let it slide @@ -736,7 +739,7 @@ func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts c.logger.Errorw(fmt.Sprintf("failed to compute renterFunds for contract: %v", err)) } - usable, recoverable, refresh, renew, reasons := isUsableContract(state.cfg, ci, cs.BlockHeight, renterFunds, f) + usable, recoverable, refresh, renew, reasons := c.isUsableContract(state.cfg, ci, cs.BlockHeight, renterFunds, ipFilter) ci.usable = usable ci.recoverable = recoverable if !usable { @@ -766,7 +769,7 @@ func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts return toKeep, toArchive, toStopUsing, toRefresh, toRenew, nil } -func (c *contractor) runContractFormations(ctx context.Context, w Worker, hosts []hostdb.Host, usedHosts map[types.PublicKey]struct{}, missing uint64, budget *types.Currency, minScore float64) ([]types.FileContractID, error) { +func (c *contractor) runContractFormations(ctx context.Context, w Worker, hosts []hostdb.Host, usedHosts map[types.PublicKey]struct{}, missing uint64, budget *types.Currency, minScore float64, ipFilter *ipFilter) ([]types.FileContractID, error) { ctx, span := tracing.Tracer.Start(ctx, "runContractFormations") defer span.End() @@ -777,6 +780,7 @@ func (c *contractor) runContractFormations(ctx context.Context, w Worker, hosts // convenience variables state := c.ap.State() + shouldFilter := !state.cfg.Hosts.AllowRedundantIPs c.logger.Debugw( "run contract formations", @@ -811,10 +815,10 @@ func (c *contractor) runContractFormations(ctx context.Context, w Worker, hosts gc := worker.NewGougingChecker(state.gs, cs, state.fee, state.cfg.Contracts.Period, state.cfg.Contracts.RenewWindow) // prepare an IP filter that contains all used hosts - f := newIPFilter(c.logger) + ipFilter.Reset() for _, h := range hosts { if _, used := usedHosts[h.PublicKey]; used { - _ = f.isRedundantIP(h.NetAddress, h.PublicKey) + _ = ipFilter.IsRedundantIP(h.NetAddress, h.PublicKey) } } @@ -854,7 +858,7 @@ func (c *contractor) runContractFormations(ctx context.Context, w Worker, hosts } // check if we already have a contract with a host on that subnet - if !state.cfg.Hosts.AllowRedundantIPs && f.isRedundantIP(host.NetAddress, host.PublicKey) { + if shouldFilter && ipFilter.IsRedundantIP(host.NetAddress, host.PublicKey) { continue } diff --git a/autopilot/hostfilter.go b/autopilot/hostfilter.go index 8630e7344..831829be5 100644 --- a/autopilot/hostfilter.go +++ b/autopilot/hostfilter.go @@ -222,38 +222,38 @@ func isUsableHost(cfg api.AutopilotConfig, rs api.RedundancySettings, gc worker. // - recoverable -> can be usable in the contract set if it is refreshed/renewed // - refresh -> should be refreshed // - renew -> should be renewed -func isUsableContract(cfg api.AutopilotConfig, ci contractInfo, bh uint64, renterFunds types.Currency, f *ipFilter) (usable, recoverable, refresh, renew bool, reasons []string) { - c, s, pt := ci.contract, ci.settings, ci.priceTable +func (c *contractor) isUsableContract(cfg api.AutopilotConfig, ci contractInfo, bh uint64, renterFunds types.Currency, f *ipFilter) (usable, recoverable, refresh, renew bool, reasons []string) { + contract, s, pt := ci.contract, ci.settings, ci.priceTable usable = true - if bh > c.EndHeight() { + if bh > contract.EndHeight() { reasons = append(reasons, errContractExpired.Error()) usable = false recoverable = false refresh = false renew = false - } else if c.Revision.RevisionNumber == math.MaxUint64 { + } else if contract.Revision.RevisionNumber == math.MaxUint64 { reasons = append(reasons, errContractMaxRevisionNumber.Error()) usable = false recoverable = false refresh = false renew = false } else { - if isOutOfCollateral(c, s, pt, renterFunds, cfg.Contracts.Period, bh) { + if isOutOfCollateral(contract, s, pt, renterFunds, cfg.Contracts.Period, bh) { reasons = append(reasons, errContractOutOfCollateral.Error()) usable = false recoverable = true refresh = true renew = false } - if isOutOfFunds(cfg, s, c) { + if isOutOfFunds(cfg, s, contract) { reasons = append(reasons, errContractOutOfFunds.Error()) usable = false recoverable = true refresh = true renew = false } - if shouldRenew, secondHalf := isUpForRenewal(cfg, *c.Revision, bh); shouldRenew { + if shouldRenew, secondHalf := isUpForRenewal(cfg, *contract.Revision, bh); shouldRenew { reasons = append(reasons, fmt.Errorf("%w; second half: %t", errContractUpForRenewal, secondHalf).Error()) usable = usable && !secondHalf // only unusable if in second half of renew window recoverable = true @@ -263,13 +263,13 @@ func isUsableContract(cfg api.AutopilotConfig, ci contractInfo, bh uint64, rente } // IP check should be last since it modifies the filter - if !cfg.Hosts.AllowRedundantIPs && (usable || recoverable) && f.isRedundantIP(c.HostIP, c.HostKey) { + shouldFilter := !cfg.Hosts.AllowRedundantIPs && (usable || recoverable) + if shouldFilter && f.IsRedundantIP(contract.HostIP, contract.HostKey) { reasons = append(reasons, errHostRedundantIP.Error()) usable = false recoverable = false // do not use in the contract set, but keep it around for downloads renew = false // do not renew, but allow refreshes so the contracts stays funded } - return } diff --git a/autopilot/ipfilter.go b/autopilot/ipfilter.go index 5cece2692..0bc11877c 100644 --- a/autopilot/ipfilter.go +++ b/autopilot/ipfilter.go @@ -2,6 +2,7 @@ package autopilot import ( "context" + "errors" "fmt" "net" "strings" @@ -20,29 +21,86 @@ const ( resolverLookupTimeout = 5 * time.Second ) +var ( + errNoSuchHost = errors.New("no such host") + errTooManyAddresses = errors.New("host has more than two addresses, or two of the same type") + errUnparsableAddress = errors.New("host address could not be parsed to a subnet") +) + type resolver interface { LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) } type ipFilter struct { - subnets map[string]string - resolver resolver - timeout time.Duration + hostIPToSubnetsCache map[string][]string + subnets map[string]string + resolver resolver + timeout time.Duration logger *zap.SugaredLogger } func newIPFilter(logger *zap.SugaredLogger) *ipFilter { return &ipFilter{ - subnets: make(map[string]string), - resolver: &net.Resolver{}, - timeout: resolverLookupTimeout, + hostIPToSubnetsCache: make(map[string][]string), + subnets: make(map[string]string), + resolver: &net.Resolver{}, + timeout: resolverLookupTimeout, logger: logger, } } -func (f *ipFilter) isRedundantIP(hostIP string, hostKey types.PublicKey) bool { +func (f *ipFilter) IsRedundantIP(hostIP string, hostKey types.PublicKey) bool { + // perform DNS lookup + subnets, err := f.performDNSLookup(hostIP) + if err != nil { + if !strings.Contains(err.Error(), errNoSuchHost.Error()) { + f.logger.Errorf("failed to check for redundant IP, treating host %v with IP %v as redundant, err: %v", hostKey, hostIP, err) + } + return true + } + + // if the lookup failed parse out the host + if len(subnets) == 0 { + f.logger.Errorf("failed to check for redundant IP, treating host %v with IP %v as redundant, err: %v", hostKey, hostIP, errUnparsableAddress) + return true + } + + // we register all subnets under the host key, so we can safely use the + // first subnet to check whether we already know this host + host, found := f.subnets[subnets[0]] + if !found { + for _, subnet := range subnets { + f.subnets[subnet] = hostKey.String() + } + return false + } + + // if the given host matches the known host, it's not redundant + sameHost := host == hostKey.String() + return !sameHost +} + +// Resetting clears the subnets, but not the cache, allowing to rebuild the IP +// filter with a list of hosts. +func (f *ipFilter) Reset() { + f.subnets = make(map[string]string) +} + +func (f *ipFilter) performDNSLookup(hostIP string) ([]string, error) { + // check the cache + subnets, found := f.hostIPToSubnetsCache[hostIP] + if found { + return subnets, nil + } + + // lookup all IP addresses for the given host + host, _, err := net.SplitHostPort(hostIP) + if err != nil { + return nil, err + } + // create a context ctx := context.Background() if f.timeout > 0 { @@ -51,40 +109,28 @@ func (f *ipFilter) isRedundantIP(hostIP string, hostKey types.PublicKey) bool { defer cancel() } - // lookup all IP addresses for the given host - host, _, err := net.SplitHostPort(hostIP) + // lookup IP addresses + addrs, err := f.resolver.LookupIPAddr(ctx, host) if err != nil { - return true - } - addresses, err := f.resolver.LookupIPAddr(ctx, host) - if err != nil { - if !strings.Contains(err.Error(), "no such host") { - f.logger.Debugf("failed to lookup IP for host %v, err: %v", hostKey, err) - } - return true + return nil, err } - // filter hosts associated with more than two addresses or two of the same type - if len(addresses) > 2 || (len(addresses) == 2) && (len(addresses[0].IP) == len(addresses[1].IP)) { - return true + // filter out hosts associated with more than two addresses or two of the same type + if len(addrs) > 2 || (len(addrs) == 2) && (len(addrs[0].IP) == len(addrs[1].IP)) { + return nil, errTooManyAddresses } - // check whether the host's subnet was already in the list, if it is in the - // list we compare the cached net address with the one from the host being - // filtered as it might be the same host - var filter bool - for _, subnet := range subnets(addresses) { - original, exists := f.subnets[subnet] - if exists && hostKey.String() != original { - filter = true - } else if !exists { - f.subnets[subnet] = hostKey.String() - } + // parse out subnets + subnets = parseSubnets(addrs) + + // cache them and return + if len(subnets) > 0 { + f.hostIPToSubnetsCache[hostIP] = subnets } - return filter + return subnets, nil } -func subnets(addresses []net.IPAddr) []string { +func parseSubnets(addresses []net.IPAddr) []string { subnets := make([]string, 0, len(addresses)) for _, address := range addresses { From 113b360c1493518b0d938b956f483926af06a4de Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 4 Oct 2023 17:22:16 +0200 Subject: [PATCH 2/3] contractor: add ip resolver --- autopilot/contractor.go | 28 +++++---- autopilot/ipfilter.go | 136 +++++++++++++++++++++++++--------------- 2 files changed, 101 insertions(+), 63 deletions(-) diff --git a/autopilot/contractor.go b/autopilot/contractor.go index 4c9fcfcb6..34220488c 100644 --- a/autopilot/contractor.go +++ b/autopilot/contractor.go @@ -90,8 +90,9 @@ const ( type ( contractor struct { - ap *Autopilot - logger *zap.SugaredLogger + ap *Autopilot + resolver *ipResolver + logger *zap.SugaredLogger maintenanceTxnID types.TransactionID revisionBroadcastInterval time.Duration @@ -125,9 +126,11 @@ type ( ) func newContractor(ap *Autopilot, revisionSubmissionBuffer uint64, revisionBroadcastInterval time.Duration) *contractor { + logger := ap.logger.Named("contractor") return &contractor{ ap: ap, - logger: ap.logger.Named("contractor"), + resolver: newIPResolver(resolverLookupTimeout, logger), + logger: logger, revisionBroadcastInterval: revisionBroadcastInterval, revisionLastBroadcast: make(map[types.FileContractID]time.Time), revisionSubmissionBuffer: revisionSubmissionBuffer, @@ -258,11 +261,8 @@ func (c *contractor) performContractMaintenance(ctx context.Context, w Worker) ( c.cachedMinScore = minScore c.mu.Unlock() - // create a new ip filter - ipFilter := newIPFilter(c.logger) - // run checks - updatedSet, toArchive, toStopUsing, toRefresh, toRenew, err := c.runContractChecks(ctx, w, contracts, isInCurrentSet, minScore, ipFilter) + updatedSet, toArchive, toStopUsing, toRefresh, toRenew, err := c.runContractChecks(ctx, w, contracts, isInCurrentSet, minScore) if err != nil { return false, fmt.Errorf("failed to run contract checks, err: %v", err) } @@ -344,7 +344,7 @@ func (c *contractor) performContractMaintenance(ctx context.Context, w Worker) ( // check if we need to form contracts and add them to the contract set var formed []types.FileContractID if uint64(len(updatedSet)) < threshold { - formed, err = c.runContractFormations(ctx, w, hosts, usedHosts, state.cfg.Contracts.Amount-uint64(len(updatedSet)), &remaining, minScore, ipFilter) + formed, err = c.runContractFormations(ctx, w, hosts, usedHosts, state.cfg.Contracts.Amount-uint64(len(updatedSet)), &remaining, minScore) if err != nil { c.logger.Errorf("failed to form contracts, err: %v", err) // continue } else { @@ -586,7 +586,7 @@ func (c *contractor) performWalletMaintenance(ctx context.Context) error { return nil } -func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts []api.Contract, inCurrentSet map[types.FileContractID]struct{}, minScore float64, ipFilter *ipFilter) (toKeep []types.FileContractID, toArchive, toStopUsing map[types.FileContractID]string, toRefresh, toRenew []contractInfo, _ error) { +func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts []api.Contract, inCurrentSet map[types.FileContractID]struct{}, minScore float64) (toKeep []types.FileContractID, toArchive, toStopUsing map[types.FileContractID]string, toRefresh, toRenew []contractInfo, _ error) { if c.ap.isStopped() { return } @@ -594,7 +594,6 @@ func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts // convenience variables state := c.ap.State() - shouldFilter := !state.cfg.Hosts.AllowRedundantIPs // fetch consensus state cs, err := c.ap.bus.ConsensusState(ctx) @@ -602,6 +601,9 @@ func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts return nil, nil, nil, nil, nil, err } + // create new IP filter + ipFilter := c.newIPFilter() + // calculate 'maxKeepLeeway' which defines the amount of contracts we'll be // lenient towards when we fail to either fetch a valid price table or the // contract's revision @@ -711,7 +713,7 @@ func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts if contract.Revision == nil { if _, found := inCurrentSet[fcid]; !found || remainingKeepLeeway == 0 { toStopUsing[fcid] = errContractNoRevision.Error() - } else if shouldFilter && ipFilter.IsRedundantIP(contract.HostIP, contract.HostKey) { + } else if !state.cfg.Hosts.AllowRedundantIPs && ipFilter.IsRedundantIP(contract.HostIP, contract.HostKey) { toStopUsing[fcid] = fmt.Sprintf("%v; %v", errHostRedundantIP, errContractNoRevision) } else { toKeep = append(toKeep, fcid) @@ -769,7 +771,7 @@ func (c *contractor) runContractChecks(ctx context.Context, w Worker, contracts return toKeep, toArchive, toStopUsing, toRefresh, toRenew, nil } -func (c *contractor) runContractFormations(ctx context.Context, w Worker, hosts []hostdb.Host, usedHosts map[types.PublicKey]struct{}, missing uint64, budget *types.Currency, minScore float64, ipFilter *ipFilter) ([]types.FileContractID, error) { +func (c *contractor) runContractFormations(ctx context.Context, w Worker, hosts []hostdb.Host, usedHosts map[types.PublicKey]struct{}, missing uint64, budget *types.Currency, minScore float64) ([]types.FileContractID, error) { ctx, span := tracing.Tracer.Start(ctx, "runContractFormations") defer span.End() @@ -815,7 +817,7 @@ func (c *contractor) runContractFormations(ctx context.Context, w Worker, hosts gc := worker.NewGougingChecker(state.gs, cs, state.fee, state.cfg.Contracts.Period, state.cfg.Contracts.RenewWindow) // prepare an IP filter that contains all used hosts - ipFilter.Reset() + ipFilter := c.newIPFilter() for _, h := range hosts { if _, used := usedHosts[h.PublicKey]; used { _ = ipFilter.IsRedundantIP(h.NetAddress, h.PublicKey) diff --git a/autopilot/ipfilter.go b/autopilot/ipfilter.go index 0bc11877c..be34d7699 100644 --- a/autopilot/ipfilter.go +++ b/autopilot/ipfilter.go @@ -17,43 +17,44 @@ const ( ipv4FilterRange = 24 ipv6FilterRange = 32 + // ipCacheEntryValidity defines the amount of time the IP filter uses a + // cached entry when it encounters an error while trying to resolve a host's + // IP address + ipCacheEntryValidity = 24 * time.Hour + // resolverLookupTimeout is the timeout we apply when resolving a host's IP address resolverLookupTimeout = 5 * time.Second ) var ( + errIOTimeout = errors.New("i/o timeout") errNoSuchHost = errors.New("no such host") + errServerMisbehaving = errors.New("server misbehaving") errTooManyAddresses = errors.New("host has more than two addresses, or two of the same type") errUnparsableAddress = errors.New("host address could not be parsed to a subnet") ) -type resolver interface { - LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) -} - -type ipFilter struct { - hostIPToSubnetsCache map[string][]string - subnets map[string]string - resolver resolver - timeout time.Duration +type ( + ipFilter struct { + subnetToHostKey map[string]string + resolver *ipResolver - logger *zap.SugaredLogger -} + logger *zap.SugaredLogger + } +) -func newIPFilter(logger *zap.SugaredLogger) *ipFilter { +func (c *contractor) newIPFilter() *ipFilter { + c.resolver.pruneCache() return &ipFilter{ - hostIPToSubnetsCache: make(map[string][]string), - subnets: make(map[string]string), - resolver: &net.Resolver{}, - timeout: resolverLookupTimeout, - - logger: logger, + subnetToHostKey: make(map[string]string), + resolver: c.resolver, + logger: c.logger, } } func (f *ipFilter) IsRedundantIP(hostIP string, hostKey types.PublicKey) bool { - // perform DNS lookup - subnets, err := f.performDNSLookup(hostIP) + // perform lookup + subnets, err := f.resolver.lookup(hostIP) if err != nil { if !strings.Contains(err.Error(), errNoSuchHost.Error()) { f.logger.Errorf("failed to check for redundant IP, treating host %v with IP %v as redundant, err: %v", hostKey, hostIP, err) @@ -61,57 +62,83 @@ func (f *ipFilter) IsRedundantIP(hostIP string, hostKey types.PublicKey) bool { return true } - // if the lookup failed parse out the host + // return early if we couldn't resolve to a subnet if len(subnets) == 0 { - f.logger.Errorf("failed to check for redundant IP, treating host %v with IP %v as redundant, err: %v", hostKey, hostIP, errUnparsableAddress) + f.logger.Errorf("failed to resolve IP to a subnet, treating host %v with IP %v as redundant, err: %v", hostKey, hostIP, errUnparsableAddress) return true } - // we register all subnets under the host key, so we can safely use the - // first subnet to check whether we already know this host - host, found := f.subnets[subnets[0]] + // check if we know about this subnet, if not register all the subnets + host, found := f.subnetToHostKey[subnets[0]] if !found { for _, subnet := range subnets { - f.subnets[subnet] = hostKey.String() + f.subnetToHostKey[subnet] = hostKey.String() } return false } - // if the given host matches the known host, it's not redundant + // otherwise compare host keys sameHost := host == hostKey.String() return !sameHost } -// Resetting clears the subnets, but not the cache, allowing to rebuild the IP -// filter with a list of hosts. -func (f *ipFilter) Reset() { - f.subnets = make(map[string]string) +type ( + resolver interface { + LookupIPAddr(ctx context.Context, host string) ([]net.IPAddr, error) + } + + ipResolver struct { + resolver resolver + cache map[string]ipCacheEntry + timeout time.Duration + + logger *zap.SugaredLogger + } + + ipCacheEntry struct { + created time.Time + subnets []string + } +) + +func newIPResolver(timeout time.Duration, logger *zap.SugaredLogger) *ipResolver { + if timeout == 0 { + panic("timeout must be greater than zero") // developer error + } + return &ipResolver{ + resolver: &net.Resolver{}, + cache: make(map[string]ipCacheEntry), + timeout: resolverLookupTimeout, + logger: logger, + } } -func (f *ipFilter) performDNSLookup(hostIP string) ([]string, error) { - // check the cache - subnets, found := f.hostIPToSubnetsCache[hostIP] - if found { - return subnets, nil +func (r *ipResolver) pruneCache() { + for hostIP, entry := range r.cache { + if time.Since(entry.created) > ipCacheEntryValidity { + delete(r.cache, hostIP) + } } +} - // lookup all IP addresses for the given host +func (r *ipResolver) lookup(hostIP string) ([]string, error) { + // split off host host, _, err := net.SplitHostPort(hostIP) if err != nil { return nil, err } - // create a context - ctx := context.Background() - if f.timeout > 0 { - var cancel context.CancelFunc - ctx, cancel = context.WithTimeout(context.Background(), f.timeout) - defer cancel() - } + // make sure we don't hang + ctx, cancel := context.WithTimeout(context.Background(), r.timeout) + defer cancel() // lookup IP addresses - addrs, err := f.resolver.LookupIPAddr(ctx, host) - if err != nil { + addrs, err := r.resolver.LookupIPAddr(ctx, host) + if err != nil && (isErr(err, errIOTimeout) || isErr(err, errServerMisbehaving)) { + if entry, found := r.cache[hostIP]; found && time.Since(entry.created) < ipCacheEntryValidity { + r.logger.Debugf("using cached IP addresses for %v, err: %v", hostIP, err) + return entry.subnets, nil + } return nil, err } @@ -121,12 +148,14 @@ func (f *ipFilter) performDNSLookup(hostIP string) ([]string, error) { } // parse out subnets - subnets = parseSubnets(addrs) + subnets := parseSubnets(addrs) - // cache them and return - if len(subnets) > 0 { - f.hostIPToSubnetsCache[hostIP] = subnets + // add to cache + r.cache[hostIP] = ipCacheEntry{ + created: time.Now(), + subnets: subnets, } + return subnets, nil } @@ -153,3 +182,10 @@ func parseSubnets(addresses []net.IPAddr) []string { return subnets } + +func isErr(err error, target error) bool { + if errors.Is(err, target) { + return true + } + return err != nil && target != nil && strings.Contains(err.Error(), target.Error()) +} From 102393e50e89e528b53fe9edee070f853fb52adb Mon Sep 17 00:00:00 2001 From: PJ Date: Wed, 4 Oct 2023 17:29:54 +0200 Subject: [PATCH 3/3] contractor: cleanup PR --- autopilot/contractor.go | 13 +++++++------ autopilot/ipfilter.go | 9 +++++---- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/autopilot/contractor.go b/autopilot/contractor.go index 34220488c..f013fcf9e 100644 --- a/autopilot/contractor.go +++ b/autopilot/contractor.go @@ -126,11 +126,10 @@ type ( ) func newContractor(ap *Autopilot, revisionSubmissionBuffer uint64, revisionBroadcastInterval time.Duration) *contractor { - logger := ap.logger.Named("contractor") return &contractor{ ap: ap, - resolver: newIPResolver(resolverLookupTimeout, logger), - logger: logger, + resolver: newIPResolver(resolverLookupTimeout, ap.logger.Named("resolver")), + logger: ap.logger.Named("contractor"), revisionBroadcastInterval: revisionBroadcastInterval, revisionLastBroadcast: make(map[types.FileContractID]time.Time), revisionSubmissionBuffer: revisionSubmissionBuffer, @@ -818,9 +817,11 @@ func (c *contractor) runContractFormations(ctx context.Context, w Worker, hosts // prepare an IP filter that contains all used hosts ipFilter := c.newIPFilter() - for _, h := range hosts { - if _, used := usedHosts[h.PublicKey]; used { - _ = ipFilter.IsRedundantIP(h.NetAddress, h.PublicKey) + if shouldFilter { + for _, h := range hosts { + if _, used := usedHosts[h.PublicKey]; used { + _ = ipFilter.IsRedundantIP(h.NetAddress, h.PublicKey) + } } } diff --git a/autopilot/ipfilter.go b/autopilot/ipfilter.go index be34d7699..559fa986f 100644 --- a/autopilot/ipfilter.go +++ b/autopilot/ipfilter.go @@ -37,9 +37,9 @@ var ( type ( ipFilter struct { subnetToHostKey map[string]string - resolver *ipResolver - logger *zap.SugaredLogger + resolver *ipResolver + logger *zap.SugaredLogger } ) @@ -47,8 +47,9 @@ func (c *contractor) newIPFilter() *ipFilter { c.resolver.pruneCache() return &ipFilter{ subnetToHostKey: make(map[string]string), - resolver: c.resolver, - logger: c.logger, + + resolver: c.resolver, + logger: c.logger, } }