diff --git a/client/src/__locales/en.json b/client/src/__locales/en.json index afc2f105eb8..207d427b582 100644 --- a/client/src/__locales/en.json +++ b/client/src/__locales/en.json @@ -586,5 +586,13 @@ "port_53_faq_link": "Port 53 is often occupied by \"DNSStubListener\" or \"systemd-resolved\" services. Please read <0>this instruction on how to resolve this.", "adg_will_drop_dns_queries": "AdGuard Home will be dropping all DNS queries from this client.", "client_not_in_allowed_clients": "The client is not allowed because it is not in the \"Allowed clients\" list.", - "experimental": "Experimental" + "experimental": "Experimental", + "rebinding_title": "DNS Rebinding Protection", + "rebinding_desc": "Here you can configure protection against DNS rebinding attacks", + "rebinding_protection_enabled": "Enable protection from DNS rebinding attacks", + "rebinding_protection_enabled_desc": "If enabled, AdGuard Home will block responses containing host on the local network.", + "rebinding_allowed_hosts_title": "Allowed domains", + "rebinding_allowed_hosts_desc": "A list of domains. If configured, AdGuard Home will allow responses containing host on the local network from these domains. Here you can specify the exact domain names, wildcards and urlfilter-rules, e.g. 'example.org', '*.example.org' or '||example.org^'.", + "rebinding_applied": "DNS rebinding protection applied", + "blocked_rebind": "Blocked rebinding" } diff --git a/client/src/actions/dnsConfig.js b/client/src/actions/dnsConfig.js index c599ca8def0..f7809de6d5b 100644 --- a/client/src/actions/dnsConfig.js +++ b/client/src/actions/dnsConfig.js @@ -37,6 +37,9 @@ export const setDnsConfig = (config) => async (dispatch) => { data.upstream_dns = splitByNewLine(config.upstream_dns); hasDnsSettings = true; } + if (Object.prototype.hasOwnProperty.call(data, 'rebinding_allowed_hosts')) { + data.rebinding_allowed_hosts = splitByNewLine(config.rebinding_allowed_hosts); + } await apiClient.setDnsConfig(data); diff --git a/client/src/components/Filters/Check/Info.js b/client/src/components/Filters/Check/Info.js index f53f254d351..bc36ef50d32 100644 --- a/client/src/components/Filters/Check/Info.js +++ b/client/src/components/Filters/Check/Info.js @@ -65,6 +65,7 @@ const getTitle = () => { [FILTERED_STATUS.FILTERED_SAFE_SEARCH]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_SAFE_BROWSING]: getReasonFiltered(reason), [FILTERED_STATUS.FILTERED_PARENTAL]: getReasonFiltered(reason), + [FILTERED_STATUS.FILTERED_BLOCKED_REBIND]: t('rebinding_applied'), }; if (Object.prototype.hasOwnProperty.call(REASON_TO_TITLE_MAP, reason)) { diff --git a/client/src/components/Logs/Cells/ClientCell.js b/client/src/components/Logs/Cells/ClientCell.js index 56a3440cb1d..5ec8e1b802f 100644 --- a/client/src/components/Logs/Cells/ClientCell.js +++ b/client/src/components/Logs/Cells/ClientCell.js @@ -4,7 +4,7 @@ import { nanoid } from 'nanoid'; import classNames from 'classnames'; import { useTranslation } from 'react-i18next'; import propTypes from 'prop-types'; -import { checkFiltered, getBlockingClientName } from '../../../helpers/helpers'; +import { checkFiltered, checkBlockedRebind, getBlockingClientName } from '../../../helpers/helpers'; import { BLOCK_ACTIONS } from '../../../helpers/constants'; import { toggleBlocking, toggleBlockingForClient } from '../../../actions'; import IconTooltip from './IconTooltip'; @@ -48,6 +48,7 @@ const ClientCell = ({ const processedData = Object.entries(data); const isFiltered = checkFiltered(reason); + const isBlockedRebinding = checkBlockedRebind(reason); const nameClass = classNames('w-90 o-hidden d-flex flex-column', { 'mt-2': isDetailed && !name && !whoisAvailable, @@ -125,7 +126,7 @@ const ClientCell = ({ 'button-action__container--detailed': isDetailed, }); - return
+ return isBlockedRebinding ||
+
+
+ + ); +}; + +Form.propTypes = { + handleSubmit: PropTypes.func.isRequired, + submitting: PropTypes.bool.isRequired, + invalid: PropTypes.bool.isRequired, +}; + +export default reduxForm({ form: FORM_NAME.REBINDING })(Form); diff --git a/client/src/components/Settings/Dns/Rebinding/index.js b/client/src/components/Settings/Dns/Rebinding/index.js new file mode 100644 index 00000000000..c1e349fcb98 --- /dev/null +++ b/client/src/components/Settings/Dns/Rebinding/index.js @@ -0,0 +1,36 @@ +import React from 'react'; +import { useTranslation } from 'react-i18next'; +import { shallowEqual, useDispatch, useSelector } from 'react-redux'; +import Form from './Form'; +import Card from '../../../ui/Card'; +import { setDnsConfig } from '../../../../actions/dnsConfig'; + +const RebindingConfig = () => { + const { t } = useTranslation(); + const dispatch = useDispatch(); + const { + rebinding_protection_enabled, rebinding_allowed_hosts, + } = useSelector((state) => state.dnsConfig, shallowEqual); + + const handleFormSubmit = (values) => { + dispatch(setDnsConfig(values)); + }; + + return ( + +
+ + ); +}; + +export default RebindingConfig; diff --git a/client/src/components/Settings/Dns/index.js b/client/src/components/Settings/Dns/index.js index 06c68f21bf8..226585d3823 100644 --- a/client/src/components/Settings/Dns/index.js +++ b/client/src/components/Settings/Dns/index.js @@ -8,6 +8,7 @@ import Config from './Config'; import PageTitle from '../../ui/PageTitle'; import Loading from '../../ui/Loading'; import CacheConfig from './Cache'; +import RebindingConfig from './Rebinding'; import { getDnsConfig } from '../../../actions/dnsConfig'; import { getAccessList } from '../../../actions/access'; @@ -33,6 +34,7 @@ const Dns = () => { + } ; }; diff --git a/client/src/helpers/constants.js b/client/src/helpers/constants.js index 76be76a6a0f..030647480c9 100644 --- a/client/src/helpers/constants.js +++ b/client/src/helpers/constants.js @@ -336,6 +336,7 @@ export const FILTERED_STATUS = { FILTERED_BLACK_LIST: 'FilteredBlackList', NOT_FILTERED_WHITE_LIST: 'NotFilteredWhiteList', NOT_FILTERED_NOT_FOUND: 'NotFilteredNotFound', + FILTERED_BLOCKED_REBIND: 'FilteredRebind', FILTERED_BLOCKED_SERVICE: 'FilteredBlockedService', REWRITE: 'Rewrite', REWRITE_HOSTS: 'RewriteEtcHosts', @@ -362,6 +363,10 @@ export const RESPONSE_FILTER = { QUERY: 'blocked', LABEL: 'show_blocked_responses', }, + BLOCKED_REBIND: { + QUERY: 'blocked_rebind', + LABEL: 'blocked_rebind', + }, BLOCKED_SERVICES: { QUERY: 'blocked_services', LABEL: 'blocked_services', @@ -443,6 +448,10 @@ export const FILTERED_STATUS_TO_META_MAP = { LABEL: RESPONSE_FILTER.BLOCKED_ADULT_WEBSITES.LABEL, COLOR: QUERY_STATUS_COLORS.YELLOW, }, + [FILTERED_STATUS.FILTERED_BLOCKED_REBIND]: { + LABEL: RESPONSE_FILTER.BLOCKED_REBIND.LABEL, + COLOR: QUERY_STATUS_COLORS.RED, + }, }; export const DEFAULT_TIME_FORMAT = 'HH:mm:ss'; @@ -514,6 +523,7 @@ export const FORM_NAME = { INSTALL: 'install', LOGIN: 'login', CACHE: 'cache', + REBINDING: 'rebinding', ...DHCP_FORM_NAMES, }; diff --git a/client/src/helpers/helpers.js b/client/src/helpers/helpers.js index 82f3024550b..6eb057261e7 100644 --- a/client/src/helpers/helpers.js +++ b/client/src/helpers/helpers.js @@ -450,6 +450,7 @@ export const checkNotFilteredNotFound = (reason) => reason === FILTERED_STATUS.N export const checkSafeSearch = (reason) => reason === FILTERED_STATUS.FILTERED_SAFE_SEARCH; export const checkSafeBrowsing = (reason) => reason === FILTERED_STATUS.FILTERED_SAFE_BROWSING; export const checkParental = (reason) => reason === FILTERED_STATUS.FILTERED_PARENTAL; +export const checkBlockedRebind = (reason) => reason === FILTERED_STATUS.FILTERED_BLOCKED_REBIND; export const checkBlockedService = (reason) => reason === FILTERED_STATUS.FILTERED_BLOCKED_SERVICE; export const getCurrentFilter = (url, filters) => { diff --git a/client/src/reducers/dnsConfig.js b/client/src/reducers/dnsConfig.js index bbe4ad2f5dc..b05d99916d5 100644 --- a/client/src/reducers/dnsConfig.js +++ b/client/src/reducers/dnsConfig.js @@ -16,6 +16,7 @@ const dnsConfig = handleActions( blocking_ipv6, upstream_dns, bootstrap_dns, + rebinding_allowed_hosts, ...values } = payload; @@ -26,6 +27,7 @@ const dnsConfig = handleActions( blocking_ipv6: blocking_ipv6 || DEFAULT_BLOCKING_IPV6, upstream_dns: (upstream_dns && upstream_dns.join('\n')) || '', bootstrap_dns: (bootstrap_dns && bootstrap_dns.join('\n')) || '', + rebinding_allowed_hosts: (rebinding_allowed_hosts && rebinding_allowed_hosts.join('\n')) || '', processingGetConfig: false, }; }, diff --git a/internal/dnsfilter/dnsfilter.go b/internal/dnsfilter/dnsfilter.go index c5c28aff722..16e4603a2a6 100644 --- a/internal/dnsfilter/dnsfilter.go +++ b/internal/dnsfilter/dnsfilter.go @@ -147,6 +147,8 @@ const ( FilteredSafeSearch // FilteredBlockedService - the host is blocked by "blocked services" settings FilteredBlockedService + // FilteredRebind - the request was blocked due to DNS rebinding protection + FilteredRebind // Rewritten is returned when there was a rewrite by a legacy DNS // rewrite rule. @@ -178,6 +180,7 @@ var reasonNames = []string{ FilteredInvalid: "FilteredInvalid", FilteredSafeSearch: "FilteredSafeSearch", FilteredBlockedService: "FilteredBlockedService", + FilteredRebind: "FilteredRebind", Rewritten: "Rewrite", RewrittenAutoHosts: "RewriteEtcHosts", diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go index 881174d1e20..e25209b652c 100644 --- a/internal/dnsforward/config.go +++ b/internal/dnsforward/config.go @@ -76,6 +76,11 @@ type FilteringConfig struct { CacheMinTTL uint32 `yaml:"cache_ttl_min"` // override TTL value (minimum) received from upstream server CacheMaxTTL uint32 `yaml:"cache_ttl_max"` // override TTL value (maximum) received from upstream server + // DNS rebinding protection settings + // -- + RebindingProtectionEnabled bool `yaml:"rebinding_protection_enabled"` + RebindingAllowedHosts []string `yaml:"rebinding_allowed_hosts"` + // Other settings // -- diff --git a/internal/dnsforward/dns.go b/internal/dnsforward/dns.go index 4a47cdc15c0..53dfc021bc0 100644 --- a/internal/dnsforward/dns.go +++ b/internal/dnsforward/dns.go @@ -54,6 +54,7 @@ func (s *Server) handleDNSRequest(_ *proxy.Proxy, d *proxy.DNSContext) error { processFilteringBeforeRequest, processUpstream, processDNSSECAfterResponse, + processRebindingFilteringAfterResponse, processFilteringAfterResponse, s.ipset.process, processQueryLogsAndStats, @@ -390,6 +391,9 @@ func processFilteringAfterResponse(ctx *dnsContext) int { d.Res.Answer = answer } + case dnsfilter.FilteredRebind: + // nothing + case dnsfilter.NotFilteredAllowList: // nothing diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go index f1b7e7d2528..13d997f103f 100644 --- a/internal/dnsforward/dnsforward.go +++ b/internal/dnsforward/dnsforward.go @@ -53,6 +53,7 @@ type Server struct { queryLog querylog.QueryLog // Query log instance stats stats.Stats access *accessCtx + rebinding *dnsRebindChecker ipset ipsetCtx @@ -122,6 +123,7 @@ func (s *Server) WriteDiskConfig(c *FilteringConfig) { c.DisallowedClients = stringArrayDup(sc.DisallowedClients) c.BlockedHosts = stringArrayDup(sc.BlockedHosts) c.UpstreamDNS = stringArrayDup(sc.UpstreamDNS) + c.RebindingAllowedHosts = stringArrayDup(sc.RebindingAllowedHosts) s.RUnlock() } @@ -221,6 +223,13 @@ func (s *Server) Prepare(config *ServerConfig) error { return err } + // Initialize DNS rebinding module + // -- + s.rebinding, err = newRebindChecker(s.conf.RebindingAllowedHosts) + if err != nil { + return err + } + // Register web handlers if necessary // -- if !webRegistered && s.conf.HTTPRegister != nil { diff --git a/internal/dnsforward/dnsforward_test.go b/internal/dnsforward/dnsforward_test.go index fdee8648770..7d20375441c 100644 --- a/internal/dnsforward/dnsforward_test.go +++ b/internal/dnsforward/dnsforward_test.go @@ -738,6 +738,100 @@ func TestRewrite(t *testing.T) { _ = s.Stop() } +func TestBlockedDNSRebinding(t *testing.T) { + s := createTestServer(t) + + err := s.Start() + if err != nil { + t.Fatalf("Failed to start server: %s", err) + } + addr := s.dnsProxy.Addr(proxy.ProtoUDP) + + // + // DNS rebinding protection + // + req := dns.Msg{} + req.Id = dns.Id() + req.RecursionDesired = true + req.Question = []dns.Question{ + {Name: "192-168-1-250.nip.io.", Qtype: dns.TypeA, Qclass: dns.ClassINET}, + } + + s.Lock() + s.conf.RebindingProtectionEnabled = true + s.Unlock() + + reply, err := dns.Exchange(&req, addr.String()) + if err != nil { + t.Fatalf("Couldn't talk to server %s: %s", addr, err) + } + + if len(reply.Answer) != 1 { + t.Fatalf("DNS server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer)) + } + + a, ok := reply.Answer[0].(*dns.A) + if !ok { + t.Fatalf("DNS server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0]) + } + + if !net.IPv4zero.Equal(a.A) { + t.Fatalf("DNS server %s returned wrong answer instead of 0.0.0.0: %v", addr, a.A) + } + + s.Lock() + s.conf.RebindingProtectionEnabled = false + s.Unlock() + + reply, err = dns.Exchange(&req, addr.String()) + if err != nil { + t.Fatalf("Couldn't talk to server %s: %s", addr, err) + } + + if len(reply.Answer) != 1 { + t.Fatalf("DNS server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer)) + } + + a, ok = reply.Answer[0].(*dns.A) + if !ok { + t.Fatalf("DNS server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0]) + } + + if !net.IPv4(192, 168, 1, 250).Equal(a.A) { + t.Fatalf("DNS server %s returned wrong answer instead of 192.168.1.250: %v", addr, a.A) + } + + s.Lock() + s.conf.RebindingProtectionEnabled = true + s.rebinding, _ = newRebindChecker([]string{ + "||nip.io^", + }) + s.Unlock() + + reply, err = dns.Exchange(&req, addr.String()) + if err != nil { + t.Fatalf("Couldn't talk to server %s: %s", addr, err) + } + + if len(reply.Answer) != 1 { + t.Fatalf("DNS server %s returned reply with wrong number of answers - %d", addr, len(reply.Answer)) + } + + a, ok = reply.Answer[0].(*dns.A) + if !ok { + t.Fatalf("DNS server %s returned wrong answer type instead of A: %v", addr, reply.Answer[0]) + } + + if !net.IPv4(192, 168, 1, 250).Equal(a.A) { + t.Fatalf("DNS server %s returned wrong answer instead of 192.168.1.250: %v", addr, a.A) + } + + err = s.Stop() + if err != nil { + t.Fatalf("DNS server failed to stop: %s", err) + } +} + func createTestServer(t *testing.T) *Server { rules := `||nxdomain.example.org ||null.example.org^ diff --git a/internal/dnsforward/filter.go b/internal/dnsforward/filter.go index 80cf26dd551..636427113b4 100644 --- a/internal/dnsforward/filter.go +++ b/internal/dnsforward/filter.go @@ -123,8 +123,8 @@ func (s *Server) filterDNSResponse(ctx *dnsContext) (*dnsfilter.Result, error) { switch v := a.(type) { case *dns.CNAME: - log.Debug("DNSFwd: Checking CNAME %s for %s", v.Target, v.Hdr.Name) host = strings.TrimSuffix(v.Target, ".") + log.Debug("DNSFwd: Checking CNAME %s for %s", v.Target, v.Hdr.Name) case *dns.A: host = v.A.String() diff --git a/internal/dnsforward/http.go b/internal/dnsforward/http.go index e24ba89eb20..1fb1420b826 100644 --- a/internal/dnsforward/http.go +++ b/internal/dnsforward/http.go @@ -37,6 +37,9 @@ type dnsConfig struct { CacheSize *uint32 `json:"cache_size"` CacheMinTTL *uint32 `json:"cache_ttl_min"` CacheMaxTTL *uint32 `json:"cache_ttl_max"` + + RebindingProtectionEnabled *bool `json:"rebinding_protection_enabled"` + RebindingAllowedHosts *[]string `json:"rebinding_allowed_hosts"` } func (s *Server) getDNSConfig() dnsConfig { @@ -61,23 +64,27 @@ func (s *Server) getDNSConfig() dnsConfig { } else if s.conf.AllServers { upstreamMode = "parallel" } + rebindingEnabled := s.conf.RebindingProtectionEnabled + rebindingAllowedHosts := stringArrayDup(s.conf.RebindingAllowedHosts) s.RUnlock() return dnsConfig{ - Upstreams: &upstreams, - UpstreamsFile: &upstreamFile, - Bootstraps: &bootstraps, - ProtectionEnabled: &protectionEnabled, - BlockingMode: &blockingMode, - BlockingIPv4: &BlockingIPv4, - BlockingIPv6: &BlockingIPv6, - RateLimit: &Ratelimit, - EDNSCSEnabled: &EnableEDNSClientSubnet, - DNSSECEnabled: &EnableDNSSEC, - DisableIPv6: &AAAADisabled, - CacheSize: &CacheSize, - CacheMinTTL: &CacheMinTTL, - CacheMaxTTL: &CacheMaxTTL, - UpstreamMode: &upstreamMode, + Upstreams: &upstreams, + UpstreamsFile: &upstreamFile, + Bootstraps: &bootstraps, + ProtectionEnabled: &protectionEnabled, + BlockingMode: &blockingMode, + BlockingIPv4: &BlockingIPv4, + BlockingIPv6: &BlockingIPv6, + RateLimit: &Ratelimit, + EDNSCSEnabled: &EnableEDNSClientSubnet, + DNSSECEnabled: &EnableDNSSEC, + DisableIPv6: &AAAADisabled, + CacheSize: &CacheSize, + CacheMinTTL: &CacheMinTTL, + CacheMaxTTL: &CacheMaxTTL, + UpstreamMode: &upstreamMode, + RebindingProtectionEnabled: &rebindingEnabled, + RebindingAllowedHosts: &rebindingAllowedHosts, } } @@ -302,6 +309,8 @@ func (s *Server) setConfig(dc dnsConfig) (restart bool) { s.conf.FastestAddr = false } } + + restart = restart || s.setRebindingConfig(dc) s.Unlock() s.conf.ConfigModified() return restart diff --git a/internal/dnsforward/http_test.go b/internal/dnsforward/http_test.go index c8e2f9c5305..80de236c41c 100644 --- a/internal/dnsforward/http_test.go +++ b/internal/dnsforward/http_test.go @@ -29,7 +29,7 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) { conf: func() ServerConfig { return defaultConf }, - want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "fastest_addr", conf: func() ServerConfig { @@ -37,7 +37,7 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) { conf.FastestAddr = true return conf }, - want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "parallel", conf: func() ServerConfig { @@ -45,7 +45,7 @@ func TestDNSForwardHTTTP_handleGetConfig(t *testing.T) { conf.AllServers = true return conf }, - want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + want: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }} for _, tc := range testCases { @@ -73,7 +73,7 @@ func TestDNSForwardHTTTP_handleSetConfig(t *testing.T) { w := httptest.NewRecorder() - const defaultConfJSON = "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n" + const defaultConfJSON = "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n" testCases := []struct { name string req string @@ -83,52 +83,52 @@ func TestDNSForwardHTTTP_handleSetConfig(t *testing.T) { name: "upstream_dns", req: "{\"upstream_dns\":[\"8.8.8.8:77\",\"8.8.4.4:77\"]}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:77\",\"8.8.4.4:77\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:77\",\"8.8.4.4:77\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "bootstraps", req: "{\"bootstrap_dns\":[\"9.9.9.10\"]}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "blocking_mode_good", req: "{\"blocking_mode\":\"refused\"}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"refused\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"refused\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "blocking_mode_bad", req: "{\"blocking_mode\":\"custom_ip\"}", wantSet: "blocking_mode: incorrect value\n", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "ratelimit", req: "{\"ratelimit\":6}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":6,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":6,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "edns_cs_enabled", req: "{\"edns_cs_enabled\":true}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":true,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":true,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "dnssec_enabled", req: "{\"dnssec_enabled\":true}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":true,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":true,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "cache_size", req: "{\"cache_size\":1024}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":1024,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"\",\"cache_size\":1024,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "upstream_mode_parallel", req: "{\"upstream_mode\":\"parallel\"}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"parallel\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "upstream_mode_fastest_addr", req: "{\"upstream_mode\":\"fastest_addr\"}", wantSet: "", - wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0}\n", + wantGet: "{\"upstream_dns\":[\"8.8.8.8:53\",\"8.8.4.4:53\"],\"upstream_dns_file\":\"\",\"bootstrap_dns\":[\"9.9.9.10\",\"149.112.112.10\",\"2620:fe::10\",\"2620:fe::fe:10\"],\"protection_enabled\":true,\"ratelimit\":0,\"blocking_mode\":\"\",\"blocking_ipv4\":\"\",\"blocking_ipv6\":\"\",\"edns_cs_enabled\":false,\"dnssec_enabled\":false,\"disable_ipv6\":false,\"upstream_mode\":\"fastest_addr\",\"cache_size\":0,\"cache_ttl_min\":0,\"cache_ttl_max\":0,\"rebinding_protection_enabled\":false,\"rebinding_allowed_hosts\":[]}\n", }, { name: "upstream_dns_bad", req: "{\"upstream_dns\":[\"\"]}", diff --git a/internal/dnsforward/rebind.go b/internal/dnsforward/rebind.go new file mode 100644 index 00000000000..7aa029c5e69 --- /dev/null +++ b/internal/dnsforward/rebind.go @@ -0,0 +1,230 @@ +// DNS Rebinding protection + +package dnsforward + +import ( + "fmt" + "net" + "strings" + + "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" + "github.com/AdguardTeam/golibs/log" + "github.com/AdguardTeam/urlfilter" + "github.com/AdguardTeam/urlfilter/filterlist" + "github.com/miekg/dns" +) + +type dnsRebindChecker struct { + allowDomainEngine *urlfilter.DNSEngine +} + +func newRebindChecker(allowedHosts []string) (*dnsRebindChecker, error) { + buf := strings.Builder{} + for _, s := range allowedHosts { + buf.WriteString(s) + buf.WriteString("\n") + } + + rulesStorage, err := filterlist.NewRuleStorage([]filterlist.RuleList{ + &filterlist.StringRuleList{ + ID: int(0), + RulesText: buf.String(), + IgnoreCosmetic: true, + }, + }) + if err != nil { + return nil, err + } + + return &dnsRebindChecker{ + allowDomainEngine: urlfilter.NewDNSEngine(rulesStorage), + }, nil +} + +func (c *dnsRebindChecker) isAllowedDomain(domain string) bool { + _, ok := c.allowDomainEngine.Match(domain) + return ok +} + +// IsPrivate reports whether ip is a private address, according to +// RFC 1918 (IPv4 addresses) and RFC 4193 (IPv6 addresses). +func (*dnsRebindChecker) isPrivate(ip net.IP) bool { + //TODO: remove once https://github.com/golang/go/pull/42793 makes it to stdlib + if ip4 := ip.To4(); ip4 != nil { + return ip4[0] == 10 || + (ip4[0] == 172 && ip4[1]&0xf0 == 16) || + (ip4[0] == 192 && ip4[1] == 168) + } + return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc +} + +func (c *dnsRebindChecker) isRebindHost(host string) bool { + if ip := net.ParseIP(host); ip != nil { + return c.isRebindIP(ip) + } + + return host == "localhost" +} + +func (c *dnsRebindChecker) isLocalNetworkV4(ip4 net.IP) bool { + // Taken care by ip.isPrivate: + /* 10.0.0.0/8 (private) */ + /* 172.16.0.0/12 (private) */ + /* 192.168.0.0/16 (private) */ + + switch { + case ip4[0] == 0: + /* 0.0.0.0/8 (RFC 5735 section 3. "here" network) */ + case ip4[0] == 169 && ip4[1] == 254: + /* 169.254.0.0/16 (zeroconf) */ + case ip4[0] == 192 && ip4[1] == 0 && ip4[2] == 2: + /* 192.0.2.0/24 (test-net) */ + case ip4[0] == 198 && ip4[1] == 51 && ip4[2] == 100: + /* 198.51.100.0/24(test-net) */ + case ip4[0] == 203 && ip4[1] == 0 && ip4[2] == 113: + /* 203.0.113.0/24 (test-net) */ + case ip4.Equal(net.IPv4bcast): + /* 255.255.255.255/32 (broadcast)*/ + default: + return false + } + + return true +} + +func (c *dnsRebindChecker) isLocalNetworkV6(ip6 net.IP) bool { + return ip6.Equal(net.IPv6zero) || + ip6.Equal(net.IPv6unspecified) || + ip6.Equal(net.IPv6interfacelocalallnodes) || + ip6.Equal(net.IPv6linklocalallnodes) || + ip6.Equal(net.IPv6linklocalallrouters) +} + +func (c *dnsRebindChecker) isRebindIP(ip net.IP) bool { + // This is compatible with dnsmasq definition + // See: https://github.com/imp/dnsmasq/blob/4e7694d7107d2299f4aaededf8917fceb5dfb924/src/rfc1035.c#L412 + + rebind := false + if ip4 := ip.To4(); ip4 != nil { + rebind = c.isLocalNetworkV4(ip4) + } else { + rebind = c.isLocalNetworkV6(ip) + } + + return rebind || c.isPrivate(ip) || ip.IsLoopback() +} + +func (c *dnsRebindChecker) filter(domain, host string) *dnsfilter.Result { + if log.GetLevel() >= log.DEBUG { + timer := log.StartTimer() + defer timer.LogElapsed("DNS Rebinding check for %s -> %s", domain, host) + } + + if c.isAllowedDomain(domain) || !c.isRebindHost(host) { + return nil + } + + return &dnsfilter.Result{ + IsFiltered: true, + Reason: dnsfilter.FilteredRebind, + } +} + +// Checks DNS rebinding attacks +// Note both whitelisted and cached hosts will bypass rebinding check (see: processFilteringAfterResponse()). +func (s *Server) filterDNSRebinding(domain, host string) *dnsfilter.Result { + if !s.conf.RebindingProtectionEnabled { + return nil + } + + return s.rebinding.filter(domain, host) +} + +func processRebindingFilteringAfterResponse(ctx *dnsContext) int { + s := ctx.srv + d := ctx.proxyCtx + res := ctx.result + var err error + + // TODO: Should we also block cached responses? + if !ctx.responseFromUpstream || res.Reason == dnsfilter.Rewritten { + return resultDone + } + + originalRes := d.Res + ctx.result, err = s.preventRebindResponse(ctx) + if err != nil { + ctx.err = err + return resultError + } + if ctx.result != nil { + ctx.origResp = originalRes // matched by response + } else { + ctx.result = &dnsfilter.Result{} + } + + return resultDone +} + +func (s *Server) setRebindingConfig(dc dnsConfig) bool { + restart := false + + if dc.RebindingProtectionEnabled != nil { + s.conf.RebindingProtectionEnabled = *dc.RebindingProtectionEnabled + } + + if dc.RebindingAllowedHosts != nil { + s.conf.RebindingAllowedHosts = *dc.RebindingAllowedHosts + restart = true + } + + return restart +} + +func (s *Server) preventRebindResponse(ctx *dnsContext) (*dnsfilter.Result, error) { + d := ctx.proxyCtx + + for _, a := range d.Res.Answer { + m := "" + domainName := "" + host := "" + + switch v := a.(type) { + case *dns.CNAME: + host = strings.TrimSuffix(v.Target, ".") + domainName = v.Hdr.Name + m = fmt.Sprintf("DNSRebind: Checking CNAME %s for %s", v.Target, v.Hdr.Name) + + case *dns.A: + host = v.A.String() + domainName = v.Hdr.Name + m = fmt.Sprintf("DNSRebind: Checking record A (%s) for %s", host, v.Hdr.Name) + + case *dns.AAAA: + host = v.AAAA.String() + domainName = v.Hdr.Name + m = fmt.Sprintf("DNSRebind: Checking record AAAA (%s) for %s", host, v.Hdr.Name) + + default: + continue + } + + s.RLock() + if !s.conf.RebindingProtectionEnabled { + s.RUnlock() + continue + } + + log.Debug(m) + res := s.filterDNSRebinding(strings.TrimSuffix(domainName, "."), host) + s.RUnlock() + + if res != nil { + log.Debug("DNSRebind: Matched %s by response: %s", d.Req.Question[0].Name, host) + d.Res = s.genDNSFilterMessage(d, res) + return res, nil + } + } + + return nil, nil +} diff --git a/internal/dnsforward/rebind_test.go b/internal/dnsforward/rebind_test.go new file mode 100644 index 00000000000..86b5512e757 --- /dev/null +++ b/internal/dnsforward/rebind_test.go @@ -0,0 +1,119 @@ +package dnsforward + +import ( + "math/rand" + "net" + "testing" + + "github.com/AdguardTeam/AdGuardHome/internal/dnsfilter" + "github.com/stretchr/testify/assert" +) + +func TestRebindingPrivateAddresses(t *testing.T) { + c, _ := newRebindChecker(nil) + + r1 := byte(rand.Int31() & 0xFF) + r2 := byte(rand.Int31() & 0xFF) + r3 := byte(rand.Int31() & 0xFF) + + for _, ip := range []net.IP{ + net.IPv4(0, r1, r2, r3), /* 0.0.0.0/8 (RFC 5735 section 3. "here" network) */ + net.IPv4(127, r1, r2, r3), /* 127.0.0.0/8 (loopback) */ + net.IPv4(10, r1, r2, r3), /* 10.0.0.0/8 (private) */ + net.IPv4(172, 16|(0x0F&r1), r2, r3), /* 172.16.0.0/12 (private) */ + net.IPv4(192, 168, r2, r3), /* 192.168.0.0/16 (private) */ + net.IPv4(169, 254, r2, r3), /* 169.254.0.0/16 (zeroconf) */ + net.IPv4(192, 0, 2, r3), /* 192.0.2.0/24 (test-net) */ + net.IPv4(198, 51, 100, r3), /* 198.51.100.0/24(test-net) */ + net.IPv4(203, 0, 113, r3), /* 203.0.113.0/24 (test-net) */ + net.IPv4(255, 255, 255, 255), /* 255.255.255.255/32 (broadcast)*/ + + /* RFC 6303 4.3 (unspecified & loopback) */ + net.IPv6zero, + net.IPv6unspecified, + + /* RFC 6303 4.4 */ + /* RFC 6303 4.5 */ + /* RFC 6303 4.6 */ + net.IPv6interfacelocalallnodes, + net.IPv6linklocalallnodes, + net.IPv6linklocalallrouters, + + /* (TODO) Check IPv4-mapped IPv6 addresses */ + } { + assert.Truef(t, c.isRebindIP(ip), "%s is not a rebind", ip) + } +} + +func TestRebindLocalhost(t *testing.T) { + c := &dnsRebindChecker{} + assert.False(t, c.isRebindHost("example.com")) + assert.False(t, c.isRebindHost("200.0.0.1")) + assert.True(t, c.isRebindHost("127.0.0.1")) + assert.True(t, c.isRebindHost("localhost")) +} + +func TestIsResponseRebind(t *testing.T) { + c, _ := newRebindChecker([]string{ + "||totally-safe.com^", + }) + s := &Server{ + rebinding: c, + } + + expectedFiltered := &dnsfilter.Result{ + IsFiltered: true, + Reason: dnsfilter.FilteredRebind, + } + + for _, host := range []string{ + "0.1.2.3", /* 0.0.0.0/8 (RFC 5735 section 3. "here" network) */ + "127.1.2.3", /* 127.0.0.0/8 (loopback) */ + "10.1.2.3", /* 10.0.0.0/8 (private) */ + "172.16.2.3", /* 172.16.0.0/12 (private) */ + "192.168.2.3", /* 192.168.0.0/16 (private) */ + "169.254.2.3", /* 169.254.0.0/16 (zeroconf) */ + "192.0.2.3", /* 192.0.2.0/24 (test-net) */ + "198.51.100.3", /* 198.51.100.0/24(test-net) */ + "203.0.113.3", /* 203.0.113.0/24 (test-net) */ + "255.255.255.255", /* 255.255.255.255/32 (broadcast)*/ + + /* RFC 6303 4.3 (unspecified & loopback) */ + net.IPv6zero.String(), + net.IPv6unspecified.String(), + + /* RFC 6303 4.4 */ + /* RFC 6303 4.5 */ + /* RFC 6303 4.6 */ + net.IPv6interfacelocalallnodes.String(), + net.IPv6linklocalallnodes.String(), + net.IPv6linklocalallrouters.String(), + + "localhost", + } { + s.conf.RebindingProtectionEnabled = true + assert.Equalf(t, expectedFiltered, s.filterDNSRebinding("example.com", host), "host: %s", host) + assert.Nilf(t, s.filterDNSRebinding("totally-safe.com", host), "host: %s", host) + assert.Nilf(t, s.filterDNSRebinding("absolutely.totally-safe.com", host), "host: %s", host) + + s.conf.RebindingProtectionEnabled = false + assert.Nilf(t, s.filterDNSRebinding("example.com", host), "host: %s", host) + assert.Nilf(t, s.filterDNSRebinding("totally-safe.com", host), "host: %s", host) + assert.Nilf(t, s.filterDNSRebinding("absolutely.totally-safe.com", host), "host: %s", host) + } + + for _, host := range []string{ + "200.168.2.3", + "another-example.com", + } { + s.conf.RebindingProtectionEnabled = true + assert.Nilf(t, s.filterDNSRebinding("example.com", host), "host: %s", host) + assert.Nilf(t, s.filterDNSRebinding("totally-safe.com", host), "host: %s", host) + assert.Nilf(t, s.filterDNSRebinding("absolutely.totally-legit.com", host), "host: %s", host) + + s.conf.RebindingProtectionEnabled = false + assert.Nilf(t, s.filterDNSRebinding("example.com", host), "host: %s", host) + assert.Nilf(t, s.filterDNSRebinding("totally-safe.com", host), "host: %s", host) + assert.Nilf(t, s.filterDNSRebinding("absolutely.totally-legit.com", host), "host: %s", host) + } +} diff --git a/internal/dnsforward/stats.go b/internal/dnsforward/stats.go index c447be05c0b..306758c74a2 100644 --- a/internal/dnsforward/stats.go +++ b/internal/dnsforward/stats.go @@ -91,6 +91,9 @@ func (s *Server) updateStats(d *proxy.DNSContext, elapsed time.Duration, res dns case dnsfilter.FilteredSafeSearch: e.Result = stats.RSafeSearch + case dnsfilter.FilteredRebind: + // Rebinding is considered as filtered, not processed + fallthrough case dnsfilter.FilteredBlockList: fallthrough case dnsfilter.FilteredInvalid: diff --git a/internal/querylog/searchcriteria.go b/internal/querylog/searchcriteria.go index f6b0ee98137..2b6493e4a49 100644 --- a/internal/querylog/searchcriteria.go +++ b/internal/querylog/searchcriteria.go @@ -18,6 +18,7 @@ const ( filteringStatusFiltered = "filtered" // all kinds of filtering filteringStatusBlocked = "blocked" // blocked or blocked services + filteringStatusBlockedRebind = "blocked_rebind" // blocked due to rebind attempt filteringStatusBlockedService = "blocked_services" // blocked filteringStatusBlockedSafebrowsing = "blocked_safebrowsing" // blocked by safebrowsing filteringStatusBlockedParental = "blocked_parental" // blocked by parental control @@ -29,7 +30,7 @@ const ( // filteringStatusValues -- array with all possible filteringStatus values var filteringStatusValues = []string{ - filteringStatusAll, filteringStatusFiltered, filteringStatusBlocked, + filteringStatusAll, filteringStatusFiltered, filteringStatusBlocked, filteringStatusBlockedRebind, filteringStatusBlockedService, filteringStatusBlockedSafebrowsing, filteringStatusBlockedParental, filteringStatusWhitelisted, filteringStatusRewritten, filteringStatusSafeSearch, filteringStatusProcessed, @@ -123,7 +124,10 @@ func (c *searchCriteria) ctFilteringStatusCase(res dnsfilter.Result) bool { case filteringStatusBlocked: return res.IsFiltered && - res.Reason.In(dnsfilter.FilteredBlockList, dnsfilter.FilteredBlockedService) + res.Reason.In(dnsfilter.FilteredBlockList, dnsfilter.FilteredBlockedService, dnsfilter.FilteredRebind) + + case filteringStatusBlockedRebind: + return res.IsFiltered && res.Reason == dnsfilter.FilteredRebind case filteringStatusBlockedService: return res.IsFiltered && res.Reason == dnsfilter.FilteredBlockedService @@ -150,6 +154,7 @@ func (c *searchCriteria) ctFilteringStatusCase(res dnsfilter.Result) bool { case filteringStatusProcessed: return !res.Reason.In( dnsfilter.FilteredBlockList, + dnsfilter.FilteredRebind, dnsfilter.FilteredBlockedService, dnsfilter.NotFilteredAllowList, )