dnsjava/0001-CVE-2024-25638-Message-normalization.patch
zhangxianting 4bd4ef6b75 Backport to fix CVE-2024-25638, remove invalid patch
(cherry picked from commit 783889b579b553732095d290a0e8e9b2a4cc37ba)
2024-07-24 16:50:17 +08:00

4218 lines
156 KiB
Diff

From 2073a0cdea2c560465f7ac0cc56f202e6fc39705 Mon Sep 17 00:00:00 2001
From: Ingo Bauersachs <ingo@jitsi.org>
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 @@
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
- <version>3.1.0</version>
+ <version>3.2.4</version>
<executions>
<execution>
<id>sign-artifacts</id>
@@ -200,7 +200,7 @@
<plugin>
<groupId>com.github.siom79.japicmp</groupId>
<artifactId>japicmp-maven-plugin</artifactId>
- <version> 0.18.3</version>
+ <version>0.20.0</version>
<configuration>
<newVersion>
<file>
@@ -417,6 +417,18 @@
<version>${org.junit.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>org.assertj</groupId>
+ <artifactId>assertj-core</artifactId>
+ <version>3.25.3</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.junit-pioneer</groupId>
+ <artifactId>junit-pioneer</artifactId>
+ <version>2.2.0</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-core</artifactId>
@@ -429,6 +441,12 @@
<version>${mockito.version}</version>
<scope>test</scope>
</dependency>
+ <dependency>
+ <groupId>net.bytebuddy</groupId>
+ <artifactId>byte-buddy-agent</artifactId>
+ <version>1.14.14</version>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
@@ -503,12 +521,29 @@
</configuration>
</plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-dependency-plugin</artifactId>
+ <version>3.6.1</version>
+ <executions>
+ <execution>
+ <phase>initialize</phase>
+ <goals>
+ <goal>properties</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>
- ${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}
</argLine>
</configuration>
</plugin>
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 <T extends Record> void addRRset(RRset rrset, int cred) {
+ addRRset(rrset, cred, false);
+ }
+
+ private synchronized <T extends Record> 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<RRset> 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<RRset> 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<Record> 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<RRset> getSectionRRsets(int section) {
+ Section.check(section);
if (sections[section] == null) {
return Collections.emptyList();
}
+
List<RRset> sets = new LinkedList<>();
- Set<Name> 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<Record>[]) 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<Resolver> 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}.
+ *
+ * <p>Normalization is only applied to {@link Rcode#NOERROR} and {@link Rcode#NXDOMAIN} responses.
+ *
+ * <p>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}.
+ *
+ * <p>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<RRset> answerSectionSets = getSectionRRsets(Section.ANSWER);
+ List<RRset> additionalSectionSets = getSectionRRsets(Section.ADDITIONAL);
+ List<RRset> authoritySectionSets = getSectionRRsets(Section.AUTHORITY);
+
+ List<RRset> cleanedAnswerSection = new ArrayList<>();
+ List<RRset> cleanedAuthoritySection = new ArrayList<>();
+ List<RRset> 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<Record> 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<Record> rrsetListToRecords(List<RRset> rrsets) {
+ if (rrsets.isEmpty()) {
+ return null;
+ }
+
+ List<Record> 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<RRset> additionalSectionSets, List<RRset> 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<Record>, 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<RRset> 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<Name> aliases;
+ /** The queries and responses that made up the result. */
+ @Getter(AccessLevel.PACKAGE)
+ private final Map<Record, Message> 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.
+ *
+ * <p><b>IMPORTANT</b>: 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:
+ *
+ * <ul>
+ * <li>has TSIG enabled
+ * <li>uses an externally secured transport, e.g. with IPSec or DNS over TLS.
+ * </ul>
+ */
+ @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<Record> records, List<Name> 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<Record> records,
+ List<Name> aliases) {
+ Map<Record, Message> 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<Integer, Cache> 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<Cache> 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<Cache> 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<LookupResult> 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.<Throwable, LookupResult>completeExceptionally(cause);
}
} else if (cause != null) {
- return completeExceptionally(cause);
+ return this.<Throwable, LookupResult>completeExceptionally(cause);
} else {
return CompletableFuture.completedFuture(result);
}
@@ -467,14 +501,54 @@ public class LookupSession {
private CompletionStage<LookupResult> lookupWithCache(Record queryRecord, List<Name> 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<LookupResult> lookupWithResolver(Record queryRecord, List<Name> 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<LookupResult> setResponseToMessageFuture(
SetResponse setResponse, Record queryRecord, List<Name> 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<Record> records =
setResponse.answers().stream()
@@ -507,17 +584,18 @@ public class LookupSession {
.collect(Collectors.toList());
return CompletableFuture.completedFuture(new LookupResult(records, aliases));
}
+
return null;
}
- private <T extends Throwable> CompletionStage<LookupResult> completeExceptionally(T failure) {
- CompletableFuture<LookupResult> future = new CompletableFuture<>();
+ private <T extends Throwable, R> CompletionStage<R> completeExceptionally(T failure) {
+ CompletableFuture<R> future = new CompletableFuture<>();
future.completeExceptionally(failure);
return future;
}
private CompletionStage<LookupResult> resolveRedirects(LookupResult response, Record query) {
- return maybeFollowRedirect(response, query, 1);
+ return maybeFollowRedirect(response, query, 0);
}
private CompletionStage<LookupResult> maybeFollowRedirect(
@@ -536,13 +614,20 @@ public class LookupSession {
}
}
+ @SuppressWarnings("deprecated")
private CompletionStage<LookupResult> maybeFollowRedirectsInAnswer(
LookupResult response, Record query, int redirectCount) {
List<Name> aliases = new ArrayList<>(response.getAliases());
List<Record> 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<Name, Record[]> 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<Integer, Integer> 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<Message> 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<Integer, Integer> 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<Integer, Integer> 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<Message> copy = new ArrayList<>(rpl.replays.size());
- copy.addAll(rpl.replays);
- List<Name> 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<Message> copy = new ArrayList<>(rpl.replays.size());
+ copy.addAll(rpl.replays);
+ List<Name> 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<String, String> ignored =
new HashMap<String, String>() {
{
@@ -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<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> resultFuture =
- lookupSession.lookupAsync(name("cname.r."), Type.value(type), IN);
+ LookupSession lookupSession = lookupSession(useCache).build();
+ CompletableFuture<LookupResult> future =
+ lookupSession.lookupAsync(name("cname.r."), Type.value(type), IN).toCompletableFuture();
- CompletableFuture<LookupResult> 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<Name, Record> 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<LookupResult> resultFuture = lookupSession.lookupAsync(name("cname.r."), A, IN);
+ CompletionStage<LookupResult> 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<Name, Record[]> 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<LookupResult> 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<Name, Record> nameToRecord = name -> name.equals(query) ? response : LOOPBACK_A;
+ wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord));
+
+ LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build();
+
+ CompletionStage<LookupResult> 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<Name, Record[]> 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<LookupResult> 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<Name, Record[]> 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<LookupResult> 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<Name, Record> 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<LookupResult> resultFuture =
- lookupSession.lookupAsync(name("x.y.to.dname."), A, IN);
+ CompletionStage<LookupResult> 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<Name, Record> 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<LookupResult> resultFuture =
- lookupSession.lookupAsync(name("first.example.com."), A, IN);
+ CompletionStage<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<Name, Record> 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<? extends Throwable> 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<LookupResult> resultFuture = lookupSession.lookupAsync(name("a."), A, IN);
-
- assertThrowsCause(
- RedirectOverflowException.class, () -> resultFuture.toCompletableFuture().get());
+ Class<? extends Throwable> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<LookupResult> 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<Message> 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 <T extends Throwable> void assertThrowsCause(Class<T> 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<Message, Message> 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