Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions client/src/__locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,8 @@
"edns_enable": "Enable EDNS client subnet",
"edns_use_custom_ip": "Use custom IP for EDNS",
"edns_use_custom_ip_desc": "Allow to use custom IP for EDNS",
"edns_use_client_addr": "Use client address from ECS",
"edns_use_client_addr_desc": "Identify clients by their EDNS Client Subnet instead of the connection address.",
"elapsed": "Elapsed",
"empty_response_status": "Empty",
"enable_protection": "Enable protection",
Expand Down
19 changes: 19 additions & 0 deletions client/src/components/Settings/Dns/Config/Form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type FormData = {
edns_cs_enabled: boolean;
edns_cs_use_custom: boolean;
edns_cs_custom_ip?: string;
edns_cs_use_client_addr?: boolean;
dnssec_enabled: boolean;
disable_ipv6: boolean;
blocking_mode: string;
Expand Down Expand Up @@ -277,6 +278,24 @@ const Form = ({ processing, initialValues, onSubmit }: Props) => {
)}
</div>

<div className="col-12">
<div className="form__group form__group--settings">
<Controller
name="edns_cs_use_client_addr"
control={control}
render={({ field }) => (
<Checkbox
{...field}
data-testid="dns_config_edns_use_client_addr"
title={t('edns_use_client_addr')}
subtitle={t('edns_use_client_addr_desc')}
disabled={processing || !edns_cs_enabled}
/>
)}
/>
</div>
</div>

{checkboxes.map(({ name, placeholder, subtitle }) => (
<div className="col-12" key={name}>
<div className="form__group form__group--settings">
Expand Down
2 changes: 2 additions & 0 deletions client/src/components/Settings/Dns/Config/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const Config = () => {
edns_cs_enabled,
edns_cs_use_custom,
edns_cs_custom_ip,
edns_cs_use_client_addr,
dnssec_enabled,
disable_ipv6,
processingSetConfig,
Expand Down Expand Up @@ -50,6 +51,7 @@ const Config = () => {
dnssec_enabled,
edns_cs_use_custom,
edns_cs_custom_ip,
edns_cs_use_client_addr,
}}
onSubmit={handleFormSubmit}
processing={processingSetConfig}
Expand Down
2 changes: 2 additions & 0 deletions client/src/initialState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ export type DnsConfigData = {
ratelimit_subnet_len_ipv6?: number;
edns_cs_use_custom?: boolean;
edns_cs_custom_ip?: string;
edns_cs_use_client_addr?: boolean;
cache_enabled?: boolean;
cache_size?: number;
cache_ttl_max?: number;
Expand Down Expand Up @@ -502,6 +503,7 @@ export const initialState: RootState = {
blocked_response_ttl: 10,
upstream_timeout: 10,
edns_cs_enabled: false,
edns_cs_use_client_addr: false,
disable_ipv6: false,
dnssec_enabled: false,
upstream_dns_file: '',
Expand Down
1 change: 1 addition & 0 deletions client/src/reducers/dnsConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ const dnsConfig = handleActions(
blocked_response_ttl: 10,
upstream_timeout: 10,
edns_cs_enabled: false,
edns_cs_use_client_addr: false,
disable_ipv6: false,
dnssec_enabled: false,
upstream_dns_file: '',
Expand Down
4 changes: 4 additions & 0 deletions internal/dnsforward/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,10 @@ type EDNSClientSubnet struct {

// UseCustom defines if CustomIP should be used.
UseCustom bool `yaml:"use_custom"`

// UseClientAddrFromECS defines if the EDNS Client Subnet is used for
// client identification instead of the connection address.
UseClientAddrFromECS bool `yaml:"use_client_addr_from_ecs"`
}

// TLSConfig contains the TLS configuration settings for DNSCrypt,
Expand Down
22 changes: 22 additions & 0 deletions internal/dnsforward/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package dnsforward
import (
"context"
"fmt"
"net/netip"
)

// ctxKey is the type for context keys.
Expand All @@ -11,6 +12,7 @@ type ctxKey int
// Context key values.
const (
ctxKeyClientID ctxKey = iota
ctxKeyECSClientAddr
)

// contextWithClientID returns a new context with the given ID.
Expand All @@ -32,3 +34,23 @@ func clientIDFromContext(ctx context.Context) (id string, ok bool) {

return id, true
}

// contextWithECSClientAddr returns a new context with the ECS client address.
func contextWithECSClientAddr(parent context.Context, addr netip.Addr) (ctx context.Context) {
return context.WithValue(parent, ctxKeyECSClientAddr, addr)
}

// ecsClientAddrFromContext returns the ECS client address for this request.
func ecsClientAddrFromContext(ctx context.Context) (addr netip.Addr, ok bool) {
v := ctx.Value(ctxKeyECSClientAddr)
if v == nil {
return addr, false
}

addr, ok = v.(netip.Addr)
if !ok {
panic(fmt.Errorf("bad type for ctxKeyECSClientAddr: %T(%[1]v)", v))
}

return addr, true
}
141 changes: 141 additions & 0 deletions internal/dnsforward/ecs_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package dnsforward

import (
"net"
"net/netip"
"testing"

"github.com/AdguardTeam/dnsproxy/proxy"
"github.com/miekg/dns"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestECSClientAddr is a test for the ecsClientAddr function.
func TestECSClientAddr(t *testing.T) {
t.Parallel()

testCases := []struct {
name string
msg func(t *testing.T) *dns.Msg
wantOK bool
wantIP netip.Addr
}{{
name: "no_opt",
msg: func(t *testing.T) (m *dns.Msg) {
return new(dns.Msg)
},
}, {
name: "opt_without_ecs",
msg: func(t *testing.T) (m *dns.Msg) {
o := new(dns.OPT)
o.Hdr.Name = "."
o.Hdr.Rrtype = dns.TypeOPT
m = new(dns.Msg)
m.Extra = append(m.Extra, o)
return m
},
}, {
name: "ecs_v4",
msg: func(t *testing.T) (m *dns.Msg) {
return newTestMsgWithECS(t, 1, 24, "1.2.3.4")
},
wantOK: true,
wantIP: netip.MustParseAddr("1.2.3.4"),
}, {
name: "ecs_v6",
msg: func(t *testing.T) (m *dns.Msg) {
return newTestMsgWithECS(t, 2, 56, "2001:db8::1")
},
wantOK: true,
wantIP: netip.MustParseAddr("2001:db8::1"),
}, {
name: "ecs_unknown_family",
msg: func(t *testing.T) (m *dns.Msg) {
return newTestMsgWithECS(t, 3, 24, "1.2.3.4")
},
}, {
name: "ecs_unspecified",
msg: func(t *testing.T) (m *dns.Msg) {
return newTestMsgWithECS(t, 1, 24, "0.0.0.0")
},
}, {
name: "ecs_invalid_v4",
msg: func(t *testing.T) (m *dns.Msg) {
// A v6 address advertised as an IPv4 ECS option should be skipped.
return newTestMsgWithECS(t, 1, 24, "2001:db8::1")
},
}}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

addr, ok := ecsClientAddr(tc.msg(t))
assert.Equal(t, tc.wantOK, ok)
if tc.wantOK {
assert.Equal(t, tc.wantIP, addr)
} else {
assert.False(t, addr.IsValid())
}
})
}
}

// newTestMsgWithECS returns a new DNS message with an EDNS Client Subnet
// option using the given family, netmask and address.
func newTestMsgWithECS(t *testing.T, family uint16, netmask uint8, addr string) (m *dns.Msg) {
t.Helper()

ip := net.ParseIP(addr)
require.NotNil(t, ip)

o := &dns.OPT{
Hdr: dns.RR_Header{
Name: ".",
Rrtype: dns.TypeOPT,
},
}
o.SetUDPSize(4096)

e := &dns.EDNS0_SUBNET{
Code: dns.EDNS0SUBNET,
Family: family,
SourceNetmask: netmask,
SourceScope: 0,
Address: ip,
}
o.Option = append(o.Option, e)

m = new(dns.Msg)
m.Extra = append(m.Extra, o)

return m
}

// TestDNSContext_clientAddr is a test for the clientAddr method.
func TestDNSContext_clientAddr(t *testing.T) {
t.Parallel()

const (
connAddr = "192.168.1.1"
ecsAddr = "192.168.1.100"
)

dctx := &dnsContext{
proxyCtx: &proxy.DNSContext{
Addr: netip.AddrPortFrom(netip.MustParseAddr(connAddr), 53),
},
}

// Without ECS, the connection address is used.
assert.Equal(t, netip.MustParseAddr(connAddr), dctx.clientAddr())

// With ECS set, it takes precedence.
dctx.ecsClientAddr = netip.MustParseAddr(ecsAddr)
assert.Equal(t, netip.MustParseAddr(ecsAddr), dctx.clientAddr())

// Clearing it falls back again.
dctx.ecsClientAddr = netip.Addr{}
assert.Equal(t, netip.MustParseAddr(connAddr), dctx.clientAddr())
}
2 changes: 1 addition & 1 deletion internal/dnsforward/filter.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import (
func (s *Server) clientRequestFilteringSettings(dctx *dnsContext) (setts *filtering.Settings) {
setts = s.dnsFilter.Settings()
setts.ProtectionEnabled = dctx.protectionEnabled
s.dnsFilter.ApplyAdditionalFiltering(dctx.proxyCtx.Addr.Addr(), dctx.clientID, setts)
s.dnsFilter.ApplyAdditionalFiltering(dctx.clientAddr(), dctx.clientID, setts)

return setts
}
Expand Down
6 changes: 6 additions & 0 deletions internal/dnsforward/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ type jsonDNSConfig struct {
// EDNSCSUseCustom defines if EDNSCSCustomIP should be used.
EDNSCSUseCustom *bool `json:"edns_cs_use_custom"`

// EDNSCSUseClientAddr defines if the client address comes from ECS.
EDNSCSUseClientAddr *bool `json:"edns_cs_use_client_addr"`

// DNSSECEnabled defines if DNSSEC is enabled.
DNSSECEnabled *bool `json:"dnssec_enabled"`

Expand Down Expand Up @@ -163,6 +166,7 @@ func (s *Server) getDNSConfig(ctx context.Context) (c *jsonDNSConfig) {
customIP := s.conf.EDNSClientSubnet.CustomIP
enableEDNSClientSubnet := s.conf.EDNSClientSubnet.Enabled
useCustom := s.conf.EDNSClientSubnet.UseCustom
useClientAddrFromECS := s.conf.EDNSClientSubnet.UseClientAddrFromECS

enableDNSSEC := s.conf.EnableDNSSEC
aaaaDisabled := s.conf.AAAADisabled
Expand Down Expand Up @@ -209,6 +213,7 @@ func (s *Server) getDNSConfig(ctx context.Context) (c *jsonDNSConfig) {
EDNSCSCustomIP: customIP,
EDNSCSEnabled: &enableEDNSClientSubnet,
EDNSCSUseCustom: &useCustom,
EDNSCSUseClientAddr: &useClientAddrFromECS,
DNSSECEnabled: &enableDNSSEC,
DisableIPv6: &aaaaDisabled,
BlockedResponseTTL: &blockedResponseTTL,
Expand Down Expand Up @@ -659,6 +664,7 @@ func (s *Server) setConfigRestartable(dc *jsonDNSConfig) (shouldRestart bool) {
setIfNotNil(&s.conf.FallbackDNS, dc.Fallbacks),
setIfNotNil(&s.conf.EDNSClientSubnet.Enabled, dc.EDNSCSEnabled),
setIfNotNil(&s.conf.EDNSClientSubnet.UseCustom, dc.EDNSCSUseCustom),
setIfNotNil(&s.conf.EDNSClientSubnet.UseClientAddrFromECS, dc.EDNSCSUseClientAddr),
setIfNotNil(&s.conf.CacheEnabled, dc.CacheEnabled),
setIfNotNil(&s.conf.CacheSize, dc.CacheSize),
setIfNotNil(&s.conf.CacheMinTTL, dc.CacheMinTTL),
Expand Down
Loading