From 89ee0d62fa77cbfe886d5036283618c93ab1a26e Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Fri, 19 Feb 2021 21:55:17 +0100 Subject: [PATCH 001/261] Bind checkstyle:check to verifiy phase --- pom.xml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/pom.xml b/pom.xml index d58e7996e..73379e74c 100644 --- a/pom.xml +++ b/pom.xml @@ -232,6 +232,15 @@ org.apache.maven.plugins maven-checkstyle-plugin 3.1.2 + + + check + + check + + verify + + + checkstyle/checkstyle-config.xml false true true - - - - - - - - diff --git a/lombok.config b/src/main/java/lombok.config similarity index 100% rename from lombok.config rename to src/main/java/lombok.config From 48785dd5d9b51e8c65338c37a97b65c8b23e2bb8 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Fri, 26 Feb 2021 00:05:32 +0100 Subject: [PATCH 007/261] Expand wildcard names in Zone lookups Closes #169 --- src/main/java/org/xbill/DNS/RRset.java | 22 ++++ src/main/java/org/xbill/DNS/Zone.java | 60 ++++++----- src/test/java/org/xbill/DNS/RRsetTest.java | 28 +++++ src/test/java/org/xbill/DNS/ZoneTest.java | 120 +++++++++++++++++++++ 4 files changed, 203 insertions(+), 27 deletions(-) create mode 100644 src/test/java/org/xbill/DNS/ZoneTest.java diff --git a/src/main/java/org/xbill/DNS/RRset.java b/src/main/java/org/xbill/DNS/RRset.java index 8233a7cb6..4a338f16e 100644 --- a/src/main/java/org/xbill/DNS/RRset.java +++ b/src/main/java/org/xbill/DNS/RRset.java @@ -8,6 +8,8 @@ import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Objects; +import lombok.EqualsAndHashCode; /** * A set of Records with the same name, type, and class. Also included are all RRSIG records signing @@ -17,6 +19,7 @@ * @see RRSIGRecord * @author Brian Wellington */ +@EqualsAndHashCode(of = {"rrs", "sigs"}) public class RRset implements Serializable { private final ArrayList rrs; private final ArrayList sigs; @@ -35,6 +38,19 @@ public RRset(Record record) { addRR(record); } + /** + * Creates an RRset and sets its contents to the specified record(s) + * + * @param records The records to add to the set. See {@link #addRR(Record)} for restrictions. + */ + public RRset(Record... records) { + this(); + Objects.requireNonNull(records); + for (Record r : records) { + addRR(r); + } + } + /** Creates an RRset with the contents of an existing RRset */ public RRset(RRset rrset) { rrs = new ArrayList<>(rrset.rrs); @@ -47,6 +63,9 @@ public RRset(RRset rrset) { * Adds a signature to this RRset. If the TTL of the added signature is not the same as existing * records in the RRset, all records are set to the lowest TTL of either the added record or the * existing records. + * + * @throws IllegalArgumentException if the RRset already contains records and the signature to add + * does not match. */ public void addRR(RRSIGRecord r) { addRR(r, sigs); @@ -56,6 +75,9 @@ public void addRR(RRSIGRecord r) { * Adds a Record to this RRset. If the TTL of the added record is not the same as existing records * in the RRset, all records are set to the lowest TTL of either the added record or the existing * records. + * + * @throws IllegalArgumentException if the RRset already contains records and the record to add + * does not match. */ public void addRR(Record r) { if (r instanceof RRSIGRecord) { diff --git a/src/main/java/org/xbill/DNS/Zone.java b/src/main/java/org/xbill/DNS/Zone.java index 84a987661..87a1f35d9 100644 --- a/src/main/java/org/xbill/DNS/Zone.java +++ b/src/main/java/org/xbill/DNS/Zone.java @@ -337,25 +337,18 @@ private synchronized void removeRRset(Name name, int type) { } private synchronized SetResponse lookup(Name name, int type) { - int labels; - int olabels; - int tlabels; - RRset rrset; - Name tname; - Object types; - SetResponse sr; - if (!name.subdomain(origin)) { return SetResponse.ofType(SetResponse.NXDOMAIN); } - labels = name.labels(); - olabels = origin.labels(); + int labels = name.labels(); + int olabels = origin.labels(); - for (tlabels = olabels; tlabels <= labels; tlabels++) { + for (int tlabels = olabels; tlabels <= labels; tlabels++) { boolean isOrigin = tlabels == olabels; boolean isExact = tlabels == labels; + Name tname; if (isOrigin) { tname = origin; } else if (isExact) { @@ -364,7 +357,7 @@ private synchronized SetResponse lookup(Name name, int type) { tname = new Name(name, labels - tlabels); } - types = exactName(tname); + Object types = exactName(tname); if (types == null) { continue; } @@ -379,9 +372,8 @@ private synchronized SetResponse lookup(Name name, int type) { /* If this is an ANY lookup, return everything. */ if (isExact && type == Type.ANY) { - sr = new SetResponse(SetResponse.SUCCESSFUL); - RRset[] sets = allRRsets(types); - for (RRset set : sets) { + SetResponse sr = new SetResponse(SetResponse.SUCCESSFUL); + for (RRset set : allRRsets(types)) { sr.addRRset(set); } return sr; @@ -392,18 +384,16 @@ private synchronized SetResponse lookup(Name name, int type) { * Otherwise, look for a DNAME. */ if (isExact) { - rrset = oneRRset(types, type); + RRset rrset = oneRRset(types, type); if (rrset != null) { - sr = new SetResponse(SetResponse.SUCCESSFUL); - sr.addRRset(rrset); - return sr; + return new SetResponse(SetResponse.SUCCESSFUL, rrset); } rrset = oneRRset(types, Type.CNAME); if (rrset != null) { return new SetResponse(SetResponse.CNAME, rrset); } } else { - rrset = oneRRset(types, Type.DNAME); + RRset rrset = oneRRset(types, Type.DNAME); if (rrset != null) { return new SetResponse(SetResponse.DNAME, rrset); } @@ -417,18 +407,23 @@ private synchronized SetResponse lookup(Name name, int type) { if (hasWild) { for (int i = 0; i < labels - olabels; i++) { - tname = name.wild(i + 1); - - types = exactName(tname); + Name tname = name.wild(i + 1); + Object types = exactName(tname); if (types == null) { continue; } - rrset = oneRRset(types, type); - if (rrset != null) { - sr = new SetResponse(SetResponse.SUCCESSFUL); - sr.addRRset(rrset); + if (type == Type.ANY) { + SetResponse sr = new SetResponse(SetResponse.SUCCESSFUL); + for (RRset set : allRRsets(types)) { + sr.addRRset(expandSet(set, name)); + } return sr; + } else { + RRset rrset = oneRRset(types, type); + if (rrset != null) { + return new SetResponse(SetResponse.SUCCESSFUL, expandSet(rrset, name)); + } } } } @@ -436,6 +431,17 @@ private synchronized SetResponse lookup(Name name, int type) { return SetResponse.ofType(SetResponse.NXDOMAIN); } + private RRset expandSet(RRset set, Name tname) { + RRset expandedSet = new RRset(); + for (Record r : set.rrs()) { + expandedSet.addRR(r.withName(tname)); + } + for (RRSIGRecord r : set.sigs()) { + expandedSet.addRR(r.withName(tname)); + } + return expandedSet; + } + /** * Looks up Records in the Zone. This follows CNAMEs and wildcards. * diff --git a/src/test/java/org/xbill/DNS/RRsetTest.java b/src/test/java/org/xbill/DNS/RRsetTest.java index 0131391fe..b176dad7d 100644 --- a/src/test/java/org/xbill/DNS/RRsetTest.java +++ b/src/test/java/org/xbill/DNS/RRsetTest.java @@ -44,7 +44,10 @@ import java.net.InetAddress; import java.net.UnknownHostException; import java.time.Instant; +import java.util.Collections; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -218,6 +221,31 @@ void ctor_1arg() { assertEquals(2, sigs.size()); } + @Test + void ctor_vararg() { + RRset set = new RRset(m_a1, m_a2); + assertEquals(Stream.of(m_a1, m_a2).collect(Collectors.toList()), set.rrs()); + assertEquals(Collections.emptyList(), set.sigs()); + } + + @Test + void ctor_vararg_sig() { + RRset set = new RRset(m_a1, m_a2, m_s1); + assertEquals(Stream.of(m_a1, m_a2).collect(Collectors.toList()), set.rrs()); + assertEquals(Collections.singletonList(m_s1), set.sigs()); + } + + @Test + void ctor_vararg_mismatch() { + TXTRecord txt = new TXTRecord(m_name, DClass.IN, 3600, "test"); + assertThrows(IllegalArgumentException.class, () -> new RRset(m_a1, m_a2, txt)); + } + + @Test + void ctor_vararg_null() { + assertThrows(NullPointerException.class, () -> new RRset((Record[]) null)); + } + @Test void test_toString() { m_rs.addRR(m_a1); diff --git a/src/test/java/org/xbill/DNS/ZoneTest.java b/src/test/java/org/xbill/DNS/ZoneTest.java new file mode 100644 index 000000000..c1ab149d0 --- /dev/null +++ b/src/test/java/org/xbill/DNS/ZoneTest.java @@ -0,0 +1,120 @@ +// SPDX-License-Identifier: BSD-2-Clause +package org.xbill.DNS; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.net.InetAddress; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.junit.jupiter.api.Test; + +class ZoneTest { + private static final ARecord A_TEST; + private static final AAAARecord AAAA_1_TEST; + private static final AAAARecord AAAA_2_TEST; + private static final ARecord A_WILD; + private static final TXTRecord TXT_WILD; + private static final Zone ZONE; + + static { + try { + Name nameZone = new Name("example."); + InetAddress localhost4 = InetAddress.getByName("127.0.0.1"); + InetAddress localhost6a = InetAddress.getByName("::1"); + InetAddress localhost6b = InetAddress.getByName("::2"); + A_TEST = new ARecord(new Name("test", nameZone), DClass.IN, 3600, localhost4); + AAAA_1_TEST = new AAAARecord(new Name("test", nameZone), DClass.IN, 3600, localhost6a); + AAAA_2_TEST = new AAAARecord(new Name("test", nameZone), DClass.IN, 3600, localhost6b); + A_WILD = new ARecord(new Name("*", nameZone), DClass.IN, 3600, localhost4); + TXT_WILD = new TXTRecord(new Name("*", nameZone), DClass.IN, 3600, "sometext"); + + Record[] zoneRecords = + new Record[] { + new SOARecord( + nameZone, + DClass.IN, + 3600L, + Name.fromConstantString("nameserver."), + new Name("hostmaster", nameZone), + 1, + 21600L, + 7200L, + 2160000L, + 3600L), + new NSRecord(nameZone, DClass.IN, 300L, Name.fromConstantString("nameserver.")), + A_TEST, + AAAA_1_TEST, + AAAA_2_TEST, + A_WILD, + TXT_WILD, + }; + ZONE = new Zone(nameZone, zoneRecords); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + @Test + void exactNameExistingALookup() { + Name testName = Name.fromConstantString("test.example."); + SetResponse resp = ZONE.findRecords(testName, Type.A); + assertEquals(oneRRset(A_TEST), resp.answers()); + } + + @Test + void exactNameTwoAaaaLookup() { + Name testName = Name.fromConstantString("test.example."); + SetResponse resp = ZONE.findRecords(testName, Type.AAAA); + assertEquals(oneRRset(AAAA_1_TEST, AAAA_2_TEST), resp.answers()); + } + + @Test + void exactNameAnyLookup() { + Name testName = Name.fromConstantString("test.example."); + SetResponse resp = ZONE.findRecords(testName, Type.ANY); + assertTrue(resp.isSuccessful()); + assertEquals(listOf(new RRset(A_TEST), new RRset(AAAA_1_TEST, AAAA_2_TEST)), resp.answers()); + } + + @Test + void wildNameExistingALookup() { + Name testName = Name.fromConstantString("undefined.example."); + SetResponse resp = ZONE.findRecords(testName, Type.A); + assertEquals(oneRRset(A_WILD.withName(testName)), resp.answers()); + } + + @Test + void wildNameExistingTxtLookup() { + Name testName = Name.fromConstantString("undefined.example."); + SetResponse resp = ZONE.findRecords(testName, Type.TXT); + assertEquals(oneRRset(TXT_WILD.withName(testName)), resp.answers()); + } + + @Test + void wildNameNonExistingMxLookup() { + SetResponse resp = ZONE.findRecords(Name.fromConstantString("undefined.example."), Type.MX); + assertTrue(resp.isNXDOMAIN()); + } + + @Test + void wildNameAnyLookup() { + Name testName = Name.fromConstantString("undefined.example."); + SetResponse resp = ZONE.findRecords(testName, Type.ANY); + assertTrue(resp.isSuccessful()); + assertEquals( + listOf(new RRset(A_WILD.withName(testName)), new RRset(TXT_WILD.withName(testName))), + resp.answers()); + } + + private static List listOf(RRset... rrsets) { + return Stream.of(rrsets).collect(Collectors.toList()); + } + + private static List oneRRset(Record... r) { + return Collections.singletonList(new RRset(r)); + } +} From 054b5077711966ad7a81a4b1313e9a007f09ad5b Mon Sep 17 00:00:00 2001 From: Walter Johnson Date: Fri, 12 Mar 2021 13:05:43 -0500 Subject: [PATCH 008/261] Properly close UDP channel upon error --- src/main/java/org/xbill/DNS/NioUdpClient.java | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/xbill/DNS/NioUdpClient.java b/src/main/java/org/xbill/DNS/NioUdpClient.java index 2c7df7264..cd39d474b 100644 --- a/src/main/java/org/xbill/DNS/NioUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioUdpClient.java @@ -138,9 +138,14 @@ public void processReadyKey(SelectionKey key) { private void silentCloseChannel() { try { channel.disconnect(); - channel.close(); } catch (IOException e) { // ignore, we either already have everything we need or can't do anything + } finally { + try { + channel.close(); + } catch (IOException e) { + // ignore + } } } } From d010ef7eb03ab33a6adbb05f72f5d5ad92ec0c0c Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Sun, 21 Mar 2021 19:14:08 +0100 Subject: [PATCH 009/261] Run Sonar on master again --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 192529e49..31b3c78a8 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -50,7 +50,7 @@ jobs: # doesn't work with PRs from forks, see https://jira.sonarsource.com/browse/MMF-1371 - name: Build with Maven and run Sonar - if: "${{ github.event.pull_request.head.repo.full_name == 'dnsjava/dnsjava' && matrix.arch == 'x64' && matrix.os == 'ubuntu-20.04' && matrix.java == '11' }}" + if: "${{ (github.ref == 'refs/heads/master' || github.event.pull_request.head.repo.full_name == 'dnsjava/dnsjava') && matrix.arch == 'x64' && matrix.os == 'ubuntu-20.04' && matrix.java == '11' }}" env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From fe85c1cfed748a90254cc357912173df24656b63 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Sun, 21 Mar 2021 19:37:37 +0100 Subject: [PATCH 010/261] Do not build twice on master --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 31b3c78a8..5c220c40b 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: architecture: ${{ matrix.arch }} - name: Build with Maven - if: "${{ !(matrix.arch == 'x64' && matrix.os == 'ubuntu-20.04' && matrix.java == '11') || github.event.pull_request.head.repo.full_name != 'dnsjava/dnsjava' }}" + if: "${{ !(matrix.arch == 'x64' && matrix.os == 'ubuntu-20.04' && matrix.java == '11') || (github.event.type == 'PullRequestEvent' && github.event.pull_request.head.repo.full_name != 'dnsjava/dnsjava') }}" run: mvn verify -B -"Dgpg.skip" # doesn't work with PRs from forks, see https://jira.sonarsource.com/browse/MMF-1371 @@ -54,7 +54,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - run: mvn -X -B -"Dgpg.skip" verify jacoco:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar + run: mvn -B -"Dgpg.skip" verify jacoco:report org.sonarsource.scanner.maven:sonar-maven-plugin:sonar - name: Run codecovc if: "${{ matrix.arch == 'x64' && matrix.os == 'ubuntu-20.04' && matrix.java == '11' }}" From 505b4ce94744408aa70cb9e8908ddf3a1aa1919c Mon Sep 17 00:00:00 2001 From: Paulo Costa Date: Mon, 22 Mar 2021 17:42:11 -0300 Subject: [PATCH 011/261] Fix load balancing (#179) * Fix load balancing `lbStart.updateAndGet(i -> i++ % resolvers.size())` is a no-op: `i++` increments the local variable, which is lost, and the returned value is simply `i % resolvers.size()` * Fix precedence * Update ExtendedResolver.java --- src/main/java/org/xbill/DNS/ExtendedResolver.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/xbill/DNS/ExtendedResolver.java b/src/main/java/org/xbill/DNS/ExtendedResolver.java index 55a64716a..9286880e5 100644 --- a/src/main/java/org/xbill/DNS/ExtendedResolver.java +++ b/src/main/java/org/xbill/DNS/ExtendedResolver.java @@ -41,7 +41,7 @@ private static class Resolution { resolvers = new ArrayList<>(eres.resolvers); endTime = System.nanoTime() + eres.timeout.toNanos(); if (eres.loadBalance) { - int start = eres.lbStart.updateAndGet(i -> i++ % resolvers.size()); + int start = eres.lbStart.updateAndGet(i -> (i + 1) % resolvers.size()); if (start > 0) { List shuffle = new ArrayList<>(resolvers.size()); for (int i = 0; i < resolvers.size(); i++) { From bd37aeab7bc54831985147a67a4d9e41b637adbf Mon Sep 17 00:00:00 2001 From: Pascal K Date: Mon, 5 Apr 2021 21:13:09 +0200 Subject: [PATCH 012/261] Fix restoring active position on byte buffers (#184) Wrong restored position lead to incomplete EDNS0 options parsing. --- src/main/java/org/xbill/DNS/DNSInput.java | 2 +- src/test/java/org/xbill/DNS/DNSInputTest.java | 17 ++ .../java/org/xbill/DNS/EDNS0Messages.java | 177 ++++++++++++++++++ 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 src/test/java/org/xbill/DNS/EDNS0Messages.java diff --git a/src/main/java/org/xbill/DNS/DNSInput.java b/src/main/java/org/xbill/DNS/DNSInput.java index 51a84531c..ce8b9bd80 100644 --- a/src/main/java/org/xbill/DNS/DNSInput.java +++ b/src/main/java/org/xbill/DNS/DNSInput.java @@ -92,7 +92,7 @@ public void restoreActive(int pos) { if (pos > byteBuffer.capacity()) { throw new IllegalArgumentException("cannot set active region past end of input"); } - byteBuffer.limit(byteBuffer.position()); + byteBuffer.limit(pos); } /** diff --git a/src/test/java/org/xbill/DNS/DNSInputTest.java b/src/test/java/org/xbill/DNS/DNSInputTest.java index 0a304c034..1c2294f0b 100644 --- a/src/test/java/org/xbill/DNS/DNSInputTest.java +++ b/src/test/java/org/xbill/DNS/DNSInputTest.java @@ -137,6 +137,23 @@ void save_restore() { assertEquals(6, m_di.remaining()); } + @Test + void save_set_restore() { + m_di.jump(4); + assertEquals(4, m_di.current()); + assertEquals(6, m_di.remaining()); + + int save = m_di.saveActive(); + assertEquals(10, save); + assertEquals(6, m_di.remaining()); + + m_di.setActive(4); + assertEquals(4, m_di.remaining()); + + m_di.restoreActive(save); + assertEquals(6, m_di.remaining()); + } + @Test void readU8_basic() throws WireParseException { int v1 = m_di.readU8(); diff --git a/src/test/java/org/xbill/DNS/EDNS0Messages.java b/src/test/java/org/xbill/DNS/EDNS0Messages.java new file mode 100644 index 000000000..c46ad9cf7 --- /dev/null +++ b/src/test/java/org/xbill/DNS/EDNS0Messages.java @@ -0,0 +1,177 @@ +package org.xbill.DNS; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; + +import java.io.IOException; +import org.junit.jupiter.api.Test; + +public class EDNS0Messages { + + /* dig +nocookie foo.dns.addere.ch */ + private static final byte[] EDNS0_EMPTY = { + 0x7d, 0x59, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x66, 0x6f, 0x6f, + 0x03, 0x64, 0x6e, 0x73, 0x06, 0x61, 0x64, 0x64, 0x65, 0x72, 0x65, 0x02, 0x63, 0x68, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 + }; + + /* dig foo.dns.addere.ch */ + private static final byte[] EDNS0_COOKIE = { + (byte) 0x95, + (byte) 0xa6, + 0x01, + 0x20, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x03, + 0x66, + 0x6f, + 0x6f, + 0x03, + 0x64, + 0x6e, + 0x73, + 0x06, + 0x61, + 0x64, + 0x64, + 0x65, + 0x72, + 0x65, + 0x02, + 0x63, + 0x68, + 0x00, + 0x00, + 0x01, + 0x00, + 0x01, + 0x00, + 0x00, + 0x29, + 0x10, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x0c, + 0x00, + 0x0a, + 0x00, + 0x08, + 0x28, + 0x75, + (byte) 0x83, + 0x7f, + 0x00, + 0x32, + (byte) 0xe5, + 0x6f + }; + + /* dig +keepalive foo.dns.addere.ch */ + private static final byte[] EDNS0_COOKIE_KEEPALIVE = { + (byte) 0x8e, + (byte) 0xdd, + 0x01, + 0x20, + 0x00, + 0x01, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x01, + 0x03, + 0x66, + 0x6f, + 0x6f, + 0x03, + 0x64, + 0x6e, + 0x73, + 0x06, + 0x61, + 0x64, + 0x64, + 0x65, + 0x72, + 0x65, + 0x02, + 0x63, + 0x68, + 0x00, + 0x00, + 0x01, + 0x00, + 0x01, + 0x00, + 0x00, + 0x29, + 0x10, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x00, + 0x10, + 0x00, + 0x0a, + 0x00, + 0x08, + (byte) 0xeb, + (byte) 0xed, + (byte) 0xfc, + 0x4c, + 0x1c, + 0x45, + 0x20, + 0x01, + 0x00, + 0x0b, + 0x00, + 0x00 + }; + + /* dig +keepalive +subnet=1.2.3.4 foo.dns.addere.ch */ + private static final byte[] EDNS0_SUBNET_COOKIE_KEEPALIVE = { + 0x42, 0x3a, 0x01, 0x20, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x03, 0x66, 0x6f, 0x6f, + 0x03, 0x64, 0x6e, 0x73, 0x06, 0x61, 0x64, 0x64, 0x65, 0x72, 0x65, 0x02, 0x63, 0x68, 0x00, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x00, 0x29, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00, 0x08, + 0x00, 0x08, 0x00, 0x01, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04, 0x00, 0x0a, 0x00, 0x08, 0x7c, 0x2e, + 0x0d, 0x0e, (byte) 0x95, (byte) 0xf3, 0x4d, (byte) 0xff, 0x00, 0x0b, 0x00, 0x00 + }; + + @Test + void testParseEdns0Empty() throws IOException { + Message msg = new Message(EDNS0_EMPTY); + assertArrayEquals(EDNS0_EMPTY, msg.toWire()); + } + + @Test + void testParseEdns0Cookie() throws IOException { + Message msg = new Message(EDNS0_COOKIE); + assertArrayEquals(EDNS0_COOKIE, msg.toWire()); + } + + @Test + void testParseEdns0CookieKeepalive() throws IOException { + Message msg = new Message(EDNS0_COOKIE_KEEPALIVE); + assertArrayEquals(EDNS0_COOKIE_KEEPALIVE, msg.toWire()); + } + + @Test + void testParseEdns0SubnetCookieKeepalive() throws IOException { + Message msg = new Message(EDNS0_SUBNET_COOKIE_KEEPALIVE); + assertArrayEquals(EDNS0_SUBNET_COOKIE_KEEPALIVE, msg.toWire()); + } +} From ba97e03ede0a7fbd4fc26c3a39d316187c97c8ef Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Sun, 21 Mar 2021 21:13:30 +0100 Subject: [PATCH 013/261] Replace wildcard imports with explicit imports to avoid conflicts Record would otherwise conflict with java.lang.Record as of Java 14 --- .../org/xbill/DNS/lookup/LookupSessionTest.java | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java b/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java index b1d9620fa..3c9af7d05 100644 --- a/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java +++ b/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java @@ -38,7 +38,21 @@ import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import org.xbill.DNS.*; +import org.xbill.DNS.ARecord; +import org.xbill.DNS.CNAMERecord; +import org.xbill.DNS.Cache; +import org.xbill.DNS.Credibility; +import org.xbill.DNS.DClass; +import org.xbill.DNS.DNAMERecord; +import org.xbill.DNS.Message; +import org.xbill.DNS.Name; +import org.xbill.DNS.RRset; +import org.xbill.DNS.Rcode; +import org.xbill.DNS.Record; +import org.xbill.DNS.Resolver; +import org.xbill.DNS.Section; +import org.xbill.DNS.SetResponse; +import org.xbill.DNS.Type; @ExtendWith(MockitoExtension.class) class LookupSessionTest { From 159576a4e37bcb17be7100828a5e4b15f192a82f Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Mon, 5 Apr 2021 21:31:10 +0200 Subject: [PATCH 014/261] Revert fe85c1cfed748a90254cc357912173df24656b63 The or condition doesn't work resulting in no Java 11 build from forks at all. --- .github/workflows/build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5c220c40b..e6e3fe502 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,7 +45,7 @@ jobs: architecture: ${{ matrix.arch }} - name: Build with Maven - if: "${{ !(matrix.arch == 'x64' && matrix.os == 'ubuntu-20.04' && matrix.java == '11') || (github.event.type == 'PullRequestEvent' && github.event.pull_request.head.repo.full_name != 'dnsjava/dnsjava') }}" + if: "${{ !(matrix.arch == 'x64' && matrix.os == 'ubuntu-20.04' && matrix.java == '11') || github.event.pull_request.head.repo.full_name != 'dnsjava/dnsjava' }}" run: mvn verify -B -"Dgpg.skip" # doesn't work with PRs from forks, see https://jira.sonarsource.com/browse/MMF-1371 From fb4889ee7a73f391f43bf6dc78b019d87ae15f15 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Mon, 5 Apr 2021 21:32:30 +0200 Subject: [PATCH 015/261] Remove BC after tests for consistency (#178) --- src/test/java/org/xbill/DNS/DNSSECWithBCProviderTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/org/xbill/DNS/DNSSECWithBCProviderTest.java b/src/test/java/org/xbill/DNS/DNSSECWithBCProviderTest.java index f5f44cd69..7d133d666 100644 --- a/src/test/java/org/xbill/DNS/DNSSECWithBCProviderTest.java +++ b/src/test/java/org/xbill/DNS/DNSSECWithBCProviderTest.java @@ -14,6 +14,7 @@ import java.security.Signature; import java.time.Instant; import org.bouncycastle.jce.provider.BouncyCastleProvider; +import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.xbill.DNS.DNSSEC.Algorithm; @@ -32,6 +33,11 @@ static void setUp() { Security.addProvider(new BouncyCastleProvider()); } + @AfterAll + static void afterAll() { + Security.removeProvider(BouncyCastleProvider.PROVIDER_NAME); + } + @Test void testSignWithDNSSECAndBCProvider() throws Exception { From ea51f46ca3dd49c3f1d60d5b945ff10a9559d9d1 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Tue, 13 Apr 2021 23:02:54 +0200 Subject: [PATCH 016/261] Update actions and early start selector thread for test --- .github/workflows/build.yml | 17 ++++++++++------- .../java/org/xbill/DNS/NioTcpClientTest.java | 9 +++++++++ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e6e3fe502..3688b7630 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,16 +33,18 @@ jobs: fetch-depth: 0 - name: Cache Maven dependencies - uses: actions/cache@v1 + uses: actions/cache@v2 with: - path: ~/.m2 + path: ~/.m2/repository key: m2-cache-${{ matrix.java }}-${{ matrix.arch }}-${{ matrix.os }} - name: Set up JDK ${{ matrix.java }} - uses: actions/setup-java@v1 + uses: actions/setup-java@v2 with: java-version: ${{ matrix.java }} architecture: ${{ matrix.arch }} + distribution: zulu + check-latest: true - name: Build with Maven if: "${{ !(matrix.arch == 'x64' && matrix.os == 'ubuntu-20.04' && matrix.java == '11') || github.event.pull_request.head.repo.full_name != 'dnsjava/dnsjava' }}" @@ -65,19 +67,20 @@ jobs: needs: test runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Cache Maven dependencies - uses: actions/cache@v1 + uses: actions/cache@v2 with: - path: ~/.m2 + path: ~/.m2/repository key: m2-cache-8-x64-ubuntu-20.04 - name: Set up JDK 8 - uses: joschi/setup-jdk@e87a7cec853d2dd7066adf837fe12bf0f3d45e52 + uses: actions/setup-java@v2 with: java-version: '8' architecture: 'x64' + distribution: zulu server-id: ossrh server-username: SONATYPE_USER server-password: SONATYPE_PW diff --git a/src/test/java/org/xbill/DNS/NioTcpClientTest.java b/src/test/java/org/xbill/DNS/NioTcpClientTest.java index 1b2e009ed..8296294e4 100644 --- a/src/test/java/org/xbill/DNS/NioTcpClientTest.java +++ b/src/test/java/org/xbill/DNS/NioTcpClientTest.java @@ -20,8 +20,12 @@ public class NioTcpClientTest { @Test void testResponseStream() throws InterruptedException, IOException { + // start the selector thread early + Client.selector(); + Record qr = Record.newRecord(Name.fromConstantString("example.com."), Type.A, DClass.IN); Message[] q = new Message[] {Message.newQuery(qr), Message.newQuery(qr)}; + CountDownLatch cdlServerThreadStart = new CountDownLatch(1); CountDownLatch cdl1 = new CountDownLatch(q.length); CountDownLatch cdl2 = new CountDownLatch(q.length); Message[] serverReceivedMessages = new Message[q.length]; @@ -33,6 +37,7 @@ void testResponseStream() throws InterruptedException, IOException { new Thread( () -> { try { + cdlServerThreadStart.countDown(); s[0] = ss.accept(); while (cdl1.getCount() > 0) { int ii = i.getAndIncrement(); @@ -56,6 +61,10 @@ void testResponseStream() throws InterruptedException, IOException { }); server.start(); + if (!cdlServerThreadStart.await(5, TimeUnit.SECONDS)) { + fail("timed out waiting for server thread to start"); + } + for (int j = 0; j < q.length; j++) { int jj = j; NioTcpClient.sendrecv( From 892fab2d826e6561c7c28b69461f3a965f2b397c Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Tue, 13 Apr 2021 23:54:05 +0200 Subject: [PATCH 017/261] Inline dummy server replies and only listen on localhost Sending the answers outside the thread can cause races and is more complicated to read. Explicitly listening on localhost seems to be required on GitHub. --- .../java/org/xbill/DNS/NioTcpClientTest.java | 79 ++++++++----------- 1 file changed, 34 insertions(+), 45 deletions(-) diff --git a/src/test/java/org/xbill/DNS/NioTcpClientTest.java b/src/test/java/org/xbill/DNS/NioTcpClientTest.java index 8296294e4..96efd017b 100644 --- a/src/test/java/org/xbill/DNS/NioTcpClientTest.java +++ b/src/test/java/org/xbill/DNS/NioTcpClientTest.java @@ -10,11 +10,11 @@ import java.net.InetSocketAddress; import java.net.ServerSocket; import java.net.Socket; +import java.net.SocketTimeoutException; import java.nio.ByteBuffer; import java.time.Duration; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; -import java.util.concurrent.atomic.AtomicInteger; import org.junit.jupiter.api.Test; public class NioTcpClientTest { @@ -26,35 +26,52 @@ void testResponseStream() throws InterruptedException, IOException { Record qr = Record.newRecord(Name.fromConstantString("example.com."), Type.A, DClass.IN); Message[] q = new Message[] {Message.newQuery(qr), Message.newQuery(qr)}; CountDownLatch cdlServerThreadStart = new CountDownLatch(1); - CountDownLatch cdl1 = new CountDownLatch(q.length); - CountDownLatch cdl2 = new CountDownLatch(q.length); - Message[] serverReceivedMessages = new Message[q.length]; - Message[] clientReceivedAnswers = new Message[q.length]; - AtomicInteger i = new AtomicInteger(0); - Socket[] s = new Socket[1]; - ServerSocket ss = new ServerSocket(0); + CountDownLatch cdlQueryRepliesReceived = new CountDownLatch(q.length); + ServerSocket ss = new ServerSocket(0, 0, InetAddress.getLoopbackAddress()); + ss.setSoTimeout(5000); Thread server = new Thread( () -> { try { cdlServerThreadStart.countDown(); - s[0] = ss.accept(); - while (cdl1.getCount() > 0) { - int ii = i.getAndIncrement(); + Socket s = ss.accept(); + for (int i = 0; i < q.length; i++) { try { - InputStream is = s[0].getInputStream(); + InputStream is = s.getInputStream(); byte[] lengthData = new byte[2]; int readLength = is.read(lengthData); assertEquals(2, readLength); byte[] messageData = new byte[(lengthData[0] << 8) + lengthData[1]]; int readMessageLength = is.read(messageData); assertEquals(messageData.length, readMessageLength); - serverReceivedMessages[ii] = new Message(messageData); - cdl1.countDown(); + Message serverReceivedMessages = new Message(messageData); + + for (int j = q.length - 1; j >= 0; j--) { + Message answer = new Message(); + answer.getHeader().setRcode(Rcode.NOERROR); + answer.getHeader().setID(serverReceivedMessages.getHeader().getID()); + answer.addRecord(serverReceivedMessages.getQuestion(), Section.QUESTION); + answer.addRecord( + new ARecord( + Name.fromConstantString("example.com."), + DClass.IN, + 900, + InetAddress.getLoopbackAddress()), + Section.ANSWER); + byte[] queryData = answer.toWire(); + ByteBuffer buffer = ByteBuffer.allocate(queryData.length + 2); + buffer.put((byte) (queryData.length >>> 8)); + buffer.put((byte) (queryData.length & 0xFF)); + buffer.put(queryData); + s.getOutputStream().write(buffer.array()); + } + } catch (IOException e) { fail(e); } } + } catch (SocketTimeoutException ste) { + fail("Timeout waiting for a client connection", ste); } catch (IOException e) { fail(e); } @@ -76,44 +93,16 @@ void testResponseStream() throws InterruptedException, IOException { .thenAccept( d -> { try { - clientReceivedAnswers[jj] = new Message(d); - cdl2.countDown(); + assertEquals(q[jj].getHeader().getID(), new Message(d).getHeader().getID()); + cdlQueryRepliesReceived.countDown(); } catch (IOException e) { fail(e); } }); } - if (!cdl1.await(5, TimeUnit.SECONDS)) { - fail("timed out waiting for messages"); - } - - for (int j = q.length - 1; j >= 0; j--) { - Message answer = new Message(); - answer.getHeader().setRcode(Rcode.NOERROR); - answer.getHeader().setID(serverReceivedMessages[j].getHeader().getID()); - answer.addRecord(serverReceivedMessages[j].getQuestion(), Section.QUESTION); - answer.addRecord( - new ARecord( - Name.fromConstantString("example.com."), - DClass.IN, - 900, - InetAddress.getLoopbackAddress()), - Section.ANSWER); - byte[] queryData = answer.toWire(); - ByteBuffer buffer = ByteBuffer.allocate(queryData.length + 2); - buffer.put((byte) (queryData.length >>> 8)); - buffer.put((byte) (queryData.length & 0xFF)); - buffer.put(queryData); - s[0].getOutputStream().write(buffer.array()); - } - - if (!cdl2.await(5, TimeUnit.SECONDS)) { + if (!cdlQueryRepliesReceived.await(5, TimeUnit.SECONDS)) { fail("timed out waiting for answers"); } - - for (int j = 0; j < q.length; j++) { - assertEquals(q[j].getHeader().getID(), clientReceivedAnswers[j].getHeader().getID()); - } } } From f3ac761cc1e962751d9718d9696b15fbaeaef0fa Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Sun, 18 Apr 2021 12:32:22 +0200 Subject: [PATCH 018/261] Add support for EDNS Extended Error Codes, RFC 8914 (#188) --- src/main/java/org/xbill/DNS/EDNSOption.java | 8 +- .../xbill/DNS/ExtendedErrorCodeOption.java | 133 ++++++++++++++++++ .../DNS/ExtendedErrorCodeOptionTest.java | 106 ++++++++++++++ 3 files changed, 246 insertions(+), 1 deletion(-) create mode 100644 src/main/java/org/xbill/DNS/ExtendedErrorCodeOption.java create mode 100644 src/test/java/org/xbill/DNS/ExtendedErrorCodeOptionTest.java diff --git a/src/main/java/org/xbill/DNS/EDNSOption.java b/src/main/java/org/xbill/DNS/EDNSOption.java index 57ad0820f..a40b866b3 100644 --- a/src/main/java/org/xbill/DNS/EDNSOption.java +++ b/src/main/java/org/xbill/DNS/EDNSOption.java @@ -61,6 +61,9 @@ private Code() {} /** Signaling Trust Anchor Knowledge in DNS Security Extensions (DNSSEC), RFC 8145 */ public static final int EDNS_KEY_TAG = 14; + /** Extended DNS Errors, RFC 8914. */ + public static final int EDNS_EXTENDED_ERROR = 15; + /** DNS EDNS Tags, draft-bellis-dnsop-edns-tags-01 */ public static final int EDNS_CLIENT_TAG = 16; @@ -88,7 +91,7 @@ private Code() {} codes.add(PADDING, "Padding"); codes.add(CHAIN, "CHAIN"); codes.add(EDNS_KEY_TAG, "edns-key-tag"); - + codes.add(EDNS_EXTENDED_ERROR, "Extended_DNS_Error"); codes.add(EDNS_CLIENT_TAG, "EDNS-Client-Tag"); codes.add(EDNS_SERVER_TAG, "EDNS-Server-Tag"); } @@ -184,6 +187,9 @@ static EDNSOption fromWire(DNSInput in) throws IOException { case Code.TCP_KEEPALIVE: option = new TcpKeepaliveOption(); break; + case Code.EDNS_EXTENDED_ERROR: + option = new ExtendedErrorCodeOption(); + break; default: option = new GenericEDNSOption(code); break; diff --git a/src/main/java/org/xbill/DNS/ExtendedErrorCodeOption.java b/src/main/java/org/xbill/DNS/ExtendedErrorCodeOption.java new file mode 100644 index 000000000..f1ac9cbec --- /dev/null +++ b/src/main/java/org/xbill/DNS/ExtendedErrorCodeOption.java @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: BSD-2-Clause +package org.xbill.DNS; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import lombok.Getter; + +/** + * EDNS option to provide additional information about the cause of DNS errors (RFC 8914). + * + * @since 3.4 + */ +public class ExtendedErrorCodeOption extends EDNSOption { + public static final int OTHER = 0; + public static final int UNSUPPORTED_DNSKEY_ALGORITHM = 1; + public static final int UNSUPPORTED_DS_DIGEST_TYPE = 2; + public static final int STALE_ANSWER = 3; + public static final int FORGED_ANSWER = 4; + public static final int DNSSEC_INDETERMINATE = 5; + public static final int DNSSEC_BOGUS = 6; + public static final int SIGNATURE_EXPIRED = 7; + public static final int SIGNATURE_NOT_YET_VALID = 8; + public static final int DNSKEY_MISSING = 9; + public static final int RRSIGS_MISSING = 10; + public static final int NO_ZONE_KEY_BIT_SET = 11; + public static final int NSEC_MISSING = 12; + public static final int CACHED_ERROR = 13; + public static final int NOT_READY = 14; + public static final int BLOCKED = 15; + public static final int CENSORED = 16; + public static final int FILTERED = 17; + public static final int PROHIBITED = 18; + public static final int STALE_NXDOMAIN_ANSWER = 19; + public static final int NOT_AUTHORITATIVE = 20; + public static final int NOT_SUPPORTED = 21; + public static final int NO_REACHABLE_AUTHORITY = 22; + public static final int NETWORK_ERROR = 23; + public static final int INVALID_DATA = 24; + + @Getter private int errorCode; + @Getter private String text; + + private static final Mnemonic codes = + new Mnemonic("EDNS Extended Error Codes", Mnemonic.CASE_SENSITIVE); + + static { + codes.setMaximum(0xFFFF); + codes.setPrefix("EDE"); + codes.add(OTHER, "Other"); + codes.add(UNSUPPORTED_DNSKEY_ALGORITHM, "Unsupported DNSKEY Algorithm"); + codes.add(UNSUPPORTED_DS_DIGEST_TYPE, "Unsupported DS Digest Type"); + codes.add(STALE_ANSWER, "Stale Answer"); + codes.add(FORGED_ANSWER, "Forged Answer"); + codes.add(DNSSEC_INDETERMINATE, "DNSSEC Indeterminate"); + codes.add(DNSSEC_BOGUS, "DNSSEC Bogus"); + codes.add(SIGNATURE_EXPIRED, "Signature Expired"); + codes.add(SIGNATURE_NOT_YET_VALID, "Signature Not Yet Valid"); + codes.add(DNSKEY_MISSING, "DNSKEY Missing"); + codes.add(RRSIGS_MISSING, "RRSIGs Missing"); + codes.add(NO_ZONE_KEY_BIT_SET, "No Zone Key Bit Set"); + codes.add(NSEC_MISSING, "NSEC Missing"); + codes.add(CACHED_ERROR, "Cached Error"); + codes.add(NOT_READY, "Not Ready"); + codes.add(BLOCKED, "Blocked"); + codes.add(CENSORED, "Censored"); + codes.add(FILTERED, "Filtered"); + codes.add(PROHIBITED, "Prohibited"); + codes.add(STALE_NXDOMAIN_ANSWER, "Stale NXDOMAIN Answer"); + codes.add(NOT_AUTHORITATIVE, "Not Authoritative"); + codes.add(NOT_SUPPORTED, "Not Supported"); + codes.add(NO_REACHABLE_AUTHORITY, "No Reachable Authority"); + codes.add(NETWORK_ERROR, "Network Error"); + codes.add(INVALID_DATA, "Invalid Data"); + } + + /** Creates an extended error code EDNS option. */ + ExtendedErrorCodeOption() { + super(Code.EDNS_EXTENDED_ERROR); + } + + /** + * Creates an extended error code EDNS option. + * + * @param errorCode the extended error. + * @param text optional error message intended for human readers. + */ + public ExtendedErrorCodeOption(int errorCode, String text) { + super(Code.EDNS_EXTENDED_ERROR); + this.errorCode = errorCode; + this.text = text; + } + + /** + * Creates an extended error code EDNS option. + * + * @param errorCode the extended error. + */ + public ExtendedErrorCodeOption(int errorCode) { + this(errorCode, null); + } + + @Override + void optionFromWire(DNSInput in) throws IOException { + errorCode = in.readU16(); + if (in.remaining() > 0) { + byte[] data = in.readByteArray(); + int len = data.length; + + // EDE text may be null terminated but MUST NOT be assumed to be + if (data[data.length - 1] == 0) { + len--; + } + + text = new String(data, 0, len, StandardCharsets.UTF_8); + } + } + + @Override + void optionToWire(DNSOutput out) { + out.writeU16(errorCode); + if (text != null && text.length() > 0) { + out.writeByteArray(text.getBytes(StandardCharsets.UTF_8)); + } + } + + @Override + String optionToString() { + if (text == null) { + return codes.getText(errorCode); + } + return codes.getText(errorCode) + ": " + text; + } +} diff --git a/src/test/java/org/xbill/DNS/ExtendedErrorCodeOptionTest.java b/src/test/java/org/xbill/DNS/ExtendedErrorCodeOptionTest.java new file mode 100644 index 000000000..2de5271d3 --- /dev/null +++ b/src/test/java/org/xbill/DNS/ExtendedErrorCodeOptionTest.java @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: BSD-2-Clause +package org.xbill.DNS; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import org.junit.jupiter.api.Test; + +class ExtendedErrorCodeOptionTest { + @Test + void testCodeOnly() throws IOException { + byte[] data = + new byte[] { + 0, + 15, // EDNS option code + 0, + 2, // option data length + 0, + 1 // extended error code + }; + EDNSOption option = EDNSOption.fromWire(data); + assertTrue(option instanceof ExtendedErrorCodeOption, "Expected ExtendedErrorCodeOption"); + ExtendedErrorCodeOption ede = (ExtendedErrorCodeOption) option; + assertEquals(1, ede.getErrorCode()); + assertNull(ede.getText()); + assertArrayEquals(data, ede.toWire()); + assertArrayEquals(data, new ExtendedErrorCodeOption(1).toWire()); + } + + @Test + void testCodeAndText() throws IOException { + byte[] data = + new byte[] { + 0, + 15, // EDNS option code + 0, + 4, // option data length + 0, + 1, // extended error code + (byte) 'a', + (byte) 'b' + }; + EDNSOption option = EDNSOption.fromWire(data); + assertTrue(option instanceof ExtendedErrorCodeOption, "Expected ExtendedErrorCodeOption"); + ExtendedErrorCodeOption ede = (ExtendedErrorCodeOption) option; + assertEquals(1, ede.getErrorCode()); + assertEquals("ab", ede.getText()); + assertArrayEquals(data, ede.toWire()); + assertArrayEquals(data, new ExtendedErrorCodeOption(1, "ab").toWire()); + } + + @Test + void testCodeAndTextNullTerminated() throws IOException { + byte[] inputData = + new byte[] { + 0, + 15, // EDNS option code + 0, + 5, // option data length + 0, + 1, // extended error code + (byte) 'a', + (byte) 'b', + 0 + }; + EDNSOption option = EDNSOption.fromWire(inputData); + assertTrue(option instanceof ExtendedErrorCodeOption, "Expected ExtendedErrorCodeOption"); + ExtendedErrorCodeOption ede = (ExtendedErrorCodeOption) option; + assertEquals(1, ede.getErrorCode()); + assertEquals("ab", ede.getText()); + + byte[] outputDataNonNullTerminated = + new byte[] { + 0, + 15, // EDNS option code + 0, + 4, // option data length + 0, + 1, // extended error code + (byte) 'a', + (byte) 'b', + }; + assertArrayEquals(outputDataNonNullTerminated, ede.toWire()); + } + + @Test + void testToStringCodeOnly() { + ExtendedErrorCodeOption option = new ExtendedErrorCodeOption(1); + assertEquals("Unsupported DNSKEY Algorithm", option.optionToString()); + } + + @Test + void testToStringUnknownCode() { + ExtendedErrorCodeOption option = new ExtendedErrorCodeOption(49152); + assertEquals("EDE49152", option.optionToString()); + } + + @Test + void testToStringCodeAndText() { + ExtendedErrorCodeOption option = new ExtendedErrorCodeOption(1, "ab"); + assertEquals("Unsupported DNSKEY Algorithm: ab", option.optionToString()); + } +} From 4a2df35c6a4516ce46f2623ea1480e19398f1ddc Mon Sep 17 00:00:00 2001 From: amitknx <82438262+amitknx@users.noreply.github.com> Date: Sun, 18 Apr 2021 19:01:51 +0530 Subject: [PATCH 019/261] Fix cache timeout based on SOA record to minimum of ttl and minimum field (#191) --- src/main/java/org/xbill/DNS/Cache.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/xbill/DNS/Cache.java b/src/main/java/org/xbill/DNS/Cache.java index d24a2cc58..8f7a1ae8e 100644 --- a/src/main/java/org/xbill/DNS/Cache.java +++ b/src/main/java/org/xbill/DNS/Cache.java @@ -91,7 +91,7 @@ public NegativeElement(Name name, int type, SOARecord soa, int cred, long maxttl this.type = type; long cttl = 0; if (soa != null) { - cttl = soa.getMinimum(); + cttl = Math.min(soa.getMinimum(), soa.getTTL()); } this.credibility = cred; this.expire = limitExpire(cttl, maxttl); @@ -398,7 +398,7 @@ public synchronized void addRRset(RRset rrset, int cred) { public synchronized void addNegative(Name name, int type, SOARecord soa, int cred) { long ttl = 0; if (soa != null) { - ttl = soa.getTTL(); + ttl = Math.min(soa.getMinimum(), soa.getTTL()); } Element element = findElement(name, type, 0); if (ttl == 0) { From 48eae36fd96a76d1845a80c58f7b29112ded155b Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Sat, 1 May 2021 16:23:47 +0200 Subject: [PATCH 020/261] Update BouncyCastle Closes #193 --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index bf64add03..db87c5f16 100644 --- a/pom.xml +++ b/pom.xml @@ -361,7 +361,7 @@ org.bouncycastle bcprov-jdk15on - 1.66 + 1.68 true From 91015e23d8e17ad9243382d143b172c5868526fb Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Sat, 29 May 2021 14:09:14 +0200 Subject: [PATCH 021/261] Add a method to shutdown the network I/O (#192) Closes #180 --- src/main/java/org/xbill/DNS/Lookup.java | 2 +- .../xbill/DNS/{Client.java => NioClient.java} | 64 ++++++-- src/main/java/org/xbill/DNS/NioTcpClient.java | 2 +- src/main/java/org/xbill/DNS/NioUdpClient.java | 2 +- src/main/java/org/xbill/DNS/TCPClient.java | 6 +- .../java/org/xbill/DNS/NioTcpClientTest.java | 152 +++++++++--------- 6 files changed, 138 insertions(+), 90 deletions(-) rename src/main/java/org/xbill/DNS/{Client.java => NioClient.java} (62%) diff --git a/src/main/java/org/xbill/DNS/Lookup.java b/src/main/java/org/xbill/DNS/Lookup.java index bea3cdf4f..32e7ecea9 100644 --- a/src/main/java/org/xbill/DNS/Lookup.java +++ b/src/main/java/org/xbill/DNS/Lookup.java @@ -215,7 +215,7 @@ private static List convertSearchPathDomainList(List domains) { * @param logger The logger */ public static synchronized void setPacketLogger(PacketLogger logger) { - Client.setPacketLogger(logger); + NioClient.setPacketLogger(logger); } private void reset() { diff --git a/src/main/java/org/xbill/DNS/Client.java b/src/main/java/org/xbill/DNS/NioClient.java similarity index 62% rename from src/main/java/org/xbill/DNS/Client.java rename to src/main/java/org/xbill/DNS/NioClient.java index 00c31c17e..5fe217f11 100644 --- a/src/main/java/org/xbill/DNS/Client.java +++ b/src/main/java/org/xbill/DNS/NioClient.java @@ -11,19 +11,31 @@ import java.util.Iterator; import java.util.List; import java.util.concurrent.CopyOnWriteArrayList; +import lombok.AccessLevel; +import lombok.NoArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.xbill.DNS.utils.hexdump; +/** + * Manages the network I/O for the {@link SimpleResolver}. It is mostly an implementation detail of + * {@code dnsjava} and the only method intended to be called is {@link #close()} - and only if + * {@code dnsjava} is used in an application container like Tomcat. In a normal JVM setup {@link + * #close()} is called by a shutdown hook. + * + * @since 3.4 + */ @Slf4j -class Client { +@NoArgsConstructor(access = AccessLevel.NONE) +public abstract class NioClient { /** Packet logger, if available. */ private static PacketLogger packetLogger = null; - private static volatile boolean run = true; private static final List timeoutTasks = new CopyOnWriteArrayList<>(); private static final List closeTasks = new CopyOnWriteArrayList<>(); private static Thread selectorThread; + private static Thread closeThread; private static volatile Selector selector; + private static volatile boolean run; interface KeyProcessor { void processReadyKey(SelectionKey key); @@ -31,15 +43,16 @@ interface KeyProcessor { static Selector selector() throws IOException { if (selector == null) { - synchronized (Client.class) { + synchronized (NioClient.class) { if (selector == null) { selector = Selector.open(); log.debug("Starting dnsjava NIO selector thread"); - selectorThread = new Thread(Client::runSelector); + run = true; + selectorThread = new Thread(NioClient::runSelector); selectorThread.setDaemon(true); selectorThread.setName("dnsjava NIO selector"); selectorThread.start(); - Thread closeThread = new Thread(Client::close); + closeThread = new Thread(() -> close(true)); closeThread.setName("dnsjava NIO shutdown hook"); Runtime.getRuntime().addShutdownHook(closeThread); } @@ -49,17 +62,48 @@ static Selector selector() throws IOException { return selector; } - private static void close() { + /** Shutdown the network I/O used by the {@link SimpleResolver}. */ + public static void close() { + close(false); + } + + private static void close(boolean fromHook) { run = false; - closeTasks.forEach(Runnable::run); - timeoutTasks.clear(); + + if (!fromHook) { + try { + Runtime.getRuntime().removeShutdownHook(closeThread); + } catch (Exception ex) { + log.warn("Failed to remove shutdown hoook, ignoring and continuing close"); + } + } + + for (Runnable closeTask : closeTasks) { + try { + closeTask.run(); + } catch (Exception e) { + log.warn("Failed to execute a shutdown task, ignoring and continuing close", e); + } + } + selector.wakeup(); + try { selector.close(); + } catch (IOException e) { + log.warn("Failed to properly close selector, ignoring and continuing close", e); + } + + try { selectorThread.join(); - } catch (InterruptedException | IOException e) { - log.warn("Failed to properly shutdown", e); + } catch (InterruptedException e) { Thread.currentThread().interrupt(); + } finally { + synchronized (NioClient.class) { + selector = null; + selectorThread = null; + closeThread = null; + } } } diff --git a/src/main/java/org/xbill/DNS/NioTcpClient.java b/src/main/java/org/xbill/DNS/NioTcpClient.java index d4061e583..1153f62db 100644 --- a/src/main/java/org/xbill/DNS/NioTcpClient.java +++ b/src/main/java/org/xbill/DNS/NioTcpClient.java @@ -23,7 +23,7 @@ @Slf4j @UtilityClass -final class NioTcpClient extends Client { +final class NioTcpClient extends NioClient { private static final Queue registrationQueue = new ConcurrentLinkedQueue<>(); private static final Map channelMap = new ConcurrentHashMap<>(); diff --git a/src/main/java/org/xbill/DNS/NioUdpClient.java b/src/main/java/org/xbill/DNS/NioUdpClient.java index cd39d474b..5dba9891d 100644 --- a/src/main/java/org/xbill/DNS/NioUdpClient.java +++ b/src/main/java/org/xbill/DNS/NioUdpClient.java @@ -22,7 +22,7 @@ @Slf4j @UtilityClass -final class NioUdpClient extends Client { +final class NioUdpClient extends NioClient { private static final int EPHEMERAL_START; private static final int EPHEMERAL_RANGE; diff --git a/src/main/java/org/xbill/DNS/TCPClient.java b/src/main/java/org/xbill/DNS/TCPClient.java index 520cc890e..aea727c81 100644 --- a/src/main/java/org/xbill/DNS/TCPClient.java +++ b/src/main/java/org/xbill/DNS/TCPClient.java @@ -13,7 +13,7 @@ import java.nio.channels.SocketChannel; import java.util.concurrent.TimeUnit; -final class TCPClient extends Client { +final class TCPClient { private long endTime; private SelectionKey key; @@ -63,7 +63,7 @@ void connect(SocketAddress addr) throws IOException { void send(byte[] data) throws IOException { SocketChannel channel = (SocketChannel) key.channel(); - verboseLog( + NioClient.verboseLog( "TCP write", channel.socket().getLocalSocketAddress(), channel.socket().getRemoteSocketAddress(), @@ -150,7 +150,7 @@ byte[] recv() throws IOException { int length = ((buf[0] & 0xFF) << 8) + (buf[1] & 0xFF); byte[] data = _recv(length); SocketChannel channel = (SocketChannel) key.channel(); - verboseLog( + NioClient.verboseLog( "TCP read", channel.socket().getLocalSocketAddress(), channel.socket().getRemoteSocketAddress(), diff --git a/src/test/java/org/xbill/DNS/NioTcpClientTest.java b/src/test/java/org/xbill/DNS/NioTcpClientTest.java index 96efd017b..bbb15ba1c 100644 --- a/src/test/java/org/xbill/DNS/NioTcpClientTest.java +++ b/src/test/java/org/xbill/DNS/NioTcpClientTest.java @@ -20,89 +20,93 @@ public class NioTcpClientTest { @Test void testResponseStream() throws InterruptedException, IOException { - // start the selector thread early - Client.selector(); + try { + // start the selector thread early + NioClient.selector(); - Record qr = Record.newRecord(Name.fromConstantString("example.com."), Type.A, DClass.IN); - Message[] q = new Message[] {Message.newQuery(qr), Message.newQuery(qr)}; - CountDownLatch cdlServerThreadStart = new CountDownLatch(1); - CountDownLatch cdlQueryRepliesReceived = new CountDownLatch(q.length); - ServerSocket ss = new ServerSocket(0, 0, InetAddress.getLoopbackAddress()); - ss.setSoTimeout(5000); - Thread server = - new Thread( - () -> { - try { - cdlServerThreadStart.countDown(); - Socket s = ss.accept(); - for (int i = 0; i < q.length; i++) { - try { - InputStream is = s.getInputStream(); - byte[] lengthData = new byte[2]; - int readLength = is.read(lengthData); - assertEquals(2, readLength); - byte[] messageData = new byte[(lengthData[0] << 8) + lengthData[1]]; - int readMessageLength = is.read(messageData); - assertEquals(messageData.length, readMessageLength); - Message serverReceivedMessages = new Message(messageData); + Record qr = Record.newRecord(Name.fromConstantString("example.com."), Type.A, DClass.IN); + Message[] q = new Message[] {Message.newQuery(qr), Message.newQuery(qr)}; + CountDownLatch cdlServerThreadStart = new CountDownLatch(1); + CountDownLatch cdlQueryRepliesReceived = new CountDownLatch(q.length); + ServerSocket ss = new ServerSocket(0, 0, InetAddress.getLoopbackAddress()); + ss.setSoTimeout(5000); + Thread server = + new Thread( + () -> { + try { + cdlServerThreadStart.countDown(); + Socket s = ss.accept(); + for (int i = 0; i < q.length; i++) { + try { + InputStream is = s.getInputStream(); + byte[] lengthData = new byte[2]; + int readLength = is.read(lengthData); + assertEquals(2, readLength); + byte[] messageData = new byte[(lengthData[0] << 8) + lengthData[1]]; + int readMessageLength = is.read(messageData); + assertEquals(messageData.length, readMessageLength); + Message serverReceivedMessages = new Message(messageData); - for (int j = q.length - 1; j >= 0; j--) { - Message answer = new Message(); - answer.getHeader().setRcode(Rcode.NOERROR); - answer.getHeader().setID(serverReceivedMessages.getHeader().getID()); - answer.addRecord(serverReceivedMessages.getQuestion(), Section.QUESTION); - answer.addRecord( - new ARecord( - Name.fromConstantString("example.com."), - DClass.IN, - 900, - InetAddress.getLoopbackAddress()), - Section.ANSWER); - byte[] queryData = answer.toWire(); - ByteBuffer buffer = ByteBuffer.allocate(queryData.length + 2); - buffer.put((byte) (queryData.length >>> 8)); - buffer.put((byte) (queryData.length & 0xFF)); - buffer.put(queryData); - s.getOutputStream().write(buffer.array()); - } + for (int j = q.length - 1; j >= 0; j--) { + Message answer = new Message(); + answer.getHeader().setRcode(Rcode.NOERROR); + answer.getHeader().setID(serverReceivedMessages.getHeader().getID()); + answer.addRecord(serverReceivedMessages.getQuestion(), Section.QUESTION); + answer.addRecord( + new ARecord( + Name.fromConstantString("example.com."), + DClass.IN, + 900, + InetAddress.getLoopbackAddress()), + Section.ANSWER); + byte[] queryData = answer.toWire(); + ByteBuffer buffer = ByteBuffer.allocate(queryData.length + 2); + buffer.put((byte) (queryData.length >>> 8)); + buffer.put((byte) (queryData.length & 0xFF)); + buffer.put(queryData); + s.getOutputStream().write(buffer.array()); + } - } catch (IOException e) { - fail(e); + } catch (IOException e) { + fail(e); + } } - } - } catch (SocketTimeoutException ste) { - fail("Timeout waiting for a client connection", ste); - } catch (IOException e) { - fail(e); - } - }); - server.start(); - - if (!cdlServerThreadStart.await(5, TimeUnit.SECONDS)) { - fail("timed out waiting for server thread to start"); - } - - for (int j = 0; j < q.length; j++) { - int jj = j; - NioTcpClient.sendrecv( - null, - (InetSocketAddress) ss.getLocalSocketAddress(), - q[j], - q[j].toWire(), - Duration.ofSeconds(5)) - .thenAccept( - d -> { - try { - assertEquals(q[jj].getHeader().getID(), new Message(d).getHeader().getID()); - cdlQueryRepliesReceived.countDown(); + } catch (SocketTimeoutException ste) { + fail("Timeout waiting for a client connection", ste); } catch (IOException e) { fail(e); } }); - } + server.start(); + + if (!cdlServerThreadStart.await(5, TimeUnit.SECONDS)) { + fail("timed out waiting for server thread to start"); + } + + for (int j = 0; j < q.length; j++) { + int jj = j; + NioTcpClient.sendrecv( + null, + (InetSocketAddress) ss.getLocalSocketAddress(), + q[j], + q[j].toWire(), + Duration.ofSeconds(5)) + .thenAccept( + d -> { + try { + assertEquals(q[jj].getHeader().getID(), new Message(d).getHeader().getID()); + cdlQueryRepliesReceived.countDown(); + } catch (IOException e) { + fail(e); + } + }); + } - if (!cdlQueryRepliesReceived.await(5, TimeUnit.SECONDS)) { - fail("timed out waiting for answers"); + if (!cdlQueryRepliesReceived.await(5, TimeUnit.SECONDS)) { + fail("timed out waiting for answers"); + } + } finally { + NioClient.close(); } } } From ca9d3815eca92d2caf00a60ffd02d84030f20dfc Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Mon, 31 May 2021 19:30:00 +0200 Subject: [PATCH 022/261] Keep order of resolvers during construction --- .../java/org/xbill/DNS/ExtendedResolver.java | 31 +++++-------------- 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/src/main/java/org/xbill/DNS/ExtendedResolver.java b/src/main/java/org/xbill/DNS/ExtendedResolver.java index 9286880e5..fe86c8394 100644 --- a/src/main/java/org/xbill/DNS/ExtendedResolver.java +++ b/src/main/java/org/xbill/DNS/ExtendedResolver.java @@ -16,7 +16,6 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; -import java.util.stream.StreamSupport; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -192,25 +191,10 @@ public ExtendedResolver() { * @exception UnknownHostException A server name could not be resolved */ public ExtendedResolver(String[] servers) throws UnknownHostException { - try { - resolvers.addAll( - Arrays.stream(servers) - .map( - server -> { - try { - Resolver r = new SimpleResolver(server); - r.setTimeout(DEFAULT_RESOLVER_TIMEOUT); - return new ResolverEntry(r); - } catch (UnknownHostException e) { - throw new RuntimeException(e); - } - }) - .collect(Collectors.toSet())); - } catch (RuntimeException e) { - if (e.getCause() instanceof UnknownHostException) { - throw (UnknownHostException) e.getCause(); - } - throw e; + for (String server : servers) { + Resolver r = new SimpleResolver(server); + r.setTimeout(DEFAULT_RESOLVER_TIMEOUT); + resolvers.add(new ResolverEntry(r)); } } @@ -231,10 +215,9 @@ public ExtendedResolver(Resolver[] resolvers) { * @param resolvers An iterable of pre-initialized {@link Resolver}s. */ public ExtendedResolver(Iterable resolvers) { - this.resolvers.addAll( - StreamSupport.stream(resolvers.spliterator(), false) - .map(ResolverEntry::new) - .collect(Collectors.toSet())); + for (Resolver r : resolvers) { + this.resolvers.add(new ResolverEntry(r)); + } } @Override From b78a29ae9163d36df32768336a656e20fa2d1802 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Wed, 9 Jun 2021 19:11:15 +0200 Subject: [PATCH 023/261] Enable parsing the local hosts database in lookups (#195) --- README.md | 9 + src/main/java/org/xbill/DNS/Lookup.java | 62 ++++ .../org/xbill/DNS/hosts/HostsFileParser.java | 235 +++++++++++++++ .../org/xbill/DNS/lookup/LookupSession.java | 76 ++++- src/test/java/org/xbill/DNS/LookupTest.java | 47 +++ .../xbill/DNS/hosts/HostsFileParserTest.java | 277 ++++++++++++++++++ .../xbill/DNS/lookup/LookupSessionTest.java | 172 +++++++++-- src/test/resources/hosts_invalid | 13 + src/test/resources/hosts_windows | 28 ++ 9 files changed, 887 insertions(+), 32 deletions(-) create mode 100644 src/main/java/org/xbill/DNS/hosts/HostsFileParser.java create mode 100644 src/test/java/org/xbill/DNS/hosts/HostsFileParserTest.java create mode 100644 src/test/resources/hosts_invalid create mode 100644 src/test/resources/hosts_windows diff --git a/README.md b/README.md index e751915d0..536c59f0b 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,15 @@ Some settings of dnsjava can be configured via Maximum number of CNAMEs to follow in a chain. + + dnsjava.lookup.use_hosts_file + Boolean + true + false + + + Use the system's hosts file for lookups before resorting to a resolver. + diff --git a/src/main/java/org/xbill/DNS/Lookup.java b/src/main/java/org/xbill/DNS/Lookup.java index 32e7ecea9..073697aae 100644 --- a/src/main/java/org/xbill/DNS/Lookup.java +++ b/src/main/java/org/xbill/DNS/Lookup.java @@ -5,13 +5,18 @@ import java.io.IOException; import java.io.InterruptedIOException; +import java.net.InetAddress; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.stream.Collectors; +import lombok.Getter; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.hosts.HostsFileParser; /** * The Lookup object issues queries to caching DNS servers. The input consists of a name, an @@ -35,6 +40,7 @@ public final class Lookup { private static List defaultSearchPath; private static Map defaultCaches; private static int defaultNdots; + private static HostsFileParser defaultHostsFileParser; private Resolver resolver; private List searchPath; @@ -63,6 +69,13 @@ public final class Lookup { private boolean cycleResults = true; private int maxIterations; + /** + * Gets or sets the local hosts database parser to use for lookup before using a {@link Resolver}. + * + * @since 3.4 + */ + @Getter @Setter private HostsFileParser hostsFileParser; + private static final Name[] noAliases = new Name[0]; /** The lookup was successful. */ @@ -85,6 +98,7 @@ public static synchronized void refreshDefault() { defaultSearchPath = ResolverConfig.getCurrentConfig().searchPath(); defaultCaches = new HashMap<>(); defaultNdots = ResolverConfig.getCurrentConfig().ndots(); + defaultHostsFileParser = new HostsFileParser(); } static { @@ -188,6 +202,24 @@ public static synchronized void setDefaultSearchPath(String... domains) defaultSearchPath = newdomains; } + /** + * Gets the default {@link HostsFileParser} to use for new Lookup instances. + * + * @since 3.4 + */ + public static synchronized HostsFileParser getDefaultHostsFileParser() { + return defaultHostsFileParser; + } + + /** + * Sets the default {@link HostsFileParser} to use for new Lookup instances. + * + * @since 3.4 + */ + public static synchronized void setDefaultHostsFileParser(HostsFileParser hostsFileParser) { + defaultHostsFileParser = hostsFileParser; + } + private static List convertSearchPathDomainList(List domains) { try { return domains.stream() @@ -274,6 +306,9 @@ public Lookup(Name name, int type, int dclass) { this.result = -1; this.maxIterations = Integer.parseInt(System.getProperty("dnsjava.lookup.max_iterations", "16")); + if (Boolean.parseBoolean(System.getProperty("dnsjava.lookup.use_hosts_file", "true"))) { + this.hostsFileParser = getDefaultHostsFileParser(); + } } /** @@ -513,6 +548,10 @@ private void processResponse(Name name, SetResponse response) { } private void lookup(Name current) { + if (lookupFromHostsFile(current)) { + return; + } + SetResponse sr = cache.lookupRecords(current, type, credibility); log.debug("Lookup for {}/{}, cache answer: {}", current, Type.string(type), sr); @@ -569,6 +608,29 @@ private void lookup(Name current) { processResponse(current, sr); } + private boolean lookupFromHostsFile(Name current) { + if (hostsFileParser != null && (type == Type.A || type == Type.AAAA)) { + try { + Optional localLookup = hostsFileParser.getAddressForHost(current, type); + if (localLookup.isPresent()) { + result = SUCCESSFUL; + done = true; + if (type == Type.A) { + answers = new ARecord[] {new ARecord(current, dclass, 0L, localLookup.get())}; + } else { + answers = new AAAARecord[] {new AAAARecord(current, dclass, 0L, localLookup.get())}; + } + + return true; + } + } catch (IOException e) { + log.debug("Local hosts database parsing failed, ignoring and using resolver", e); + } + } + + return false; + } + private void resolve(Name current, Name suffix) { doneCurrent = false; Name tname; diff --git a/src/main/java/org/xbill/DNS/hosts/HostsFileParser.java b/src/main/java/org/xbill/DNS/hosts/HostsFileParser.java new file mode 100644 index 000000000..9dca2ccbb --- /dev/null +++ b/src/main/java/org/xbill/DNS/hosts/HostsFileParser.java @@ -0,0 +1,235 @@ +// SPDX-License-Identifier: BSD-2-Clause +package org.xbill.DNS.hosts; + +import java.io.BufferedReader; +import java.io.IOException; +import java.net.InetAddress; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.Instant; +import java.util.Arrays; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.Address; +import org.xbill.DNS.Name; +import org.xbill.DNS.TextParseException; +import org.xbill.DNS.Type; + +/** + * Parses and caches the systems local hosts database, otherwise known as {@code /etc/hosts}. The + * cache is cleared when the file is modified. + * + * @since 3.4 + */ +@Slf4j +public final class HostsFileParser { + private static final int MAX_FULL_CACHE_FILE_SIZE_BYTES = 16384; + + private final Map hostsCache = new HashMap<>(); + private final Path path; + private final boolean clearCacheOnChange; + private Instant lastFileReadTime = Instant.MIN; + private boolean isEntireFileParsed; + + /** + * Creates a new instance based on the current OS's default. Unix and alike (or rather everything + * else than Windows) use {@code /etc/hosts}, while on Windows {@code + * %SystemRoot%\System32\drivers\etc\hosts} is used. The cache is cleared when the file has + * changed. + */ + public HostsFileParser() { + this( + System.getProperty("os.name").contains("Windows") + ? Paths.get(System.getenv("SystemRoot"), "\\System32\\drivers\\etc\\hosts") + : Paths.get("/etc/hosts"), + true); + } + + /** + * Creates an instance with a custom hosts database path. The cache is cleared when the file has + * changed. + * + * @param path The path to the hosts database. + */ + public HostsFileParser(Path path) { + this(path, true); + } + + /** + * Creates an instance with a custom hosts database path. + * + * @param path The path to the hosts database. + * @param clearCacheOnChange set to true to clear the cache when the hosts file changes. + */ + public HostsFileParser(Path path, boolean clearCacheOnChange) { + this.path = Objects.requireNonNull(path, "path is required"); + this.clearCacheOnChange = clearCacheOnChange; + if (Files.isDirectory(path)) { + throw new IllegalArgumentException("path must be a file"); + } + } + + /** + * Performs on-demand parsing and caching of the local hosts database. + * + * @param name the hostname to search for. + * @param type Record type to search for, see {@link org.xbill.DNS.Type}. + * @return The first address found for the requested hostname. + * @throws IOException When the parsing fails. + * @throws IllegalArgumentException when {@code type} is not {@link org.xbill.DNS.Type#A} or{@link + * org.xbill.DNS.Type#AAAA}. + */ + public synchronized Optional getAddressForHost(Name name, int type) + throws IOException { + Objects.requireNonNull(name, "name is required"); + if (type != Type.A && type != Type.AAAA) { + throw new IllegalArgumentException("type can only be A or AAAA"); + } + + validateCache(); + + InetAddress cachedAddress = hostsCache.get(key(name, type)); + if (cachedAddress != null) { + return Optional.of(cachedAddress); + } + + if (isEntireFileParsed || !Files.exists(path)) { + return Optional.empty(); + } + + if (Files.size(path) <= MAX_FULL_CACHE_FILE_SIZE_BYTES) { + parseEntireHostsFile(); + } else { + searchHostsFileForEntry(name, type); + } + + return Optional.ofNullable(hostsCache.get(key(name, type))); + } + + private void parseEntireHostsFile() throws IOException { + String line; + int lineNumber = 0; + try (BufferedReader hostsReader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + while ((line = hostsReader.readLine()) != null) { + LineData lineData = parseLine(++lineNumber, line); + if (lineData != null) { + for (Name lineName : lineData.names) { + InetAddress lineAddress = + InetAddress.getByAddress(lineName.toString(true), lineData.address); + hostsCache.putIfAbsent(key(lineName, lineData.type), lineAddress); + } + } + } + } + + isEntireFileParsed = true; + } + + private void searchHostsFileForEntry(Name name, int type) throws IOException { + String line; + int lineNumber = 0; + try (BufferedReader hostsReader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) { + while ((line = hostsReader.readLine()) != null) { + LineData lineData = parseLine(++lineNumber, line); + if (lineData != null) { + for (Name lineName : lineData.names) { + boolean isSearchedEntry = lineName.equals(name); + if (isSearchedEntry && type == lineData.type) { + InetAddress lineAddress = + InetAddress.getByAddress(lineName.toString(true), lineData.address); + hostsCache.putIfAbsent(key(lineName, lineData.type), lineAddress); + return; + } + } + } + } + } + } + + @RequiredArgsConstructor + private static final class LineData { + final int type; + final byte[] address; + final Iterable names; + } + + private LineData parseLine(int lineNumber, String line) { + String[] lineTokens = getLineTokens(line); + if (lineTokens.length < 2) { + return null; + } + + int lineAddressType = Type.A; + byte[] lineAddressBytes = Address.toByteArray(lineTokens[0], Address.IPv4); + if (lineAddressBytes == null) { + lineAddressBytes = Address.toByteArray(lineTokens[0], Address.IPv6); + lineAddressType = Type.AAAA; + } + + if (lineAddressBytes == null) { + log.warn("Could not decode address {}, {}#L{}", lineTokens[0], path, lineNumber); + return null; + } + + Iterable lineNames = + Arrays.stream(lineTokens) + .skip(1) + .map(lineTokenName -> safeName(lineTokenName, lineNumber)) + .filter(Objects::nonNull) + ::iterator; + return new LineData(lineAddressType, lineAddressBytes, lineNames); + } + + private Name safeName(String name, int lineNumber) { + try { + return Name.fromString(name, Name.root); + } catch (TextParseException e) { + log.warn("Could not decode name {}, {}#L{}, skipping", name, path, lineNumber); + return null; + } + } + + private String[] getLineTokens(String line) { + // everything after a # until the end of the line is a comment + int commentStart = line.indexOf('#'); + if (commentStart == -1) { + commentStart = line.length(); + } + + return line.substring(0, commentStart).trim().split("\\s+"); + } + + private void validateCache() throws IOException { + if (clearCacheOnChange) { + // A filewatcher / inotify etc. would be nicer, but doesn't work. c.f. the write up at + // https://blog.arkey.fr/2019/09/13/watchservice-and-bind-mount/ + Instant fileTime = + Files.exists(path) ? Files.getLastModifiedTime(path).toInstant() : Instant.MAX; + if (fileTime.isAfter(lastFileReadTime)) { + // skip logging noise when the cache is empty anyway + if (!hostsCache.isEmpty()) { + log.info("Local hosts database has changed at {}, clearing cache", fileTime); + hostsCache.clear(); + } + + isEntireFileParsed = false; + lastFileReadTime = fileTime; + } + } + } + + private String key(Name name, int type) { + return name.toString() + '\t' + type; + } + + // for unit testing only + int cacheSize() { + return hostsCache.size(); + } +} diff --git a/src/main/java/org/xbill/DNS/lookup/LookupSession.java b/src/main/java/org/xbill/DNS/lookup/LookupSession.java index 6e4ccc0c1..f30ae79cd 100644 --- a/src/main/java/org/xbill/DNS/lookup/LookupSession.java +++ b/src/main/java/org/xbill/DNS/lookup/LookupSession.java @@ -1,6 +1,8 @@ // SPDX-License-Identifier: BSD-2-Clause package org.xbill.DNS.lookup; +import java.io.IOException; +import java.net.InetAddress; import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; @@ -11,15 +13,18 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.stream.Collectors; -import java.util.stream.Stream; import lombok.Builder; import lombok.NonNull; import lombok.Singular; +import lombok.extern.slf4j.Slf4j; +import org.xbill.DNS.AAAARecord; +import org.xbill.DNS.ARecord; import org.xbill.DNS.CNAMERecord; import org.xbill.DNS.Cache; import org.xbill.DNS.Credibility; import org.xbill.DNS.DClass; import org.xbill.DNS.DNAMERecord; +import org.xbill.DNS.Lookup; import org.xbill.DNS.Message; import org.xbill.DNS.Name; import org.xbill.DNS.NameTooLongException; @@ -29,6 +34,7 @@ import org.xbill.DNS.Section; import org.xbill.DNS.SetResponse; import org.xbill.DNS.Type; +import org.xbill.DNS.hosts.HostsFileParser; /** * LookupSession provides facilities to make DNS Queries. A LookupSession is intended to be long @@ -36,6 +42,7 @@ * instance returned by the builder() method. */ @Builder +@Slf4j public class LookupSession { public static final int DEFAULT_MAX_ITERATIONS = 16; public static final int DEFAULT_NDOTS = 1; @@ -70,6 +77,9 @@ public class LookupSession { @Singular("cache") private final Map caches; + /** Configures the local hosts database file parser to use within this session. */ + private final HostsFileParser hostsFileParser; + /** * A builder for {@link LookupSession} instances where functionality is mostly generated as * described in the Lombok Builder @@ -79,6 +89,16 @@ public class LookupSession { * LookupSessionBuilder#build()} on the builder instance. */ public static class LookupSessionBuilder { + /** + * Enable querying the local hosts database using the system defaults. + * + * @see HostsFileParser + */ + public LookupSessionBuilder defaultHostsFileParser() { + hostsFileParser = new HostsFileParser(); + return this; + } + void preBuild() { // note that this transform is idempotent, as concatenating an already absolute Name with root // is a noop. @@ -118,30 +138,39 @@ public LookupSession build() { * @return A {@link CompletionStage} what will yield the eventual lookup result. */ public CompletionStage lookupAsync(Name name, int type, int dclass) { + List searchNames = expandName(name); + LookupResult localHostsLookupResult = lookupWithHosts(searchNames, type); + if (localHostsLookupResult != null) { + return CompletableFuture.completedFuture(localHostsLookupResult); + } + CompletableFuture future = new CompletableFuture<>(); - lookupUntilSuccess(expandName(name).iterator(), type, dclass, future); + lookupUntilSuccess(searchNames.iterator(), type, dclass, future); return future; } /** * Generate a stream of names according to the search path application semantics. The semantics of - * this is a bit odd, but they are inherited from Lookup.java. Note that the stream returned is + * this is a bit odd, but they are inherited from {@link Lookup}. Note that the stream returned is * never empty, as it will at the very least always contain {@code name}. */ - Stream expandName(Name name) { + List expandName(Name name) { if (name.isAbsolute()) { - return Stream.of(name); + return Collections.singletonList(name); } - Stream fromSearchPath = - Stream.concat( - searchPath.stream() - .map(searchSuffix -> safeConcat(name, searchSuffix)) - .filter(Objects::nonNull), - Stream.of(safeConcat(name, Name.root))); + + List fromSearchPath = + searchPath.stream() + .map(searchSuffix -> safeConcat(name, searchSuffix)) + .filter(Objects::nonNull) + .collect(Collectors.toCollection(ArrayList::new)); if (name.labels() > ndots) { - return Stream.concat(Stream.of(safeConcat(name, Name.root)), fromSearchPath); + fromSearchPath.add(0, safeConcat(name, Name.root)); + } else { + fromSearchPath.add(safeConcat(name, Name.root)); } + return fromSearchPath; } @@ -153,6 +182,29 @@ private static Name safeConcat(Name name, Name suffix) { } } + private LookupResult lookupWithHosts(List names, int type) { + if (hostsFileParser != null && (type == Type.A || type == Type.AAAA)) { + try { + for (Name name : names) { + Optional result = hostsFileParser.getAddressForHost(name, type); + if (result.isPresent()) { + Record r; + if (type == Type.A) { + r = new ARecord(name, DClass.IN, 0, result.get()); + } else { + r = new AAAARecord(name, DClass.IN, 0, result.get()); + } + return new LookupResult(Collections.singletonList(r), Collections.emptyList()); + } + } + } catch (IOException e) { + log.debug("Local hosts database parsing failed, ignoring and using resolver", e); + } + } + + return null; + } + private void lookupUntilSuccess( Iterator names, int type, int dclass, CompletableFuture future) { diff --git a/src/test/java/org/xbill/DNS/LookupTest.java b/src/test/java/org/xbill/DNS/LookupTest.java index 404b24179..7adedf50f 100644 --- a/src/test/java/org/xbill/DNS/LookupTest.java +++ b/src/test/java/org/xbill/DNS/LookupTest.java @@ -5,6 +5,7 @@ import static java.util.Collections.singletonList; import static java.util.stream.Collectors.joining; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -16,6 +17,9 @@ import java.io.IOException; import java.io.InterruptedIOException; import java.net.InetAddress; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.nio.file.Paths; import java.util.List; import java.util.function.Function; import java.util.stream.IntStream; @@ -24,6 +28,7 @@ import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; +import org.xbill.DNS.hosts.HostsFileParser; public class LookupTest { public static final Name DUMMY_NAME = Name.fromConstantString("to.be.replaced."); @@ -337,6 +342,48 @@ void testLookup_constructorFailsWithMetaTypes() { assertThrows(IllegalArgumentException.class, () -> new Lookup("example.com.", Type.OPT)); } + @Test + void testLookupFromHosts() throws TextParseException, URISyntaxException, UnknownHostException { + Lookup lookup = new Lookup("host.docker.internal", Type.A); + wireUpMockResolver( + mockResolver, + q -> { + throw new RuntimeException("The resolver should not be invoked"); + }); + lookup.setResolver(mockResolver); + lookup.setHostsFileParser( + new HostsFileParser(Paths.get(LookupTest.class.getResource("/hosts_windows").toURI()))); + Record[] run = lookup.run(); + assertNotNull(run); + assertEquals(1, run.length); + assertEquals( + InetAddress.getByAddress( + "host.docker.internal", new byte[] {(byte) 192, (byte) 168, 10, 96}), + ((ARecord) run[0]).getAddress()); + } + + @Test + void testLookupFromHostsWithSearchDomain() + throws TextParseException, URISyntaxException, UnknownHostException { + Lookup lookup = new Lookup("host", Type.A); + lookup.setSearchPath("docker.internal"); + wireUpMockResolver( + mockResolver, + q -> { + throw new RuntimeException("The resolver should not be invoked"); + }); + lookup.setResolver(mockResolver); + lookup.setHostsFileParser( + new HostsFileParser(Paths.get(LookupTest.class.getResource("/hosts_windows").toURI()))); + Record[] run = lookup.run(); + assertNotNull(run); + assertEquals(1, run.length); + assertEquals( + InetAddress.getByAddress( + "host.docker.internal", new byte[] {(byte) 192, (byte) 168, 10, 96}), + ((ARecord) run[0]).getAddress()); + } + private Message goodAnswerWhenThreeLabels(Message query) { return answer( query, diff --git a/src/test/java/org/xbill/DNS/hosts/HostsFileParserTest.java b/src/test/java/org/xbill/DNS/hosts/HostsFileParserTest.java new file mode 100644 index 000000000..24560d5c1 --- /dev/null +++ b/src/test/java/org/xbill/DNS/hosts/HostsFileParserTest.java @@ -0,0 +1,277 @@ +// SPDX-License-Identifier: BSD-2-Clause +package org.xbill.DNS.hosts; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.BufferedWriter; +import java.io.IOException; +import java.net.InetAddress; +import java.net.URISyntaxException; +import java.net.UnknownHostException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardCopyOption; +import java.nio.file.StandardOpenOption; +import java.nio.file.attribute.FileTime; +import java.util.Optional; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; +import org.xbill.DNS.Name; +import org.xbill.DNS.Type; + +class HostsFileParserTest { + private static final Name kubernetesName = Name.fromConstantString("kubernetes.docker.internal."); + private static final byte[] localhostBytes = new byte[] {127, 0, 0, 1}; + private static Path hostsFileWindows; + private static Path hostsFileInvalid; + private static InetAddress kubernetesAddress; + + @TempDir Path tempDir; + + @BeforeAll + static void beforeAll() throws URISyntaxException, UnknownHostException { + hostsFileWindows = Paths.get(HostsFileParserTest.class.getResource("/hosts_windows").toURI()); + hostsFileInvalid = Paths.get(HostsFileParserTest.class.getResource("/hosts_invalid").toURI()); + kubernetesAddress = InetAddress.getByAddress(kubernetesName.toString(), localhostBytes); + } + + @Test + void testArguments() { + assertThrows(NullPointerException.class, () -> new HostsFileParser(null)); + assertThrows(IllegalArgumentException.class, () -> new HostsFileParser(tempDir)); + } + + @Test + void testLookupType() { + HostsFileParser hostsFileParser = new HostsFileParser(hostsFileWindows); + assertThrows( + IllegalArgumentException.class, + () -> hostsFileParser.getAddressForHost(kubernetesName, Type.MX)); + } + + @Test + void testEntireFileParsing() throws IOException { + HostsFileParser hostsFileParser = new HostsFileParser(hostsFileWindows); + assertEquals( + kubernetesAddress, + hostsFileParser + .getAddressForHost(kubernetesName, Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + } + + @Test + void testMissingFileIsEmptyResult() throws IOException { + HostsFileParser hostsFileParser = new HostsFileParser(tempDir.resolve("missing")); + assertEquals(Optional.empty(), hostsFileParser.getAddressForHost(kubernetesName, Type.A)); + } + + @Test + void testCacheLookup() throws IOException { + Path tempHosts = Files.copy(hostsFileWindows, tempDir, StandardCopyOption.REPLACE_EXISTING); + HostsFileParser hostsFileParser = new HostsFileParser(tempHosts, false); + assertEquals(0, hostsFileParser.cacheSize()); + assertEquals( + kubernetesAddress, + hostsFileParser + .getAddressForHost(kubernetesName, Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + assertTrue(hostsFileParser.cacheSize() > 1, "Cache must not be empty"); + Files.delete(tempHosts); + assertEquals( + kubernetesAddress, + hostsFileParser + .getAddressForHost(kubernetesName, Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + } + + @Test + void testFileDeletionClearsCache() throws IOException { + Path tempHosts = + Files.copy( + hostsFileWindows, + tempDir.resolve("testFileWatcherClearsCache"), + StandardCopyOption.REPLACE_EXISTING); + HostsFileParser hostsFileParser = new HostsFileParser(tempHosts); + assertEquals(0, hostsFileParser.cacheSize()); + assertEquals( + kubernetesAddress, + hostsFileParser + .getAddressForHost(kubernetesName, Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + assertTrue(hostsFileParser.cacheSize() > 1, "Cache must not be empty"); + Files.delete(tempHosts); + assertEquals(Optional.empty(), hostsFileParser.getAddressForHost(kubernetesName, Type.A)); + assertEquals(0, hostsFileParser.cacheSize()); + } + + @Test + void testFileChangeClearsCache() throws IOException { + Path tempHosts = + Files.copy( + hostsFileWindows, + tempDir.resolve("testFileWatcherClearsCache"), + StandardCopyOption.REPLACE_EXISTING); + Files.setLastModifiedTime(tempHosts, FileTime.fromMillis(0)); + HostsFileParser hostsFileParser = new HostsFileParser(tempHosts); + assertEquals(0, hostsFileParser.cacheSize()); + assertEquals( + kubernetesAddress, + hostsFileParser + .getAddressForHost(kubernetesName, Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + assertTrue(hostsFileParser.cacheSize() > 1, "Cache must not be empty"); + Name testName = Name.fromConstantString("testFileChangeClearsCache."); + try (BufferedWriter w = + Files.newBufferedWriter(tempHosts, StandardCharsets.UTF_8, StandardOpenOption.APPEND)) { + w.append("127.0.0.1 ").append(testName.toString()); + w.newLine(); + } + + Files.setLastModifiedTime(tempHosts, FileTime.fromMillis(10_0000)); + assertEquals( + InetAddress.getByAddress(testName.toString(), localhostBytes), + hostsFileParser + .getAddressForHost(testName, Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + } + + @Test + void testInvalidContentIsIgnored() throws IOException { + HostsFileParser hostsFileParser = new HostsFileParser(hostsFileInvalid); + assertEquals( + InetAddress.getByAddress("localhost", localhostBytes), + hostsFileParser + .getAddressForHost(Name.fromConstantString("localhost."), Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + assertEquals( + InetAddress.getByAddress("localalias", localhostBytes), + hostsFileParser + .getAddressForHost(Name.fromConstantString("localalias."), Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + assertEquals( + Optional.empty(), + hostsFileParser.getAddressForHost(Name.fromConstantString("some-junk."), Type.A)); + assertNotEquals( + Optional.empty(), + hostsFileParser.getAddressForHost(Name.fromConstantString("example.org."), Type.A)); + } + + @Test + void testBigFileIsNotCompletelyCachedA() throws IOException { + HostsFileParser hostsFileParser = generateLargeHostsFile("testBigFileIsNotCompletelyCachedA"); + hostsFileParser + .getAddressForHost(Name.fromConstantString("localhost-10."), Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found")); + assertEquals(1, hostsFileParser.cacheSize()); + } + + @Test + void testBigFileIsNotCompletelyCachedAAAA() throws IOException { + HostsFileParser hostsFileParser = + generateLargeHostsFile("testBigFileIsNotCompletelyCachedAAAA"); + hostsFileParser + .getAddressForHost(Name.fromConstantString("localhost-10."), Type.AAAA) + .orElseThrow(() -> new IllegalStateException("Host entry not found")); + assertEquals(1, hostsFileParser.cacheSize()); + } + + private HostsFileParser generateLargeHostsFile(String name) throws IOException { + Path generatedLargeFile = tempDir.resolve(name); + try (BufferedWriter w = Files.newBufferedWriter(generatedLargeFile)) { + for (int i = 0; i < 1024; i++) { + w.append("127.0.0.") + .append(String.valueOf(i)) + .append(" localhost-") + .append(String.valueOf(i)); + w.newLine(); + w.append("::") + .append(Integer.toHexString(i)) + .append(" localhost-") + .append(String.valueOf(i)); + w.newLine(); + } + } + return new HostsFileParser(generatedLargeFile); + } + + @Test + void testBigFileNotFoundA() throws IOException { + HostsFileParser hostsFileParser = generateLargeHostsFile("testBigFileNotFoundA"); + hostsFileParser + .getAddressForHost(Name.fromConstantString("localhost-1024."), Type.A) + .ifPresent( + entry -> { + throw new IllegalStateException("Host entry not found"); + }); + assertEquals(0, hostsFileParser.cacheSize()); + } + + @Test + void testBigFileNotFoundAAAA() throws IOException { + HostsFileParser hostsFileParser = generateLargeHostsFile("testBigFileNotFoundAAAA"); + hostsFileParser + .getAddressForHost(Name.fromConstantString("localhost-1024."), Type.AAAA) + .ifPresent( + entry -> { + throw new IllegalStateException("Host entry not found"); + }); + assertEquals(0, hostsFileParser.cacheSize()); + } + + @Test + void testDualStackLookup() throws IOException { + HostsFileParser hostsFileParser = new HostsFileParser(hostsFileInvalid); + assertEquals( + InetAddress.getByAddress("localhost", localhostBytes), + hostsFileParser + .getAddressForHost(Name.fromConstantString("localhost."), Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + byte[] ipv6Localhost = new byte[16]; + ipv6Localhost[15] = 1; + assertEquals( + InetAddress.getByAddress("localhost", ipv6Localhost), + hostsFileParser + .getAddressForHost(Name.fromConstantString("localhost."), Type.AAAA) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + } + + @Test + void testDuplicateItemReturnsFirst() throws IOException { + HostsFileParser hostsFileParser = new HostsFileParser(hostsFileInvalid); + assertEquals( + InetAddress.getByAddress("example.com", new byte[] {127, 0, 0, 5}), + hostsFileParser + .getAddressForHost(Name.fromConstantString("example.com."), Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + + // lookup a second time to validate the cache entry + assertEquals( + InetAddress.getByAddress("example.com", new byte[] {127, 0, 0, 5}), + hostsFileParser + .getAddressForHost(Name.fromConstantString("example.com."), Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + } + + @Test + void testDuplicateItemReturnsFirstOnLargeFile() throws IOException { + Path generatedLargeFile = tempDir.resolve("testDuplicateItemReturnsFirstOnLargeFile"); + try (BufferedWriter w = Files.newBufferedWriter(generatedLargeFile)) { + for (int i = 1; i < 1024; i++) { + w.append("127.0.0.").append(String.valueOf(i)).append(" localhost"); + w.newLine(); + } + } + HostsFileParser hostsFileParser = new HostsFileParser(generatedLargeFile); + assertEquals( + InetAddress.getByAddress("localhost", new byte[] {127, 0, 0, 1}), + hostsFileParser + .getAddressForHost(Name.fromConstantString("localhost."), Type.A) + .orElseThrow(() -> new IllegalStateException("Host entry not found"))); + } +} diff --git a/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java b/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java index 3c9af7d05..46fe2cab2 100644 --- a/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java +++ b/src/test/java/org/xbill/DNS/lookup/LookupSessionTest.java @@ -4,7 +4,6 @@ import static java.lang.String.format; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; -import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; @@ -20,25 +19,33 @@ import static org.xbill.DNS.LookupTest.answer; import static org.xbill.DNS.LookupTest.fail; 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.MX; import java.net.InetAddress; +import java.net.URISyntaxException; import java.net.UnknownHostException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.ExecutionException; import java.util.function.Function; -import java.util.stream.Stream; 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.mockito.ArgumentCaptor; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.xbill.DNS.AAAARecord; import org.xbill.DNS.ARecord; +import org.xbill.DNS.Address; import org.xbill.DNS.CNAMERecord; import org.xbill.DNS.Cache; import org.xbill.DNS.Credibility; @@ -53,14 +60,44 @@ import org.xbill.DNS.Section; import org.xbill.DNS.SetResponse; import org.xbill.DNS.Type; +import org.xbill.DNS.hosts.HostsFileParser; @ExtendWith(MockitoExtension.class) class LookupSessionTest { - @Mock Resolver mockResolver; + @TempDir Path tempDir; + + private static final ARecord LOOPBACK_A = + new ARecord(DUMMY_NAME, IN, 3600, InetAddress.getLoopbackAddress()); + private static final AAAARecord LOOPBACK_AAAA; + private HostsFileParser lookupSessionTestHostsFileParser; + + static { + AAAARecord record = null; + try { + record = + new AAAARecord( + DUMMY_NAME, + IN, + 3600, + InetAddress.getByAddress(Address.toByteArray("::1", Address.IPv6))); + } catch (UnknownHostException e) { + // cannot happen + } + + LOOPBACK_AAAA = record; + } + + @BeforeEach + void beforeEach() throws URISyntaxException { + lookupSessionTestHostsFileParser = + new HostsFileParser( + Paths.get(LookupSessionTest.class.getResource("/hosts_windows").toURI())); + } + @AfterEach - void after() { + void afterEach() { verifyNoMoreInteractions(mockResolver); } @@ -78,6 +115,105 @@ void lookupAsync_absoluteQuery() throws InterruptedException, ExecutionException verify(mockResolver).sendAsync(any()); } + @Test + void lookupAsync_absoluteQueryWithHosts() throws InterruptedException, ExecutionException { + LookupSession lookupSession = + LookupSession.builder() + .resolver(mockResolver) + .hostsFileParser(lookupSessionTestHostsFileParser) + .build(); + CompletionStage resultFuture = + lookupSession.lookupAsync(Name.fromConstantString("kubernetes.docker.internal."), A, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); + assertEquals( + singletonList(LOOPBACK_A.withName(name("kubernetes.docker.internal."))), + result.getRecords()); + } + + @Test + void lookupAsync_absoluteQueryWithHostsInvalidType() { + wireUpMockResolver(mockResolver, query -> fail(query, Rcode.NXDOMAIN)); + LookupSession lookupSession = + LookupSession.builder() + .resolver(mockResolver) + .hostsFileParser(lookupSessionTestHostsFileParser) + .build(); + CompletionStage resultFuture = + lookupSession.lookupAsync(Name.fromConstantString("kubernetes.docker.internal."), MX, IN); + + assertThrowsCause(NoSuchDomainException.class, () -> resultFuture.toCompletableFuture().get()); + verify(mockResolver).sendAsync(any()); + } + + @Test + void lookupAsync_absoluteAaaaQueryWithHosts() throws InterruptedException, ExecutionException { + LookupSession lookupSession = + LookupSession.builder() + .resolver(mockResolver) + .hostsFileParser(lookupSessionTestHostsFileParser) + .build(); + CompletionStage resultFuture = + lookupSession.lookupAsync(Name.fromConstantString("kubernetes.docker.internal."), AAAA, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); + assertEquals( + singletonList(LOOPBACK_AAAA.withName(name("kubernetes.docker.internal."))), + result.getRecords()); + } + + @Test + void lookupAsync_relativeQueryWithHosts() throws InterruptedException, ExecutionException { + LookupSession lookupSession = + LookupSession.builder() + .resolver(mockResolver) + .hostsFileParser(lookupSessionTestHostsFileParser) + .build(); + CompletionStage resultFuture = + lookupSession.lookupAsync(Name.fromConstantString("kubernetes.docker.internal"), A, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); + assertEquals( + singletonList(LOOPBACK_A.withName(name("kubernetes.docker.internal."))), + result.getRecords()); + } + + @Test + void lookupAsync_relativeQueryWithHostsNdots3() throws InterruptedException, ExecutionException { + LookupSession lookupSession = + LookupSession.builder() + .resolver(mockResolver) + .ndots(3) + .hostsFileParser(lookupSessionTestHostsFileParser) + .build(); + CompletionStage resultFuture = + lookupSession.lookupAsync(Name.fromConstantString("kubernetes.docker.internal"), A, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); + assertEquals( + singletonList(LOOPBACK_A.withName(name("kubernetes.docker.internal."))), + result.getRecords()); + } + + @Test + void lookupAsync_relativeQueryWithInvalidHosts() throws InterruptedException, ExecutionException { + wireUpMockResolver(mockResolver, query -> answer(query, name -> LOOPBACK_A)); + LookupSession lookupSession = + LookupSession.builder() + .resolver(mockResolver) + .hostsFileParser( + new HostsFileParser(tempDir.resolve("lookupAsync_relativeQueryWithInvalidHosts"))) + .build(); + CompletionStage resultFuture = + lookupSession.lookupAsync(Name.fromConstantString("kubernetes.docker.internal"), A, IN); + + LookupResult result = resultFuture.toCompletableFuture().get(); + assertEquals( + singletonList(LOOPBACK_A.withName(name("kubernetes.docker.internal."))), + result.getRecords()); + verify(mockResolver).sendAsync(any()); + } + @Test void lookupAsync_absoluteQueryWithCacheMiss() throws InterruptedException, ExecutionException { wireUpMockResolver(mockResolver, query -> answer(query, name -> LOOPBACK_A)); @@ -474,40 +610,39 @@ void lookupAsync_absoluteQueryWithCacheCycleResults() @Test void expandName_absolute() { LookupSession session = LookupSession.builder().resolver(mockResolver).build(); - Stream nameStream = session.expandName(name("a.")); - assertEquals(singletonList(name("a.")), nameStream.collect(toList())); + List nameStream = session.expandName(name("a.")); + assertEquals(singletonList(name("a.")), nameStream); } @Test void expandName_singleSearchPath() { LookupSession session = LookupSession.builder().resolver(mockResolver).searchPath(name("example.com.")).build(); - Stream nameStream = session.expandName(name("host")); - assertEquals(asList(name("host.example.com."), name("host.")), nameStream.collect(toList())); + List nameStream = session.expandName(name("host")); + assertEquals(asList(name("host.example.com."), name("host.")), nameStream); } @Test void expandName_notSetSearchPath() { LookupSession session = LookupSession.builder().resolver(mockResolver).build(); - Stream nameStream = session.expandName(name("host")); - assertEquals(singletonList(name("host.")), nameStream.collect(toList())); + List nameStream = session.expandName(name("host")); + assertEquals(singletonList(name("host.")), nameStream); } @Test void expandName_searchPathIsMadeAbsolute() { LookupSession session = LookupSession.builder().resolver(mockResolver).searchPath(name("example.com")).build(); - Stream nameStream = session.expandName(name("host")); - assertEquals(asList(name("host.example.com."), name("host.")), nameStream.collect(toList())); + List nameStream = session.expandName(name("host")); + assertEquals(asList(name("host.example.com."), name("host.")), nameStream); } @Test void expandName_defaultNdots() { LookupSession session = LookupSession.builder().resolver(mockResolver).searchPath(name("example.com")).build(); - Stream nameStream = session.expandName(name("a.b")); - assertEquals( - asList(name("a.b."), name("a.b.example.com."), name("a.b.")), nameStream.collect(toList())); + List nameStream = session.expandName(name("a.b")); + assertEquals(asList(name("a.b."), name("a.b.example.com.")), nameStream); } @Test @@ -518,13 +653,10 @@ void expandName_ndotsMoreThanOne() { .resolver(mockResolver) .ndots(2) .build(); - Stream nameStream = session.expandName(name("a.b")); - assertEquals(asList(name("a.b.example.com."), name("a.b.")), nameStream.collect(toList())); + List nameStream = session.expandName(name("a.b")); + assertEquals(asList(name("a.b.example.com."), name("a.b.")), nameStream); } - private static final ARecord LOOPBACK_A = - new ARecord(DUMMY_NAME, IN, 3600, InetAddress.getLoopbackAddress()); - private static CNAMERecord cname(String name, String target) { return new CNAMERecord(name(name), IN, 0, name(target)); } diff --git a/src/test/resources/hosts_invalid b/src/test/resources/hosts_invalid new file mode 100644 index 000000000..2cf1e51b7 --- /dev/null +++ b/src/test/resources/hosts_invalid @@ -0,0 +1,13 @@ +127.0.0.5 example.com +127.0.0.7 example.com +127.0.0.2 .. + +127.0.0.3 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc.ddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd +127.0.0.4 #bla + +::/0 example.net +:::1 invalid-ipv6 +127.0.0.256 invalid-ipv4 +127.0.0.1 localhost localalias +127.0.0.6 example.org #localalias some-junk +::1 localhost diff --git a/src/test/resources/hosts_windows b/src/test/resources/hosts_windows new file mode 100644 index 000000000..c922c1975 --- /dev/null +++ b/src/test/resources/hosts_windows @@ -0,0 +1,28 @@ +# Copyright (c) 1993-2009 Microsoft Corp. +# +# This is a sample HOSTS file used by Microsoft TCP/IP for Windows. +# +# This file contains the mappings of IP addresses to host names. Each +# entry should be kept on an individual line. The IP address should +# be placed in the first column followed by the corresponding host name. +# The IP address and the host name should be separated by at least one +# space. +# +# Additionally, comments (such as these) may be inserted on individual +# lines or following the machine name denoted by a '#' symbol. +# +# For example: +# +# 102.54.94.97 rhino.acme.com # source server +# 38.25.63.10 x.acme.com # x client host + +# localhost name resolution is handled within DNS itself. +# 127.0.0.1 localhost +# ::1 localhost +# Added by Docker Desktop +192.168.10.96 host.docker.internal +192.168.10.96 gateway.docker.internal +# To allow the same kube context to work on the host and the container: +127.0.0.1 kubernetes.docker.internal +::1 kubernetes.docker.internal +# End of section From 87a81d2051787117512bd98257eca0c3076927bc Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Wed, 9 Jun 2021 19:15:39 +0200 Subject: [PATCH 024/261] Clarify documentation about CNAME and wildcards #196 --- src/main/java/org/xbill/DNS/Zone.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/xbill/DNS/Zone.java b/src/main/java/org/xbill/DNS/Zone.java index 87a1f35d9..c8cda55fb 100644 --- a/src/main/java/org/xbill/DNS/Zone.java +++ b/src/main/java/org/xbill/DNS/Zone.java @@ -443,7 +443,8 @@ private RRset expandSet(RRset set, Name tname) { } /** - * Looks up Records in the Zone. This follows CNAMEs and wildcards. + * Looks up Records in the Zone. The answer can be a {@code CNAME} instead of the actual requested + * type and wildcards are expanded. * * @param name The name to look up * @param type The type to look up From 11ae6ce6c8d8763d4258c3782a607d626f217656 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Wed, 9 Jun 2021 19:58:16 +0200 Subject: [PATCH 025/261] Release v3.4.0 --- Changelog | 17 +++++++++++++++++ pom.xml | 4 ++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index 9dc751b58..fbce9f4ca 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,20 @@ +06/09/2021 + - 3.4.0 released + - UnknownHostException provides details in message (#154) + - Limit length of relative Name to 254 (#165) + - Fix wildcard lookups in Zone (#169) + - Properly close UDP channel upon error + (#177, Walter Scott Johnson) + - Fix load balancing in ExtendedResolver + (#179, Paulo Costa) + - Add method to shutdown NIO threads (#180) + - Fix restoring active position on byte buffers + (#184, @ryru) + - Add support for extended DNS errors (RFC8914, #187) + - Fix TTL for SOA record to minimum of TTL and minimum field + (#191, @amitknx) + - Add support for hosts file in lookups (#195) + 10/28/2020 - 3.3.1 released - Fix value of getAlias in C/DNameRecord (#136) diff --git a/pom.xml b/pom.xml index db87c5f16..8a3316b6e 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ dnsjava dnsjava bundle - 3.4.0-SNAPSHOT + 3.4.0 dnsjava dnsjava is an implementation of DNS in Java. It supports all defined record types (including the DNSSEC types), and unknown types. It can be used for queries, zone transfers, and dynamic updates. It includes a cache @@ -30,7 +30,7 @@ scm:git:https://github.com/dnsjava/dnsjava scm:git:https://github.com/dnsjava/dnsjava https://github.com/dnsjava/dnsjava - HEAD + v3.4.0 From 769a813dea6c1df85e24bb159a4e6dc14eb60fdd Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Wed, 9 Jun 2021 20:14:15 +0200 Subject: [PATCH 026/261] Return to -snapshot --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 8a3316b6e..ca218363e 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ dnsjava dnsjava bundle - 3.4.0 + 3.4.1-SNAPSHOT dnsjava dnsjava is an implementation of DNS in Java. It supports all defined record types (including the DNSSEC types), and unknown types. It can be used for queries, zone transfers, and dynamic updates. It includes a cache @@ -30,7 +30,7 @@ scm:git:https://github.com/dnsjava/dnsjava scm:git:https://github.com/dnsjava/dnsjava https://github.com/dnsjava/dnsjava - v3.4.0 + HEAD From 67809fa7d1ed4a06a4deac9d1c5aca5269501e11 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Wed, 9 Jun 2021 20:16:56 +0200 Subject: [PATCH 027/261] Replace Travis build badge with GitHub --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 536c59f0b..f3926811e 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -[![Build Status](https://travis-ci.org/dnsjava/dnsjava.svg?branch=master)](https://travis-ci.org/dnsjava/dnsjava) +[![dnsjava CI](https://github.com/dnsjava/dnsjava/actions/workflows/build.yml/badge.svg)](https://github.com/dnsjava/dnsjava/actions/workflows/build.yml) [![codecov](https://codecov.io/gh/dnsjava/dnsjava/branch/master/graph/badge.svg?token=FKmcwl1Oys)](https://codecov.io/gh/dnsjava/dnsjava) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/dnsjava/dnsjava/badge.svg)](https://search.maven.org/artifact/dnsjava/dnsjava) [![Javadocs](http://javadoc.io/badge/dnsjava/dnsjava.svg)](http://javadoc.io/doc/dnsjava/dnsjava) From 170c5c6db5567e24e57561dfd4a4456a2102b269 Mon Sep 17 00:00:00 2001 From: Klaus Malorny Date: Thu, 24 Jun 2021 23:11:39 +0200 Subject: [PATCH 028/261] fixes to allow signing with ED25519 and ED448 algorithms (#200) * fixes to allow signing with ED25519 and ED448 algorithms * allow keys with "EdDSA", too * added lost parenthesis --- src/main/java/org/xbill/DNS/DNSSEC.java | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/xbill/DNS/DNSSEC.java b/src/main/java/org/xbill/DNS/DNSSEC.java index b7e446005..100b8668b 100644 --- a/src/main/java/org/xbill/DNS/DNSSEC.java +++ b/src/main/java/org/xbill/DNS/DNSSEC.java @@ -680,8 +680,9 @@ public static String algString(int alg) throws UnsupportedAlgorithmException { case Algorithm.ECDSAP384SHA384: return "SHA384withECDSA"; case Algorithm.ED25519: + return "Ed25519"; case Algorithm.ED448: - return "EdDSA"; + return "Ed448"; default: throw new UnsupportedAlgorithmException(alg); } @@ -1117,6 +1118,16 @@ static void checkAlgorithm(PrivateKey key, int alg) throws UnsupportedAlgorithmE throw new IncompatibleKeyException(); } break; + case Algorithm.ED25519: + if (!"Ed25519".equals(key.getAlgorithm()) && !"EdDSA".equals(key.getAlgorithm())) { + throw new IncompatibleKeyException(); + } + break; + case Algorithm.ED448: + if (!"Ed448".equals(key.getAlgorithm()) && !"EdDSA".equals(key.getAlgorithm())) { + throw new IncompatibleKeyException(); + } + break; default: throw new UnsupportedAlgorithmException(alg); } From 259ea0d6816e0b54151bb20888ad9540a15734f9 Mon Sep 17 00:00:00 2001 From: adam-stoler <68395423+adam-stoler@users.noreply.github.com> Date: Wed, 7 Jul 2021 17:02:06 -0400 Subject: [PATCH 029/261] Rename echconfig to ech in SVCB/HTTPS records (#202) * Rename echconfig to ech * Add test for obsolete echconfig name being rejected * Add duplicate ECHCONFIG value and ParameterEchConfig class for backwards compatibility * Make ParameterEchConfig a deprecated duplicate class instead of inheriting for full compatibility * Updates for javadoc * Add backwards compatibility support for parsing echconfig name as an alias of ech Co-authored-by: Adam Stoler --- src/main/java/org/xbill/DNS/HTTPSRecord.java | 3 +- src/main/java/org/xbill/DNS/SVCBBase.java | 65 +++++++++++++++++-- src/main/java/org/xbill/DNS/SVCBRecord.java | 3 +- src/main/java/org/xbill/DNS/Type.java | 4 +- .../java/org/xbill/DNS/HTTPSRecordTest.java | 8 ++- .../java/org/xbill/DNS/SVCBRecordTest.java | 34 ++++++---- 6 files changed, 95 insertions(+), 22 deletions(-) diff --git a/src/main/java/org/xbill/DNS/HTTPSRecord.java b/src/main/java/org/xbill/DNS/HTTPSRecord.java index 01d8a0833..bce0182e6 100644 --- a/src/main/java/org/xbill/DNS/HTTPSRecord.java +++ b/src/main/java/org/xbill/DNS/HTTPSRecord.java @@ -7,7 +7,8 @@ * HTTPS Service Location and Parameter Binding Record * * @see draft-ietf-dnsop-svcb-https + * href="https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-06">draft-ietf-dnsop-svcb-https + * @since 3.3 */ public class HTTPSRecord extends SVCBBase { HTTPSRecord() {} diff --git a/src/main/java/org/xbill/DNS/SVCBBase.java b/src/main/java/org/xbill/DNS/SVCBBase.java index 26bdb2916..6ae9d11f2 100644 --- a/src/main/java/org/xbill/DNS/SVCBBase.java +++ b/src/main/java/org/xbill/DNS/SVCBBase.java @@ -17,8 +17,14 @@ import java.util.function.Supplier; import java.util.stream.Collectors; -/** Implements common functionality for SVCB and HTTPS records */ -abstract class SVCBBase extends Record { +/** + * Implements common functionality for SVCB and HTTPS records + * + * @see draft-ietf-dnsop-svcb-https + * @since 3.3 + */ +public abstract class SVCBBase extends Record { protected int svcPriority; protected Name targetName; protected final Map svcParams; @@ -28,8 +34,10 @@ abstract class SVCBBase extends Record { public static final int NO_DEFAULT_ALPN = 2; public static final int PORT = 3; public static final int IPV4HINT = 4; - public static final int ECHCONFIG = 5; + public static final int ECH = 5; public static final int IPV6HINT = 6; + /** @deprecated use {@link #ECH} */ + @Deprecated public static final int ECHCONFIG = 5; protected SVCBBase() { svcParams = new TreeMap<>(); @@ -105,8 +113,10 @@ public Supplier getFactory(int val) { parameters.add(NO_DEFAULT_ALPN, "no-default-alpn", ParameterNoDefaultAlpn::new); parameters.add(PORT, "port", ParameterPort::new); parameters.add(IPV4HINT, "ipv4hint", ParameterIpv4Hint::new); - parameters.add(ECHCONFIG, "echconfig", ParameterEchConfig::new); + parameters.add(ECH, "ech", ParameterEch::new); parameters.add(IPV6HINT, "ipv6hint", ParameterIpv6Hint::new); + /* Support obsolete echconfig name as an alias for ech */ + parameters.addAlias(ECH, "echconfig"); } public abstract static class ParameterBase { @@ -442,6 +452,53 @@ public String toString() { } } + public static class ParameterEch extends ParameterBase { + private byte[] data; + + public ParameterEch() { + super(); + } + + public ParameterEch(byte[] data) { + super(); + this.data = data; + } + + public byte[] getData() { + return data; + } + + @Override + public int getKey() { + return ECH; + } + + @Override + public void fromWire(byte[] bytes) { + data = bytes; + } + + @Override + public void fromString(String string) throws TextParseException { + if (string == null || string.isEmpty()) { + throw new TextParseException("Non-empty base64 value must be specified for ech"); + } + data = Base64.getDecoder().decode(string); + } + + @Override + public byte[] toWire() { + return data; + } + + @Override + public String toString() { + return Base64.getEncoder().encodeToString(data); + } + } + + /** @deprecated use {@link ParameterEch} */ + @Deprecated public static class ParameterEchConfig extends ParameterBase { private byte[] data; diff --git a/src/main/java/org/xbill/DNS/SVCBRecord.java b/src/main/java/org/xbill/DNS/SVCBRecord.java index 4bc9eb5c0..d78292ccd 100644 --- a/src/main/java/org/xbill/DNS/SVCBRecord.java +++ b/src/main/java/org/xbill/DNS/SVCBRecord.java @@ -7,7 +7,8 @@ * Service Location and Parameter Binding Record * * @see draft-ietf-dnsop-svcb-https + * href="https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-06">draft-ietf-dnsop-svcb-https + * @since 3.3 */ public class SVCBRecord extends SVCBBase { SVCBRecord() {} diff --git a/src/main/java/org/xbill/DNS/Type.java b/src/main/java/org/xbill/DNS/Type.java index 2b48ca745..f4348b8a1 100644 --- a/src/main/java/org/xbill/DNS/Type.java +++ b/src/main/java/org/xbill/DNS/Type.java @@ -227,7 +227,7 @@ public final class Type { * Service Location and Parameter Binding * * @see draft-ietf-dnsop-svcb-https + * href="https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-06">draft-ietf-dnsop-svcb-https */ public static final int SVCB = 64; @@ -235,7 +235,7 @@ public final class Type { * HTTPS Service Location and Parameter Binding * * @see draft-ietf-dnsop-svcb-https + * href="https://tools.ietf.org/html/draft-ietf-dnsop-svcb-https-06">draft-ietf-dnsop-svcb-https */ public static final int HTTPS = 65; diff --git a/src/test/java/org/xbill/DNS/HTTPSRecordTest.java b/src/test/java/org/xbill/DNS/HTTPSRecordTest.java index 5bdda5770..a8ee3a8f1 100644 --- a/src/test/java/org/xbill/DNS/HTTPSRecordTest.java +++ b/src/test/java/org/xbill/DNS/HTTPSRecordTest.java @@ -40,6 +40,10 @@ void createParams() throws UnknownHostException, TextParseException { assertEquals(ipv4List, ipv4hint.getAddresses()); byte[] data = {'a', 'b', 'c'}; + SVCBBase.ParameterEch ech = new SVCBBase.ParameterEch(data); + assertEquals(HTTPSRecord.ECH, ech.getKey()); + assertEquals(data, ech.getData()); + HTTPSRecord.ParameterEchConfig echconfig = new HTTPSRecord.ParameterEchConfig(data); assertEquals(HTTPSRecord.ECHCONFIG, echconfig.getKey()); assertEquals(data, echconfig.getData()); @@ -107,8 +111,8 @@ void serviceModePort() throws IOException { } @Test - void serviceModeEchConfigMulti() throws IOException { - String str = "1 h3pool. alpn=h2,h3 echconfig=1234"; + void serviceModeEchMulti() throws IOException { + String str = "1 h3pool. alpn=h2,h3 ech=1234"; assertEquals(str, SVCBRecordTest.stringToWireToString(str)); } diff --git a/src/test/java/org/xbill/DNS/SVCBRecordTest.java b/src/test/java/org/xbill/DNS/SVCBRecordTest.java index b1edf55da..fa6d8ec04 100644 --- a/src/test/java/org/xbill/DNS/SVCBRecordTest.java +++ b/src/test/java/org/xbill/DNS/SVCBRecordTest.java @@ -41,6 +41,10 @@ void createParams() throws UnknownHostException, TextParseException { assertEquals(ipv4List, ipv4hint.getAddresses()); byte[] data = {'a', 'b', 'c'}; + SVCBBase.ParameterEch ech = new SVCBBase.ParameterEch(data); + assertEquals(SVCBRecord.ECH, ech.getKey()); + assertEquals(data, ech.getData()); + SVCBRecord.ParameterEchConfig echconfig = new SVCBRecord.ParameterEchConfig(data); assertEquals(SVCBRecord.ECHCONFIG, echconfig.getKey()); assertEquals(data, echconfig.getData()); @@ -198,27 +202,33 @@ void serviceModeEscapedDomain() throws IOException { } @Test - void serviceModeEchConfig() throws IOException { - String str = "1 h3pool. echconfig=1234"; + void serviceModeEch() throws IOException { + String str = "1 h3pool. ech=1234"; assertEquals(str, stringToWireToString(str)); } @Test - void serviceModeEchConfigMulti() throws IOException { - String str = "1 h3pool. alpn=h2,h3 echconfig=1234"; + void serviceModeEchMulti() throws IOException { + String str = "1 h3pool. alpn=h2,h3 ech=1234"; assertEquals(str, stringToWireToString(str)); } @Test - void serviceModeEchConfigOutOfOrder() throws IOException { - String str = "1 h3pool. echconfig=1234 alpn=h2,h3"; - assertEquals("1 h3pool. alpn=h2,h3 echconfig=1234", stringToWireToString(str)); + void serviceModeEchOutOfOrder() throws IOException { + String str = "1 h3pool. ech=1234 alpn=h2,h3"; + assertEquals("1 h3pool. alpn=h2,h3 ech=1234", stringToWireToString(str)); + } + + @Test + void serviceModeEchQuoted() throws IOException { + String str = "1 h3pool. alpn=h2,h3 ech=\"1234\""; + assertEquals("1 h3pool. alpn=h2,h3 ech=1234", stringToWireToString(str)); } @Test - void serviceModeEchConfigQuoted() throws IOException { - String str = "1 h3pool. alpn=h2,h3 echconfig=\"1234\""; - assertEquals("1 h3pool. alpn=h2,h3 echconfig=1234", stringToWireToString(str)); + void serviceModeObsoleteEchConfigName() throws IOException { + String str = "1 . echconfig=1234"; + assertEquals("1 . ech=1234", stringToWireToString(str)); } @Test @@ -377,8 +387,8 @@ void zeroLengthIpv4Hint() { } @Test - void zeroLengthEchConfig() { - String str = "1 . echconfig"; + void zeroLengthEch() { + String str = "1 . ech"; assertThrows(TextParseException.class, () -> stringToWire(str)); } From 403ce32982fdc8fde772cbd74d2eec9c2c7d336f Mon Sep 17 00:00:00 2001 From: adam-stoler <68395423+adam-stoler@users.noreply.github.com> Date: Sun, 18 Jul 2021 15:22:15 -0400 Subject: [PATCH 030/261] Fix issue with Name compareTo and the handling of octal digit byte values (#205) * Fix signed integer issue with octal digits in Name.compareTo() * Also properly handle signed byte in Name.hashCode() Co-authored-by: Adam Stoler --- src/main/java/org/xbill/DNS/Name.java | 6 ++++-- src/test/java/org/xbill/DNS/NameTest.java | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/xbill/DNS/Name.java b/src/main/java/org/xbill/DNS/Name.java index 31c965a91..9f3d4998a 100644 --- a/src/main/java/org/xbill/DNS/Name.java +++ b/src/main/java/org/xbill/DNS/Name.java @@ -809,7 +809,7 @@ public int hashCode() { } int code = 0; for (int i = offset(0); i < name.length; i++) { - code += (code << 3) + lowercase[name[i] & 0xFF]; + code += (code << 3) + (lowercase[name[i] & 0xFF] & 0xFF); } hashcode = code; return hashcode; @@ -839,7 +839,9 @@ public int compareTo(Name arg) { int length = name[start]; int alength = arg.name[astart]; for (int j = 0; j < length && j < alength; j++) { - int n = lowercase[name[j + start + 1] & 0xFF] - lowercase[arg.name[j + astart + 1] & 0xFF]; + int n = + (lowercase[name[j + start + 1] & 0xFF] & 0xFF) + - (lowercase[arg.name[j + astart + 1] & 0xFF] & 0xFF); if (n != 0) { return n; } diff --git a/src/test/java/org/xbill/DNS/NameTest.java b/src/test/java/org/xbill/DNS/NameTest.java index 15769065b..589e08188 100644 --- a/src/test/java/org/xbill/DNS/NameTest.java +++ b/src/test/java/org/xbill/DNS/NameTest.java @@ -1502,6 +1502,24 @@ void more_labels() throws TextParseException { assertTrue(n1.compareTo(n2) < 0); assertTrue(n2.compareTo(n1) > 0); } + + @Test + void octal_digits_low() throws TextParseException { + Name n1 = new Name("\004.b.a."); + Name n2 = new Name("c.b.a."); + + assertTrue(n1.compareTo(n2) < 0); + assertTrue(n2.compareTo(n1) > 0); + } + + @Test + void octal_digits_high() throws TextParseException { + Name n1 = new Name("c.b.a."); + Name n2 = new Name("\237.b.a."); + + assertTrue(n1.compareTo(n2) < 0); + assertTrue(n2.compareTo(n1) > 0); + } } @Test From 018b45f5b2d49612c607e46d64f5744ca28e6a91 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Fri, 30 Jul 2021 11:33:32 +0200 Subject: [PATCH 031/261] Release v3.4.1 --- Changelog | 9 +++++++++ pom.xml | 4 ++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/Changelog b/Changelog index fbce9f4ca..94a196c1b 100644 --- a/Changelog +++ b/Changelog @@ -1,3 +1,12 @@ +07/30/2021 + - 3.4.1 released + - Allow signing with ED25519 and ED448 algorithms + (#200, Klaus Malorny) + - Rename echconfig to ech in SVCB/HTTPS records + (#202, @adam-stoler) + - Fix bug in Name.compareTo with byte-values >= 128 + (#205, @adam-stoler) + 06/09/2021 - 3.4.0 released - UnknownHostException provides details in message (#154) diff --git a/pom.xml b/pom.xml index ca218363e..b2ecd4917 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ dnsjava dnsjava bundle - 3.4.1-SNAPSHOT + 3.4.1 dnsjava dnsjava is an implementation of DNS in Java. It supports all defined record types (including the DNSSEC types), and unknown types. It can be used for queries, zone transfers, and dynamic updates. It includes a cache @@ -30,7 +30,7 @@ scm:git:https://github.com/dnsjava/dnsjava scm:git:https://github.com/dnsjava/dnsjava https://github.com/dnsjava/dnsjava - HEAD + v3.4.1 From b52f4b882538d1f29c22cbe2ae28862d24ab2662 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Fri, 30 Jul 2021 12:01:43 +0200 Subject: [PATCH 032/261] Return to -snapshot --- pom.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index b2ecd4917..a76c62d43 100644 --- a/pom.xml +++ b/pom.xml @@ -7,7 +7,7 @@ dnsjava dnsjava bundle - 3.4.1 + 3.5.0-SNAPSHOT dnsjava dnsjava is an implementation of DNS in Java. It supports all defined record types (including the DNSSEC types), and unknown types. It can be used for queries, zone transfers, and dynamic updates. It includes a cache @@ -30,7 +30,7 @@ scm:git:https://github.com/dnsjava/dnsjava scm:git:https://github.com/dnsjava/dnsjava https://github.com/dnsjava/dnsjava - v3.4.1 + HEAD From f639a4701d9c5482a216b62362fb57e5ea64cce2 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Fri, 30 Jul 2021 12:19:29 +0200 Subject: [PATCH 033/261] Test with Java 17ea and switch to AdoptOpenJDK --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3688b7630..60c172c57 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: os: [ ubuntu-18.04, ubuntu-20.04, windows-latest ] - java: [ '8', '11' ] + java: [ '8', '11', '17-ea' ] arch: [ 'x86', 'x64' ] exclude: - os: ubuntu-18.04 @@ -43,7 +43,7 @@ jobs: with: java-version: ${{ matrix.java }} architecture: ${{ matrix.arch }} - distribution: zulu + distribution: adopt check-latest: true - name: Build with Maven From f9b8c08a426aa42776ceaaf0d5de5e9f1b727586 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Fri, 30 Jul 2021 13:57:03 +0200 Subject: [PATCH 034/261] Update dependencies --- pom.xml | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/pom.xml b/pom.xml index a76c62d43..fc309854d 100644 --- a/pom.xml +++ b/pom.xml @@ -46,14 +46,16 @@ UTF-8 1.8 - 5.7.0 - 1.7.30 + 5.7.2 + 1.7.32 dnsjava_dnsjava dnsjava https://sonarcloud.io 8 - ${project.build.directory}/site/jacoco/jacoco.xml + ${project.build.directory}/site/jacoco/jacoco.xml + ${project.build.directory}/generated-sources/delombok + 1.18.20 @@ -61,7 +63,7 @@ org.apache.maven.plugins maven-gpg-plugin - 1.6 + 3.0.1 sign-artifacts @@ -99,7 +101,7 @@ org.apache.felix maven-bundle-plugin - 5.1.1 + 5.1.2 true @@ -165,7 +167,7 @@ org.jacoco jacoco-maven-plugin - 0.8.6 + 0.8.7 prepare-agent @@ -257,7 +259,7 @@ com.puppycrawl.tools checkstyle - 8.40 + 8.44 @@ -276,7 +278,7 @@ ossrh https://oss.sonatype.org/ - false + true @@ -310,7 +312,7 @@ org.projectlombok lombok-maven-plugin - 1.18.18.0 + ${lombok.version}.0 ${project.build.sourceDirectory} false @@ -325,6 +327,17 @@ + + org.apache.maven.plugins + maven-resources-plugin + 3.2.0 + + + + org.apache.maven.plugins + maven-install-plugin + 2.5.2 + @@ -337,7 +350,7 @@ org.projectlombok lombok - 1.18.10 + ${lombok.version} provided From 9a317695ac52efeead68d4e3d6d9d3fd9c83c87e Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Fri, 30 Jul 2021 14:25:20 +0200 Subject: [PATCH 035/261] Fix usage of cache action and disable Java 17 due to Lombok --- .github/workflows/build.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 60c172c57..1d746b9ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,9 +13,10 @@ jobs: runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ ubuntu-18.04, ubuntu-20.04, windows-latest ] - java: [ '8', '11', '17-ea' ] + java: [ '8', '11' ] arch: [ 'x86', 'x64' ] exclude: - os: ubuntu-18.04 @@ -36,7 +37,9 @@ jobs: uses: actions/cache@v2 with: path: ~/.m2/repository - key: m2-cache-${{ matrix.java }}-${{ matrix.arch }}-${{ matrix.os }} + key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} + restore-keys: | + ${{ runner.os }}-maven- - name: Set up JDK ${{ matrix.java }} uses: actions/setup-java@v2 From a118e796158cdad222e3a80d73d94884b6f02cb2 Mon Sep 17 00:00:00 2001 From: Ingo Bauersachs Date: Sun, 1 Aug 2021 14:54:43 +0200 Subject: [PATCH 036/261] Fix a bunch of Javadoc errors --- pom.xml | 7 +++--- .../org/xbill/DNS/ClientSubnetOption.java | 2 +- src/main/java/org/xbill/DNS/Options.java | 24 ++++++++++++------- src/main/java/org/xbill/DNS/Rcode.java | 6 ++++- src/main/java/org/xbill/DNS/Serial.java | 2 +- src/main/java/org/xbill/DNS/TSIG.java | 6 ++++- .../DNS/lookup/LookupFailedException.java | 8 ++----- 7 files changed, 34 insertions(+), 21 deletions(-) diff --git a/pom.xml b/pom.xml index fc309854d..81e5e28d0 100644 --- a/pom.xml +++ b/pom.xml @@ -45,13 +45,14 @@ UTF-8 - 1.8 + 8 + 5.7.2 1.7.32 dnsjava_dnsjava dnsjava https://sonarcloud.io - 8 + ${target.jdk} ${project.build.directory}/site/jacoco/jacoco.xml ${project.build.directory}/generated-sources/delombok @@ -140,6 +141,7 @@ maven-javadoc-plugin 3.2.0 + ${target.jdk} true dnsjava documentation