Pull request: 2179 ipset subdomains

Merge in DNS/adguard-home from 2179-ipset-subdomains to master

Closes #2179.

Squashed commit of the following:

commit de17caac4c2ea2bc7931f162c6dfa7822a71554f
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Fri Jan 29 18:34:46 2021 +0300

    dnsforward: imp code, docs

commit e5ab957560bcfba80feac4b72f9b22535ecd4c7d
Author: Ainar Garipov <A.Garipov@AdGuard.COM>
Date:   Tue Jan 26 20:43:31 2021 +0300

    dnsforward: imp code

commit 2b84d27b752832885e4896d0e75de2576e2b965b
Author: David Sheets <sheets@alum.mit.edu>
Date:   Tue Oct 6 16:34:06 2020 +0100

    dnsforward: support subdomain matching in ipset

    This is a squash of all commits in #2179.
This commit is contained in:
Ainar Garipov 2021-01-29 18:53:39 +03:00
parent 0d0a419bd3
commit 510573a904
8 changed files with 425 additions and 186 deletions

View File

@ -10,11 +10,12 @@ and this project adheres to
## [Unreleased]
<!--
## [v0.105.0] - 2021-01-27
## [v0.105.0] - 2021-02-03
-->
### Added
- `ipset` subdomain matching, just like `dnsmasq` does ([#2179]).
- Client ID support for DNS-over-HTTPS, DNS-over-QUIC, and DNS-over-TLS
([#1383]).
- `$dnsrewrite` modifier for filters ([#2102]).
@ -31,6 +32,7 @@ and this project adheres to
[#1361]: https://github.com/AdguardTeam/AdGuardHome/issues/1361
[#1383]: https://github.com/AdguardTeam/AdGuardHome/issues/1383
[#2102]: https://github.com/AdguardTeam/AdGuardHome/issues/2102
[#2179]: https://github.com/AdguardTeam/AdGuardHome/issues/2179
[#2302]: https://github.com/AdguardTeam/AdGuardHome/issues/2302
[#2304]: https://github.com/AdguardTeam/AdGuardHome/issues/2304
[#2305]: https://github.com/AdguardTeam/AdGuardHome/issues/2305

3
go.mod
View File

@ -8,6 +8,7 @@ require (
github.com/AdguardTeam/urlfilter v0.14.2
github.com/NYTimes/gziphandler v1.1.1
github.com/ameshkov/dnscrypt/v2 v2.0.1
github.com/digineo/go-ipset/v2 v2.2.1
github.com/fsnotify/fsnotify v1.4.9
github.com/go-ping/ping v0.0.0-20201115131931-3300c582a663
github.com/gobuffalo/envy v1.9.0 // indirect
@ -19,6 +20,7 @@ require (
github.com/karrick/godirwalk v1.16.1 // indirect
github.com/lucas-clemente/quic-go v0.19.3
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7
github.com/mdlayher/netlink v1.1.2-0.20201013204415-ded538f7f4be
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065
github.com/miekg/dns v1.1.35
github.com/rogpeppe/go-internal v1.6.2 // indirect
@ -26,6 +28,7 @@ require (
github.com/sirupsen/logrus v1.7.0 // indirect
github.com/spf13/cobra v1.1.1 // indirect
github.com/stretchr/testify v1.6.1
github.com/ti-mo/netfilter v0.4.0
github.com/u-root/u-root v7.0.0+incompatible
go.etcd.io/bbolt v1.3.5
golang.org/x/crypto v0.0.0-20201217014255-9d1352758620

13
go.sum
View File

@ -83,6 +83,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ=
github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no=
github.com/digineo/go-ipset/v2 v2.2.1 h1:k6skY+0fMqeUjjeWO/m5OuWPSZUAn7AucHMnQ1MX77g=
github.com/digineo/go-ipset/v2 v2.2.1/go.mod h1:wBsNzJlZlABHUITkesrggFnZQtgW5wkqw1uo8Qxe0VU=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
@ -223,6 +225,7 @@ github.com/joomcode/errorx v1.0.3/go.mod h1:eQzdtdlNyN7etw6YCS4W4+lu442waxZYw5yv
github.com/jsimonetti/rtnetlink v0.0.0-20190606172950-9527aa82566a/go.mod h1:Oz+70psSo5OFh8DBl0Zv2ACw7Esh6pPUphlvZG9x7uw=
github.com/jsimonetti/rtnetlink v0.0.0-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c h1:7cpGGTQO6+OuYQWkueqeXuErSjs1NZtpALpv1x7Mq4g=
github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
@ -269,10 +272,14 @@ github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNx
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7 h1:lez6TS6aAau+8wXUP3G9I3TGlmPFEq2CTxBaRqY6AGE=
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
github.com/mdlayher/netlink v0.0.0-20190313131330-258ea9dff42c/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
github.com/mdlayher/netlink v1.0.0/go.mod h1:KxeJAFOFLG6AjpyDkQ/iIhxygIUKD+vcwqcnu43w/+M=
github.com/mdlayher/netlink v1.1.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
github.com/mdlayher/netlink v1.1.1 h1:VqG+Voq9V4uZ+04vjIrcSCWDpf91B1xxbP4QBUmUJE8=
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/netlink v1.1.2-0.20201013204415-ded538f7f4be h1:7JeFwhE5SIdgKRd0qnqjOYJxY8AML8x/j+/qvFZ8R+c=
github.com/mdlayher/netlink v1.1.2-0.20201013204415-ded538f7f4be/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
github.com/mdlayher/raw v0.0.0-20190606142536-fef19f00fc18/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
@ -411,6 +418,9 @@ github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
github.com/ti-mo/netfilter v0.2.0/go.mod h1:8GbBGsY/8fxtyIdfwy29JiluNcPK4K7wIT+x42ipqUU=
github.com/ti-mo/netfilter v0.4.0 h1:rTN1nBYULDmMfDeBHZpKuNKX/bWEXQUhe02a/10orzg=
github.com/ti-mo/netfilter v0.4.0/go.mod h1:V54q75mUx8CNA2JnFl+wv9iZ5+JP9nCcRlaFS5OZSRM=
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
github.com/u-root/u-root v7.0.0+incompatible h1:u+KSS04pSxJGI5E7WE4Bs9+Zd75QjFv+REkjy/aoAc8=
github.com/u-root/u-root v7.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY=
@ -506,6 +516,7 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381 h1:VXak5I6aEWmAXeQjA+QSZzlgN
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200904194848-62affa334b73/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201016165138-7b1cca2348c0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME=
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11 h1:lwlPPsmjDKK0J6eG6xDWd5XPehI0R024zxjDnw3esPA=
@ -540,6 +551,7 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190316082340-a2f829d7f35f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190322080309-f49334f85ddc/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190411185658-b44545bcd369/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@ -567,6 +579,7 @@ golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201015000850-e3ed0017c211/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd h1:5CtCZbICpIOFdgO940moixOPjc0178IU44m4EjOO5IY=
golang.org/x/sys v0.0.0-20201112073958-5cba982894dd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

View File

@ -108,6 +108,12 @@ func (s *Server) Close() {
s.stats = nil
s.queryLog = nil
s.dnsProxy = nil
err := s.ipset.Close()
if err != nil {
log.Error("closing ipset: %s", err)
}
s.Unlock()
}
@ -190,11 +196,14 @@ func (s *Server) Prepare(config *ServerConfig) error {
// Initialize IPSET configuration
// --
s.ipset.init(s.conf.IPSETList)
err := s.ipset.init(s.conf.IPSETList)
if err != nil {
return err
}
// Prepare DNS servers settings
// --
err := s.prepareUpstreamSettings()
err = s.prepareUpstreamSettings()
if err != nil {
return err
}

View File

@ -1,142 +0,0 @@
package dnsforward
import (
"net"
"strings"
"sync"
"github.com/AdguardTeam/AdGuardHome/internal/util"
"github.com/AdguardTeam/golibs/log"
"github.com/miekg/dns"
)
type ipsetCtx struct {
ipsetList map[string][]string // domain -> []ipset_name
ipsetCache map[[4]byte]bool // cache for IP[] to prevent duplicate calls to ipset program
ipsetMutex *sync.Mutex
ipset6Cache map[[16]byte]bool // cache for IP[] to prevent duplicate calls to ipset program
ipset6Mutex *sync.Mutex
}
// Convert configuration settings to an internal map
// DOMAIN[,DOMAIN].../IPSET1_NAME[,IPSET2_NAME]...
func (c *ipsetCtx) init(ipsetConfig []string) {
c.ipsetList = make(map[string][]string)
c.ipsetCache = make(map[[4]byte]bool)
c.ipsetMutex = &sync.Mutex{}
c.ipset6Cache = make(map[[16]byte]bool)
c.ipset6Mutex = &sync.Mutex{}
for _, it := range ipsetConfig {
it = strings.TrimSpace(it)
hostsAndNames := strings.Split(it, "/")
if len(hostsAndNames) != 2 {
log.Debug("IPSET: invalid value %q", it)
continue
}
ipsetNames := strings.Split(hostsAndNames[1], ",")
if len(ipsetNames) == 0 {
log.Debug("IPSET: invalid value %q", it)
continue
}
bad := false
for i := range ipsetNames {
ipsetNames[i] = strings.TrimSpace(ipsetNames[i])
if len(ipsetNames[i]) == 0 {
bad = true
break
}
}
if bad {
log.Debug("IPSET: invalid value %q", it)
continue
}
hosts := strings.Split(hostsAndNames[0], ",")
for _, host := range hosts {
host = strings.TrimSpace(host)
host = strings.ToLower(host)
if len(host) == 0 {
log.Debug("IPSET: invalid value %q", it)
continue
}
c.ipsetList[host] = ipsetNames
}
}
log.Debug("IPSET: added %d hosts", len(c.ipsetList))
}
func (c *ipsetCtx) getIP(rr dns.RR) net.IP {
switch a := rr.(type) {
case *dns.A:
var ip4 [4]byte
copy(ip4[:], a.A.To4())
c.ipsetMutex.Lock()
defer c.ipsetMutex.Unlock()
_, found := c.ipsetCache[ip4]
if found {
return nil // this IP was added before
}
c.ipsetCache[ip4] = false
return a.A
case *dns.AAAA:
var ip6 [16]byte
copy(ip6[:], a.AAAA)
c.ipset6Mutex.Lock()
defer c.ipset6Mutex.Unlock()
_, found := c.ipset6Cache[ip6]
if found {
return nil // this IP was added before
}
c.ipset6Cache[ip6] = false
return a.AAAA
default:
return nil
}
}
// Add IP addresses of the specified in configuration domain names to an ipset list
func (c *ipsetCtx) process(ctx *dnsContext) (rc resultCode) {
req := ctx.proxyCtx.Req
if !(req.Question[0].Qtype == dns.TypeA ||
req.Question[0].Qtype == dns.TypeAAAA) ||
!ctx.responseFromUpstream {
return resultCodeSuccess
}
host := req.Question[0].Name
host = strings.TrimSuffix(host, ".")
host = strings.ToLower(host)
ipsetNames, found := c.ipsetList[host]
if !found {
return resultCodeSuccess
}
log.Debug("IPSET: found ipsets %v for host %s", ipsetNames, host)
for _, it := range ctx.proxyCtx.Res.Answer {
ip := c.getIP(it)
if ip == nil {
continue
}
ipStr := ip.String()
for _, name := range ipsetNames {
code, out, err := util.RunCommand("ipset", "add", name, ipStr)
if err != nil {
log.Info("IPSET: %s(%s) -> %s: %s", host, ipStr, name, err)
continue
}
if code != 0 {
log.Info("IPSET: ipset add: code:%d output:%q", code, out)
continue
}
log.Debug("IPSET: added %s(%s) -> %s", host, ipStr, name)
}
}
return resultCodeSuccess
}

View File

@ -0,0 +1,369 @@
// +build linux
package dnsforward
import (
"fmt"
"net"
"strings"
"sync"
"github.com/AdguardTeam/AdGuardHome/internal/agherr"
"github.com/AdguardTeam/golibs/log"
"github.com/digineo/go-ipset/v2"
"github.com/mdlayher/netlink"
"github.com/miekg/dns"
"github.com/ti-mo/netfilter"
)
// TODO(a.garipov): Cover with unit tests as well as document how to test it
// manually. The original PR by @dsheets on Github contained an integration
// test, but unfortunately I didn't have the time to properly refactor it and
// check it in.
//
// See https://github.com/AdguardTeam/AdGuardHome/issues/2611.
// ipsetProps contains one Linux Netfilter ipset properties.
type ipsetProps struct {
name string
family netfilter.ProtoFamily
}
// ipsetCtx is the Linux Netfilter ipset context.
type ipsetCtx struct {
// mu protects all properties below.
mu *sync.Mutex
nameToIpset map[string]ipsetProps
domainToIpsets map[string][]ipsetProps
addedIPs map[[16]byte]struct{}
ipv4Conn *ipset.Conn
ipv6Conn *ipset.Conn
}
// dialNetfilter establishes connections to Linux's netfilter module.
func (c *ipsetCtx) dialNetfilter(config *netlink.Config) (err error) {
// The kernel API does not actually require two sockets but package
// github.com/digineo/go-ipset does.
//
// TODO(a.garipov): Perhaps we can ditch package ipset altogether and
// just use packages netfilter and netlink.
c.ipv4Conn, err = ipset.Dial(netfilter.ProtoIPv4, config)
if err != nil {
return fmt.Errorf("dialing v4: %w", err)
}
c.ipv6Conn, err = ipset.Dial(netfilter.ProtoIPv6, config)
if err != nil {
return fmt.Errorf("dialing v6: %w", err)
}
return nil
}
// ipsetProps returns the properties of an ipset with the given name.
func (c *ipsetCtx) ipsetProps(name string) (set ipsetProps, err error) {
// The family doesn't seem to matter when we use a header query, so
// query only the IPv4 one.
//
// TODO(a.garipov): Find out if this is a bug or a feature.
res, err := c.ipv4Conn.Header(name)
if err != nil {
return set, err
}
if res == nil || res.Family == nil {
return set, agherr.Error("empty response or no family data")
}
family := netfilter.ProtoFamily(res.Family.Value)
if family != netfilter.ProtoIPv4 && family != netfilter.ProtoIPv6 {
return set, fmt.Errorf("unexpected ipset family %s", family)
}
return ipsetProps{
name: name,
family: family,
}, nil
}
// ipsets returns currently known ipsets.
func (c *ipsetCtx) ipsets(names []string) (sets []ipsetProps, err error) {
for _, name := range names {
set, ok := c.nameToIpset[name]
if ok {
sets = append(sets, set)
continue
}
var err error
set, err = c.ipsetProps(name)
if err != nil {
return nil, fmt.Errorf("querying ipset %q: %w", name, err)
}
c.nameToIpset[name] = set
sets = append(sets, set)
}
return sets, nil
}
// parseIpsetConfig parses one ipset configuration string.
func parseIpsetConfig(cfgStr string) (hosts, ipsetNames []string, err error) {
cfgStr = strings.TrimSpace(cfgStr)
hostsAndNames := strings.Split(cfgStr, "/")
if len(hostsAndNames) != 2 {
return nil, nil, fmt.Errorf("invalid value %q: expected one slash", cfgStr)
}
hosts = strings.Split(hostsAndNames[0], ",")
ipsetNames = strings.Split(hostsAndNames[1], ",")
if len(ipsetNames) == 0 {
log.Info("ipset: resolutions for %q will not be stored", hosts)
return nil, nil, nil
}
for i := range ipsetNames {
ipsetNames[i] = strings.TrimSpace(ipsetNames[i])
if len(ipsetNames[i]) == 0 {
return nil, nil, fmt.Errorf("invalid value %q: empty ipset name", cfgStr)
}
}
for i := range hosts {
hosts[i] = strings.TrimSpace(hosts[i])
hosts[i] = strings.ToLower(hosts[i])
if len(hosts[i]) == 0 {
log.Info("ipset: root catchall in %q", ipsetNames)
}
}
return hosts, ipsetNames, nil
}
// init initializes the ipset context. It is not safe for concurrent use.
//
// TODO(a.garipov): Rewrite into a simple constructor?
func (c *ipsetCtx) init(ipsetConfig []string) (err error) {
c.mu = &sync.Mutex{}
c.nameToIpset = make(map[string]ipsetProps)
c.domainToIpsets = make(map[string][]ipsetProps)
c.addedIPs = make(map[[16]byte]struct{})
err = c.dialNetfilter(&netlink.Config{})
if err != nil {
return fmt.Errorf("ipset: dialing netfilter: %w", err)
}
for i, cfgStr := range ipsetConfig {
var hosts, ipsetNames []string
hosts, ipsetNames, err = parseIpsetConfig(cfgStr)
if err != nil {
return fmt.Errorf("ipset: config line at index %d: %w", i, err)
}
var ipsets []ipsetProps
ipsets, err = c.ipsets(ipsetNames)
if err != nil {
return fmt.Errorf("ipset: getting ipsets config line at index %d: %w", i, err)
}
for _, host := range hosts {
c.domainToIpsets[host] = append(c.domainToIpsets[host], ipsets...)
}
}
log.Debug("ipset: added %d domains for %d ipsets", len(c.domainToIpsets), len(c.nameToIpset))
return nil
}
// Close closes the Linux Netfilter connections.
func (c *ipsetCtx) Close() (err error) {
var errors []error
err = c.ipv4Conn.Close()
if err != nil {
errors = append(errors, err)
}
err = c.ipv6Conn.Close()
if err != nil {
errors = append(errors, err)
}
if len(errors) != 0 {
return agherr.Many("closing ipsets", errors...)
}
return nil
}
// ipFromRR returns an IP address from a DNS resource record.
func ipFromRR(rr dns.RR) (ip net.IP) {
switch a := rr.(type) {
case *dns.A:
return a.A
case *dns.AAAA:
return a.AAAA
default:
return nil
}
}
// lookupHost find the ipsets for the host, taking subdomain wildcards into
// account.
func (c *ipsetCtx) lookupHost(host string) (sets []ipsetProps) {
// Search for matching ipset hosts starting with most specific
// subdomain. We could use a trie here but the simple, inefficient
// solution isn't that expensive. ~75 % for 10 subdomains vs 0, but
// still sub-microsecond on a Core i7.
//
// TODO(a.garipov): Re-add benchmarks from the original PR.
for i := 0; i != -1; i++ {
host = host[i:]
sets = c.domainToIpsets[host]
if sets != nil {
return sets
}
i = strings.Index(host, ".")
}
// Check the root catch-all one.
return c.domainToIpsets[""]
}
// addIPs adds the IP addresses for the host to the ipset. set must be same
// family as set's family.
func (c *ipsetCtx) addIPs(host string, set ipsetProps, ips []net.IP) (err error) {
if len(ips) == 0 {
return
}
entries := make([]*ipset.Entry, 0, len(ips))
for _, ip := range ips {
entries = append(entries, ipset.NewEntry(ipset.EntryIP(ip)))
}
var conn *ipset.Conn
switch set.family {
case netfilter.ProtoIPv4:
conn = c.ipv4Conn
case netfilter.ProtoIPv6:
conn = c.ipv6Conn
default:
return fmt.Errorf("unexpected family %s for ipset %q", set.family, set.name)
}
err = conn.Add(set.name, entries...)
if err != nil {
return fmt.Errorf("adding %q%s to ipset %q: %w", host, ips, set.name, err)
}
log.Debug("ipset: added %s%s to ipset %s", host, ips, set.name)
return nil
}
// skipIpsetProcessing returns true when the ipset processing can be skipped for
// this request.
func (c *ipsetCtx) skipIpsetProcessing(ctx *dnsContext) (ok bool) {
if len(c.domainToIpsets) == 0 || ctx == nil || !ctx.responseFromUpstream {
return true
}
req := ctx.proxyCtx.Req
if req == nil || len(req.Question) == 0 {
return true
}
qt := req.Question[0].Qtype
return qt != dns.TypeA && qt != dns.TypeAAAA && qt != dns.TypeANY
}
// process adds the resolved IP addresses to the domain's ipsets, if any.
func (c *ipsetCtx) process(ctx *dnsContext) (rc resultCode) {
if c == nil {
return resultCodeSuccess
}
c.mu.Lock()
defer c.mu.Unlock()
if c.skipIpsetProcessing(ctx) {
log.Debug("ipset: skipped processing for request")
return resultCodeSuccess
}
req := ctx.proxyCtx.Req
host := req.Question[0].Name
host = strings.TrimSuffix(host, ".")
host = strings.ToLower(host)
sets := c.lookupHost(host)
if len(sets) == 0 {
return resultCodeSuccess
}
log.Debug("ipset: found ipsets %+v for host %s", sets, host)
if ctx.proxyCtx.Res == nil {
return resultCodeSuccess
}
ans := ctx.proxyCtx.Res.Answer
l := len(ans)
v4s := make([]net.IP, 0, l)
v6s := make([]net.IP, 0, l)
for _, rr := range ans {
ip := ipFromRR(rr)
if ip == nil {
continue
}
var iparr [16]byte
copy(iparr[:], ip.To16())
if _, added := c.addedIPs[iparr]; added {
continue
}
if ip.To4() == nil {
v6s = append(v6s, ip)
continue
}
v4s = append(v4s, ip)
}
var err error
setLoop:
for _, set := range sets {
switch set.family {
case netfilter.ProtoIPv4:
err = c.addIPs(host, set, v4s)
if err != nil {
break setLoop
}
case netfilter.ProtoIPv6:
err = c.addIPs(host, set, v6s)
if err != nil {
break setLoop
}
default:
err = fmt.Errorf("unexpected family %s for ipset %q", set.family, set.name)
break setLoop
}
}
if err != nil {
log.Error("ipset: adding host ips: %s", err)
}
return resultCodeSuccess
}

View File

@ -0,0 +1,26 @@
// +build !linux
package dnsforward
import (
"github.com/AdguardTeam/golibs/log"
)
type ipsetCtx struct{}
// init initializes the ipset context.
func (c *ipsetCtx) init(ipsetConfig []string) (err error) {
if len(ipsetConfig) != 0 {
log.Info("ipset: only available on linux")
}
return nil
}
// process adds the resolved IP addresses to the domain's ipsets, if any.
func (c *ipsetCtx) process(_ *dnsContext) (rc resultCode) {
return resultCodeSuccess
}
// Close closes the Linux Netfilter connections.
func (c *ipsetCtx) Close() (_ error) { return nil }

View File

@ -1,41 +0,0 @@
package dnsforward
import (
"testing"
"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
)
func TestIPSET(t *testing.T) {
s := Server{}
s.conf.IPSETList = append(s.conf.IPSETList, "HOST.com/name")
s.conf.IPSETList = append(s.conf.IPSETList, "host2.com,host3.com/name23")
s.conf.IPSETList = append(s.conf.IPSETList, "host4.com/name4,name41")
c := ipsetCtx{}
c.init(s.conf.IPSETList)
assert.Equal(t, "name", c.ipsetList["host.com"][0])
assert.Equal(t, "name23", c.ipsetList["host2.com"][0])
assert.Equal(t, "name23", c.ipsetList["host3.com"][0])
assert.Equal(t, "name4", c.ipsetList["host4.com"][0])
assert.Equal(t, "name41", c.ipsetList["host4.com"][1])
_, ok := c.ipsetList["host0.com"]
assert.False(t, ok)
ctx := &dnsContext{
srv: &s,
}
ctx.proxyCtx = &proxy.DNSContext{}
ctx.proxyCtx.Req = &dns.Msg{
Question: []dns.Question{
{
Name: "host.com.",
Qtype: dns.TypeA,
},
},
}
assert.Equal(t, resultCodeSuccess, c.process(ctx))
}