4218 lines
156 KiB
Diff
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
|
||
|
|
|