From 2073a0cdea2c560465f7ac0cc56f202e6fc39705 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Sun, 7 Apr 2024 13:47:14 +0200 Subject: [PATCH] CVE-2024-25638: Message normalization --- README.adoc | 7 + pom.xml | 41 +- src/main/java/org/xbill/DNS/Cache.java | 115 +++- src/main/java/org/xbill/DNS/Lookup.java | 8 +- src/main/java/org/xbill/DNS/Message.java | 465 ++++++++++++- src/main/java/org/xbill/DNS/RRset.java | 13 +- src/main/java/org/xbill/DNS/Record.java | 12 + src/main/java/org/xbill/DNS/Section.java | 9 + src/main/java/org/xbill/DNS/SetResponse.java | 18 +- .../java/org/xbill/DNS/SetResponseType.java | 1 + .../DNS/lookup/IrrelevantRecordMode.java | 10 + .../org/xbill/DNS/lookup/LookupResult.java | 66 ++ .../org/xbill/DNS/lookup/LookupSession.java | 117 +++- .../DNS/lookup/RedirectLoopException.java | 13 + .../DNS/lookup/RedirectOverflowException.java | 2 +- src/test/java/org/xbill/DNS/LookupTest.java | 20 + src/test/java/org/xbill/DNS/MessageTest.java | 19 +- .../java/org/xbill/DNS/SetResponseTest.java | 43 ++ src/test/java/org/xbill/DNS/dnssec/Rpl.java | 1 + .../java/org/xbill/DNS/dnssec/RplParser.java | 4 +- .../java/org/xbill/DNS/dnssec/TestBase.java | 11 +- .../org/xbill/DNS/dnssec/UnboundTests.java | 447 ++++++++++--- .../xbill/DNS/lookup/LookupResultTest.java | 79 ++- .../xbill/DNS/lookup/LookupSessionTest.java | 630 ++++++++++++++---- src/test/resources/unbound/val_adcopy.rpl | 6 +- .../resources/unbound/val_unalgo_anchor.rpl | 8 +- 26 files changed, 1827 insertions(+), 338 deletions(-) create mode 100644 src/main/java/org/xbill/DNS/lookup/IrrelevantRecordMode.java create mode 100644 src/main/java/org/xbill/DNS/lookup/RedirectLoopException.java diff --git a/README.adoc b/README.adoc index 9a0aebd..79c74f4 100644 --- a/README.adoc +++ b/README.adoc @@ -108,6 +108,13 @@ Do NOT use it. |1000 |700 +.2+|dnsjava.harden_unknown_additional +3+|Harden against unknown records in the authority section and additional section. +If disabled, such records are copied from the upstream and presented to the client together with the answer. +|Boolean +|True +|False + 4+h|dnssec options .2+|dnsjava.dnssec.keycache.max_ttl 3+|Maximum time-to-live (TTL) of entries in the key cache in seconds. diff --git a/pom.xml b/pom.xml index 0135f64..f15d386 100644 --- a/pom.xml +++ b/pom.xml @@ -69,7 +69,7 @@ org.apache.maven.plugins maven-gpg-plugin - 3.1.0 + 3.2.4 sign-artifacts @@ -200,7 +200,7 @@ com.github.siom79.japicmp japicmp-maven-plugin - 0.18.3 + 0.20.0 @@ -417,6 +417,18 @@ ${org.junit.version} test + + org.assertj + assertj-core + 3.25.3 + test + + + org.junit-pioneer + junit-pioneer + 2.2.0 + test + org.mockito mockito-core @@ -429,6 +441,12 @@ ${mockito.version} test + + net.bytebuddy + byte-buddy-agent + 1.14.14 + test + org.slf4j slf4j-simple @@ -503,12 +521,29 @@ + + org.apache.maven.plugins + maven-dependency-plugin + 3.6.1 + + + initialize + + properties + + + + + org.apache.maven.plugins maven-surefire-plugin - ${argLine} --add-opens java.base/sun.net.dns=ALL-UNNAMED + @{argLine} + --add-opens java.base/sun.net.dns=ALL-UNNAMED + --add-opens java.base/sun.net.dns=org.dnsjava + -javaagent:${net.bytebuddy:byte-buddy-agent:jar} diff --git a/src/main/java/org/xbill/DNS/Cache.java b/src/main/java/org/xbill/DNS/Cache.java index a93af2a..1f77c6a 100644 --- a/src/main/java/org/xbill/DNS/Cache.java +++ b/src/main/java/org/xbill/DNS/Cache.java @@ -31,6 +31,8 @@ public class Cache { int compareCredibility(int cred); int getType(); + + boolean isAuthenticated(); } private static int limitExpire(long ttl, long maxttl) { @@ -44,23 +46,23 @@ public class Cache { return (int) expire; } - private static class CacheRRset extends RRset implements Element { - private static final long serialVersionUID = 5971755205903597024L; - + static class CacheRRset extends RRset implements Element { int credibility; int expire; + boolean isAuthenticated; - public CacheRRset(Record rec, int cred, long maxttl) { - super(); + public CacheRRset(Record rec, int cred, long maxttl, boolean isAuthenticated) { this.credibility = cred; this.expire = limitExpire(rec.getTTL(), maxttl); + this.isAuthenticated = isAuthenticated; addRR(rec); } - public CacheRRset(RRset rrset, int cred, long maxttl) { + public CacheRRset(RRset rrset, int cred, long maxttl, boolean isAuthenticated) { super(rrset); this.credibility = cred; this.expire = limitExpire(rrset.getTTL(), maxttl); + this.isAuthenticated = isAuthenticated; } @Override @@ -78,6 +80,11 @@ public class Cache { public String toString() { return super.toString() + " cl = " + credibility; } + + @Override + public boolean isAuthenticated() { + return isAuthenticated; + } } private static class NegativeElement implements Element { @@ -85,8 +92,10 @@ public class Cache { Name name; int credibility; int expire; + boolean isAuthenticated; - public NegativeElement(Name name, int type, SOARecord soa, int cred, long maxttl) { + public NegativeElement( + Name name, int type, SOARecord soa, int cred, long maxttl, boolean isAuthenticated) { this.name = name; this.type = type; long cttl = 0; @@ -95,6 +104,7 @@ public class Cache { } this.credibility = cred; this.expire = limitExpire(cttl, maxttl); + this.isAuthenticated = isAuthenticated; } @Override @@ -113,6 +123,11 @@ public class Cache { return credibility - cred; } + @Override + public boolean isAuthenticated() { + return isAuthenticated; + } + @Override public String toString() { StringBuilder sb = new StringBuilder(); @@ -326,7 +341,7 @@ public class Cache { */ @Deprecated public synchronized void addRecord(Record r, int cred, Object o) { - addRecord(r, cred); + addRecord(r, cred, false); } /** @@ -337,6 +352,10 @@ public class Cache { * @see Record */ public synchronized void addRecord(Record r, int cred) { + addRecord(r, cred, false); + } + + private synchronized void addRecord(Record r, int cred, boolean isAuthenticated) { Name name = r.getName(); int type = r.getRRsetType(); if (!Type.isRR(type)) { @@ -344,8 +363,8 @@ public class Cache { } Element element = findElement(name, type, cred); if (element == null) { - CacheRRset crrset = new CacheRRset(r, cred, maxcache); - addRRset(crrset, cred); + CacheRRset crrset = new CacheRRset(r, cred, maxcache, isAuthenticated); + addRRset(crrset, cred, isAuthenticated); } else if (element.compareCredibility(cred) == 0) { if (element instanceof CacheRRset) { CacheRRset crrset = (CacheRRset) element; @@ -362,6 +381,11 @@ public class Cache { * @see RRset */ public synchronized void addRRset(RRset rrset, int cred) { + addRRset(rrset, cred, false); + } + + private synchronized void addRRset( + RRset rrset, int cred, boolean isAuthenticated) { long ttl = rrset.getTTL(); Name name = rrset.getName(); int type = rrset.getType(); @@ -379,7 +403,7 @@ public class Cache { if (rrset instanceof CacheRRset) { crrset = (CacheRRset) rrset; } else { - crrset = new CacheRRset(rrset, cred, maxcache); + crrset = new CacheRRset(rrset, cred, maxcache, isAuthenticated); } addElement(name, crrset); } @@ -396,6 +420,11 @@ public class Cache { * @param cred The credibility of the negative entry */ public synchronized void addNegative(Name name, int type, SOARecord soa, int cred) { + addNegative(name, type, soa, cred, false); + } + + private synchronized void addNegative( + Name name, int type, SOARecord soa, int cred, boolean isAuthenticated) { long ttl = 0; if (soa != null) { ttl = Math.min(soa.getMinimum(), soa.getTTL()); @@ -410,7 +439,7 @@ public class Cache { element = null; } if (element == null) { - addElement(name, new NegativeElement(name, type, soa, cred, maxncache)); + addElement(name, new NegativeElement(name, type, soa, cred, maxncache, isAuthenticated)); } } } @@ -535,7 +564,7 @@ public class Cache { * * @param name The name to look up * @param type The type to look up - * @return An array of RRsets, or null + * @return A list of matching RRsets, or {@code null}. * @see Credibility */ public List findRecords(Name name, int type) { @@ -548,7 +577,7 @@ public class Cache { * * @param name The name to look up * @param type The type to look up - * @return An array of RRsets, or null + * @return A list of matching RRsets, or {@code null}. * @see Credibility */ public List findAnyRecords(Name name, int type) { @@ -599,7 +628,8 @@ public class Cache { * @see Message */ public SetResponse addMessage(Message in) { - boolean isAuth = in.getHeader().getFlag(Flags.AA); + boolean isAuthoritative = in.getHeader().getFlag(Flags.AA); + boolean isAuthenticated = in.getHeader().getFlag(Flags.AD); Record question = in.getQuestion(); Name qname; Name curname; @@ -625,15 +655,16 @@ public class Cache { additionalNames = new HashSet<>(); answers = in.getSectionRRsets(Section.ANSWER); - for (RRset answer : answers) { + for (int i = 0; i < answers.size(); i++) { + RRset answer = answers.get(i); if (answer.getDClass() != qclass) { continue; } int type = answer.getType(); Name name = answer.getName(); - cred = getCred(Section.ANSWER, isAuth); + cred = getCred(Section.ANSWER, isAuthoritative); if ((type == qtype || qtype == Type.ANY) && name.equals(curname)) { - addRRset(answer, cred); + addRRset(answer, cred, isAuthenticated); completed = true; if (curname == qname) { if (response == null) { @@ -642,26 +673,36 @@ public class Cache { response.addRRset(answer); } markAdditional(answer, additionalNames); - } else if (type == Type.CNAME && name.equals(curname)) { - CNAMERecord cname; - addRRset(answer, cred); - if (curname == qname) { - response = SetResponse.ofType(SetResponseType.CNAME, answer); - } - cname = (CNAMERecord) answer.first(); - curname = cname.getTarget(); } else if (type == Type.DNAME && curname.subdomain(name)) { DNAMERecord dname; - addRRset(answer, cred); + addRRset(answer, cred, isAuthenticated); if (curname == qname) { - response = SetResponse.ofType(SetResponseType.DNAME, answer); + response = SetResponse.ofType(SetResponseType.DNAME, answer, isAuthenticated); + } + + if (i + 1 < answers.size()) { + RRset next = answers.get(i + 1); + if (next.getType() == Type.CNAME && next.getName().equals(curname)) { + // Skip generating the next name from the current DNAME, the synthesized CNAME did that + // for us + continue; + } } + dname = (DNAMERecord) answer.first(); try { curname = curname.fromDNAME(dname); } catch (NameTooLongException e) { break; } + } else if (type == Type.CNAME && name.equals(curname)) { + CNAMERecord cname; + addRRset(answer, cred, isAuthenticated); + if (curname == qname) { + response = SetResponse.ofType(SetResponseType.CNAME, answer, isAuthenticated); + } + cname = (CNAMERecord) answer.first(); + curname = cname.getTarget(); } } @@ -680,12 +721,12 @@ public class Cache { int cachetype = (rcode == Rcode.NXDOMAIN) ? 0 : qtype; if (rcode == Rcode.NXDOMAIN || soa != null || ns == null) { /* Negative response */ - cred = getCred(Section.AUTHORITY, isAuth); + cred = getCred(Section.AUTHORITY, isAuthoritative); SOARecord soarec = null; if (soa != null) { soarec = (SOARecord) soa.first(); } - addNegative(curname, cachetype, soarec, cred); + addNegative(curname, cachetype, soarec, cred, isAuthenticated); if (response == null) { SetResponseType responseType; if (rcode == Rcode.NXDOMAIN) { @@ -698,17 +739,17 @@ public class Cache { /* DNSSEC records are not cached. */ } else { /* Referral response */ - cred = getCred(Section.AUTHORITY, isAuth); - addRRset(ns, cred); + cred = getCred(Section.AUTHORITY, isAuthoritative); + addRRset(ns, cred, isAuthenticated); markAdditional(ns, additionalNames); if (response == null) { - response = new SetResponse(SetResponse.DELEGATION, ns); + response = SetResponse.ofType(SetResponseType.DELEGATION, ns, isAuthenticated); } } } else if (rcode == Rcode.NOERROR && ns != null) { /* Cache the NS set from a positive response. */ - cred = getCred(Section.AUTHORITY, isAuth); - addRRset(ns, cred); + cred = getCred(Section.AUTHORITY, isAuthoritative); + addRRset(ns, cred, isAuthenticated); markAdditional(ns, additionalNames); } @@ -722,8 +763,8 @@ public class Cache { if (!additionalNames.contains(name)) { continue; } - cred = getCred(Section.ADDITIONAL, isAuth); - addRRset(rRset, cred); + cred = getCred(Section.ADDITIONAL, isAuthoritative); + addRRset(rRset, cred, isAuthenticated); } log.debug( diff --git a/src/main/java/org/xbill/DNS/Lookup.java b/src/main/java/org/xbill/DNS/Lookup.java index b8b27cd..e04098a 100644 --- a/src/main/java/org/xbill/DNS/Lookup.java +++ b/src/main/java/org/xbill/DNS/Lookup.java @@ -432,12 +432,18 @@ public final class Lookup { * results of this lookup should not be permanently cached, null can be provided here. * * @param cache The cache to use. + * @throws IllegalArgumentException If the DClass of the cache doesn't match this Lookup's DClass. */ public void setCache(Cache cache) { if (cache == null) { this.cache = new Cache(dclass); this.temporary_cache = true; } else { + if (cache.getDClass() != dclass) { + throw new IllegalArgumentException( + "DClass of cache doesn't match DClass of this Lookup instance"); + } + this.cache = cache; this.temporary_cache = false; } @@ -570,7 +576,7 @@ public final class Lookup { Message query = Message.newQuery(question); Message response; try { - response = resolver.send(query); + response = resolver.send(query).normalize(query); } catch (IOException e) { log.debug( "Lookup for {}/{}, id={} failed using resolver {}", diff --git a/src/main/java/org/xbill/DNS/Message.java b/src/main/java/org/xbill/DNS/Message.java index 1cdfa48..909570a 100644 --- a/src/main/java/org/xbill/DNS/Message.java +++ b/src/main/java/org/xbill/DNS/Message.java @@ -1,5 +1,6 @@ // SPDX-License-Identifier: BSD-3-Clause // Copyright (c) 1999-2004 Brian Wellington (bwelling@xbill.org) +// Copyright (c) 2007-2023 NLnet Labs package org.xbill.DNS; @@ -7,12 +8,11 @@ import java.io.IOException; import java.nio.ByteBuffer; import java.util.ArrayList; import java.util.Collections; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Optional; -import java.util.Set; import lombok.SneakyThrows; +import lombok.extern.slf4j.Slf4j; /** * A DNS Message. A message is the basic unit of communication between the client and server of a @@ -23,6 +23,7 @@ import lombok.SneakyThrows; * @see Section * @author Brian Wellington */ +@Slf4j public class Message implements Cloneable { /** The maximum length of a message in wire format. */ @@ -192,6 +193,7 @@ public class Message implements Cloneable { * @see Section */ public boolean removeRecord(Record r, int section) { + Section.check(section); if (sections[section] != null && sections[section].remove(r)) { header.decCount(section); return true; @@ -207,6 +209,7 @@ public class Message implements Cloneable { * @see Section */ public void removeAllRecords(int section) { + Section.check(section); sections[section] = null; header.setCount(section, 0); } @@ -218,6 +221,7 @@ public class Message implements Cloneable { * @see Section */ public boolean findRecord(Record r, int section) { + Section.check(section); return sections[section] != null && sections[section].contains(r); } @@ -243,6 +247,8 @@ public class Message implements Cloneable { * @see Section */ public boolean findRRset(Name name, int type, int section) { + Type.check(type); + Section.check(section); if (sections[section] == null) { return false; } @@ -364,6 +370,7 @@ public class Message implements Cloneable { */ @Deprecated public Record[] getSectionArray(int section) { + Section.check(section); if (sections[section] == null) { return emptyRecordArray; } @@ -378,51 +385,43 @@ public class Message implements Cloneable { * @see Section */ public List getSection(int section) { + Section.check(section); if (sections[section] == null) { return Collections.emptyList(); } return Collections.unmodifiableList(sections[section]); } - private static boolean sameSet(Record r1, Record r2) { - return r1.getRRsetType() == r2.getRRsetType() - && r1.getDClass() == r2.getDClass() - && r1.getName().equals(r2.getName()); - } - /** * Returns an array containing all records in the given section grouped into RRsets. * * @see RRset * @see Section */ + @SuppressWarnings("java:S1119") // label public List getSectionRRsets(int section) { + Section.check(section); if (sections[section] == null) { return Collections.emptyList(); } + List sets = new LinkedList<>(); - Set hash = new HashSet<>(); - for (Record rec : getSection(section)) { - Name name = rec.getName(); - boolean newset = true; - if (hash.contains(name)) { - for (int j = sets.size() - 1; j >= 0; j--) { - RRset set = sets.get(j); - if (set.getType() == rec.getRRsetType() - && set.getDClass() == rec.getDClass() - && set.getName().equals(name)) { - set.addRR(rec); - newset = false; - break; - } + record_iteration: + for (Record rec : sections[section]) { + for (int j = sets.size() - 1; j >= 0; j--) { + RRset set = sets.get(j); + if (rec.sameRRset(set)) { + set.addRR(rec); + + // Existing set found, continue with the next record + continue record_iteration; } } - if (newset) { - RRset set = new RRset(rec); - sets.add(set); - hash.add(name); - } + + // No existing set found, create a new one + sets.add(new RRset(rec)); } + return sets; } @@ -453,7 +452,7 @@ public class Message implements Cloneable { continue; } - if (lastrec != null && !sameSet(rec, lastrec)) { + if (lastrec != null && !rec.sameRRset(lastrec)) { pos = out.current(); rendered = count; } @@ -604,13 +603,10 @@ public class Message implements Cloneable { * * @see Section */ - public String sectionToString(int i) { - if (i > 3) { - return null; - } - + public String sectionToString(int section) { + Section.check(section); StringBuilder sb = new StringBuilder(); - sectionToString(sb, i); + sectionToString(sb, section); return sb.toString(); } @@ -677,10 +673,10 @@ public class Message implements Cloneable { */ @Override @SneakyThrows(CloneNotSupportedException.class) - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "java:S2975"}) public Message clone() { Message m = (Message) super.clone(); - m.sections = (List[]) new List[sections.length]; + m.sections = new List[sections.length]; for (int i = 0; i < sections.length; i++) { if (sections[i] != null) { m.sections[i] = new LinkedList<>(sections[i]); @@ -705,4 +701,401 @@ public class Message implements Cloneable { public Optional getResolver() { return Optional.ofNullable(resolver); } + + /** + * Checks if a record {@link Type} is allowed within a {@link Section}. + * + * @return {@code true} if the type is allowed, {@code false} otherwise. + */ + boolean isTypeAllowedInSection(int type, int section) { + Type.check(type); + Section.check(section); + switch (section) { + case Section.AUTHORITY: + if (type == Type.SOA + || type == Type.NS + || type == Type.DS + || type == Type.NSEC + || type == Type.NSEC3) { + return true; + } + break; + case Section.ADDITIONAL: + if (type == Type.A || type == Type.AAAA) { + return true; + } + break; + } + + return !Boolean.parseBoolean(System.getProperty("dnsjava.harden_unknown_additional", "true")); + } + + /** + * Creates a normalized copy of this message by following xNAME chains, synthesizing CNAMEs from + * DNAMEs if necessary, and removing illegal RRsets from {@link Section#AUTHORITY} and {@link + * Section#ADDITIONAL}. + * + *

Normalization is only applied to {@link Rcode#NOERROR} and {@link Rcode#NXDOMAIN} responses. + * + *

This method is equivalent to calling {@link #normalize(Message, boolean)} with {@code + * false}. + * + * @param query The query that produced this message. + * @return {@code null} if the message could not be normalized or is otherwise invalid. + * @since 3.6 + */ + public Message normalize(Message query) { + try { + return normalize(query, false); + } catch (WireParseException e) { + // Cannot happen with 'false' + } + + return null; + } + + /** + * Creates a normalized copy of this message by following xNAME chains, synthesizing CNAMEs from + * DNAMEs if necessary, and removing illegal RRsets from {@link Section#AUTHORITY} and {@link + * Section#ADDITIONAL}. + * + *

Normalization is only applied to {@link Rcode#NOERROR} and {@link Rcode#NXDOMAIN} responses. + * + * @param query The query that produced this message. + * @param throwOnIrrelevantRecord If {@code true}, throw an exception instead of silently ignoring + * irrelevant records. + * @return {@code null} if the message could not be normalized or is otherwise invalid. + * @throws WireParseException when {@code throwOnIrrelevantRecord} is {@code true} and an invalid + * or irrelevant record was found. + * @since 3.6 + */ + public Message normalize(Message query, boolean throwOnIrrelevantRecord) + throws WireParseException { + if (getRcode() != Rcode.NOERROR && getRcode() != Rcode.NXDOMAIN) { + return this; + } + + Name sname = query.getQuestion().getName(); + List answerSectionSets = getSectionRRsets(Section.ANSWER); + List additionalSectionSets = getSectionRRsets(Section.ADDITIONAL); + List authoritySectionSets = getSectionRRsets(Section.AUTHORITY); + + List cleanedAnswerSection = new ArrayList<>(); + List cleanedAuthoritySection = new ArrayList<>(); + List cleanedAdditionalSection = new ArrayList<>(); + boolean hadNsInAuthority = false; + + // For the ANSWER section, remove all "irrelevant" records and add synthesized CNAMEs from + // DNAMEs. This will strip out-of-order CNAMEs as well. + for (int i = 0; i < answerSectionSets.size(); i++) { + RRset rrset = answerSectionSets.get(i); + Name oldSname = sname; + + if (rrset.getType() == Type.DNAME && sname.subdomain(rrset.getName())) { + if (rrset.size() > 1) { + String template = + "Normalization failed in response to <{}/{}/{}> (id {}), found {} entries (instead of just one) in DNAME RRSet <{}/{}>"; + if (throwOnIrrelevantRecord) { + throw new WireParseException(template.replace("{}", "%s")); + } + log.warn( + template, + sname, + Type.string(query.getQuestion().getType()), + DClass.string(query.getQuestion().getDClass()), + getHeader().getID(), + rrset.size(), + rrset.getName(), + DClass.string(rrset.getDClass())); + return null; + } + + // If DNAME was queried, don't attempt to synthesize CNAME + if (query.getQuestion().getType() != Type.DNAME) { + // The DNAME is valid, accept it + cleanedAnswerSection.add(rrset); + + // Check if the next rrset is correct CNAME, otherwise synthesize a CNAME + RRset nextRRSet = answerSectionSets.size() >= i + 2 ? answerSectionSets.get(i + 1) : null; + DNAMERecord dname = ((DNAMERecord) rrset.first()); + try { + // Validate that an existing CNAME matches what we would synthesize + if (nextRRSet != null + && nextRRSet.getType() == Type.CNAME + && nextRRSet.getName().equals(sname)) { + Name expected = + Name.concatenate( + nextRRSet.getName().relativize(dname.getName()), dname.getTarget()); + if (expected.equals(((CNAMERecord) nextRRSet.first()).getTarget())) { + continue; + } + } + + // Add a synthesized CNAME; TTL=0 to avoid caching + Name dnameTarget = sname.fromDNAME(dname); + cleanedAnswerSection.add( + new RRset(new CNAMERecord(sname, dname.getDClass(), 0, dnameTarget))); + sname = dnameTarget; + + // In DNAME ANY response, can have data after DNAME + if (query.getQuestion().getType() == Type.ANY) { + for (i++; i < answerSectionSets.size(); i++) { + rrset = answerSectionSets.get(i); + if (rrset.getName().equals(oldSname)) { + cleanedAnswerSection.add(rrset); + } else { + break; + } + } + } + + continue; + } catch (NameTooLongException e) { + String template = + "Normalization failed in response to <{}/{}/{}> (id {}), could not synthesize CNAME for DNAME <{}/{}>"; + if (throwOnIrrelevantRecord) { + throw new WireParseException(template.replace("{}", "%s"), e); + } + log.warn( + template, + sname, + Type.string(query.getQuestion().getType()), + DClass.string(query.getQuestion().getDClass()), + getHeader().getID(), + rrset.getName(), + DClass.string(rrset.getDClass())); + return null; + } + } + } + + // Ignore irrelevant records + if (!sname.equals(rrset.getName())) { + logOrThrow( + throwOnIrrelevantRecord, + "Ignoring irrelevant RRset <{}/{}/{}> in response to <{}/{}/{}> (id {})", + rrset, + sname, + query); + continue; + } + + // Follow CNAMEs + if (rrset.getType() == Type.CNAME && query.getQuestion().getType() != Type.CNAME) { + if (rrset.size() > 1) { + String template = + "Found {} CNAMEs in <{}/{}> response to <{}/{}/{}> (id {}), removing all but the first"; + if (throwOnIrrelevantRecord) { + throw new WireParseException( + String.format( + template.replace("{}", "%s"), + rrset.rrs(false).size(), + rrset.getName(), + DClass.string(rrset.getDClass()), + sname, + Type.string(query.getQuestion().getType()), + DClass.string(query.getQuestion().getDClass()), + getHeader().getID())); + } + log.warn( + template, + rrset.rrs(false).size(), + rrset.getName(), + DClass.string(rrset.getDClass()), + sname, + Type.string(query.getQuestion().getType()), + DClass.string(query.getQuestion().getDClass()), + getHeader().getID()); + List cnameRRset = rrset.rrs(false); + for (int cnameIndex = 1; cnameIndex < cnameRRset.size(); cnameIndex++) { + rrset.deleteRR(cnameRRset.get(i)); + } + } + + sname = ((CNAMERecord) rrset.first()).getTarget(); + cleanedAnswerSection.add(rrset); + + // In CNAME ANY response, can have data after CNAME + if (query.getQuestion().getType() == Type.ANY) { + for (i++; i < answerSectionSets.size(); i++) { + rrset = answerSectionSets.get(i); + if (rrset.getName().equals(oldSname)) { + cleanedAnswerSection.add(rrset); + } else { + break; + } + } + } + + continue; + } + + // Remove records that don't match the queried type + int qtype = getQuestion().getType(); + if (qtype != Type.ANY && rrset.getActualType() != qtype) { + logOrThrow( + throwOnIrrelevantRecord, + "Ignoring irrelevant RRset <{}/{}/{}> in ANSWER section response to <{}/{}/{}> (id {})", + rrset, + sname, + query); + continue; + } + + // Mark the additional names from relevant RRset as OK + cleanedAnswerSection.add(rrset); + if (sname.equals(rrset.getName())) { + addAdditionalRRset(rrset, additionalSectionSets, cleanedAdditionalSection); + } + } + + for (RRset rrset : authoritySectionSets) { + switch (rrset.getType()) { + case Type.DNAME: + case Type.CNAME: + case Type.A: + case Type.AAAA: + logOrThrow( + throwOnIrrelevantRecord, + "Ignoring forbidden RRset <{}/{}/{}> in AUTHORITY section response to <{}/{}/{}> (id {})", + rrset, + sname, + query); + continue; + } + + if (!isTypeAllowedInSection(rrset.getType(), Section.AUTHORITY)) { + logOrThrow( + throwOnIrrelevantRecord, + "Ignoring disallowed RRset <{}/{}/{}> in AUTHORITY section response to <{}/{}/{}> (id {})", + rrset, + sname, + query); + continue; + } + + if (rrset.getType() == Type.NS) { + // NS set must be pertinent to the query + if (!sname.subdomain(rrset.getName())) { + logOrThrow( + throwOnIrrelevantRecord, + "Ignoring disallowed RRset <{}/{}/{}> in AUTHORITY section response to <{}/{}/{}> (id {}), not a subdomain of the query", + rrset, + sname, + query); + continue; + } + + // We don't want NS sets for NODATA or NXDOMAIN answers, because they could contain + // poisonous contents, from e.g. fragmentation attacks, inserted after long RRSIGs in the + // packet get to the packet border and such + if (getRcode() == Rcode.NXDOMAIN + || (getRcode() == Rcode.NOERROR + && authoritySectionSets.stream().anyMatch(set -> set.getType() == Type.SOA) + && sections[Section.ANSWER] == null)) { + logOrThrow( + throwOnIrrelevantRecord, + "Ignoring disallowed RRset <{}/{}/{}> in AUTHORITY section response to <{}/{}/{}> (id {}), NXDOMAIN or NODATA", + rrset, + sname, + query); + continue; + } + + if (!hadNsInAuthority) { + hadNsInAuthority = true; + } else { + logOrThrow( + throwOnIrrelevantRecord, + "Ignoring disallowed RRset <{}/{}/{}> in AUTHORITY section response to <{}/{}/{}> (id {}), already seen another NS", + rrset, + sname, + query); + continue; + } + } + + cleanedAuthoritySection.add(rrset); + addAdditionalRRset(rrset, additionalSectionSets, cleanedAdditionalSection); + } + + Message cleanedMessage = new Message(this.getHeader()); + cleanedMessage.sections[Section.QUESTION] = this.sections[Section.QUESTION]; + cleanedMessage.sections[Section.ANSWER] = rrsetListToRecords(cleanedAnswerSection); + cleanedMessage.sections[Section.AUTHORITY] = rrsetListToRecords(cleanedAuthoritySection); + cleanedMessage.sections[Section.ADDITIONAL] = rrsetListToRecords(cleanedAdditionalSection); + return cleanedMessage; + } + + private void logOrThrow( + boolean throwOnIrrelevantRecord, String format, RRset rrset, Name sname, Message query) + throws WireParseException { + if (throwOnIrrelevantRecord) { + throw new WireParseException( + String.format( + format.replace("{}", "%s") + this, + rrset.getName(), + DClass.string(rrset.getDClass()), + Type.string(rrset.getType()), + sname, + Type.string(query.getQuestion().getType()), + DClass.string(query.getQuestion().getDClass()), + getHeader().getID())); + } + log.debug( + format, + rrset.getName(), + DClass.string(rrset.getDClass()), + Type.string(rrset.getType()), + sname, + Type.string(query.getQuestion().getType()), + DClass.string(query.getQuestion().getDClass()), + getHeader().getID()); + } + + private List rrsetListToRecords(List rrsets) { + if (rrsets.isEmpty()) { + return null; + } + + List result = new ArrayList<>(rrsets.size()); + for (RRset set : rrsets) { + result.addAll(set.rrs(false)); + result.addAll(set.sigs()); + } + + return result; + } + + private void addAdditionalRRset( + RRset rrset, List additionalSectionSets, List cleanedAdditionalSection) { + if (!doesTypeHaveAdditionalRecords(rrset.getType())) { + return; + } + + for (Record r : rrset.rrs(false)) { + for (RRset set : additionalSectionSets) { + if (set.getName().equals(r.getAdditionalName()) + && isTypeAllowedInSection(set.getType(), Section.ADDITIONAL)) { + cleanedAdditionalSection.add(set); + } + } + } + } + + private boolean doesTypeHaveAdditionalRecords(int type) { + switch (type) { + case Type.MB: + case Type.MD: + case Type.MF: + case Type.NS: + case Type.MX: + case Type.KX: + case Type.SRV: + case Type.NAPTR: + return true; + } + + return false; + } } diff --git a/src/main/java/org/xbill/DNS/RRset.java b/src/main/java/org/xbill/DNS/RRset.java index b0dc86c..be48023 100644 --- a/src/main/java/org/xbill/DNS/RRset.java +++ b/src/main/java/org/xbill/DNS/RRset.java @@ -206,7 +206,8 @@ public class RRset implements Serializable { } /** - * Returns the type of the records + * Returns the type of the records. If this set contains only signatures, it returns the covered + * type. * * @see Type */ @@ -214,6 +215,16 @@ public class RRset implements Serializable { return first().getRRsetType(); } + /** + * Returns the actual type of the records, i.e. for signatures not the type covered but {@link + * Type#RRSIG}. + * + * @see Type + */ + int getActualType() { + return first().getType(); + } + /** * Returns the class of the records * diff --git a/src/main/java/org/xbill/DNS/Record.java b/src/main/java/org/xbill/DNS/Record.java index 63f2d3b..5f135bb 100644 --- a/src/main/java/org/xbill/DNS/Record.java +++ b/src/main/java/org/xbill/DNS/Record.java @@ -562,6 +562,18 @@ public abstract class Record implements Cloneable, Comparable, Serializa return getRRsetType() == rec.getRRsetType() && dclass == rec.dclass && name.equals(rec.name); } + /** + * Determines if this Record could be part of the passed RRset. This compares the name, type, and + * class of the Record and the set. + * + * @since 3.6 + */ + public boolean sameRRset(RRset set) { + return getRRsetType() == set.getType() + && dclass == set.getDClass() + && name.equals(set.getName()); + } + /** * Determines if two Records are identical. This compares the name, type, class, and rdata (with * names canonicalized). The TTLs are not compared. diff --git a/src/main/java/org/xbill/DNS/Section.java b/src/main/java/org/xbill/DNS/Section.java index a72f6ee..75483a0 100644 --- a/src/main/java/org/xbill/DNS/Section.java +++ b/src/main/java/org/xbill/DNS/Section.java @@ -79,4 +79,13 @@ public final class Section { public static int value(String s) { return sections.getValue(s); } + + /** + * Checks that a numeric section value is valid. + * + * @since 3.6 + */ + public static void check(int section) { + sections.check(section); + } } diff --git a/src/main/java/org/xbill/DNS/SetResponse.java b/src/main/java/org/xbill/DNS/SetResponse.java index b67db66..b5e1427 100644 --- a/src/main/java/org/xbill/DNS/SetResponse.java +++ b/src/main/java/org/xbill/DNS/SetResponse.java @@ -13,6 +13,7 @@ import static org.xbill.DNS.SetResponseType.UNKNOWN; import java.util.ArrayList; import java.util.List; +import lombok.AccessLevel; import lombok.Getter; /** @@ -33,10 +34,8 @@ public class SetResponse { private final SetResponseType type; - /** - * @since 3.6 - */ - @Getter private boolean isAuthenticated; + @Getter(AccessLevel.PACKAGE) + private boolean isAuthenticated; private List data; private SetResponse(SetResponseType type, RRset rrset, boolean isAuthenticated) { @@ -55,6 +54,10 @@ public class SetResponse { return ofType(type, rrset, false); } + static SetResponse ofType(SetResponseType type, Cache.CacheRRset rrset) { + return ofType(type, rrset, rrset.isAuthenticated()); + } + static SetResponse ofType(SetResponseType type, RRset rrset, boolean isAuthenticated) { switch (type) { case UNKNOWN: @@ -80,6 +83,13 @@ public class SetResponse { if (data == null) { data = new ArrayList<>(); + if (rrset instanceof Cache.CacheRRset) { + isAuthenticated = ((Cache.CacheRRset) rrset).isAuthenticated(); + } + } else { + if (rrset instanceof Cache.CacheRRset && isAuthenticated) { + isAuthenticated = ((Cache.CacheRRset) rrset).isAuthenticated(); + } } data.add(rrset); diff --git a/src/main/java/org/xbill/DNS/SetResponseType.java b/src/main/java/org/xbill/DNS/SetResponseType.java index 791c774..9e8411a 100644 --- a/src/main/java/org/xbill/DNS/SetResponseType.java +++ b/src/main/java/org/xbill/DNS/SetResponseType.java @@ -1,3 +1,4 @@ +// SPDX-License-Identifier: BSD-3-Clause package org.xbill.DNS; import lombok.Getter; diff --git a/src/main/java/org/xbill/DNS/lookup/IrrelevantRecordMode.java b/src/main/java/org/xbill/DNS/lookup/IrrelevantRecordMode.java new file mode 100644 index 0000000..2ff8129 --- /dev/null +++ b/src/main/java/org/xbill/DNS/lookup/IrrelevantRecordMode.java @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS.lookup; + +/** Defines the handling of irrelevant records during messages normalization. */ +enum IrrelevantRecordMode { + /** Irrelevant records are removed from the message, but otherwise ignored. */ + REMOVE, + /** Throws an error when an irrelevant record is found. */ + THROW, +} diff --git a/src/main/java/org/xbill/DNS/lookup/LookupResult.java b/src/main/java/org/xbill/DNS/lookup/LookupResult.java index 956dd35..3721bdc 100644 --- a/src/main/java/org/xbill/DNS/lookup/LookupResult.java +++ b/src/main/java/org/xbill/DNS/lookup/LookupResult.java @@ -3,8 +3,15 @@ package org.xbill.DNS.lookup; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import lombok.AccessLevel; import lombok.Data; +import lombok.Getter; +import org.xbill.DNS.Flags; +import org.xbill.DNS.Message; import org.xbill.DNS.Name; import org.xbill.DNS.Record; @@ -22,18 +29,77 @@ public final class LookupResult { */ private final List aliases; + /** The queries and responses that made up the result. */ + @Getter(AccessLevel.PACKAGE) + private final Map queryResponsePairs; + + /** + * Gets an indication if the message(s) that provided this result were authenticated, e.g. by + * using {@link org.xbill.DNS.dnssec.ValidatingResolver} or when the upstream resolver has set the + * {@link org.xbill.DNS.Flags#AD} flag. + * + *

IMPORTANT: Note that in the latter case, the flag cannot be trusted unless the {@link + * org.xbill.DNS.Resolver} used by the {@link LookupSession} that created this result: + * + *

    + *
  • has TSIG enabled + *
  • uses an externally secured transport, e.g. with IPSec or DNS over TLS. + *
+ */ + @Getter(AccessLevel.PACKAGE) + private final boolean isAuthenticated; + /** * Construct an instance with the provided records and, in the case of a CNAME or DNAME * indirection a List of aliases. * * @param records a list of records to return. * @param aliases a list of aliases discovered during lookup, or null if there was no indirection. + * @deprecated This class is not intended for public instantiation. */ + @Deprecated public LookupResult(List records, List aliases) { this.records = Collections.unmodifiableList(new ArrayList<>(records)); this.aliases = aliases == null ? Collections.emptyList() : Collections.unmodifiableList(new ArrayList<>(aliases)); + queryResponsePairs = Collections.emptyMap(); + isAuthenticated = false; + } + + LookupResult(boolean isAuthenticated) { + queryResponsePairs = Collections.emptyMap(); + this.isAuthenticated = isAuthenticated; + records = Collections.emptyList(); + aliases = Collections.emptyList(); + } + + LookupResult(Record query, boolean isAuthenticated, Record record) { + this.queryResponsePairs = Collections.singletonMap(query, null); + this.isAuthenticated = isAuthenticated; + this.records = Collections.singletonList(record); + this.aliases = Collections.emptyList(); + } + + LookupResult( + LookupResult previous, + Record query, + Message answer, + boolean isAuthenticated, + List records, + List aliases) { + Map map = new HashMap<>(previous.queryResponsePairs.size() + 1); + map.putAll(previous.queryResponsePairs); + map.put(query, answer); + this.queryResponsePairs = Collections.unmodifiableMap(map); + this.isAuthenticated = + previous.isAuthenticated + && isAuthenticated + && this.queryResponsePairs.values().stream() + .filter(Objects::nonNull) + .allMatch(a -> a.getHeader().getFlag(Flags.AD)); + this.records = Collections.unmodifiableList(new ArrayList<>(records)); + this.aliases = Collections.unmodifiableList(new ArrayList<>(aliases)); } } diff --git a/src/main/java/org/xbill/DNS/lookup/LookupSession.java b/src/main/java/org/xbill/DNS/lookup/LookupSession.java index 2236b15..168a130 100644 --- a/src/main/java/org/xbill/DNS/lookup/LookupSession.java +++ b/src/main/java/org/xbill/DNS/lookup/LookupSession.java @@ -41,6 +41,7 @@ import org.xbill.DNS.Section; import org.xbill.DNS.SetResponse; import org.xbill.DNS.SimpleResolver; import org.xbill.DNS.Type; +import org.xbill.DNS.WireParseException; import org.xbill.DNS.hosts.HostsFileParser; /** @@ -61,6 +62,7 @@ public class LookupSession { private final Map caches; private final HostsFileParser hostsFileParser; private final Executor executor; + private IrrelevantRecordMode irrelevantRecordMode; private LookupSession( @NonNull Resolver resolver, @@ -70,7 +72,8 @@ public class LookupSession { boolean cycleResults, List caches, HostsFileParser hostsFileParser, - Executor executor) { + Executor executor, + IrrelevantRecordMode irrelevantRecordMode) { this.resolver = resolver; this.maxRedirects = maxRedirects; this.ndots = ndots; @@ -82,6 +85,7 @@ public class LookupSession { : caches.stream().collect(Collectors.toMap(Cache::getDClass, e -> e)); this.hostsFileParser = hostsFileParser; this.executor = executor == null ? ForkJoinPool.commonPool() : executor; + this.irrelevantRecordMode = irrelevantRecordMode; } /** @@ -100,6 +104,7 @@ public class LookupSession { private List caches; private HostsFileParser hostsFileParser; private Executor executor; + private IrrelevantRecordMode irrelevantRecordMode = IrrelevantRecordMode.REMOVE; private LookupSessionBuilder() {} @@ -206,6 +211,17 @@ public class LookupSession { return this; } + /** + * Sets how irrelevant records in a {@link Message} returned from the {@link + * #resolver(Resolver)} is handled. The default is {@link IrrelevantRecordMode#REMOVE}. + * + * @return {@code this}. + */ + LookupSessionBuilder irrelevantRecordMode(IrrelevantRecordMode irrelevantRecordMode) { + this.irrelevantRecordMode = irrelevantRecordMode; + return this; + } + /** * Enable querying the local hosts database using the system defaults. * @@ -318,7 +334,8 @@ public class LookupSession { cycleResults, caches, hostsFileParser, - executor); + executor, + irrelevantRecordMode); } } @@ -356,6 +373,22 @@ public class LookupSession { .defaultHostsFileParser(); } + // Visible for testing only + Cache getCache(int dclass) { + return caches.get(dclass); + } + + /** + * Make an asynchronous lookup with the provided {@link Record}. + * + * @param question the name, type and DClass to look up. + * @return A {@link CompletionStage} what will yield the eventual lookup result. + * @since 3.6 + */ + public CompletionStage lookupAsync(Record question) { + return lookupAsync(question.getName(), question.getType(), question.getDClass()); + } + /** * Make an asynchronous lookup of the provided name using the default {@link DClass#IN}. * @@ -430,7 +463,8 @@ public class LookupSession { } else { r = new AAAARecord(name, DClass.IN, 0, result.get()); } - return new LookupResult(Collections.singletonList(r), Collections.emptyList()); + + return new LookupResult(Record.newRecord(name, type, DClass.IN), true, r); } } } catch (IOException e) { @@ -454,10 +488,10 @@ public class LookupSession { if (names.hasNext()) { return lookupUntilSuccess(names, type, dclass); } else { - return completeExceptionally(cause); + return this.completeExceptionally(cause); } } else if (cause != null) { - return completeExceptionally(cause); + return this.completeExceptionally(cause); } else { return CompletableFuture.completedFuture(result); } @@ -467,14 +501,54 @@ public class LookupSession { private CompletionStage lookupWithCache(Record queryRecord, List aliases) { return Optional.ofNullable(caches.get(queryRecord.getDClass())) - .map(c -> c.lookupRecords(queryRecord.getName(), queryRecord.getType(), Credibility.NORMAL)) + .map( + c -> { + log.debug( + "Looking for <{}/{}/{}> in cache", + queryRecord.getName(), + Type.string(queryRecord.getType()), + DClass.string(queryRecord.getDClass())); + return c.lookupRecords( + queryRecord.getName(), queryRecord.getType(), Credibility.NORMAL); + }) .map(setResponse -> setResponseToMessageFuture(setResponse, queryRecord, aliases)) .orElseGet(() -> lookupWithResolver(queryRecord, aliases)); } private CompletionStage lookupWithResolver(Record queryRecord, List aliases) { + Message query = Message.newQuery(queryRecord); + log.debug( + "Asking {} for <{}/{}/{}>", + resolver, + queryRecord.getName(), + Type.string(queryRecord.getType()), + DClass.string(queryRecord.getDClass())); return resolver - .sendAsync(Message.newQuery(queryRecord), executor) + .sendAsync(query, executor) + .thenCompose( + m -> { + try { + Message normalized = + m.normalize(query, irrelevantRecordMode == IrrelevantRecordMode.THROW); + + log.trace( + "Normalized response for <{}/{}/{}> from \n{}\ninto\n{}", + queryRecord.getName(), + Type.string(queryRecord.getType()), + DClass.string(queryRecord.getDClass()), + m, + normalized); + if (normalized == null) { + return completeExceptionally( + new InvalidZoneDataException("Failed to normalize message")); + } + return CompletableFuture.completedFuture(normalized); + } catch (WireParseException e) { + return completeExceptionally( + new LookupFailedException( + "Message normalization failed, refusing to return it", e)); + } + }) .thenApply(this::maybeAddToCache) .thenApply(answer -> buildResult(answer, aliases, queryRecord)); } @@ -490,16 +564,19 @@ public class LookupSession { return message; } + @SuppressWarnings("deprecated") private CompletionStage setResponseToMessageFuture( SetResponse setResponse, Record queryRecord, List aliases) { if (setResponse.isNXDOMAIN()) { return completeExceptionally( new NoSuchDomainException(queryRecord.getName(), queryRecord.getType())); } + if (setResponse.isNXRRSET()) { return completeExceptionally( new NoSuchRRSetException(queryRecord.getName(), queryRecord.getType())); } + if (setResponse.isSuccessful()) { List records = setResponse.answers().stream() @@ -507,17 +584,18 @@ public class LookupSession { .collect(Collectors.toList()); return CompletableFuture.completedFuture(new LookupResult(records, aliases)); } + return null; } - private CompletionStage completeExceptionally(T failure) { - CompletableFuture future = new CompletableFuture<>(); + private CompletionStage completeExceptionally(T failure) { + CompletableFuture future = new CompletableFuture<>(); future.completeExceptionally(failure); return future; } private CompletionStage resolveRedirects(LookupResult response, Record query) { - return maybeFollowRedirect(response, query, 1); + return maybeFollowRedirect(response, query, 0); } private CompletionStage maybeFollowRedirect( @@ -536,13 +614,20 @@ public class LookupSession { } } + @SuppressWarnings("deprecated") private CompletionStage maybeFollowRedirectsInAnswer( LookupResult response, Record query, int redirectCount) { List aliases = new ArrayList<>(response.getAliases()); List results = new ArrayList<>(); Name current = query.getName(); for (Record r : response.getRecords()) { - if (redirectCount > maxRedirects) { + // Abort with a dedicated exception for loops instead of simply trying until reaching the max + // redirects + if (aliases.contains(current)) { + return completeExceptionally(new RedirectLoopException(maxRedirects)); + } + + if (redirectCount >= maxRedirects) { throw new RedirectOverflowException(maxRedirects); } @@ -572,11 +657,17 @@ public class LookupSession { return CompletableFuture.completedFuture(new LookupResult(results, aliases)); } - if (redirectCount > maxRedirects) { + // Abort with a dedicated exception for loops instead of simply trying until reaching the max + // redirects + if (aliases.contains(current)) { + return completeExceptionally(new RedirectLoopException(maxRedirects)); + } + + if (redirectCount >= maxRedirects) { throw new RedirectOverflowException(maxRedirects); } - int finalRedirectCount = redirectCount + 1; + int finalRedirectCount = redirectCount; Record redirectQuery = Record.newRecord(current, query.getType(), query.getDClass()); return lookupWithCache(redirectQuery, aliases) .thenCompose( diff --git a/src/main/java/org/xbill/DNS/lookup/RedirectLoopException.java b/src/main/java/org/xbill/DNS/lookup/RedirectLoopException.java new file mode 100644 index 0000000..3518af5 --- /dev/null +++ b/src/main/java/org/xbill/DNS/lookup/RedirectLoopException.java @@ -0,0 +1,13 @@ +// SPDX-License-Identifier: BSD-3-Clause +package org.xbill.DNS.lookup; + +/** + * Thrown if the lookup results in a loop of CNAME and/or DNAME indirections. + * + * @since 3.6 + */ +public class RedirectLoopException extends RedirectOverflowException { + public RedirectLoopException(int maxRedirects) { + super(maxRedirects); + } +} diff --git a/src/main/java/org/xbill/DNS/lookup/RedirectOverflowException.java b/src/main/java/org/xbill/DNS/lookup/RedirectOverflowException.java index 47e2871..0297b39 100644 --- a/src/main/java/org/xbill/DNS/lookup/RedirectOverflowException.java +++ b/src/main/java/org/xbill/DNS/lookup/RedirectOverflowException.java @@ -18,7 +18,7 @@ public class RedirectOverflowException extends LookupFailedException { } public RedirectOverflowException(int maxRedirects) { - super("Refusing to follow more than " + maxRedirects + " redirects"); + super("Detected a redirect loop: Refusing to follow more than " + maxRedirects + " redirects"); this.maxRedirects = maxRedirects; } } diff --git a/src/test/java/org/xbill/DNS/LookupTest.java b/src/test/java/org/xbill/DNS/LookupTest.java index a5132c8..b794c3e 100644 --- a/src/test/java/org/xbill/DNS/LookupTest.java +++ b/src/test/java/org/xbill/DNS/LookupTest.java @@ -445,8 +445,28 @@ public class LookupTest { if (DUMMY_NAME.equals(response.getName())) { response = response.withName(query.getQuestion().getName()); } + response.setTTL(120); answer.addRecord(response, Section.ANSWER); } return answer; } + + public static Message multiAnswer(Message query, Function recordMaker) { + Message answer = new Message(query.getHeader().getID()); + answer.addRecord(query.getQuestion(), Section.QUESTION); + Name questionName = query.getQuestion().getName(); + Record[] response = recordMaker.apply(questionName); + if (response == null) { + answer.getHeader().setRcode(Rcode.NXDOMAIN); + } else { + for (Record r : response) { + if (DUMMY_NAME.equals(r.getName())) { + r = r.withName(query.getQuestion().getName()); + } + r.setTTL(120); + answer.addRecord(r, Section.ANSWER); + } + } + return answer; + } } diff --git a/src/test/java/org/xbill/DNS/MessageTest.java b/src/test/java/org/xbill/DNS/MessageTest.java index 8610014..54e32d5 100644 --- a/src/test/java/org/xbill/DNS/MessageTest.java +++ b/src/test/java/org/xbill/DNS/MessageTest.java @@ -53,9 +53,9 @@ class MessageTest { Message m = new Message(); assertTrue(m.getSection(0).isEmpty()); assertTrue(m.getSection(1).isEmpty()); - assertTrue(m.getSection(3).isEmpty()); assertTrue(m.getSection(2).isEmpty()); - assertThrows(IndexOutOfBoundsException.class, () -> m.getSection(4)); + assertTrue(m.getSection(3).isEmpty()); + assertThrows(IllegalArgumentException.class, () -> m.getSection(4)); Header h = m.getHeader(); assertEquals(0, h.getCount(0)); assertEquals(0, h.getCount(1)); @@ -71,7 +71,7 @@ class MessageTest { assertTrue(m.getSection(1).isEmpty()); assertTrue(m.getSection(2).isEmpty()); assertTrue(m.getSection(3).isEmpty()); - assertThrows(IndexOutOfBoundsException.class, () -> m.getSection(4)); + assertThrows(IllegalArgumentException.class, () -> m.getSection(4)); Header h = m.getHeader(); assertEquals(0, h.getCount(0)); assertEquals(0, h.getCount(1)); @@ -167,4 +167,17 @@ class MessageTest { assertEquals(clone.getQuestion(), response.getQuestion()); assertEquals(clone.getSection(Section.ANSWER), response.getSection(Section.ANSWER)); } + + @Test + void normalize() throws WireParseException { + Record queryRecord = + Record.newRecord(Name.fromConstantString("example.com."), Type.MX, DClass.IN); + Message query = Message.newQuery(queryRecord); + Message response = new Message(); + response.addRecord(queryRecord, Section.QUESTION); + response.addRecord(queryRecord, Section.ADDITIONAL); + response = response.normalize(query, true); + assertTrue(response.getSection(Section.ANSWER).isEmpty()); + assertTrue(response.getSection(Section.ADDITIONAL).isEmpty()); + } } diff --git a/src/test/java/org/xbill/DNS/SetResponseTest.java b/src/test/java/org/xbill/DNS/SetResponseTest.java index 7bc460d..d436685 100644 --- a/src/test/java/org/xbill/DNS/SetResponseTest.java +++ b/src/test/java/org/xbill/DNS/SetResponseTest.java @@ -46,7 +46,9 @@ import java.net.InetAddress; import java.net.UnknownHostException; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; import org.junit.jupiter.params.provider.EnumSource; +import org.junit.jupiter.params.provider.ValueSource; class SetResponseTest { private static final ARecord A_RECORD_1 = @@ -129,6 +131,47 @@ class SetResponseTest { assertArrayEquals(exp, sr.answers().toArray()); } + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void ofTypeWithCachedRRset(boolean isAuthenticated) { + SetResponse sr = + SetResponse.ofType( + SetResponseType.SUCCESSFUL, + new Cache.CacheRRset(new RRset(A_RECORD_1), 0, 0, isAuthenticated)); + assertEquals(isAuthenticated, sr.isAuthenticated()); + } + + @ParameterizedTest + @CsvSource({ + "false,true,true,true,true", + "false,false,true,false,false", + "true,true,false,true,false", + "true,false,false,false,false", + }) + void addRRsetAuthenticated( + boolean addInitial, + boolean first, + boolean second, + boolean firstResult, + boolean secondResult) { + RRset rrs = new RRset(A_RECORD_1); + SetResponse sr; + if (addInitial) { + sr = SetResponse.ofType(SetResponseType.SUCCESSFUL, rrs, first); + } else { + sr = SetResponse.ofType(SetResponseType.SUCCESSFUL); + sr.addRRset(new Cache.CacheRRset(rrs, 0, 0, first)); + } + + RRset[] exp = new RRset[] {rrs}; + assertArrayEquals(exp, sr.answers().toArray()); + assertEquals(firstResult, sr.isAuthenticated()); + + sr.addRRset(new Cache.CacheRRset(new RRset(A_RECORD_1), 0, 0, second)); + assertEquals(secondResult, sr.isAuthenticated()); + assertEquals(2, sr.answers().size()); + } + @Test void addRRset_multiple() throws TextParseException, UnknownHostException { RRset rrs = new RRset(); diff --git a/src/test/java/org/xbill/DNS/dnssec/Rpl.java b/src/test/java/org/xbill/DNS/dnssec/Rpl.java index 71562e3..b865101 100644 --- a/src/test/java/org/xbill/DNS/dnssec/Rpl.java +++ b/src/test/java/org/xbill/DNS/dnssec/Rpl.java @@ -17,6 +17,7 @@ class Rpl { TreeMap nsec3iterations; String digestPreference; boolean hardenAlgoDowngrade; + boolean hardenUnknownAdditional = true; boolean enableSha1; boolean enableDsa; boolean loadBouncyCastle; diff --git a/src/test/java/org/xbill/DNS/dnssec/RplParser.java b/src/test/java/org/xbill/DNS/dnssec/RplParser.java index b61c12f..e198767 100644 --- a/src/test/java/org/xbill/DNS/dnssec/RplParser.java +++ b/src/test/java/org/xbill/DNS/dnssec/RplParser.java @@ -73,7 +73,7 @@ class RplParser { if (line.startsWith("server:")) { state = ParseState.Server; } else if (line.startsWith("SCENARIO_BEGIN")) { - rpl.scenario = line.substring(line.indexOf(" ")); + rpl.scenario = line.substring(line.indexOf(" ")).trim(); rpl.replays = new LinkedList<>(); rpl.checks = new TreeMap<>(); } else if (line.startsWith("ENTRY_BEGIN")) { @@ -126,6 +126,8 @@ class RplParser { rpl.enableSha1 = "yes".equalsIgnoreCase(line.split(":")[1].trim()); } else if (line.matches("\\s*fake-dsa:.*")) { rpl.enableDsa = "yes".equalsIgnoreCase(line.split(":")[1].trim()); + } else if (line.matches("\\s*harden-unknown-additional:.*")) { + rpl.hardenUnknownAdditional = "yes".equalsIgnoreCase(line.split(":")[1].trim()); } else if (line.matches("\\s*bouncycastle:.*")) { rpl.loadBouncyCastle = "yes".equalsIgnoreCase(line.split(":")[1].trim()); } else if (line.startsWith("CONFIG_END")) { diff --git a/src/test/java/org/xbill/DNS/dnssec/TestBase.java b/src/test/java/org/xbill/DNS/dnssec/TestBase.java index 818fcb9..251c2a6 100644 --- a/src/test/java/org/xbill/DNS/dnssec/TestBase.java +++ b/src/test/java/org/xbill/DNS/dnssec/TestBase.java @@ -27,6 +27,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; +import lombok.extern.slf4j.Slf4j; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -48,9 +49,8 @@ import org.xbill.DNS.SimpleResolver; import org.xbill.DNS.TXTRecord; import org.xbill.DNS.Type; +@Slf4j public abstract class TestBase { - private static final Logger logger = LoggerFactory.getLogger(TestBase.class); - private static final boolean offline = !Boolean.getBoolean("dnsjava.dnssec.online"); private static final boolean partialOffline = "partial".equals(System.getProperty("dnsjava.dnssec.offline")); @@ -125,6 +125,7 @@ public abstract class TestBase { Message m; while ((m = messageReader.readMessage(r)) != null) { + m = m.normalize(Message.newQuery(m.getQuestion()), true); queryResponsePairs.put(key(m), m); } @@ -162,9 +163,13 @@ public abstract class TestBase { new SimpleResolver("8.8.4.4") { @Override public CompletionStage sendAsync(Message query) { - logger.info("---{}", key(query)); Message response = queryResponsePairs.get(key(query)); if (response != null) { + if (!log.isTraceEnabled()) { + log.debug("---{}", key(query)); + } + + log.trace("---{}\n{}", key(query), response); return CompletableFuture.completedFuture(response); } else if ((offline && !partialOffline) || unboundTest || alwaysOffline) { fail("Response for " + key(query) + " not found."); diff --git a/src/test/java/org/xbill/DNS/dnssec/UnboundTests.java b/src/test/java/org/xbill/DNS/dnssec/UnboundTests.java index 11f9bdf..d709fd4 100644 --- a/src/test/java/org/xbill/DNS/dnssec/UnboundTests.java +++ b/src/test/java/org/xbill/DNS/dnssec/UnboundTests.java @@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.when; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.security.Security; @@ -15,10 +16,13 @@ import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; +import lombok.extern.slf4j.Slf4j; import org.bouncycastle.jce.provider.BouncyCastleProvider; import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.xbill.DNS.CNAMERecord; +import org.xbill.DNS.DClass; import org.xbill.DNS.DNAMERecord; import org.xbill.DNS.DNSSEC; import org.xbill.DNS.Flags; @@ -31,133 +35,150 @@ import org.xbill.DNS.Record; import org.xbill.DNS.Section; import org.xbill.DNS.Type; +@Slf4j class UnboundTests extends TestBase { void runUnboundTest() throws ParseException, IOException { - InputStream data = getClass().getResourceAsStream("/unbound/" + testName + ".rpl"); - RplParser p = new RplParser(data); - Rpl rpl = p.parse(); - Properties config = new Properties(); - if (rpl.nsec3iterations != null) { - for (Entry e : rpl.nsec3iterations.entrySet()) { - config.put("dnsjava.dnssec.nsec3.iterations." + e.getKey(), e.getValue()); + try { + InputStream data = getClass().getResourceAsStream("/unbound/" + testName + ".rpl"); + RplParser p = new RplParser(data); + Rpl rpl = p.parse(); + Properties config = new Properties(); + if (rpl.nsec3iterations != null) { + for (Entry e : rpl.nsec3iterations.entrySet()) { + config.put("dnsjava.dnssec.nsec3.iterations." + e.getKey(), e.getValue()); + } } - } - if (rpl.digestPreference != null) { - config.put(ValUtils.DIGEST_PREFERENCE, rpl.digestPreference); - } + if (rpl.digestPreference != null) { + config.put(ValUtils.DIGEST_PREFERENCE, rpl.digestPreference); + } - config.put(ValUtils.DIGEST_HARDEN_DOWNGRADE, Boolean.toString(rpl.hardenAlgoDowngrade)); + config.put(ValUtils.DIGEST_HARDEN_DOWNGRADE, Boolean.toString(rpl.hardenAlgoDowngrade)); - if (rpl.enableSha1) { - config.put(ValUtils.DIGEST_ENABLED + "." + DNSSEC.Digest.SHA1, Boolean.TRUE.toString()); - } + if (rpl.enableSha1) { + config.put(ValUtils.DIGEST_ENABLED + "." + DNSSEC.Digest.SHA1, Boolean.TRUE.toString()); + } - if (rpl.enableDsa || rpl.enableSha1) { - config.put(ValUtils.ALGORITHM_ENABLED + "." + DNSSEC.Algorithm.DSA, Boolean.TRUE.toString()); - config.put( - ValUtils.ALGORITHM_ENABLED + "." + DNSSEC.Algorithm.DSA_NSEC3_SHA1, - Boolean.TRUE.toString()); - } + if (rpl.enableDsa || rpl.enableSha1) { + config.put( + ValUtils.ALGORITHM_ENABLED + "." + DNSSEC.Algorithm.DSA, Boolean.TRUE.toString()); + config.put( + ValUtils.ALGORITHM_ENABLED + "." + DNSSEC.Algorithm.DSA_NSEC3_SHA1, + Boolean.TRUE.toString()); + } - if (rpl.loadBouncyCastle) { - Security.addProvider(new BouncyCastleProvider()); - } + if (!rpl.hardenUnknownAdditional) { + System.setProperty("dnsjava.harden_unknown_additional", Boolean.TRUE.toString()); + } - for (Message m : rpl.replays) { - add(m); - } + if (rpl.loadBouncyCastle) { + Security.addProvider(new BouncyCastleProvider()); + } - // merge xNAME queries into one - List copy = new ArrayList<>(rpl.replays.size()); - copy.addAll(rpl.replays); - List copiedTargets = new ArrayList<>(5); - for (Message m : copy) { - Name target = null; - for (RRset s : m.getSectionRRsets(Section.ANSWER)) { - if (s.getType() == Type.CNAME) { - target = ((CNAMERecord) s.first()).getTarget(); - } else if (s.getType() == Type.DNAME) { - target = ((DNAMERecord) s.first()).getTarget(); - } + for (Message m : rpl.replays) { + add(m); + } - while (target != null) { - Message a = get(target, m.getQuestion().getType()); - if (a == null) { - a = get(target, Type.CNAME); + // merge xNAME queries into one + List copy = new ArrayList<>(rpl.replays.size()); + copy.addAll(rpl.replays); + List copiedTargets = new ArrayList<>(5); + for (Message m : copy) { + Name target = null; + for (RRset s : m.getSectionRRsets(Section.ANSWER)) { + if (s.getType() == Type.CNAME) { + target = ((CNAMERecord) s.first()).getTarget(); + } else if (s.getType() == Type.DNAME) { + target = ((DNAMERecord) s.first()).getTarget(); } - if (a == null) { - a = get(target, Type.DNAME); - } + while (target != null) { + Message a = get(target, m.getQuestion().getType()); + if (a == null) { + a = get(target, Type.CNAME); + } - if (a != null) { - target = add(m, a); - if (copiedTargets.contains(target)) { - break; + if (a == null) { + a = get(target, Type.DNAME); } - copiedTargets.add(target); - rpl.replays.remove(a); - } else { - target = null; + if (a != null) { + target = add(m, a); + if (copiedTargets.contains(target)) { + break; + } + + copiedTargets.add(target); + rpl.replays.remove(a); + } else { + target = null; + } } } } - } - // promote any DS records in auth. sections to real queries - copy = new ArrayList<>(rpl.replays.size()); - copy.addAll(rpl.replays); - for (Message m : copy) { - for (RRset s : m.getSectionRRsets(Section.AUTHORITY)) { - if (s.getType() == Type.DS) { - Message ds = new Message(); - ds.addRecord(Record.newRecord(s.getName(), s.getType(), s.getDClass()), Section.QUESTION); - for (Record rr : s.rrs()) { - ds.addRecord(rr, Section.ANSWER); - } + // promote any DS records in auth. sections to real queries + copy = new ArrayList<>(rpl.replays.size()); + copy.addAll(rpl.replays); + for (Message m : copy) { + for (RRset s : m.getSectionRRsets(Section.AUTHORITY)) { + if (s.getType() == Type.DS) { + Message ds = new Message(); + ds.addRecord( + Record.newRecord(s.getName(), s.getType(), s.getDClass()), Section.QUESTION); + for (Record rr : s.rrs()) { + ds.addRecord(rr, Section.ANSWER); + } - for (RRSIGRecord sig : s.sigs()) { - ds.addRecord(sig, Section.ANSWER); - } + for (RRSIGRecord sig : s.sigs()) { + ds.addRecord(sig, Section.ANSWER); + } - rpl.replays.add(ds); + rpl.replays.add(ds); + } } } - } - - clear(); - for (Message m : rpl.replays) { - add(m); - } - if (rpl.date != null) { - try { - when(resolverClock.instant()).thenReturn(rpl.date); - } catch (Exception e) { - throw new RuntimeException(e); + clear(); + for (Message m : rpl.replays) { + add(m); } - } - if (rpl.trustAnchors != null) { - resolver.getTrustAnchors().clear(); - for (SRRset rrset : rpl.trustAnchors) { - resolver.getTrustAnchors().store(rrset); + if (rpl.date != null) { + try { + when(resolverClock.instant()).thenReturn(rpl.date); + } catch (Exception e) { + throw new RuntimeException(e); + } } - } - resolver.init(config); + if (rpl.trustAnchors != null) { + resolver.getTrustAnchors().clear(); + for (SRRset rrset : rpl.trustAnchors) { + resolver.getTrustAnchors().store(rrset); + } + } - for (Check c : rpl.checks.values()) { - Message s = resolver.send(c.query); + resolver.init(config); + + for (Check c : rpl.checks.values()) { + Message s = resolver.send(c.query).normalize(c.query, true); + log.trace( + "{}/{}/{} ---> \n{}", + c.query.getQuestion().getName(), + Type.string(c.query.getQuestion().getType()), + DClass.string(c.query.getQuestion().getDClass()), + s); + assertEquals( + c.response.getHeader().getFlag(Flags.AD), + s.getHeader().getFlag(Flags.AD), + "AD Flag must match"); + assertEquals( + Rcode.string(c.response.getRcode()), Rcode.string(s.getRcode()), "RCode must match"); + } + } finally { Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); - assertEquals( - c.response.getHeader().getFlag(Flags.AD), - s.getHeader().getFlag(Flags.AD), - "AD Flag must match"); - assertEquals( - Rcode.string(c.response.getRcode()), Rcode.string(s.getRcode()), "RCode must match"); + System.clearProperty("dnsjava.harden_unknown_additional"); } } @@ -182,7 +203,7 @@ class UnboundTests extends TestBase { return next; } - static void xmain(String[] xargs) { + static void main(String[] xargs) throws IOException, ParseException { Map ignored = new HashMap() { { @@ -207,8 +228,12 @@ class UnboundTests extends TestBase { put("val_cnametoinsecure.rpl", "incomplete CNAME answer"); put("val_nsec3_optout_cache.rpl", "more cache stuff"); put("val_unsecds_qtypeds.rpl", "tests the iterative resolver"); - put("val_anchor_nx.rpl", "tests caching of NX from a parent resolver"); - put("val_anchor_nx_nosig.rpl", "tests caching of NX from a parent resolver"); + put( + "val_anchor_nx.rpl", + "tests resolving conflicting responses in a recursive resolver"); + put( + "val_anchor_nx_nosig.rpl", + "tests resolving conflicting responses in a recursive resolver"); put("val_negcache_nta.rpl", "tests unbound option domain-insecure, not available here"); } }; @@ -219,7 +244,9 @@ class UnboundTests extends TestBase { System.out.println(" @Disabled(\"" + comment + "\")"); } + Rpl rpl = new RplParser(new FileInputStream("./src/test/resources/unbound/" + f)).parse(); System.out.println(" @Test"); + System.out.println(" @DisplayName(\"" + f + ": " + rpl.scenario + "\")"); System.out.println( " void " + f.split("\\.")[0] + "() throws ParseException, IOException {"); System.out.println(" runUnboundTest();"); @@ -229,798 +256,1010 @@ class UnboundTests extends TestBase { } @Test + @DisplayName("val_adbit.rpl: Test validator AD bit signaling") void val_adbit() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_adcopy.rpl: Test validator AD bit sent by untrusted upstream") void val_adcopy() throws ParseException, IOException { runUnboundTest(); } - @Disabled("tests caching of NX from a parent resolver") + @Disabled("tests resolving conflicting responses in a recursive resolver") @Test + @DisplayName("val_anchor_nx.rpl: Test validator with secure proof of trust anchor nxdomain") void val_anchor_nx() throws ParseException, IOException { runUnboundTest(); } - @Disabled("tests caching of NX from a parent resolver") + @Disabled("tests resolving conflicting responses in a recursive resolver") @Test + @DisplayName("val_anchor_nx_nosig.rpl: Test validator with unsigned denial of trust anchor") void val_anchor_nx_nosig() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ans_dsent.rpl: Test validator with empty nonterminals on the trust chain.") void val_ans_dsent() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ans_nx.rpl: Test validator with DS nodata as nxdomain on trust chain") void val_ans_nx() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_any.rpl: Test validator with response to qtype ANY") void val_any() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_any_cname.rpl: Test validator with response to qtype ANY that includes CNAME") void val_any_cname() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_any_dname.rpl: Test validator with response to qtype ANY that includes DNAME") void val_any_dname() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnameinsectopos.rpl: Test validator with an insecure cname to positive cached") void val_cnameinsectopos() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_cnamenx_dblnsec.rpl: Test validator with cname-nxdomain for duplicate NSEC detection") void val_cnamenx_dblnsec() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnamenx_rcodenx.rpl: Test validator with cname-nxdomain with rcode nxdomain") void val_cnamenx_rcodenx() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnameqtype.rpl: Test validator with a query for type cname") void val_cnameqtype() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnametocloser.rpl: Test validator with CNAME to closer anchor under optout.") void val_cnametocloser() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_cnametocloser_nosig.rpl: Test validator with CNAME to closer anchor optout missing sigs.") void val_cnametocloser_nosig() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_cnametocnamewctoposwc.rpl: Test validator with a regular cname to wildcard cname to wildcard response") void val_cnametocnamewctoposwc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnametodname.rpl: Test validator with a cname to a dname") void val_cnametodname() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_cnametodnametocnametopos.rpl: Test validator with cname, dname, cname, positive answer") void val_cnametodnametocnametopos() throws ParseException, IOException { runUnboundTest(); } @Disabled("incomplete CNAME answer") @Test + @DisplayName("val_cnametoinsecure.rpl: Test validator with CNAME to insecure NSEC or NSEC3.") void val_cnametoinsecure() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnametonodata.rpl: Test validator with cname to nodata") void val_cnametonodata() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnametonodata_nonsec.rpl: Test validator with cname to nodata") void val_cnametonodata_nonsec() throws ParseException, IOException { runUnboundTest(); } @Disabled("incomplete CNAME answer") @Test + @DisplayName("val_cnametonsec.rpl: Test validator with CNAME to insecure NSEC delegation") void val_cnametonsec() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnametonx.rpl: Test validator with cname to nxdomain") void val_cnametonx() throws ParseException, IOException { runUnboundTest(); } @Disabled("incomplete CNAME answer") @Test + @DisplayName("val_cnametooptin.rpl: Test validator with CNAME to insecure optin NSEC3") void val_cnametooptin() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnametooptout.rpl: Test validator with CNAME to optout NSEC3 span NODATA") void val_cnametooptout() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnametopos.rpl: Test validator with a cname to positive") void val_cnametopos() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_cnametoposnowc.rpl: Test validator with a cname to positive wildcard without proof") void val_cnametoposnowc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnametoposwc.rpl: Test validator with a cname to positive wildcard") void val_cnametoposwc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnamewctonodata.rpl: Test validator with wildcard cname to nodata") void val_cnamewctonodata() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnamewctonx.rpl: Test validator with wildcard cname to nxdomain") void val_cnamewctonx() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cnamewctoposwc.rpl: Test validator with wildcard cname to positive wildcard") void val_cnamewctoposwc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cname_loop1.rpl: Test validator with cname loop") void val_cname_loop1() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cname_loop2.rpl: Test validator with cname 2 step loop") void val_cname_loop2() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_cname_loop3.rpl: Test validator with cname 3 step loop") void val_cname_loop3() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_deleg_nons.rpl: Test validator with unsigned delegation with no NS bit in NSEC") + void val_deleg_nons() throws ParseException, IOException { + runUnboundTest(); + } + + @Test + @DisplayName("val_dnametoolong.rpl: Test validator with a dname too long response") void val_dnametoolong() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_dnametopos.rpl: Test validator with a dname to positive") void val_dnametopos() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_dnametoposwc.rpl: Test validator with a dname to positive wildcard") void val_dnametoposwc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_dnamewc.rpl: Test validator with a wildcarded dname") void val_dnamewc() throws ParseException, IOException { runUnboundTest(); } @Disabled("we don't do negative caching") @Test + @DisplayName("val_dsnsec.rpl: Test pickup of DS NSEC from the cache.") void val_dsnsec() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ds_afterprime.rpl: Test DS lookup after key prime is done.") void val_ds_afterprime() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ds_cname.rpl: Test validator with CNAME response to DS") void val_ds_cname() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ds_cnamesub.rpl: Test validator with CNAME response to DS in chain of trust") void val_ds_cnamesub() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_ds_cnamesubbogus.rpl: Test validator with bogus CNAME response to DS in chain of trust") void val_ds_cnamesubbogus() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ds_gost.rpl: Test validator with GOST DS digest") void val_ds_gost() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ds_gost_downgrade.rpl: Test validator with GOST DS digest downgrade attack") void val_ds_gost_downgrade() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ds_sha2.rpl: Test validator with SHA256 DS digest") void val_ds_sha2() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ds_sha2_downgrade.rpl: Test validator with SHA256 DS downgrade to SHA1") void val_ds_sha2_downgrade() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_ds_sha2_downgrade_override.rpl: Test validator with SHA256 DS downgrade to SHA1") void val_ds_sha2_downgrade_override() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ds_sha2_lenient.rpl: Test validator with SHA256 DS downgrade to SHA1 lenience") void val_ds_sha2_lenient() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_entds.rpl: Test validator with lots of ENTs in the chain of trust") void val_entds() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_faildnskey.rpl: Test validator with failed DNSKEY request") void val_faildnskey() throws ParseException, IOException { runUnboundTest(); } @Disabled("tests an unbound specific config option") @Test + @DisplayName( + "val_faildnskey_ok.rpl: Test validator with failed DNSKEY request, but not hardened.") void val_faildnskey_ok() throws ParseException, IOException { runUnboundTest(); } @Disabled("irrelevant, we're not a recursive resolver") @Test + @DisplayName("val_fwdds.rpl: Test forward-zone with DS query") void val_fwdds() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_keyprefetch.rpl: Test validator with key prefetch") void val_keyprefetch() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_keyprefetch_verify.rpl: Test validator with key prefetch and verify with the anchor") void val_keyprefetch_verify() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_mal_wc.rpl: Test validator with nodata, wildcards and ENT") void val_mal_wc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_negcache_ds.rpl: Test validator with negative cache DS response") void val_negcache_ds() throws ParseException, IOException { runUnboundTest(); } @Disabled("we don't do negative caching") @Test + @DisplayName( + "val_negcache_dssoa.rpl: Test validator with negative cache DS response with cached SOA") void val_negcache_dssoa() throws ParseException, IOException { runUnboundTest(); } @Disabled("aggressive NSEC is not supported") @Test + @DisplayName( + "val_negcache_nodata.rpl: Test validator with negative cache NXDOMAIN response (aggressive NSEC)") void val_negcache_nodata() throws ParseException, IOException { runUnboundTest(); } @Disabled("tests unbound option domain-insecure, not available here") @Test + @DisplayName("val_negcache_nta.rpl: Test to not do aggressive NSEC for domains under NTA") void val_negcache_nta() throws ParseException, IOException { runUnboundTest(); } @Disabled("aggressive NSEC is not supported") @Test + @DisplayName( + "val_negcache_nxdomain.rpl: Test validator with negative cache NXDOMAIN response (aggressive NSEC)") void val_negcache_nxdomain() throws ParseException, IOException { runUnboundTest(); } @Disabled("irrelevant - if we wouldn't want AD, we wouldn't be using this stuff") @Test + @DisplayName("val_noadwhennodo.rpl: Test if AD bit is returned on non-DO query.") void val_noadwhennodo() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nodata.rpl: Test validator with nodata response") void val_nodata() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nodatawc.rpl: Test validator with wildcard nodata response") void val_nodatawc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nodatawc_badce.rpl: Test validator with wildcard nodata, bad closest encloser") void val_nodatawc_badce() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nodatawc_nodeny.rpl: Test validator with wildcard nodata response without qdenial") void val_nodatawc_nodeny() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nodatawc_one.rpl: Test validator with wildcard nodata response with one NSEC") void val_nodatawc_one() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nodatawc_wcns.rpl: Test validator with wildcard nodata response from parent zone with SOA") void val_nodatawc_wcns() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nodatawc_wrongdeleg.rpl: Test validator with wildcard nodata response from parent zone") void val_nodatawc_wrongdeleg() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nodata_ent.rpl: Test validator with nodata on empty nonterminal response") void val_nodata_ent() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nodata_entnx.rpl: Test validator with nodata on empty nonterminal response with rcode NXDOMAIN") void val_nodata_entnx() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nodata_entwc.rpl: Test validator with wildcard nodata on empty nonterminal response") void val_nodata_entwc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nodata_failsig.rpl: Test validator with nodata response with bogus RRSIG") void val_nodata_failsig() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nodata_failwc.rpl: Test validator with nodata response with wildcard expanded NSEC record, original NSEC owner does not provide proof for QNAME. CVE-2017-15105 test.") void val_nodata_failwc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nodata_hasdata.rpl: Test validator with nodata response, that proves the data.") void val_nodata_hasdata() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nodata_zonecut.rpl: Test validator with nodata response from wrong side of zonecut") void val_nodata_zonecut() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nokeyprime.rpl: Test validator with failed key prime, no keys.") void val_nokeyprime() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_b1_nameerror.rpl: Test validator NSEC3 B.1 name error.") void val_nsec3_b1_nameerror() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b1_nameerror_noce.rpl: Test validator NSEC3 B.1 name error without ce NSEC3.") void val_nsec3_b1_nameerror_noce() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b1_nameerror_nonc.rpl: Test validator NSEC3 B.1 name error without nc NSEC3.") void val_nsec3_b1_nameerror_nonc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b1_nameerror_nowc.rpl: Test validator NSEC3 B.1 name error without wc NSEC3.") void val_nsec3_b1_nameerror_nowc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_b21_nodataent.rpl: Test validator NSEC3 B.2.1 no data empty nonterminal.") void val_nsec3_b21_nodataent() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b21_nodataent_wr.rpl: Test validator NSEC3 B.2.1 no data empty nonterminal, wrong rr.") void val_nsec3_b21_nodataent_wr() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_b2_nodata.rpl: Test validator NSEC3 B.2 no data.") void val_nsec3_b2_nodata() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_b2_nodata_nons.rpl: Test validator NSEC3 B.2 no data, without NSEC3.") void val_nsec3_b2_nodata_nons() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b3_optout.rpl: Test validator NSEC3 B.3 referral to optout unsigned zone.") void val_nsec3_b3_optout() throws ParseException, IOException { runUnboundTest(); } @Disabled("we don't do negative caching") @Test + @DisplayName( + "val_nsec3_b3_optout_negcache.rpl: Test validator NSEC3 B.3 referral optout with negative cache.") void val_nsec3_b3_optout_negcache() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b3_optout_noce.rpl: Test validator NSEC3 B.3 optout unsigned, without ce.") void val_nsec3_b3_optout_noce() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b3_optout_nonc.rpl: Test validator NSEC3 B.3 optout unsigned, without nc.") void val_nsec3_b3_optout_nonc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_b4_wild.rpl: Test validator NSEC3 B.4 wildcard expansion.") void val_nsec3_b4_wild() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b4_wild_wr.rpl: Test validator NSEC3 B.4 wildcard expansion, wrong NSEC3.") void val_nsec3_b4_wild_wr() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_b5_wcnodata.rpl: Test validator NSEC3 B.5 wildcard nodata.") void val_nsec3_b5_wcnodata() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b5_wcnodata_noce.rpl: Test validator NSEC3 B.5 wildcard nodata, without ce.") void val_nsec3_b5_wcnodata_noce() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b5_wcnodata_nonc.rpl: Test validator NSEC3 B.5 wildcard nodata, without nc.") void val_nsec3_b5_wcnodata_nonc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_b5_wcnodata_nowc.rpl: Test validator NSEC3 B.5 wildcard nodata, without wc.") void val_nsec3_b5_wcnodata_nowc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_cnametocnamewctoposwc.rpl: Test validator with a regular cname to wildcard cname to wildcard response") void val_nsec3_cnametocnamewctoposwc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_cname_ds.rpl: Test validator with NSEC3 CNAME for qtype DS.") void val_nsec3_cname_ds() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_cname_par.rpl: Test validator with NSEC3 wildcard CNAME to parent.") void val_nsec3_cname_par() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_cname_sub.rpl: Test validator with NSEC3 wildcard CNAME to subzone.") void val_nsec3_cname_sub() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_entnodata_optout.rpl: Test validator with NSEC3 response for NODATA ENT with optout.") void val_nsec3_entnodata_optout() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_entnodata_optout_badopt.rpl: Test validator with NSEC3 response for NODATA ENT with optout.") void val_nsec3_entnodata_optout_badopt() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_entnodata_optout_match.rpl: Test validator NODATA ENT with nsec3 optout matches the ent.") void val_nsec3_entnodata_optout_match() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_iter_high.rpl: Test validator with nxdomain NSEC3 with too high iterations") void val_nsec3_iter_high() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_nodatawccname.rpl: Test validator with nodata NSEC3 abused wildcarded CNAME.") void val_nsec3_nodatawccname() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_nods.rpl: Test validator with NSEC3 with no DS referral.") void val_nsec3_nods() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_nods_badopt.rpl: Test validator with NSEC3 with no DS with wrong optout bit.") void val_nsec3_nods_badopt() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_nods_badsig.rpl: Test validator with NSEC3 with no DS referral with bad signature.") void val_nsec3_nods_badsig() throws ParseException, IOException { runUnboundTest(); } @Disabled("we don't do negative caching") @Test + @DisplayName( + "val_nsec3_nods_negcache.rpl: Test validator with NSEC3 with no DS referral from neg cache.") void val_nsec3_nods_negcache() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_nods_soa.rpl: Test validator with NSEC3 with no DS referral abuse of apex.") void val_nsec3_nods_soa() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_optout_ad.rpl: Test validator with optout NSEC3 response that gets no AD.") void val_nsec3_optout_ad() throws ParseException, IOException { runUnboundTest(); } @Disabled("more cache stuff") @Test + @DisplayName( + "val_nsec3_optout_cache.rpl: Test validator with NSEC3 span change and cache effects.") void val_nsec3_optout_cache() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nsec3_wcany.rpl: Test validator with NSEC3 wildcard qtype ANY response.") void val_nsec3_wcany() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nsec3_wcany_nodeny.rpl: Test validator with NSEC3 wildcard qtype ANY without denial.") void val_nsec3_wcany_nodeny() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nx.rpl: Test validator with nxdomain response") void val_nx() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nx_failwc.rpl: Test validator with nxdomain response with wildcard expanded NSEC record, original NSEC owner does not provide proof for QNAME. CVE-2017-15105 test.") void val_nx_failwc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nx_nodeny.rpl: Test validator with nxdomain response missing qname denial") void val_nx_nodeny() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nx_nowc.rpl: Test validator with nxdomain response missing wildcard denial") void val_nx_nowc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nx_nsec3_collision.rpl: Test validator with nxdomain NSEC3 with a collision.") void val_nx_nsec3_collision() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nx_nsec3_collision2.rpl: Test validator with nxdomain NSEC3 with a salt mismatch.") void val_nx_nsec3_collision2() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nx_nsec3_collision3.rpl: Test validator with nxdomain NSEC3 with a collision.") void val_nx_nsec3_collision3() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nx_nsec3_collision4.rpl: Test validator with nxdomain NSEC3 with a collision.") void val_nx_nsec3_collision4() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nx_nsec3_hashalg.rpl: Test validator with unknown NSEC3 hash algorithm.") void val_nx_nsec3_hashalg() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_nx_nsec3_nsecmix.rpl: Test validator with NSEC3 responses that has an NSEC mixed in.") void val_nx_nsec3_nsecmix() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nx_nsec3_params.rpl: Test validator with nxdomain NSEC3 several parameters.") void val_nx_nsec3_params() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_nx_overreach.rpl: Test validator with overreaching NSEC record") void val_nx_overreach() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_positive.rpl: Test validator with positive response") void val_positive() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_positive_nosigs.rpl: Test validator with positive response, signatures removed.") void val_positive_nosigs() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_positive_wc.rpl: Test validator with positive wildcard response") void val_positive_wc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_positive_wc_nodeny.rpl: Test validator with positive wildcard without qname denial") void val_positive_wc_nodeny() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_pos_truncns.rpl: Test validator with badly truncated positive response") void val_pos_truncns() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_qds_badanc.rpl: Test validator with DS query and a bad anchor") void val_qds_badanc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_qds_oneanc.rpl: Test validator with DS query and one anchor") void val_qds_oneanc() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_qds_twoanc.rpl: Test validator with DS query and two anchors") void val_qds_twoanc() throws ParseException, IOException { runUnboundTest(); } @Disabled("NSEC records missing for validation, tests caching stuff") @Test + @DisplayName("val_referd.rpl: Test validator with cache referral") void val_referd() throws ParseException, IOException { runUnboundTest(); } @Disabled("we don't do negative caching") @Test + @DisplayName("val_referglue.rpl: Test validator with cache referral with unsigned glue") void val_referglue() throws ParseException, IOException { runUnboundTest(); } @Disabled("we don't do negative caching") @Test + @DisplayName("val_refer_unsignadd.rpl: Test validator with a referral with unsigned additional") void val_refer_unsignadd() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_rrsig.rpl: Test validator with qtype RRSIG response") void val_rrsig() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_secds.rpl: Test validator with secure delegation") void val_secds() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_secds_nosig.rpl: Test validator with no signatures after secure delegation") void val_secds_nosig() throws ParseException, IOException { runUnboundTest(); } - @Disabled("tests unbound specific config (stub zones)") @Test - void val_stubds() throws ParseException, IOException { + @DisplayName("val_spurious_ns.rpl: Test validator with spurious unsigned NS in auth section") + void val_spurious_ns() throws ParseException, IOException { runUnboundTest(); } + @Disabled("tests unbound specific config (stub zones)") @Test - void val_spurious_ns() throws ParseException, IOException { + @DisplayName("val_stubds.rpl: Test stub with DS query") + void val_stubds() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_stub_noroot.rpl: Test validation of stub zone without root prime.") void val_stub_noroot() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ta_algo_dnskey.rpl: Test validator with multiple algorithm trust anchor") void val_ta_algo_dnskey() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName( + "val_ta_algo_dnskey_dp.rpl: Test validator with multiple algorithm trust anchor without harden") void val_ta_algo_dnskey_dp() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ta_algo_missing.rpl: Test validator with multiple algorithm missing one") void val_ta_algo_missing() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_ta_algo_missing_dp.rpl: Test validator with multiple algorithm missing one") void val_ta_algo_missing_dp() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_twocname.rpl: Test validator with unsigned CNAME to signed CNAME to data") void val_twocname() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_unalgo_anchor.rpl: Test validator with unsupported algorithm trust anchor") void val_unalgo_anchor() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_unalgo_dlv.rpl: Test validator with unknown algorithm DLV anchor") void val_unalgo_dlv() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_unalgo_ds.rpl: Test validator with unknown algorithm delegation") void val_unalgo_ds() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_unsecds.rpl: Test validator with insecure delegation") void val_unsecds() throws ParseException, IOException { runUnboundTest(); } @Disabled("we don't do negative caching") @Test + @DisplayName( + "val_unsecds_negcache.rpl: Test validator with insecure delegation and DS negative cache") void val_unsecds_negcache() throws ParseException, IOException { runUnboundTest(); } @Disabled("tests the iterative resolver") @Test + @DisplayName("val_unsecds_qtypeds.rpl: Test validator with insecure delegation and qtype DS.") void val_unsecds_qtypeds() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_unsec_cname.rpl: Test validator with DS, unsec, cname sequence.") void val_unsec_cname() throws ParseException, IOException { runUnboundTest(); } @Test + @DisplayName("val_wild_pos.rpl: Test validator with direct wildcard positive response") void val_wild_pos() throws ParseException, IOException { runUnboundTest(); } diff --git a/src/test/java/org/xbill/DNS/lookup/LookupResultTest.java b/src/test/java/org/xbill/DNS/lookup/LookupResultTest.java index 2ce793d..b571f27 100644 --- a/src/test/java/org/xbill/DNS/lookup/LookupResultTest.java +++ b/src/test/java/org/xbill/DNS/lookup/LookupResultTest.java @@ -3,34 +3,101 @@ package org.xbill.DNS.lookup; import static java.util.Collections.singletonList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import java.net.InetAddress; +import java.util.Collections; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.xbill.DNS.ARecord; +import org.xbill.DNS.CNAMERecord; import org.xbill.DNS.DClass; import org.xbill.DNS.Name; import org.xbill.DNS.Record; class LookupResultTest { + private static final LookupResult PREVIOUS = new LookupResult(false); + private static final ARecord A_RECORD = + new ARecord(Name.fromConstantString("a."), DClass.IN, 0, InetAddress.getLoopbackAddress()); + @Test void ctor_nullRecords() { - assertThrows(NullPointerException.class, () -> new LookupResult(null, null)); + assertThrows( + NullPointerException.class, + () -> new LookupResult(PREVIOUS, null, null, false, null, Collections.emptyList())); + } + + @Test + void ctor_nullAliases() { + assertThrows( + NullPointerException.class, + () -> new LookupResult(PREVIOUS, null, null, false, Collections.emptyList(), null)); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void ctor_authOnly(boolean isAuthenticated) { + LookupResult lookupResult = new LookupResult(isAuthenticated); + assertEquals(isAuthenticated, lookupResult.isAuthenticated()); + assertEquals(0, lookupResult.getAliases().size()); + assertEquals(0, lookupResult.getRecords().size()); + assertEquals(0, lookupResult.getQueryResponsePairs().size()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void ctor_singleRecord(boolean isAuthenticated) { + LookupResult lookupResult = new LookupResult(A_RECORD, isAuthenticated, A_RECORD); + assertEquals(isAuthenticated, lookupResult.isAuthenticated()); + assertEquals(0, lookupResult.getAliases().size()); + assertEquals(1, lookupResult.getRecords().size()); + assertEquals(1, lookupResult.getQueryResponsePairs().size()); + assertNull(lookupResult.getQueryResponsePairs().get(A_RECORD)); } @Test void getResult() { - Record record = - new ARecord(Name.fromConstantString("a."), DClass.IN, 0, InetAddress.getLoopbackAddress()); - LookupResult lookupResult = new LookupResult(singletonList(record), null); - assertEquals(singletonList(record), lookupResult.getRecords()); + LookupResult lookupResult = + new LookupResult( + PREVIOUS, null, null, false, singletonList(A_RECORD), Collections.emptyList()); + assertEquals(singletonList(A_RECORD), lookupResult.getRecords()); } @Test void getAliases() { Name name = Name.fromConstantString("b."); Record record = new ARecord(name, DClass.IN, 0, InetAddress.getLoopbackAddress()); - LookupResult lookupResult = new LookupResult(singletonList(record), singletonList(name)); + LookupResult lookupResult = + new LookupResult(PREVIOUS, null, null, false, singletonList(record), singletonList(name)); assertEquals(singletonList(name), lookupResult.getAliases()); } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void isAuthenticated(boolean isAuthenticated) { + LookupResult lookupResult = + new LookupResult( + new LookupResult(isAuthenticated), + null, + null, + isAuthenticated, + singletonList(A_RECORD), + Collections.emptyList()); + assertEquals(isAuthenticated, lookupResult.isAuthenticated()); + } + + @ParameterizedTest + @ValueSource(booleans = {false, true}) + void isAuthenticatedRequiresAllForTrue(boolean isAuthenticated) { + Name nameA = Name.fromConstantString("a."); + Name nameB = Name.fromConstantString("b."); + Record cname = new CNAMERecord(nameA, DClass.IN, 0, nameB); + Record a = new ARecord(nameB, DClass.IN, 0, InetAddress.getLoopbackAddress()); + LookupResult lookupResult1 = new LookupResult(isAuthenticated); + LookupResult lookupResult2 = + new LookupResult(lookupResult1, cname, null, true, singletonList(a), singletonList(nameA)); + assertEquals(isAuthenticated, lookupResult2.isAuthenticated()); + } } diff --git a/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java b/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java index a24c8d9..ef65d27 100644 --- a/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java +++ b/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java @@ -5,12 +5,19 @@ import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.emptyList; import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junitpioneer.jupiter.cartesian.CartesianTest.Enum; +import static org.junitpioneer.jupiter.cartesian.CartesianTest.Values; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -20,9 +27,11 @@ import static org.xbill.DNS.LookupTest.DUMMY_NAME; import static org.xbill.DNS.LookupTest.LONG_LABEL; import static org.xbill.DNS.LookupTest.answer; import static org.xbill.DNS.LookupTest.fail; +import static org.xbill.DNS.LookupTest.multiAnswer; import static org.xbill.DNS.Type.A; import static org.xbill.DNS.Type.AAAA; import static org.xbill.DNS.Type.CNAME; +import static org.xbill.DNS.Type.DNAME; import static org.xbill.DNS.Type.MX; import java.io.IOException; @@ -44,17 +53,19 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; -import org.junit.jupiter.api.function.Executable; import org.junit.jupiter.api.io.TempDir; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.EnumSource; import org.junit.jupiter.params.provider.ValueSource; +import org.junitpioneer.jupiter.cartesian.CartesianTest; import org.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.xbill.DNS.*; import org.xbill.DNS.Record; +import org.xbill.DNS.WireParseException; import org.xbill.DNS.hosts.HostsFileParser; @ExtendWith(MockitoExtension.class) @@ -65,7 +76,9 @@ class LookupSessionTest { private static final ARecord LOOPBACK_A = new ARecord(DUMMY_NAME, IN, 3600, InetAddress.getLoopbackAddress()); + private static final ARecord EXAMPLE_A = (ARecord) LOOPBACK_A.withName(name("example.com.")); private static final AAAARecord LOOPBACK_AAAA; + private static final String INVALID_SERVER_RESPONSE_MESSAGE = "refusing to return it"; private HostsFileParser lookupSessionTestHostsFileParser; static { @@ -109,6 +122,31 @@ class LookupSessionTest { verify(mockResolver).sendAsync(any(), any(Executor.class)); } + @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") + void lookupAsync_absoluteQueryNoExtra( + @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) + throws ExecutionException, InterruptedException { + wireUpMockResolver( + mockResolver, query -> multiAnswer(query, name -> new Record[] {LOOPBACK_A, EXAMPLE_A})); + + LookupSession lookupSession = lookupSession(useCache).irrelevantRecordMode(mode).build(); + CompletableFuture future = + lookupSession.lookupAsync(name("a.b."), A, IN).toCompletableFuture(); + if (mode == IrrelevantRecordMode.THROW) { + assertThatThrownBy(future::get) + .cause() + .isInstanceOf(LookupFailedException.class) + .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); + } else { + LookupResult result = future.get(); + assertThat(result.getAliases()).isEmpty(); + assertThat(result.getRecords()).containsExactly(LOOPBACK_A.withName(name("a.b."))); + } + + assertCacheUnused(useCache, mode, lookupSession); + verify(mockResolver).sendAsync(any(), any(Executor.class)); + } + @Test void lookupAsync_absoluteQuery_defaultClass() throws InterruptedException, ExecutionException { wireUpMockResolver(mockResolver, query -> answer(query, name -> LOOPBACK_A)); @@ -145,10 +183,14 @@ class LookupSessionTest { when(mockHosts.getAddressForHost(any(), anyInt())).thenThrow(IOException.class); LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).hostsFileParser(mockHosts).build(); - CompletionStage resultFuture = - lookupSession.lookupAsync(name("kubernetes.docker.internal."), A, IN); - assertThrowsCause(NoSuchDomainException.class, () -> resultFuture.toCompletableFuture().get()); + assertThatThrownBy( + lookupSession + .lookupAsync(name("kubernetes.docker.internal."), A, IN) + .toCompletableFuture() + ::get) + .cause() + .isInstanceOf(NoSuchDomainException.class); } @Test @@ -159,10 +201,14 @@ class LookupSessionTest { .resolver(mockResolver) .hostsFileParser(lookupSessionTestHostsFileParser) .build(); - CompletionStage resultFuture = - lookupSession.lookupAsync(name("kubernetes.docker.internal."), MX, IN); - assertThrowsCause(NoSuchDomainException.class, () -> resultFuture.toCompletableFuture().get()); + assertThatThrownBy( + lookupSession + .lookupAsync(name("kubernetes.docker.internal."), MX, IN) + .toCompletableFuture() + ::get) + .cause() + .isInstanceOf(NoSuchDomainException.class); verify(mockResolver).sendAsync(any(), any(Executor.class)); } @@ -340,11 +386,12 @@ class LookupSessionTest { when(mockCache.lookupRecords(name("host.tld."), A, Credibility.NORMAL)) .thenReturn(mock(SetResponse.class)); - SetResponse second = mock(SetResponse.class); - when(second.isSuccessful()).thenReturn(true); - when(second.answers()) + SetResponse anotherTldResponse = mock(SetResponse.class); + when(anotherTldResponse.isSuccessful()).thenReturn(true); + when(anotherTldResponse.answers()) .thenReturn(singletonList(new RRset(LOOPBACK_A.withName(name("another.tld."))))); - when(mockCache.lookupRecords(name("another.tld."), A, Credibility.NORMAL)).thenReturn(second); + when(mockCache.lookupRecords(name("another.tld."), A, Credibility.NORMAL)) + .thenReturn(anotherTldResponse); LookupSession lookupSession = LookupSession.builder() @@ -417,11 +464,7 @@ class LookupSessionTest { }; wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); - LookupSession lookupSession = - useCache - ? LookupSession.builder().cache(new Cache()).resolver(mockResolver).build() - : LookupSession.builder().resolver(mockResolver).build(); - + LookupSession lookupSession = lookupSession(useCache).build(); CompletionStage resultFuture = lookupSession.lookupAsync(name("cname.a."), A, IN); LookupResult result = resultFuture.toCompletableFuture().get(); @@ -429,17 +472,17 @@ class LookupSessionTest { assertEquals( Stream.of(name("cname.a."), name("cname.b.")).collect(Collectors.toList()), result.getAliases()); + if (useCache) { + assertEquals(3, lookupSession.getCache(IN).getSize()); + } verify(mockResolver, times(3)).sendAsync(any(), any(Executor.class)); } - @ParameterizedTest - @CsvSource({ - "false,false", - "true,false", - "false,true", - "true,true", - }) - void lookupAsync_twoDnameRedirectOneQuery(boolean useCache, boolean includeSyntheticCnames) + @CartesianTest(name = "useCache={0}, includeSyntheticCnames={1}, irrelevantRecordMode={2}") + void lookupAsync_twoDnameRedirectOneQuery( + @Values(booleans = {true, false}) boolean useCache, + @Values(booleans = {true, false}) boolean includeSyntheticCnames, + @Enum IrrelevantRecordMode mode) throws Exception { wireUpMockResolver( mockResolver, @@ -459,11 +502,7 @@ class LookupSessionTest { return answer; }); - LookupSession lookupSession = - useCache - ? LookupSession.builder().cache(new Cache()).resolver(mockResolver).build() - : LookupSession.builder().resolver(mockResolver).build(); - + LookupSession lookupSession = lookupSession(useCache).irrelevantRecordMode(mode).build(); CompletionStage resultFuture = lookupSession.lookupAsync(name("www.example.org."), A, IN); @@ -473,6 +512,9 @@ class LookupSessionTest { Stream.of(name("www.example.org."), name("www.example.net."), name("www.example.com.")) .collect(Collectors.toList()), result.getAliases()); + if (useCache) { + assertEquals(4 + (includeSyntheticCnames ? 2 : 0), lookupSession.getCache(IN).getSize()); + } verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); } @@ -490,11 +532,7 @@ class LookupSessionTest { return answer; }); - LookupSession lookupSession = - useCache - ? LookupSession.builder().cache(new Cache()).resolver(mockResolver).build() - : LookupSession.builder().resolver(mockResolver).build(); - + LookupSession lookupSession = lookupSession(useCache).build(); CompletionStage resultFuture = lookupSession.lookupAsync(name("cname.a."), A, IN); LookupResult result = resultFuture.toCompletableFuture().get(); @@ -530,11 +568,7 @@ class LookupSessionTest { return answer; }); - LookupSession lookupSession = - useCache - ? LookupSession.builder().cache(new Cache()).resolver(mockResolver).build() - : LookupSession.builder().resolver(mockResolver).build(); - + LookupSession lookupSession = lookupSession(useCache).build(); CompletionStage resultFuture = lookupSession.lookupAsync(name("cname.a."), A, IN); LookupResult result = resultFuture.toCompletableFuture().get(); @@ -542,6 +576,9 @@ class LookupSessionTest { assertEquals( Stream.of(name("cname.a."), name("cname.b.")).collect(Collectors.toList()), result.getAliases()); + if (useCache) { + assertEquals(3, lookupSession.getCache(IN).getSize()); + } verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); } @@ -586,37 +623,69 @@ class LookupSessionTest { } }); - LookupSession lookupSession = - useCache - ? LookupSession.builder().cache(new Cache()).resolver(mockResolver).build() - : LookupSession.builder().resolver(mockResolver).build(); - - CompletionStage resultFuture = - lookupSession.lookupAsync(name("cname.r."), Type.value(type), IN); + LookupSession lookupSession = lookupSession(useCache).build(); + CompletableFuture future = + lookupSession.lookupAsync(name("cname.r."), Type.value(type), IN).toCompletableFuture(); - CompletableFuture future = resultFuture.toCompletableFuture(); if (rcode.equals("NXDOMAIN")) { - assertThrowsCause(NoSuchDomainException.class, future::get); + assertThatThrownBy(future::get).cause().isInstanceOf(NoSuchDomainException.class); } else { LookupResult result = future.get(); - assertEquals(0, result.getRecords().size()); + assertThat(result.getRecords()).isEmpty(); } verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); } @Test void lookupAsync_simpleCnameRedirect() throws Exception { + Name cname = name("cname.r."); + Name target = name("a.b."); Function nameToRecord = - name -> name("cname.r.").equals(name) ? cname("cname.r.", "a.b.") : LOOPBACK_A; + name -> cname.equals(name) ? cname(cname, target) : LOOPBACK_A; wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); - CompletionStage resultFuture = lookupSession.lookupAsync(name("cname.r."), A, IN); + CompletionStage resultFuture = lookupSession.lookupAsync(cname, A, IN); LookupResult result = resultFuture.toCompletableFuture().get(); assertEquals(singletonList(LOOPBACK_A.withName(name("a.b."))), result.getRecords()); - assertEquals(singletonList(name("cname.r.")), result.getAliases()); + assertEquals(singletonList(cname), result.getAliases()); + verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); + } + + @ParameterizedTest + @EnumSource(value = IrrelevantRecordMode.class) + void lookupAsync_simpleCnameRedirectNoExtra(IrrelevantRecordMode mode) + throws ExecutionException, InterruptedException { + Name query = name("cname.r."); + Name target = name("a.b."); + Function nameToRecord = + name -> + query.equals(name) + ? new Record[] {cname(query, target)} + : new Record[] { + LOOPBACK_A, EXAMPLE_A, + }; + wireUpMockResolver(mockResolver, q -> multiAnswer(q, nameToRecord)); + + LookupSession lookupSession = + LookupSession.builder().resolver(mockResolver).irrelevantRecordMode(mode).build(); + + CompletableFuture f = + lookupSession.lookupAsync(query, A, IN).toCompletableFuture(); + if (mode == IrrelevantRecordMode.REMOVE) { + LookupResult result = f.get(); + assertThat(result.getRecords()).hasSize(1).containsExactly(LOOPBACK_A.withName(target)); + } else { + assertThatThrownBy(f::get) + .cause() + .isInstanceOf(LookupFailedException.class) + .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE) + .rootCause() + .isInstanceOf(WireParseException.class); + } + verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); } @@ -637,16 +706,95 @@ class LookupSessionTest { verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); } + @Test + void lookupAsync_dnameQuery() throws Exception { + Name query = name("dname.r."); + DNAMERecord response = dname(query, "a.b."); + Function nameToRecord = name -> name.equals(query) ? response : LOOPBACK_A; + wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); + + LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); + + CompletionStage resultFuture = lookupSession.lookupAsync(query, DNAME, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); + assertEquals(singletonList(response), result.getRecords()); + assertEquals(emptyList(), result.getAliases()); + verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); + } + + @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") + void lookupAsync_cnameQueryExtra( + @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) + throws ExecutionException, InterruptedException { + Name query = name("cname.r."); + Name target = name("a.b."); + CNAMERecord response1 = cname(query, target); + CNAMERecord response2 = cname(name("additional.r."), target); + Function nameToRecord = + name -> + query.equals(name) ? new Record[] {response1, response2} : new Record[] {LOOPBACK_A}; + wireUpMockResolver(mockResolver, q -> multiAnswer(q, nameToRecord)); + + LookupSession lookupSession = lookupSession(useCache, mode).build(); + CompletableFuture future = + lookupSession.lookupAsync(query, CNAME, IN).toCompletableFuture(); + if (mode == IrrelevantRecordMode.THROW) { + assertThatThrownBy(future::get) + .cause() + .isInstanceOf(LookupFailedException.class) + .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); + } else { + LookupResult result = future.get(); + assertThat(result.getAliases()).isEmpty(); + assertThat(result.getRecords()).containsExactly(cname(query, target)); + } + + assertCacheUnused(useCache, mode, lookupSession); + verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); + } + + @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") + void lookupAsync_dnameQueryExtra( + @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) + throws ExecutionException, InterruptedException { + Name query = name("cname.r."); + Name target = name("a.b."); + DNAMERecord response1 = dname(query, target); + DNAMERecord response2 = dname(name("additional.r."), target); + Function nameToRecord = + name -> + query.equals(name) ? new Record[] {response1, response2} : new Record[] {LOOPBACK_A}; + wireUpMockResolver(mockResolver, q -> multiAnswer(q, nameToRecord)); + + LookupSession lookupSession = lookupSession(useCache, mode).build(); + CompletableFuture future = + lookupSession.lookupAsync(query, DNAME, IN).toCompletableFuture(); + if (mode == IrrelevantRecordMode.THROW) { + assertThatThrownBy(future::get) + .cause() + .isInstanceOf(LookupFailedException.class) + .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); + } else { + LookupResult result = future.get(); + assertThat(result.getAliases()).isEmpty(); + assertThat(result.getRecords()).containsExactly(response1); + } + + assertCacheUnused(useCache, mode, lookupSession); + verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); + } + @Test void lookupAsync_simpleDnameRedirect() throws Exception { + Name query = name("x.y.to.dname."); Function nameToRecord = - n -> name("x.y.to.dname.").equals(n) ? dname("to.dname.", "to.a.") : LOOPBACK_A; + name -> name.equals(query) ? dname("to.dname.", "to.a.") : LOOPBACK_A; wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); - CompletionStage resultFuture = - lookupSession.lookupAsync(name("x.y.to.dname."), A, IN); + CompletionStage resultFuture = lookupSession.lookupAsync(query, A, IN); LookupResult result = resultFuture.toCompletableFuture().get(); assertEquals(singletonList(LOOPBACK_A.withName(name("x.y.to.a."))), result.getRecords()); @@ -654,23 +802,228 @@ class LookupSessionTest { } @Test - void lookupAsync_redirectLoop() { - Function nameToRecord = - name -> name("a.b.").equals(name) ? cname("a.", "b.") : cname("b.", "a."); - wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); + void lookupAsync_simpleDnameRedirectSynthesizedCname() throws Exception { + Name query = name("x.y.example.org."); + wireUpMockResolver( + mockResolver, + q -> + multiAnswer( + q, + name -> + new Record[] { + dname("example.org.", "example.net."), + cname("x.y.example.org.", "x.y.example.net."), + LOOPBACK_A.withName(name("x.y.example.net.")), + })); - LookupSession lookupSession = - LookupSession.builder().resolver(mockResolver).maxRedirects(2).build(); + LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); - CompletionStage resultFuture = - lookupSession.lookupAsync(name("first.example.com."), A, IN); + CompletionStage resultFuture = lookupSession.lookupAsync(query, A, IN); - assertThrowsCause( - RedirectOverflowException.class, () -> resultFuture.toCompletableFuture().get()); - verify(mockResolver, times(3)).sendAsync(any(), any(Executor.class)); + LookupResult result = resultFuture.toCompletableFuture().get(); + assertEquals(singletonList(LOOPBACK_A.withName(name("x.y.example.net."))), result.getRecords()); + assertEquals(singletonList(name("x.y.example.org.")), result.getAliases()); + verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); } @ParameterizedTest + @CsvSource( + value = { + "x.y.example.com.,x.y.example.org.,REMOVE", + "x.y.example.com.,x.y.example.org.,THROW", + "x.y.example.org.,x.y.example.com.,REMOVE", + "x.y.example.org.,x.y.example.com.,THROW", + }) + void lookupAsync_simpleDnameRedirectWrongSynthesizedCname( + String from, String to, IrrelevantRecordMode mode) + throws ExecutionException, InterruptedException { + Name query = name("x.y.example.org."); + wireUpMockResolver( + mockResolver, + q -> + multiAnswer( + q, + name -> + new Record[] { + // Correct + dname("example.org.", "example.net."), + // Extra and wrong + cname(from, to), + // Correct + LOOPBACK_A.withName(name("x.y.example.net.")), + // Extra and wrong + LOOPBACK_A.withName(name(to)), + })); + + LookupSession lookupSession = + LookupSession.builder().resolver(mockResolver).irrelevantRecordMode(mode).build(); + + CompletableFuture future = + lookupSession.lookupAsync(query, A, IN).toCompletableFuture(); + if (mode == IrrelevantRecordMode.THROW) { + assertThatThrownBy(future::get) + .cause() + .isInstanceOf(LookupFailedException.class) + .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); + } else { + LookupResult result = future.get(); + assertThat(result.getAliases()).containsExactly(name("x.y.example.org.")); + assertThat(result.getRecords()) + .containsExactly(LOOPBACK_A.withName(name("x.y.example.net."))); + } + verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); + } + + @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") + void lookupAsync_simpleDnameRedirectNoExtra( + @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) + throws ExecutionException, InterruptedException { + Name queryName = name("x.y.to.dname."); + wireUpMockResolver( + mockResolver, + question -> + multiAnswer( + question, + name -> + name.equals(queryName) + ? new Record[] {dname("to.dname.", "to.a.")} + : new Record[] { + // LOOPBACK_A will be transformed to 'x.y.to.a.' + LOOPBACK_A, EXAMPLE_A, + })); + + LookupSession lookupSession = lookupSession(useCache, mode).build(); + CompletableFuture future = + lookupSession.lookupAsync(queryName, A, IN).toCompletableFuture(); + if (mode == IrrelevantRecordMode.THROW) { + assertThatThrownBy(future::get) + .cause() + .isInstanceOf(LookupFailedException.class) + .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); + } else { + LookupResult result = future.get(); + assertAll( + () -> { + assertThat(result.getAliases()).containsExactly(name("x.y.to.dname.")); + assertThat(result.getRecords()).containsExactly(LOOPBACK_A.withName(name("x.y.to.a."))); + }); + } + + if (useCache && mode == IrrelevantRecordMode.THROW) { + // Verify that the invalid response didn't end up in the cache + Cache cache = lookupSession.getCache(IN); + verify(cache, times(1)).addMessage(any(Message.class)); + assertEquals(1, cache.getSize()); + assertTrue(cache.lookupRecords(name("example.com."), A, Credibility.NORMAL).isUnknown()); + } + + verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); + } + + @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") + void lookupAsync_simpleCnameWrongInitial( + @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) + throws ExecutionException, InterruptedException { + Name query = name("first.example.com."); + wireUpMockResolver(mockResolver, q -> answer(q, name -> cname("a.", "b."))); + + LookupSession lookupSession = lookupSession(useCache).irrelevantRecordMode(mode).build(); + CompletableFuture future = + lookupSession.lookupAsync(query, A, IN).toCompletableFuture(); + if (mode == IrrelevantRecordMode.THROW) { + assertThatThrownBy(future::get) + .cause() + .isInstanceOf(LookupFailedException.class) + .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); + } else { + LookupResult result = future.get(); + assertThat(result.getAliases()).isEmpty(); + assertThat(result.getRecords()).isEmpty(); + } + + assertCacheUnused(useCache, mode, lookupSession); + + verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); + } + + @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") + void lookupAsync_simpleDnameWrongInitial( + @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) + throws ExecutionException, InterruptedException { + Name query = name("first.example.com."); + wireUpMockResolver(mockResolver, q -> answer(q, name -> dname("a.", "b."))); + + LookupSession lookupSession = + lookupSession(useCache, mode == IrrelevantRecordMode.THROW) + .irrelevantRecordMode(mode) + .build(); + + CompletableFuture future = + lookupSession.lookupAsync(query, A, IN).toCompletableFuture(); + if (mode == IrrelevantRecordMode.THROW) { + assertThatThrownBy(future::get) + .cause() + .isInstanceOf(LookupFailedException.class) + .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); + } else { + LookupResult result = future.get(); + assertThat(result.getAliases()).isEmpty(); + assertThat(result.getRecords()).isEmpty(); + } + + assertCacheUnused(useCache, mode, lookupSession); + verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); + } + + private static void assertCacheUnused( + boolean useCache, IrrelevantRecordMode mode, LookupSession lookupSession) { + if (useCache && mode == IrrelevantRecordMode.THROW) { + // Verify that the invalid response didn't end up in the cache + Cache cache = lookupSession.getCache(IN); + verify(cache, times(0)).addMessage(any(Message.class)); + assertEquals(0, cache.getSize()); + } + } + + @CartesianTest(name = "maxRedirects={0}, irrelevantRecordMode={1}") + void lookupAsync_redirectLoop( + @Values(ints = {3, 4}) int maxRedirects, @Enum IrrelevantRecordMode mode) { + CNAMERecord cnameA = cname("a.", "b."); + CNAMERecord cnameB = cname("b.", "c."); + CNAMERecord cnameC = cname("c.", "d."); + CNAMERecord cnameD = cname("d.", "a."); + Function nameToRecord = + name -> { + if (name.equals(cnameA.getName())) { + return cnameA; + } else if (name.equals(cnameB.getName())) { + return cnameB; + } else if (name.equals(cnameC.getName())) { + return cnameC; + } else if (name.equals(cnameD.getName())) { + return cnameD; + } else { + throw new RuntimeException("Unexpected query"); + } + }; + wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); + LookupSession lookupSession = + LookupSession.builder() + .maxRedirects(maxRedirects) + .resolver(mockResolver) + .irrelevantRecordMode(mode) + .build(); + + Class expected = + maxRedirects == 3 ? RedirectOverflowException.class : RedirectLoopException.class; + assertThatThrownBy( + lookupSession.lookupAsync(cnameA.getName(), A, IN).toCompletableFuture()::get) + .cause() + .isInstanceOf(expected); + verify(mockResolver, times(maxRedirects)).sendAsync(any(), any(Executor.class)); + } + + @ParameterizedTest(name = "maxRedirects={0}") @ValueSource(ints = {3, 4}) void lookupAsync_redirectLoopOneAnswer(int maxRedirects) { wireUpMockResolver( @@ -688,10 +1041,11 @@ class LookupSessionTest { LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).maxRedirects(maxRedirects).build(); - CompletionStage resultFuture = lookupSession.lookupAsync(name("a."), A, IN); - - assertThrowsCause( - RedirectOverflowException.class, () -> resultFuture.toCompletableFuture().get()); + Class expected = + maxRedirects == 3 ? RedirectOverflowException.class : RedirectLoopException.class; + assertThatThrownBy(lookupSession.lookupAsync(name("a."), A, IN).toCompletableFuture()::get) + .cause() + .isInstanceOf(expected); verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); } @@ -703,7 +1057,7 @@ class LookupSessionTest { CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); LookupResult result = resultFuture.toCompletableFuture().get(); - assertEquals(0, result.getRecords().size()); + assertThat(result.getRecords()).isEmpty(); verify(mockResolver).sendAsync(any(), any(Executor.class)); } @@ -712,9 +1066,9 @@ class LookupSessionTest { wireUpMockResolver(mockResolver, q -> fail(q, Rcode.NXDOMAIN)); LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); - CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); - - assertThrowsCause(NoSuchDomainException.class, () -> resultFuture.toCompletableFuture().get()); + assertThatThrownBy(lookupSession.lookupAsync(name("a.b."), A, IN).toCompletableFuture()::get) + .cause() + .isInstanceOf(NoSuchDomainException.class); verify(mockResolver).sendAsync(any(), any(Executor.class)); } @@ -723,9 +1077,9 @@ class LookupSessionTest { wireUpMockResolver(mockResolver, q -> fail(q, Rcode.SERVFAIL)); LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); - CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); - - assertThrowsCause(ServerFailedException.class, () -> resultFuture.toCompletableFuture().get()); + assertThatThrownBy(lookupSession.lookupAsync(name("a.b."), A, IN).toCompletableFuture()::get) + .cause() + .isInstanceOf(ServerFailedException.class); verify(mockResolver).sendAsync(any(), any(Executor.class)); } @@ -734,9 +1088,9 @@ class LookupSessionTest { wireUpMockResolver(mockResolver, q -> fail(q, Rcode.NOTIMP)); LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); - CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); - - assertThrowsCause(LookupFailedException.class, () -> resultFuture.toCompletableFuture().get()); + assertThatThrownBy(lookupSession.lookupAsync(name("a.b."), A, IN).toCompletableFuture()::get) + .cause() + .isInstanceOf(LookupFailedException.class); verify(mockResolver).sendAsync(any(), any(Executor.class)); } @@ -745,9 +1099,9 @@ class LookupSessionTest { wireUpMockResolver(mockResolver, q -> fail(q, Rcode.NXRRSET)); LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); - CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); - - assertThrowsCause(NoSuchRRSetException.class, () -> resultFuture.toCompletableFuture().get()); + assertThatThrownBy(lookupSession.lookupAsync(name("a.b."), A, IN).toCompletableFuture()::get) + .cause() + .isInstanceOf(NoSuchRRSetException.class); verify(mockResolver).sendAsync(any(), any(Executor.class)); } @@ -758,36 +1112,35 @@ class LookupSessionTest { LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); Name toLookup = name(format("%s.%s.%s.to.dname.", LONG_LABEL, LONG_LABEL, LONG_LABEL)); - CompletionStage resultFuture = lookupSession.lookupAsync(toLookup, A, IN); - - assertThrowsCause( - InvalidZoneDataException.class, () -> resultFuture.toCompletableFuture().get()); + assertThatThrownBy(lookupSession.lookupAsync(toLookup, A, IN).toCompletableFuture()::get) + .cause() + .isInstanceOf(InvalidZoneDataException.class); verify(mockResolver).sendAsync(any(), any(Executor.class)); } @Test - void lookupAsync_MultipleCNAMEs() { + void lookupAsync_MultipleCNAMEs() throws ExecutionException, InterruptedException { + Record testQuestion = Record.newRecord(name("a.b."), A, IN); // According to https://docstore.mik.ua/orelly/networking_2ndEd/dns/ch10_07.htm this is - // apparently something that BIND 4 did. - wireUpMockResolver(mockResolver, LookupSessionTest::multipleCNAMEs); + // apparently something that BIND 4 / BIND 9 before 9.1 could do. + wireUpMockResolver( + mockResolver, + query -> { + Message answer = new Message(query.getHeader().getID()); + answer.addRecord(testQuestion, Section.QUESTION); + answer.addRecord(cname(testQuestion.getName(), "target1."), Section.ANSWER); + answer.addRecord(cname(testQuestion.getName(), "target2."), Section.ANSWER); + return answer; + }); LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); - CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); + LookupResult result = lookupSession.lookupAsync(testQuestion).toCompletableFuture().get(); - assertThrowsCause( - InvalidZoneDataException.class, () -> resultFuture.toCompletableFuture().get()); - verify(mockResolver).sendAsync(any(), any(Executor.class)); - } + assertTrue(result.getRecords().isEmpty()); + assertThat(result.getAliases()).containsExactly(testQuestion.getName()); - private static Message multipleCNAMEs(Message query) { - Message answer = new Message(query.getHeader().getID()); - Record question = query.getQuestion(); - answer.addRecord(question, Section.QUESTION); - answer.addRecord( - new CNAMERecord(question.getName(), CNAME, IN, name("target1.")), Section.ANSWER); - answer.addRecord( - new CNAMERecord(question.getName(), CNAME, IN, name("target2.")), Section.ANSWER); - return answer; + // Two invocations as the result doesn't include an actual answer + verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); } @Test @@ -806,12 +1159,11 @@ class LookupSessionTest { ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); verify(mockResolver).sendAsync(messageCaptor.capture(), any(Executor.class)); - assertEquals( - Record.newRecord(name("host.example.com."), Type.A, DClass.IN, 0L), - messageCaptor.getValue().getSection(Section.QUESTION).get(0)); + assertThat(messageCaptor.getValue().getSection(Section.QUESTION)) + .containsExactly(Record.newRecord(name("host.example.com."), Type.A, DClass.IN, 0L)); - assertEquals( - singletonList(LOOPBACK_A.withName(name("host.example.com."))), lookupResult.getRecords()); + assertThat(lookupResult.getRecords()) + .containsExactly(LOOPBACK_A.withName(name("host.example.com."))); } @Test @@ -984,26 +1336,68 @@ class LookupSessionTest { } private static CNAMERecord cname(String name, String target) { - return cname(name(name), target); + return cname(name(name), name(target)); } + @SuppressWarnings("SameParameterValue") private static CNAMERecord cname(Name name, String target) { - return new CNAMERecord(name, IN, 0, name(target)); + return cname(name, name(target)); + } + + private static CNAMERecord cname(Name name, Name target) { + return new CNAMERecord(name, IN, 120, target); } - @SuppressWarnings("SameParameterValue") private static DNAMERecord dname(String name, String target) { - return new DNAMERecord(name(name), IN, 0, name(target)); + return dname(name(name), name(target)); + } + + @SuppressWarnings("SameParameterValue") + private static DNAMERecord dname(Name name, String target) { + return dname(name, name(target)); + } + + private static DNAMERecord dname(Name name, Name target) { + return new DNAMERecord(name, IN, 120, target); } private static Name name(String name) { return Name.fromConstantString(name); } - @SuppressWarnings("SameParameterValue") - private void assertThrowsCause(Class ex, Executable executable) { - Throwable outerException = assertThrows(Throwable.class, executable); - assertEquals(ex, outerException.getCause().getClass()); + private LookupSession.LookupSessionBuilder lookupSession(boolean useCache) { + return lookupSession(useCache, false); + } + + private LookupSession.LookupSessionBuilder lookupSession( + boolean useCache, IrrelevantRecordMode mode) { + return lookupSession(useCache, mode, false); + } + + private LookupSession.LookupSessionBuilder lookupSession(boolean useCache, boolean throwOnUse) { + return lookupSession(useCache, IrrelevantRecordMode.REMOVE, throwOnUse); + } + + private LookupSession.LookupSessionBuilder lookupSession( + boolean useCache, IrrelevantRecordMode mode, boolean throwOnUse) { + LookupSession.LookupSessionBuilder builder = + LookupSession.builder().resolver(mockResolver).irrelevantRecordMode(mode); + if (useCache) { + Cache cache = spy(new Cache()); + builder.cache(cache); + if (throwOnUse) { + lenient() + .doThrow(new RuntimeException("Unexpected addMessage")) + .when(cache) + .addMessage(any(Message.class)); + lenient() + .doThrow(new RuntimeException("Unexpected addRecord")) + .when(cache) + .addRecord(any(Record.class), anyInt()); + } + } + + return builder; } private void wireUpMockResolver(Resolver mockResolver, Function handler) { diff --git a/src/test/resources/unbound/val_adcopy.rpl b/src/test/resources/unbound/val_adcopy.rpl index 604fd57..aeb8bfd 100644 --- a/src/test/resources/unbound/val_adcopy.rpl +++ b/src/test/resources/unbound/val_adcopy.rpl @@ -17,7 +17,7 @@ SCENARIO_BEGIN Test validator AD bit sent by untrusted upstream ; K.ROOT-SERVERS.NET. RANGE_BEGIN 0 100 - ADDRESS 193.0.14.129 + ADDRESS 193.0.14.129 ENTRY_BEGIN MATCH opcode qtype qname ADJUST copy_id @@ -115,13 +115,13 @@ SECTION QUESTION www.example.com. IN A SECTION ANSWER www.example.com. IN A 10.20.30.40 -ns.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCQMyTjn7WWwpwAR1LlVeLpRgZGuQIUCcJDEkwAuzytTDRlYK7nIMwH1CM= ;{id = 2854} +www.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFC99iE9K5y2WNgI0gFvBWaTi9wm6AhUAoUqOpDtG5Zct+Qr9F3mSdnbc6V4= ;{id = 2854} SECTION AUTHORITY example.com. IN NS ns.example.com. example.com. 3600 IN RRSIG NS 3 2 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCN+qHdJxoI/2tNKwsb08pra/G7aAIUAWA5sDdJTbrXA1/3OaesGBAO3sI= ;{id = 2854} SECTION ADDITIONAL ns.example.com. IN A 1.2.3.4 -www.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFC99iE9K5y2WNgI0gFvBWaTi9wm6AhUAoUqOpDtG5Zct+Qr9F3mSdnbc6V4= ;{id = 2854} +ns.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCQMyTjn7WWwpwAR1LlVeLpRgZGuQIUCcJDEkwAuzytTDRlYK7nIMwH1CM= ;{id = 2854} ENTRY_END RANGE_END diff --git a/src/test/resources/unbound/val_unalgo_anchor.rpl b/src/test/resources/unbound/val_unalgo_anchor.rpl index fbbf288..de6b281 100644 --- a/src/test/resources/unbound/val_unalgo_anchor.rpl +++ b/src/test/resources/unbound/val_unalgo_anchor.rpl @@ -13,11 +13,11 @@ stub-zone: stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET. CONFIG_END -SCENARIO_BEGIN Test validator with unsupported algorithm trust anchor +SCENARIO_BEGIN Test validator with unsupported algorithm trust anchor ; K.ROOT-SERVERS.NET. RANGE_BEGIN 0 100 - ADDRESS 193.0.14.129 + ADDRESS 193.0.14.129 ENTRY_BEGIN MATCH opcode qtype qname ADJUST copy_id @@ -115,13 +115,13 @@ SECTION QUESTION www.example.com. IN A SECTION ANSWER www.example.com. IN A 10.20.30.40 -ns.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCQMyTjn7WWwpwAR1LlVeLpRgZGuQIUCcJDEkwAuzytTDRlYK7nIMwH1CM= ;{id = 2854} +www.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFC99iE9K5y2WNgI0gFvBWaTi9wm6AhUAoUqOpDtG5Zct+Qr9F3mSdnbc6V4= ;{id = 2854} SECTION AUTHORITY example.com. IN NS ns.example.com. example.com. 3600 IN RRSIG NS 3 2 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCN+qHdJxoI/2tNKwsb08pra/G7aAIUAWA5sDdJTbrXA1/3OaesGBAO3sI= ;{id = 2854} SECTION ADDITIONAL ns.example.com. IN A 1.2.3.4 -www.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFC99iE9K5y2WNgI0gFvBWaTi9wm6AhUAoUqOpDtG5Zct+Qr9F3mSdnbc6V4= ;{id = 2854} +ns.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCQMyTjn7WWwpwAR1LlVeLpRgZGuQIUCcJDEkwAuzytTDRlYK7nIMwH1CM= ;{id = 2854} ENTRY_END RANGE_END -- 2.33.0