Skip to content

Commit

Permalink
+ DNS: add "ipset" configuration setting
Browse files Browse the repository at this point in the history
Close #1191

Squashed commit of the following:

commit ba14b53
Merge: 362f4c4 6b61429
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 14:03:19 2020 +0300

    Merge remote-tracking branch 'origin/master' into 1191-ipset

commit 362f4c4
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 12:50:56 2020 +0300

    minor

commit 28e1245
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Wed Sep 2 12:43:25 2020 +0300

    minor

commit bdbd732
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Sep 1 18:40:04 2020 +0300

    move code, ipset-v6

commit 77f4d94
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Tue Sep 1 15:53:27 2020 +0300

    comment

commit 1640132
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Aug 31 17:43:23 2020 +0300

    minor

commit c8410e9
Author: Simon Zolin <s.zolin@adguard.com>
Date:   Mon Aug 31 15:30:52 2020 +0300

    + DNS: add "ipset" configuration setting
  • Loading branch information
szolin committed Sep 2, 2020
1 parent 6b61429 commit 7931e50
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 8 deletions.
23 changes: 23 additions & 0 deletions AGHTechDoc.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Contents:
* API: Log out
* API: Get current user info
* Safe services
* ipset


## Relations between subsystems
Expand Down Expand Up @@ -1882,3 +1883,25 @@ Check if host name is blocked by SB/PC service:
sha256(host.com)[0..1] -> hashes[0],hashes[1],...
sha256(sub.host.com)[0..1] -> hashes[2],...
...


## ipset

AGH can add IP addresses of the specified in configuration domain names to an ipset list.

Prepare: user creates an ipset list and configures AGH for using it.

1. User --( ipset create my_ipset hash:ip ) -> OS
2. User --( ipset: host.com,host2.com/my_ipset )-> AGH

Syntax:

ipset: "DOMAIN[,DOMAIN].../IPSET1_NAME[,IPSET2_NAME]..."

IPv4 addresses are added to an ipset list with `ipv4` family, IPv6 addresses - to `ipv6` ipset list.

Run-time: AGH adds IP addresses of a domain name to a corresponding ipset list.

1. AGH --( resolve host.com )-> upstream
2. AGH <-( host.com:[1.1.1.1,2.2.2.2] )-- upstream
3. AGH --( ipset.add(my_ipset, [1.1.1.1,2.2.2.2] ))-> OS
5 changes: 5 additions & 0 deletions dnsforward/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,11 @@ type FilteringConfig struct {
EnableDNSSEC bool `yaml:"enable_dnssec"` // Set DNSSEC flag in outcoming DNS request
EnableEDNSClientSubnet bool `yaml:"edns_client_subnet"` // Enable EDNS Client Subnet option
MaxGoroutines uint32 `yaml:"max_goroutines"` // Max. number of parallel goroutines for processing incoming requests

// IPSET configuration - add IP addresses of the specified domain names to an ipset list
// Syntax:
// "DOMAIN[,DOMAIN].../IPSET_NAME"
IPSETList []string `yaml:"ipset"`
}

// TLSConfig is the TLS configuration for HTTPS, DNS-over-HTTPS, and DNS-over-TLS
Expand Down
22 changes: 14 additions & 8 deletions dnsforward/dnsforward.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ type Server struct {
stats stats.Stats
access *accessCtx

ipset ipsetCtx

tableHostToIP map[string]net.IP // "hostname -> IP" table for internal addresses (DHCP)
tableHostToIPLock sync.Mutex

Expand Down Expand Up @@ -168,7 +170,7 @@ func (s *Server) startInternal() error {

// Prepare the object
func (s *Server) Prepare(config *ServerConfig) error {
// 1. Initialize the server configuration
// Initialize the server configuration
// --
if config != nil {
s.conf = *config
Expand All @@ -184,45 +186,49 @@ func (s *Server) Prepare(config *ServerConfig) error {
}
}

// 2. Set default values in the case if nothing is configured
// Set default values in the case if nothing is configured
// --
s.initDefaultSettings()

// 3. Prepare DNS servers settings
// Initialize IPSET configuration
// --
s.ipset.init(s.conf.IPSETList)

// Prepare DNS servers settings
// --
err := s.prepareUpstreamSettings()
if err != nil {
return err
}

// 3. Create DNS proxy configuration
// Create DNS proxy configuration
// --
var proxyConfig proxy.Config
proxyConfig, err = s.createProxyConfig()
if err != nil {
return err
}

// 4. Prepare a DNS proxy instance that we use for internal DNS queries
// Prepare a DNS proxy instance that we use for internal DNS queries
// --
s.prepareIntlProxy()

// 5. Initialize DNS access module
// Initialize DNS access module
// --
s.access = &accessCtx{}
err = s.access.Init(s.conf.AllowedClients, s.conf.DisallowedClients, s.conf.BlockedHosts)
if err != nil {
return err
}

// 6. Register web handlers if necessary
// Register web handlers if necessary
// --
if !webRegistered && s.conf.HTTPRegister != nil {
webRegistered = true
s.registerHandlers()
}

// 7. Create the main DNS proxy instance
// Create the main DNS proxy instance
// --
s.dnsProxy = &proxy.Proxy{Config: proxyConfig}
return nil
Expand Down
1 change: 1 addition & 0 deletions dnsforward/handle_dns.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error {
processUpstream,
processDNSSECAfterResponse,
processFilteringAfterResponse,
s.ipset.process,
processQueryLogsAndStats,
}
for _, process := range mods {
Expand Down
133 changes: 133 additions & 0 deletions dnsforward/ipset.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package dnsforward

import (
"net"
"strings"

"github.com/AdguardTeam/AdGuardHome/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
ipset6Cache map[[16]byte]bool // cache for IP[] to prevent duplicate calls to ipset program
}

// 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.ipset6Cache = make(map[[16]byte]bool)

for _, it := range ipsetConfig {
it = strings.TrimSpace(it)
hostsAndNames := strings.Split(it, "/")
if len(hostsAndNames) != 2 {
log.Debug("IPSET: invalid value '%s'", it)
continue
}

ipsetNames := strings.Split(hostsAndNames[1], ",")
if len(ipsetNames) == 0 {
log.Debug("IPSET: invalid value '%s'", 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 '%s'", 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 '%s'", 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())
_, 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)
_, 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) int {
req := ctx.proxyCtx.Req
if !(req.Question[0].Qtype == dns.TypeA ||
req.Question[0].Qtype == dns.TypeAAAA) ||
!ctx.responseFromUpstream {
return resultDone
}

host := req.Question[0].Name
host = strings.TrimSuffix(host, ".")
host = strings.ToLower(host)
ipsetNames, found := c.ipsetList[host]
if !found {
return resultDone
}

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:'%s'", code, out)
continue
}
log.Debug("IPSET: added %s(%s) -> %s", host, ipStr, name)
}
}

return resultDone
}
41 changes: 41 additions & 0 deletions dnsforward/ipset_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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, resultDone, c.process(ctx))
}

0 comments on commit 7931e50

Please sign in to comment.