Skip to content

Commit

Permalink
proxy: turn ecs into ipnet
Browse files Browse the repository at this point in the history
  • Loading branch information
EugeneOne1 committed Feb 16, 2022
1 parent 99ec30d commit d2b98d4
Show file tree
Hide file tree
Showing 13 changed files with 228 additions and 128 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,5 @@ require (
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)

replace github.com/AdguardTeam/golibs v0.10.5 => ../../golibs/head
4 changes: 0 additions & 4 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,6 @@ dmitri.shuralyov.com/service/change v0.0.0-20181023043359-a85b471d5412/go.mod h1
dmitri.shuralyov.com/state v0.0.0-20180228185332-28bcc343414c/go.mod h1:0PRwlb0D6DFvNNtx+9ybjezNCa8XF0xaYcETyp6rHWU=
git.apache.org/thrift.git v0.0.0-20180902110319-2566ecd5d999/go.mod h1:fPE2ZNJGynbRyZ4dJvy6G277gSllfV2HJqblrnkyeyg=
github.com/AdguardTeam/golibs v0.4.2/go.mod h1:skKsDKIBB7kkFflLJBpfGX+G8QFTx0WKUzB6TIgtUj4=
github.com/AdguardTeam/golibs v0.10.3 h1:FBgk17zf35ESVWQKIqEUiqqB2bDaCBC8X5vMU760yB4=
github.com/AdguardTeam/golibs v0.10.3/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/AdguardTeam/golibs v0.10.5 h1:4/nl1yIBJOv5luVu9SURW8LfgOjI3zQ2moIUy/1k0y4=
github.com/AdguardTeam/golibs v0.10.5/go.mod h1:rSfQRGHIdgfxriDDNgNJ7HmE5zRoURq8R+VdR81Zuzw=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
Expand Down
40 changes: 20 additions & 20 deletions proxy/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,17 +179,15 @@ func (c *cache) get(req *dns.Msg) (ci *cacheItem, expired bool, key []byte) {
return ci, expired, key
}

// getWithSubnet returns cached item for the req if it's found by client's IP
// and a subnet mask. expired is true if the item's TTL is expired. key is the
// resulting key for req. It's returned to avoid recalculating it afterwards.
// getWithSubnet returns cached item for the req if it's found by n. expired is
// true if the item's TTL is expired. k is the resulting key for req. It's
// returned to avoid recalculating it afterwards.
//
// Note that a slow longest-prefix-match algorithm is used, so cache searches
// are performed up to mask+1 times.
func (c *cache) getWithSubnet(req *dns.Msg, cliIP net.IP, mask uint8) (
ci *cacheItem,
expired bool,
key []byte,
) {
func (c *cache) getWithSubnet(req *dns.Msg, n *net.IPNet) (ci *cacheItem, expired bool, k []byte) {
mask, _ := n.Mask.Size()

c.itemsWithSubnetLock.RLock()
defer c.itemsWithSubnetLock.RUnlock()

Expand All @@ -200,18 +198,18 @@ func (c *cache) getWithSubnet(req *dns.Msg, cliIP net.IP, mask uint8) (
var data []byte
for mask++; mask > 0 && data == nil; {
mask--
key = msgToKeyWithSubnet(req, cliIP, mask)
data = c.itemsWithSubnet.Get(key)
k = msgToKeyWithSubnet(req, n.IP, mask)
data = c.itemsWithSubnet.Get(k)
}
if data == nil {
return nil, false, key
return nil, false, k
}

if ci, expired = c.unpackItem(data, req); ci == nil {
c.items.Del(key)
c.itemsWithSubnet.Del(k)
}

return ci, expired, key
return ci, expired, k
}

// initLazy initializes the cache for general requests.
Expand Down Expand Up @@ -267,14 +265,15 @@ func (c *cache) set(ci *cacheItem) {

// setWithSubnet tries to add the ci into cache with subnet and ip used to
// calculate the key.
func (c *cache) setWithSubnet(ci *cacheItem, ip net.IP, mask uint8) {
func (c *cache) setWithSubnet(ci *cacheItem, subnet *net.IPNet) {
if !isCacheable(ci.m) {
return
}

c.initLazyWithSubnet()

key := msgToKeyWithSubnet(ci.m, ip, mask)
pref, _ := subnet.Mask.Size()
key := msgToKeyWithSubnet(ci.m, subnet.IP, pref)
packed := ci.pack()

c.itemsWithSubnetLock.RLock()
Expand Down Expand Up @@ -417,11 +416,12 @@ func msgToKey(m *dns.Msg) (b []byte) {
}

// msgToKeyWithSubnet constructs the cache key from DO bit, type, class, subnet
// mask, client's IP address and question's name of m.
func msgToKeyWithSubnet(m *dns.Msg, clientIP net.IP, mask uint8) (key []byte) {
// mask, client's IP address and question's name of m. ecsIP is expected to be
// masked already.
func msgToKeyWithSubnet(m *dns.Msg, ecsIP net.IP, mask int) (key []byte) {
q := m.Question[0]
cap := 1 + 2*packedMsgLenSz + 1 + len(q.Name)
ipLen := len(clientIP)
ipLen := len(ecsIP)
masked := mask != 0
if masked {
cap += ipLen
Expand All @@ -448,10 +448,10 @@ func msgToKeyWithSubnet(m *dns.Msg, clientIP net.IP, mask uint8) (key []byte) {
k += packedMsgLenSz

// Add mask.
key[k] = mask
key[k] = uint8(mask)
k++
if masked {
k += copy(key[k:], clientIP)
k += copy(key[k:], ecsIP)
}
copy(key[k:], strings.ToLower(q.Name))

Expand Down
34 changes: 25 additions & 9 deletions proxy/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/AdguardTeam/dnsproxy/upstream"
glcache "github.com/AdguardTeam/golibs/cache"
"github.com/AdguardTeam/golibs/netutil"
"github.com/AdguardTeam/golibs/testutil"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -634,7 +635,10 @@ func TestSubnet(t *testing.T) {
req := (&dns.Msg{}).SetQuestion("example.com.", dns.TypeA)

t.Run("empty", func(t *testing.T) {
ci, expired, key := c.getWithSubnet(req, ip1234, 24)
ci, expired, key := c.getWithSubnet(req, &net.IPNet{
IP: ip1234,
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
})
assert.False(t, expired)
assert.Nil(t, key)
assert.Nil(t, ci)
Expand All @@ -652,10 +656,13 @@ func TestSubnet(t *testing.T) {
Answer: []dns.RR{newRR(t, "example.com. 1 IN A 1.1.1.1")},
}).SetQuestion("example.com.", dns.TypeA)
item.m = resp
c.setWithSubnet(item, ip1234, 16)
c.setWithSubnet(item, &net.IPNet{IP: ip1234, Mask: net.CIDRMask(16, netutil.IPv4BitLen)})

t.Run("different_ip", func(t *testing.T) {
ci, expired, key := c.getWithSubnet(req, ip2234, 24)
ci, expired, key := c.getWithSubnet(req, &net.IPNet{
IP: ip2234,
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
})
assert.False(t, expired)
assert.Equal(t, msgToKeyWithSubnet(req, ip2234, 0), key)

Expand All @@ -670,7 +677,7 @@ func TestSubnet(t *testing.T) {
Answer: []dns.RR{newRR(t, "example.com. 1 IN A 2.2.2.2")},
}).SetQuestion("example.com.", dns.TypeA)
item.m = resp
c.setWithSubnet(item, ip2234, 16)
c.setWithSubnet(item, &net.IPNet{IP: ip2234, Mask: net.CIDRMask(16, netutil.IPv4BitLen)})

// Add a response entry without subnet.
resp = (&dns.Msg{
Expand All @@ -680,10 +687,13 @@ func TestSubnet(t *testing.T) {
Answer: []dns.RR{newRR(t, "example.com. 1 IN A 3.3.3.3")},
}).SetQuestion("example.com.", dns.TypeA)
item.m = resp
c.setWithSubnet(item, net.IP{}, 0)
c.setWithSubnet(item, &net.IPNet{IP: nil, Mask: nil})

t.Run("with_subnet_1", func(t *testing.T) {
ci, expired, key := c.getWithSubnet(req, ip1234, 24)
ci, expired, key := c.getWithSubnet(req, &net.IPNet{
IP: ip1234,
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
})
assert.False(t, expired)
assert.Equal(t, msgToKeyWithSubnet(req, ip1234, 16), key)

Expand All @@ -698,7 +708,10 @@ func TestSubnet(t *testing.T) {
})

t.Run("with_subnet_2", func(t *testing.T) {
ci, expired, key := c.getWithSubnet(req, ip2234, 24)
ci, expired, key := c.getWithSubnet(req, &net.IPNet{
IP: ip2234,
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
})
assert.False(t, expired)
assert.Equal(t, msgToKeyWithSubnet(req, ip2234, 16), key)

Expand All @@ -713,9 +726,12 @@ func TestSubnet(t *testing.T) {
})

t.Run("with_subnet_3", func(t *testing.T) {
ci, expired, key := c.getWithSubnet(req, ip3234, 24)
ci, expired, key := c.getWithSubnet(req, &net.IPNet{
IP: ip3234,
Mask: net.CIDRMask(24, netutil.IPv4BitLen),
})
assert.False(t, expired)
assert.Equal(t, msgToKeyWithSubnet(req, ip3234, 0), key)
assert.Equal(t, msgToKeyWithSubnet(req, ip1234, 0), key)

require.NotNil(t, ci)
require.NotNil(t, ci.m)
Expand Down
33 changes: 17 additions & 16 deletions proxy/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,25 +85,26 @@ type Config struct {
// Similar to dnsmasq's "bogus-nxdomain"
BogusNXDomain []*net.IPNet

// Enable EDNS Client Subnet option
// DNS requests to the upstream server will contain an OPT record with Client Subnet option.
// If the original request already has this option set, we pass it through as is.
// Otherwise, we set it ourselves using the client IP with subnet /24 (for IPv4) and /112 (for IPv6).
// Enable EDNS Client Subnet option DNS requests to the upstream server will
// contain an OPT record with Client Subnet option. If the original request
// already has this option set, we pass it through as is. Otherwise, we set
// it ourselves using the client IP with subnet /24 (for IPv4) and /56 (for
// IPv6).
//
// If the upstream server supports ECS, it sets subnet number in the response.
// This subnet number along with the client IP and other data is used as a cache key.
// Next time, if a client from the same subnet requests this host name,
// we get the response from cache.
// If another client from a different subnet requests this host name,
// we pass his request to the upstream server.
// If the upstream server supports ECS, it sets subnet number in the
// response. This subnet number along with the client IP and other data is
// used as a cache key. Next time, if a client from the same subnet
// requests this host name, we get the response from cache. If another
// client from a different subnet requests this host name, we pass his
// request to the upstream server.
//
// If the upstream server doesn't support ECS (there's no subnet number in response),
// this response will be cached for all clients.
// If the upstream server doesn't support ECS (there's no subnet number in
// response), this response will be cached for all clients.
//
// If client IP is private (i.e. not public), we don't add EDNS record into a request.
// And so there will be no EDNS record in response either.
// We store these responses in general cache (without subnet)
// so they will never be used for clients with public IP addresses.
// If client IP is private (i.e. not public), we don't add EDNS record into
// a request. And so there will be no EDNS record in response either. We
// store these responses in general cache (without subnet) so they will
// never be used for clients with public IP addresses.
EnableEDNSClientSubnet bool
EDNSAddr net.IP // ECS IP used in request

Expand Down
8 changes: 4 additions & 4 deletions proxy/dns64.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ func (p *Proxy) isEmptyAAAAResponse(resp, req *dns.Msg) bool {
req.Question[0].Qtype == dns.TypeAAAA
}

// isNAT64PrefixAvailable returns true if NAT64 prefix was calculated
// isNAT64PrefixAvailable returns true if NAT64 prefix was calculated.
func (p *Proxy) isNAT64PrefixAvailable() bool {
p.nat64PrefixLock.Lock()
prefixSize := len(p.nat64Prefix)
p.nat64PrefixLock.Unlock()
return prefixSize == NAT64PrefixLength
defer p.nat64PrefixLock.Unlock()

return len(p.nat64Prefix) == NAT64PrefixLength
}

// SetNAT64Prefix sets NAT64 prefix
Expand Down
7 changes: 2 additions & 5 deletions proxy/dns_context.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,11 +62,8 @@ type DNSContext struct {
// instance.
RequestID uint64

// ECSReqIP is the EDNS Client-Subnet IP address used in the request.
ECSReqIP net.IP
// ECSReqMask is the length of the EDNS Client-Subnet mask used in the
// request.
ECSReqMask uint8
// ReqECS is the EDNS Client Subnet used in the request.
ReqECS *net.IPNet

// adBit is the authenticated data flag from the request.
adBit bool
Expand Down
36 changes: 24 additions & 12 deletions proxy/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,13 +67,15 @@ func genSOA(request *dns.Msg, retry uint32) []dns.RR {
return []dns.RR{&soa}
}

// parseECS parses the EDNS client subnet option from m.
func parseECS(m *dns.Msg) (ip net.IP, mask, scope uint8) {
// ecsFromMsg returns the subnet from EDNS Client-Subnet option of m if any.
func ecsFromMsg(m *dns.Msg) (subnet *net.IPNet, scope int) {
opt := m.IsEdns0()
if opt == nil {
return nil, 0, 0
return nil, 0
}

var ip net.IP
var mask net.IPMask
for _, e := range opt.Option {
sn, ok := e.(*dns.EDNS0_SUBNET)
if !ok {
Expand All @@ -82,24 +84,29 @@ func parseECS(m *dns.Msg) (ip net.IP, mask, scope uint8) {

switch sn.Family {
case 1:
return sn.Address.To4(), sn.SourceNetmask, sn.SourceScope
ip = sn.Address.To4()
mask = net.CIDRMask(int(sn.SourceNetmask), netutil.IPv4BitLen)
case 2:
return sn.Address, sn.SourceNetmask, sn.SourceScope
ip = sn.Address
mask = net.CIDRMask(int(sn.SourceNetmask), netutil.IPv6BitLen)
default:
// Go on.
continue
}

return &net.IPNet{IP: ip, Mask: mask}, int(sn.SourceScope)
}

return nil, 0, 0
return nil, 0
}

// setECS sets the EDNS client subnet option based on ip and scope into m. It
// returns masked IP and mask length.
func setECS(m *dns.Msg, ip net.IP, scope uint8) (net.IP, uint8) {
func setECS(m *dns.Msg, ip net.IP, scope uint8) (subnet *net.IPNet) {
const (
// defaultECSv4 is the default length of network mask for IPv4 address
// in EDNS client subnet option.
defaultECSv4 = 24

// defaultECSv6 is the default length of network mask for IPv6 address
// in EDNS client subnet option. The size of 7 octets is chosen as a
// reasonable minimum since at least Google's public DNS refuses
Expand All @@ -111,23 +118,28 @@ func setECS(m *dns.Msg, ip net.IP, scope uint8) (net.IP, uint8) {
Code: dns.EDNS0SUBNET,
SourceScope: scope,
}

subnet = &net.IPNet{}
if ip4 := ip.To4(); ip4 != nil {
e.Family = 1
e.SourceNetmask = defaultECSv4
e.Address = ip4.Mask(net.CIDRMask(defaultECSv4, netutil.IPv4BitLen))
subnet.Mask = net.CIDRMask(defaultECSv4, netutil.IPv4BitLen)
ip = ip4
} else {
// Assume the IP address has already been validated.
e.Family = 2
e.SourceNetmask = defaultECSv6
e.Address = ip.Mask(net.CIDRMask(defaultECSv6, netutil.IPv6BitLen))
subnet.Mask = net.CIDRMask(defaultECSv6, netutil.IPv6BitLen)
}
subnet.IP = ip.Mask(subnet.Mask)
e.Address = subnet.IP

// If OPT record already exists so just add EDNS option inside it. Note
// that servers may return FORMERR if they meet several OPT RRs.
if opt := m.IsEdns0(); opt != nil {
opt.Option = append(opt.Option, e)

return e.Address, e.SourceNetmask
return subnet
}

// Create an OPT record and add EDNS option inside it.
Expand All @@ -141,7 +153,7 @@ func setECS(m *dns.Msg, ip net.IP, scope uint8) (net.IP, uint8) {
o.SetUDPSize(4096)
m.Extra = append(m.Extra, o)

return e.Address, e.SourceNetmask
return subnet
}

// isPublicIP returns true if ip is within public Internet IP range.
Expand Down
Loading

0 comments on commit d2b98d4

Please sign in to comment.