Adds DnsWriter that implements DNS UPDATE protocol

* DnsUpdateWriter publishes changes to NS, DS, A, AAAA records
  for domains/hosts as appropriate using RFC 2136 DNS UPDATE protocol
* Static configuration separate from RegistryConfig
* Include dnsjava library as new third party dependency
  to generate DNS protocol messages
* Expose /_dr/task/writeDns in RegistryTestServer
* Currently not included in BackendComponent
This commit is contained in:
Hans Ridder 2016-04-06 08:56:54 -07:00
parent 954d7e1e8f
commit 20f214b9d0
14 changed files with 1006 additions and 2 deletions

View file

@ -30,15 +30,22 @@ public interface DnsWriter extends AutoCloseable {
/**
* Loads {@code domainName} from datastore and publishes its NS/DS records to the DNS server.
* Replaces existing records for the exact name supplied with an NS record for each name server
* and a DS record for each delegation signer stored in the registry for the supplied domain name.
* If the domain is deleted or is in a "non-publish" state then any existing records are deleted.
*
* @param domainName the fully qualified domain name
* @param domainName the fully qualified domain name, with no trailing dot
*/
void publishDomain(String domainName);
/**
* Loads {@code hostName} from datastore and publishes its A/AAAA glue records to the DNS server.
* Replaces existing records for the exact name supplied, with an A or AAAA record (as
* appropriate) for each address stored in the registry, for the supplied host name. If the host is
* deleted then the existing records are deleted. Assumes that this method will only be called for
* in-bailiwick hosts. The registry does not have addresses for other hosts.
*
* @param hostName the fully qualified host name
* @param hostName the fully qualified host name, with no trailing dot
*/
void publishHost(String hostName);

View file

@ -0,0 +1,26 @@
package(
default_visibility = ["//java/com/google/domain/registry:registry_project"],
)
java_library(
name = "dnsupdate",
srcs = glob(["*.java"]),
deps = [
"//java/com/google/common/annotations",
"//java/com/google/common/base",
"//java/com/google/common/collect",
"//java/com/google/common/io",
"//java/com/google/common/net",
"//java/com/google/common/primitives",
"//java/com/google/domain/registry/config",
"//java/com/google/domain/registry/dns/writer/api",
"//java/com/google/domain/registry/model",
"//java/com/google/domain/registry/util",
"//third_party/java/joda_time",
"//third_party/java/dagger",
"//third_party/java/dnsjava",
"//third_party/java/jsr305_annotations",
"//third_party/java/jsr330_inject",
],
)

View file

@ -0,0 +1,135 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.dns.writer.dnsupdate;
import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Verify.verify;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.primitives.Ints;
import com.google.domain.registry.config.ConfigModule.Config;
import org.joda.time.Duration;
import org.xbill.DNS.Message;
import org.xbill.DNS.Opcode;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.net.Socket;
import java.nio.ByteBuffer;
import javax.inject.Inject;
import javax.net.SocketFactory;
/**
* A transport for DNS messages. Sends/receives DNS messages over TCP using old-style {@link Socket}
* s and the message framing defined in <a href="https://tools.ietf.org/html/rfc1035">RFC 1035</a>.
* We would like use the dnsjava library's {@link SimpleResolver} class for this, but it requires
* {@link SocketChannel} which is not supported on AppEngine.
*/
public class DnsMessageTransport {
/**
* Size of message length field for DNS TCP transport.
*
* @see <a href="https://tools.ietf.org/html/rfc1035">RFC 1035</a>
*/
static final int MESSAGE_LENGTH_FIELD_BYTES = 2;
private static final int MESSAGE_MAXIMUM_LENGTH = (1 << (MESSAGE_LENGTH_FIELD_BYTES * 8)) - 1;
/**
* The standard DNS port number.
*
* @see <a href="https://tools.ietf.org/html/rfc1035">RFC 1035</a>
*/
@VisibleForTesting static final int DNS_PORT = 53;
private final SocketFactory factory;
private final String updateHost;
private final int updateTimeout;
/**
* Class constructor.
*
* @param factory a factory for TCP sockets
* @param updateHost host name of the DNS server
* @param updateTimeout update I/O timeout
*/
@Inject
public DnsMessageTransport(
SocketFactory factory,
@Config("dnsUpdateHost") String updateHost,
@Config("dnsUpdateTimeout") Duration updateTimeout) {
this.factory = factory;
this.updateHost = updateHost;
this.updateTimeout = Ints.checkedCast(updateTimeout.getMillis());
}
/**
* Sends a DNS "query" message (most likely an UPDATE) and returns the response. The response is
* checked for matching ID and opcode.
*
* @param query a message to send
* @return the response received from the server
* @throws IOException if the Socket input/output streams throws one
* @throws IllegalArgumentException if the query is too large to be sent (> 65535 bytes)
*/
public Message send(Message query) throws IOException {
try (Socket socket = factory.createSocket(InetAddress.getByName(updateHost), DNS_PORT)) {
socket.setSoTimeout(updateTimeout);
writeMessage(socket.getOutputStream(), query);
Message response = readMessage(socket.getInputStream());
checkValidResponse(query, response);
return response;
}
}
private void checkValidResponse(Message query, Message response) {
verify(
response.getHeader().getID() == query.getHeader().getID(),
"response ID %s does not match query ID %s",
response.getHeader().getID(),
query.getHeader().getID());
verify(
response.getHeader().getOpcode() == query.getHeader().getOpcode(),
"response opcode '%s' does not match query opcode '%s'",
Opcode.string(response.getHeader().getOpcode()),
Opcode.string(query.getHeader().getOpcode()));
}
private void writeMessage(OutputStream outputStream, Message message) throws IOException {
byte[] messageData = message.toWire();
checkArgument(
messageData.length <= MESSAGE_MAXIMUM_LENGTH,
"DNS request message larger than maximum of %s: %s",
MESSAGE_MAXIMUM_LENGTH,
messageData.length);
ByteBuffer buffer = ByteBuffer.allocate(messageData.length + MESSAGE_LENGTH_FIELD_BYTES);
buffer.putShort((short) messageData.length);
buffer.put(messageData);
outputStream.write(buffer.array());
}
private Message readMessage(InputStream inputStream) throws IOException {
DataInputStream stream = new DataInputStream(inputStream);
int length = stream.readUnsignedShort();
byte[] messageData = new byte[length];
stream.readFully(messageData);
return new Message(messageData);
}
}

View file

@ -0,0 +1,54 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.dns.writer.dnsupdate;
import com.google.domain.registry.config.ConfigModule.Config;
import org.joda.time.Duration;
import dagger.Module;
import dagger.Provides;
@Module
public class DnsUpdateConfigModule {
/**
* Host that receives DNS updates from the registry.
* Usually a "hidden master" for the TLDs.
*/
@Provides
@Config("dnsUpdateHost")
public static String provideDnsUpdateHost() {
return "localhost";
}
/**
* Timeout on the socket for DNS update requests.
*/
@Provides
@Config("dnsUpdateTimeout")
public static Duration provideDnsUpdateTimeout() {
return Duration.standardSeconds(30);
}
/**
* The DNS time-to-live (TTL) for resource records created by the registry.
*/
@Provides
@Config("dnsUpdateTimeToLive")
public static Duration provideDnsUpdateTimeToLive() {
return Duration.standardHours(2);
}
}

View file

@ -0,0 +1,215 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.dns.writer.dnsupdate;
import static com.google.common.base.Verify.verify;
import static com.google.domain.registry.model.EppResourceUtils.loadByUniqueId;
import com.google.common.net.InternetDomainName;
import com.google.domain.registry.config.ConfigModule.Config;
import com.google.domain.registry.dns.writer.api.DnsWriter;
import com.google.domain.registry.model.domain.DomainResource;
import com.google.domain.registry.model.domain.secdns.DelegationSignerData;
import com.google.domain.registry.model.host.HostResource;
import com.google.domain.registry.model.registry.Registries;
import com.google.domain.registry.util.Clock;
import org.joda.time.Duration;
import org.xbill.DNS.AAAARecord;
import org.xbill.DNS.ARecord;
import org.xbill.DNS.DClass;
import org.xbill.DNS.DSRecord;
import org.xbill.DNS.Message;
import org.xbill.DNS.NSRecord;
import org.xbill.DNS.Name;
import org.xbill.DNS.RRset;
import org.xbill.DNS.Rcode;
import org.xbill.DNS.TextParseException;
import org.xbill.DNS.Type;
import org.xbill.DNS.Update;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import javax.inject.Inject;
/**
* A DnsWriter that implements the DNS UPDATE protocol as specified in
* <a href="https://tools.ietf.org/html/rfc2136">RFC 2136</a>. Publishes changes in the
* domain-registry to a (capable) external DNS server, sometimes called a "hidden master". DNS
* UPDATE messages are sent via a "resolver" class which implements the network transport. For each
* publish call, a single UPDATE message is created containing the records required to "synchronize"
* the DNS with the current (at the time of processing) state of the registry, for the supplied
* domain/host.
*
* <p>The general strategy of the publish methods is to delete <em>all</em> resource records of any
* <em>type</em> that match the exact domain/host name supplied. And then for create/update cases,
* add any required records. Deleting all records of any type assumes that the registry is
* authoritative for all records for names in the zone. This seems appropriate for a TLD DNS server,
* which should only contain records required for proper DNS delegation.
*
* <p>Only NS, DS, A, and AAAA records are published, and in particular no DNSSEC signing is done
* assuming that this will be done by a third party DNS provider.
*
* <p>Each publish call is treated as an atomic update to the DNS. If an update fails an exception is
* thrown, expecting the caller to retry the update later. The SOA record serial number is
* implicitly incremented by the server on each UPDATE message, as required by RFC 2136. Care must
* be taken to make sure the SOA serial number does not go backwards if the entire TLD (zone) is
* "reset" to empty and republished.
*/
public class DnsUpdateWriter implements DnsWriter {
private final Duration dnsTimeToLive;
private final DnsMessageTransport resolver;
private final Clock clock;
/**
* Class constructor.
*
* @param dnsTimeToLive TTL used for any created resource records
* @param resolver a resolver used to send/receive the UPDATE messages
* @param clock a source of time
*/
@Inject
public DnsUpdateWriter(
@Config("dnsUpdateTimeToLive") Duration dnsTimeToLive,
DnsMessageTransport resolver,
Clock clock) {
this.dnsTimeToLive = dnsTimeToLive;
this.resolver = resolver;
this.clock = clock;
}
@Override
public void publishDomain(String domainName) {
DomainResource domain = loadByUniqueId(DomainResource.class, domainName, clock.nowUtc());
try {
Update update = new Update(toAbsoluteName(findTldFromName(domainName)));
update.delete(toAbsoluteName(domainName), Type.ANY);
if (domain != null && domain.shouldPublishToDns()) {
update.add(makeNameServerSet(domainName, domain.loadNameservers()));
update.add(makeDelegationSignerSet(domainName, domain.getDsData()));
}
Message response = resolver.send(update);
verify(
response.getRcode() == Rcode.NOERROR,
"DNS server failed domain update for '%s' rcode: %s",
domainName,
Rcode.string(response.getRcode()));
} catch (IOException e) {
throw new RuntimeException("publishDomain failed: " + domainName, e);
}
}
@Override
public void publishHost(String hostName) {
HostResource host = loadByUniqueId(HostResource.class, hostName, clock.nowUtc());
try {
Update update = new Update(toAbsoluteName(findTldFromName(hostName)));
update.delete(toAbsoluteName(hostName), Type.ANY);
if (host != null) {
update.add(makeAddressSet(hostName, host.getInetAddresses()));
update.add(makeV6AddressSet(hostName, host.getInetAddresses()));
}
Message response = resolver.send(update);
verify(
response.getRcode() == Rcode.NOERROR,
"DNS server failed host update for '%s' rcode: %s",
hostName,
Rcode.string(response.getRcode()));
} catch (IOException e) {
throw new RuntimeException("publishHost failed: " + hostName, e);
}
}
/**
* Does nothing. Publish calls are synchronous and atomic.
*/
@Override
public void close() {}
private RRset makeDelegationSignerSet(String domainName, Iterable<DelegationSignerData> dsData)
throws TextParseException {
RRset signerSet = new RRset();
for (DelegationSignerData signerData : dsData) {
DSRecord dsRecord =
new DSRecord(
toAbsoluteName(domainName),
DClass.IN,
dnsTimeToLive.getStandardSeconds(),
signerData.getKeyTag(),
signerData.getAlgorithm(),
signerData.getDigestType(),
signerData.getDigest());
signerSet.addRR(dsRecord);
}
return signerSet;
}
private RRset makeNameServerSet(String domainName, Iterable<HostResource> nameservers)
throws TextParseException {
RRset nameServerSet = new RRset();
for (HostResource host : nameservers) {
NSRecord record =
new NSRecord(
toAbsoluteName(domainName),
DClass.IN,
dnsTimeToLive.getStandardSeconds(),
toAbsoluteName(host.getFullyQualifiedHostName()));
nameServerSet.addRR(record);
}
return nameServerSet;
}
private RRset makeAddressSet(String hostName, Iterable<InetAddress> addresses)
throws TextParseException {
RRset addressSet = new RRset();
for (InetAddress address : addresses) {
if (address instanceof Inet4Address) {
ARecord record =
new ARecord(
toAbsoluteName(hostName), DClass.IN, dnsTimeToLive.getStandardSeconds(), address);
addressSet.addRR(record);
}
}
return addressSet;
}
private RRset makeV6AddressSet(String hostName, Iterable<InetAddress> addresses)
throws TextParseException {
RRset addressSet = new RRset();
for (InetAddress address : addresses) {
if (address instanceof Inet6Address) {
AAAARecord record =
new AAAARecord(
toAbsoluteName(hostName), DClass.IN, dnsTimeToLive.getStandardSeconds(), address);
addressSet.addRR(record);
}
}
return addressSet;
}
private String findTldFromName(String name) {
return Registries.findTldForNameOrThrow(InternetDomainName.from(name)).toString();
}
private Name toAbsoluteName(String name) throws TextParseException {
return Name.fromString(name, Name.root);
}
}

View file

@ -0,0 +1,37 @@
// Copyright 2016 The Domain Registry Authors. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package com.google.domain.registry.dns.writer.dnsupdate;
import com.google.domain.registry.dns.writer.api.DnsWriter;
import javax.net.SocketFactory;
import dagger.Module;
import dagger.Provides;
/** Dagger module that provides a DnsUpdateWriter. */
@Module
public final class DnsUpdateWriterModule {
@Provides
static DnsWriter provideDnsWriter(DnsUpdateWriter dnsWriter) {
return dnsWriter;
}
@Provides
static SocketFactory provideSocketFactory() {
return SocketFactory.getDefault();
}
}

View file

@ -16,6 +16,7 @@ java_library(
"//java/com/google/domain/registry/cron",
"//java/com/google/domain/registry/dns",
"//java/com/google/domain/registry/dns/writer/api",
"//java/com/google/domain/registry/dns/writer/dnsupdate",
"//java/com/google/domain/registry/export",
"//java/com/google/domain/registry/export/sheet",
"//java/com/google/domain/registry/flows",

View file

@ -206,6 +206,12 @@ def domain_registry_repositories():
sha1 = "80276338d1c2542ebebac542b535d1ecd48a3fd7",
)
native.maven_jar(
name = "dnsjava",
artifact = "dnsjava:dnsjava:2.1.7",
sha1 = "0a1ed0a251d22bf528cebfafb94c55e6f3f339cf",
)
native.maven_jar(
name = "eclipse_jdt_core",
artifact = "org.eclipse.jdt:org.eclipse.jdt.core:3.10.0",