Async DNS over TCP (#25690)

This commit is contained in:
Arnout Engelen 2018-10-16 15:35:55 +02:00 committed by GitHub
parent d71ba251ed
commit 23b7f86a06
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 848 additions and 108 deletions

View file

@ -0,0 +1,50 @@
# The bind.keys file is used to override the built-in DNSSEC trust anchors
# which are included as part of BIND 9. The only trust anchors it contains
# are for the DNS root zone ("."). Trust anchors for any other zones MUST
# be configured elsewhere; if they are configured here, they will not be
# recognized or used by named.
#
# The built-in trust anchors are provided for convenience of configuration.
# They are not activated within named.conf unless specifically switched on.
# To use the built-in key, use "dnssec-validation auto;" in the
# named.conf options. Without this option being set, the keys in this
# file are ignored.
#
# This file is NOT expected to be user-configured.
#
# These keys are current as of October 2017. If any key fails to
# initialize correctly, it may have expired. In that event you should
# replace this file with a current version. The latest version of
# bind.keys can always be obtained from ISC at https://www.isc.org/bind-keys.
#
# See https://data.iana.org/root-anchors/root-anchors.xml
# for current trust anchor information for the root zone.
managed-keys {
# This key (19036) is to be phased out starting in 2017. It will
# remain in the root zone for some time after its successor key
# has been added. It will remain this file until it is removed from
# the root zone.
. initial-key 257 3 8 "AwEAAagAIKlVZrpC6Ia7gEzahOR+9W29euxhJhVVLOyQbSEW0O8gcCjF
FVQUTf6v58fLjwBd0YI0EzrAcQqBGCzh/RStIoO8g0NfnfL2MTJRkxoX
bfDaUeVPQuYEhg37NZWAJQ9VnMVDxP/VHL496M/QZxkjf5/Efucp2gaD
X6RS6CXpoY68LsvPVjR0ZSwzz1apAzvN9dlzEheX7ICJBBtuA6G3LQpz
W5hOA2hzCTMjJPJ8LbqF6dsV6DoBQzgul0sGIcGOYl7OyQdXfZ57relS
Qageu+ipAdTTJ25AsRTAoub8ONGcLmqrAmRLKBP1dfwhYB4N7knNnulq
QxA+Uk1ihz0=";
# This key (20326) was published in the root zone in 2017.
# Servers which were already using the old key (19036) should
# roll seamlessly to this new one via RFC 5011 rollover. Servers
# being set up for the first time can use the contents of this
# file as initializing keys; thereafter, the keys in the
# managed key database will be trusted and maintained
# automatically.
. initial-key 257 3 8 "AwEAAaz/tAm8yTn4Mfeh5eyI96WSVexTBAvkMgJzkKTOiW1vkIbzxeF3
+/4RgWOq7HrxRixHlFlExOLAJr5emLvN7SWXgnLh4+B5xQlNVz8Og8kv
ArMtNROxVQuCaSnIDdD5LKyWbRd2n9WGe2R8PzgCmr3EgVLrjyBxWezF
0jLHwVN8efS3rCj/EWgvIWgb9tarpVUDK/b58Da+sqqls3eNbuv7pr+e
oZG+SrDK6nWeL3c6H5Apxz7LjVc1uTIdsIXxuOLYA4/ilBmSVIzuDWfd
RUfhHdY6+cn8HFRm+2hM8AnXGXws9555KrUB5qihylGa8subX2Nn6UwN
R1AkUTV74bU=";
};

View file

@ -0,0 +1,12 @@
;
; BIND reverse data file for broadcast zone
;
$TTL 604800
@ IN SOA localhost. root.localhost. (
1 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS localhost.

View file

@ -0,0 +1,13 @@
;
; BIND reverse data file for local loopback interface
;
$TTL 604800
@ IN SOA localhost. root.localhost. (
1 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS localhost.
1.0.0 IN PTR localhost.

View file

@ -0,0 +1,12 @@
;
; BIND reverse data file for broadcast zone
;
$TTL 604800
@ IN SOA localhost. root.localhost. (
1 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS localhost.

View file

@ -0,0 +1,13 @@
$TTL 86400
@ IN SOA bar.example root.bar.example (
2017010302
3600
900
604800
86400
)
@ IN NS example
example IN A 192.168.2.19
a-single IN A 192.168.2.20

View file

@ -0,0 +1,14 @@
; BIND reverse data file for empty rfc1918 zone
;
; DO NOT EDIT THIS FILE - it is used for multiple zones.
; Instead, copy it, edit named.conf, and use that copy.
;
$TTL 86400
@ IN SOA localhost. root.localhost. (
1 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
86400 ) ; Negative Cache TTL
;
@ IN NS localhost.

View file

@ -0,0 +1,80 @@
$TTL 86400
@ IN SOA foo.test root.foo.test (
2017010302
3600
900
604800
86400
)
@ IN NS test
test IN A 192.168.1.19
a-single IN A 192.168.1.20
a-double IN A 192.168.1.21
a-double IN A 192.168.1.22
aaaa-single IN AAAA fd4d:36b2:3eca:a2d8:0:0:0:1
aaaa-double IN AAAA fd4d:36b2:3eca:a2d8:0:0:0:2
aaaa-double IN AAAA fd4d:36b2:3eca:a2d8:0:0:0:3
a-aaaa IN AAAA fd4d:36b2:3eca:a2d8:0:0:0:4
a-aaaa IN AAAA fd4d:36b2:3eca:a2d8:0:0:0:5
a-aaaa IN A 192.168.1.23
a-aaaa IN A 192.168.1.24
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d00
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d01
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d02
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d03
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d04
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d05
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d06
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d07
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d08
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d09
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d0a
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d0b
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d0c
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d0d
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d0e
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d0f
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d10
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d11
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d12
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d13
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d14
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d15
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d16
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d17
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d18
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d19
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d1a
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d1b
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d1c
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d1d
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d1e
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d1f
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d20
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d21
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d22
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d23
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d24
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d25
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d26
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d27
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d28
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d29
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d2a
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d2b
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d2c
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d2d
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d2e
many in AAAA 2001:985:965:1:ba27:ebff:fe5f:9d2f
service.tcp 86400 IN SRV 10 60 5060 a-single
service.tcp 86400 IN SRV 10 40 5070 a-double
cname-in IN CNAME a-double
cname-ext IN CNAME a-single.bar.example.

View file

@ -0,0 +1,14 @@
;
; BIND data file for local loopback interface
;
$TTL 604800
@ IN SOA localhost. root.localhost. (
2 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604800 ) ; Negative Cache TTL
;
@ IN NS localhost.
@ IN A 127.0.0.1
@ IN AAAA ::1

View file

@ -0,0 +1,90 @@
; This file holds the information on root name servers needed to
; initialize cache of Internet domain name servers
; (e.g. reference this file in the "cache . <file>"
; configuration file of BIND domain name servers).
;
; This file is made available by InterNIC
; under anonymous FTP as
; file /domain/named.cache
; on server FTP.INTERNIC.NET
; -OR- RS.INTERNIC.NET
;
; last update: February 17, 2016
; related version of root zone: 2016021701
;
; formerly NS.INTERNIC.NET
;
. 3600000 NS A.ROOT-SERVERS.NET.
A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4
A.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:ba3e::2:30
;
; FORMERLY NS1.ISI.EDU
;
. 3600000 NS B.ROOT-SERVERS.NET.
B.ROOT-SERVERS.NET. 3600000 A 192.228.79.201
B.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:84::b
;
; FORMERLY C.PSI.NET
;
. 3600000 NS C.ROOT-SERVERS.NET.
C.ROOT-SERVERS.NET. 3600000 A 192.33.4.12
C.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2::c
;
; FORMERLY TERP.UMD.EDU
;
. 3600000 NS D.ROOT-SERVERS.NET.
D.ROOT-SERVERS.NET. 3600000 A 199.7.91.13
D.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2d::d
;
; FORMERLY NS.NASA.GOV
;
. 3600000 NS E.ROOT-SERVERS.NET.
E.ROOT-SERVERS.NET. 3600000 A 192.203.230.10
;
; FORMERLY NS.ISC.ORG
;
. 3600000 NS F.ROOT-SERVERS.NET.
F.ROOT-SERVERS.NET. 3600000 A 192.5.5.241
F.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2f::f
;
; FORMERLY NS.NIC.DDN.MIL
;
. 3600000 NS G.ROOT-SERVERS.NET.
G.ROOT-SERVERS.NET. 3600000 A 192.112.36.4
;
; FORMERLY AOS.ARL.ARMY.MIL
;
. 3600000 NS H.ROOT-SERVERS.NET.
H.ROOT-SERVERS.NET. 3600000 A 198.97.190.53
H.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:1::53
;
; FORMERLY NIC.NORDU.NET
;
. 3600000 NS I.ROOT-SERVERS.NET.
I.ROOT-SERVERS.NET. 3600000 A 192.36.148.17
I.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fe::53
;
; OPERATED BY VERISIGN, INC.
;
. 3600000 NS J.ROOT-SERVERS.NET.
J.ROOT-SERVERS.NET. 3600000 A 192.58.128.30
J.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:c27::2:30
;
; OPERATED BY RIPE NCC
;
. 3600000 NS K.ROOT-SERVERS.NET.
K.ROOT-SERVERS.NET. 3600000 A 193.0.14.129
K.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fd::1
;
; OPERATED BY ICANN
;
. 3600000 NS L.ROOT-SERVERS.NET.
L.ROOT-SERVERS.NET. 3600000 A 199.7.83.42
L.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:3::42
;
; OPERATED BY WIDE
;
. 3600000 NS M.ROOT-SERVERS.NET.
M.ROOT-SERVERS.NET. 3600000 A 202.12.27.33
M.ROOT-SERVERS.NET. 3600000 AAAA 2001:dc3::35
; End of file

View file

@ -0,0 +1,11 @@
// This is the primary configuration file for the BIND DNS server named.
//
// Please read /usr/share/doc/bind9/README.Debian.gz for information on the
// structure of BIND configuration files in Debian, *BEFORE* you customize
// this configuration file.
//
// If you are just adding zones, please do that in /etc/bind/named.conf.local
include "/etc/bind/named.conf.options";
include "/etc/bind/named.conf.local";
include "/etc/bind/named.conf.default-zones";

View file

@ -0,0 +1,30 @@
// prime the server with knowledge of the root servers
zone "." {
type hint;
file "/etc/bind/db.root";
};
// be authoritative for the localhost forward and reverse zones, and for
// broadcast zones as per RFC 1912
zone "localhost" {
type master;
file "/etc/bind/db.local";
};
zone "127.in-addr.arpa" {
type master;
file "/etc/bind/db.127";
};
zone "0.in-addr.arpa" {
type master;
file "/etc/bind/db.0";
};
zone "255.in-addr.arpa" {
type master;
file "/etc/bind/db.255";
};

View file

@ -0,0 +1,17 @@
//
// Do any local configuration here
//
// Consider adding the 1918 zones here, if they are not used in your
// organization
include "/etc/bind/zones.rfc1918";
zone "bar.example" {
type master;
file "/etc/bind/db.bar.example";
};
zone "foo.test" {
type master;
file "/etc/bind/db.foo.test";
};

View file

@ -0,0 +1,26 @@
options {
directory "/var/cache/bind";
// If there is a firewall between you and nameservers you want
// to talk to, you may need to fix the firewall to allow multiple
// ports to talk. See http://www.kb.cert.org/vuls/id/800113
// If your ISP provided one or more IP addresses for stable
// nameservers, you probably want to use them as forwarders.
// Uncomment the following block, and insert the addresses replacing
// the all-0's placeholder.
// forwarders {
// 0.0.0.0;
// };
//========================================================================
// If BIND logs error messages about the root key being expired,
// you will need to update your keys. See https://www.isc.org/bind-keys
//========================================================================
dnssec-validation auto;
auth-nxdomain no; # conform to RFC1035
listen-on-v6 { any; };
};

View file

@ -0,0 +1,4 @@
key "rndc-key" {
algorithm hmac-md5;
secret "WNiF81LrIxYbbPwt/twgUA==";
};

View file

@ -0,0 +1,20 @@
zone "10.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "16.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "17.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "18.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "19.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "20.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "21.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "22.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "23.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "24.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "25.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "26.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "27.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "28.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "29.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "30.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "31.172.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };
zone "168.192.in-addr.arpa" { type master; file "/etc/bind/db.empty"; };

View file

@ -9,92 +9,36 @@ import java.net.InetAddress
import akka.io.dns.DnsProtocol.{ Ip, RequestType, Srv } import akka.io.dns.DnsProtocol.{ Ip, RequestType, Srv }
import akka.io.{ Dns, IO } import akka.io.{ Dns, IO }
import akka.pattern.ask import akka.pattern.ask
import akka.testkit.AkkaSpec import akka.testkit.{ AkkaSpec, SocketUtil }
import akka.util.Timeout import akka.util.Timeout
import scala.concurrent.duration._ import scala.concurrent.duration._
/* /*
Relies on two zones setup, akka.test and akka.test2 e.g. These tests rely on a DNS server with 2 zones configured, foo.test and bar.example.
* Install bind
* Create the two zones in /var/named/akka.test.zone and /var/named/akka.test2.zone
* Add the following to /etc/named.conf:
zone "akka.test" IN {
type master;
file "akka.test.zone";
};
zone "akka.test2" IN {
type master;
file "akka.test2.zone";
};
/var/named/akka.test.zone:
$TTL 86400
@ IN SOA akka.test root.akka.test (
2017010302
3600
900
604800
86400
)
@ IN NS test
test IN A 192.168.1.19
a-single IN A 192.168.1.20
a-double IN A 192.168.1.21
a-double IN A 192.168.1.22
aaaa-single IN AAAA fd4d:36b2:3eca:a2d8:0:0:0:1
aaaa-double IN AAAA fd4d:36b2:3eca:a2d8:0:0:0:2
aaaa-double IN AAAA fd4d:36b2:3eca:a2d8:0:0:0:3
a-aaaa IN AAAA fd4d:36b2:3eca:a2d8:0:0:0:4
a-aaaa IN AAAA fd4d:36b2:3eca:a2d8:0:0:0:5
a-aaaa IN A 192.168.1.23
a-aaaa IN A 192.168.1.24
service.tcp 86400 IN SRV 10 60 5060 a-single
service.tcp 86400 IN SRV 10 40 5070 a-double
cname-in IN CNAME a-double
cname-ext IN CNAME a-single.akka.test2.
/var/named/akka.test2.zone:
$TTL 86400
@ IN SOA akka.test2 root.akka.test2 (
2017010302
3600
900
604800
86400
)
@ IN NS test2
test2 IN A 192.168.2.19
a-single IN A 192.168.2.20
The configuration to start a bind DNS server in Docker with this configuration
is included, and the test will automatically start this container when the
test starts and tear it down when it finishes.
*/ */
class AsyncDnsResolverIntegrationSpec extends AkkaSpec( class AsyncDnsResolverIntegrationSpec extends AkkaSpec(
""" s"""
akka.loglevel = DEBUG akka.loglevel = DEBUG
akka.io.dns.resolver = async-dns akka.io.dns.resolver = async-dns
akka.io.dns.async-dns.nameservers = [localhost] akka.io.dns.async-dns.nameservers = ["localhost:${AsyncDnsResolverIntegrationSpec.dockerDnsServerPort}"]
// akka.io.dns.async-dns.nameservers = default // akka.io.dns.async-dns.nameservers = default
""") { """) with DockerBindDnsService {
val duration = 10.seconds val duration = 10.seconds
implicit val timeout = Timeout(duration) implicit val timeout = Timeout(duration)
"Resolver" must { val hostPort = AsyncDnsResolverIntegrationSpec.dockerDnsServerPort
pending // PENDING since needs `bind` server to be running to test end-to-end "Resolver" must {
if (!dockerAvailable())
pending
"resolve single A record" in { "resolve single A record" in {
val name = "a-single.akka.test" val name = "a-single.foo.test"
val answer = resolve(name, DnsProtocol.Ip(ipv6 = false)) val answer = resolve(name, DnsProtocol.Ip(ipv6 = false))
withClue(answer) { withClue(answer) {
answer.name shouldEqual name answer.name shouldEqual name
@ -105,7 +49,7 @@ class AsyncDnsResolverIntegrationSpec extends AkkaSpec(
} }
"resolve double A records" in { "resolve double A records" in {
val name = "a-double.akka.test" val name = "a-double.foo.test"
val answer = resolve(name) val answer = resolve(name)
answer.name shouldEqual name answer.name shouldEqual name
answer.records.map(_.asInstanceOf[ARecord].ip).toSet shouldEqual Set( answer.records.map(_.asInstanceOf[ARecord].ip).toSet shouldEqual Set(
@ -115,14 +59,14 @@ class AsyncDnsResolverIntegrationSpec extends AkkaSpec(
} }
"resolve single AAAA record" in { "resolve single AAAA record" in {
val name = "aaaa-single.akka.test" val name = "aaaa-single.foo.test"
val answer = resolve(name) val answer = resolve(name)
answer.name shouldEqual name answer.name shouldEqual name
answer.records.map(_.asInstanceOf[AAAARecord].ip) shouldEqual Seq(InetAddress.getByName("fd4d:36b2:3eca:a2d8:0:0:0:1")) answer.records.map(_.asInstanceOf[AAAARecord].ip) shouldEqual Seq(InetAddress.getByName("fd4d:36b2:3eca:a2d8:0:0:0:1"))
} }
"resolve double AAAA records" in { "resolve double AAAA records" in {
val name = "aaaa-double.akka.test" val name = "aaaa-double.foo.test"
val answer = resolve(name) val answer = resolve(name)
answer.name shouldEqual name answer.name shouldEqual name
answer.records.map(_.asInstanceOf[AAAARecord].ip).toSet shouldEqual Set( answer.records.map(_.asInstanceOf[AAAARecord].ip).toSet shouldEqual Set(
@ -132,7 +76,7 @@ class AsyncDnsResolverIntegrationSpec extends AkkaSpec(
} }
"resolve mixed A/AAAA records" in { "resolve mixed A/AAAA records" in {
val name = "a-aaaa.akka.test" val name = "a-aaaa.foo.test"
val answer = resolve(name) val answer = resolve(name)
answer.name shouldEqual name answer.name shouldEqual name
@ -148,11 +92,11 @@ class AsyncDnsResolverIntegrationSpec extends AkkaSpec(
} }
"resolve external CNAME record" in { "resolve external CNAME record" in {
val name = "cname-ext.akka.test" val name = "cname-ext.foo.test"
val answer = (IO(Dns) ? DnsProtocol.Resolve(name)).mapTo[DnsProtocol.Resolved].futureValue val answer = (IO(Dns) ? DnsProtocol.Resolve(name)).mapTo[DnsProtocol.Resolved].futureValue
answer.name shouldEqual name answer.name shouldEqual name
answer.records.collect { case r: CNameRecord r.canonicalName }.toSet shouldEqual Set( answer.records.collect { case r: CNameRecord r.canonicalName }.toSet shouldEqual Set(
"a-single.akka.test2" "a-single.bar.example"
) )
answer.records.collect { case r: ARecord r.ip }.toSet shouldEqual Set( answer.records.collect { case r: ARecord r.ip }.toSet shouldEqual Set(
InetAddress.getByName("192.168.2.20") InetAddress.getByName("192.168.2.20")
@ -160,11 +104,11 @@ class AsyncDnsResolverIntegrationSpec extends AkkaSpec(
} }
"resolve internal CNAME record" in { "resolve internal CNAME record" in {
val name = "cname-in.akka.test" val name = "cname-in.foo.test"
val answer = resolve(name) val answer = resolve(name)
answer.name shouldEqual name answer.name shouldEqual name
answer.records.collect { case r: CNameRecord r.canonicalName }.toSet shouldEqual Set( answer.records.collect { case r: CNameRecord r.canonicalName }.toSet shouldEqual Set(
"a-double.akka.test" "a-double.foo.test"
) )
answer.records.collect { case r: ARecord r.ip }.toSet shouldEqual Set( answer.records.collect { case r: ARecord r.ip }.toSet shouldEqual Set(
InetAddress.getByName("192.168.1.21"), InetAddress.getByName("192.168.1.21"),
@ -173,29 +117,39 @@ class AsyncDnsResolverIntegrationSpec extends AkkaSpec(
} }
"resolve SRV record" in { "resolve SRV record" in {
val name = "service.tcp.akka.test" val name = "service.tcp.foo.test"
val answer = resolve("service.tcp.akka.test", Srv) val answer = resolve("service.tcp.foo.test", Srv)
answer.name shouldEqual name answer.name shouldEqual name
answer.records.collect { case r: SRVRecord r }.toSet shouldEqual Set( answer.records.collect { case r: SRVRecord r }.toSet shouldEqual Set(
SRVRecord("service.tcp.akka.test", 86400, 10, 60, 5060, "a-single.akka.test"), SRVRecord("service.tcp.foo.test", 86400, 10, 60, 5060, "a-single.foo.test"),
SRVRecord("service.tcp.akka.test", 86400, 10, 40, 5070, "a-double.akka.test") SRVRecord("service.tcp.foo.test", 86400, 10, 40, 5070, "a-double.foo.test")
) )
} }
"resolve same address twice" in { "resolve same address twice" in {
resolve("a-single.akka.test").records.map(_.asInstanceOf[ARecord].ip) shouldEqual Seq(InetAddress.getByName("192.168.1.20")) resolve("a-single.foo.test").records.map(_.asInstanceOf[ARecord].ip) shouldEqual Seq(InetAddress.getByName("192.168.1.20"))
resolve("a-single.akka.test").records.map(_.asInstanceOf[ARecord].ip) shouldEqual Seq(InetAddress.getByName("192.168.1.20")) resolve("a-single.foo.test").records.map(_.asInstanceOf[ARecord].ip) shouldEqual Seq(InetAddress.getByName("192.168.1.20"))
} }
"handle nonexistent domains" in { "handle nonexistent domains" in {
val answer = (IO(Dns) ? DnsProtocol.Resolve("nonexistent.akka.test")).mapTo[DnsProtocol.Resolved].futureValue val answer = (IO(Dns) ? DnsProtocol.Resolve("nonexistent.foo.test")).mapTo[DnsProtocol.Resolved].futureValue
answer.records shouldEqual List.empty answer.records shouldEqual List.empty
} }
"resolve queries that are too big for UDP" in {
val name = "many.foo.test"
val answer = resolve(name)
answer.name shouldEqual name
answer.records.length should be(48)
}
def resolve(name: String, requestType: RequestType = Ip()): DnsProtocol.Resolved = { def resolve(name: String, requestType: RequestType = Ip()): DnsProtocol.Resolved = {
(IO(Dns) ? DnsProtocol.Resolve(name, requestType)).mapTo[DnsProtocol.Resolved].futureValue (IO(Dns) ? DnsProtocol.Resolve(name, requestType)).mapTo[DnsProtocol.Resolved].futureValue
} }
} }
} }
object AsyncDnsResolverIntegrationSpec {
lazy val dockerDnsServerPort = SocketUtil.temporaryLocalPort()
}

View file

@ -0,0 +1,69 @@
/*
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
*/
package akka.io.dns
import collection.JavaConverters._
import akka.testkit.AkkaSpec
import com.spotify.docker.client.DefaultDockerClient
import com.spotify.docker.client.DockerClient.LogsParam
import com.spotify.docker.client.messages.{ ContainerConfig, HostConfig, PortBinding }
import org.scalatest.concurrent.Eventually
import scala.util.Try
import scala.util.control.NonFatal
trait DockerBindDnsService extends Eventually { self: AkkaSpec
val client = DefaultDockerClient.fromEnv().build()
val hostPort: Int
var id: Option[String] = None
def dockerAvailable() = Try(client.ping()).isSuccess
override def atStartup(): Unit = {
self.atStartup()
// https://github.com/sameersbn/docker-bind/pull/61
val image = "raboof/bind:9.11.3-20180713-nochown"
try {
client.pull(image)
} catch {
case NonFatal(_)
log.warning(s"Failed to pull docker image [$image], is docker running?")
return
}
val containerConfig = ContainerConfig.builder()
.image(image)
.env("NO_CHOWN=true")
.hostConfig(
HostConfig.builder()
.portBindings(Map(
"53/tcp" -> List(PortBinding.of("", hostPort)).asJava,
"53/udp" -> List(PortBinding.of("", hostPort)).asJava
).asJava)
.binds(HostConfig.Bind.from(new java.io.File("akka-actor-tests/src/test/bind/").getAbsolutePath).to("/data/bind").build())
.build()
)
.build()
val creation = client.createContainer(containerConfig, "akka-test-dns-" + getClass.getCanonicalName)
creation.warnings() should be(null)
id = Some(creation.id())
client.startContainer(creation.id())
eventually {
client.logs(creation.id(), LogsParam.stderr()).readFully() should include("all zones loaded")
}
}
override def afterTermination(): Unit = {
self.afterTermination()
id.foreach(client.killContainer)
id.foreach(client.removeContainer)
}
}

View file

@ -0,0 +1,77 @@
/*
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
*/
package akka.io.dns.internal
import java.net.InetSocketAddress
import java.util.concurrent.atomic.AtomicBoolean
import scala.collection.immutable.Seq
import akka.actor.Props
import akka.io.Udp
import akka.io.dns.{ RecordClass, RecordType }
import akka.io.dns.internal.DnsClient.{ Answer, Question4 }
import akka.testkit.{ AkkaSpec, ImplicitSender, TestProbe }
class DnsClientSpec extends AkkaSpec with ImplicitSender {
"The async DNS client" should {
val exampleRequest = Question4(42, "akka.io")
val exampleRequestMessage =
Message(42, MessageFlags(), questions = Seq(Question("akka.io", RecordType.A, RecordClass.IN)))
val exampleResponseMessage = Message(42, MessageFlags(answer = true))
val exampleResponse = Answer(42, Nil)
val dnsServerAddress = InetSocketAddress.createUnresolved("foo", 53)
val localAddress = InetSocketAddress.createUnresolved("localhost", 13441)
"not connect to the DNS server over TCP eagerly" in {
val udpExtensionProbe = TestProbe()
val tcpClientCreated = new AtomicBoolean(false)
val client = system.actorOf(Props(new DnsClient(dnsServerAddress) {
override val udp = udpExtensionProbe.ref
override def createTcpClient = {
tcpClientCreated.set(true)
TestProbe().ref
}
}))
client ! exampleRequest
udpExtensionProbe.expectMsgType[Udp.Bind]
udpExtensionProbe.lastSender ! Udp.Bound(InetSocketAddress.createUnresolved("localhost", 41325))
expectMsgType[Udp.Send]
client ! Udp.Received(exampleResponseMessage.write(), dnsServerAddress)
expectMsg(exampleResponse)
tcpClientCreated.get() should be(false)
}
"Fall back to TCP when the UDP response is truncated" in {
val udpExtensionProbe = TestProbe()
val tcpClientProbe = TestProbe()
val client = system.actorOf(Props(new DnsClient(dnsServerAddress) {
override val udp = udpExtensionProbe.ref
override def createTcpClient = tcpClientProbe.ref
}))
client ! exampleRequest
udpExtensionProbe.expectMsgType[Udp.Bind]
udpExtensionProbe.lastSender ! Udp.Bound(InetSocketAddress.createUnresolved("localhost", 41325))
expectMsgType[Udp.Send]
client ! Udp.Received(Message(exampleRequest.id, MessageFlags(truncated = true)).write(), dnsServerAddress)
tcpClientProbe.expectMsg(exampleRequestMessage)
tcpClientProbe.reply(exampleResponse)
expectMsg(exampleResponse)
}
}
}

View file

@ -0,0 +1,101 @@
/*
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
*/
package akka.io.dns.internal
import java.net.InetSocketAddress
import akka.actor.{ Props, Terminated }
import akka.io.Tcp
import akka.io.Tcp.{ CommandFailed, Connected, PeerClosed, Register }
import akka.io.dns.{ RecordClass, RecordType }
import akka.io.dns.internal.DnsClient.Answer
import akka.testkit.{ AkkaSpec, ImplicitSender, TestProbe }
import scala.collection.immutable.Seq
class TcpDnsClientSpec extends AkkaSpec with ImplicitSender {
import TcpDnsClient._
"The async TCP DNS client" should {
val exampleRequestMessage =
Message(42, MessageFlags(), questions = Seq(Question("akka.io", RecordType.A, RecordClass.IN)))
val exampleResponseMessage = Message(42, MessageFlags(answer = true))
val dnsServerAddress = InetSocketAddress.createUnresolved("foo", 53)
val localAddress = InetSocketAddress.createUnresolved("localhost", 13441)
"reconnect when the server closes the connection" in {
val tcpExtensionProbe = TestProbe()
val answerProbe = TestProbe()
val client = system.actorOf(Props(new TcpDnsClient(tcpExtensionProbe.ref, dnsServerAddress, answerProbe.ref)))
client ! exampleRequestMessage
tcpExtensionProbe.expectMsg(Tcp.Connect(dnsServerAddress))
tcpExtensionProbe.lastSender ! Connected(dnsServerAddress, localAddress)
expectMsgType[Register]
val registered = tcpExtensionProbe.lastSender
expectMsgType[Tcp.Write]
registered ! Tcp.Received(encodeLength(exampleResponseMessage.write().length) ++ exampleResponseMessage.write())
answerProbe.expectMsg(Answer(42, Nil))
// When a new request arrived after the connection is closed
registered ! PeerClosed
client ! exampleRequestMessage
// Expect a reconnect
tcpExtensionProbe.expectMsg(Tcp.Connect(dnsServerAddress))
}
"accept a fragmented TCP response" in {
val tcpExtensionProbe = TestProbe()
val answerProbe = TestProbe()
val client = system.actorOf(Props(new TcpDnsClient(tcpExtensionProbe.ref, dnsServerAddress, answerProbe.ref)))
client ! exampleRequestMessage
tcpExtensionProbe.expectMsg(Tcp.Connect(dnsServerAddress))
tcpExtensionProbe.lastSender ! Connected(dnsServerAddress, localAddress)
expectMsgType[Register]
val registered = tcpExtensionProbe.lastSender
expectMsgType[Tcp.Write]
val fullResponse = encodeLength(exampleResponseMessage.write().length) ++ exampleResponseMessage.write()
registered ! Tcp.Received(fullResponse.take(8))
registered ! Tcp.Received(fullResponse.drop(8))
answerProbe.expectMsg(Answer(42, Nil))
}
"accept merged TCP responses" in {
val tcpExtensionProbe = TestProbe()
val answerProbe = TestProbe()
val client = system.actorOf(Props(new TcpDnsClient(tcpExtensionProbe.ref, dnsServerAddress, answerProbe.ref)))
client ! exampleRequestMessage
client ! exampleRequestMessage.copy(id = 43)
tcpExtensionProbe.expectMsg(Tcp.Connect(dnsServerAddress))
tcpExtensionProbe.lastSender ! Connected(dnsServerAddress, localAddress)
expectMsgType[Register]
val registered = tcpExtensionProbe.lastSender
expectMsgType[Tcp.Write]
expectMsgType[Tcp.Write]
val fullResponse =
encodeLength(exampleResponseMessage.write().length) ++ exampleResponseMessage.write() ++
encodeLength(exampleResponseMessage.write().length) ++ exampleResponseMessage.copy(id = 43).write()
registered ! Tcp.Received(fullResponse.take(8))
registered ! Tcp.Received(fullResponse.drop(8))
answerProbe.expectMsg(Answer(42, Nil))
answerProbe.expectMsg(Answer(43, Nil))
}
}
}

View file

@ -7,13 +7,15 @@ package akka.io.dns.internal
import java.net.{ InetAddress, InetSocketAddress } import java.net.{ InetAddress, InetSocketAddress }
import akka.actor.Status.Failure import akka.actor.Status.Failure
import akka.actor.{ Actor, ActorLogging, ActorRef, NoSerializationVerificationNeeded, Stash } import akka.actor.{ Actor, ActorLogging, ActorRef, NoSerializationVerificationNeeded, Props, Stash }
import akka.annotation.InternalApi import akka.annotation.InternalApi
import akka.io.dns.{ RecordClass, RecordType, ResourceRecord } import akka.io.dns.{ RecordClass, RecordType, ResourceRecord }
import akka.io.{ IO, Udp } import akka.io.{ IO, Tcp, Udp }
import akka.pattern.BackoffSupervisor
import scala.collection.{ immutable im } import scala.collection.{ immutable im }
import scala.util.Try import scala.util.Try
import scala.concurrent.duration._
/** /**
* INTERNAL API * INTERNAL API
@ -38,13 +40,20 @@ import scala.util.Try
import context.system import context.system
IO(Udp) ! Udp.Bind(self, new InetSocketAddress(InetAddress.getByAddress(Array.ofDim(4)), 0)) val udp = IO(Udp)
val tcp = IO(Tcp)
var inflightRequests: Map[Short, ActorRef] = Map.empty var inflightRequests: Map[Short, (ActorRef, Message)] = Map.empty
lazy val tcpDnsClient: ActorRef = createTcpClient()
override def preStart() = {
udp ! Udp.Bind(self, new InetSocketAddress(InetAddress.getByAddress(Array.ofDim(4)), 0))
}
def receive: Receive = { def receive: Receive = {
case Udp.Bound(local) case Udp.Bound(local)
log.debug(s"Bound to UDP address [{}]", local) log.debug("Bound to UDP address [{}]", local)
context.become(ready(sender())) context.become(ready(sender()))
unstashAll() unstashAll()
case _: Question4 case _: Question4
@ -65,23 +74,23 @@ import scala.util.Try
inflightRequests -= id inflightRequests -= id
case Question4(id, name) case Question4(id, name)
log.debug("Resolving [{}] (A)", name) log.debug("Resolving [{}] (A)", name)
inflightRequests += (id -> sender())
val msg = message(name, id, RecordType.A) val msg = message(name, id, RecordType.A)
log.debug(s"Message [{}] to [{}]: [{}]", id, ns, msg) inflightRequests += (id -> (sender(), msg))
log.debug("Message [{}] to [{}]: [{}]", id, ns, msg)
socket ! Udp.Send(msg.write(), ns) socket ! Udp.Send(msg.write(), ns)
case Question6(id, name) case Question6(id, name)
log.debug("Resolving [{}] (AAAA)", name) log.debug("Resolving [{}] (AAAA)", name)
inflightRequests += (id -> sender())
val msg = message(name, id, RecordType.AAAA) val msg = message(name, id, RecordType.AAAA)
log.debug(s"Message to [{}]: [{}]", ns, msg) inflightRequests += (id -> (sender(), msg))
log.debug("Message to [{}]: [{}]", ns, msg)
socket ! Udp.Send(msg.write(), ns) socket ! Udp.Send(msg.write(), ns)
case SrvQuestion(id, name) case SrvQuestion(id, name)
log.debug("Resolving [{}] (SRV)", name) log.debug("Resolving [{}] (SRV)", name)
inflightRequests += (id -> sender())
val msg = message(name, id, RecordType.SRV) val msg = message(name, id, RecordType.SRV)
log.debug(s"Message to {}: msg", ns, msg) inflightRequests += (id -> (sender(), msg))
log.debug("Message to [{}]: [{}]", ns, msg)
socket ! Udp.Send(msg.write(), ns) socket ! Udp.Send(msg.write(), ns)
case Udp.CommandFailed(cmd) case Udp.CommandFailed(cmd)
@ -91,33 +100,51 @@ import scala.util.Try
// best effort, don't throw // best effort, don't throw
Try { Try {
val msg = Message.parse(send.payload) val msg = Message.parse(send.payload)
inflightRequests.get(msg.id).foreach { s inflightRequests.get(msg.id).foreach {
s ! Failure(new RuntimeException("Send failed to nameserver")) case (s, _)
inflightRequests -= msg.id s ! Failure(new RuntimeException("Send failed to nameserver"))
inflightRequests -= msg.id
} }
} }
case _ case _
log.warning("Dns client failed to send {}", cmd) log.warning("Dns client failed to send {}", cmd)
} }
case Udp.Received(data, remote) case Udp.Received(data, remote)
log.debug(s"Received message from [{}]: [{}]", remote, data) log.debug("Received message from [{}]: [{}]", remote, data)
val msg = Message.parse(data) val msg = Message.parse(data)
log.debug(s"Decoded: $msg") log.debug("Decoded UDP DNS response [{}]", data)
// TODO remove me when #25460 is implemented
if (msg.flags.isTruncated) { if (msg.flags.isTruncated) {
log.warning("DNS response truncated and fallback to TCP is not yet implemented. See #25460") log.debug("DNS response truncated, falling back to TCP")
inflightRequests.get(msg.id) match {
case Some((_, msg))
tcpDnsClient ! msg
case _
log.debug("Client for id {} not found. Discarding unsuccessful response.", msg.id)
}
} else {
val (recs, additionalRecs) = if (msg.flags.responseCode == ResponseCode.SUCCESS) (msg.answerRecs, msg.additionalRecs) else (Nil, Nil)
self ! Answer(msg.id, recs, additionalRecs)
} }
val (recs, additionalRecs) = if (msg.flags.responseCode == ResponseCode.SUCCESS) (msg.answerRecs, msg.additionalRecs) else (Nil, Nil) case response: Answer
val response = Answer(msg.id, recs, additionalRecs)
inflightRequests.get(response.id) match { inflightRequests.get(response.id) match {
case Some(reply) case Some((reply, _))
reply ! response reply ! response
inflightRequests -= response.id inflightRequests -= response.id
case None case None
log.debug("Client for id {} not found. Discarding response.", response.id) log.debug("Client for id {} not found. Discarding response.", response.id)
} }
case Udp.Unbind socket ! Udp.Unbind case Udp.Unbind socket ! Udp.Unbind
case Udp.Unbound context.stop(self) case Udp.Unbound context.stop(self)
} }
def createTcpClient() = {
context.actorOf(BackoffSupervisor.props(
Props(classOf[TcpDnsClient], tcp, ns, self),
childName = "tcpDnsClient",
minBackoff = 10.millis,
maxBackoff = 20.seconds,
randomFactor = 0.1
), "tcpDnsClientSupervisor")
}
} }

View file

@ -0,0 +1,100 @@
/*
* Copyright (C) 2018 Lightbend Inc. <https://www.lightbend.com>
*/
package akka.io.dns.internal
import java.net.InetSocketAddress
import akka.AkkaException
import akka.actor.{ Actor, ActorLogging, ActorRef, Stash }
import akka.annotation.InternalApi
import akka.io.Tcp._
import akka.io.dns.internal.DnsClient.{ Answer, DnsQuestion, Question4 }
import akka.io.{ IO, Tcp }
import akka.util.ByteString
/**
* INTERNAL API
*/
@InternalApi private[akka] class TcpDnsClient(tcp: ActorRef, ns: InetSocketAddress, answerRecipient: ActorRef) extends Actor with ActorLogging with Stash {
import TcpDnsClient._
import context.system
override def receive: Receive = idle
val idle: Receive = {
case _: Message
stash()
log.debug("Connecting to [{}]", ns)
tcp ! Tcp.Connect(ns)
context.become(connecting)
}
val connecting: Receive = {
case failure @ CommandFailed(_: Connect)
throwFailure(s"Failed to connect to TCP DNS server at [$ns]", failure.cause)
case _: Tcp.Connected
log.debug("Connected to TCP address [{}]", ns)
val connection = sender()
context.become(ready(connection))
connection ! Register(self)
unstashAll()
case _: Message
stash()
}
def ready(connection: ActorRef, buffer: ByteString = ByteString.empty): Receive = {
case msg: Message
val bytes = msg.write()
connection ! Tcp.Write(encodeLength(bytes.length) ++ bytes)
case failure @ CommandFailed(_: Write)
throwFailure("Write failed", failure.cause)
case Received(newData)
val data = buffer ++ newData
// TCP DNS responses are prefixed by 2 bytes encoding the length of the response
val prefixSize = 2
if (data.length < prefixSize)
context.become(ready(connection, data))
else {
val expectedPayloadLength = decodeLength(data)
if (data.drop(prefixSize).length < expectedPayloadLength)
context.become(ready(connection, data))
else {
answerRecipient ! parseResponse(data.drop(prefixSize))
context.become(ready(connection))
if (data.length > prefixSize + expectedPayloadLength) {
self ! Received(data.drop(prefixSize + expectedPayloadLength))
}
}
}
case PeerClosed
context.become(idle)
}
private def parseResponse(data: ByteString) = {
val msg = Message.parse(data)
log.debug("Decoded TCP DNS response [{}]", msg)
if (msg.flags.isTruncated) {
log.warning("TCP DNS response truncated")
}
val (recs, additionalRecs) = if (msg.flags.responseCode == ResponseCode.SUCCESS) (msg.answerRecs, msg.additionalRecs) else (Nil, Nil)
Answer(msg.id, recs, additionalRecs)
}
}
private[internal] object TcpDnsClient {
def encodeLength(length: Int): ByteString =
ByteString((length / 256).toByte, length.toByte)
def decodeLength(data: ByteString): Int =
((data(0).toInt + 256) % 256) * 256 + ((data(1) + 256) % 256)
def throwFailure(message: String, cause: Option[Throwable]): Unit =
cause match {
case None
throw new AkkaException(message)
case Some(throwable)
throw new AkkaException(message, throwable)
}
}

View file

@ -101,6 +101,9 @@ object Dependencies {
// in-memory filesystem for file related tests // in-memory filesystem for file related tests
val jimfs = "com.google.jimfs" % "jimfs" % "1.1" % "test" // ApacheV2 val jimfs = "com.google.jimfs" % "jimfs" % "1.1" % "test" // ApacheV2
// docker utils
val dockerClient = "com.spotify" % "docker-client" % "8.13.1" % "test" // ApacheV2
// metrics, measurements, perf testing // metrics, measurements, perf testing
val metrics = "io.dropwizard.metrics" % "metrics-core" % "3.2.5" % "test" // ApacheV2 val metrics = "io.dropwizard.metrics" % "metrics-core" % "3.2.5" % "test" // ApacheV2
val metricsJvm = "io.dropwizard.metrics" % "metrics-jvm" % "3.2.5" % "test" // ApacheV2 val metricsJvm = "io.dropwizard.metrics" % "metrics-jvm" % "3.2.5" % "test" // ApacheV2
@ -147,8 +150,11 @@ object Dependencies {
val testkit = l ++= Seq(Test.junit, Test.scalatest.value) ++ Test.metricsAll val testkit = l ++= Seq(Test.junit, Test.scalatest.value) ++ Test.metricsAll
val actorTests = l ++= Seq(Test.junit, Test.scalatest.value, Test.commonsCodec, Test.commonsMath, val actorTests = l ++= Seq(
Test.mockito, Test.scalacheck.value, Test.jimfs) Test.junit, Test.scalatest.value, Test.commonsCodec, Test.commonsMath,
Test.mockito, Test.scalacheck.value, Test.jimfs,
Test.dockerClient
)
val actorTestkitTyped = l ++= Seq(Provided.junit, Provided.scalatest.value) val actorTestkitTyped = l ++= Seq(Provided.junit, Provided.scalatest.value)