package swarm import ( "fmt" "sync" ma "github.com/multiformats/go-multiaddr" manet "github.com/multiformats/go-multiaddr/net" ) type blackHoleState int const ( blackHoleStateProbing blackHoleState = iota blackHoleStateAllowed blackHoleStateBlocked ) func (st blackHoleState) String() string { switch st { case blackHoleStateProbing: return "Probing" case blackHoleStateAllowed: return "Allowed" case blackHoleStateBlocked: return "Blocked" default: return fmt.Sprintf("Unknown %d", st) } } type blackHoleResult int const ( blackHoleResultAllowed blackHoleResult = iota blackHoleResultProbing blackHoleResultBlocked ) // blackHoleFilter provides black hole filtering for dials. This filter should be used in // concert with a UDP of IPv6 address filter to detect UDP or IPv6 black hole. In a black // holed environments dial requests are blocked and only periodic probes to check the // state of the black hole are allowed. // // Requests are blocked if the number of successes in the last n dials is less than // minSuccesses. If a request succeeds in Blocked state, the filter state is reset and n // subsequent requests are allowed before reevaluating black hole state. Dials cancelled // when some other concurrent dial succeeded are counted as failures. A sufficiently large // n prevents false negatives in such cases. type blackHoleFilter struct { // n serves the dual purpose of being the minimum number of requests after which we // probe the state of the black hole in blocked state and the minimum number of // completed dials required before evaluating black hole state. n int // minSuccesses is the minimum number of Success required in the last n dials // to consider we are not blocked. minSuccesses int // name for the detector. name string // requests counts number of dial requests to peers. We handle request at a peer // level and record results at individual address dial level. requests int // dialResults of the last `n` dials. A successful dial is true. dialResults []bool // successes is the count of successful dials in outcomes successes int // state is the current state of the detector state blackHoleState mu sync.Mutex metricsTracer MetricsTracer } // RecordResult records the outcome of a dial. A successful dial will change the state // of the filter to Allowed. A failed dial only blocks subsequent requests if the success // fraction over the last n outcomes is less than the minSuccessFraction of the filter. func (b *blackHoleFilter) RecordResult(success bool) { b.mu.Lock() defer b.mu.Unlock() if b.state == blackHoleStateBlocked && success { // If the call succeeds in a blocked state we reset to allowed. // This is better than slowly accumulating values till we cross the minSuccessFraction // threshold since a blackhole is a binary property. b.reset() return } if success { b.successes++ } b.dialResults = append(b.dialResults, success) if len(b.dialResults) > b.n { if b.dialResults[0] { b.successes-- } b.dialResults = b.dialResults[1:] } b.updateState() b.trackMetrics() } // HandleRequest returns the result of applying the black hole filter for the request. func (b *blackHoleFilter) HandleRequest() blackHoleResult { b.mu.Lock() defer b.mu.Unlock() b.requests++ b.trackMetrics() if b.state == blackHoleStateAllowed { return blackHoleResultAllowed } else if b.state == blackHoleStateProbing || b.requests%b.n == 0 { return blackHoleResultProbing } else { return blackHoleResultBlocked } } func (b *blackHoleFilter) reset() { b.successes = 0 b.dialResults = b.dialResults[:0] b.requests = 0 b.updateState() } func (b *blackHoleFilter) updateState() { st := b.state if len(b.dialResults) < b.n { b.state = blackHoleStateProbing } else if b.successes >= b.minSuccesses { b.state = blackHoleStateAllowed } else { b.state = blackHoleStateBlocked } if st != b.state { log.Debugf("%s blackHoleDetector state changed from %s to %s", b.name, st, b.state) } } func (b *blackHoleFilter) trackMetrics() { if b.metricsTracer == nil { return } nextRequestAllowedAfter := 0 if b.state == blackHoleStateBlocked { nextRequestAllowedAfter = b.n - (b.requests % b.n) } successFraction := 0.0 if len(b.dialResults) > 0 { successFraction = float64(b.successes) / float64(len(b.dialResults)) } b.metricsTracer.UpdatedBlackHoleFilterState( b.name, b.state, nextRequestAllowedAfter, successFraction, ) } // blackHoleDetector provides UDP and IPv6 black hole detection using a `blackHoleFilter` // for each. For details of the black hole detection logic see `blackHoleFilter`. // // black hole filtering is done at a peer dial level to ensure that periodic probes to // detect change of the black hole state are actually dialed and are not skipped // because of dial prioritisation logic. type blackHoleDetector struct { udp, ipv6 *blackHoleFilter } // FilterAddrs filters the peer's addresses removing black holed addresses func (d *blackHoleDetector) FilterAddrs(addrs []ma.Multiaddr) (valid []ma.Multiaddr, blackHoled []ma.Multiaddr) { hasUDP, hasIPv6 := false, false for _, a := range addrs { if is, err := manet.IsPublicAddr(a); !is || err != nil { continue } if isProtocolAddr(a, ma.P_UDP) { hasUDP = true } if isProtocolAddr(a, ma.P_IP6) { hasIPv6 = true } } udpRes := blackHoleResultAllowed if d.udp != nil && hasUDP { udpRes = d.udp.HandleRequest() } ipv6Res := blackHoleResultAllowed if d.ipv6 != nil && hasIPv6 { ipv6Res = d.ipv6.HandleRequest() } blackHoled = make([]ma.Multiaddr, 0, len(addrs)) return ma.FilterAddrs( addrs, func(a ma.Multiaddr) bool { if is, err := manet.IsPublicAddr(a); !is && err == nil { return true } // allow all UDP addresses while probing irrespective of IPv6 black hole state if udpRes == blackHoleResultProbing && isProtocolAddr(a, ma.P_UDP) { return true } // allow all IPv6 addresses while probing irrespective of UDP black hole state if ipv6Res == blackHoleResultProbing && isProtocolAddr(a, ma.P_IP6) { return true } if udpRes == blackHoleResultBlocked && isProtocolAddr(a, ma.P_UDP) { blackHoled = append(blackHoled, a) return false } if ipv6Res == blackHoleResultBlocked && isProtocolAddr(a, ma.P_IP6) { blackHoled = append(blackHoled, a) return false } return true }, ), blackHoled } // RecordResult updates the state of the relevant `blackHoleFilter`s for addr func (d *blackHoleDetector) RecordResult(addr ma.Multiaddr, success bool) { if is, err := manet.IsPublicAddr(addr); !is && err == nil { return } if d.udp != nil && isProtocolAddr(addr, ma.P_UDP) { d.udp.RecordResult(success) } if d.ipv6 != nil && isProtocolAddr(addr, ma.P_IP6) { d.ipv6.RecordResult(success) } } // blackHoleConfig is the config used for black hole detection type blackHoleConfig struct { // Enabled enables black hole detection Enabled bool // N is the size of the sliding window used to evaluate black hole state N int // MinSuccesses is the minimum number of successes out of N required to not // block requests MinSuccesses int } func newBlackHoleDetector(udpConfig, ipv6Config blackHoleConfig, mt MetricsTracer) *blackHoleDetector { d := &blackHoleDetector{} if udpConfig.Enabled { d.udp = &blackHoleFilter{ n: udpConfig.N, minSuccesses: udpConfig.MinSuccesses, name: "UDP", metricsTracer: mt, } } if ipv6Config.Enabled { d.ipv6 = &blackHoleFilter{ n: ipv6Config.N, minSuccesses: ipv6Config.MinSuccesses, name: "IPv6", metricsTracer: mt, } } return d }