642 lines
17 KiB
Go
642 lines
17 KiB
Go
// Package dnsforward contains a DNS forwarding server.
|
|
package dnsforward
|
|
|
|
import (
|
|
"fmt"
|
|
"net"
|
|
"net/http"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/AdguardTeam/AdGuardHome/internal/aghnet"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/dhcpd"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/filtering"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/querylog"
|
|
"github.com/AdguardTeam/AdGuardHome/internal/stats"
|
|
"github.com/AdguardTeam/dnsproxy/proxy"
|
|
"github.com/AdguardTeam/dnsproxy/upstream"
|
|
"github.com/AdguardTeam/golibs/cache"
|
|
"github.com/AdguardTeam/golibs/errors"
|
|
"github.com/AdguardTeam/golibs/log"
|
|
"github.com/AdguardTeam/golibs/netutil"
|
|
"github.com/AdguardTeam/golibs/stringutil"
|
|
"github.com/miekg/dns"
|
|
)
|
|
|
|
// DefaultTimeout is the default upstream timeout
|
|
const DefaultTimeout = 10 * time.Second
|
|
|
|
// defaultClientIDCacheCount is the default count of items in the LRU ClientID
|
|
// cache. The assumption here is that there won't be more than this many
|
|
// requests between the BeforeRequestHandler stage and the actual processing.
|
|
const defaultClientIDCacheCount = 1024
|
|
|
|
const (
|
|
safeBrowsingBlockHost = "standard-block.dns.adguard.com"
|
|
parentalBlockHost = "family-block.dns.adguard.com"
|
|
)
|
|
|
|
var defaultDNS = []string{
|
|
"https://dns10.quad9.net/dns-query",
|
|
}
|
|
var defaultBootstrap = []string{"9.9.9.10", "149.112.112.10", "2620:fe::10", "2620:fe::fe:10"}
|
|
|
|
// Often requested by all kinds of DNS probes
|
|
var defaultBlockedHosts = []string{"version.bind", "id.server", "hostname.bind"}
|
|
|
|
var webRegistered bool
|
|
|
|
// hostToIPTable is an alias for the type of Server.tableHostToIP.
|
|
type hostToIPTable = map[string]net.IP
|
|
|
|
// Server is the main way to start a DNS server.
|
|
//
|
|
// Example:
|
|
// s := dnsforward.Server{}
|
|
// err := s.Start(nil) // will start a DNS server listening on default port 53, in a goroutine
|
|
// err := s.Reconfigure(ServerConfig{UDPListenAddr: &net.UDPAddr{Port: 53535}}) // will reconfigure running DNS server to listen on UDP port 53535
|
|
// err := s.Stop() // will stop listening on port 53535 and cancel all goroutines
|
|
// err := s.Start(nil) // will start listening again, on port 53535, in a goroutine
|
|
//
|
|
// The zero Server is empty and ready for use.
|
|
type Server struct {
|
|
dnsProxy *proxy.Proxy // DNS proxy instance
|
|
dnsFilter *filtering.DNSFilter // DNS filter instance
|
|
dhcpServer dhcpd.ServerInterface // DHCP server instance (optional)
|
|
queryLog querylog.QueryLog // Query log instance
|
|
stats stats.Stats
|
|
access *accessCtx
|
|
|
|
// localDomainSuffix is the suffix used to detect internal hosts. It
|
|
// must be a valid domain name plus dots on each side.
|
|
localDomainSuffix string
|
|
|
|
ipset ipsetCtx
|
|
subnetDetector *aghnet.SubnetDetector
|
|
localResolvers *proxy.Proxy
|
|
sysResolvers aghnet.SystemResolvers
|
|
recDetector *recursionDetector
|
|
|
|
// anonymizer masks the client's IP addresses if needed.
|
|
anonymizer *aghnet.IPMut
|
|
|
|
tableHostToIP hostToIPTable
|
|
tableHostToIPLock sync.Mutex
|
|
|
|
tableIPToHost *netutil.IPMap
|
|
tableIPToHostLock sync.Mutex
|
|
|
|
// clientIDCache is a temporary storage for ClientIDs that were extracted
|
|
// during the BeforeRequestHandler stage.
|
|
clientIDCache cache.Cache
|
|
|
|
// DNS proxy instance for internal usage
|
|
// We don't Start() it and so no listen port is required.
|
|
internalProxy *proxy.Proxy
|
|
|
|
isRunning bool
|
|
|
|
conf ServerConfig
|
|
// serverLock protects Server.
|
|
serverLock sync.RWMutex
|
|
}
|
|
|
|
// defaultLocalDomainSuffix is the default suffix used to detect internal hosts
|
|
// when no suffix is provided.
|
|
//
|
|
// See the documentation for Server.localDomainSuffix.
|
|
const defaultLocalDomainSuffix = ".lan."
|
|
|
|
// DNSCreateParams are parameters to create a new server.
|
|
type DNSCreateParams struct {
|
|
DNSFilter *filtering.DNSFilter
|
|
Stats stats.Stats
|
|
QueryLog querylog.QueryLog
|
|
DHCPServer dhcpd.ServerInterface
|
|
SubnetDetector *aghnet.SubnetDetector
|
|
Anonymizer *aghnet.IPMut
|
|
LocalDomain string
|
|
}
|
|
|
|
// domainNameToSuffix converts a domain name into a local domain suffix.
|
|
func domainNameToSuffix(tld string) (suffix string) {
|
|
l := len(tld) + 2
|
|
b := make([]byte, l)
|
|
b[0] = '.'
|
|
copy(b[1:], tld)
|
|
b[l-1] = '.'
|
|
|
|
return string(b)
|
|
}
|
|
|
|
const (
|
|
// recursionTTL is the time recursive request is cached for.
|
|
recursionTTL = 1 * time.Second
|
|
// cachedRecurrentReqNum is the maximum number of cached recurrent
|
|
// requests.
|
|
cachedRecurrentReqNum = 1000
|
|
)
|
|
|
|
// NewServer creates a new instance of the dnsforward.Server
|
|
// Note: this function must be called only once
|
|
func NewServer(p DNSCreateParams) (s *Server, err error) {
|
|
var localDomainSuffix string
|
|
if p.LocalDomain == "" {
|
|
localDomainSuffix = defaultLocalDomainSuffix
|
|
} else {
|
|
err = netutil.ValidateDomainName(p.LocalDomain)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("local domain: %w", err)
|
|
}
|
|
|
|
localDomainSuffix = domainNameToSuffix(p.LocalDomain)
|
|
}
|
|
|
|
if p.Anonymizer == nil {
|
|
p.Anonymizer = aghnet.NewIPMut(nil)
|
|
}
|
|
s = &Server{
|
|
dnsFilter: p.DNSFilter,
|
|
stats: p.Stats,
|
|
queryLog: p.QueryLog,
|
|
subnetDetector: p.SubnetDetector,
|
|
localDomainSuffix: localDomainSuffix,
|
|
recDetector: newRecursionDetector(recursionTTL, cachedRecurrentReqNum),
|
|
clientIDCache: cache.New(cache.Config{
|
|
EnableLRU: true,
|
|
MaxCount: defaultClientIDCacheCount,
|
|
}),
|
|
anonymizer: p.Anonymizer,
|
|
}
|
|
|
|
// TODO(e.burkov): Enable the refresher after the actual implementation
|
|
// passes the public testing.
|
|
s.sysResolvers, err = aghnet.NewSystemResolvers(0, nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("initializing system resolvers: %w", err)
|
|
}
|
|
|
|
if p.DHCPServer != nil {
|
|
s.dhcpServer = p.DHCPServer
|
|
s.dhcpServer.SetOnLeaseChanged(s.onDHCPLeaseChanged)
|
|
s.onDHCPLeaseChanged(dhcpd.LeaseChangedAdded)
|
|
}
|
|
|
|
if runtime.GOARCH == "mips" || runtime.GOARCH == "mipsle" {
|
|
// Use plain DNS on MIPS, encryption is too slow
|
|
defaultDNS = defaultBootstrap
|
|
}
|
|
|
|
return s, nil
|
|
}
|
|
|
|
// NewCustomServer creates a new instance of *Server with custom internal proxy.
|
|
func NewCustomServer(internalProxy *proxy.Proxy) *Server {
|
|
s := &Server{
|
|
recDetector: newRecursionDetector(0, 1),
|
|
}
|
|
if internalProxy != nil {
|
|
s.internalProxy = internalProxy
|
|
}
|
|
|
|
return s
|
|
}
|
|
|
|
// Close gracefully closes the server. It is safe for concurrent use.
|
|
//
|
|
// TODO(e.burkov): A better approach would be making Stop method waiting for all
|
|
// its workers finished. But it would require the upstream.Upstream to have the
|
|
// Close method to prevent from hanging while waiting for unresponsive server to
|
|
// respond.
|
|
func (s *Server) Close() {
|
|
s.serverLock.Lock()
|
|
defer s.serverLock.Unlock()
|
|
|
|
s.dnsFilter = nil
|
|
s.stats = nil
|
|
s.queryLog = nil
|
|
s.dnsProxy = nil
|
|
|
|
if err := s.ipset.close(); err != nil {
|
|
log.Error("closing ipset: %s", err)
|
|
}
|
|
}
|
|
|
|
// WriteDiskConfig - write configuration
|
|
func (s *Server) WriteDiskConfig(c *FilteringConfig) {
|
|
s.serverLock.RLock()
|
|
defer s.serverLock.RUnlock()
|
|
|
|
sc := s.conf.FilteringConfig
|
|
*c = sc
|
|
c.RatelimitWhitelist = stringutil.CloneSlice(sc.RatelimitWhitelist)
|
|
c.BootstrapDNS = stringutil.CloneSlice(sc.BootstrapDNS)
|
|
c.AllowedClients = stringutil.CloneSlice(sc.AllowedClients)
|
|
c.DisallowedClients = stringutil.CloneSlice(sc.DisallowedClients)
|
|
c.BlockedHosts = stringutil.CloneSlice(sc.BlockedHosts)
|
|
c.TrustedProxies = stringutil.CloneSlice(sc.TrustedProxies)
|
|
c.UpstreamDNS = stringutil.CloneSlice(sc.UpstreamDNS)
|
|
}
|
|
|
|
// RDNSSettings returns the copy of actual RDNS configuration.
|
|
func (s *Server) RDNSSettings() (localPTRResolvers []string, resolveClients, resolvePTR bool) {
|
|
s.serverLock.RLock()
|
|
defer s.serverLock.RUnlock()
|
|
|
|
return stringutil.CloneSlice(s.conf.LocalPTRResolvers),
|
|
s.conf.ResolveClients,
|
|
s.conf.UsePrivateRDNS
|
|
}
|
|
|
|
// Resolve - get IP addresses by host name from an upstream server.
|
|
// No request/response filtering is performed.
|
|
// Query log and Stats are not updated.
|
|
// This method may be called before Start().
|
|
func (s *Server) Resolve(host string) ([]net.IPAddr, error) {
|
|
s.serverLock.RLock()
|
|
defer s.serverLock.RUnlock()
|
|
|
|
return s.internalProxy.LookupIPAddr(host)
|
|
}
|
|
|
|
// RDNSExchanger is a resolver for clients' addresses.
|
|
type RDNSExchanger interface {
|
|
// Exchange tries to resolve the ip in a suitable way, e.g. either as
|
|
// local or as external.
|
|
Exchange(ip net.IP) (host string, err error)
|
|
// ResolvesPrivatePTR returns true if the RDNSExchanger is able to
|
|
// resolve PTR requests for locally-served addresses.
|
|
ResolvesPrivatePTR() (ok bool)
|
|
}
|
|
|
|
const (
|
|
// rDNSEmptyAnswerErr is returned by Exchange method when the answer
|
|
// section of respond is empty.
|
|
rDNSEmptyAnswerErr errors.Error = "the answer section is empty"
|
|
|
|
// rDNSNotPTRErr is returned by Exchange method when the response is not
|
|
// of PTR type.
|
|
rDNSNotPTRErr errors.Error = "the response is not a ptr"
|
|
)
|
|
|
|
// Exchange implements the RDNSExchanger interface for *Server.
|
|
func (s *Server) Exchange(ip net.IP) (host string, err error) {
|
|
s.serverLock.RLock()
|
|
defer s.serverLock.RUnlock()
|
|
|
|
if !s.conf.ResolveClients {
|
|
return "", nil
|
|
}
|
|
|
|
arpa, err := netutil.IPToReversedAddr(ip)
|
|
if err != nil {
|
|
return "", fmt.Errorf("reversing ip: %w", err)
|
|
}
|
|
|
|
arpa = dns.Fqdn(arpa)
|
|
req := &dns.Msg{
|
|
MsgHdr: dns.MsgHdr{
|
|
Id: dns.Id(),
|
|
RecursionDesired: true,
|
|
},
|
|
Compress: true,
|
|
Question: []dns.Question{{
|
|
Name: arpa,
|
|
Qtype: dns.TypePTR,
|
|
Qclass: dns.ClassINET,
|
|
}},
|
|
}
|
|
ctx := &proxy.DNSContext{
|
|
Proto: "udp",
|
|
Req: req,
|
|
StartTime: time.Now(),
|
|
}
|
|
|
|
resolver := s.internalProxy
|
|
if s.subnetDetector.IsLocallyServedNetwork(ip) {
|
|
if !s.conf.UsePrivateRDNS {
|
|
return "", nil
|
|
}
|
|
|
|
resolver = s.localResolvers
|
|
s.recDetector.add(*req)
|
|
}
|
|
|
|
if err = resolver.Resolve(ctx); err != nil {
|
|
return "", err
|
|
}
|
|
|
|
resp := ctx.Res
|
|
if len(resp.Answer) == 0 {
|
|
return "", fmt.Errorf("lookup for %q: %w", arpa, rDNSEmptyAnswerErr)
|
|
}
|
|
|
|
ptr, ok := resp.Answer[0].(*dns.PTR)
|
|
if !ok {
|
|
return "", fmt.Errorf("type checking: %w", rDNSNotPTRErr)
|
|
}
|
|
|
|
return strings.TrimSuffix(ptr.Ptr, "."), nil
|
|
}
|
|
|
|
// ResolvesPrivatePTR implements the RDNSExchanger interface for *Server.
|
|
func (s *Server) ResolvesPrivatePTR() (ok bool) {
|
|
s.serverLock.RLock()
|
|
defer s.serverLock.RUnlock()
|
|
|
|
return s.conf.UsePrivateRDNS
|
|
}
|
|
|
|
// Start starts the DNS server.
|
|
func (s *Server) Start() error {
|
|
s.serverLock.Lock()
|
|
defer s.serverLock.Unlock()
|
|
|
|
return s.startLocked()
|
|
}
|
|
|
|
// startLocked starts the DNS server without locking. For internal use only.
|
|
func (s *Server) startLocked() error {
|
|
err := s.dnsProxy.Start()
|
|
if err == nil {
|
|
s.isRunning = true
|
|
}
|
|
return err
|
|
}
|
|
|
|
// defaultLocalTimeout is the default timeout for resolving addresses from
|
|
// locally-served networks. It is assumed that local resolvers should work much
|
|
// faster than ordinary upstreams.
|
|
const defaultLocalTimeout = 1 * time.Second
|
|
|
|
// collectDNSIPAddrs returns IP addresses the server is listening on without
|
|
// port numbers. For internal use only.
|
|
func (s *Server) collectDNSIPAddrs() (addrs []string, err error) {
|
|
addrs = make([]string, len(s.conf.TCPListenAddrs)+len(s.conf.UDPListenAddrs))
|
|
var i int
|
|
var ip net.IP
|
|
for _, addr := range s.conf.TCPListenAddrs {
|
|
if addr == nil {
|
|
continue
|
|
}
|
|
|
|
if ip = addr.IP; ip.IsUnspecified() {
|
|
return aghnet.CollectAllIfacesAddrs()
|
|
}
|
|
|
|
addrs[i] = ip.String()
|
|
i++
|
|
}
|
|
for _, addr := range s.conf.UDPListenAddrs {
|
|
if addr == nil {
|
|
continue
|
|
}
|
|
|
|
if ip = addr.IP; ip.IsUnspecified() {
|
|
return aghnet.CollectAllIfacesAddrs()
|
|
}
|
|
|
|
addrs[i] = ip.String()
|
|
i++
|
|
}
|
|
|
|
return addrs[:i], nil
|
|
}
|
|
|
|
func (s *Server) filterOurDNSAddrs(addrs []string) (filtered []string, err error) {
|
|
var ourAddrs []string
|
|
ourAddrs, err = s.collectDNSIPAddrs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ourAddrsSet := stringutil.NewSet(ourAddrs...)
|
|
|
|
// TODO(e.burkov): The approach of subtracting sets of strings is not
|
|
// really applicable here since in case of listening on all network
|
|
// interfaces we should check the whole interface's network to cut off
|
|
// all the loopback addresses as well.
|
|
return stringutil.FilterOut(addrs, ourAddrsSet.Has), nil
|
|
}
|
|
|
|
// setupResolvers initializes the resolvers for local addresses. For internal
|
|
// use only.
|
|
func (s *Server) setupResolvers(localAddrs []string) (err error) {
|
|
bootstraps := s.conf.BootstrapDNS
|
|
if len(localAddrs) == 0 {
|
|
localAddrs = s.sysResolvers.Get()
|
|
bootstraps = nil
|
|
}
|
|
|
|
localAddrs, err = s.filterOurDNSAddrs(localAddrs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debug("upstreams to resolve PTR for local addresses: %v", localAddrs)
|
|
|
|
var upsConfig *proxy.UpstreamConfig
|
|
upsConfig, err = proxy.ParseUpstreamsConfig(
|
|
localAddrs,
|
|
&upstream.Options{
|
|
Bootstrap: bootstraps,
|
|
Timeout: defaultLocalTimeout,
|
|
// TODO(e.burkov): Should we verify server's certificates?
|
|
},
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing upstreams: %w", err)
|
|
}
|
|
|
|
s.localResolvers = &proxy.Proxy{
|
|
Config: proxy.Config{
|
|
UpstreamConfig: upsConfig,
|
|
},
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Prepare the object
|
|
func (s *Server) Prepare(config *ServerConfig) error {
|
|
// Initialize the server configuration
|
|
// --
|
|
if config != nil {
|
|
s.conf = *config
|
|
if s.conf.BlockingMode == "custom_ip" {
|
|
if s.conf.BlockingIPv4 == nil || s.conf.BlockingIPv6 == nil {
|
|
return fmt.Errorf("dns: invalid custom blocking IP address specified")
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set default values in the case if nothing is configured
|
|
// --
|
|
s.initDefaultSettings()
|
|
|
|
// Initialize ipset configuration
|
|
// --
|
|
err := s.ipset.init(s.conf.IpsetList)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
log.Debug("inited ipset")
|
|
|
|
// Prepare DNS servers settings
|
|
// --
|
|
err = s.prepareUpstreamSettings()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Create DNS proxy configuration
|
|
// --
|
|
var proxyConfig proxy.Config
|
|
proxyConfig, err = s.createProxyConfig()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Prepare a DNS proxy instance that we use for internal DNS queries
|
|
// --
|
|
s.prepareIntlProxy()
|
|
|
|
s.access, err = newAccessCtx(s.conf.AllowedClients, s.conf.DisallowedClients, s.conf.BlockedHosts)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Register web handlers if necessary
|
|
// --
|
|
if !webRegistered && s.conf.HTTPRegister != nil {
|
|
webRegistered = true
|
|
s.registerHandlers()
|
|
}
|
|
|
|
// Create the main DNS proxy instance
|
|
// --
|
|
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
|
|
|
|
err = s.setupResolvers(s.conf.LocalPTRResolvers)
|
|
if err != nil {
|
|
return fmt.Errorf("setting up resolvers: %w", err)
|
|
}
|
|
|
|
s.recDetector.clear()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Stop stops the DNS server.
|
|
func (s *Server) Stop() error {
|
|
s.serverLock.Lock()
|
|
defer s.serverLock.Unlock()
|
|
|
|
return s.stopLocked()
|
|
}
|
|
|
|
// stopLocked stops the DNS server without locking. For internal use only.
|
|
func (s *Server) stopLocked() error {
|
|
if s.dnsProxy != nil {
|
|
err := s.dnsProxy.Stop()
|
|
if err != nil {
|
|
return fmt.Errorf("could not stop the DNS server properly: %w", err)
|
|
}
|
|
}
|
|
|
|
s.isRunning = false
|
|
return nil
|
|
}
|
|
|
|
// IsRunning returns true if the DNS server is running.
|
|
func (s *Server) IsRunning() bool {
|
|
s.serverLock.RLock()
|
|
defer s.serverLock.RUnlock()
|
|
|
|
return s.isRunning
|
|
}
|
|
|
|
// srvClosedErr is returned when the method can't complete without inaccessible
|
|
// data from the closing server.
|
|
const srvClosedErr errors.Error = "server is closed"
|
|
|
|
// proxy returns a pointer to the current DNS proxy instance. If p is nil, the
|
|
// server is closing.
|
|
//
|
|
// See https://github.com/AdguardTeam/AdGuardHome/issues/3655.
|
|
func (s *Server) proxy() (p *proxy.Proxy) {
|
|
s.serverLock.RLock()
|
|
defer s.serverLock.RUnlock()
|
|
|
|
return s.dnsProxy
|
|
}
|
|
|
|
// Reconfigure applies the new configuration to the DNS server.
|
|
func (s *Server) Reconfigure(config *ServerConfig) error {
|
|
s.serverLock.Lock()
|
|
defer s.serverLock.Unlock()
|
|
|
|
log.Print("Start reconfiguring the server")
|
|
err := s.stopLocked()
|
|
if err != nil {
|
|
return fmt.Errorf("could not reconfigure the server: %w", err)
|
|
}
|
|
|
|
// It seems that net.Listener.Close() doesn't close file descriptors right away.
|
|
// We wait for some time and hope that this fd will be closed.
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
err = s.Prepare(config)
|
|
if err != nil {
|
|
return fmt.Errorf("could not reconfigure the server: %w", err)
|
|
}
|
|
|
|
err = s.startLocked()
|
|
if err != nil {
|
|
return fmt.Errorf("could not reconfigure the server: %w", err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ServeHTTP is a HTTP handler method we use to provide DNS-over-HTTPS.
|
|
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|
if prx := s.proxy(); prx != nil {
|
|
prx.ServeHTTP(w, r)
|
|
}
|
|
}
|
|
|
|
// IsBlockedClient returns true if the client is blocked by the current access
|
|
// settings.
|
|
func (s *Server) IsBlockedClient(ip net.IP, clientID string) (blocked bool, rule string) {
|
|
s.serverLock.RLock()
|
|
defer s.serverLock.RUnlock()
|
|
|
|
allowlistMode := s.access.allowlistMode()
|
|
blockedByIP, rule := s.access.isBlockedIP(ip)
|
|
blockedByClientID := s.access.isBlockedClientID(clientID)
|
|
|
|
// Allow if at least one of the checks allows in allowlist mode, but
|
|
// block if at least one of the checks blocks in blocklist mode.
|
|
if allowlistMode && blockedByIP && blockedByClientID {
|
|
log.Debug("client %s (id %q) is not in access allowlist", ip, clientID)
|
|
|
|
// Return now without substituting the empty rule for the
|
|
// clientID because the rule can't be empty here.
|
|
return true, rule
|
|
} else if !allowlistMode && (blockedByIP || blockedByClientID) {
|
|
log.Debug("client %s (id %q) is in access blocklist", ip, clientID)
|
|
|
|
blocked = true
|
|
}
|
|
|
|
if rule == "" {
|
|
rule = clientID
|
|
}
|
|
|
|
return blocked, rule
|
|
}
|