From b338bf9b3ff1a43c4848ddedde134bac3bd27119 Mon Sep 17 00:00:00 2001 From: Reinaldo de Souza Jr Date: Tue, 1 Dec 2020 18:36:04 +0100 Subject: [PATCH] Check for DNS rebinding attacks --- internal/dnsfilter/dnsfilter.go | 3 + internal/dnsforward/config.go | 5 ++ internal/dnsforward/dnsforward.go | 1 + internal/dnsforward/filter.go | 2 +- internal/dnsforward/http_test.go | 28 ++++---- internal/dnsforward/rebind.go | 95 ++++++++++++++++++++++++ internal/dnsforward/rebind_test.go | 111 +++++++++++++++++++++++++++++ 7 files changed, 230 insertions(+), 15 deletions(-) create mode 100644 internal/dnsforward/rebind.go create mode 100644 internal/dnsforward/rebind_test.go diff --git a/internal/dnsfilter/dnsfilter.go b/internal/dnsfilter/dnsfilter.go index 855bbf60..bfca47d4 100644 --- a/internal/dnsfilter/dnsfilter.go +++ b/internal/dnsfilter/dnsfilter.go @@ -146,6 +146,8 @@ const ( FilteredSafeSearch // FilteredBlockedService - the host is blocked by "blocked services" settings FilteredBlockedService + // FilteredRebind - the request was blocked due to DNS rebinding protection + FilteredRebind // ReasonRewrite - rewrite rule was applied ReasonRewrite @@ -165,6 +167,7 @@ var reasonNames = []string{ "FilteredInvalid", "FilteredSafeSearch", "FilteredBlockedService", + "FilteredRebind", "Rewrite", "RewriteEtcHosts", diff --git a/internal/dnsforward/config.go b/internal/dnsforward/config.go index 881174d1..dbdc15b8 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 + // -- + RebindingEnabled bool `yaml:"rebinding_enabled"` + RebindingAllowedHosts []string `yaml:"rebinding_allowed_hosts"` + // Other settings // -- diff --git a/internal/dnsforward/dnsforward.go b/internal/dnsforward/dnsforward.go index 3640101c..d6825e46 100644 --- a/internal/dnsforward/dnsforward.go +++ b/internal/dnsforward/dnsforward.go @@ -122,6 +122,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() } diff --git a/internal/dnsforward/filter.go b/internal/dnsforward/filter.go index 11267adf..ca394aa6 100644 --- a/internal/dnsforward/filter.go +++ b/internal/dnsforward/filter.go @@ -113,8 +113,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_test.go b/internal/dnsforward/http_test.go index c8e2f9c5..fa071bcf 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_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_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_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_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_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_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_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_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_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_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_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_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_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_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 00000000..9484e107 --- /dev/null +++ b/internal/dnsforward/rebind.go @@ -0,0 +1,95 @@ +// DNS Rebinding protection + +package dnsforward + +import ( + "net" + "strings" + + "github.com/AdguardTeam/golibs/log" +) + +type dnsRebindChecker struct { +} + +// 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) 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 { + + /* 0.0.0.0/8 (RFC 5735 section 3. "here" network) */ + rebind = ip4[0] == 0 || + + /* 10.0.0.0/8 (private) */ + ip4[0] == 10 || + + /* 172.16.0.0/12 (private) */ + (ip4[0] == 172 && ip4[1]&0x10 == 0x10) || + + /* 169.254.0.0/16 (zeroconf) */ + (ip4[0] == 169 && ip4[1] == 254) || + + /* 192.0.2.0/24 (test-net) */ + (ip4[0] == 192 && ip4[1] == 0 && ip4[2] == 2) || + + /* 198.51.100.0/24(test-net) */ + (ip4[0] == 198 && ip4[1] == 51 && ip4[2] == 100) || + + /* 203.0.113.0/24 (test-net) */ + (ip4[0] == 203 && ip4[1] == 0 && ip4[2] == 113) || + + /* 255.255.255.255/32 (broadcast)*/ + ip4.Equal(net.IPv4bcast) + } else { + rebind = ip.Equal(net.IPv6zero) || ip.Equal(net.IPv6unspecified) || + ip.Equal(net.IPv6interfacelocalallnodes) || + ip.Equal(net.IPv6linklocalallnodes) || + ip.Equal(net.IPv6linklocalallrouters) + } + + return rebind || c.isPrivate(ip) || ip.IsLoopback() +} + +// Checks DNS rebinding attacks +// Note both whitelisted and cached hosts will bypass rebinding check (see: processFilteringAfterResponse()). +func (s *Server) isResponseRebind(domain, host string) bool { + if !s.conf.RebindingEnabled { + return false + } + + if log.GetLevel() >= log.DEBUG { + timer := log.StartTimer() + defer timer.LogElapsed("DNS Rebinding check for %s -> %s", domain, host) + } + + for _, h := range s.conf.RebindingAllowedHosts { + if strings.HasSuffix(domain, h) { + return false + } + } + + c := dnsRebindChecker{} + return c.isRebindHost(host) +} diff --git a/internal/dnsforward/rebind_test.go b/internal/dnsforward/rebind_test.go new file mode 100644 index 00000000..5535631c --- /dev/null +++ b/internal/dnsforward/rebind_test.go @@ -0,0 +1,111 @@ +package dnsforward + +import ( + "math/rand" + "net" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestRebindingPrivateAddresses(t *testing.T) { + c := &dnsRebindChecker{} + + r1 := byte(rand.Int31() & 0xFE) + r2 := byte(rand.Int31() & 0xFE) + r3 := byte(rand.Int31() & 0xFE) + + 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, 0x10|byte(1&rand.Int31()), 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) { + s := &Server{} + s.conf.RebindingAllowedHosts = []string{ + "totally-safe.com", + } + + 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.RebindingEnabled = true + assert.True(t, s.isResponseRebind("example.com", host)) + assert.False(t, s.isResponseRebind("totally-safe.com", host)) + assert.False(t, s.isResponseRebind("absolutely.totally-safe.com", host)) + + s.conf.RebindingEnabled = false + assert.False(t, s.isResponseRebind("example.com", host)) + assert.False(t, s.isResponseRebind("totally-safe.com", host)) + assert.False(t, s.isResponseRebind("absolutely.totally-safe.com", host)) + } + + for _, host := range []string{ + "200.168.2.3", + "another-example.com", + } { + s.conf.RebindingEnabled = true + assert.False(t, s.isResponseRebind("example.com", host)) + assert.False(t, s.isResponseRebind("totally-safe.com", host)) + assert.False(t, s.isResponseRebind("absolutely.totally-legit.com", host)) + + s.conf.RebindingEnabled = false + assert.False(t, s.isResponseRebind("example.com", host)) + assert.False(t, s.isResponseRebind("totally-safe.com", host)) + assert.False(t, s.isResponseRebind("absolutely.totally-legit.com", host)) + } +}