// Copyright 2017 The Nomulus 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 google.registry.dns.writer.dnsupdate; import static com.google.common.base.Verify.verify; import static com.google.common.collect.Sets.intersection; import static com.google.common.collect.Sets.union; import static google.registry.model.EppResourceUtils.loadByForeignKey; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.google.common.net.InternetDomainName; import google.registry.config.RegistryConfig.Config; import google.registry.dns.writer.BaseDnsWriter; import google.registry.dns.writer.DnsWriterZone; import google.registry.model.domain.DomainResource; import google.registry.model.domain.secdns.DelegationSignerData; import google.registry.model.host.HostResource; import google.registry.model.registry.Registries; import google.registry.util.Clock; import java.io.IOException; import java.net.Inet4Address; import java.net.Inet6Address; import java.net.InetAddress; import java.util.Optional; import javax.inject.Inject; 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; /** * A DnsWriter that implements the DNS UPDATE protocol as specified in * RFC 2136. Publishes changes in the * domain-registry to a (capable) external DNS server, sometimes called a "hidden master". DNS * UPDATE messages are sent via a supplied "transport" class. * * On call to {@link #commit()}, 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. * *

The general strategy of the publish methods is to delete all resource records of any * type 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. * *

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. * *

Each commit call is treated as an atomic update to the DNS. If a commit fails an exception * is thrown. 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 extends BaseDnsWriter { /** * The name of the pricing engine, as used in {@code Registry.dnsWriter}. Remember to change * the value on affected Registry objects to prevent runtime failures. */ public static final String NAME = "DnsUpdateWriter"; private final Duration dnsDefaultATtl; private final Duration dnsDefaultNsTtl; private final Duration dnsDefaultDsTtl; private final DnsMessageTransport transport; private final Clock clock; private final Update update; private final String zoneName; /** * Class constructor. * * @param dnsDefaultATtl TTL used for any created resource records * @param dnsDefaultNsTtl TTL used for any created nameserver records * @param dnsDefaultDsTtl TTL used for any created DS records * @param transport the transport used to send/receive the UPDATE messages * @param clock a source of time */ @Inject public DnsUpdateWriter( @DnsWriterZone String zoneName, @Config("dnsDefaultATtl") Duration dnsDefaultATtl, @Config("dnsDefaultNsTtl") Duration dnsDefaultNsTtl, @Config("dnsDefaultDsTtl") Duration dnsDefaultDsTtl, DnsMessageTransport transport, Clock clock) { this.zoneName = zoneName; this.update = new Update(toAbsoluteName(zoneName)); this.dnsDefaultATtl = dnsDefaultATtl; this.dnsDefaultNsTtl = dnsDefaultNsTtl; this.dnsDefaultDsTtl = dnsDefaultDsTtl; this.transport = transport; this.clock = clock; } /** * Publish the domain, while keeping tracking of which host refresh quest triggered this domain * refresh. Delete the requesting host in addition to all subordinate hosts. * * @param domainName the fully qualified domain name, with no trailing dot * @param requestingHostName the fully qualified host name, with no trailing dot, that triggers * this domain refresh request */ private void publishDomain(String domainName, String requestingHostName) { DomainResource domain = loadByForeignKey(DomainResource.class, domainName, clock.nowUtc()); update.delete(toAbsoluteName(domainName), Type.ANY); if (domain != null) { // As long as the domain exists, orphan glues should be cleaned. deleteSubordinateHostAddressSet(domain, requestingHostName, update); if (domain.shouldPublishToDns()) { addInBailiwickNameServerSet(domain, update); update.add(makeNameServerSet(domain)); update.add(makeDelegationSignerSet(domain)); } } } @Override public void publishDomain(String domainName) { publishDomain(domainName, null); } @Override public void publishHost(String hostName) { // Get the superordinate domain name of the host. InternetDomainName host = InternetDomainName.from(hostName); ImmutableList hostParts = host.parts(); Optional tld = Registries.findTldForName(host); // host not managed by our registry, no need to update DNS. if (!tld.isPresent()) { return; } ImmutableList tldParts = tld.get().parts(); ImmutableList domainParts = hostParts.subList(hostParts.size() - tldParts.size() - 1, hostParts.size()); String domain = Joiner.on(".").join(domainParts); // Refresh the superordinate domain, always delete the host first to ensure idempotency, // and only publish the host if it is a glue record. publishDomain(domain, hostName); } @Override protected void commitUnchecked() { try { Message response = transport.send(update); verify( response.getRcode() == Rcode.NOERROR, "DNS server failed domain update for '%s' rcode: %s", zoneName, Rcode.string(response.getRcode())); } catch (IOException e) { throw new RuntimeException("publishDomain failed for zone: " + zoneName, e); } } private RRset makeDelegationSignerSet(DomainResource domain) { RRset signerSet = new RRset(); for (DelegationSignerData signerData : domain.getDsData()) { DSRecord dsRecord = new DSRecord( toAbsoluteName(domain.getFullyQualifiedDomainName()), DClass.IN, dnsDefaultDsTtl.getStandardSeconds(), signerData.getKeyTag(), signerData.getAlgorithm(), signerData.getDigestType(), signerData.getDigest()); signerSet.addRR(dsRecord); } return signerSet; } private void deleteSubordinateHostAddressSet( DomainResource domain, String additionalHost, Update update) { for (String hostName : union( domain.getSubordinateHosts(), (additionalHost == null ? ImmutableSet.of() : ImmutableSet.of(additionalHost)))) { update.delete(toAbsoluteName(hostName), Type.ANY); } } private void addInBailiwickNameServerSet(DomainResource domain, Update update) { for (String hostName : intersection( domain.loadNameserverFullyQualifiedHostNames(), domain.getSubordinateHosts())) { HostResource host = loadByForeignKey(HostResource.class, hostName, clock.nowUtc()); update.add(makeAddressSet(host)); update.add(makeV6AddressSet(host)); } } private RRset makeNameServerSet(DomainResource domain) { RRset nameServerSet = new RRset(); for (String hostName : domain.loadNameserverFullyQualifiedHostNames()) { NSRecord record = new NSRecord( toAbsoluteName(domain.getFullyQualifiedDomainName()), DClass.IN, dnsDefaultNsTtl.getStandardSeconds(), toAbsoluteName(hostName)); nameServerSet.addRR(record); } return nameServerSet; } private RRset makeAddressSet(HostResource host) { RRset addressSet = new RRset(); for (InetAddress address : host.getInetAddresses()) { if (address instanceof Inet4Address) { ARecord record = new ARecord( toAbsoluteName(host.getFullyQualifiedHostName()), DClass.IN, dnsDefaultATtl.getStandardSeconds(), address); addressSet.addRR(record); } } return addressSet; } private RRset makeV6AddressSet(HostResource host) { RRset addressSet = new RRset(); for (InetAddress address : host.getInetAddresses()) { if (address instanceof Inet6Address) { AAAARecord record = new AAAARecord( toAbsoluteName(host.getFullyQualifiedHostName()), DClass.IN, dnsDefaultATtl.getStandardSeconds(), address); addressSet.addRR(record); } } return addressSet; } private Name toAbsoluteName(String name) { try { return Name.fromString(name, Name.root); } catch (TextParseException e) { throw new RuntimeException( String.format("toAbsoluteName failed for name: %s in zone: %s", name, zoneName), e); } } }