Files
elmprodvpn/selective-vpn-api/app/resolver_dns_cooldown.go

143 lines
3.1 KiB
Go

package app
import (
"os"
"strings"
)
func newDNSRunCooldown() *dnsRunCooldown {
enabled := true
switch strings.ToLower(strings.TrimSpace(os.Getenv("RESOLVE_DNS_COOLDOWN_ENABLED"))) {
case "0", "false", "no", "off":
enabled = false
}
c := &dnsRunCooldown{
enabled: enabled,
minAttempts: envInt("RESOLVE_DNS_COOLDOWN_MIN_ATTEMPTS", 300),
timeoutRatePct: envInt("RESOLVE_DNS_COOLDOWN_TIMEOUT_RATE_PCT", 70),
failStreak: envInt("RESOLVE_DNS_COOLDOWN_FAIL_STREAK", 25),
banSec: envInt("RESOLVE_DNS_COOLDOWN_BAN_SEC", 60),
maxBanSec: envInt("RESOLVE_DNS_COOLDOWN_MAX_BAN_SEC", 300),
temporaryAsError: true,
byUpstream: map[string]*dnsCooldownState{},
}
if c.minAttempts < 50 {
c.minAttempts = 50
}
if c.minAttempts > 2000 {
c.minAttempts = 2000
}
if c.timeoutRatePct < 40 {
c.timeoutRatePct = 40
}
if c.timeoutRatePct > 95 {
c.timeoutRatePct = 95
}
if c.failStreak < 8 {
c.failStreak = 8
}
if c.failStreak > 200 {
c.failStreak = 200
}
if c.banSec < 10 {
c.banSec = 10
}
if c.banSec > 3600 {
c.banSec = 3600
}
if c.maxBanSec < c.banSec {
c.maxBanSec = c.banSec
}
if c.maxBanSec > 3600 {
c.maxBanSec = 3600
}
return c
}
func (c *dnsRunCooldown) configSnapshot() (enabled bool, minAttempts, timeoutRatePct, failStreak, banSec, maxBanSec int) {
if c == nil {
return false, 0, 0, 0, 0, 0
}
return c.enabled, c.minAttempts, c.timeoutRatePct, c.failStreak, c.banSec, c.maxBanSec
}
func (c *dnsRunCooldown) stateFor(upstream string) *dnsCooldownState {
if c.byUpstream == nil {
c.byUpstream = map[string]*dnsCooldownState{}
}
st, ok := c.byUpstream[upstream]
if ok {
return st
}
st = &dnsCooldownState{}
c.byUpstream[upstream] = st
return st
}
func (c *dnsRunCooldown) shouldSkip(upstream string, now int64) bool {
if c == nil || !c.enabled {
return false
}
c.mu.Lock()
defer c.mu.Unlock()
st := c.stateFor(upstream)
return st.BanUntil > now
}
func (c *dnsRunCooldown) observeSuccess(upstream string) {
if c == nil || !c.enabled {
return
}
c.mu.Lock()
defer c.mu.Unlock()
st := c.stateFor(upstream)
st.Attempts++
st.FailStreak = 0
}
func (c *dnsRunCooldown) observeError(upstream string, kind dnsErrorKind, now int64) (bool, int) {
if c == nil || !c.enabled {
return false, 0
}
c.mu.Lock()
defer c.mu.Unlock()
st := c.stateFor(upstream)
st.Attempts++
timeoutLike := kind == dnsErrorTimeout || (c.temporaryAsError && kind == dnsErrorTemporary)
if timeoutLike {
st.TimeoutLike++
st.FailStreak++
} else {
st.FailStreak = 0
return false, 0
}
if st.BanUntil > now {
return false, 0
}
rateBan := st.Attempts >= c.minAttempts && (st.TimeoutLike*100 >= c.timeoutRatePct*st.Attempts)
streakBan := st.FailStreak >= c.failStreak
if !rateBan && !streakBan {
return false, 0
}
st.BanLevel++
dur := c.banSec
if st.BanLevel > 1 {
for i := 1; i < st.BanLevel; i++ {
dur *= 2
if dur >= c.maxBanSec {
dur = c.maxBanSec
break
}
}
}
if dur > c.maxBanSec {
dur = c.maxBanSec
}
st.BanUntil = now + int64(dur)
st.FailStreak = 0
return true, dur
}