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

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

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

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

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

    ++ *
  • has TSIG enabled ++ *
  • uses an externally secured transport, e.g. with IPSec or DNS over TLS. ++ *
++ */ ++ @Getter(AccessLevel.PACKAGE) ++ private final boolean isAuthenticated; ++ + /** + * Construct an instance with the provided records and, in the case of a CNAME or DNAME + * indirection a List of aliases. + * + * @param records a list of records to return. + * @param aliases a list of aliases discovered during lookup, or null if there was no indirection. ++ * @deprecated This class is not intended for public instantiation. + */ ++ @Deprecated + public LookupResult(List records, List aliases) { + this.records = Collections.unmodifiableList(new ArrayList<>(records)); + this.aliases = + aliases == null + ? Collections.emptyList() + : Collections.unmodifiableList(new ArrayList<>(aliases)); ++ queryResponsePairs = Collections.emptyMap(); ++ isAuthenticated = false; ++ } ++ ++ LookupResult(boolean isAuthenticated) { ++ queryResponsePairs = Collections.emptyMap(); ++ this.isAuthenticated = isAuthenticated; ++ records = Collections.emptyList(); ++ aliases = Collections.emptyList(); ++ } ++ ++ LookupResult(Record query, boolean isAuthenticated, Record record) { ++ this.queryResponsePairs = Collections.singletonMap(query, null); ++ this.isAuthenticated = isAuthenticated; ++ this.records = Collections.singletonList(record); ++ this.aliases = Collections.emptyList(); ++ } ++ ++ LookupResult( ++ LookupResult previous, ++ Record query, ++ Message answer, ++ boolean isAuthenticated, ++ List records, ++ List aliases) { ++ Map map = new HashMap<>(previous.queryResponsePairs.size() + 1); ++ map.putAll(previous.queryResponsePairs); ++ map.put(query, answer); ++ this.queryResponsePairs = Collections.unmodifiableMap(map); ++ this.isAuthenticated = ++ previous.isAuthenticated ++ && isAuthenticated ++ && this.queryResponsePairs.values().stream() ++ .filter(Objects::nonNull) ++ .allMatch(a -> a.getHeader().getFlag(Flags.AD)); ++ this.records = Collections.unmodifiableList(new ArrayList<>(records)); ++ this.aliases = Collections.unmodifiableList(new ArrayList<>(aliases)); + } + } +diff --git a/src/main/java/org/xbill/DNS/lookup/LookupSession.java b/src/main/java/org/xbill/DNS/lookup/LookupSession.java +index 2236b15..168a130 100644 +--- a/src/main/java/org/xbill/DNS/lookup/LookupSession.java ++++ b/src/main/java/org/xbill/DNS/lookup/LookupSession.java +@@ -41,6 +41,7 @@ import org.xbill.DNS.Section; + import org.xbill.DNS.SetResponse; + import org.xbill.DNS.SimpleResolver; + import org.xbill.DNS.Type; ++import org.xbill.DNS.WireParseException; + import org.xbill.DNS.hosts.HostsFileParser; + + /** +@@ -61,6 +62,7 @@ public class LookupSession { + private final Map caches; + private final HostsFileParser hostsFileParser; + private final Executor executor; ++ private IrrelevantRecordMode irrelevantRecordMode; + + private LookupSession( + @NonNull Resolver resolver, +@@ -70,7 +72,8 @@ public class LookupSession { + boolean cycleResults, + List caches, + HostsFileParser hostsFileParser, +- Executor executor) { ++ Executor executor, ++ IrrelevantRecordMode irrelevantRecordMode) { + this.resolver = resolver; + this.maxRedirects = maxRedirects; + this.ndots = ndots; +@@ -82,6 +85,7 @@ public class LookupSession { + : caches.stream().collect(Collectors.toMap(Cache::getDClass, e -> e)); + this.hostsFileParser = hostsFileParser; + this.executor = executor == null ? ForkJoinPool.commonPool() : executor; ++ this.irrelevantRecordMode = irrelevantRecordMode; + } + + /** +@@ -100,6 +104,7 @@ public class LookupSession { + private List caches; + private HostsFileParser hostsFileParser; + private Executor executor; ++ private IrrelevantRecordMode irrelevantRecordMode = IrrelevantRecordMode.REMOVE; + + private LookupSessionBuilder() {} + +@@ -206,6 +211,17 @@ public class LookupSession { + return this; + } + ++ /** ++ * Sets how irrelevant records in a {@link Message} returned from the {@link ++ * #resolver(Resolver)} is handled. The default is {@link IrrelevantRecordMode#REMOVE}. ++ * ++ * @return {@code this}. ++ */ ++ LookupSessionBuilder irrelevantRecordMode(IrrelevantRecordMode irrelevantRecordMode) { ++ this.irrelevantRecordMode = irrelevantRecordMode; ++ return this; ++ } ++ + /** + * Enable querying the local hosts database using the system defaults. + * +@@ -318,7 +334,8 @@ public class LookupSession { + cycleResults, + caches, + hostsFileParser, +- executor); ++ executor, ++ irrelevantRecordMode); + } + } + +@@ -356,6 +373,22 @@ public class LookupSession { + .defaultHostsFileParser(); + } + ++ // Visible for testing only ++ Cache getCache(int dclass) { ++ return caches.get(dclass); ++ } ++ ++ /** ++ * Make an asynchronous lookup with the provided {@link Record}. ++ * ++ * @param question the name, type and DClass to look up. ++ * @return A {@link CompletionStage} what will yield the eventual lookup result. ++ * @since 3.6 ++ */ ++ public CompletionStage lookupAsync(Record question) { ++ return lookupAsync(question.getName(), question.getType(), question.getDClass()); ++ } ++ + /** + * Make an asynchronous lookup of the provided name using the default {@link DClass#IN}. + * +@@ -430,7 +463,8 @@ public class LookupSession { + } else { + r = new AAAARecord(name, DClass.IN, 0, result.get()); + } +- return new LookupResult(Collections.singletonList(r), Collections.emptyList()); ++ ++ return new LookupResult(Record.newRecord(name, type, DClass.IN), true, r); + } + } + } catch (IOException e) { +@@ -454,10 +488,10 @@ public class LookupSession { + if (names.hasNext()) { + return lookupUntilSuccess(names, type, dclass); + } else { +- return completeExceptionally(cause); ++ return this.completeExceptionally(cause); + } + } else if (cause != null) { +- return completeExceptionally(cause); ++ return this.completeExceptionally(cause); + } else { + return CompletableFuture.completedFuture(result); + } +@@ -467,14 +501,54 @@ public class LookupSession { + + private CompletionStage lookupWithCache(Record queryRecord, List aliases) { + return Optional.ofNullable(caches.get(queryRecord.getDClass())) +- .map(c -> c.lookupRecords(queryRecord.getName(), queryRecord.getType(), Credibility.NORMAL)) ++ .map( ++ c -> { ++ log.debug( ++ "Looking for <{}/{}/{}> in cache", ++ queryRecord.getName(), ++ Type.string(queryRecord.getType()), ++ DClass.string(queryRecord.getDClass())); ++ return c.lookupRecords( ++ queryRecord.getName(), queryRecord.getType(), Credibility.NORMAL); ++ }) + .map(setResponse -> setResponseToMessageFuture(setResponse, queryRecord, aliases)) + .orElseGet(() -> lookupWithResolver(queryRecord, aliases)); + } + + private CompletionStage lookupWithResolver(Record queryRecord, List aliases) { ++ Message query = Message.newQuery(queryRecord); ++ log.debug( ++ "Asking {} for <{}/{}/{}>", ++ resolver, ++ queryRecord.getName(), ++ Type.string(queryRecord.getType()), ++ DClass.string(queryRecord.getDClass())); + return resolver +- .sendAsync(Message.newQuery(queryRecord), executor) ++ .sendAsync(query, executor) ++ .thenCompose( ++ m -> { ++ try { ++ Message normalized = ++ m.normalize(query, irrelevantRecordMode == IrrelevantRecordMode.THROW); ++ ++ log.trace( ++ "Normalized response for <{}/{}/{}> from \n{}\ninto\n{}", ++ queryRecord.getName(), ++ Type.string(queryRecord.getType()), ++ DClass.string(queryRecord.getDClass()), ++ m, ++ normalized); ++ if (normalized == null) { ++ return completeExceptionally( ++ new InvalidZoneDataException("Failed to normalize message")); ++ } ++ return CompletableFuture.completedFuture(normalized); ++ } catch (WireParseException e) { ++ return completeExceptionally( ++ new LookupFailedException( ++ "Message normalization failed, refusing to return it", e)); ++ } ++ }) + .thenApply(this::maybeAddToCache) + .thenApply(answer -> buildResult(answer, aliases, queryRecord)); + } +@@ -490,16 +564,19 @@ public class LookupSession { + return message; + } + ++ @SuppressWarnings("deprecated") + private CompletionStage setResponseToMessageFuture( + SetResponse setResponse, Record queryRecord, List aliases) { + if (setResponse.isNXDOMAIN()) { + return completeExceptionally( + new NoSuchDomainException(queryRecord.getName(), queryRecord.getType())); + } ++ + if (setResponse.isNXRRSET()) { + return completeExceptionally( + new NoSuchRRSetException(queryRecord.getName(), queryRecord.getType())); + } ++ + if (setResponse.isSuccessful()) { + List records = + setResponse.answers().stream() +@@ -507,17 +584,18 @@ public class LookupSession { + .collect(Collectors.toList()); + return CompletableFuture.completedFuture(new LookupResult(records, aliases)); + } ++ + return null; + } + +- private CompletionStage completeExceptionally(T failure) { +- CompletableFuture future = new CompletableFuture<>(); ++ private CompletionStage completeExceptionally(T failure) { ++ CompletableFuture future = new CompletableFuture<>(); + future.completeExceptionally(failure); + return future; + } + + private CompletionStage resolveRedirects(LookupResult response, Record query) { +- return maybeFollowRedirect(response, query, 1); ++ return maybeFollowRedirect(response, query, 0); + } + + private CompletionStage maybeFollowRedirect( +@@ -536,13 +614,20 @@ public class LookupSession { + } + } + ++ @SuppressWarnings("deprecated") + private CompletionStage maybeFollowRedirectsInAnswer( + LookupResult response, Record query, int redirectCount) { + List aliases = new ArrayList<>(response.getAliases()); + List results = new ArrayList<>(); + Name current = query.getName(); + for (Record r : response.getRecords()) { +- if (redirectCount > maxRedirects) { ++ // Abort with a dedicated exception for loops instead of simply trying until reaching the max ++ // redirects ++ if (aliases.contains(current)) { ++ return completeExceptionally(new RedirectLoopException(maxRedirects)); ++ } ++ ++ if (redirectCount >= maxRedirects) { + throw new RedirectOverflowException(maxRedirects); + } + +@@ -572,11 +657,17 @@ public class LookupSession { + return CompletableFuture.completedFuture(new LookupResult(results, aliases)); + } + +- if (redirectCount > maxRedirects) { ++ // Abort with a dedicated exception for loops instead of simply trying until reaching the max ++ // redirects ++ if (aliases.contains(current)) { ++ return completeExceptionally(new RedirectLoopException(maxRedirects)); ++ } ++ ++ if (redirectCount >= maxRedirects) { + throw new RedirectOverflowException(maxRedirects); + } + +- int finalRedirectCount = redirectCount + 1; ++ int finalRedirectCount = redirectCount; + Record redirectQuery = Record.newRecord(current, query.getType(), query.getDClass()); + return lookupWithCache(redirectQuery, aliases) + .thenCompose( +diff --git a/src/main/java/org/xbill/DNS/lookup/RedirectLoopException.java b/src/main/java/org/xbill/DNS/lookup/RedirectLoopException.java +new file mode 100644 +index 0000000..3518af5 +--- /dev/null ++++ b/src/main/java/org/xbill/DNS/lookup/RedirectLoopException.java +@@ -0,0 +1,13 @@ ++// SPDX-License-Identifier: BSD-3-Clause ++package org.xbill.DNS.lookup; ++ ++/** ++ * Thrown if the lookup results in a loop of CNAME and/or DNAME indirections. ++ * ++ * @since 3.6 ++ */ ++public class RedirectLoopException extends RedirectOverflowException { ++ public RedirectLoopException(int maxRedirects) { ++ super(maxRedirects); ++ } ++} +diff --git a/src/main/java/org/xbill/DNS/lookup/RedirectOverflowException.java b/src/main/java/org/xbill/DNS/lookup/RedirectOverflowException.java +index 47e2871..0297b39 100644 +--- a/src/main/java/org/xbill/DNS/lookup/RedirectOverflowException.java ++++ b/src/main/java/org/xbill/DNS/lookup/RedirectOverflowException.java +@@ -18,7 +18,7 @@ public class RedirectOverflowException extends LookupFailedException { + } + + public RedirectOverflowException(int maxRedirects) { +- super("Refusing to follow more than " + maxRedirects + " redirects"); ++ super("Detected a redirect loop: Refusing to follow more than " + maxRedirects + " redirects"); + this.maxRedirects = maxRedirects; + } + } +diff --git a/src/test/java/org/xbill/DNS/LookupTest.java b/src/test/java/org/xbill/DNS/LookupTest.java +index a5132c8..b794c3e 100644 +--- a/src/test/java/org/xbill/DNS/LookupTest.java ++++ b/src/test/java/org/xbill/DNS/LookupTest.java +@@ -445,8 +445,28 @@ public class LookupTest { + if (DUMMY_NAME.equals(response.getName())) { + response = response.withName(query.getQuestion().getName()); + } ++ response.setTTL(120); + answer.addRecord(response, Section.ANSWER); + } + return answer; + } ++ ++ public static Message multiAnswer(Message query, Function recordMaker) { ++ Message answer = new Message(query.getHeader().getID()); ++ answer.addRecord(query.getQuestion(), Section.QUESTION); ++ Name questionName = query.getQuestion().getName(); ++ Record[] response = recordMaker.apply(questionName); ++ if (response == null) { ++ answer.getHeader().setRcode(Rcode.NXDOMAIN); ++ } else { ++ for (Record r : response) { ++ if (DUMMY_NAME.equals(r.getName())) { ++ r = r.withName(query.getQuestion().getName()); ++ } ++ r.setTTL(120); ++ answer.addRecord(r, Section.ANSWER); ++ } ++ } ++ return answer; ++ } + } +diff --git a/src/test/java/org/xbill/DNS/MessageTest.java b/src/test/java/org/xbill/DNS/MessageTest.java +index 8610014..54e32d5 100644 +--- a/src/test/java/org/xbill/DNS/MessageTest.java ++++ b/src/test/java/org/xbill/DNS/MessageTest.java +@@ -53,9 +53,9 @@ class MessageTest { + Message m = new Message(); + assertTrue(m.getSection(0).isEmpty()); + assertTrue(m.getSection(1).isEmpty()); +- assertTrue(m.getSection(3).isEmpty()); + assertTrue(m.getSection(2).isEmpty()); +- assertThrows(IndexOutOfBoundsException.class, () -> m.getSection(4)); ++ assertTrue(m.getSection(3).isEmpty()); ++ assertThrows(IllegalArgumentException.class, () -> m.getSection(4)); + Header h = m.getHeader(); + assertEquals(0, h.getCount(0)); + assertEquals(0, h.getCount(1)); +@@ -71,7 +71,7 @@ class MessageTest { + assertTrue(m.getSection(1).isEmpty()); + assertTrue(m.getSection(2).isEmpty()); + assertTrue(m.getSection(3).isEmpty()); +- assertThrows(IndexOutOfBoundsException.class, () -> m.getSection(4)); ++ assertThrows(IllegalArgumentException.class, () -> m.getSection(4)); + Header h = m.getHeader(); + assertEquals(0, h.getCount(0)); + assertEquals(0, h.getCount(1)); +@@ -167,4 +167,17 @@ class MessageTest { + assertEquals(clone.getQuestion(), response.getQuestion()); + assertEquals(clone.getSection(Section.ANSWER), response.getSection(Section.ANSWER)); + } ++ ++ @Test ++ void normalize() throws WireParseException { ++ Record queryRecord = ++ Record.newRecord(Name.fromConstantString("example.com."), Type.MX, DClass.IN); ++ Message query = Message.newQuery(queryRecord); ++ Message response = new Message(); ++ response.addRecord(queryRecord, Section.QUESTION); ++ response.addRecord(queryRecord, Section.ADDITIONAL); ++ response = response.normalize(query, true); ++ assertTrue(response.getSection(Section.ANSWER).isEmpty()); ++ assertTrue(response.getSection(Section.ADDITIONAL).isEmpty()); ++ } + } +diff --git a/src/test/java/org/xbill/DNS/SetResponseTest.java b/src/test/java/org/xbill/DNS/SetResponseTest.java +index 7bc460d..d436685 100644 +--- a/src/test/java/org/xbill/DNS/SetResponseTest.java ++++ b/src/test/java/org/xbill/DNS/SetResponseTest.java +@@ -46,7 +46,9 @@ import java.net.InetAddress; + import java.net.UnknownHostException; + import org.junit.jupiter.api.Test; + import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.CsvSource; + import org.junit.jupiter.params.provider.EnumSource; ++import org.junit.jupiter.params.provider.ValueSource; + + class SetResponseTest { + private static final ARecord A_RECORD_1 = +@@ -129,6 +131,47 @@ class SetResponseTest { + assertArrayEquals(exp, sr.answers().toArray()); + } + ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void ofTypeWithCachedRRset(boolean isAuthenticated) { ++ SetResponse sr = ++ SetResponse.ofType( ++ SetResponseType.SUCCESSFUL, ++ new Cache.CacheRRset(new RRset(A_RECORD_1), 0, 0, isAuthenticated)); ++ assertEquals(isAuthenticated, sr.isAuthenticated()); ++ } ++ ++ @ParameterizedTest ++ @CsvSource({ ++ "false,true,true,true,true", ++ "false,false,true,false,false", ++ "true,true,false,true,false", ++ "true,false,false,false,false", ++ }) ++ void addRRsetAuthenticated( ++ boolean addInitial, ++ boolean first, ++ boolean second, ++ boolean firstResult, ++ boolean secondResult) { ++ RRset rrs = new RRset(A_RECORD_1); ++ SetResponse sr; ++ if (addInitial) { ++ sr = SetResponse.ofType(SetResponseType.SUCCESSFUL, rrs, first); ++ } else { ++ sr = SetResponse.ofType(SetResponseType.SUCCESSFUL); ++ sr.addRRset(new Cache.CacheRRset(rrs, 0, 0, first)); ++ } ++ ++ RRset[] exp = new RRset[] {rrs}; ++ assertArrayEquals(exp, sr.answers().toArray()); ++ assertEquals(firstResult, sr.isAuthenticated()); ++ ++ sr.addRRset(new Cache.CacheRRset(new RRset(A_RECORD_1), 0, 0, second)); ++ assertEquals(secondResult, sr.isAuthenticated()); ++ assertEquals(2, sr.answers().size()); ++ } ++ + @Test + void addRRset_multiple() throws TextParseException, UnknownHostException { + RRset rrs = new RRset(); +diff --git a/src/test/java/org/xbill/DNS/dnssec/Rpl.java b/src/test/java/org/xbill/DNS/dnssec/Rpl.java +index 71562e3..b865101 100644 +--- a/src/test/java/org/xbill/DNS/dnssec/Rpl.java ++++ b/src/test/java/org/xbill/DNS/dnssec/Rpl.java +@@ -17,6 +17,7 @@ class Rpl { + TreeMap nsec3iterations; + String digestPreference; + boolean hardenAlgoDowngrade; ++ boolean hardenUnknownAdditional = true; + boolean enableSha1; + boolean enableDsa; + boolean loadBouncyCastle; +diff --git a/src/test/java/org/xbill/DNS/dnssec/RplParser.java b/src/test/java/org/xbill/DNS/dnssec/RplParser.java +index b61c12f..e198767 100644 +--- a/src/test/java/org/xbill/DNS/dnssec/RplParser.java ++++ b/src/test/java/org/xbill/DNS/dnssec/RplParser.java +@@ -73,7 +73,7 @@ class RplParser { + if (line.startsWith("server:")) { + state = ParseState.Server; + } else if (line.startsWith("SCENARIO_BEGIN")) { +- rpl.scenario = line.substring(line.indexOf(" ")); ++ rpl.scenario = line.substring(line.indexOf(" ")).trim(); + rpl.replays = new LinkedList<>(); + rpl.checks = new TreeMap<>(); + } else if (line.startsWith("ENTRY_BEGIN")) { +@@ -126,6 +126,8 @@ class RplParser { + rpl.enableSha1 = "yes".equalsIgnoreCase(line.split(":")[1].trim()); + } else if (line.matches("\\s*fake-dsa:.*")) { + rpl.enableDsa = "yes".equalsIgnoreCase(line.split(":")[1].trim()); ++ } else if (line.matches("\\s*harden-unknown-additional:.*")) { ++ rpl.hardenUnknownAdditional = "yes".equalsIgnoreCase(line.split(":")[1].trim()); + } else if (line.matches("\\s*bouncycastle:.*")) { + rpl.loadBouncyCastle = "yes".equalsIgnoreCase(line.split(":")[1].trim()); + } else if (line.startsWith("CONFIG_END")) { +diff --git a/src/test/java/org/xbill/DNS/dnssec/TestBase.java b/src/test/java/org/xbill/DNS/dnssec/TestBase.java +index 818fcb9..251c2a6 100644 +--- a/src/test/java/org/xbill/DNS/dnssec/TestBase.java ++++ b/src/test/java/org/xbill/DNS/dnssec/TestBase.java +@@ -27,6 +27,7 @@ import java.util.Optional; + import java.util.concurrent.CompletableFuture; + import java.util.concurrent.CompletionStage; + import java.util.concurrent.ExecutionException; ++import lombok.extern.slf4j.Slf4j; + import org.junit.jupiter.api.AfterEach; + import org.junit.jupiter.api.BeforeAll; + import org.junit.jupiter.api.BeforeEach; +@@ -48,9 +49,8 @@ import org.xbill.DNS.SimpleResolver; + import org.xbill.DNS.TXTRecord; + import org.xbill.DNS.Type; + ++@Slf4j + public abstract class TestBase { +- private static final Logger logger = LoggerFactory.getLogger(TestBase.class); +- + private static final boolean offline = !Boolean.getBoolean("dnsjava.dnssec.online"); + private static final boolean partialOffline = + "partial".equals(System.getProperty("dnsjava.dnssec.offline")); +@@ -125,6 +125,7 @@ public abstract class TestBase { + + Message m; + while ((m = messageReader.readMessage(r)) != null) { ++ m = m.normalize(Message.newQuery(m.getQuestion()), true); + queryResponsePairs.put(key(m), m); + } + +@@ -162,9 +163,13 @@ public abstract class TestBase { + new SimpleResolver("8.8.4.4") { + @Override + public CompletionStage sendAsync(Message query) { +- logger.info("---{}", key(query)); + Message response = queryResponsePairs.get(key(query)); + if (response != null) { ++ if (!log.isTraceEnabled()) { ++ log.debug("---{}", key(query)); ++ } ++ ++ log.trace("---{}\n{}", key(query), response); + return CompletableFuture.completedFuture(response); + } else if ((offline && !partialOffline) || unboundTest || alwaysOffline) { + fail("Response for " + key(query) + " not found."); +diff --git a/src/test/java/org/xbill/DNS/dnssec/UnboundTests.java b/src/test/java/org/xbill/DNS/dnssec/UnboundTests.java +index 11f9bdf..d709fd4 100644 +--- a/src/test/java/org/xbill/DNS/dnssec/UnboundTests.java ++++ b/src/test/java/org/xbill/DNS/dnssec/UnboundTests.java +@@ -5,6 +5,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; + import static org.mockito.Mockito.when; + + import java.io.File; ++import java.io.FileInputStream; + import java.io.IOException; + import java.io.InputStream; + import java.security.Security; +@@ -15,10 +16,13 @@ import java.util.List; + import java.util.Map; + import java.util.Map.Entry; + import java.util.Properties; ++import lombok.extern.slf4j.Slf4j; + import org.bouncycastle.jce.provider.BouncyCastleProvider; + import org.junit.jupiter.api.Disabled; ++import org.junit.jupiter.api.DisplayName; + import org.junit.jupiter.api.Test; + import org.xbill.DNS.CNAMERecord; ++import org.xbill.DNS.DClass; + import org.xbill.DNS.DNAMERecord; + import org.xbill.DNS.DNSSEC; + import org.xbill.DNS.Flags; +@@ -31,133 +35,150 @@ import org.xbill.DNS.Record; + import org.xbill.DNS.Section; + import org.xbill.DNS.Type; + ++@Slf4j + class UnboundTests extends TestBase { + void runUnboundTest() throws ParseException, IOException { +- InputStream data = getClass().getResourceAsStream("/unbound/" + testName + ".rpl"); +- RplParser p = new RplParser(data); +- Rpl rpl = p.parse(); +- Properties config = new Properties(); +- if (rpl.nsec3iterations != null) { +- for (Entry e : rpl.nsec3iterations.entrySet()) { +- config.put("dnsjava.dnssec.nsec3.iterations." + e.getKey(), e.getValue()); ++ try { ++ InputStream data = getClass().getResourceAsStream("/unbound/" + testName + ".rpl"); ++ RplParser p = new RplParser(data); ++ Rpl rpl = p.parse(); ++ Properties config = new Properties(); ++ if (rpl.nsec3iterations != null) { ++ for (Entry e : rpl.nsec3iterations.entrySet()) { ++ config.put("dnsjava.dnssec.nsec3.iterations." + e.getKey(), e.getValue()); ++ } + } +- } + +- if (rpl.digestPreference != null) { +- config.put(ValUtils.DIGEST_PREFERENCE, rpl.digestPreference); +- } ++ if (rpl.digestPreference != null) { ++ config.put(ValUtils.DIGEST_PREFERENCE, rpl.digestPreference); ++ } + +- config.put(ValUtils.DIGEST_HARDEN_DOWNGRADE, Boolean.toString(rpl.hardenAlgoDowngrade)); ++ config.put(ValUtils.DIGEST_HARDEN_DOWNGRADE, Boolean.toString(rpl.hardenAlgoDowngrade)); + +- if (rpl.enableSha1) { +- config.put(ValUtils.DIGEST_ENABLED + "." + DNSSEC.Digest.SHA1, Boolean.TRUE.toString()); +- } ++ if (rpl.enableSha1) { ++ config.put(ValUtils.DIGEST_ENABLED + "." + DNSSEC.Digest.SHA1, Boolean.TRUE.toString()); ++ } + +- if (rpl.enableDsa || rpl.enableSha1) { +- config.put(ValUtils.ALGORITHM_ENABLED + "." + DNSSEC.Algorithm.DSA, Boolean.TRUE.toString()); +- config.put( +- ValUtils.ALGORITHM_ENABLED + "." + DNSSEC.Algorithm.DSA_NSEC3_SHA1, +- Boolean.TRUE.toString()); +- } ++ if (rpl.enableDsa || rpl.enableSha1) { ++ config.put( ++ ValUtils.ALGORITHM_ENABLED + "." + DNSSEC.Algorithm.DSA, Boolean.TRUE.toString()); ++ config.put( ++ ValUtils.ALGORITHM_ENABLED + "." + DNSSEC.Algorithm.DSA_NSEC3_SHA1, ++ Boolean.TRUE.toString()); ++ } + +- if (rpl.loadBouncyCastle) { +- Security.addProvider(new BouncyCastleProvider()); +- } ++ if (!rpl.hardenUnknownAdditional) { ++ System.setProperty("dnsjava.harden_unknown_additional", Boolean.TRUE.toString()); ++ } + +- for (Message m : rpl.replays) { +- add(m); +- } ++ if (rpl.loadBouncyCastle) { ++ Security.addProvider(new BouncyCastleProvider()); ++ } + +- // merge xNAME queries into one +- List copy = new ArrayList<>(rpl.replays.size()); +- copy.addAll(rpl.replays); +- List copiedTargets = new ArrayList<>(5); +- for (Message m : copy) { +- Name target = null; +- for (RRset s : m.getSectionRRsets(Section.ANSWER)) { +- if (s.getType() == Type.CNAME) { +- target = ((CNAMERecord) s.first()).getTarget(); +- } else if (s.getType() == Type.DNAME) { +- target = ((DNAMERecord) s.first()).getTarget(); +- } ++ for (Message m : rpl.replays) { ++ add(m); ++ } + +- while (target != null) { +- Message a = get(target, m.getQuestion().getType()); +- if (a == null) { +- a = get(target, Type.CNAME); ++ // merge xNAME queries into one ++ List copy = new ArrayList<>(rpl.replays.size()); ++ copy.addAll(rpl.replays); ++ List copiedTargets = new ArrayList<>(5); ++ for (Message m : copy) { ++ Name target = null; ++ for (RRset s : m.getSectionRRsets(Section.ANSWER)) { ++ if (s.getType() == Type.CNAME) { ++ target = ((CNAMERecord) s.first()).getTarget(); ++ } else if (s.getType() == Type.DNAME) { ++ target = ((DNAMERecord) s.first()).getTarget(); + } + +- if (a == null) { +- a = get(target, Type.DNAME); +- } ++ while (target != null) { ++ Message a = get(target, m.getQuestion().getType()); ++ if (a == null) { ++ a = get(target, Type.CNAME); ++ } + +- if (a != null) { +- target = add(m, a); +- if (copiedTargets.contains(target)) { +- break; ++ if (a == null) { ++ a = get(target, Type.DNAME); + } + +- copiedTargets.add(target); +- rpl.replays.remove(a); +- } else { +- target = null; ++ if (a != null) { ++ target = add(m, a); ++ if (copiedTargets.contains(target)) { ++ break; ++ } ++ ++ copiedTargets.add(target); ++ rpl.replays.remove(a); ++ } else { ++ target = null; ++ } + } + } + } +- } + +- // promote any DS records in auth. sections to real queries +- copy = new ArrayList<>(rpl.replays.size()); +- copy.addAll(rpl.replays); +- for (Message m : copy) { +- for (RRset s : m.getSectionRRsets(Section.AUTHORITY)) { +- if (s.getType() == Type.DS) { +- Message ds = new Message(); +- ds.addRecord(Record.newRecord(s.getName(), s.getType(), s.getDClass()), Section.QUESTION); +- for (Record rr : s.rrs()) { +- ds.addRecord(rr, Section.ANSWER); +- } ++ // promote any DS records in auth. sections to real queries ++ copy = new ArrayList<>(rpl.replays.size()); ++ copy.addAll(rpl.replays); ++ for (Message m : copy) { ++ for (RRset s : m.getSectionRRsets(Section.AUTHORITY)) { ++ if (s.getType() == Type.DS) { ++ Message ds = new Message(); ++ ds.addRecord( ++ Record.newRecord(s.getName(), s.getType(), s.getDClass()), Section.QUESTION); ++ for (Record rr : s.rrs()) { ++ ds.addRecord(rr, Section.ANSWER); ++ } + +- for (RRSIGRecord sig : s.sigs()) { +- ds.addRecord(sig, Section.ANSWER); +- } ++ for (RRSIGRecord sig : s.sigs()) { ++ ds.addRecord(sig, Section.ANSWER); ++ } + +- rpl.replays.add(ds); ++ rpl.replays.add(ds); ++ } + } + } +- } +- +- clear(); +- for (Message m : rpl.replays) { +- add(m); +- } + +- if (rpl.date != null) { +- try { +- when(resolverClock.instant()).thenReturn(rpl.date); +- } catch (Exception e) { +- throw new RuntimeException(e); ++ clear(); ++ for (Message m : rpl.replays) { ++ add(m); + } +- } + +- if (rpl.trustAnchors != null) { +- resolver.getTrustAnchors().clear(); +- for (SRRset rrset : rpl.trustAnchors) { +- resolver.getTrustAnchors().store(rrset); ++ if (rpl.date != null) { ++ try { ++ when(resolverClock.instant()).thenReturn(rpl.date); ++ } catch (Exception e) { ++ throw new RuntimeException(e); ++ } + } +- } + +- resolver.init(config); ++ if (rpl.trustAnchors != null) { ++ resolver.getTrustAnchors().clear(); ++ for (SRRset rrset : rpl.trustAnchors) { ++ resolver.getTrustAnchors().store(rrset); ++ } ++ } + +- for (Check c : rpl.checks.values()) { +- Message s = resolver.send(c.query); ++ resolver.init(config); ++ ++ for (Check c : rpl.checks.values()) { ++ Message s = resolver.send(c.query).normalize(c.query, true); ++ log.trace( ++ "{}/{}/{} ---> \n{}", ++ c.query.getQuestion().getName(), ++ Type.string(c.query.getQuestion().getType()), ++ DClass.string(c.query.getQuestion().getDClass()), ++ s); ++ assertEquals( ++ c.response.getHeader().getFlag(Flags.AD), ++ s.getHeader().getFlag(Flags.AD), ++ "AD Flag must match"); ++ assertEquals( ++ Rcode.string(c.response.getRcode()), Rcode.string(s.getRcode()), "RCode must match"); ++ } ++ } finally { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); +- assertEquals( +- c.response.getHeader().getFlag(Flags.AD), +- s.getHeader().getFlag(Flags.AD), +- "AD Flag must match"); +- assertEquals( +- Rcode.string(c.response.getRcode()), Rcode.string(s.getRcode()), "RCode must match"); ++ System.clearProperty("dnsjava.harden_unknown_additional"); + } + } + +@@ -182,7 +203,7 @@ class UnboundTests extends TestBase { + return next; + } + +- static void xmain(String[] xargs) { ++ static void main(String[] xargs) throws IOException, ParseException { + Map ignored = + new HashMap() { + { +@@ -207,8 +228,12 @@ class UnboundTests extends TestBase { + put("val_cnametoinsecure.rpl", "incomplete CNAME answer"); + put("val_nsec3_optout_cache.rpl", "more cache stuff"); + put("val_unsecds_qtypeds.rpl", "tests the iterative resolver"); +- put("val_anchor_nx.rpl", "tests caching of NX from a parent resolver"); +- put("val_anchor_nx_nosig.rpl", "tests caching of NX from a parent resolver"); ++ put( ++ "val_anchor_nx.rpl", ++ "tests resolving conflicting responses in a recursive resolver"); ++ put( ++ "val_anchor_nx_nosig.rpl", ++ "tests resolving conflicting responses in a recursive resolver"); + put("val_negcache_nta.rpl", "tests unbound option domain-insecure, not available here"); + } + }; +@@ -219,7 +244,9 @@ class UnboundTests extends TestBase { + System.out.println(" @Disabled(\"" + comment + "\")"); + } + ++ Rpl rpl = new RplParser(new FileInputStream("./src/test/resources/unbound/" + f)).parse(); + System.out.println(" @Test"); ++ System.out.println(" @DisplayName(\"" + f + ": " + rpl.scenario + "\")"); + System.out.println( + " void " + f.split("\\.")[0] + "() throws ParseException, IOException {"); + System.out.println(" runUnboundTest();"); +@@ -229,798 +256,1010 @@ class UnboundTests extends TestBase { + } + + @Test ++ @DisplayName("val_adbit.rpl: Test validator AD bit signaling") + void val_adbit() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_adcopy.rpl: Test validator AD bit sent by untrusted upstream") + void val_adcopy() throws ParseException, IOException { + runUnboundTest(); + } + +- @Disabled("tests caching of NX from a parent resolver") ++ @Disabled("tests resolving conflicting responses in a recursive resolver") + @Test ++ @DisplayName("val_anchor_nx.rpl: Test validator with secure proof of trust anchor nxdomain") + void val_anchor_nx() throws ParseException, IOException { + runUnboundTest(); + } + +- @Disabled("tests caching of NX from a parent resolver") ++ @Disabled("tests resolving conflicting responses in a recursive resolver") + @Test ++ @DisplayName("val_anchor_nx_nosig.rpl: Test validator with unsigned denial of trust anchor") + void val_anchor_nx_nosig() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ans_dsent.rpl: Test validator with empty nonterminals on the trust chain.") + void val_ans_dsent() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ans_nx.rpl: Test validator with DS nodata as nxdomain on trust chain") + void val_ans_nx() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_any.rpl: Test validator with response to qtype ANY") + void val_any() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_any_cname.rpl: Test validator with response to qtype ANY that includes CNAME") + void val_any_cname() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_any_dname.rpl: Test validator with response to qtype ANY that includes DNAME") + void val_any_dname() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnameinsectopos.rpl: Test validator with an insecure cname to positive cached") + void val_cnameinsectopos() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_cnamenx_dblnsec.rpl: Test validator with cname-nxdomain for duplicate NSEC detection") + void val_cnamenx_dblnsec() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnamenx_rcodenx.rpl: Test validator with cname-nxdomain with rcode nxdomain") + void val_cnamenx_rcodenx() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnameqtype.rpl: Test validator with a query for type cname") + void val_cnameqtype() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnametocloser.rpl: Test validator with CNAME to closer anchor under optout.") + void val_cnametocloser() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_cnametocloser_nosig.rpl: Test validator with CNAME to closer anchor optout missing sigs.") + void val_cnametocloser_nosig() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_cnametocnamewctoposwc.rpl: Test validator with a regular cname to wildcard cname to wildcard response") + void val_cnametocnamewctoposwc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnametodname.rpl: Test validator with a cname to a dname") + void val_cnametodname() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_cnametodnametocnametopos.rpl: Test validator with cname, dname, cname, positive answer") + void val_cnametodnametocnametopos() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("incomplete CNAME answer") + @Test ++ @DisplayName("val_cnametoinsecure.rpl: Test validator with CNAME to insecure NSEC or NSEC3.") + void val_cnametoinsecure() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnametonodata.rpl: Test validator with cname to nodata") + void val_cnametonodata() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnametonodata_nonsec.rpl: Test validator with cname to nodata") + void val_cnametonodata_nonsec() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("incomplete CNAME answer") + @Test ++ @DisplayName("val_cnametonsec.rpl: Test validator with CNAME to insecure NSEC delegation") + void val_cnametonsec() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnametonx.rpl: Test validator with cname to nxdomain") + void val_cnametonx() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("incomplete CNAME answer") + @Test ++ @DisplayName("val_cnametooptin.rpl: Test validator with CNAME to insecure optin NSEC3") + void val_cnametooptin() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnametooptout.rpl: Test validator with CNAME to optout NSEC3 span NODATA") + void val_cnametooptout() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnametopos.rpl: Test validator with a cname to positive") + void val_cnametopos() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_cnametoposnowc.rpl: Test validator with a cname to positive wildcard without proof") + void val_cnametoposnowc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnametoposwc.rpl: Test validator with a cname to positive wildcard") + void val_cnametoposwc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnamewctonodata.rpl: Test validator with wildcard cname to nodata") + void val_cnamewctonodata() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnamewctonx.rpl: Test validator with wildcard cname to nxdomain") + void val_cnamewctonx() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cnamewctoposwc.rpl: Test validator with wildcard cname to positive wildcard") + void val_cnamewctoposwc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cname_loop1.rpl: Test validator with cname loop") + void val_cname_loop1() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cname_loop2.rpl: Test validator with cname 2 step loop") + void val_cname_loop2() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_cname_loop3.rpl: Test validator with cname 3 step loop") + void val_cname_loop3() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_deleg_nons.rpl: Test validator with unsigned delegation with no NS bit in NSEC") ++ void val_deleg_nons() throws ParseException, IOException { ++ runUnboundTest(); ++ } ++ ++ @Test ++ @DisplayName("val_dnametoolong.rpl: Test validator with a dname too long response") + void val_dnametoolong() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_dnametopos.rpl: Test validator with a dname to positive") + void val_dnametopos() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_dnametoposwc.rpl: Test validator with a dname to positive wildcard") + void val_dnametoposwc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_dnamewc.rpl: Test validator with a wildcarded dname") + void val_dnamewc() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("we don't do negative caching") + @Test ++ @DisplayName("val_dsnsec.rpl: Test pickup of DS NSEC from the cache.") + void val_dsnsec() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ds_afterprime.rpl: Test DS lookup after key prime is done.") + void val_ds_afterprime() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ds_cname.rpl: Test validator with CNAME response to DS") + void val_ds_cname() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ds_cnamesub.rpl: Test validator with CNAME response to DS in chain of trust") + void val_ds_cnamesub() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_ds_cnamesubbogus.rpl: Test validator with bogus CNAME response to DS in chain of trust") + void val_ds_cnamesubbogus() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ds_gost.rpl: Test validator with GOST DS digest") + void val_ds_gost() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ds_gost_downgrade.rpl: Test validator with GOST DS digest downgrade attack") + void val_ds_gost_downgrade() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ds_sha2.rpl: Test validator with SHA256 DS digest") + void val_ds_sha2() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ds_sha2_downgrade.rpl: Test validator with SHA256 DS downgrade to SHA1") + void val_ds_sha2_downgrade() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_ds_sha2_downgrade_override.rpl: Test validator with SHA256 DS downgrade to SHA1") + void val_ds_sha2_downgrade_override() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ds_sha2_lenient.rpl: Test validator with SHA256 DS downgrade to SHA1 lenience") + void val_ds_sha2_lenient() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_entds.rpl: Test validator with lots of ENTs in the chain of trust") + void val_entds() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_faildnskey.rpl: Test validator with failed DNSKEY request") + void val_faildnskey() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("tests an unbound specific config option") + @Test ++ @DisplayName( ++ "val_faildnskey_ok.rpl: Test validator with failed DNSKEY request, but not hardened.") + void val_faildnskey_ok() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("irrelevant, we're not a recursive resolver") + @Test ++ @DisplayName("val_fwdds.rpl: Test forward-zone with DS query") + void val_fwdds() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_keyprefetch.rpl: Test validator with key prefetch") + void val_keyprefetch() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_keyprefetch_verify.rpl: Test validator with key prefetch and verify with the anchor") + void val_keyprefetch_verify() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_mal_wc.rpl: Test validator with nodata, wildcards and ENT") + void val_mal_wc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_negcache_ds.rpl: Test validator with negative cache DS response") + void val_negcache_ds() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("we don't do negative caching") + @Test ++ @DisplayName( ++ "val_negcache_dssoa.rpl: Test validator with negative cache DS response with cached SOA") + void val_negcache_dssoa() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("aggressive NSEC is not supported") + @Test ++ @DisplayName( ++ "val_negcache_nodata.rpl: Test validator with negative cache NXDOMAIN response (aggressive NSEC)") + void val_negcache_nodata() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("tests unbound option domain-insecure, not available here") + @Test ++ @DisplayName("val_negcache_nta.rpl: Test to not do aggressive NSEC for domains under NTA") + void val_negcache_nta() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("aggressive NSEC is not supported") + @Test ++ @DisplayName( ++ "val_negcache_nxdomain.rpl: Test validator with negative cache NXDOMAIN response (aggressive NSEC)") + void val_negcache_nxdomain() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("irrelevant - if we wouldn't want AD, we wouldn't be using this stuff") + @Test ++ @DisplayName("val_noadwhennodo.rpl: Test if AD bit is returned on non-DO query.") + void val_noadwhennodo() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nodata.rpl: Test validator with nodata response") + void val_nodata() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nodatawc.rpl: Test validator with wildcard nodata response") + void val_nodatawc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nodatawc_badce.rpl: Test validator with wildcard nodata, bad closest encloser") + void val_nodatawc_badce() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nodatawc_nodeny.rpl: Test validator with wildcard nodata response without qdenial") + void val_nodatawc_nodeny() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nodatawc_one.rpl: Test validator with wildcard nodata response with one NSEC") + void val_nodatawc_one() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nodatawc_wcns.rpl: Test validator with wildcard nodata response from parent zone with SOA") + void val_nodatawc_wcns() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nodatawc_wrongdeleg.rpl: Test validator with wildcard nodata response from parent zone") + void val_nodatawc_wrongdeleg() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nodata_ent.rpl: Test validator with nodata on empty nonterminal response") + void val_nodata_ent() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nodata_entnx.rpl: Test validator with nodata on empty nonterminal response with rcode NXDOMAIN") + void val_nodata_entnx() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nodata_entwc.rpl: Test validator with wildcard nodata on empty nonterminal response") + void val_nodata_entwc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nodata_failsig.rpl: Test validator with nodata response with bogus RRSIG") + void val_nodata_failsig() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nodata_failwc.rpl: Test validator with nodata response with wildcard expanded NSEC record, original NSEC owner does not provide proof for QNAME. CVE-2017-15105 test.") + void val_nodata_failwc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nodata_hasdata.rpl: Test validator with nodata response, that proves the data.") + void val_nodata_hasdata() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nodata_zonecut.rpl: Test validator with nodata response from wrong side of zonecut") + void val_nodata_zonecut() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nokeyprime.rpl: Test validator with failed key prime, no keys.") + void val_nokeyprime() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_b1_nameerror.rpl: Test validator NSEC3 B.1 name error.") + void val_nsec3_b1_nameerror() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b1_nameerror_noce.rpl: Test validator NSEC3 B.1 name error without ce NSEC3.") + void val_nsec3_b1_nameerror_noce() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b1_nameerror_nonc.rpl: Test validator NSEC3 B.1 name error without nc NSEC3.") + void val_nsec3_b1_nameerror_nonc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b1_nameerror_nowc.rpl: Test validator NSEC3 B.1 name error without wc NSEC3.") + void val_nsec3_b1_nameerror_nowc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_b21_nodataent.rpl: Test validator NSEC3 B.2.1 no data empty nonterminal.") + void val_nsec3_b21_nodataent() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b21_nodataent_wr.rpl: Test validator NSEC3 B.2.1 no data empty nonterminal, wrong rr.") + void val_nsec3_b21_nodataent_wr() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_b2_nodata.rpl: Test validator NSEC3 B.2 no data.") + void val_nsec3_b2_nodata() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_b2_nodata_nons.rpl: Test validator NSEC3 B.2 no data, without NSEC3.") + void val_nsec3_b2_nodata_nons() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b3_optout.rpl: Test validator NSEC3 B.3 referral to optout unsigned zone.") + void val_nsec3_b3_optout() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("we don't do negative caching") + @Test ++ @DisplayName( ++ "val_nsec3_b3_optout_negcache.rpl: Test validator NSEC3 B.3 referral optout with negative cache.") + void val_nsec3_b3_optout_negcache() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b3_optout_noce.rpl: Test validator NSEC3 B.3 optout unsigned, without ce.") + void val_nsec3_b3_optout_noce() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b3_optout_nonc.rpl: Test validator NSEC3 B.3 optout unsigned, without nc.") + void val_nsec3_b3_optout_nonc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_b4_wild.rpl: Test validator NSEC3 B.4 wildcard expansion.") + void val_nsec3_b4_wild() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b4_wild_wr.rpl: Test validator NSEC3 B.4 wildcard expansion, wrong NSEC3.") + void val_nsec3_b4_wild_wr() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_b5_wcnodata.rpl: Test validator NSEC3 B.5 wildcard nodata.") + void val_nsec3_b5_wcnodata() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b5_wcnodata_noce.rpl: Test validator NSEC3 B.5 wildcard nodata, without ce.") + void val_nsec3_b5_wcnodata_noce() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b5_wcnodata_nonc.rpl: Test validator NSEC3 B.5 wildcard nodata, without nc.") + void val_nsec3_b5_wcnodata_nonc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_b5_wcnodata_nowc.rpl: Test validator NSEC3 B.5 wildcard nodata, without wc.") + void val_nsec3_b5_wcnodata_nowc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_cnametocnamewctoposwc.rpl: Test validator with a regular cname to wildcard cname to wildcard response") + void val_nsec3_cnametocnamewctoposwc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_cname_ds.rpl: Test validator with NSEC3 CNAME for qtype DS.") + void val_nsec3_cname_ds() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_cname_par.rpl: Test validator with NSEC3 wildcard CNAME to parent.") + void val_nsec3_cname_par() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_cname_sub.rpl: Test validator with NSEC3 wildcard CNAME to subzone.") + void val_nsec3_cname_sub() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_entnodata_optout.rpl: Test validator with NSEC3 response for NODATA ENT with optout.") + void val_nsec3_entnodata_optout() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_entnodata_optout_badopt.rpl: Test validator with NSEC3 response for NODATA ENT with optout.") + void val_nsec3_entnodata_optout_badopt() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_entnodata_optout_match.rpl: Test validator NODATA ENT with nsec3 optout matches the ent.") + void val_nsec3_entnodata_optout_match() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_iter_high.rpl: Test validator with nxdomain NSEC3 with too high iterations") + void val_nsec3_iter_high() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_nodatawccname.rpl: Test validator with nodata NSEC3 abused wildcarded CNAME.") + void val_nsec3_nodatawccname() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_nods.rpl: Test validator with NSEC3 with no DS referral.") + void val_nsec3_nods() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_nods_badopt.rpl: Test validator with NSEC3 with no DS with wrong optout bit.") + void val_nsec3_nods_badopt() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_nods_badsig.rpl: Test validator with NSEC3 with no DS referral with bad signature.") + void val_nsec3_nods_badsig() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("we don't do negative caching") + @Test ++ @DisplayName( ++ "val_nsec3_nods_negcache.rpl: Test validator with NSEC3 with no DS referral from neg cache.") + void val_nsec3_nods_negcache() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_nods_soa.rpl: Test validator with NSEC3 with no DS referral abuse of apex.") + void val_nsec3_nods_soa() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_optout_ad.rpl: Test validator with optout NSEC3 response that gets no AD.") + void val_nsec3_optout_ad() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("more cache stuff") + @Test ++ @DisplayName( ++ "val_nsec3_optout_cache.rpl: Test validator with NSEC3 span change and cache effects.") + void val_nsec3_optout_cache() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nsec3_wcany.rpl: Test validator with NSEC3 wildcard qtype ANY response.") + void val_nsec3_wcany() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nsec3_wcany_nodeny.rpl: Test validator with NSEC3 wildcard qtype ANY without denial.") + void val_nsec3_wcany_nodeny() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nx.rpl: Test validator with nxdomain response") + void val_nx() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nx_failwc.rpl: Test validator with nxdomain response with wildcard expanded NSEC record, original NSEC owner does not provide proof for QNAME. CVE-2017-15105 test.") + void val_nx_failwc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nx_nodeny.rpl: Test validator with nxdomain response missing qname denial") + void val_nx_nodeny() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nx_nowc.rpl: Test validator with nxdomain response missing wildcard denial") + void val_nx_nowc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nx_nsec3_collision.rpl: Test validator with nxdomain NSEC3 with a collision.") + void val_nx_nsec3_collision() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nx_nsec3_collision2.rpl: Test validator with nxdomain NSEC3 with a salt mismatch.") + void val_nx_nsec3_collision2() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nx_nsec3_collision3.rpl: Test validator with nxdomain NSEC3 with a collision.") + void val_nx_nsec3_collision3() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nx_nsec3_collision4.rpl: Test validator with nxdomain NSEC3 with a collision.") + void val_nx_nsec3_collision4() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nx_nsec3_hashalg.rpl: Test validator with unknown NSEC3 hash algorithm.") + void val_nx_nsec3_hashalg() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_nx_nsec3_nsecmix.rpl: Test validator with NSEC3 responses that has an NSEC mixed in.") + void val_nx_nsec3_nsecmix() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nx_nsec3_params.rpl: Test validator with nxdomain NSEC3 several parameters.") + void val_nx_nsec3_params() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_nx_overreach.rpl: Test validator with overreaching NSEC record") + void val_nx_overreach() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_positive.rpl: Test validator with positive response") + void val_positive() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_positive_nosigs.rpl: Test validator with positive response, signatures removed.") + void val_positive_nosigs() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_positive_wc.rpl: Test validator with positive wildcard response") + void val_positive_wc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_positive_wc_nodeny.rpl: Test validator with positive wildcard without qname denial") + void val_positive_wc_nodeny() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_pos_truncns.rpl: Test validator with badly truncated positive response") + void val_pos_truncns() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_qds_badanc.rpl: Test validator with DS query and a bad anchor") + void val_qds_badanc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_qds_oneanc.rpl: Test validator with DS query and one anchor") + void val_qds_oneanc() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_qds_twoanc.rpl: Test validator with DS query and two anchors") + void val_qds_twoanc() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("NSEC records missing for validation, tests caching stuff") + @Test ++ @DisplayName("val_referd.rpl: Test validator with cache referral") + void val_referd() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("we don't do negative caching") + @Test ++ @DisplayName("val_referglue.rpl: Test validator with cache referral with unsigned glue") + void val_referglue() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("we don't do negative caching") + @Test ++ @DisplayName("val_refer_unsignadd.rpl: Test validator with a referral with unsigned additional") + void val_refer_unsignadd() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_rrsig.rpl: Test validator with qtype RRSIG response") + void val_rrsig() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_secds.rpl: Test validator with secure delegation") + void val_secds() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_secds_nosig.rpl: Test validator with no signatures after secure delegation") + void val_secds_nosig() throws ParseException, IOException { + runUnboundTest(); + } + +- @Disabled("tests unbound specific config (stub zones)") + @Test +- void val_stubds() throws ParseException, IOException { ++ @DisplayName("val_spurious_ns.rpl: Test validator with spurious unsigned NS in auth section") ++ void val_spurious_ns() throws ParseException, IOException { + runUnboundTest(); + } + ++ @Disabled("tests unbound specific config (stub zones)") + @Test +- void val_spurious_ns() throws ParseException, IOException { ++ @DisplayName("val_stubds.rpl: Test stub with DS query") ++ void val_stubds() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_stub_noroot.rpl: Test validation of stub zone without root prime.") + void val_stub_noroot() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ta_algo_dnskey.rpl: Test validator with multiple algorithm trust anchor") + void val_ta_algo_dnskey() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName( ++ "val_ta_algo_dnskey_dp.rpl: Test validator with multiple algorithm trust anchor without harden") + void val_ta_algo_dnskey_dp() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ta_algo_missing.rpl: Test validator with multiple algorithm missing one") + void val_ta_algo_missing() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_ta_algo_missing_dp.rpl: Test validator with multiple algorithm missing one") + void val_ta_algo_missing_dp() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_twocname.rpl: Test validator with unsigned CNAME to signed CNAME to data") + void val_twocname() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_unalgo_anchor.rpl: Test validator with unsupported algorithm trust anchor") + void val_unalgo_anchor() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_unalgo_dlv.rpl: Test validator with unknown algorithm DLV anchor") + void val_unalgo_dlv() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_unalgo_ds.rpl: Test validator with unknown algorithm delegation") + void val_unalgo_ds() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_unsecds.rpl: Test validator with insecure delegation") + void val_unsecds() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("we don't do negative caching") + @Test ++ @DisplayName( ++ "val_unsecds_negcache.rpl: Test validator with insecure delegation and DS negative cache") + void val_unsecds_negcache() throws ParseException, IOException { + runUnboundTest(); + } + + @Disabled("tests the iterative resolver") + @Test ++ @DisplayName("val_unsecds_qtypeds.rpl: Test validator with insecure delegation and qtype DS.") + void val_unsecds_qtypeds() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_unsec_cname.rpl: Test validator with DS, unsec, cname sequence.") + void val_unsec_cname() throws ParseException, IOException { + runUnboundTest(); + } + + @Test ++ @DisplayName("val_wild_pos.rpl: Test validator with direct wildcard positive response") + void val_wild_pos() throws ParseException, IOException { + runUnboundTest(); + } +diff --git a/src/test/java/org/xbill/DNS/lookup/LookupResultTest.java b/src/test/java/org/xbill/DNS/lookup/LookupResultTest.java +index 2ce793d..b571f27 100644 +--- a/src/test/java/org/xbill/DNS/lookup/LookupResultTest.java ++++ b/src/test/java/org/xbill/DNS/lookup/LookupResultTest.java +@@ -3,34 +3,101 @@ package org.xbill.DNS.lookup; + + import static java.util.Collections.singletonList; + import static org.junit.jupiter.api.Assertions.assertEquals; ++import static org.junit.jupiter.api.Assertions.assertNull; + import static org.junit.jupiter.api.Assertions.assertThrows; + + import java.net.InetAddress; ++import java.util.Collections; + import org.junit.jupiter.api.Test; ++import org.junit.jupiter.params.ParameterizedTest; ++import org.junit.jupiter.params.provider.ValueSource; + import org.xbill.DNS.ARecord; ++import org.xbill.DNS.CNAMERecord; + import org.xbill.DNS.DClass; + import org.xbill.DNS.Name; + import org.xbill.DNS.Record; + + class LookupResultTest { ++ private static final LookupResult PREVIOUS = new LookupResult(false); ++ private static final ARecord A_RECORD = ++ new ARecord(Name.fromConstantString("a."), DClass.IN, 0, InetAddress.getLoopbackAddress()); ++ + @Test + void ctor_nullRecords() { +- assertThrows(NullPointerException.class, () -> new LookupResult(null, null)); ++ assertThrows( ++ NullPointerException.class, ++ () -> new LookupResult(PREVIOUS, null, null, false, null, Collections.emptyList())); ++ } ++ ++ @Test ++ void ctor_nullAliases() { ++ assertThrows( ++ NullPointerException.class, ++ () -> new LookupResult(PREVIOUS, null, null, false, Collections.emptyList(), null)); ++ } ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void ctor_authOnly(boolean isAuthenticated) { ++ LookupResult lookupResult = new LookupResult(isAuthenticated); ++ assertEquals(isAuthenticated, lookupResult.isAuthenticated()); ++ assertEquals(0, lookupResult.getAliases().size()); ++ assertEquals(0, lookupResult.getRecords().size()); ++ assertEquals(0, lookupResult.getQueryResponsePairs().size()); ++ } ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void ctor_singleRecord(boolean isAuthenticated) { ++ LookupResult lookupResult = new LookupResult(A_RECORD, isAuthenticated, A_RECORD); ++ assertEquals(isAuthenticated, lookupResult.isAuthenticated()); ++ assertEquals(0, lookupResult.getAliases().size()); ++ assertEquals(1, lookupResult.getRecords().size()); ++ assertEquals(1, lookupResult.getQueryResponsePairs().size()); ++ assertNull(lookupResult.getQueryResponsePairs().get(A_RECORD)); + } + + @Test + void getResult() { +- Record record = +- new ARecord(Name.fromConstantString("a."), DClass.IN, 0, InetAddress.getLoopbackAddress()); +- LookupResult lookupResult = new LookupResult(singletonList(record), null); +- assertEquals(singletonList(record), lookupResult.getRecords()); ++ LookupResult lookupResult = ++ new LookupResult( ++ PREVIOUS, null, null, false, singletonList(A_RECORD), Collections.emptyList()); ++ assertEquals(singletonList(A_RECORD), lookupResult.getRecords()); + } + + @Test + void getAliases() { + Name name = Name.fromConstantString("b."); + Record record = new ARecord(name, DClass.IN, 0, InetAddress.getLoopbackAddress()); +- LookupResult lookupResult = new LookupResult(singletonList(record), singletonList(name)); ++ LookupResult lookupResult = ++ new LookupResult(PREVIOUS, null, null, false, singletonList(record), singletonList(name)); + assertEquals(singletonList(name), lookupResult.getAliases()); + } ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void isAuthenticated(boolean isAuthenticated) { ++ LookupResult lookupResult = ++ new LookupResult( ++ new LookupResult(isAuthenticated), ++ null, ++ null, ++ isAuthenticated, ++ singletonList(A_RECORD), ++ Collections.emptyList()); ++ assertEquals(isAuthenticated, lookupResult.isAuthenticated()); ++ } ++ ++ @ParameterizedTest ++ @ValueSource(booleans = {false, true}) ++ void isAuthenticatedRequiresAllForTrue(boolean isAuthenticated) { ++ Name nameA = Name.fromConstantString("a."); ++ Name nameB = Name.fromConstantString("b."); ++ Record cname = new CNAMERecord(nameA, DClass.IN, 0, nameB); ++ Record a = new ARecord(nameB, DClass.IN, 0, InetAddress.getLoopbackAddress()); ++ LookupResult lookupResult1 = new LookupResult(isAuthenticated); ++ LookupResult lookupResult2 = ++ new LookupResult(lookupResult1, cname, null, true, singletonList(a), singletonList(nameA)); ++ assertEquals(isAuthenticated, lookupResult2.isAuthenticated()); ++ } + } +diff --git a/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java b/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java +index a24c8d9..ef65d27 100644 +--- a/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java ++++ b/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java +@@ -5,12 +5,19 @@ import static java.lang.String.format; + import static java.util.Arrays.asList; + import static java.util.Collections.emptyList; + import static java.util.Collections.singletonList; ++import static org.assertj.core.api.Assertions.assertThat; ++import static org.assertj.core.api.Assertions.assertThatThrownBy; ++import static org.junit.jupiter.api.Assertions.assertAll; + import static org.junit.jupiter.api.Assertions.assertEquals; +-import static org.junit.jupiter.api.Assertions.assertThrows; ++import static org.junit.jupiter.api.Assertions.assertTrue; ++import static org.junitpioneer.jupiter.cartesian.CartesianTest.Enum; ++import static org.junitpioneer.jupiter.cartesian.CartesianTest.Values; + import static org.mockito.ArgumentMatchers.any; + import static org.mockito.ArgumentMatchers.anyInt; + import static org.mockito.Mockito.inOrder; ++import static org.mockito.Mockito.lenient; + import static org.mockito.Mockito.mock; ++import static org.mockito.Mockito.spy; + import static org.mockito.Mockito.times; + import static org.mockito.Mockito.verify; + import static org.mockito.Mockito.verifyNoMoreInteractions; +@@ -20,9 +27,11 @@ import static org.xbill.DNS.LookupTest.DUMMY_NAME; + import static org.xbill.DNS.LookupTest.LONG_LABEL; + import static org.xbill.DNS.LookupTest.answer; + import static org.xbill.DNS.LookupTest.fail; ++import static org.xbill.DNS.LookupTest.multiAnswer; + import static org.xbill.DNS.Type.A; + import static org.xbill.DNS.Type.AAAA; + import static org.xbill.DNS.Type.CNAME; ++import static org.xbill.DNS.Type.DNAME; + import static org.xbill.DNS.Type.MX; + + import java.io.IOException; +@@ -44,17 +53,19 @@ import org.junit.jupiter.api.AfterEach; + import org.junit.jupiter.api.BeforeEach; + import org.junit.jupiter.api.Test; + import org.junit.jupiter.api.extension.ExtendWith; +-import org.junit.jupiter.api.function.Executable; + import org.junit.jupiter.api.io.TempDir; + import org.junit.jupiter.params.ParameterizedTest; + import org.junit.jupiter.params.provider.CsvSource; ++import org.junit.jupiter.params.provider.EnumSource; + import org.junit.jupiter.params.provider.ValueSource; ++import org.junitpioneer.jupiter.cartesian.CartesianTest; + import org.mockito.ArgumentCaptor; + import org.mockito.InOrder; + import org.mockito.Mock; + import org.mockito.junit.jupiter.MockitoExtension; + import org.xbill.DNS.*; + import org.xbill.DNS.Record; ++import org.xbill.DNS.WireParseException; + import org.xbill.DNS.hosts.HostsFileParser; + + @ExtendWith(MockitoExtension.class) +@@ -65,7 +76,9 @@ class LookupSessionTest { + + private static final ARecord LOOPBACK_A = + new ARecord(DUMMY_NAME, IN, 3600, InetAddress.getLoopbackAddress()); ++ private static final ARecord EXAMPLE_A = (ARecord) LOOPBACK_A.withName(name("example.com.")); + private static final AAAARecord LOOPBACK_AAAA; ++ private static final String INVALID_SERVER_RESPONSE_MESSAGE = "refusing to return it"; + private HostsFileParser lookupSessionTestHostsFileParser; + + static { +@@ -109,6 +122,31 @@ class LookupSessionTest { + verify(mockResolver).sendAsync(any(), any(Executor.class)); + } + ++ @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") ++ void lookupAsync_absoluteQueryNoExtra( ++ @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) ++ throws ExecutionException, InterruptedException { ++ wireUpMockResolver( ++ mockResolver, query -> multiAnswer(query, name -> new Record[] {LOOPBACK_A, EXAMPLE_A})); ++ ++ LookupSession lookupSession = lookupSession(useCache).irrelevantRecordMode(mode).build(); ++ CompletableFuture future = ++ lookupSession.lookupAsync(name("a.b."), A, IN).toCompletableFuture(); ++ if (mode == IrrelevantRecordMode.THROW) { ++ assertThatThrownBy(future::get) ++ .cause() ++ .isInstanceOf(LookupFailedException.class) ++ .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); ++ } else { ++ LookupResult result = future.get(); ++ assertThat(result.getAliases()).isEmpty(); ++ assertThat(result.getRecords()).containsExactly(LOOPBACK_A.withName(name("a.b."))); ++ } ++ ++ assertCacheUnused(useCache, mode, lookupSession); ++ verify(mockResolver).sendAsync(any(), any(Executor.class)); ++ } ++ + @Test + void lookupAsync_absoluteQuery_defaultClass() throws InterruptedException, ExecutionException { + wireUpMockResolver(mockResolver, query -> answer(query, name -> LOOPBACK_A)); +@@ -145,10 +183,14 @@ class LookupSessionTest { + when(mockHosts.getAddressForHost(any(), anyInt())).thenThrow(IOException.class); + LookupSession lookupSession = + LookupSession.builder().resolver(mockResolver).hostsFileParser(mockHosts).build(); +- CompletionStage resultFuture = +- lookupSession.lookupAsync(name("kubernetes.docker.internal."), A, IN); + +- assertThrowsCause(NoSuchDomainException.class, () -> resultFuture.toCompletableFuture().get()); ++ assertThatThrownBy( ++ lookupSession ++ .lookupAsync(name("kubernetes.docker.internal."), A, IN) ++ .toCompletableFuture() ++ ::get) ++ .cause() ++ .isInstanceOf(NoSuchDomainException.class); + } + + @Test +@@ -159,10 +201,14 @@ class LookupSessionTest { + .resolver(mockResolver) + .hostsFileParser(lookupSessionTestHostsFileParser) + .build(); +- CompletionStage resultFuture = +- lookupSession.lookupAsync(name("kubernetes.docker.internal."), MX, IN); + +- assertThrowsCause(NoSuchDomainException.class, () -> resultFuture.toCompletableFuture().get()); ++ assertThatThrownBy( ++ lookupSession ++ .lookupAsync(name("kubernetes.docker.internal."), MX, IN) ++ .toCompletableFuture() ++ ::get) ++ .cause() ++ .isInstanceOf(NoSuchDomainException.class); + verify(mockResolver).sendAsync(any(), any(Executor.class)); + } + +@@ -340,11 +386,12 @@ class LookupSessionTest { + when(mockCache.lookupRecords(name("host.tld."), A, Credibility.NORMAL)) + .thenReturn(mock(SetResponse.class)); + +- SetResponse second = mock(SetResponse.class); +- when(second.isSuccessful()).thenReturn(true); +- when(second.answers()) ++ SetResponse anotherTldResponse = mock(SetResponse.class); ++ when(anotherTldResponse.isSuccessful()).thenReturn(true); ++ when(anotherTldResponse.answers()) + .thenReturn(singletonList(new RRset(LOOPBACK_A.withName(name("another.tld."))))); +- when(mockCache.lookupRecords(name("another.tld."), A, Credibility.NORMAL)).thenReturn(second); ++ when(mockCache.lookupRecords(name("another.tld."), A, Credibility.NORMAL)) ++ .thenReturn(anotherTldResponse); + + LookupSession lookupSession = + LookupSession.builder() +@@ -417,11 +464,7 @@ class LookupSessionTest { + }; + wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); + +- LookupSession lookupSession = +- useCache +- ? LookupSession.builder().cache(new Cache()).resolver(mockResolver).build() +- : LookupSession.builder().resolver(mockResolver).build(); +- ++ LookupSession lookupSession = lookupSession(useCache).build(); + CompletionStage resultFuture = lookupSession.lookupAsync(name("cname.a."), A, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); +@@ -429,17 +472,17 @@ class LookupSessionTest { + assertEquals( + Stream.of(name("cname.a."), name("cname.b.")).collect(Collectors.toList()), + result.getAliases()); ++ if (useCache) { ++ assertEquals(3, lookupSession.getCache(IN).getSize()); ++ } + verify(mockResolver, times(3)).sendAsync(any(), any(Executor.class)); + } + +- @ParameterizedTest +- @CsvSource({ +- "false,false", +- "true,false", +- "false,true", +- "true,true", +- }) +- void lookupAsync_twoDnameRedirectOneQuery(boolean useCache, boolean includeSyntheticCnames) ++ @CartesianTest(name = "useCache={0}, includeSyntheticCnames={1}, irrelevantRecordMode={2}") ++ void lookupAsync_twoDnameRedirectOneQuery( ++ @Values(booleans = {true, false}) boolean useCache, ++ @Values(booleans = {true, false}) boolean includeSyntheticCnames, ++ @Enum IrrelevantRecordMode mode) + throws Exception { + wireUpMockResolver( + mockResolver, +@@ -459,11 +502,7 @@ class LookupSessionTest { + return answer; + }); + +- LookupSession lookupSession = +- useCache +- ? LookupSession.builder().cache(new Cache()).resolver(mockResolver).build() +- : LookupSession.builder().resolver(mockResolver).build(); +- ++ LookupSession lookupSession = lookupSession(useCache).irrelevantRecordMode(mode).build(); + CompletionStage resultFuture = + lookupSession.lookupAsync(name("www.example.org."), A, IN); + +@@ -473,6 +512,9 @@ class LookupSessionTest { + Stream.of(name("www.example.org."), name("www.example.net."), name("www.example.com.")) + .collect(Collectors.toList()), + result.getAliases()); ++ if (useCache) { ++ assertEquals(4 + (includeSyntheticCnames ? 2 : 0), lookupSession.getCache(IN).getSize()); ++ } + verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); + } + +@@ -490,11 +532,7 @@ class LookupSessionTest { + return answer; + }); + +- LookupSession lookupSession = +- useCache +- ? LookupSession.builder().cache(new Cache()).resolver(mockResolver).build() +- : LookupSession.builder().resolver(mockResolver).build(); +- ++ LookupSession lookupSession = lookupSession(useCache).build(); + CompletionStage resultFuture = lookupSession.lookupAsync(name("cname.a."), A, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); +@@ -530,11 +568,7 @@ class LookupSessionTest { + return answer; + }); + +- LookupSession lookupSession = +- useCache +- ? LookupSession.builder().cache(new Cache()).resolver(mockResolver).build() +- : LookupSession.builder().resolver(mockResolver).build(); +- ++ LookupSession lookupSession = lookupSession(useCache).build(); + CompletionStage resultFuture = lookupSession.lookupAsync(name("cname.a."), A, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); +@@ -542,6 +576,9 @@ class LookupSessionTest { + assertEquals( + Stream.of(name("cname.a."), name("cname.b.")).collect(Collectors.toList()), + result.getAliases()); ++ if (useCache) { ++ assertEquals(3, lookupSession.getCache(IN).getSize()); ++ } + verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); + } + +@@ -586,37 +623,69 @@ class LookupSessionTest { + } + }); + +- LookupSession lookupSession = +- useCache +- ? LookupSession.builder().cache(new Cache()).resolver(mockResolver).build() +- : LookupSession.builder().resolver(mockResolver).build(); +- +- CompletionStage resultFuture = +- lookupSession.lookupAsync(name("cname.r."), Type.value(type), IN); ++ LookupSession lookupSession = lookupSession(useCache).build(); ++ CompletableFuture future = ++ lookupSession.lookupAsync(name("cname.r."), Type.value(type), IN).toCompletableFuture(); + +- CompletableFuture future = resultFuture.toCompletableFuture(); + if (rcode.equals("NXDOMAIN")) { +- assertThrowsCause(NoSuchDomainException.class, future::get); ++ assertThatThrownBy(future::get).cause().isInstanceOf(NoSuchDomainException.class); + } else { + LookupResult result = future.get(); +- assertEquals(0, result.getRecords().size()); ++ assertThat(result.getRecords()).isEmpty(); + } + verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); + } + + @Test + void lookupAsync_simpleCnameRedirect() throws Exception { ++ Name cname = name("cname.r."); ++ Name target = name("a.b."); + Function nameToRecord = +- name -> name("cname.r.").equals(name) ? cname("cname.r.", "a.b.") : LOOPBACK_A; ++ name -> cname.equals(name) ? cname(cname, target) : LOOPBACK_A; + wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); + + LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); + +- CompletionStage resultFuture = lookupSession.lookupAsync(name("cname.r."), A, IN); ++ CompletionStage resultFuture = lookupSession.lookupAsync(cname, A, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); + assertEquals(singletonList(LOOPBACK_A.withName(name("a.b."))), result.getRecords()); +- assertEquals(singletonList(name("cname.r.")), result.getAliases()); ++ assertEquals(singletonList(cname), result.getAliases()); ++ verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); ++ } ++ ++ @ParameterizedTest ++ @EnumSource(value = IrrelevantRecordMode.class) ++ void lookupAsync_simpleCnameRedirectNoExtra(IrrelevantRecordMode mode) ++ throws ExecutionException, InterruptedException { ++ Name query = name("cname.r."); ++ Name target = name("a.b."); ++ Function nameToRecord = ++ name -> ++ query.equals(name) ++ ? new Record[] {cname(query, target)} ++ : new Record[] { ++ LOOPBACK_A, EXAMPLE_A, ++ }; ++ wireUpMockResolver(mockResolver, q -> multiAnswer(q, nameToRecord)); ++ ++ LookupSession lookupSession = ++ LookupSession.builder().resolver(mockResolver).irrelevantRecordMode(mode).build(); ++ ++ CompletableFuture f = ++ lookupSession.lookupAsync(query, A, IN).toCompletableFuture(); ++ if (mode == IrrelevantRecordMode.REMOVE) { ++ LookupResult result = f.get(); ++ assertThat(result.getRecords()).hasSize(1).containsExactly(LOOPBACK_A.withName(target)); ++ } else { ++ assertThatThrownBy(f::get) ++ .cause() ++ .isInstanceOf(LookupFailedException.class) ++ .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE) ++ .rootCause() ++ .isInstanceOf(WireParseException.class); ++ } ++ + verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); + } + +@@ -637,16 +706,95 @@ class LookupSessionTest { + verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); + } + ++ @Test ++ void lookupAsync_dnameQuery() throws Exception { ++ Name query = name("dname.r."); ++ DNAMERecord response = dname(query, "a.b."); ++ Function nameToRecord = name -> name.equals(query) ? response : LOOPBACK_A; ++ wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); ++ ++ LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); ++ ++ CompletionStage resultFuture = lookupSession.lookupAsync(query, DNAME, IN); ++ ++ LookupResult result = resultFuture.toCompletableFuture().get(); ++ assertEquals(singletonList(response), result.getRecords()); ++ assertEquals(emptyList(), result.getAliases()); ++ verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); ++ } ++ ++ @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") ++ void lookupAsync_cnameQueryExtra( ++ @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) ++ throws ExecutionException, InterruptedException { ++ Name query = name("cname.r."); ++ Name target = name("a.b."); ++ CNAMERecord response1 = cname(query, target); ++ CNAMERecord response2 = cname(name("additional.r."), target); ++ Function nameToRecord = ++ name -> ++ query.equals(name) ? new Record[] {response1, response2} : new Record[] {LOOPBACK_A}; ++ wireUpMockResolver(mockResolver, q -> multiAnswer(q, nameToRecord)); ++ ++ LookupSession lookupSession = lookupSession(useCache, mode).build(); ++ CompletableFuture future = ++ lookupSession.lookupAsync(query, CNAME, IN).toCompletableFuture(); ++ if (mode == IrrelevantRecordMode.THROW) { ++ assertThatThrownBy(future::get) ++ .cause() ++ .isInstanceOf(LookupFailedException.class) ++ .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); ++ } else { ++ LookupResult result = future.get(); ++ assertThat(result.getAliases()).isEmpty(); ++ assertThat(result.getRecords()).containsExactly(cname(query, target)); ++ } ++ ++ assertCacheUnused(useCache, mode, lookupSession); ++ verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); ++ } ++ ++ @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") ++ void lookupAsync_dnameQueryExtra( ++ @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) ++ throws ExecutionException, InterruptedException { ++ Name query = name("cname.r."); ++ Name target = name("a.b."); ++ DNAMERecord response1 = dname(query, target); ++ DNAMERecord response2 = dname(name("additional.r."), target); ++ Function nameToRecord = ++ name -> ++ query.equals(name) ? new Record[] {response1, response2} : new Record[] {LOOPBACK_A}; ++ wireUpMockResolver(mockResolver, q -> multiAnswer(q, nameToRecord)); ++ ++ LookupSession lookupSession = lookupSession(useCache, mode).build(); ++ CompletableFuture future = ++ lookupSession.lookupAsync(query, DNAME, IN).toCompletableFuture(); ++ if (mode == IrrelevantRecordMode.THROW) { ++ assertThatThrownBy(future::get) ++ .cause() ++ .isInstanceOf(LookupFailedException.class) ++ .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); ++ } else { ++ LookupResult result = future.get(); ++ assertThat(result.getAliases()).isEmpty(); ++ assertThat(result.getRecords()).containsExactly(response1); ++ } ++ ++ assertCacheUnused(useCache, mode, lookupSession); ++ verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); ++ } ++ + @Test + void lookupAsync_simpleDnameRedirect() throws Exception { ++ Name query = name("x.y.to.dname."); + Function nameToRecord = +- n -> name("x.y.to.dname.").equals(n) ? dname("to.dname.", "to.a.") : LOOPBACK_A; ++ name -> name.equals(query) ? dname("to.dname.", "to.a.") : LOOPBACK_A; + wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); + + LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); + +- CompletionStage resultFuture = +- lookupSession.lookupAsync(name("x.y.to.dname."), A, IN); ++ CompletionStage resultFuture = lookupSession.lookupAsync(query, A, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); + assertEquals(singletonList(LOOPBACK_A.withName(name("x.y.to.a."))), result.getRecords()); +@@ -654,23 +802,228 @@ class LookupSessionTest { + } + + @Test +- void lookupAsync_redirectLoop() { +- Function nameToRecord = +- name -> name("a.b.").equals(name) ? cname("a.", "b.") : cname("b.", "a."); +- wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); ++ void lookupAsync_simpleDnameRedirectSynthesizedCname() throws Exception { ++ Name query = name("x.y.example.org."); ++ wireUpMockResolver( ++ mockResolver, ++ q -> ++ multiAnswer( ++ q, ++ name -> ++ new Record[] { ++ dname("example.org.", "example.net."), ++ cname("x.y.example.org.", "x.y.example.net."), ++ LOOPBACK_A.withName(name("x.y.example.net.")), ++ })); + +- LookupSession lookupSession = +- LookupSession.builder().resolver(mockResolver).maxRedirects(2).build(); ++ LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); + +- CompletionStage resultFuture = +- lookupSession.lookupAsync(name("first.example.com."), A, IN); ++ CompletionStage resultFuture = lookupSession.lookupAsync(query, A, IN); + +- assertThrowsCause( +- RedirectOverflowException.class, () -> resultFuture.toCompletableFuture().get()); +- verify(mockResolver, times(3)).sendAsync(any(), any(Executor.class)); ++ LookupResult result = resultFuture.toCompletableFuture().get(); ++ assertEquals(singletonList(LOOPBACK_A.withName(name("x.y.example.net."))), result.getRecords()); ++ assertEquals(singletonList(name("x.y.example.org.")), result.getAliases()); ++ verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); + } + + @ParameterizedTest ++ @CsvSource( ++ value = { ++ "x.y.example.com.,x.y.example.org.,REMOVE", ++ "x.y.example.com.,x.y.example.org.,THROW", ++ "x.y.example.org.,x.y.example.com.,REMOVE", ++ "x.y.example.org.,x.y.example.com.,THROW", ++ }) ++ void lookupAsync_simpleDnameRedirectWrongSynthesizedCname( ++ String from, String to, IrrelevantRecordMode mode) ++ throws ExecutionException, InterruptedException { ++ Name query = name("x.y.example.org."); ++ wireUpMockResolver( ++ mockResolver, ++ q -> ++ multiAnswer( ++ q, ++ name -> ++ new Record[] { ++ // Correct ++ dname("example.org.", "example.net."), ++ // Extra and wrong ++ cname(from, to), ++ // Correct ++ LOOPBACK_A.withName(name("x.y.example.net.")), ++ // Extra and wrong ++ LOOPBACK_A.withName(name(to)), ++ })); ++ ++ LookupSession lookupSession = ++ LookupSession.builder().resolver(mockResolver).irrelevantRecordMode(mode).build(); ++ ++ CompletableFuture future = ++ lookupSession.lookupAsync(query, A, IN).toCompletableFuture(); ++ if (mode == IrrelevantRecordMode.THROW) { ++ assertThatThrownBy(future::get) ++ .cause() ++ .isInstanceOf(LookupFailedException.class) ++ .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); ++ } else { ++ LookupResult result = future.get(); ++ assertThat(result.getAliases()).containsExactly(name("x.y.example.org.")); ++ assertThat(result.getRecords()) ++ .containsExactly(LOOPBACK_A.withName(name("x.y.example.net."))); ++ } ++ verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); ++ } ++ ++ @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") ++ void lookupAsync_simpleDnameRedirectNoExtra( ++ @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) ++ throws ExecutionException, InterruptedException { ++ Name queryName = name("x.y.to.dname."); ++ wireUpMockResolver( ++ mockResolver, ++ question -> ++ multiAnswer( ++ question, ++ name -> ++ name.equals(queryName) ++ ? new Record[] {dname("to.dname.", "to.a.")} ++ : new Record[] { ++ // LOOPBACK_A will be transformed to 'x.y.to.a.' ++ LOOPBACK_A, EXAMPLE_A, ++ })); ++ ++ LookupSession lookupSession = lookupSession(useCache, mode).build(); ++ CompletableFuture future = ++ lookupSession.lookupAsync(queryName, A, IN).toCompletableFuture(); ++ if (mode == IrrelevantRecordMode.THROW) { ++ assertThatThrownBy(future::get) ++ .cause() ++ .isInstanceOf(LookupFailedException.class) ++ .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); ++ } else { ++ LookupResult result = future.get(); ++ assertAll( ++ () -> { ++ assertThat(result.getAliases()).containsExactly(name("x.y.to.dname.")); ++ assertThat(result.getRecords()).containsExactly(LOOPBACK_A.withName(name("x.y.to.a."))); ++ }); ++ } ++ ++ if (useCache && mode == IrrelevantRecordMode.THROW) { ++ // Verify that the invalid response didn't end up in the cache ++ Cache cache = lookupSession.getCache(IN); ++ verify(cache, times(1)).addMessage(any(Message.class)); ++ assertEquals(1, cache.getSize()); ++ assertTrue(cache.lookupRecords(name("example.com."), A, Credibility.NORMAL).isUnknown()); ++ } ++ ++ verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); ++ } ++ ++ @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") ++ void lookupAsync_simpleCnameWrongInitial( ++ @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) ++ throws ExecutionException, InterruptedException { ++ Name query = name("first.example.com."); ++ wireUpMockResolver(mockResolver, q -> answer(q, name -> cname("a.", "b."))); ++ ++ LookupSession lookupSession = lookupSession(useCache).irrelevantRecordMode(mode).build(); ++ CompletableFuture future = ++ lookupSession.lookupAsync(query, A, IN).toCompletableFuture(); ++ if (mode == IrrelevantRecordMode.THROW) { ++ assertThatThrownBy(future::get) ++ .cause() ++ .isInstanceOf(LookupFailedException.class) ++ .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); ++ } else { ++ LookupResult result = future.get(); ++ assertThat(result.getAliases()).isEmpty(); ++ assertThat(result.getRecords()).isEmpty(); ++ } ++ ++ assertCacheUnused(useCache, mode, lookupSession); ++ ++ verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); ++ } ++ ++ @CartesianTest(name = "useCache={0}, irrelevantRecordMode={1}") ++ void lookupAsync_simpleDnameWrongInitial( ++ @Values(booleans = {true, false}) boolean useCache, @Enum IrrelevantRecordMode mode) ++ throws ExecutionException, InterruptedException { ++ Name query = name("first.example.com."); ++ wireUpMockResolver(mockResolver, q -> answer(q, name -> dname("a.", "b."))); ++ ++ LookupSession lookupSession = ++ lookupSession(useCache, mode == IrrelevantRecordMode.THROW) ++ .irrelevantRecordMode(mode) ++ .build(); ++ ++ CompletableFuture future = ++ lookupSession.lookupAsync(query, A, IN).toCompletableFuture(); ++ if (mode == IrrelevantRecordMode.THROW) { ++ assertThatThrownBy(future::get) ++ .cause() ++ .isInstanceOf(LookupFailedException.class) ++ .hasMessageContaining(INVALID_SERVER_RESPONSE_MESSAGE); ++ } else { ++ LookupResult result = future.get(); ++ assertThat(result.getAliases()).isEmpty(); ++ assertThat(result.getRecords()).isEmpty(); ++ } ++ ++ assertCacheUnused(useCache, mode, lookupSession); ++ verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); ++ } ++ ++ private static void assertCacheUnused( ++ boolean useCache, IrrelevantRecordMode mode, LookupSession lookupSession) { ++ if (useCache && mode == IrrelevantRecordMode.THROW) { ++ // Verify that the invalid response didn't end up in the cache ++ Cache cache = lookupSession.getCache(IN); ++ verify(cache, times(0)).addMessage(any(Message.class)); ++ assertEquals(0, cache.getSize()); ++ } ++ } ++ ++ @CartesianTest(name = "maxRedirects={0}, irrelevantRecordMode={1}") ++ void lookupAsync_redirectLoop( ++ @Values(ints = {3, 4}) int maxRedirects, @Enum IrrelevantRecordMode mode) { ++ CNAMERecord cnameA = cname("a.", "b."); ++ CNAMERecord cnameB = cname("b.", "c."); ++ CNAMERecord cnameC = cname("c.", "d."); ++ CNAMERecord cnameD = cname("d.", "a."); ++ Function nameToRecord = ++ name -> { ++ if (name.equals(cnameA.getName())) { ++ return cnameA; ++ } else if (name.equals(cnameB.getName())) { ++ return cnameB; ++ } else if (name.equals(cnameC.getName())) { ++ return cnameC; ++ } else if (name.equals(cnameD.getName())) { ++ return cnameD; ++ } else { ++ throw new RuntimeException("Unexpected query"); ++ } ++ }; ++ wireUpMockResolver(mockResolver, q -> answer(q, nameToRecord)); ++ LookupSession lookupSession = ++ LookupSession.builder() ++ .maxRedirects(maxRedirects) ++ .resolver(mockResolver) ++ .irrelevantRecordMode(mode) ++ .build(); ++ ++ Class expected = ++ maxRedirects == 3 ? RedirectOverflowException.class : RedirectLoopException.class; ++ assertThatThrownBy( ++ lookupSession.lookupAsync(cnameA.getName(), A, IN).toCompletableFuture()::get) ++ .cause() ++ .isInstanceOf(expected); ++ verify(mockResolver, times(maxRedirects)).sendAsync(any(), any(Executor.class)); ++ } ++ ++ @ParameterizedTest(name = "maxRedirects={0}") + @ValueSource(ints = {3, 4}) + void lookupAsync_redirectLoopOneAnswer(int maxRedirects) { + wireUpMockResolver( +@@ -688,10 +1041,11 @@ class LookupSessionTest { + LookupSession lookupSession = + LookupSession.builder().resolver(mockResolver).maxRedirects(maxRedirects).build(); + +- CompletionStage resultFuture = lookupSession.lookupAsync(name("a."), A, IN); +- +- assertThrowsCause( +- RedirectOverflowException.class, () -> resultFuture.toCompletableFuture().get()); ++ Class expected = ++ maxRedirects == 3 ? RedirectOverflowException.class : RedirectLoopException.class; ++ assertThatThrownBy(lookupSession.lookupAsync(name("a."), A, IN).toCompletableFuture()::get) ++ .cause() ++ .isInstanceOf(expected); + verify(mockResolver, times(1)).sendAsync(any(), any(Executor.class)); + } + +@@ -703,7 +1057,7 @@ class LookupSessionTest { + CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); +- assertEquals(0, result.getRecords().size()); ++ assertThat(result.getRecords()).isEmpty(); + verify(mockResolver).sendAsync(any(), any(Executor.class)); + } + +@@ -712,9 +1066,9 @@ class LookupSessionTest { + wireUpMockResolver(mockResolver, q -> fail(q, Rcode.NXDOMAIN)); + + LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); +- CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); +- +- assertThrowsCause(NoSuchDomainException.class, () -> resultFuture.toCompletableFuture().get()); ++ assertThatThrownBy(lookupSession.lookupAsync(name("a.b."), A, IN).toCompletableFuture()::get) ++ .cause() ++ .isInstanceOf(NoSuchDomainException.class); + verify(mockResolver).sendAsync(any(), any(Executor.class)); + } + +@@ -723,9 +1077,9 @@ class LookupSessionTest { + wireUpMockResolver(mockResolver, q -> fail(q, Rcode.SERVFAIL)); + + LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); +- CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); +- +- assertThrowsCause(ServerFailedException.class, () -> resultFuture.toCompletableFuture().get()); ++ assertThatThrownBy(lookupSession.lookupAsync(name("a.b."), A, IN).toCompletableFuture()::get) ++ .cause() ++ .isInstanceOf(ServerFailedException.class); + verify(mockResolver).sendAsync(any(), any(Executor.class)); + } + +@@ -734,9 +1088,9 @@ class LookupSessionTest { + wireUpMockResolver(mockResolver, q -> fail(q, Rcode.NOTIMP)); + + LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); +- CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); +- +- assertThrowsCause(LookupFailedException.class, () -> resultFuture.toCompletableFuture().get()); ++ assertThatThrownBy(lookupSession.lookupAsync(name("a.b."), A, IN).toCompletableFuture()::get) ++ .cause() ++ .isInstanceOf(LookupFailedException.class); + verify(mockResolver).sendAsync(any(), any(Executor.class)); + } + +@@ -745,9 +1099,9 @@ class LookupSessionTest { + wireUpMockResolver(mockResolver, q -> fail(q, Rcode.NXRRSET)); + + LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); +- CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); +- +- assertThrowsCause(NoSuchRRSetException.class, () -> resultFuture.toCompletableFuture().get()); ++ assertThatThrownBy(lookupSession.lookupAsync(name("a.b."), A, IN).toCompletableFuture()::get) ++ .cause() ++ .isInstanceOf(NoSuchRRSetException.class); + verify(mockResolver).sendAsync(any(), any(Executor.class)); + } + +@@ -758,36 +1112,35 @@ class LookupSessionTest { + + LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); + Name toLookup = name(format("%s.%s.%s.to.dname.", LONG_LABEL, LONG_LABEL, LONG_LABEL)); +- CompletionStage resultFuture = lookupSession.lookupAsync(toLookup, A, IN); +- +- assertThrowsCause( +- InvalidZoneDataException.class, () -> resultFuture.toCompletableFuture().get()); ++ assertThatThrownBy(lookupSession.lookupAsync(toLookup, A, IN).toCompletableFuture()::get) ++ .cause() ++ .isInstanceOf(InvalidZoneDataException.class); + verify(mockResolver).sendAsync(any(), any(Executor.class)); + } + + @Test +- void lookupAsync_MultipleCNAMEs() { ++ void lookupAsync_MultipleCNAMEs() throws ExecutionException, InterruptedException { ++ Record testQuestion = Record.newRecord(name("a.b."), A, IN); + // According to https://docstore.mik.ua/orelly/networking_2ndEd/dns/ch10_07.htm this is +- // apparently something that BIND 4 did. +- wireUpMockResolver(mockResolver, LookupSessionTest::multipleCNAMEs); ++ // apparently something that BIND 4 / BIND 9 before 9.1 could do. ++ wireUpMockResolver( ++ mockResolver, ++ query -> { ++ Message answer = new Message(query.getHeader().getID()); ++ answer.addRecord(testQuestion, Section.QUESTION); ++ answer.addRecord(cname(testQuestion.getName(), "target1."), Section.ANSWER); ++ answer.addRecord(cname(testQuestion.getName(), "target2."), Section.ANSWER); ++ return answer; ++ }); + + LookupSession lookupSession = LookupSession.builder().resolver(mockResolver).build(); +- CompletionStage resultFuture = lookupSession.lookupAsync(name("a.b."), A, IN); ++ LookupResult result = lookupSession.lookupAsync(testQuestion).toCompletableFuture().get(); + +- assertThrowsCause( +- InvalidZoneDataException.class, () -> resultFuture.toCompletableFuture().get()); +- verify(mockResolver).sendAsync(any(), any(Executor.class)); +- } ++ assertTrue(result.getRecords().isEmpty()); ++ assertThat(result.getAliases()).containsExactly(testQuestion.getName()); + +- private static Message multipleCNAMEs(Message query) { +- Message answer = new Message(query.getHeader().getID()); +- Record question = query.getQuestion(); +- answer.addRecord(question, Section.QUESTION); +- answer.addRecord( +- new CNAMERecord(question.getName(), CNAME, IN, name("target1.")), Section.ANSWER); +- answer.addRecord( +- new CNAMERecord(question.getName(), CNAME, IN, name("target2.")), Section.ANSWER); +- return answer; ++ // Two invocations as the result doesn't include an actual answer ++ verify(mockResolver, times(2)).sendAsync(any(), any(Executor.class)); + } + + @Test +@@ -806,12 +1159,11 @@ class LookupSessionTest { + ArgumentCaptor messageCaptor = ArgumentCaptor.forClass(Message.class); + verify(mockResolver).sendAsync(messageCaptor.capture(), any(Executor.class)); + +- assertEquals( +- Record.newRecord(name("host.example.com."), Type.A, DClass.IN, 0L), +- messageCaptor.getValue().getSection(Section.QUESTION).get(0)); ++ assertThat(messageCaptor.getValue().getSection(Section.QUESTION)) ++ .containsExactly(Record.newRecord(name("host.example.com."), Type.A, DClass.IN, 0L)); + +- assertEquals( +- singletonList(LOOPBACK_A.withName(name("host.example.com."))), lookupResult.getRecords()); ++ assertThat(lookupResult.getRecords()) ++ .containsExactly(LOOPBACK_A.withName(name("host.example.com."))); + } + + @Test +@@ -984,26 +1336,68 @@ class LookupSessionTest { + } + + private static CNAMERecord cname(String name, String target) { +- return cname(name(name), target); ++ return cname(name(name), name(target)); + } + ++ @SuppressWarnings("SameParameterValue") + private static CNAMERecord cname(Name name, String target) { +- return new CNAMERecord(name, IN, 0, name(target)); ++ return cname(name, name(target)); ++ } ++ ++ private static CNAMERecord cname(Name name, Name target) { ++ return new CNAMERecord(name, IN, 120, target); + } + +- @SuppressWarnings("SameParameterValue") + private static DNAMERecord dname(String name, String target) { +- return new DNAMERecord(name(name), IN, 0, name(target)); ++ return dname(name(name), name(target)); ++ } ++ ++ @SuppressWarnings("SameParameterValue") ++ private static DNAMERecord dname(Name name, String target) { ++ return dname(name, name(target)); ++ } ++ ++ private static DNAMERecord dname(Name name, Name target) { ++ return new DNAMERecord(name, IN, 120, target); + } + + private static Name name(String name) { + return Name.fromConstantString(name); + } + +- @SuppressWarnings("SameParameterValue") +- private void assertThrowsCause(Class ex, Executable executable) { +- Throwable outerException = assertThrows(Throwable.class, executable); +- assertEquals(ex, outerException.getCause().getClass()); ++ private LookupSession.LookupSessionBuilder lookupSession(boolean useCache) { ++ return lookupSession(useCache, false); ++ } ++ ++ private LookupSession.LookupSessionBuilder lookupSession( ++ boolean useCache, IrrelevantRecordMode mode) { ++ return lookupSession(useCache, mode, false); ++ } ++ ++ private LookupSession.LookupSessionBuilder lookupSession(boolean useCache, boolean throwOnUse) { ++ return lookupSession(useCache, IrrelevantRecordMode.REMOVE, throwOnUse); ++ } ++ ++ private LookupSession.LookupSessionBuilder lookupSession( ++ boolean useCache, IrrelevantRecordMode mode, boolean throwOnUse) { ++ LookupSession.LookupSessionBuilder builder = ++ LookupSession.builder().resolver(mockResolver).irrelevantRecordMode(mode); ++ if (useCache) { ++ Cache cache = spy(new Cache()); ++ builder.cache(cache); ++ if (throwOnUse) { ++ lenient() ++ .doThrow(new RuntimeException("Unexpected addMessage")) ++ .when(cache) ++ .addMessage(any(Message.class)); ++ lenient() ++ .doThrow(new RuntimeException("Unexpected addRecord")) ++ .when(cache) ++ .addRecord(any(Record.class), anyInt()); ++ } ++ } ++ ++ return builder; + } + + private void wireUpMockResolver(Resolver mockResolver, Function handler) { +diff --git a/src/test/resources/unbound/val_adcopy.rpl b/src/test/resources/unbound/val_adcopy.rpl +index 604fd57..aeb8bfd 100644 +--- a/src/test/resources/unbound/val_adcopy.rpl ++++ b/src/test/resources/unbound/val_adcopy.rpl +@@ -17,7 +17,7 @@ SCENARIO_BEGIN Test validator AD bit sent by untrusted upstream + + ; K.ROOT-SERVERS.NET. + RANGE_BEGIN 0 100 +- ADDRESS 193.0.14.129 ++ ADDRESS 193.0.14.129 + ENTRY_BEGIN + MATCH opcode qtype qname + ADJUST copy_id +@@ -115,13 +115,13 @@ SECTION QUESTION + www.example.com. IN A + SECTION ANSWER + www.example.com. IN A 10.20.30.40 +-ns.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCQMyTjn7WWwpwAR1LlVeLpRgZGuQIUCcJDEkwAuzytTDRlYK7nIMwH1CM= ;{id = 2854} ++www.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFC99iE9K5y2WNgI0gFvBWaTi9wm6AhUAoUqOpDtG5Zct+Qr9F3mSdnbc6V4= ;{id = 2854} + SECTION AUTHORITY + example.com. IN NS ns.example.com. + example.com. 3600 IN RRSIG NS 3 2 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCN+qHdJxoI/2tNKwsb08pra/G7aAIUAWA5sDdJTbrXA1/3OaesGBAO3sI= ;{id = 2854} + SECTION ADDITIONAL + ns.example.com. IN A 1.2.3.4 +-www.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFC99iE9K5y2WNgI0gFvBWaTi9wm6AhUAoUqOpDtG5Zct+Qr9F3mSdnbc6V4= ;{id = 2854} ++ns.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCQMyTjn7WWwpwAR1LlVeLpRgZGuQIUCcJDEkwAuzytTDRlYK7nIMwH1CM= ;{id = 2854} + ENTRY_END + RANGE_END + +diff --git a/src/test/resources/unbound/val_unalgo_anchor.rpl b/src/test/resources/unbound/val_unalgo_anchor.rpl +index fbbf288..de6b281 100644 +--- a/src/test/resources/unbound/val_unalgo_anchor.rpl ++++ b/src/test/resources/unbound/val_unalgo_anchor.rpl +@@ -13,11 +13,11 @@ stub-zone: + stub-addr: 193.0.14.129 # K.ROOT-SERVERS.NET. + CONFIG_END + +-SCENARIO_BEGIN Test validator with unsupported algorithm trust anchor ++SCENARIO_BEGIN Test validator with unsupported algorithm trust anchor + + ; K.ROOT-SERVERS.NET. + RANGE_BEGIN 0 100 +- ADDRESS 193.0.14.129 ++ ADDRESS 193.0.14.129 + ENTRY_BEGIN + MATCH opcode qtype qname + ADJUST copy_id +@@ -115,13 +115,13 @@ SECTION QUESTION + www.example.com. IN A + SECTION ANSWER + www.example.com. IN A 10.20.30.40 +-ns.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCQMyTjn7WWwpwAR1LlVeLpRgZGuQIUCcJDEkwAuzytTDRlYK7nIMwH1CM= ;{id = 2854} ++www.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFC99iE9K5y2WNgI0gFvBWaTi9wm6AhUAoUqOpDtG5Zct+Qr9F3mSdnbc6V4= ;{id = 2854} + SECTION AUTHORITY + example.com. IN NS ns.example.com. + example.com. 3600 IN RRSIG NS 3 2 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCN+qHdJxoI/2tNKwsb08pra/G7aAIUAWA5sDdJTbrXA1/3OaesGBAO3sI= ;{id = 2854} + SECTION ADDITIONAL + ns.example.com. IN A 1.2.3.4 +-www.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFC99iE9K5y2WNgI0gFvBWaTi9wm6AhUAoUqOpDtG5Zct+Qr9F3mSdnbc6V4= ;{id = 2854} ++ns.example.com. 3600 IN RRSIG A 3 3 3600 20070926134150 20070829134150 2854 example.com. MC0CFQCQMyTjn7WWwpwAR1LlVeLpRgZGuQIUCcJDEkwAuzytTDRlYK7nIMwH1CM= ;{id = 2854} + ENTRY_END + RANGE_END + +-- +2.33.0 + diff --git a/0001-Remove-mix-of-how-SetResponse-is-constructed.patch b/0001-Remove-mix-of-how-SetResponse-is-constructed.patch new file mode 100644 index 0000000..1e1befa --- /dev/null +++ b/0001-Remove-mix-of-how-SetResponse-is-constructed.patch @@ -0,0 +1,773 @@ +From e6302ef9d580f99f1704e29dfece28aef04e0579 Mon Sep 17 00:00:00 2001 +From: Ingo Bauersachs +Date: Sat, 17 Feb 2024 21:56:48 +0100 +Subject: [PATCH] Remove mix of how SetResponse is constructed + +--- + src/main/java/org/xbill/DNS/Cache.java | 34 +-- + src/main/java/org/xbill/DNS/SetResponse.java | 119 +++----- + .../java/org/xbill/DNS/SetResponseType.java | 48 +++ + src/main/java/org/xbill/DNS/Zone.java | 20 +- + .../java/org/xbill/DNS/SetResponseTest.java | 274 ++++++------------ + 5 files changed, 207 insertions(+), 288 deletions(-) + create mode 100644 src/main/java/org/xbill/DNS/SetResponseType.java + +diff --git a/src/main/java/org/xbill/DNS/Cache.java b/src/main/java/org/xbill/DNS/Cache.java +index e1c88ea..a93af2a 100644 +--- a/src/main/java/org/xbill/DNS/Cache.java ++++ b/src/main/java/org/xbill/DNS/Cache.java +@@ -422,7 +422,6 @@ public class Cache { + Element element; + Name tname; + Object types; +- SetResponse sr; + + labels = name.labels(); + +@@ -449,8 +448,8 @@ public class Cache { + * Otherwise, look for a DNAME. + */ + if (isExact && type == Type.ANY) { +- sr = new SetResponse(SetResponse.SUCCESSFUL); + Element[] elements = allElements(types); ++ SetResponse sr = SetResponse.ofType(SetResponseType.SUCCESSFUL); + int added = 0; + for (Element value : elements) { + element = value; +@@ -474,40 +473,37 @@ public class Cache { + } else if (isExact) { + element = oneElement(tname, types, type, minCred); + if (element instanceof CacheRRset) { +- sr = new SetResponse(SetResponse.SUCCESSFUL); +- sr.addRRset((CacheRRset) element); +- return sr; ++ return SetResponse.ofType(SetResponseType.SUCCESSFUL, (CacheRRset) element); + } else if (element != null) { +- sr = new SetResponse(SetResponse.NXRRSET); +- return sr; ++ return SetResponse.ofType(SetResponseType.NXRRSET); + } + + element = oneElement(tname, types, Type.CNAME, minCred); + if (element instanceof CacheRRset) { +- return new SetResponse(SetResponse.CNAME, (CacheRRset) element); ++ return SetResponse.ofType(SetResponseType.CNAME, (CacheRRset) element); + } + } else { + element = oneElement(tname, types, Type.DNAME, minCred); + if (element instanceof CacheRRset) { +- return new SetResponse(SetResponse.DNAME, (CacheRRset) element); ++ return SetResponse.ofType(SetResponseType.DNAME, (CacheRRset) element); + } + } + + /* Look for an NS */ + element = oneElement(tname, types, Type.NS, minCred); + if (element instanceof CacheRRset) { +- return new SetResponse(SetResponse.DELEGATION, (CacheRRset) element); ++ return SetResponse.ofType(SetResponseType.DELEGATION, (CacheRRset) element); + } + + /* Check for the special NXDOMAIN element. */ + if (isExact) { + element = oneElement(tname, types, 0, minCred); + if (element != null) { +- return SetResponse.ofType(SetResponse.NXDOMAIN); ++ return SetResponse.ofType(SetResponseType.NXDOMAIN); + } + } + } +- return SetResponse.ofType(SetResponse.UNKNOWN); ++ return SetResponse.ofType(SetResponseType.UNKNOWN); + } + + /** +@@ -641,7 +637,7 @@ public class Cache { + completed = true; + if (curname == qname) { + if (response == null) { +- response = new SetResponse(SetResponse.SUCCESSFUL); ++ response = SetResponse.ofType(SetResponseType.SUCCESSFUL); + } + response.addRRset(answer); + } +@@ -650,7 +646,7 @@ public class Cache { + CNAMERecord cname; + addRRset(answer, cred); + if (curname == qname) { +- response = new SetResponse(SetResponse.CNAME, answer); ++ response = SetResponse.ofType(SetResponseType.CNAME, answer); + } + cname = (CNAMERecord) answer.first(); + curname = cname.getTarget(); +@@ -658,7 +654,7 @@ public class Cache { + DNAMERecord dname; + addRRset(answer, cred); + if (curname == qname) { +- response = new SetResponse(SetResponse.DNAME, answer); ++ response = SetResponse.ofType(SetResponseType.DNAME, answer); + } + dname = (DNAMERecord) answer.first(); + try { +@@ -691,13 +687,13 @@ public class Cache { + } + addNegative(curname, cachetype, soarec, cred); + if (response == null) { +- int responseType; ++ SetResponseType responseType; + if (rcode == Rcode.NXDOMAIN) { +- responseType = SetResponse.NXDOMAIN; ++ responseType = SetResponseType.NXDOMAIN; + } else { +- responseType = SetResponse.NXRRSET; ++ responseType = SetResponseType.NXRRSET; + } +- response = SetResponse.ofType(responseType); ++ response = SetResponse.ofType(SetResponseType.DELEGATION, ns); + } + /* DNSSEC records are not cached. */ + } else { +diff --git a/src/main/java/org/xbill/DNS/SetResponse.java b/src/main/java/org/xbill/DNS/SetResponse.java +index 3fbf855..b67db66 100644 +--- a/src/main/java/org/xbill/DNS/SetResponse.java ++++ b/src/main/java/org/xbill/DNS/SetResponse.java +@@ -3,8 +3,17 @@ + + package org.xbill.DNS; + ++import static org.xbill.DNS.SetResponseType.CNAME; ++import static org.xbill.DNS.SetResponseType.DELEGATION; ++import static org.xbill.DNS.SetResponseType.DNAME; ++import static org.xbill.DNS.SetResponseType.NXDOMAIN; ++import static org.xbill.DNS.SetResponseType.NXRRSET; ++import static org.xbill.DNS.SetResponseType.SUCCESSFUL; ++import static org.xbill.DNS.SetResponseType.UNKNOWN; ++ + import java.util.ArrayList; + import java.util.List; ++import lombok.Getter; + + /** + * The Response from a query to {@link Cache#lookupRecords(Name, int, int)} or {@link +@@ -15,93 +24,64 @@ import java.util.List; + * @author Brian Wellington + */ + public class SetResponse { ++ private static final SetResponse SR_UNKNOWN = new SetResponse(UNKNOWN, null, false); ++ private static final SetResponse SR_UNKNOWN_AUTH = new SetResponse(UNKNOWN, null, true); ++ private static final SetResponse SR_NXDOMAIN = new SetResponse(NXDOMAIN, null, false); ++ private static final SetResponse SR_NXDOMAIN_AUTH = new SetResponse(NXDOMAIN, null, true); ++ private static final SetResponse SR_NXRRSET = new SetResponse(NXRRSET, null, false); ++ private static final SetResponse SR_NXRRSET_AUTH = new SetResponse(NXRRSET, null, true); + +- /** The Cache contains no information about the requested name/type */ +- static final int UNKNOWN = 0; ++ private final SetResponseType type; + + /** +- * The Zone does not contain the requested name, or the Cache has determined that the name does +- * not exist. ++ * @since 3.6 + */ +- static final int NXDOMAIN = 1; +- +- /** +- * The Zone contains the name, but no data of the requested type, or the Cache has determined that +- * the name exists and has no data of the requested type. +- */ +- static final int NXRRSET = 2; +- +- /** A delegation enclosing the requested name was found. */ +- static final int DELEGATION = 3; ++ @Getter private boolean isAuthenticated; + +- /** +- * The Cache/Zone found a CNAME when looking for the name. +- * +- * @see CNAMERecord +- */ +- static final int CNAME = 4; +- +- /** +- * The Cache/Zone found a DNAME when looking for the name. +- * +- * @see DNAMERecord +- */ +- static final int DNAME = 5; +- +- /** The Cache/Zone has successfully answered the question for the requested name/type/class. */ +- static final int SUCCESSFUL = 6; +- +- private static final SetResponse unknown = new SetResponse(UNKNOWN); +- private static final SetResponse nxdomain = new SetResponse(NXDOMAIN); +- private static final SetResponse nxrrset = new SetResponse(NXRRSET); +- +- private int type; + private List data; +- +- private SetResponse() {} +- +- SetResponse(int type, RRset rrset) { +- if (type < 0 || type > 6) { +- throw new IllegalArgumentException("invalid type"); +- } ++ private SetResponse(SetResponseType type, RRset rrset, boolean isAuthenticated) { + this.type = type; +- this.data = new ArrayList<>(); +- this.data.add(rrset); ++ this.isAuthenticated = isAuthenticated; ++ if (rrset != null) { ++ addRRset(rrset); ++ } + } + +- SetResponse(int type) { +- if (type < 0 || type > 6) { +- throw new IllegalArgumentException("invalid type"); +- } +- this.type = type; +- this.data = null; ++ static SetResponse ofType(SetResponseType type) { ++ return ofType(type, null, false); + } + +- static SetResponse ofType(int type) { ++ static SetResponse ofType(SetResponseType type, RRset rrset) { ++ return ofType(type, rrset, false); ++ } ++ ++ static SetResponse ofType(SetResponseType type, RRset rrset, boolean isAuthenticated) { + switch (type) { + case UNKNOWN: +- return unknown; ++ return isAuthenticated ? SR_UNKNOWN_AUTH : SR_UNKNOWN; + case NXDOMAIN: +- return nxdomain; ++ return isAuthenticated ? SR_NXDOMAIN_AUTH : SR_NXDOMAIN; + case NXRRSET: +- return nxrrset; ++ return isAuthenticated ? SR_NXRRSET_AUTH : SR_NXRRSET; + case DELEGATION: + case CNAME: + case DNAME: + case SUCCESSFUL: +- SetResponse sr = new SetResponse(); +- sr.type = type; +- sr.data = null; +- return sr; ++ return new SetResponse(type, rrset, isAuthenticated); + default: + throw new IllegalArgumentException("invalid type"); + } + } + + void addRRset(RRset rrset) { ++ if (type.isSealed()) { ++ throw new IllegalStateException("Attempted to add RRset to sealed response of type " + type); ++ } ++ + if (data == null) { + data = new ArrayList<>(); + } ++ + data.add(rrset); + } + +@@ -160,29 +140,12 @@ public class SetResponse { + + /** If the query hit a delegation point, return the NS set. */ + public RRset getNS() { +- return (data != null) ? data.get(0) : null; ++ return data != null ? data.get(0) : null; + } + + /** Prints the value of the SetResponse */ + @Override + public String toString() { +- switch (type) { +- case UNKNOWN: +- return "unknown"; +- case NXDOMAIN: +- return "NXDOMAIN"; +- case NXRRSET: +- return "NXRRSET"; +- case DELEGATION: +- return "delegation: " + data.get(0); +- case CNAME: +- return "CNAME: " + data.get(0); +- case DNAME: +- return "DNAME: " + data.get(0); +- case SUCCESSFUL: +- return "successful"; +- default: +- throw new IllegalStateException(); +- } ++ return type + (type.isPrintRecords() ? ": " + data.get(0) : ""); + } + } +diff --git a/src/main/java/org/xbill/DNS/SetResponseType.java b/src/main/java/org/xbill/DNS/SetResponseType.java +new file mode 100644 +index 0000000..791c774 +--- /dev/null ++++ b/src/main/java/org/xbill/DNS/SetResponseType.java +@@ -0,0 +1,48 @@ ++package org.xbill.DNS; ++ ++import lombok.Getter; ++import lombok.RequiredArgsConstructor; ++ ++@Getter ++@RequiredArgsConstructor ++enum SetResponseType { ++ /** The Cache contains no information about the requested name/type */ ++ UNKNOWN(false, true), ++ ++ /** ++ * The Zone does not contain the requested name, or the Cache has determined that the name does ++ * not exist. ++ */ ++ NXDOMAIN(false, true), ++ ++ /** ++ * The Zone contains the name, but no data of the requested type, or the Cache has determined that ++ * the name exists and has no data of the requested type. ++ */ ++ NXRRSET(false, true), ++ ++ /** A delegation enclosing the requested name was found. */ ++ DELEGATION(true, false), ++ ++ /** ++ * The Cache/Zone found a CNAME when looking for the name. ++ * ++ * @see CNAMERecord ++ */ ++ CNAME(true, false), ++ ++ /** ++ * The Cache/Zone found a DNAME when looking for the name. ++ * ++ * @see DNAMERecord ++ */ ++ DNAME(true, false), ++ ++ /** The Cache/Zone has successfully answered the question for the requested name/type/class. */ ++ SUCCESSFUL(false, false); ++ ++ private final boolean printRecords; ++ ++ /** If true, no RRsets can be added. Intended for static NX* instances. */ ++ private final boolean isSealed; ++} +diff --git a/src/main/java/org/xbill/DNS/Zone.java b/src/main/java/org/xbill/DNS/Zone.java +index e335a27..7c9e7d1 100644 +--- a/src/main/java/org/xbill/DNS/Zone.java ++++ b/src/main/java/org/xbill/DNS/Zone.java +@@ -338,7 +338,7 @@ public class Zone implements Serializable { + + private synchronized SetResponse lookup(Name name, int type) { + if (!name.subdomain(origin)) { +- return SetResponse.ofType(SetResponse.NXDOMAIN); ++ return SetResponse.ofType(SetResponseType.NXDOMAIN); + } + + int labels = name.labels(); +@@ -366,13 +366,13 @@ public class Zone implements Serializable { + if (!isOrigin) { + RRset ns = oneRRset(types, Type.NS); + if (ns != null) { +- return new SetResponse(SetResponse.DELEGATION, ns); ++ return SetResponse.ofType(SetResponseType.DELEGATION, ns); + } + } + + /* If this is an ANY lookup, return everything. */ + if (isExact && type == Type.ANY) { +- SetResponse sr = new SetResponse(SetResponse.SUCCESSFUL); ++ SetResponse sr = SetResponse.ofType(SetResponseType.SUCCESSFUL); + for (RRset set : allRRsets(types)) { + sr.addRRset(set); + } +@@ -386,22 +386,22 @@ public class Zone implements Serializable { + if (isExact) { + RRset rrset = oneRRset(types, type); + if (rrset != null) { +- return new SetResponse(SetResponse.SUCCESSFUL, rrset); ++ return SetResponse.ofType(SetResponseType.SUCCESSFUL, rrset); + } + rrset = oneRRset(types, Type.CNAME); + if (rrset != null) { +- return new SetResponse(SetResponse.CNAME, rrset); ++ return SetResponse.ofType(SetResponseType.CNAME, rrset); + } + } else { + RRset rrset = oneRRset(types, Type.DNAME); + if (rrset != null) { +- return new SetResponse(SetResponse.DNAME, rrset); ++ return SetResponse.ofType(SetResponseType.DNAME, rrset); + } + } + + /* We found the name, but not the type. */ + if (isExact) { +- return SetResponse.ofType(SetResponse.NXRRSET); ++ return SetResponse.ofType(SetResponseType.NXRRSET); + } + } + +@@ -414,7 +414,7 @@ public class Zone implements Serializable { + } + + if (type == Type.ANY) { +- SetResponse sr = new SetResponse(SetResponse.SUCCESSFUL); ++ SetResponse sr = SetResponse.ofType(SetResponseType.SUCCESSFUL); + for (RRset set : allRRsets(types)) { + sr.addRRset(expandSet(set, name)); + } +@@ -422,13 +422,13 @@ public class Zone implements Serializable { + } else { + RRset rrset = oneRRset(types, type); + if (rrset != null) { +- return new SetResponse(SetResponse.SUCCESSFUL, expandSet(rrset, name)); ++ return SetResponse.ofType(SetResponseType.SUCCESSFUL, expandSet(rrset, name)); + } + } + } + } + +- return SetResponse.ofType(SetResponse.NXDOMAIN); ++ return SetResponse.ofType(SetResponseType.NXDOMAIN); + } + + private RRset expandSet(RRset set, Name tname) { +diff --git a/src/test/java/org/xbill/DNS/SetResponseTest.java b/src/test/java/org/xbill/DNS/SetResponseTest.java +index 36d59d7..7bc460d 100644 +--- a/src/test/java/org/xbill/DNS/SetResponseTest.java ++++ b/src/test/java/org/xbill/DNS/SetResponseTest.java +@@ -45,145 +45,85 @@ import static org.junit.jupiter.api.Assertions.assertThrows; + 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.EnumSource; + + class SetResponseTest { +- @Test +- void ctor_1arg() { +- final int[] types = +- new int[] { +- SetResponse.UNKNOWN, +- SetResponse.NXDOMAIN, +- SetResponse.NXRRSET, +- SetResponse.DELEGATION, +- SetResponse.CNAME, +- SetResponse.DNAME, +- SetResponse.SUCCESSFUL +- }; +- +- for (int type : types) { +- SetResponse sr = new SetResponse(type); +- assertNull(sr.getNS()); +- assertEquals(type == SetResponse.UNKNOWN, sr.isUnknown()); +- assertEquals(type == SetResponse.NXDOMAIN, sr.isNXDOMAIN()); +- assertEquals(type == SetResponse.NXRRSET, sr.isNXRRSET()); +- assertEquals(type == SetResponse.DELEGATION, sr.isDelegation()); +- assertEquals(type == SetResponse.CNAME, sr.isCNAME()); +- assertEquals(type == SetResponse.DNAME, sr.isDNAME()); +- assertEquals(type == SetResponse.SUCCESSFUL, sr.isSuccessful()); +- } +- } +- +- @Test +- void ctor_1arg_toosmall() { +- assertThrows(IllegalArgumentException.class, () -> new SetResponse(-1)); +- } +- +- @Test +- void ctor_1arg_toobig() { +- assertThrows(IllegalArgumentException.class, () -> new SetResponse(7)); +- } +- +- @Test +- void ctor_2arg() { +- final int[] types = +- new int[] { +- SetResponse.UNKNOWN, +- SetResponse.NXDOMAIN, +- SetResponse.NXRRSET, +- SetResponse.DELEGATION, +- SetResponse.CNAME, +- SetResponse.DNAME, +- SetResponse.SUCCESSFUL +- }; +- +- for (int type : types) { +- RRset rs = new RRset(); +- SetResponse sr = new SetResponse(type, rs); +- assertSame(rs, sr.getNS()); +- assertEquals(type == SetResponse.UNKNOWN, sr.isUnknown()); +- assertEquals(type == SetResponse.NXDOMAIN, sr.isNXDOMAIN()); +- assertEquals(type == SetResponse.NXRRSET, sr.isNXRRSET()); +- assertEquals(type == SetResponse.DELEGATION, sr.isDelegation()); +- assertEquals(type == SetResponse.CNAME, sr.isCNAME()); +- assertEquals(type == SetResponse.DNAME, sr.isDNAME()); +- assertEquals(type == SetResponse.SUCCESSFUL, sr.isSuccessful()); +- } +- } +- +- @Test +- void ctor_2arg_toosmall() { +- assertThrows(IllegalArgumentException.class, () -> new SetResponse(-1, new RRset())); +- } +- +- @Test +- void ctor_2arg_toobig() { +- assertThrows(IllegalArgumentException.class, () -> new SetResponse(7, new RRset())); +- } +- +- @Test +- void ofType_basic() { +- final int[] types = +- new int[] { +- SetResponse.DELEGATION, SetResponse.CNAME, SetResponse.DNAME, SetResponse.SUCCESSFUL +- }; +- +- for (int type : types) { +- SetResponse sr = SetResponse.ofType(type); +- assertNull(sr.getNS()); +- assertEquals(type == SetResponse.UNKNOWN, sr.isUnknown()); +- assertEquals(type == SetResponse.NXDOMAIN, sr.isNXDOMAIN()); +- assertEquals(type == SetResponse.NXRRSET, sr.isNXRRSET()); +- assertEquals(type == SetResponse.DELEGATION, sr.isDelegation()); +- assertEquals(type == SetResponse.CNAME, sr.isCNAME()); +- assertEquals(type == SetResponse.DNAME, sr.isDNAME()); +- assertEquals(type == SetResponse.SUCCESSFUL, sr.isSuccessful()); +- +- SetResponse sr2 = SetResponse.ofType(type); +- assertNotSame(sr, sr2); +- } +- } +- +- @Test +- void ofType_singleton() { +- final int[] types = new int[] {SetResponse.UNKNOWN, SetResponse.NXDOMAIN, SetResponse.NXRRSET}; +- +- for (int type : types) { +- SetResponse sr = SetResponse.ofType(type); +- assertNull(sr.getNS()); +- assertEquals(type == SetResponse.UNKNOWN, sr.isUnknown()); +- assertEquals(type == SetResponse.NXDOMAIN, sr.isNXDOMAIN()); +- assertEquals(type == SetResponse.NXRRSET, sr.isNXRRSET()); +- assertEquals(type == SetResponse.DELEGATION, sr.isDelegation()); +- assertEquals(type == SetResponse.CNAME, sr.isCNAME()); +- assertEquals(type == SetResponse.DNAME, sr.isDNAME()); +- assertEquals(type == SetResponse.SUCCESSFUL, sr.isSuccessful()); +- +- SetResponse sr2 = SetResponse.ofType(type); +- assertSame(sr, sr2); +- } +- } +- +- @Test +- void ofType_toosmall() { +- assertThrows(IllegalArgumentException.class, () -> SetResponse.ofType(-1)); +- } +- +- @Test +- void ofType_toobig() { +- assertThrows(IllegalArgumentException.class, () -> SetResponse.ofType(7)); +- } +- +- @Test +- void addRRset() throws TextParseException, UnknownHostException { ++ private static final ARecord A_RECORD_1 = ++ new ARecord( ++ Name.fromConstantString("The.Name."), ++ DClass.IN, ++ 0xABCD, ++ new byte[] {(byte) 192, (byte) 168, 0, 1}); ++ private static final ARecord A_RECORD_2 = ++ new ARecord( ++ Name.fromConstantString("The.Name."), ++ DClass.IN, ++ 0xABCD, ++ new byte[] {(byte) 192, (byte) 168, 0, 2}); ++ ++ @ParameterizedTest ++ @EnumSource(value = SetResponseType.class) ++ void ctor_1arg(SetResponseType type) { ++ SetResponse sr = SetResponse.ofType(type); ++ assertNull(sr.getNS()); ++ assertEquals(type == SetResponseType.UNKNOWN, sr.isUnknown()); ++ assertEquals(type == SetResponseType.NXDOMAIN, sr.isNXDOMAIN()); ++ assertEquals(type == SetResponseType.NXRRSET, sr.isNXRRSET()); ++ assertEquals(type == SetResponseType.DELEGATION, sr.isDelegation()); ++ assertEquals(type == SetResponseType.CNAME, sr.isCNAME()); ++ assertEquals(type == SetResponseType.DNAME, sr.isDNAME()); ++ assertEquals(type == SetResponseType.SUCCESSFUL, sr.isSuccessful()); ++ } ++ ++ @ParameterizedTest ++ @EnumSource( ++ value = SetResponseType.class, ++ names = { ++ "DELEGATION", ++ "CNAME", ++ "DNAME", ++ "SUCCESSFUL", ++ }) ++ void ofType_basic(SetResponseType type) { ++ RRset rs = new RRset(); ++ SetResponse sr = SetResponse.ofType(type, rs); ++ assertSame(rs, sr.getNS()); ++ assertEquals(type == SetResponseType.DELEGATION, sr.isDelegation()); ++ assertEquals(type == SetResponseType.CNAME, sr.isCNAME()); ++ assertEquals(type == SetResponseType.DNAME, sr.isDNAME()); ++ assertEquals(type == SetResponseType.SUCCESSFUL, sr.isSuccessful()); ++ ++ SetResponse sr2 = SetResponse.ofType(type, rs); ++ assertNotSame(sr, sr2); ++ } ++ ++ @ParameterizedTest ++ @EnumSource( ++ value = SetResponseType.class, ++ names = { ++ "UNKNOWN", ++ "NXDOMAIN", ++ "NXRRSET", ++ }) ++ void ofType_singleton(SetResponseType type) { ++ SetResponse sr = SetResponse.ofType(type); ++ assertNull(sr.getNS()); ++ assertEquals(type == SetResponseType.UNKNOWN, sr.isUnknown()); ++ assertEquals(type == SetResponseType.NXDOMAIN, sr.isNXDOMAIN()); ++ assertEquals(type == SetResponseType.NXRRSET, sr.isNXRRSET()); ++ assertThrows(IllegalStateException.class, () -> sr.addRRset(new RRset())); ++ ++ SetResponse sr2 = SetResponse.ofType(type); ++ assertSame(sr, sr2); ++ } ++ ++ @Test ++ void addRRset() { + RRset rrs = new RRset(); +- rrs.addRR( +- new ARecord( +- Name.fromString("The.Name."), DClass.IN, 0xABCD, InetAddress.getByName("192.168.0.1"))); +- rrs.addRR( +- new ARecord( +- Name.fromString("The.Name."), DClass.IN, 0xABCD, InetAddress.getByName("192.168.0.2"))); +- SetResponse sr = new SetResponse(SetResponse.SUCCESSFUL); +- sr.addRRset(rrs); ++ rrs.addRR(A_RECORD_1); ++ rrs.addRR(A_RECORD_2); ++ SetResponse sr = SetResponse.ofType(SetResponseType.SUCCESSFUL, rrs); + + RRset[] exp = new RRset[] {rrs}; + assertArrayEquals(exp, sr.answers().toArray()); +@@ -192,12 +132,8 @@ class SetResponseTest { + @Test + void addRRset_multiple() throws TextParseException, UnknownHostException { + RRset rrs = new RRset(); +- rrs.addRR( +- new ARecord( +- Name.fromString("The.Name."), DClass.IN, 0xABCD, InetAddress.getByName("192.168.0.1"))); +- rrs.addRR( +- new ARecord( +- Name.fromString("The.Name."), DClass.IN, 0xABCD, InetAddress.getByName("192.168.0.2"))); ++ rrs.addRR(A_RECORD_1); ++ rrs.addRR(A_RECORD_2); + + RRset rrs2 = new RRset(); + rrs2.addRR( +@@ -213,7 +149,7 @@ class SetResponseTest { + 0xABCE, + InetAddress.getByName("192.168.1.2"))); + +- SetResponse sr = new SetResponse(SetResponse.SUCCESSFUL); ++ SetResponse sr = SetResponse.ofType(SetResponseType.SUCCESSFUL); + sr.addRRset(rrs); + sr.addRRset(rrs2); + +@@ -223,63 +159,39 @@ class SetResponseTest { + + @Test + void answers_nonSUCCESSFUL() { +- SetResponse sr = new SetResponse(SetResponse.UNKNOWN, new RRset()); ++ SetResponse sr = SetResponse.ofType(SetResponseType.UNKNOWN, new RRset()); + assertNull(sr.answers()); + } + + @Test + void getCNAME() throws TextParseException { +- RRset rrs = new RRset(); + CNAMERecord cr = + new CNAMERecord( + Name.fromString("The.Name."), DClass.IN, 0xABCD, Name.fromString("The.Alias.")); +- rrs.addRR(cr); +- SetResponse sr = new SetResponse(SetResponse.CNAME, rrs); ++ RRset rrs = new RRset(cr); ++ SetResponse sr = SetResponse.ofType(SetResponseType.CNAME, rrs); + assertEquals(cr, sr.getCNAME()); + } + + @Test + void getDNAME() throws TextParseException { +- RRset rrs = new RRset(); + DNAMERecord dr = + new DNAMERecord( + Name.fromString("The.Name."), DClass.IN, 0xABCD, Name.fromString("The.Alias.")); +- rrs.addRR(dr); +- SetResponse sr = new SetResponse(SetResponse.DNAME, rrs); ++ RRset rrs = new RRset(dr); ++ SetResponse sr = SetResponse.ofType(SetResponseType.DNAME, rrs); + assertEquals(dr, sr.getDNAME()); + } + +- @Test +- void test_toString() throws TextParseException, UnknownHostException { +- final int[] types = +- new int[] { +- SetResponse.UNKNOWN, +- SetResponse.NXDOMAIN, +- SetResponse.NXRRSET, +- SetResponse.DELEGATION, +- SetResponse.CNAME, +- SetResponse.DNAME, +- SetResponse.SUCCESSFUL +- }; +- RRset rrs = new RRset(); +- rrs.addRR( +- new ARecord( +- Name.fromString("The.Name."), DClass.IN, 0xABCD, InetAddress.getByName("192.168.0.1"))); +- +- final String[] labels = +- new String[] { +- "unknown", +- "NXDOMAIN", +- "NXRRSET", +- "delegation: " + rrs, +- "CNAME: " + rrs, +- "DNAME: " + rrs, +- "successful" +- }; +- +- for (int i = 0; i < types.length; ++i) { +- SetResponse sr = new SetResponse(types[i], rrs); +- assertEquals(labels[i], sr.toString()); ++ @ParameterizedTest ++ @EnumSource(SetResponseType.class) ++ void test_toString(SetResponseType type) { ++ RRset rrs = new RRset(A_RECORD_1); ++ SetResponse sr = SetResponse.ofType(type, rrs); ++ if (type.isPrintRecords()) { ++ assertEquals(type + ": " + rrs, sr.toString()); ++ } else { ++ assertEquals(type.toString(), sr.toString()); + } + } + } +-- +2.33.0 + diff --git a/backport-CVE-2024-25638.patch b/backport-CVE-2024-25638.patch deleted file mode 100644 index 7b2f3a7..0000000 --- a/backport-CVE-2024-25638.patch +++ /dev/null @@ -1,1604 +0,0 @@ -From bc51df1c455e6c9fb7cbd42fcb6d62d16047818d Mon Sep 17 00:00:00 2001 -From: Ingo Bauersachs -Date: Sun, 21 Jul 2024 13:34:12 +0200 -Subject: [PATCH] Fix links to RFCs and IANA registries - ---- - EXAMPLES.md | 2 +- - README.adoc | 8 +- - pom.xml | 2 +- - src/main/java/org/xbill/DNS/A6Record.java | 3 +- - src/main/java/org/xbill/DNS/AAAARecord.java | 4 +- - src/main/java/org/xbill/DNS/AFSDBRecord.java | 2 +- - src/main/java/org/xbill/DNS/APLRecord.java | 4 +- - src/main/java/org/xbill/DNS/ARecord.java | 4 +- - src/main/java/org/xbill/DNS/CAARecord.java | 4 +- - .../java/org/xbill/DNS/CDNSKEYRecord.java | 4 +- - src/main/java/org/xbill/DNS/CDSRecord.java | 4 +- - src/main/java/org/xbill/DNS/CERTRecord.java | 4 +- - src/main/java/org/xbill/DNS/CNAMERecord.java | 4 +- - .../org/xbill/DNS/ClientSubnetOption.java | 4 +- - src/main/java/org/xbill/DNS/CookieOption.java | 3 +- - src/main/java/org/xbill/DNS/DHCIDRecord.java | 4 +- - src/main/java/org/xbill/DNS/DLVRecord.java | 4 +- - src/main/java/org/xbill/DNS/DNAMERecord.java | 3 +- - src/main/java/org/xbill/DNS/DNSKEYRecord.java | 4 +- - src/main/java/org/xbill/DNS/DNSSEC.java | 3 +- - src/main/java/org/xbill/DNS/DSRecord.java | 4 +- - src/main/java/org/xbill/DNS/DohResolver.java | 6 +- - src/main/java/org/xbill/DNS/EDNSOption.java | 108 +++++++++++++++--- - .../xbill/DNS/ExtendedErrorCodeOption.java | 4 +- - src/main/java/org/xbill/DNS/GPOSRecord.java | 4 +- - src/main/java/org/xbill/DNS/HINFORecord.java | 4 +- - src/main/java/org/xbill/DNS/HTTPSRecord.java | 5 +- - .../java/org/xbill/DNS/IPSECKEYRecord.java | 22 +++- - src/main/java/org/xbill/DNS/ISDNRecord.java | 2 +- - src/main/java/org/xbill/DNS/KEYRecord.java | 8 +- - src/main/java/org/xbill/DNS/KXRecord.java | 4 +- - src/main/java/org/xbill/DNS/LOCRecord.java | 4 +- - src/main/java/org/xbill/DNS/MBRecord.java | 4 +- - src/main/java/org/xbill/DNS/MDRecord.java | 2 +- - src/main/java/org/xbill/DNS/MFRecord.java | 2 +- - src/main/java/org/xbill/DNS/MGRecord.java | 4 +- - src/main/java/org/xbill/DNS/MINFORecord.java | 4 +- - src/main/java/org/xbill/DNS/MRRecord.java | 4 +- - src/main/java/org/xbill/DNS/MXRecord.java | 8 +- - src/main/java/org/xbill/DNS/NAPTRRecord.java | 4 +- - src/main/java/org/xbill/DNS/NSAPRecord.java | 3 +- - .../java/org/xbill/DNS/NSAP_PTRRecord.java | 3 +- - .../java/org/xbill/DNS/NSEC3PARAMRecord.java | 4 +- - src/main/java/org/xbill/DNS/NSEC3Record.java | 4 +- - src/main/java/org/xbill/DNS/NSECRecord.java | 4 +- - src/main/java/org/xbill/DNS/NSIDOption.java | 4 +- - src/main/java/org/xbill/DNS/NSRecord.java | 4 +- - src/main/java/org/xbill/DNS/NULLRecord.java | 4 +- - src/main/java/org/xbill/DNS/NXTRecord.java | 4 +- - src/main/java/org/xbill/DNS/NioTcpClient.java | 2 +- - src/main/java/org/xbill/DNS/NioUdpClient.java | 2 +- - .../java/org/xbill/DNS/OPENPGPKEYRecord.java | 4 +- - src/main/java/org/xbill/DNS/OPTRecord.java | 3 +- - src/main/java/org/xbill/DNS/Opcode.java | 6 +- - src/main/java/org/xbill/DNS/PTRRecord.java | 4 +- - src/main/java/org/xbill/DNS/PXRecord.java | 4 +- - src/main/java/org/xbill/DNS/RPRecord.java | 2 +- - src/main/java/org/xbill/DNS/RRSIGRecord.java | 4 +- - src/main/java/org/xbill/DNS/RTRecord.java | 4 +- - src/main/java/org/xbill/DNS/SIGRecord.java | 4 +- - src/main/java/org/xbill/DNS/SMIMEARecord.java | 4 +- - src/main/java/org/xbill/DNS/SOARecord.java | 4 +- - src/main/java/org/xbill/DNS/SPFRecord.java | 4 +- - src/main/java/org/xbill/DNS/SRVRecord.java | 4 +- - src/main/java/org/xbill/DNS/SSHFPRecord.java | 4 +- - src/main/java/org/xbill/DNS/SVCBBase.java | 3 +- - src/main/java/org/xbill/DNS/SVCBRecord.java | 3 +- - src/main/java/org/xbill/DNS/TKEYRecord.java | 4 +- - src/main/java/org/xbill/DNS/TLSARecord.java | 28 ++++- - src/main/java/org/xbill/DNS/TSIG.java | 4 +- - src/main/java/org/xbill/DNS/TSIGRecord.java | 2 +- - src/main/java/org/xbill/DNS/TXTRecord.java | 4 +- - .../org/xbill/DNS/TcpKeepaliveOption.java | 2 +- - src/main/java/org/xbill/DNS/Type.java | 25 ++-- - src/main/java/org/xbill/DNS/URIRecord.java | 4 +- - src/main/java/org/xbill/DNS/WKSRecord.java | 6 +- - src/main/java/org/xbill/DNS/X25Record.java | 2 +- - .../org/xbill/DNS/lookup/LookupSession.java | 2 +- - .../DNS/lookup/NoSuchRRSetException.java | 5 +- - .../org/xbill/DNS/dnssec/ResolveExample.java | 4 +- - 80 files changed, 294 insertions(+), 178 deletions(-) - -diff --git a/EXAMPLES.md b/EXAMPLES.md -index 18a0eaf..bdb4b24 100644 ---- a/EXAMPLES.md -+++ b/EXAMPLES.md -@@ -144,7 +144,7 @@ public class ResolveExample { - - // Send the same queries using the validating resolver with the - // trust anchor of the root zone -- // http://data.iana.org/root-anchors/root-anchors.xml -+ // https://data.iana.org/root-anchors/root-anchors.xml - ValidatingResolver vr = new ValidatingResolver(sr); - vr.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII))); - System.out.println("\n\nValidating resolver:"); -diff --git a/README.adoc b/README.adoc -index 9a0aebd..9c76545 100644 ---- a/README.adoc -+++ b/README.adoc -@@ -122,7 +122,7 @@ Do NOT use it. - |5000 - - .2+|org.jitsi.dnssec.nsec3.iterations.N --3+a|Maximum iteration count for the NSEC3 hashing function depending on the key size N. The defaults are from https://www.rfc-editor.org/rfc/rfc5155.html#section-10.3[RFC5155]. -+3+a|Maximum iteration count for the NSEC3 hashing function depending on the key size N. The defaults are from https://datatracker.ietf.org/doc/html/rfc5155#section-10.3[RFC5155]. - |Integer - 2+a|- 1024 bit keys: 150 iterations - - 2048 bit keys: 500 iterations -@@ -169,7 +169,7 @@ https://www.rfc-editor.org/rfc/rfc8624.html#section-3.1[RFC8624] for recommended - .2+|dnsjava.dnssec.digest_enabled.ID - 3+|Enable or disable a DS record digest algorithm. - See --https://www.rfc-editor.org/rfc/rfc8624.html#section-3.3[RFC8624] for recommended values. -+https://datatracker.ietf.org/doc/html/rfc8624#section-3.3[RFC8624] for recommended values. - |Boolean - 2+|Disable SHA.1: - `dnsjava.dnssec.digest_enabled.1=false` -@@ -219,7 +219,7 @@ To migrate from dnssecjava, replace `org.jitsi` with `org.xbill.DNS` in Java pac - Validated, secure responses contain the DNS `AD`-flag, while responses that failed validation return the `SERVFAIL`-RCode. - Insecure responses return the actual return code without the `AD`-flag set. - The reason why the validation failed or is insecure is provided as a localized string in the additional section under the record ./65280/TXT (a TXT record for the owner name of the root zone in the private query class `ValidatingResolver.VALIDATION_REASON_QCLASS`). --The Extended DNS Errors (EDE, https://www.rfc-editor.org/rfc/rfc8914.html[RFC8914]) also provides the failure reason, although in less detail. -+The Extended DNS Errors (EDE, https://datatracker.ietf.org/doc/html/rfc8914[RFC8914]) also provides the failure reason, although in less detail. - - The link:EXAMPLES.md[examples] contain a small demo. - -@@ -230,7 +230,7 @@ The most important changes are: - - * Requires at least Java 8 - --* Uses http://www.slf4j.org/[slf4j] for logging and thus needs `slf4j-api` -+* Uses https://www.slf4j.org/[slf4j] for logging and thus needs `slf4j-api` - on the classpath - - * The link:USAGE.md[command line tools] were moved to the `org.xbill.DNS.tools` -diff --git a/pom.xml b/pom.xml -index 0135f64..b2edf41 100644 ---- a/pom.xml -+++ b/pom.xml -@@ -1,6 +1,6 @@ - - - - 4.0.0 -diff --git a/src/main/java/org/xbill/DNS/A6Record.java b/src/main/java/org/xbill/DNS/A6Record.java -index 3d8441e..7f19152 100644 ---- a/src/main/java/org/xbill/DNS/A6Record.java -+++ b/src/main/java/org/xbill/DNS/A6Record.java -@@ -11,7 +11,8 @@ import java.net.UnknownHostException; - * A6 Record - maps a domain name to an IPv6 address (historic) - * - * @author Brian Wellington -- * @see RFC 6563: Moving A6 to Historic Status -+ * @see RFC 6563: Moving A6 to Historic -+ * Status - */ - public class A6Record extends Record { - private int prefixBits; -diff --git a/src/main/java/org/xbill/DNS/AAAARecord.java b/src/main/java/org/xbill/DNS/AAAARecord.java -index 6f7c4b9..cd9a4be 100644 ---- a/src/main/java/org/xbill/DNS/AAAARecord.java -+++ b/src/main/java/org/xbill/DNS/AAAARecord.java -@@ -11,8 +11,8 @@ import java.net.UnknownHostException; - * IPv6 Address Record - maps a domain name to an IPv6 address - * - * @author Brian Wellington -- * @see RFC 3596: DNS Extensions to Support IP Version -- * 6 -+ * @see RFC 3596: DNS Extensions to Support -+ * IP Version 6 - */ - public class AAAARecord extends Record { - private byte[] address; -diff --git a/src/main/java/org/xbill/DNS/AFSDBRecord.java b/src/main/java/org/xbill/DNS/AFSDBRecord.java -index 6a41251..6ca2f05 100644 ---- a/src/main/java/org/xbill/DNS/AFSDBRecord.java -+++ b/src/main/java/org/xbill/DNS/AFSDBRecord.java -@@ -7,7 +7,7 @@ package org.xbill.DNS; - * AFS Data Base Record - maps a domain name to the name of an AFS cell database server. - * - * @author Brian Wellington -- * @see RFC 1183: New DNS RR Definitions -+ * @see RFC 1183: New DNS RR Definitions - */ - public class AFSDBRecord extends U16NameBase { - AFSDBRecord() {} -diff --git a/src/main/java/org/xbill/DNS/APLRecord.java b/src/main/java/org/xbill/DNS/APLRecord.java -index 3a5d3ee..fab315c 100644 ---- a/src/main/java/org/xbill/DNS/APLRecord.java -+++ b/src/main/java/org/xbill/DNS/APLRecord.java -@@ -19,8 +19,8 @@ import org.xbill.DNS.utils.base16; - * APL - Address Prefix List. - * - * @author Brian Wellington -- * @see RFC 3123: A DNS RR Type for Lists of Address -- * Prefixes (APL RR) -+ * @see RFC 3123: A DNS RR Type for Lists of -+ * Address Prefixes (APL RR) - */ - public class APLRecord extends Record { - -diff --git a/src/main/java/org/xbill/DNS/ARecord.java b/src/main/java/org/xbill/DNS/ARecord.java -index a4739fc..4cccc55 100644 ---- a/src/main/java/org/xbill/DNS/ARecord.java -+++ b/src/main/java/org/xbill/DNS/ARecord.java -@@ -11,8 +11,8 @@ import java.net.UnknownHostException; - * Address Record - maps a domain name to an Internet address - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification - */ - public class ARecord extends Record { - private int addr; -diff --git a/src/main/java/org/xbill/DNS/CAARecord.java b/src/main/java/org/xbill/DNS/CAARecord.java -index 47a4dd2..2fc61ce 100644 ---- a/src/main/java/org/xbill/DNS/CAARecord.java -+++ b/src/main/java/org/xbill/DNS/CAARecord.java -@@ -9,8 +9,8 @@ import java.io.IOException; - * Certification Authority Authorization - * - * @author Brian Wellington -- * @see RFC 6844: DNS Certification Authority -- * Authorization (CAA) Resource Record -+ * @see RFC 6844: DNS Certification -+ * Authority Authorization (CAA) Resource Record - */ - public class CAARecord extends Record { - public static class Flags { -diff --git a/src/main/java/org/xbill/DNS/CDNSKEYRecord.java b/src/main/java/org/xbill/DNS/CDNSKEYRecord.java -index 8879027..778ac69 100644 ---- a/src/main/java/org/xbill/DNS/CDNSKEYRecord.java -+++ b/src/main/java/org/xbill/DNS/CDNSKEYRecord.java -@@ -7,8 +7,8 @@ import java.security.PublicKey; - * Child DNSKEY record as specified in RFC 8078. - * - * @see DNSSEC -- * @see RFC 8078: Managing DS Records from the Parent -- * via CDS/CDNSKEY -+ * @see RFC 8078: Managing DS Records from -+ * the Parent via CDS/CDNSKEY - */ - public class CDNSKEYRecord extends DNSKEYRecord { - CDNSKEYRecord() {} -diff --git a/src/main/java/org/xbill/DNS/CDSRecord.java b/src/main/java/org/xbill/DNS/CDSRecord.java -index d3e8382..c9eee1f 100644 ---- a/src/main/java/org/xbill/DNS/CDSRecord.java -+++ b/src/main/java/org/xbill/DNS/CDSRecord.java -@@ -5,8 +5,8 @@ package org.xbill.DNS; - * Child Delegation Signer record as specified in RFC 8078. - * - * @see DNSSEC -- * @see RFC 8078: Managing DS Records from the Parent -- * via CDS/CDNSKEY -+ * @see RFC 8078: Managing DS Records from -+ * the Parent via CDS/CDNSKEY - */ - public class CDSRecord extends DSRecord { - CDSRecord() {} -diff --git a/src/main/java/org/xbill/DNS/CERTRecord.java b/src/main/java/org/xbill/DNS/CERTRecord.java -index 624f600..5ab7fd0 100644 ---- a/src/main/java/org/xbill/DNS/CERTRecord.java -+++ b/src/main/java/org/xbill/DNS/CERTRecord.java -@@ -12,8 +12,8 @@ import org.xbill.DNS.utils.base64; - * - * @see KEYRecord - * @author Brian Wellington -- * @see RFC 4398: Storing Certificates in the Domain -- * Name System (DNS) -+ * @see RFC 4398: Storing Certificates in -+ * the Domain Name System (DNS) - */ - public class CERTRecord extends Record { - -diff --git a/src/main/java/org/xbill/DNS/CNAMERecord.java b/src/main/java/org/xbill/DNS/CNAMERecord.java -index 0359cf3..5033d9f 100644 ---- a/src/main/java/org/xbill/DNS/CNAMERecord.java -+++ b/src/main/java/org/xbill/DNS/CNAMERecord.java -@@ -7,8 +7,8 @@ package org.xbill.DNS; - * CNAME Record - maps an alias to its real name - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification - */ - public class CNAMERecord extends SingleCompressedNameBase { - CNAMERecord() {} -diff --git a/src/main/java/org/xbill/DNS/ClientSubnetOption.java b/src/main/java/org/xbill/DNS/ClientSubnetOption.java -index 8d2730a..6d8df8d 100644 ---- a/src/main/java/org/xbill/DNS/ClientSubnetOption.java -+++ b/src/main/java/org/xbill/DNS/ClientSubnetOption.java -@@ -7,8 +7,7 @@ import java.net.InetAddress; - import java.net.UnknownHostException; - - /** -- * The Client Subnet EDNS Option, defined in Client -- * subnet in DNS requests. -+ * The Client Subnet EDNS Option. - * - *

The option is used to convey information about the IP address of the originating client, so - * that an authoritative server can make decisions based on this address, rather than the address of -@@ -22,6 +21,7 @@ import java.net.UnknownHostException; - * length (where the final octet is padded with bits set to 0) - * - * @see OPTRecord -+ * @see RFC 7871 - * @author Brian Wellington - * @author Ming Zhou <mizhou@bnivideo.com>, Beaumaris Networks - */ -diff --git a/src/main/java/org/xbill/DNS/CookieOption.java b/src/main/java/org/xbill/DNS/CookieOption.java -index 17f2247..5228f31 100644 ---- a/src/main/java/org/xbill/DNS/CookieOption.java -+++ b/src/main/java/org/xbill/DNS/CookieOption.java -@@ -6,9 +6,10 @@ import java.util.Optional; - import org.xbill.DNS.utils.base16; - - /** -- * Cookie EDNS0 Option, as defined in https://tools.ietf.org/html/rfc7873 -+ * Cookie EDNS0 Option. - * - * @see OPTRecord -+ * @see RFC 7873 - * @author Klaus Malorny - */ - public class CookieOption extends EDNSOption { -diff --git a/src/main/java/org/xbill/DNS/DHCIDRecord.java b/src/main/java/org/xbill/DNS/DHCIDRecord.java -index 827c976..be39470 100644 ---- a/src/main/java/org/xbill/DNS/DHCIDRecord.java -+++ b/src/main/java/org/xbill/DNS/DHCIDRecord.java -@@ -10,8 +10,8 @@ import org.xbill.DNS.utils.base64; - * DHCID - Dynamic Host Configuration Protocol (DHCP) ID (RFC 4701) - * - * @author Brian Wellington -- * @see RFC 4701: A DNS Resource Record (RR) for -- * Encoding Dynamic Host Configuration Protocol (DHCP) Information (DHCID RR) -+ * @see RFC 4701: A DNS Resource Record (RR) -+ * for Encoding Dynamic Host Configuration Protocol (DHCP) Information (DHCID RR) - */ - public class DHCIDRecord extends Record { - private byte[] data; -diff --git a/src/main/java/org/xbill/DNS/DLVRecord.java b/src/main/java/org/xbill/DNS/DLVRecord.java -index f890427..c6d8ff6 100644 ---- a/src/main/java/org/xbill/DNS/DLVRecord.java -+++ b/src/main/java/org/xbill/DNS/DLVRecord.java -@@ -14,8 +14,8 @@ import org.xbill.DNS.utils.base16; - * @see DSRecord - * @author David Blacka - * @author Brian Wellington -- * @see RFC 4431: The DNSSEC Lookaside Validation -- * (DLV) DNS Resource Record -+ * @see RFC 4431: The DNSSEC Lookaside -+ * Validation (DLV) DNS Resource Record - */ - public class DLVRecord extends Record { - -diff --git a/src/main/java/org/xbill/DNS/DNAMERecord.java b/src/main/java/org/xbill/DNS/DNAMERecord.java -index 429aae5..d088d8f 100644 ---- a/src/main/java/org/xbill/DNS/DNAMERecord.java -+++ b/src/main/java/org/xbill/DNS/DNAMERecord.java -@@ -7,7 +7,8 @@ package org.xbill.DNS; - * DNAME Record - maps a nonterminal alias (subtree) to a different domain - * - * @author Brian Wellington -- * @see RFC 6672: DNAME Redirection in the DNS -+ * @see RFC 6672: DNAME Redirection in the -+ * DNS - */ - public class DNAMERecord extends SingleNameBase { - DNAMERecord() {} -diff --git a/src/main/java/org/xbill/DNS/DNSKEYRecord.java b/src/main/java/org/xbill/DNS/DNSKEYRecord.java -index 438fa1d..d0d9b34 100644 ---- a/src/main/java/org/xbill/DNS/DNSKEYRecord.java -+++ b/src/main/java/org/xbill/DNS/DNSKEYRecord.java -@@ -12,8 +12,8 @@ import java.security.PublicKey; - * - * @see DNSSEC - * @author Brian Wellington -- * @see RFC 4034: Resource Records for the DNS -- * Security Extensions -+ * @see RFC 4034: Resource Records for the -+ * DNS Security Extensions - */ - public class DNSKEYRecord extends KEYBase { - -diff --git a/src/main/java/org/xbill/DNS/DNSSEC.java b/src/main/java/org/xbill/DNS/DNSSEC.java -index dbf684a..1b11631 100644 ---- a/src/main/java/org/xbill/DNS/DNSSEC.java -+++ b/src/main/java/org/xbill/DNS/DNSSEC.java -@@ -43,8 +43,9 @@ public class DNSSEC { - private Algorithm() {} - - /** -- * Delete DS record in parent zone, RFC8078. -+ * Delete DS record in parent zone. - * -+ * @see RFC 8078 - * @since 3.5 - */ - public static final int DELETE = 0; -diff --git a/src/main/java/org/xbill/DNS/DSRecord.java b/src/main/java/org/xbill/DNS/DSRecord.java -index bcfeeed..9970d02 100644 ---- a/src/main/java/org/xbill/DNS/DSRecord.java -+++ b/src/main/java/org/xbill/DNS/DSRecord.java -@@ -13,8 +13,8 @@ import org.xbill.DNS.utils.base16; - * @see DNSSEC - * @author David Blacka - * @author Brian Wellington -- * @see RFC 4034: Resource Records for the DNS -- * Security Extensions -+ * @see RFC 4034: Resource Records for the -+ * DNS Security Extensions - */ - public class DSRecord extends Record { - -diff --git a/src/main/java/org/xbill/DNS/DohResolver.java b/src/main/java/org/xbill/DNS/DohResolver.java -index 6e7d73f..0933ef1 100644 ---- a/src/main/java/org/xbill/DNS/DohResolver.java -+++ b/src/main/java/org/xbill/DNS/DohResolver.java -@@ -35,9 +35,9 @@ import org.xbill.DNS.AsyncSemaphore.Permit; - import org.xbill.DNS.utils.base64; - - /** -- * Proof-of-concept DNS over HTTP (DoH) resolver. -- * This class is not suitable for high load scenarios because of the shortcomings of Java's built-in -- * HTTP clients. For more control, implement your own {@link Resolver} using e.g. DNS over HTTP (DoH) -+ * resolver. This class is not suitable for high load scenarios because of the shortcomings of -+ * Java's built-in HTTP clients. For more control, implement your own {@link Resolver} using e.g. OkHttp. - * - *

On Java 8, it uses HTTP/1.1, which is against the recommendation of RFC 8484 to use HTTP/2 and -diff --git a/src/main/java/org/xbill/DNS/EDNSOption.java b/src/main/java/org/xbill/DNS/EDNSOption.java -index 4de7ad1..81906f8 100644 ---- a/src/main/java/org/xbill/DNS/EDNSOption.java -+++ b/src/main/java/org/xbill/DNS/EDNSOption.java -@@ -22,54 +22,129 @@ public abstract class EDNSOption { - public static class Code { - private Code() {} - -- /** Apple's DNS Long-Lived Queries protocol, draft-sekar-dns-llq-06 */ -+ /** -+ * Apple's DNS Long-Lived Queries protocol. -+ * -+ * @see RFC 8764 -+ */ - public static final int LLQ = 1; - -- /** Dynamic DNS Update Leases, draft-sekar-dns-ul-02 */ -+ /** -+ * Dynamic DNS Update Leases. -+ * -+ * @see draft-sekar-dns-ul-03 -+ */ - public static final int UL = 2; - -- /** Name Server Identifier, RFC 5001 */ -+ /** -+ * Name Server Identifier. -+ * -+ * @see RFC 5001 -+ */ - public static final int NSID = 3; - -- /** DNSSEC Algorithm Understood (DAU), RFC 6975 */ -+ /** -+ * DNSSEC Algorithm Understood (DAU). -+ * -+ * @see RFC 6975 -+ */ - public static final int DAU = 5; - -- /** DNSSEC DS Hash Understood (DHU), RFC 6975 */ -+ /** -+ * DNSSEC DS Hash Understood (DHU). -+ * -+ * @see RFC 8764 -+ */ - public static final int DHU = 6; - -- /** DNSSEC NSEC3 Hash Understood (N3U), RFC 6975 */ -+ /** -+ * DNSSEC NSEC3 Hash Understood (N3U). -+ * -+ * @see RFC 6975 -+ */ - public static final int N3U = 7; - -- /** Client Subnet, RFC 7871 */ -+ /** -+ * Client Subnet. -+ * -+ * @see RFC 7871 -+ */ - public static final int CLIENT_SUBNET = 8; - -- /** (EDNS) EXPIRE Option, RFC 7314 */ -+ /** -+ * (EDNS) EXPIRE Option. -+ * -+ * @see RFC 7314 -+ */ - public static final int EDNS_EXPIRE = 9; - -- /** Cookie, RFC 7873 */ -+ /** -+ * Cookie. -+ * -+ * @see RFC 7873 -+ */ - public static final int COOKIE = 10; - -- /** TCP Keepalive, RFC 7828 */ -+ /** -+ * TCP Keepalive. -+ * -+ * @see RFC 7828 -+ */ - public static final int TCP_KEEPALIVE = 11; - -- /** EDNS(0) Padding Option, RFC 7830 */ -+ /** -+ * EDNS(0) Padding Option. -+ * -+ * @see RFC 7830 -+ */ - public static final int PADDING = 12; - -- /** CHAIN Query Requests in DNS, RFC 7901 */ -+ /** -+ * CHAIN Query Requests in DNS. -+ * -+ * @see RFC 7901 -+ */ - public static final int CHAIN = 13; - -- /** Signaling Trust Anchor Knowledge in DNS Security Extensions (DNSSEC), RFC 8145 */ -+ /** -+ * Signaling Trust Anchor Knowledge in DNS Security Extensions (DNSSEC). -+ * -+ * @see RFC 8145 -+ */ - public static final int EDNS_KEY_TAG = 14; - -- /** Extended DNS Errors, RFC 8914. */ -+ /** -+ * Extended DNS Errors. -+ * -+ * @see RFC 8914 -+ */ - public static final int EDNS_EXTENDED_ERROR = 15; - -- /** DNS EDNS Tags, draft-bellis-dnsop-edns-tags-01 */ -+ /** -+ * DNS EDNS Tags. -+ * -+ * @see draft-bellis-dnsop-edns-tags-01 -+ */ - public static final int EDNS_CLIENT_TAG = 16; - -- /** DNS EDNS Tags, draft-bellis-dnsop-edns-tags-01 */ -+ /** -+ * DNS EDNS Tags. -+ * -+ * @see draft-bellis-dnsop-edns-tags-01 -+ */ - public static final int EDNS_SERVER_TAG = 17; - -+ /** -+ * Report Channel. -+ * -+ * @see RFC 9567 -+ * @since 3.6 -+ */ -+ public static final int REPORT_CHANNEL = 18; -+ - private static final Mnemonic codes = - new Mnemonic("EDNS Option Codes", Mnemonic.CASE_SENSITIVE); - -@@ -95,6 +170,7 @@ public abstract class EDNSOption { - codes.add(EDNS_EXTENDED_ERROR, "Extended_DNS_Error"); - codes.add(EDNS_CLIENT_TAG, "EDNS-Client-Tag"); - codes.add(EDNS_SERVER_TAG, "EDNS-Server-Tag"); -+ codes.add(REPORT_CHANNEL, "Report-Channel"); - } - - /** Converts an EDNS Option Code into its textual representation */ -diff --git a/src/main/java/org/xbill/DNS/ExtendedErrorCodeOption.java b/src/main/java/org/xbill/DNS/ExtendedErrorCodeOption.java -index d511465..6f04fba 100644 ---- a/src/main/java/org/xbill/DNS/ExtendedErrorCodeOption.java -+++ b/src/main/java/org/xbill/DNS/ExtendedErrorCodeOption.java -@@ -40,7 +40,9 @@ public class ExtendedErrorCodeOption extends EDNSOption { - - /** - * The resolver attempted to perform DNSSEC validation, but validation ended in the Indeterminate -- * state [RFC4035]. -+ * state. -+ * -+ * @see RFC 1712: DNS Encoding of Geographical -- * Location -+ * @see RFC 1712: DNS Encoding of -+ * Geographical Location - */ - public class GPOSRecord extends Record { - private byte[] latitude, longitude, altitude; -diff --git a/src/main/java/org/xbill/DNS/HINFORecord.java b/src/main/java/org/xbill/DNS/HINFORecord.java -index 6981ee6..06d32ec 100644 ---- a/src/main/java/org/xbill/DNS/HINFORecord.java -+++ b/src/main/java/org/xbill/DNS/HINFORecord.java -@@ -9,8 +9,8 @@ import java.io.IOException; - * Host Information - describes the CPU and OS of a host - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification - */ - public class HINFORecord extends Record { - private byte[] cpu, os; -diff --git a/src/main/java/org/xbill/DNS/HTTPSRecord.java b/src/main/java/org/xbill/DNS/HTTPSRecord.java -index 56659f4..785401c 100644 ---- a/src/main/java/org/xbill/DNS/HTTPSRecord.java -+++ b/src/main/java/org/xbill/DNS/HTTPSRecord.java -@@ -4,11 +4,10 @@ package org.xbill.DNS; - import java.util.List; - - /** -- * HTTPS Service Location and Parameter Binding Record -+ * HTTPS Service Location and Parameter Binding Record. - * -- * @see draft-ietf-dnsop-svcb-https - * @since 3.3 -+ * @see RFC 9460 - */ - public class HTTPSRecord extends SVCBBase { - HTTPSRecord() {} -diff --git a/src/main/java/org/xbill/DNS/IPSECKEYRecord.java b/src/main/java/org/xbill/DNS/IPSECKEYRecord.java -index 5a2439d..59c5be2 100644 ---- a/src/main/java/org/xbill/DNS/IPSECKEYRecord.java -+++ b/src/main/java/org/xbill/DNS/IPSECKEYRecord.java -@@ -12,8 +12,8 @@ import org.xbill.DNS.utils.base64; - * IPsec Keying Material (RFC 4025) - * - * @author Brian Wellington -- * @see RFC 4025: A Method for Storing IPsec Keying -- * Material in DNS -+ * @see RFC 4025: A Method for Storing IPsec -+ * Keying Material in DNS - */ - public class IPSECKEYRecord extends Record { - /** -@@ -24,13 +24,25 @@ public class IPSECKEYRecord extends Record { - public static class Algorithm { - private Algorithm() {} - -- /** A DSA key is present, in the format defined in [RFC2536] */ -+ /** -+ * A DSA key is present. -+ * -+ * @see RFC 2536 -+ */ - public static final int DSA = 1; - -- /** A RSA key is present, in the format defined in [RFC3110] */ -+ /** -+ * A RSA key is present. -+ * -+ * @see RFC 3110 -+ */ - public static final int RSA = 2; - -- /** An ECDSA key is present, in the format defined in [RFC6605] */ -+ /** -+ * An ECDSA key is present. -+ * -+ * @see RFC 6605 -+ */ - public static final int ECDSA = 3; - } - -diff --git a/src/main/java/org/xbill/DNS/ISDNRecord.java b/src/main/java/org/xbill/DNS/ISDNRecord.java -index 329f4e7..4e27574 100644 ---- a/src/main/java/org/xbill/DNS/ISDNRecord.java -+++ b/src/main/java/org/xbill/DNS/ISDNRecord.java -@@ -9,7 +9,7 @@ import java.io.IOException; - * ISDN - identifies the ISDN number and subaddress associated with a name. - * - * @author Brian Wellington -- * @see RFC 1183: New DNS RR Definitions -+ * @see RFC 1183: New DNS RR Definitions - */ - public class ISDNRecord extends Record { - private byte[] address; -diff --git a/src/main/java/org/xbill/DNS/KEYRecord.java b/src/main/java/org/xbill/DNS/KEYRecord.java -index f5b40cb..2335964 100644 ---- a/src/main/java/org/xbill/DNS/KEYRecord.java -+++ b/src/main/java/org/xbill/DNS/KEYRecord.java -@@ -14,10 +14,10 @@ import java.util.StringTokenizer; - * @see DNSSEC - * @see DNSKEYRecord - * @author Brian Wellington -- * @see RFC 2535: Domain Name System Security -- * Extensions -- * @see RFC 3755: Legacy Resolver Compatibility for -- * Delegation Signer (DS) -+ * @see RFC 2535: Domain Name System -+ * Security Extensions -+ * @see RFC 3755: Legacy Resolver -+ * Compatibility for Delegation Signer (DS) - */ - public class KEYRecord extends KEYBase { - /** KEY protocol identifiers. */ -diff --git a/src/main/java/org/xbill/DNS/KXRecord.java b/src/main/java/org/xbill/DNS/KXRecord.java -index 5918bfa..7ea5cf9 100644 ---- a/src/main/java/org/xbill/DNS/KXRecord.java -+++ b/src/main/java/org/xbill/DNS/KXRecord.java -@@ -7,8 +7,8 @@ package org.xbill.DNS; - * Key Exchange - delegation of authority - * - * @author Brian Wellington -- * @see RFC 2230: Key Exchange Delegation Record for -- * the DNS -+ * @see RFC 2230: Key Exchange Delegation -+ * Record for the DNS - */ - public class KXRecord extends U16NameBase { - KXRecord() {} -diff --git a/src/main/java/org/xbill/DNS/LOCRecord.java b/src/main/java/org/xbill/DNS/LOCRecord.java -index 60d5724..25d06db 100644 ---- a/src/main/java/org/xbill/DNS/LOCRecord.java -+++ b/src/main/java/org/xbill/DNS/LOCRecord.java -@@ -11,8 +11,8 @@ import java.text.NumberFormat; - * Location - describes the physical location of hosts, networks, subnets. - * - * @author Brian Wellington -- * @see RFC 1876: A Means for Expressing Location -- * Information in the Domain Name System -+ * @see RFC 1876: A Means for Expressing -+ * Location Information in the Domain Name System - */ - public class LOCRecord extends Record { - private static final NumberFormat w2; -diff --git a/src/main/java/org/xbill/DNS/MBRecord.java b/src/main/java/org/xbill/DNS/MBRecord.java -index 626df3f..a2fbc91 100644 ---- a/src/main/java/org/xbill/DNS/MBRecord.java -+++ b/src/main/java/org/xbill/DNS/MBRecord.java -@@ -7,8 +7,8 @@ package org.xbill.DNS; - * Mailbox Record - specifies a host containing a mailbox. - * - * @author Brian Wellington -- * @see RFC 883: Domain Names - Implementation and -- * Specification -+ * @see RFC 883: Domain Names - -+ * Implementation and Specification - */ - public class MBRecord extends SingleNameBase { - MBRecord() {} -diff --git a/src/main/java/org/xbill/DNS/MDRecord.java b/src/main/java/org/xbill/DNS/MDRecord.java -index 84077db..78c6a18 100644 ---- a/src/main/java/org/xbill/DNS/MDRecord.java -+++ b/src/main/java/org/xbill/DNS/MDRecord.java -@@ -7,7 +7,7 @@ package org.xbill.DNS; - * Mail Destination Record - specifies a mail agent which delivers mail for a domain (obsolete) - * - * @author Brian Wellington -- * @see RFC 973: Domain System Changes and -+ * @see RFC 973: Domain System Changes and - * Observations - */ - public class MDRecord extends SingleNameBase { -diff --git a/src/main/java/org/xbill/DNS/MFRecord.java b/src/main/java/org/xbill/DNS/MFRecord.java -index 55828b6..0e0d59c 100644 ---- a/src/main/java/org/xbill/DNS/MFRecord.java -+++ b/src/main/java/org/xbill/DNS/MFRecord.java -@@ -7,7 +7,7 @@ package org.xbill.DNS; - * Mail Forwarder Record - specifies a mail agent which forwards mail for a domain (obsolete) - * - * @author Brian Wellington -- * @see RFC 973: Domain System Changes and -+ * @see RFC 973: Domain System Changes and - * Observations - */ - public class MFRecord extends SingleNameBase { -diff --git a/src/main/java/org/xbill/DNS/MGRecord.java b/src/main/java/org/xbill/DNS/MGRecord.java -index 4d0d0fb..fd5077b 100644 ---- a/src/main/java/org/xbill/DNS/MGRecord.java -+++ b/src/main/java/org/xbill/DNS/MGRecord.java -@@ -7,8 +7,8 @@ package org.xbill.DNS; - * Mail Group Record - specifies a mailbox which is a member of a mail group. - * - * @author Brian Wellington -- * @see RFC 883: Domain Names - Implementation and -- * Specification -+ * @see RFC 883: Domain Names - -+ * Implementation and Specification - */ - public class MGRecord extends SingleNameBase { - MGRecord() {} -diff --git a/src/main/java/org/xbill/DNS/MINFORecord.java b/src/main/java/org/xbill/DNS/MINFORecord.java -index 242600f..a730095 100644 ---- a/src/main/java/org/xbill/DNS/MINFORecord.java -+++ b/src/main/java/org/xbill/DNS/MINFORecord.java -@@ -10,8 +10,8 @@ import java.io.IOException; - * address to receive error messages relating to the mailing list/mailbox. - * - * @author Brian Wellington -- * @see RFC 883: Domain Names - Implementation and -- * Specification -+ * @see RFC 883: Domain Names - -+ * Implementation and Specification - */ - public class MINFORecord extends Record { - private Name responsibleAddress; -diff --git a/src/main/java/org/xbill/DNS/MRRecord.java b/src/main/java/org/xbill/DNS/MRRecord.java -index 574997e..68e9f15 100644 ---- a/src/main/java/org/xbill/DNS/MRRecord.java -+++ b/src/main/java/org/xbill/DNS/MRRecord.java -@@ -7,8 +7,8 @@ package org.xbill.DNS; - * Mailbox Rename Record - specifies a rename of a mailbox. - * - * @author Brian Wellington -- * @see RFC 883: Domain Names - Implementation and -- * Specification -+ * @see RFC 883: Domain Names - -+ * Implementation and Specification - */ - public class MRRecord extends SingleNameBase { - MRRecord() {} -diff --git a/src/main/java/org/xbill/DNS/MXRecord.java b/src/main/java/org/xbill/DNS/MXRecord.java -index 8a6689f..f5ec13c 100644 ---- a/src/main/java/org/xbill/DNS/MXRecord.java -+++ b/src/main/java/org/xbill/DNS/MXRecord.java -@@ -7,10 +7,10 @@ package org.xbill.DNS; - * Mail Exchange - specifies where mail to a domain is sent - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -- * @see RFC 7505: A "Null MX" No Service Resource -- * Record for Domains That Accept No Mail -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification -+ * @see RFC 7505: A "Null MX" No Service -+ * Resource Record for Domains That Accept No Mail - */ - public class MXRecord extends U16NameBase { - MXRecord() {} -diff --git a/src/main/java/org/xbill/DNS/NAPTRRecord.java b/src/main/java/org/xbill/DNS/NAPTRRecord.java -index da3bf9a..6b07fe8 100644 ---- a/src/main/java/org/xbill/DNS/NAPTRRecord.java -+++ b/src/main/java/org/xbill/DNS/NAPTRRecord.java -@@ -10,8 +10,8 @@ import java.io.IOException; - * will produce a new domain. - * - * @author Chuck Santos -- * @see RFC 3403: Dynamic Delegation Discovery System -- * (DDDS) Part Three: The Domain Name System (DNS) Database -+ * @see RFC 3403: Dynamic Delegation -+ * Discovery System (DDDS) Part Three: The Domain Name System (DNS) Database - */ - public class NAPTRRecord extends Record { - private int order, preference; -diff --git a/src/main/java/org/xbill/DNS/NSAPRecord.java b/src/main/java/org/xbill/DNS/NSAPRecord.java -index 3be0047..8b88175 100644 ---- a/src/main/java/org/xbill/DNS/NSAPRecord.java -+++ b/src/main/java/org/xbill/DNS/NSAPRecord.java -@@ -11,7 +11,8 @@ import org.xbill.DNS.utils.base16; - * NSAP Address Record. - * - * @author Brian Wellington -- * @see RFC 1706: DNS NSAP Resource Records -+ * @see RFC 1706: DNS NSAP Resource -+ * Records - */ - public class NSAPRecord extends Record { - private byte[] address; -diff --git a/src/main/java/org/xbill/DNS/NSAP_PTRRecord.java b/src/main/java/org/xbill/DNS/NSAP_PTRRecord.java -index 8e1da00..478ddd1 100644 ---- a/src/main/java/org/xbill/DNS/NSAP_PTRRecord.java -+++ b/src/main/java/org/xbill/DNS/NSAP_PTRRecord.java -@@ -7,7 +7,8 @@ package org.xbill.DNS; - * NSAP Pointer Record - maps a domain name representing an NSAP Address to a hostname. (obsolete) - * - * @author Brian Wellington -- * @see RFC 1706: DNS NSAP Resource Records -+ * @see RFC 1706: DNS NSAP Resource -+ * Records - */ - public class NSAP_PTRRecord extends SingleNameBase { - NSAP_PTRRecord() {} -diff --git a/src/main/java/org/xbill/DNS/NSEC3PARAMRecord.java b/src/main/java/org/xbill/DNS/NSEC3PARAMRecord.java -index 9875040..f82f34c 100644 ---- a/src/main/java/org/xbill/DNS/NSEC3PARAMRecord.java -+++ b/src/main/java/org/xbill/DNS/NSEC3PARAMRecord.java -@@ -15,8 +15,8 @@ import org.xbill.DNS.utils.base16; - * - * @author Brian Wellington - * @author David Blacka -- * @see RFC 5155: DNS Security (DNSSEC) Hashed -- * Authenticated Denial of Existence -+ * @see RFC 5155: DNS Security (DNSSEC) -+ * Hashed Authenticated Denial of Existence - */ - public class NSEC3PARAMRecord extends Record { - private int hashAlg; -diff --git a/src/main/java/org/xbill/DNS/NSEC3Record.java b/src/main/java/org/xbill/DNS/NSEC3Record.java -index 8fe2dfe..b97b3b5 100644 ---- a/src/main/java/org/xbill/DNS/NSEC3Record.java -+++ b/src/main/java/org/xbill/DNS/NSEC3Record.java -@@ -18,8 +18,8 @@ import org.xbill.DNS.utils.base32; - * - * @author Brian Wellington - * @author David Blacka -- * @see RFC 5155: DNS Security (DNSSEC) Hashed -- * Authenticated Denial of Existence -+ * @see RFC 5155: DNS Security (DNSSEC) -+ * Hashed Authenticated Denial of Existence - */ - public class NSEC3Record extends Record { - -diff --git a/src/main/java/org/xbill/DNS/NSECRecord.java b/src/main/java/org/xbill/DNS/NSECRecord.java -index cf28f5d..6737594 100644 ---- a/src/main/java/org/xbill/DNS/NSECRecord.java -+++ b/src/main/java/org/xbill/DNS/NSECRecord.java -@@ -14,8 +14,8 @@ import java.io.IOException; - * - * @author Brian Wellington - * @author David Blacka -- * @see RFC 4034: Resource Records for the DNS -- * Security Extensions -+ * @see RFC 4034: Resource Records for the -+ * DNS Security Extensions - */ - public class NSECRecord extends Record { - private Name next; -diff --git a/src/main/java/org/xbill/DNS/NSIDOption.java b/src/main/java/org/xbill/DNS/NSIDOption.java -index 31dcef4..477e36e 100644 ---- a/src/main/java/org/xbill/DNS/NSIDOption.java -+++ b/src/main/java/org/xbill/DNS/NSIDOption.java -@@ -8,8 +8,8 @@ package org.xbill.DNS; - * - * @see OPTRecord - * @author Brian Wellington -- * @see RFC 5001: DNS Name Server Identifier (NSID) -- * Option -+ * @see RFC 5001: DNS Name Server Identifier -+ * (NSID) Option - */ - public class NSIDOption extends GenericEDNSOption { - NSIDOption() { -diff --git a/src/main/java/org/xbill/DNS/NSRecord.java b/src/main/java/org/xbill/DNS/NSRecord.java -index 1c48370..e5a0757 100644 ---- a/src/main/java/org/xbill/DNS/NSRecord.java -+++ b/src/main/java/org/xbill/DNS/NSRecord.java -@@ -7,8 +7,8 @@ package org.xbill.DNS; - * Name Server Record - contains the name server serving the named zone - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification - */ - public class NSRecord extends SingleCompressedNameBase { - NSRecord() {} -diff --git a/src/main/java/org/xbill/DNS/NULLRecord.java b/src/main/java/org/xbill/DNS/NULLRecord.java -index 00e1a29..a96554f 100644 ---- a/src/main/java/org/xbill/DNS/NULLRecord.java -+++ b/src/main/java/org/xbill/DNS/NULLRecord.java -@@ -9,8 +9,8 @@ import java.io.IOException; - * The NULL Record. This has no defined purpose, but can be used to hold arbitrary data. - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification - */ - public class NULLRecord extends Record { - private byte[] data; -diff --git a/src/main/java/org/xbill/DNS/NXTRecord.java b/src/main/java/org/xbill/DNS/NXTRecord.java -index 9c600ed..ac1347f 100644 ---- a/src/main/java/org/xbill/DNS/NXTRecord.java -+++ b/src/main/java/org/xbill/DNS/NXTRecord.java -@@ -12,8 +12,8 @@ import java.util.BitSet; - * signifies a failed query for data in a DNSSEC-signed zone. - * - * @author Brian Wellington -- * @see RFC 2065: Domain Name System Security -- * Extensions -+ * @see RFC 2065: Domain Name System -+ * Security Extensions - */ - public class NXTRecord extends Record { - private Name next; -diff --git a/src/main/java/org/xbill/DNS/NioTcpClient.java b/src/main/java/org/xbill/DNS/NioTcpClient.java -index aacc189..80b7a42 100644 ---- a/src/main/java/org/xbill/DNS/NioTcpClient.java -+++ b/src/main/java/org/xbill/DNS/NioTcpClient.java -@@ -89,7 +89,7 @@ final class NioTcpClient extends NioClient { - queryData); - - // combine length+message to avoid multiple TCP packets -- // https://tools.ietf.org/html/rfc7766#section-8 -+ // https://datatracker.ietf.org/doc/html/rfc7766#section-8 - ByteBuffer buffer = ByteBuffer.allocate(queryData.length + 2); - buffer.put((byte) (queryData.length >>> 8)); - buffer.put((byte) (queryData.length & 0xFF)); -diff --git a/src/main/java/org/xbill/DNS/NioUdpClient.java b/src/main/java/org/xbill/DNS/NioUdpClient.java -index 3cd218a..3653f71 100644 ---- a/src/main/java/org/xbill/DNS/NioUdpClient.java -+++ b/src/main/java/org/xbill/DNS/NioUdpClient.java -@@ -31,7 +31,7 @@ final class NioUdpClient extends NioClient { - private static final Queue pendingTransactions = new ConcurrentLinkedQueue<>(); - - static { -- // https://tools.ietf.org/html/rfc6335#section-6 -+ // https://datatracker.ietf.org/doc/html/rfc6335#section-6 - int ephemeralStartDefault = 49152; - int ephemeralEndDefault = 65535; - -diff --git a/src/main/java/org/xbill/DNS/OPENPGPKEYRecord.java b/src/main/java/org/xbill/DNS/OPENPGPKEYRecord.java -index 1801b8a..362a905 100644 ---- a/src/main/java/org/xbill/DNS/OPENPGPKEYRecord.java -+++ b/src/main/java/org/xbill/DNS/OPENPGPKEYRecord.java -@@ -9,8 +9,8 @@ import org.xbill.DNS.utils.base64; - * - * @author Brian Wellington - * @author Valentin Hauner -- * @see RFC 7929: DNS-Based Authentication of Named -- * Entities (DANE) Bindings for OpenPGP -+ * @see RFC 7929: DNS-Based Authentication -+ * of Named Entities (DANE) Bindings for OpenPGP - */ - public class OPENPGPKEYRecord extends Record { - private byte[] cert; -diff --git a/src/main/java/org/xbill/DNS/OPTRecord.java b/src/main/java/org/xbill/DNS/OPTRecord.java -index f026113..a8d7cae 100644 ---- a/src/main/java/org/xbill/DNS/OPTRecord.java -+++ b/src/main/java/org/xbill/DNS/OPTRecord.java -@@ -18,7 +18,8 @@ import java.util.List; - * - * @see Message - * @see Resolver -- * @see RFC 6891: Extension Mechanisms for DNS -+ * @see RFC 6891: Extension Mechanisms for -+ * DNS - * @author Brian Wellington - */ - public class OPTRecord extends Record { -diff --git a/src/main/java/org/xbill/DNS/Opcode.java b/src/main/java/org/xbill/DNS/Opcode.java -index 2445736..b3d9a0d 100644 ---- a/src/main/java/org/xbill/DNS/Opcode.java -+++ b/src/main/java/org/xbill/DNS/Opcode.java -@@ -25,7 +25,11 @@ public final class Opcode { - /** A dynamic update message */ - public static final int UPDATE = 5; - -- /** DNS Stateful Operations (DSO, RFC8490) */ -+ /** -+ * DNS Stateful Operations (DSO). -+ * -+ * @see RFC 8490 -+ */ - public static final int DSO = 6; - - private static final Mnemonic opcodes = new Mnemonic("DNS Opcode", Mnemonic.CASE_UPPER); -diff --git a/src/main/java/org/xbill/DNS/PTRRecord.java b/src/main/java/org/xbill/DNS/PTRRecord.java -index 2ecaffe..68c139d 100644 ---- a/src/main/java/org/xbill/DNS/PTRRecord.java -+++ b/src/main/java/org/xbill/DNS/PTRRecord.java -@@ -7,8 +7,8 @@ package org.xbill.DNS; - * Pointer Record - maps a domain name representing an Internet Address to a hostname. - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification - */ - public class PTRRecord extends SingleCompressedNameBase { - PTRRecord() {} -diff --git a/src/main/java/org/xbill/DNS/PXRecord.java b/src/main/java/org/xbill/DNS/PXRecord.java -index c81db60..135de5d 100644 ---- a/src/main/java/org/xbill/DNS/PXRecord.java -+++ b/src/main/java/org/xbill/DNS/PXRecord.java -@@ -9,8 +9,8 @@ import java.io.IOException; - * X.400 mail mapping record. - * - * @author Brian Wellington -- * @see RFC 2163: Using the Internet DNS to Distribute -- * MIXER Conformant Global Address Mapping (MCGAM) -+ * @see RFC 2163: Using the Internet DNS to -+ * Distribute MIXER Conformant Global Address Mapping (MCGAM) - */ - public class PXRecord extends Record { - private int preference; -diff --git a/src/main/java/org/xbill/DNS/RPRecord.java b/src/main/java/org/xbill/DNS/RPRecord.java -index 461306b..93c7eb5 100644 ---- a/src/main/java/org/xbill/DNS/RPRecord.java -+++ b/src/main/java/org/xbill/DNS/RPRecord.java -@@ -11,7 +11,7 @@ import java.io.IOException; - * - * @author Tom Scola (tscola@research.att.com) - * @author Brian Wellington -- * @see RFC 1183: New DNS RR Definitions -+ * @see RFC 1183: New DNS RR Definitions - */ - public class RPRecord extends Record { - private Name mailbox; -diff --git a/src/main/java/org/xbill/DNS/RRSIGRecord.java b/src/main/java/org/xbill/DNS/RRSIGRecord.java -index 701973c..a7fef0c 100644 ---- a/src/main/java/org/xbill/DNS/RRSIGRecord.java -+++ b/src/main/java/org/xbill/DNS/RRSIGRecord.java -@@ -14,8 +14,8 @@ import java.util.Date; - * @see RRset - * @see DNSSEC - * @see KEYRecord -- * @see RFC 4034: Resource Records for the DNS -- * Security Extensions -+ * @see RFC 4034: Resource Records for the -+ * DNS Security Extensions - * @author Brian Wellington - */ - public class RRSIGRecord extends SIGBase { -diff --git a/src/main/java/org/xbill/DNS/RTRecord.java b/src/main/java/org/xbill/DNS/RTRecord.java -index a66b69e..143cfec 100644 ---- a/src/main/java/org/xbill/DNS/RTRecord.java -+++ b/src/main/java/org/xbill/DNS/RTRecord.java -@@ -7,8 +7,8 @@ package org.xbill.DNS; - * Route Through Record - lists a route preference and intermediate host. - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification - */ - public class RTRecord extends U16NameBase { - RTRecord() {} -diff --git a/src/main/java/org/xbill/DNS/SIGRecord.java b/src/main/java/org/xbill/DNS/SIGRecord.java -index e25c2d0..9a012bd 100644 ---- a/src/main/java/org/xbill/DNS/SIGRecord.java -+++ b/src/main/java/org/xbill/DNS/SIGRecord.java -@@ -13,8 +13,8 @@ import java.util.Date; - * - * @see RRset - * @see DNSSEC -- * @see KEYRecord RFC 2535: Domain Name System -- * Security Extensions -+ * @see KEYRecord RFC 2535: Domain Name -+ * System Security Extensions - * @author Brian Wellington - */ - public class SIGRecord extends SIGBase { -diff --git a/src/main/java/org/xbill/DNS/SMIMEARecord.java b/src/main/java/org/xbill/DNS/SMIMEARecord.java -index fe3b502..d98302e 100644 ---- a/src/main/java/org/xbill/DNS/SMIMEARecord.java -+++ b/src/main/java/org/xbill/DNS/SMIMEARecord.java -@@ -6,8 +6,8 @@ package org.xbill.DNS; - /** - * S/MIME cert association - * -- * @see RFC 8162: Using Secure DNS to Associate -- * Certificates with Domain Names for S/MIME -+ * @see RFC 8162: Using Secure DNS to -+ * Associate Certificates with Domain Names for S/MIME - * @author Brian Wellington - */ - public class SMIMEARecord extends TLSARecord { -diff --git a/src/main/java/org/xbill/DNS/SOARecord.java b/src/main/java/org/xbill/DNS/SOARecord.java -index 58e97af..4eceb55 100644 ---- a/src/main/java/org/xbill/DNS/SOARecord.java -+++ b/src/main/java/org/xbill/DNS/SOARecord.java -@@ -9,8 +9,8 @@ import java.io.IOException; - * Start of Authority - describes properties of a zone. - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification - */ - public class SOARecord extends Record { - private Name host, admin; -diff --git a/src/main/java/org/xbill/DNS/SPFRecord.java b/src/main/java/org/xbill/DNS/SPFRecord.java -index 01a993c..074b4b0 100644 ---- a/src/main/java/org/xbill/DNS/SPFRecord.java -+++ b/src/main/java/org/xbill/DNS/SPFRecord.java -@@ -9,8 +9,8 @@ import java.util.List; - * Sender Policy Framework (discontinued in RFC 7208) - * - * @author Brian Wellington -- * @see RFC 7208: Sender Policy Framework (SPF) for -- * Authorizing Use of Domains in Email, Version 1 -+ * @see RFC 7208: Sender Policy Framework -+ * (SPF) for Authorizing Use of Domains in Email, Version 1 - */ - public class SPFRecord extends TXTBase { - SPFRecord() {} -diff --git a/src/main/java/org/xbill/DNS/SRVRecord.java b/src/main/java/org/xbill/DNS/SRVRecord.java -index be0805a..b48be11 100644 ---- a/src/main/java/org/xbill/DNS/SRVRecord.java -+++ b/src/main/java/org/xbill/DNS/SRVRecord.java -@@ -11,8 +11,8 @@ import java.io.IOException; - * (for the secure SIP protocol) and _http._tcp.example.com (if HTTP used SRV records) - * - * @author Brian Wellington -- * @see RFC 2782: A DNS RR for specifying the location -- * of services (DNS SRV) -+ * @see RFC 2782: A DNS RR for specifying -+ * the location of services (DNS SRV) - */ - public class SRVRecord extends Record { - private int priority, weight, port; -diff --git a/src/main/java/org/xbill/DNS/SSHFPRecord.java b/src/main/java/org/xbill/DNS/SSHFPRecord.java -index ccf9a81..4d7e971 100644 ---- a/src/main/java/org/xbill/DNS/SSHFPRecord.java -+++ b/src/main/java/org/xbill/DNS/SSHFPRecord.java -@@ -10,8 +10,8 @@ import org.xbill.DNS.utils.base16; - * SSH Fingerprint - stores the fingerprint of an SSH host key. - * - * @author Brian Wellington -- * @see RFC 4255: Using DNS to Securely Publish Secure -- * Shell (SSH) Key Fingerprints -+ * @see RFC 4255: Using DNS to Securely -+ * Publish Secure Shell (SSH) Key Fingerprints - */ - public class SSHFPRecord extends Record { - public static class Algorithm { -diff --git a/src/main/java/org/xbill/DNS/SVCBBase.java b/src/main/java/org/xbill/DNS/SVCBBase.java -index 695f9da..19ae05a 100644 ---- a/src/main/java/org/xbill/DNS/SVCBBase.java -+++ b/src/main/java/org/xbill/DNS/SVCBBase.java -@@ -20,9 +20,8 @@ import org.xbill.DNS.utils.base64; - /** - * Implements common functionality for SVCB and HTTPS records - * -- * @see draft-ietf-dnsop-svcb-https - * @since 3.3 -+ * @see RFC 9460 - */ - public abstract class SVCBBase extends Record { - protected int svcPriority; -diff --git a/src/main/java/org/xbill/DNS/SVCBRecord.java b/src/main/java/org/xbill/DNS/SVCBRecord.java -index bed4de0..1785e52 100644 ---- a/src/main/java/org/xbill/DNS/SVCBRecord.java -+++ b/src/main/java/org/xbill/DNS/SVCBRecord.java -@@ -6,9 +6,8 @@ import java.util.List; - /** - * Service Location and Parameter Binding Record - * -- * @see draft-ietf-dnsop-svcb-https - * @since 3.3 -+ * @see RFC 9460 - */ - public class SVCBRecord extends SVCBBase { - SVCBRecord() {} -diff --git a/src/main/java/org/xbill/DNS/TKEYRecord.java b/src/main/java/org/xbill/DNS/TKEYRecord.java -index 0664604..27b5bb1 100644 ---- a/src/main/java/org/xbill/DNS/TKEYRecord.java -+++ b/src/main/java/org/xbill/DNS/TKEYRecord.java -@@ -13,8 +13,8 @@ import org.xbill.DNS.utils.base64; - * - * @see TSIG - * @author Brian Wellington -- * @see RFC 2930: Secret Key Establishment for DNS -- * (TKEY RR) -+ * @see RFC 2930: Secret Key Establishment -+ * for DNS (TKEY RR) - */ - public class TKEYRecord extends Record { - private Name alg; -diff --git a/src/main/java/org/xbill/DNS/TLSARecord.java b/src/main/java/org/xbill/DNS/TLSARecord.java -index 1499180..46714fc 100644 ---- a/src/main/java/org/xbill/DNS/TLSARecord.java -+++ b/src/main/java/org/xbill/DNS/TLSARecord.java -@@ -10,8 +10,8 @@ import org.xbill.DNS.utils.base16; - * Transport Layer Security Authentication - * - * @author Brian Wellington -- * @see RFC 6698: The DNS-Based Authentication of -- * Named Entities (DANE) Transport Layer Security (TLS) Protocol: TLSA -+ * @see RFC 6698: The DNS-Based -+ * Authentication of Named Entities (DANE) Transport Layer Security (TLS) Protocol: TLSA - */ - public class TLSARecord extends Record { - public static class CertificateUsage { -@@ -26,10 +26,18 @@ public class TLSARecord extends Record { - public static class Selector { - private Selector() {} - -- /** Full certificate; the Certificate binary structure defined in [RFC5280] */ -+ /** -+ * Full certificate; the Certificate binary structure. -+ * -+ * @see RFC 5280 -+ */ - public static final int FULL_CERTIFICATE = 0; - -- /** SubjectPublicKeyInfo; DER-encoded binary structure defined in [RFC5280] */ -+ /** -+ * SubjectPublicKeyInfo; DER-encoded binary structure. -+ * -+ * @see RFC 5280 -+ */ - public static final int SUBJECT_PUBLIC_KEY_INFO = 1; - } - -@@ -39,10 +47,18 @@ public class TLSARecord extends Record { - /** Exact match on selected content */ - public static final int EXACT = 0; - -- /** SHA-256 hash of selected content [RFC6234] */ -+ /** -+ * SHA-256 hash of selected content. -+ * -+ * @see RFC 6234 -+ */ - public static final int SHA256 = 1; - -- /** SHA-512 hash of selected content [RFC6234] */ -+ /** -+ * SHA-512 hash of selected content. -+ * -+ * @see RFC 6234 -+ */ - public static final int SHA512 = 2; - } - -diff --git a/src/main/java/org/xbill/DNS/TSIG.java b/src/main/java/org/xbill/DNS/TSIG.java -index 29ecbea..ac62da9 100644 ---- a/src/main/java/org/xbill/DNS/TSIG.java -+++ b/src/main/java/org/xbill/DNS/TSIG.java -@@ -665,7 +665,7 @@ public class TSIG { - } - - // validate time after the signature, as per -- // https://www.rfc-editor.org/rfc/rfc8945.html#section-5.4 -+ // https://datatracker.ietf.org/doc/html/rfc8945#section-5.4 - int badtime = verifyTime(tsig); - if (badtime != Rcode.NOERROR) { - return badtime; -@@ -804,7 +804,7 @@ public class TSIG { - * Creates an instance to sign multiple message for use in a stream. - * - *

This class creates a {@link TSIGRecord} on every message to conform with RFC 8945, 5.3.1. -+ * href="https://datatracker.ietf.org/doc/html/rfc8945#section-5.3.1">RFC 8945, 5.3.1. - * - * @param key The TSIG key used to create the signature records. - * @param queryTsig The initial TSIG records, e.g. from a query to a server. -diff --git a/src/main/java/org/xbill/DNS/TSIGRecord.java b/src/main/java/org/xbill/DNS/TSIGRecord.java -index 2296994..fd00e0b 100644 ---- a/src/main/java/org/xbill/DNS/TSIGRecord.java -+++ b/src/main/java/org/xbill/DNS/TSIGRecord.java -@@ -15,7 +15,7 @@ import org.xbill.DNS.utils.base64; - * - * @see Resolver - * @see TSIG -- * @see RFC 2845: Secret Key Transaction -+ * @see RFC 2845: Secret Key Transaction - * Authentication for DNS (TSIG) - * @author Brian Wellington - */ -diff --git a/src/main/java/org/xbill/DNS/TXTRecord.java b/src/main/java/org/xbill/DNS/TXTRecord.java -index 6f338e4..221555f 100644 ---- a/src/main/java/org/xbill/DNS/TXTRecord.java -+++ b/src/main/java/org/xbill/DNS/TXTRecord.java -@@ -9,8 +9,8 @@ import java.util.List; - * Text - stores text strings - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification - */ - public class TXTRecord extends TXTBase { - TXTRecord() {} -diff --git a/src/main/java/org/xbill/DNS/TcpKeepaliveOption.java b/src/main/java/org/xbill/DNS/TcpKeepaliveOption.java -index ea42698..f9e279f 100644 ---- a/src/main/java/org/xbill/DNS/TcpKeepaliveOption.java -+++ b/src/main/java/org/xbill/DNS/TcpKeepaliveOption.java -@@ -7,7 +7,7 @@ import java.util.Optional; - import java.util.OptionalInt; - - /** -- * TCP Keepalive EDNS0 Option, as defined in https://tools.ietf.org/html/rfc7828 -+ * TCP Keepalive EDNS0 Option, as defined in https://datatracker.ietf.org/doc/html/rfc7828 - * - * @see OPTRecord - * @author Klaus Malorny -diff --git a/src/main/java/org/xbill/DNS/Type.java b/src/main/java/org/xbill/DNS/Type.java -index b87c38d..ebf2e41 100644 ---- a/src/main/java/org/xbill/DNS/Type.java -+++ b/src/main/java/org/xbill/DNS/Type.java -@@ -104,18 +104,18 @@ public final class Type { - public static final int NXT = 30; - - /** -- * Endpoint identifier -+ * DNS Resource Records for Nimrod Routing Architecture, Endpoint identifier. - * -- * @see DNS Resource Records for -- * Nimrod Routing Architecture -+ * @see draft-ietf-nimrod-dns - */ - public static final int EID = 31; - - /** -- * Nimrod locator -+ * DNS Resource Records for Nimrod Routing Architecture, Nimrod locator. - * -- * @see DNS Resource Records for -- * Nimrod Routing Architecture -+ * @see draft-ietf-nimrod-dns - */ - public static final int NIMLOC = 32; - -@@ -188,7 +188,8 @@ public final class Type { - /** - * Zone Status (ZS). - * -- * @see draft-reid-dnsext-zs-01 -+ * @see draft-reid-dnsext-zs-01 - */ - public static final int NINFO = 56; - -@@ -196,7 +197,7 @@ public final class Type { - * RKEY DNS Resource Record, used for encryption of NAPTR records. - * - * @see draft-reid-dnsext-rkey-00 -+ * href="https://datatracker.ietf.org/doc/html/draft-reid-dnsext-zs-01">draft-reid-dnsext-rkey-01 - */ - public static final int RKEY = 57; - -@@ -204,7 +205,7 @@ public final class Type { - * DNSSEC Trust Anchor History Service. - * - * @see draft-wijngaards-dnsop-trust-history-02 -+ * href="https://datatracker.ietf.org/doc/html/draft-wijngaards-dnsop-trust-history-02">draft-wijngaards-dnsop-trust-history-02 - */ - public static final int TALINK = 58; - -@@ -226,16 +227,14 @@ public final class Type { - /** - * Service Location and Parameter Binding - * -- * @see draft-ietf-dnsop-svcb-https -+ * @see RFC 9460 - */ - public static final int SVCB = 64; - - /** - * HTTPS Service Location and Parameter Binding - * -- * @see draft-ietf-dnsop-svcb-https -+ * @see RFC 9460 - */ - public static final int HTTPS = 65; - -diff --git a/src/main/java/org/xbill/DNS/URIRecord.java b/src/main/java/org/xbill/DNS/URIRecord.java -index c1035bb..f36e258 100644 ---- a/src/main/java/org/xbill/DNS/URIRecord.java -+++ b/src/main/java/org/xbill/DNS/URIRecord.java -@@ -10,8 +10,8 @@ import java.io.IOException; - * Uniform Resource Identifier (URI) DNS Resource Record - * - * @author Anthony Kirby -- * @see RFC 7553: The Uniform Resource Identifier -- * (URI) DNS Resource Record -+ * @see RFC 7553: The Uniform Resource -+ * Identifier (URI) DNS Resource Record - */ - public class URIRecord extends Record { - private int priority, weight; -diff --git a/src/main/java/org/xbill/DNS/WKSRecord.java b/src/main/java/org/xbill/DNS/WKSRecord.java -index dfac7d8..d726650 100644 ---- a/src/main/java/org/xbill/DNS/WKSRecord.java -+++ b/src/main/java/org/xbill/DNS/WKSRecord.java -@@ -14,14 +14,14 @@ import java.util.List; - * Well Known Services - Lists services offered by this host. - * - * @author Brian Wellington -- * @see RFC 1035: Domain Names - Implementation and -- * Specification -+ * @see RFC 1035: Domain Names - -+ * Implementation and Specification - */ - public class WKSRecord extends Record { - /** - * IP protocol identifiers. This is basically copied out of RFC 1010. - * -- * @see RFC 1010: Assigned Numbers -+ * @see RFC 1010: Assigned Numbers - */ - public static class Protocol { - -diff --git a/src/main/java/org/xbill/DNS/X25Record.java b/src/main/java/org/xbill/DNS/X25Record.java -index 80ec9c2..9b2ca5d 100644 ---- a/src/main/java/org/xbill/DNS/X25Record.java -+++ b/src/main/java/org/xbill/DNS/X25Record.java -@@ -10,7 +10,7 @@ import java.io.IOException; - * associated with a name. - * - * @author Brian Wellington -- * @see RFC 1183: New DNS RR Definitions -+ * @see RFC 1183: New DNS RR Definitions - */ - public class X25Record extends Record { - private byte[] address; -diff --git a/src/main/java/org/xbill/DNS/lookup/LookupSession.java b/src/main/java/org/xbill/DNS/lookup/LookupSession.java -index 2236b15..4abce77 100644 ---- a/src/main/java/org/xbill/DNS/lookup/LookupSession.java -+++ b/src/main/java/org/xbill/DNS/lookup/LookupSession.java -@@ -482,7 +482,7 @@ public class LookupSession { - private Message maybeAddToCache(Message message) { - for (RRset set : message.getSectionRRsets(Section.ANSWER)) { - if ((set.getType() == Type.CNAME || set.getType() == Type.DNAME) && set.size() != 1) { -- throw new InvalidZoneDataException("Multiple CNAME RRs not allowed, see RFC1034 3.6.2"); -+ throw new InvalidZoneDataException("Multiple CNAME RRs not allowed, see RFC 1034 3.6.2"); - } - } - Optional.ofNullable(caches.get(message.getQuestion().getDClass())) -diff --git a/src/main/java/org/xbill/DNS/lookup/NoSuchRRSetException.java b/src/main/java/org/xbill/DNS/lookup/NoSuchRRSetException.java -index 2002d88..cb7b6fc 100644 ---- a/src/main/java/org/xbill/DNS/lookup/NoSuchRRSetException.java -+++ b/src/main/java/org/xbill/DNS/lookup/NoSuchRRSetException.java -@@ -5,7 +5,10 @@ import org.xbill.DNS.Name; - - /** - * Thrown to indicate that records of the name and type queried does not exist, corresponding to the -- * NXRRSET return code as specified in RFC2136 Section 2.2. -+ * NXRRSET return code as specified in RFC 2136 Section 2.2. -+ * -+ * @since 3.4 -+ * @see RFC 2136 - */ - public class NoSuchRRSetException extends LookupFailedException { - public NoSuchRRSetException(Name name, int type) { -diff --git a/src/test/java/org/xbill/DNS/dnssec/ResolveExample.java b/src/test/java/org/xbill/DNS/dnssec/ResolveExample.java -index bcc7f18..f578f0a 100644 ---- a/src/test/java/org/xbill/DNS/dnssec/ResolveExample.java -+++ b/src/test/java/org/xbill/DNS/dnssec/ResolveExample.java -@@ -21,7 +21,7 @@ class ResolveExample { - static String ROOT = - ". IN DS 20326 8 2 E06D44B80B8F1D39A95C0B0D7C65D08458E880409BBC683457104237C7F8EC8D"; - -- static void main(String[] args) throws Exception { -+ public static void main(String[] args) throws Exception { - // Send two sample queries using a standard DNSJAVA resolver - SimpleResolver sr = new SimpleResolver("8.8.8.8"); - System.out.println("Standard resolver:"); -@@ -30,7 +30,7 @@ class ResolveExample { - - // Send the same queries using the validating resolver with the - // trust anchor of the root zone -- // http://data.iana.org/root-anchors/root-anchors.xml -+ // https://data.iana.org/root-anchors/root-anchors.xml - ValidatingResolver vr = new ValidatingResolver(sr); - vr.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes("ASCII"))); - vr.loadTrustAnchors(new ByteArrayInputStream(ROOT.getBytes(StandardCharsets.US_ASCII))); --- -2.33.0 - diff --git a/dnsjava.spec b/dnsjava.spec index 04d4838..c204fa6 100644 --- a/dnsjava.spec +++ b/dnsjava.spec @@ -1,12 +1,13 @@ %global do_not_test 1 Name: dnsjava Version: 3.5.3 -Release: 2 +Release: 3 Summary: Java DNS implementation License: BSD and MIT URL: http://www.dnsjava.org/ Source0: https://github.com/dnsjava/dnsjava/archive/v%{version}/%{name}-%{version}.tar.gz -Patch0: backport-CVE-2024-25638.patch +Patch0: 0001-Remove-mix-of-how-SetResponse-is-constructed.patch +Patch1: 0001-CVE-2024-25638-Message-normalization.patch BuildRequires: aqute-bnd javapackages-local BuildRequires: maven-local BuildRequires: mvn(org.apache.felix:maven-bundle-plugin) @@ -44,7 +45,8 @@ Javadoc for %{name}. rm -rf doc/ find -name "*.class" -print -delete find -name "*.jar" -print -delete -%patch0 -p1 -b .CVE-2024-25638 +%patch0 -p1 +%patch1 -p1 iconv -f iso8859-1 -t utf8 Changelog > Changelog.tmp touch -r Changelog Changelog.tmp mv -f Changelog.tmp Changelog @@ -74,6 +76,9 @@ cp -rf target/xmvn-apidocs/* %{buildroot}%{_javadocdir}/%{name} %license LICENSE %changelog +* Wed Jul 24 2024 zhangxianting - 3.5.3-3 +- Backport to fix CVE-2024-25638, remove invalid patch + * Tue Jul 23 2024 zhangxianting - 3.5.3-2 - Fix CVE-2024-25638 -- Gitee