Integrate MiniDNS and change static DNS server

This commit is contained in:
Arne 2024-11-09 20:34:43 +01:00
parent 028559ffc1
commit 7d3fb6c4b9
151 changed files with 16799 additions and 171 deletions

View file

@ -42,6 +42,8 @@ configurations {
}
dependencies {
implementation 'org.junit.jupiter:junit-jupiter:5.8.1'
implementation 'junit:junit:4.13.2'
androidTestImplementation 'tools.fastlane:screengrab:2.1.1'
androidTestImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.6.2'
@ -82,7 +84,6 @@ dependencies {
implementation 'org.bouncycastle:bcmail-jdk18on:1.78.1'
implementation 'com.google.zxing:core:3.5.3'
implementation 'org.minidns:minidns-hla:1.1.1'
implementation 'me.leolin:ShortcutBadger:1.1.22@aar'
implementation 'org.whispersystems:signal-protocol-java:2.6.2'
implementation "com.wefika:flowlayout:0.4.1"
@ -307,7 +308,7 @@ android {
}
packagingOptions {
resources {
excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF', 'META-INF/versions/9/OSGI-INF/MANIFEST.MF']
excludes += ['META-INF/BCKEY.DSA', 'META-INF/BCKEY.SF', 'META-INF/versions/9/OSGI-INF/MANIFEST.MF', 'META-INF/LICENSE.md', 'META-INF/LICENSE-notice.md']
}
}
lint {

View file

@ -1,169 +0,0 @@
package de.monocles.chat.test;
import java.util.concurrent.TimeoutException;
import java.lang.Thread;
import java.util.Arrays;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import androidx.test.InstrumentationRegistry;
import androidx.test.core.app.ActivityScenario;
import androidx.test.core.app.ApplicationProvider;
import androidx.test.ext.junit.rules.ActivityScenarioRule;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.rule.ServiceTestRule;
import static androidx.test.espresso.Espresso.onView;
import static androidx.test.espresso.action.ViewActions.click;
import static androidx.test.espresso.matcher.ViewMatchers.withId;
import tools.fastlane.screengrab.Screengrab;
import tools.fastlane.screengrab.cleanstatusbar.CleanStatusBar;
import tools.fastlane.screengrab.locale.LocaleTestRule;
import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.entities.Contact;
import eu.siacs.conversations.entities.Conversation;
import eu.siacs.conversations.entities.Message;
import eu.siacs.conversations.entities.Presence;
import eu.siacs.conversations.entities.ServiceDiscoveryResult;
import eu.siacs.conversations.entities.TransferablePlaceholder;
import eu.siacs.conversations.persistance.FileBackend;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.test.R;
import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.StartConversationActivity;
import eu.siacs.conversations.xml.Element;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.pep.Avatar;
import eu.siacs.conversations.xmpp.stanzas.IqPacket;
@RunWith(AndroidJUnit4.class)
public class ScreenshotTest {
static String pkg = InstrumentationRegistry.getInstrumentation().getContext().getPackageName();
static XmppConnectionService xmppConnectionService;
static Account account;
@ClassRule
public static final LocaleTestRule localeTestRule = new LocaleTestRule();
@ClassRule
public static final ServiceTestRule xmppServiceRule = new ServiceTestRule();
@BeforeClass
public static void setup() throws TimeoutException {
CleanStatusBar.enableWithDefaults();
Intent intent = new Intent(ApplicationProvider.getApplicationContext(), XmppConnectionService.class);
intent.setAction("ui");
xmppConnectionService = ((XmppConnectionBinder) xmppServiceRule.bindService(intent)).getService();
account = xmppConnectionService.findAccountByJid(Jid.of("carrot@chaosah.hereva"));
if (account == null) {
account = new Account(
Jid.of("carrot@chaosah.hereva"),
"orangeandfurry"
);
xmppConnectionService.createAccount(account);
}
Uri avatarUri = Uri.parse("android.resource://" + pkg + "/" + String.valueOf(R.drawable.carrot));
final Avatar avatar = xmppConnectionService.getFileBackend().getPepAvatar(avatarUri, 192, Bitmap.CompressFormat.WEBP);
xmppConnectionService.getFileBackend().save(avatar);
account.setAvatar(avatar.getFilename());
Contact cheogram = account.getRoster().getContact(Jid.of("cheogram.com"));
cheogram.setOption(Contact.Options.IN_ROSTER);
cheogram.setPhotoUri("android.resource://" + pkg + "/" + String.valueOf(R.drawable.cheogram));
Presence cheogramPresence = Presence.parse(null, null, "");
IqPacket discoPacket = new IqPacket(IqPacket.TYPE.RESULT);
Element query = discoPacket.addChild("query", "http://jabber.org/protocol/disco#info");
Element identity = query.addChild("identity");
identity.setAttribute("category", "gateway");
identity.setAttribute("type", "pstn");
cheogramPresence.setServiceDiscoveryResult(new ServiceDiscoveryResult(discoPacket));
cheogram.updatePresence("gw", cheogramPresence);
}
@AfterClass
public static void teardown() {
CleanStatusBar.disable();
}
@Test
public void testConversation() throws FileBackend.FileCopyException, InterruptedException {
Conversation conversation = xmppConnectionService.findOrCreateConversation(account, Jid.of("+15550737737@cheogram.com"), false, false);
conversation.getContact().setOption(Contact.Options.IN_ROSTER);
conversation.getContact().setSystemName("Pepper");
conversation.getContact().setPhotoUri("android.resource://" + pkg + "/" + String.valueOf(R.drawable.pepper));
Message voicemail = new Message(conversation, "", 0, Message.STATUS_RECEIVED);
voicemail.setOob("https://example.com/thing.mp3");
voicemail.setFileParams(new Message.FileParams("https://example.com/thing.mp3|5000|0|0|10000"));
voicemail.setType(Message.TYPE_FILE);
voicemail.setSubject("Voicemail Recording");
Message transcript = new Message(conversation, "Where are you?", 0, Message.STATUS_RECEIVED);
transcript.setSubject("Voicemail Transcription");
Message picture = new Message(conversation, "", 0, Message.STATUS_SEND_RECEIVED);
picture.setOob("https://example.com/thing.webp");
picture.setType(Message.TYPE_FILE);
xmppConnectionService.getFileBackend().copyFileToPrivateStorage(
picture,
Uri.parse("android.resource://" + pkg + "/" + String.valueOf(R.drawable.komona)),
"image/webp"
);
xmppConnectionService.getFileBackend().updateFileParams(picture);
conversation.addAll(0, Arrays.asList(
voicemail,
transcript,
new Message(conversation, "Meow", 0, Message.STATUS_SEND_RECEIVED),
picture,
new Message(conversation, "👍", 0, Message.STATUS_RECEIVED)
));
ActivityScenario scenario = ActivityScenario.launch(ConversationsActivity.class);
scenario.onActivity((Activity activity) -> {
((ConversationsActivity) activity).switchToConversation(conversation);
});
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
Thread.sleep(100); // ImageView not paited yet after waitForIdleSync
Screengrab.screenshot("conversation");
}
@Test
public void testStartConversation() throws InterruptedException {
ActivityScenario scenario = ActivityScenario.launch(StartConversationActivity.class);
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
Thread.sleep(100); // ImageView not paited yet after waitForIdleSync
Screengrab.screenshot("startConversation");
}
@Test
public void testAddContact() throws InterruptedException {
ActivityScenario scenario = ActivityScenario.launch(StartConversationActivity.class);
onView(withId(eu.siacs.conversations.R.id.speed_dial)).perform(click());
Screengrab.screenshot("startConversationOptions");
// Not actually online, so can't screenshot the gateway selector yet
/*onView(withId(eu.siacs.conversations.R.id.create_contact)).perform(click());
InstrumentationRegistry.getInstrumentation().waitForIdleSync();
Thread.sleep(10000); // ImageView not paited yet after waitForIdleSync
Screengrab.screenshot("addContact");*/
}
}

View file

@ -0,0 +1,6 @@
compileJava {
options.bootstrapClasspath = files(androidBootClasspath)
}
javadoc {
classpath += files(androidBootClasspath)
}

View file

@ -0,0 +1,10 @@
plugins {
id 'ru.vyarus.animalsniffer'
id 'org.minidns.common-conventions'
}
dependencies {
signature "net.sf.androidscents.signature:android-api-level-${minAndroidSdk}:4.4.2_r4@signature"
}
animalsniffer {
sourceSets = [sourceSets.main]
}

View file

@ -0,0 +1,12 @@
plugins {
id 'application'
}
application {
applicationDefaultJvmArgs = ["-enableassertions"]
}
run {
// Pass all system properties down to the "application" run
systemProperties System.getProperties()
}

View file

@ -0,0 +1,36 @@
ext {
javaVersion = JavaVersion.VERSION_11
javaMajor = javaVersion.getMajorVersion()
minAndroidSdk = 19
androidBootClasspath = getAndroidRuntimeJar(minAndroidSdk)
// Export the function by turning it into a closure.
// https://stackoverflow.com/a/23290820/194894
getAndroidRuntimeJar = this.&getAndroidRuntimeJar
}
repositories {
mavenLocal()
mavenCentral()
}
def getAndroidRuntimeJar(androidApiLevel) {
def androidHome = getAndroidHome()
def androidJar = new File("$androidHome/platforms/android-${androidApiLevel}/android.jar")
if (androidJar.isFile()) {
return androidJar
} else {
throw new Exception("Can't find android.jar for API level ${androidApiLevel}. Please install corresponding SDK platform package")
}
}
def getAndroidHome() {
def androidHomeEnv = System.getenv("ANDROID_HOME")
if (androidHomeEnv == null) {
throw new Exception("ANDROID_HOME environment variable is not set")
}
def androidHome = new File(androidHomeEnv)
if (!androidHome.isDirectory()) throw new Exception("Environment variable ANDROID_HOME is not pointing to a directory")
return androidHome
}

View file

@ -0,0 +1,302 @@
plugins {
id 'biz.aQute.bnd.builder'
id 'checkstyle'
id 'eclipse'
id 'idea'
id 'jacoco'
id 'java'
id 'java-library'
id 'java-test-fixtures'
id 'maven-publish'
id 'net.ltgt.errorprone'
id 'signing'
id 'jacoco-report-aggregation'
id 'test-report-aggregation'
id 'org.minidns.common-conventions'
id 'org.minidns.javadoc-conventions'
}
version readVersionFile()
ext {
isSnapshot = version.endsWith('-SNAPSHOT')
gitCommit = getGitCommit()
rootConfigDir = new File(rootDir, 'config')
sonatypeCredentialsAvailable = project.hasProperty('sonatypeUsername') && project.hasProperty('sonatypePassword')
isReleaseVersion = !isSnapshot
isContinuousIntegrationEnvironment = Boolean.parseBoolean(System.getenv('CI'))
signingRequired = !(isSnapshot || isContinuousIntegrationEnvironment)
sonatypeSnapshotUrl = 'https://oss.sonatype.org/content/repositories/snapshots'
sonatypeStagingUrl = 'https://oss.sonatype.org/service/local/staging/deploy/maven2'
builtDate = (new java.text.SimpleDateFormat("yyyy-MM-dd")).format(new Date())
junitVersion = '5.9.2'
if (project.hasProperty("useSonatype")) {
useSonatype = project.getProperty("useSonatype").toBoolean()
} else {
// Default to true
useSonatype = true
}
}
group = 'org.minidns'
java {
sourceCompatibility = javaVersion
targetCompatibility = sourceCompatibility
}
eclipse {
classpath {
downloadJavadoc = true
}
}
// Make all project's 'test' target depend on javadoc, so that
// javadoc is also linted.
test.dependsOn javadoc
tasks.withType(JavaCompile) {
// Some systems may not have set their platform default
// converter to 'utf8', but we use unicode in our source
// files. Therefore ensure that javac uses unicode
options.encoding = "utf8"
options.compilerArgs = [
'-Xlint:all',
// Set '-options' because a non-java7 javac will emit a
// warning if source/target is set to 1.7 and
// bootclasspath is *not* set.
'-Xlint:-options',
// TODO: Enable xlint serial
'-Xlint:-serial',
'-Werror',
]
options.release = Integer.valueOf(javaMajor)
}
jacoco {
toolVersion = "0.8.12"
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true
}
}
dependencies {
testImplementation "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
testFixturesApi "org.junit.jupiter:junit-jupiter-api:$junitVersion"
testImplementation "org.junit.jupiter:junit-jupiter-params:$junitVersion"
testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:$junitVersion"
// https://stackoverflow.com/a/77274251/194894
testRuntimeOnly "org.junit.platform:junit-platform-launcher:1.11.0"
errorprone 'com.google.errorprone:error_prone_core:2.32.0'
}
test {
useJUnitPlatform()
maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1
// Enable full stacktraces of failed tests. Especially handy
// for CI environments.
testLogging {
events "failed"
exceptionFormat "full"
}
}
jar {
manifest {
attributes(
'Implementation-Version': version,
'Implementation-GitRevision': gitCommit,
'Built-JDK': System.getProperty('java.version'),
'Built-Gradle': gradle.gradleVersion,
'Built-By': System.getProperty('user.name')
)
}
bundle {
bnd(
'-removeheaders': 'Tool, Bnd-*',
'-exportcontents': 'org.minidns.*',
'Import-Package': '!android,*'
)
}
}
checkstyle {
toolVersion = '10.18.2'
configProperties.checkstyleLicenseHeader = "header"
}
task sourcesJar(type: Jar, dependsOn: classes) {
archiveClassifier = 'sources'
from sourceSets.main.allSource
}
task javadocJar(type: Jar, dependsOn: javadoc) {
archiveClassifier = 'javadoc'
from javadoc.destinationDir
}
task testsJar(type: Jar) {
archiveClassifier = 'tests'
from sourceSets.test.output
}
configurations {
testRuntime
}
artifacts {
// Add a 'testRuntime' configuration including the tests so that
// it can be consumed by other projects. See
// http://stackoverflow.com/a/21946676/194894
testRuntime testsJar
}
publishing {
publications {
mavenJava(MavenPublication) {
from components.java
artifact sourcesJar
artifact javadocJar
artifact testsJar
pom {
name = 'MiniDNS'
packaging = 'jar'
inceptionYear = '2014'
url = 'https://github.com/minidns/minidns'
afterEvaluate {
description = project.description
}
scm {
url = 'https://github.com/minidns/minidns'
connection = 'scm:https://github.com/minidns/minidns'
developerConnection = 'scm:git://github.com/minidns/minidns.git'
}
licenses {
license {
name = 'The Apache Software License, Version 2.0'
url = 'http://www.apache.org/licenses/LICENSE-2.0.txt'
distribution = 'repo'
}
}
developers {
developer {
id = 'rtreffer'
name = 'Rene Treffer'
email = 'treffer@measite.de'
}
developer {
id = 'flow'
name = 'Florian Schmaus'
email = 'flow@geekplace.eu'
}
}
}
}
}
repositories {
if (sonatypeCredentialsAvailable && useSonatype) {
maven {
url isSnapshot ? sonatypeSnapshotUrl : sonatypeStagingUrl
credentials {
username = sonatypeUsername
password = sonatypePassword
}
}
}
// Use
// gradle publish -P customRepoUrl=https://www.igniterealtime.org/archiva/repository/maven -P customRepoUsername=bamboo -P customRepoPassword=hidden -P useSonatype=false
// to deploy to this repo.
if (project.hasProperty("customRepoUrl")) {
maven {
name 'customRepo'
url customRepoUrl
if (project.hasProperty("customRepoUsername")) {
credentials {
username customRepoUsername
password customRepoPassword
}
}
}
}
}
}
// Workaround for gpg signatory not supporting the 'required' option
// See https://github.com/gradle/gradle/issues/5064#issuecomment-381924984
// Note what we use 'signing.gnupg.keyName' instead of 'signing.keyId'.
tasks.withType(Sign) {
onlyIf {
project.hasProperty('signing.gnupg.keyName')
}
}
signing {
required { signingRequired }
useGpgCmd()
sign publishing.publications.mavenJava
}
tasks.withType(JavaCompile) {
options.errorprone {
disableWarningsInGeneratedCode = true
excludedPaths = ".*/jmh_generated/.*"
error(
"UnusedVariable",
"UnusedMethod",
"MethodCanBeStatic",
)
errorproneArgs = [
// Disable MissingCasesInEnumSwitch error prone check
// because this check is already done by javac as incomplete-switch.
'-Xep:MissingCasesInEnumSwitch:OFF',
'-Xep:StringSplitter:OFF',
'-Xep:JavaTimeDefaultTimeZone:OFF',
'-Xep:InlineMeSuggester:OFF',
]
}
}
// Work around https://github.com/gradle/gradle/issues/4046
task copyJavadocDocFiles(type: Copy) {
from('src/javadoc')
into 'build/docs/javadoc'
include '**/doc-files/*.*'
}
javadoc.dependsOn copyJavadocDocFiles
def getGitCommit() {
def projectDirFile = new File("$projectDir")
def cmd = 'git describe --always --tags --dirty=+'
def proc = cmd.execute(null, projectDirFile)
def exitStatus = proc.waitFor()
if (exitStatus != 0) return "non-git build"
def gitCommit = proc.text.trim()
assert !gitCommit.isEmpty()
gitCommit
}
def readVersionFile() {
def versionFile = new File(rootDir, 'version')
if (!versionFile.isFile()) {
throw new Exception("Could not find version file")
}
if (versionFile.text.isEmpty()) {
throw new Exception("Version file does not contain a version")
}
versionFile.text.trim()
}

View file

@ -0,0 +1,24 @@
plugins {
// Javadoc linking requires repositories to bet configured. And
// those are declared in common-conventions, hence we add it here.
id 'org.minidns.common-conventions'
}
tasks.withType(Javadoc) {
// The '-quiet' as second argument is actually a hack,
// since the one parameter addStringOption doesn't seem to
// work, we extra add '-quiet', which is added anyway by
// gradle.
// We disable 'missing' as we do most of javadoc checking via checkstyle.
options.addStringOption('Xdoclint:all,-missing', '-quiet')
// Abort on javadoc warnings.
// See JDK-8200363 (https://bugs.openjdk.java.net/browse/JDK-8200363)
// for information about the -Xwerror option.
options.addStringOption('Xwerror', '-quiet')
options.addStringOption('-release', javaMajor)
}
tasks.withType(Javadoc) {
options.charSet = "UTF-8"
}

View file

@ -0,0 +1,477 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns;
import org.minidns.MiniDnsFuture.InternalMiniDnsFuture;
import org.minidns.cache.LruCache;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.record.A;
import org.minidns.record.AAAA;
import org.minidns.record.Data;
import org.minidns.record.NS;
import org.minidns.record.Record;
import org.minidns.record.Record.CLASS;
import org.minidns.record.Record.TYPE;
import org.minidns.source.DnsDataSource;
import org.minidns.source.NetworkDataSource;
import java.io.IOException;
import java.net.InetAddress;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.Collections;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support.
* This circumvents the missing javax.naming package on android.
*/
public abstract class AbstractDnsClient {
protected static final LruCache DEFAULT_CACHE = new LruCache();
protected static final Logger LOGGER = Logger.getLogger(AbstractDnsClient.class.getName());
/**
* This callback is used by the synchronous query() method <b>and</b> by the asynchronous queryAync() method in order to update the
* cache. In the asynchronous case, hand this callback into the async call, so that it can get called once the result is available.
*/
private final DnsDataSource.OnResponseCallback onResponseCallback = new DnsDataSource.OnResponseCallback() {
@Override
public void onResponse(DnsMessage requestMessage, DnsQueryResult responseMessage) {
final Question q = requestMessage.getQuestion();
if (cache != null && isResponseCacheable(q, responseMessage)) {
cache.put(requestMessage.asNormalizedVersion(), responseMessage);
}
}
};
/**
* The internal random class for sequence generation.
*/
protected final Random random;
protected final Random insecureRandom = new Random();
/**
* The internal DNS cache.
*/
protected final DnsCache cache;
protected DnsDataSource dataSource = new NetworkDataSource();
public enum IpVersionSetting {
v4only(true, false),
v6only(false, true),
v4v6(true, true),
v6v4(true, true),
;
public final boolean v4;
public final boolean v6;
IpVersionSetting(boolean v4, boolean v6) {
this.v4 = v4;
this.v6 = v6;
}
}
protected static IpVersionSetting DEFAULT_IP_VERSION_SETTING = IpVersionSetting.v4v6;
public static void setDefaultIpVersion(IpVersionSetting preferedIpVersion) {
if (preferedIpVersion == null) {
throw new IllegalArgumentException();
}
AbstractDnsClient.DEFAULT_IP_VERSION_SETTING = preferedIpVersion;
}
protected IpVersionSetting ipVersionSetting = DEFAULT_IP_VERSION_SETTING;
public void setPreferedIpVersion(IpVersionSetting preferedIpVersion) {
if (preferedIpVersion == null) {
throw new IllegalArgumentException();
}
ipVersionSetting = preferedIpVersion;
}
public IpVersionSetting getPreferedIpVersion() {
return ipVersionSetting;
}
/**
* Create a new DNS client with the given DNS cache.
*
* @param cache The backend DNS cache.
*/
protected AbstractDnsClient(DnsCache cache) {
Random random;
try {
random = SecureRandom.getInstance("SHA1PRNG");
} catch (NoSuchAlgorithmException e1) {
random = new SecureRandom();
}
this.random = random;
this.cache = cache;
}
/**
* Create a new DNS client using the global default cache.
*/
protected AbstractDnsClient() {
this(DEFAULT_CACHE);
}
/**
* Query the system nameservers for a single entry of any class.
*
* This can be used to determine the name server version, if name
* is version.bind, type is TYPE.TXT and clazz is CLASS.CH.
*
* @param name The DNS name to request.
* @param type The DNS type to request (SRV, A, AAAA, ...).
* @param clazz The class of the request (usually IN for Internet).
* @return The response (or null on timeout/error).
* @throws IOException if an IO error occurs.
*/
public final DnsQueryResult query(String name, TYPE type, CLASS clazz) throws IOException {
Question q = new Question(name, type, clazz);
return query(q);
}
/**
* Query the system nameservers for a single entry of the class IN
* (which is used for MX, SRV, A, AAAA and most other RRs).
*
* @param name The DNS name to request.
* @param type The DNS type to request (SRV, A, AAAA, ...).
* @return The response (or null on timeout/error).
* @throws IOException if an IO error occurs.
*/
public final DnsQueryResult query(DnsName name, TYPE type) throws IOException {
Question q = new Question(name, type, CLASS.IN);
return query(q);
}
/**
* Query the system nameservers for a single entry of the class IN
* (which is used for MX, SRV, A, AAAA and most other RRs).
*
* @param name The DNS name to request.
* @param type The DNS type to request (SRV, A, AAAA, ...).
* @return The response (or null on timeout/error).
* @throws IOException if an IO error occurs.
*/
public final DnsQueryResult query(CharSequence name, TYPE type) throws IOException {
Question q = new Question(name, type, CLASS.IN);
return query(q);
}
public DnsQueryResult query(Question q) throws IOException {
DnsMessage.Builder query = buildMessage(q);
return query(query);
}
/**
* Send a query request to the DNS system.
*
* @param query The query to send to the server.
* @return The response (or null).
* @throws IOException if an IO error occurs.
*/
protected abstract DnsQueryResult query(DnsMessage.Builder query) throws IOException;
public final MiniDnsFuture<DnsQueryResult, IOException> queryAsync(CharSequence name, TYPE type) {
Question q = new Question(name, type, CLASS.IN);
return queryAsync(q);
}
public final MiniDnsFuture<DnsQueryResult, IOException> queryAsync(Question q) {
DnsMessage.Builder query = buildMessage(q);
return queryAsync(query);
}
/**
* Default implementation of an asynchronous DNS query which just wraps the synchronous case.
* <p>
* Subclasses override this method to support true asynchronous queries.
* </p>
*
* @param query the query.
* @return a future for this query.
*/
protected MiniDnsFuture<DnsQueryResult, IOException> queryAsync(DnsMessage.Builder query) {
InternalMiniDnsFuture<DnsQueryResult, IOException> future = new InternalMiniDnsFuture<>();
DnsQueryResult result;
try {
result = query(query);
} catch (IOException e) {
future.setException(e);
return future;
}
future.setResult(result);
return future;
}
public final DnsQueryResult query(Question q, InetAddress server, int port) throws IOException {
DnsMessage query = getQueryFor(q);
return query(query, server, port);
}
public final DnsQueryResult query(DnsMessage requestMessage, InetAddress address, int port) throws IOException {
// See if we have the answer to this question already cached
DnsQueryResult responseMessage = (cache == null) ? null : cache.get(requestMessage);
if (responseMessage != null) {
return responseMessage;
}
final Question q = requestMessage.getQuestion();
final Level TRACE_LOG_LEVEL = Level.FINE;
LOGGER.log(TRACE_LOG_LEVEL, "Asking {0} on {1} for {2} with:\n{3}", new Object[] { address, port, q, requestMessage });
try {
responseMessage = dataSource.query(requestMessage, address, port);
} catch (IOException e) {
LOGGER.log(TRACE_LOG_LEVEL, "IOException {0} on {1} while resolving {2}: {3}", new Object[] { address, port, q, e});
throw e;
}
LOGGER.log(TRACE_LOG_LEVEL, "Response from {0} on {1} for {2}:\n{3}", new Object[] { address, port, q, responseMessage });
onResponseCallback.onResponse(requestMessage, responseMessage);
return responseMessage;
}
public final MiniDnsFuture<DnsQueryResult, IOException> queryAsync(DnsMessage requestMessage, InetAddress address, int port) {
// See if we have the answer to this question already cached
DnsQueryResult responseMessage = (cache == null) ? null : cache.get(requestMessage);
if (responseMessage != null) {
return MiniDnsFuture.from(responseMessage);
}
final Question q = requestMessage.getQuestion();
final Level TRACE_LOG_LEVEL = Level.FINE;
LOGGER.log(TRACE_LOG_LEVEL, "Asynchronusly asking {0} on {1} for {2} with:\n{3}", new Object[] { address, port, q, requestMessage });
return dataSource.queryAsync(requestMessage, address, port, onResponseCallback);
}
/**
* Whether a response from the DNS system should be cached or not.
*
* @param q The question the response message should answer.
* @param result The DNS query result.
* @return True, if the response should be cached, false otherwise.
*/
protected boolean isResponseCacheable(Question q, DnsQueryResult result) {
DnsMessage dnsMessage = result.response;
for (Record<? extends Data> record : dnsMessage.answerSection) {
if (record.isAnswer(q)) {
return true;
}
}
return false;
}
/**
* Builds a {@link DnsMessage} object carrying the given Question.
*
* @param question {@link Question} to be put in the DNS request.
* @return A {@link DnsMessage} requesting the answer for the given Question.
*/
final DnsMessage.Builder buildMessage(Question question) {
DnsMessage.Builder message = DnsMessage.builder();
message.setQuestion(question);
message.setId(random.nextInt());
message = newQuestion(message);
return message;
}
protected abstract DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage);
/**
* Query a nameserver for a single entry.
*
* @param name The DNS name to request.
* @param type The DNS type to request (SRV, A, AAAA, ...).
* @param clazz The class of the request (usually IN for Internet).
* @param address The DNS server address.
* @param port The DNS server port.
* @return The response (or null on timeout / failure).
* @throws IOException On IO Errors.
*/
public DnsQueryResult query(String name, TYPE type, CLASS clazz, InetAddress address, int port)
throws IOException {
Question q = new Question(name, type, clazz);
return query(q, address, port);
}
/**
* Query a nameserver for a single entry.
*
* @param name The DNS name to request.
* @param type The DNS type to request (SRV, A, AAAA, ...).
* @param clazz The class of the request (usually IN for Internet).
* @param address The DNS server host.
* @return The response (or null on timeout / failure).
* @throws IOException On IO Errors.
*/
public DnsQueryResult query(String name, TYPE type, CLASS clazz, InetAddress address)
throws IOException {
Question q = new Question(name, type, clazz);
return query(q, address);
}
/**
* Query a nameserver for a single entry of class IN.
*
* @param name The DNS name to request.
* @param type The DNS type to request (SRV, A, AAAA, ...).
* @param address The DNS server host.
* @return The response (or null on timeout / failure).
* @throws IOException On IO Errors.
*/
public DnsQueryResult query(String name, TYPE type, InetAddress address)
throws IOException {
Question q = new Question(name, type, CLASS.IN);
return query(q, address);
}
public final DnsQueryResult query(DnsMessage query, InetAddress host) throws IOException {
return query(query, host, 53);
}
/**
* Query a specific server for one entry.
*
* @param q The question section of the DNS query.
* @param address The dns server address.
* @return The a DNS query result.
* @throws IOException On IOErrors.
*/
public DnsQueryResult query(Question q, InetAddress address) throws IOException {
return query(q, address, 53);
}
public final MiniDnsFuture<DnsQueryResult, IOException> queryAsync(DnsMessage query, InetAddress dnsServer) {
return queryAsync(query, dnsServer, 53);
}
/**
* Returns the currently used {@link DnsDataSource}. See {@link #setDataSource(DnsDataSource)} for details.
*
* @return The currently used {@link DnsDataSource}
*/
public DnsDataSource getDataSource() {
return dataSource;
}
/**
* Set a {@link DnsDataSource} to be used by the DnsClient.
* The default implementation will direct all queries directly to the Internet.
*
* This can be used to define a non-default handling for outgoing data. This can be useful to redirect the requests
* to a proxy or to modify requests after or responses before they are handled by the DnsClient implementation.
*
* @param dataSource An implementation of DNSDataSource that shall be used.
*/
public void setDataSource(DnsDataSource dataSource) {
if (dataSource == null) {
throw new IllegalArgumentException();
}
this.dataSource = dataSource;
}
/**
* Get the cache used by this DNS client.
*
* @return the cached used by this DNS client or <code>null</code>.
*/
public DnsCache getCache() {
return cache;
}
protected DnsMessage getQueryFor(Question q) {
DnsMessage.Builder messageBuilder = buildMessage(q);
DnsMessage query = messageBuilder.build();
return query;
}
private <D extends Data> Set<D> getCachedRecordsFor(DnsName dnsName, TYPE type) {
if (cache == null)
return Collections.emptySet();
Question dnsNameNs = new Question(dnsName, type);
DnsMessage queryDnsNameNs = getQueryFor(dnsNameNs);
DnsQueryResult cachedResult = cache.get(queryDnsNameNs);
if (cachedResult == null)
return Collections.emptySet();
return cachedResult.response.getAnswersFor(dnsNameNs);
}
public Set<NS> getCachedNameserverRecordsFor(DnsName dnsName) {
return getCachedRecordsFor(dnsName, TYPE.NS);
}
public Set<A> getCachedIPv4AddressesFor(DnsName dnsName) {
return getCachedRecordsFor(dnsName, TYPE.A);
}
public Set<AAAA> getCachedIPv6AddressesFor(DnsName dnsName) {
return getCachedRecordsFor(dnsName, TYPE.AAAA);
}
@SuppressWarnings("unchecked")
private <D extends Data> Set<D> getCachedIPNameserverAddressesFor(DnsName dnsName, TYPE type) {
Set<NS> nsSet = getCachedNameserverRecordsFor(dnsName);
if (nsSet.isEmpty())
return Collections.emptySet();
Set<D> res = new HashSet<>(3 * nsSet.size());
for (NS ns : nsSet) {
Set<D> addresses;
switch (type) {
case A:
addresses = (Set<D>) getCachedIPv4AddressesFor(ns.target);
break;
case AAAA:
addresses = (Set<D>) getCachedIPv6AddressesFor(ns.target);
break;
default:
throw new AssertionError();
}
res.addAll(addresses);
}
return res;
}
public Set<A> getCachedIPv4NameserverAddressesFor(DnsName dnsName) {
return getCachedIPNameserverAddressesFor(dnsName, TYPE.A);
}
public Set<AAAA> getCachedIPv6NameserverAddressesFor(DnsName dnsName) {
return getCachedIPNameserverAddressesFor(dnsName, TYPE.AAAA);
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsqueryresult.CachedDnsQueryResult;
import org.minidns.dnsqueryresult.DnsQueryResult;
/**
* Cache for DNS Entries. Implementations must be thread safe.
*/
public abstract class DnsCache {
public static final int DEFAULT_CACHE_SIZE = 512;
/**
* Add an an dns answer/response for a given dns question. Implementations
* should honor the ttl / receive timestamp.
* @param query The query message containing a question.
* @param result The DNS query result.
*/
public final void put(DnsMessage query, DnsQueryResult result) {
putNormalized(query.asNormalizedVersion(), result);
}
protected abstract void putNormalized(DnsMessage normalizedQuery, DnsQueryResult result);
public abstract void offer(DnsMessage query, DnsQueryResult result, DnsName authoritativeZone);
/**
* Request a cached dns response.
* @param query The query message containing a question.
* @return The dns message.
*/
public final CachedDnsQueryResult get(DnsMessage query) {
return getNormalized(query.asNormalizedVersion());
}
protected abstract CachedDnsQueryResult getNormalized(DnsMessage normalizedQuery);
}

View file

@ -0,0 +1,492 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns;
import org.minidns.MiniDnsException.ErrorResponseException;
import org.minidns.MiniDnsException.NoQueryPossibleException;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.dnsserverlookup.AndroidUsingExec;
import org.minidns.dnsserverlookup.AndroidUsingReflection;
import org.minidns.dnsserverlookup.DnsServerLookupMechanism;
import org.minidns.dnsserverlookup.UnixUsingEtcResolvConf;
import org.minidns.record.Record.TYPE;
import org.minidns.util.CollectionsUtil;
import org.minidns.util.InetAddressUtil;
import org.minidns.util.MultipleIoException;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.logging.Level;
/**
* A minimal DNS client for SRV/A/AAAA/NS and CNAME lookups, with IDN support.
* This circumvents the missing javax.naming package on android.
*/
public class DnsClient extends AbstractDnsClient {
static final List<DnsServerLookupMechanism> LOOKUP_MECHANISMS = new CopyOnWriteArrayList<>();
static final Set<Inet4Address> STATIC_IPV4_DNS_SERVERS = new CopyOnWriteArraySet<>();
static final Set<Inet6Address> STATIC_IPV6_DNS_SERVERS = new CopyOnWriteArraySet<>();
static {
addDnsServerLookupMechanism(AndroidUsingExec.INSTANCE);
addDnsServerLookupMechanism(AndroidUsingReflection.INSTANCE);
addDnsServerLookupMechanism(UnixUsingEtcResolvConf.INSTANCE);
try {
Inet4Address dnsforgeV4Dns = InetAddressUtil.ipv4From("176.9.93.198");
STATIC_IPV4_DNS_SERVERS.add(dnsforgeV4Dns);
} catch (IllegalArgumentException e) {
LOGGER.log(Level.WARNING, "Could not add static IPv4 DNS Server", e);
}
try {
Inet6Address dnsforgeV6Dns = InetAddressUtil.ipv6From("[2a01:4f8:151:34aa::198]");
STATIC_IPV6_DNS_SERVERS.add(dnsforgeV6Dns);
} catch (IllegalArgumentException e) {
LOGGER.log(Level.WARNING, "Could not add static IPv6 DNS Server", e);
}
}
private static final Set<String> blacklistedDnsServers = Collections.newSetFromMap(new ConcurrentHashMap<String, Boolean>(4));
private final Set<InetAddress> nonRaServers = Collections.newSetFromMap(new ConcurrentHashMap<InetAddress, Boolean>(4));
private boolean askForDnssec = false;
private boolean disableResultFilter = false;
private boolean useHardcodedDnsServers = true;
/**
* Create a new DNS client using the global default cache.
*/
public DnsClient() {
super();
}
public DnsClient(DnsCache dnsCache) {
super(dnsCache);
}
@Override
protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) {
message.setRecursionDesired(true);
message.getEdnsBuilder().setUdpPayloadSize(dataSource.getUdpPayloadSize()).setDnssecOk(askForDnssec);
return message;
}
private List<InetAddress> getServerAddresses() {
List<InetAddress> dnsServerAddresses = findDnsAddresses();
if (useHardcodedDnsServers) {
InetAddress primaryHardcodedDnsServer, secondaryHardcodedDnsServer = null;
switch (ipVersionSetting) {
case v4v6:
primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer();
secondaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer();
break;
case v6v4:
primaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer();
secondaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer();
break;
case v4only:
primaryHardcodedDnsServer = getRandomHardcodedIpv4DnsServer();
break;
case v6only:
primaryHardcodedDnsServer = getRandomHarcodedIpv6DnsServer();
break;
default:
throw new AssertionError("Unknown ipVersionSetting: " + ipVersionSetting);
}
dnsServerAddresses.add(primaryHardcodedDnsServer);
if (secondaryHardcodedDnsServer != null) {
dnsServerAddresses.add(secondaryHardcodedDnsServer);
}
}
return dnsServerAddresses;
}
@Override
public DnsQueryResult query(DnsMessage.Builder queryBuilder) throws IOException {
DnsMessage q = newQuestion(queryBuilder).build();
// While this query method does in fact re-use query(Question, String)
// we still do a cache lookup here in order to avoid unnecessary
// findDNS()calls, which are expensive on Android. Note that we do not
// put the results back into the Cache, as this is already done by
// query(Question, String).
DnsQueryResult dnsQueryResult = (cache == null) ? null : cache.get(q);
if (dnsQueryResult != null) {
return dnsQueryResult;
}
List<InetAddress> dnsServerAddresses = getServerAddresses();
List<IOException> ioExceptions = new ArrayList<>(dnsServerAddresses.size());
for (InetAddress dns : dnsServerAddresses) {
if (nonRaServers.contains(dns)) {
LOGGER.finer("Skipping " + dns + " because it was marked as \"recursion not available\"");
continue;
}
try {
dnsQueryResult = query(q, dns);
} catch (IOException ioe) {
ioExceptions.add(ioe);
continue;
}
DnsMessage responseMessage = dnsQueryResult.response;
if (!responseMessage.recursionAvailable) {
boolean newRaServer = nonRaServers.add(dns);
if (newRaServer) {
LOGGER.warning("The DNS server " + dns
+ " returned a response without the \"recursion available\" (RA) flag set. This likely indicates a misconfiguration because the server is not suitable for DNS resolution");
}
continue;
}
if (disableResultFilter) {
return dnsQueryResult;
}
switch (responseMessage.responseCode) {
case NO_ERROR:
case NX_DOMAIN:
break;
default:
String warning = "Response from " + dns + " asked for " + q.getQuestion() + " with error code: "
+ responseMessage.responseCode + '.';
if (!LOGGER.isLoggable(Level.FINE)) {
// Only append the responseMessage is log level is not fine. If it is fine or higher, the
// response has already been logged.
warning += "\n" + responseMessage;
}
LOGGER.warning(warning);
ErrorResponseException exception = new ErrorResponseException(q, dnsQueryResult);
ioExceptions.add(exception);
continue;
}
return dnsQueryResult;
}
MultipleIoException.throwIfRequired(ioExceptions);
// TODO: Shall we add the attempted DNS servers to the exception?
throw new NoQueryPossibleException(q);
}
@Override
protected MiniDnsFuture<DnsQueryResult, IOException> queryAsync(DnsMessage.Builder queryBuilder) {
DnsMessage q = newQuestion(queryBuilder).build();
// While this query method does in fact re-use query(Question, String)
// we still do a cache lookup here in order to avoid unnecessary
// findDNS()calls, which are expensive on Android. Note that we do not
// put the results back into the Cache, as this is already done by
// query(Question, String).
DnsQueryResult responseMessage = (cache == null) ? null : cache.get(q);
if (responseMessage != null) {
return MiniDnsFuture.from(responseMessage);
}
final List<InetAddress> dnsServerAddresses = getServerAddresses();
// Filter loop.
Iterator<InetAddress> it = dnsServerAddresses.iterator();
while (it.hasNext()) {
InetAddress dns = it.next();
if (nonRaServers.contains(dns)) {
it.remove();
LOGGER.finer("Skipping " + dns + " because it was marked as \"recursion not available\"");
continue;
}
}
List<MiniDnsFuture<DnsQueryResult, IOException>> futures = new ArrayList<>(dnsServerAddresses.size());
// "Main" loop.
for (InetAddress dns : dnsServerAddresses) {
MiniDnsFuture<DnsQueryResult, IOException> f = queryAsync(q, dns);
futures.add(f);
}
return MiniDnsFuture.anySuccessfulOf(futures);
}
/**
* Retrieve a list of currently configured DNS servers IP addresses. This method does verify that only IP addresses are returned and
* nothing else (e.g. DNS names).
* <p>
* The addresses are discovered by using one (or more) of the configured {@link DnsServerLookupMechanism}s.
* </p>
*
* @return A list of DNS server IP addresses configured for this system.
*/
public static List<String> findDNS() {
List<String> res = null;
final Level TRACE_LOG_LEVEL = Level.FINE;
for (DnsServerLookupMechanism mechanism : LOOKUP_MECHANISMS) {
try {
res = mechanism.getDnsServerAddresses();
} catch (SecurityException exception) {
LOGGER.log(Level.WARNING, "Could not lookup DNS server", exception);
}
if (res == null) {
LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism '" + mechanism.getName() + "' did not return any DNS server");
continue;
}
if (LOGGER.isLoggable(TRACE_LOG_LEVEL)) {
// TODO: Use String.join() once MiniDNS is Android API 26 (or higher).
StringBuilder sb = new StringBuilder();
for (Iterator<String> it = res.iterator(); it.hasNext();) {
String s = it.next();
sb.append(s);
if (it.hasNext()) {
sb.append(", ");
}
}
String dnsServers = sb.toString();
LOGGER.log(TRACE_LOG_LEVEL, "DnsServerLookupMechanism {0} returned the following DNS servers: {1}",
new Object[] { mechanism.getName(), dnsServers });
}
assert !res.isEmpty();
// We could cache if res only contains IP addresses and avoid the verification in case. Not sure if its really that beneficial
// though, because the list returned by the server mechanism is rather short.
// Verify the returned DNS servers: Ensure that only valid IP addresses are returned. We want to avoid that something else,
// especially a valid DNS name is returned, as this would cause the following String to InetAddress conversation using
// getByName(String) to cause a DNS lookup, which would be performed outside of the realm of MiniDNS and therefore also outside
// of its DNSSEC guarantees.
Iterator<String> it = res.iterator();
while (it.hasNext()) {
String potentialDnsServer = it.next();
if (!InetAddressUtil.isIpAddress(potentialDnsServer)) {
LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName()
+ "' returned an invalid non-IP address result: '" + potentialDnsServer + "'");
it.remove();
} else if (blacklistedDnsServers.contains(potentialDnsServer)) {
LOGGER.fine("The DNS server lookup mechanism '" + mechanism.getName()
+ "' returned a blacklisted result: '" + potentialDnsServer + "'");
it.remove();
}
}
if (!res.isEmpty()) {
break;
}
LOGGER.warning("The DNS server lookup mechanism '" + mechanism.getName()
+ "' returned not a single valid IP address after sanitazion");
res = null;
}
return res;
}
/**
* Retrieve a list of currently configured DNS server addresses.
* <p>
* Note that unlike {@link #findDNS()}, the list returned by this method
* will take the IP version setting into account, and order the list by the
* preferred address types (IPv4/v6). The returned list is modifiable.
* </p>
*
* @return A list of DNS server addresses.
* @see #findDNS()
*/
public static List<InetAddress> findDnsAddresses() {
// The findDNS() method contract guarantees that only IP addresses will be returned.
List<String> res = findDNS();
if (res == null) {
return new ArrayList<>();
}
final IpVersionSetting setting = DEFAULT_IP_VERSION_SETTING;
List<Inet4Address> ipv4DnsServer = null;
List<Inet6Address> ipv6DnsServer = null;
if (setting.v4) {
ipv4DnsServer = new ArrayList<>(res.size());
}
if (setting.v6) {
ipv6DnsServer = new ArrayList<>(res.size());
}
int validServerAddresses = 0;
for (String dnsServerString : res) {
// The following invariant must hold: "dnsServerString is a IP address". Therefore findDNS() must only return a List of Strings
// representing IP addresses. Otherwise the following call of getByName(String) may perform a DNS lookup without MiniDNS being
// involved. Something we want to avoid.
assert InetAddressUtil.isIpAddress(dnsServerString);
InetAddress dnsServerAddress;
try {
dnsServerAddress = InetAddress.getByName(dnsServerString);
} catch (UnknownHostException e) {
LOGGER.log(Level.SEVERE, "Could not transform '" + dnsServerString + "' to InetAddress", e);
continue;
}
if (dnsServerAddress instanceof Inet4Address) {
if (!setting.v4) {
continue;
}
Inet4Address ipv4DnsServerAddress = (Inet4Address) dnsServerAddress;
ipv4DnsServer.add(ipv4DnsServerAddress);
} else if (dnsServerAddress instanceof Inet6Address) {
if (!setting.v6) {
continue;
}
Inet6Address ipv6DnsServerAddress = (Inet6Address) dnsServerAddress;
ipv6DnsServer.add(ipv6DnsServerAddress);
} else {
throw new AssertionError("The address '" + dnsServerAddress + "' is neither of type Inet(4|6)Address");
}
validServerAddresses++;
}
List<InetAddress> dnsServers = new ArrayList<>(validServerAddresses);
switch (setting) {
case v4v6:
dnsServers.addAll(ipv4DnsServer);
dnsServers.addAll(ipv6DnsServer);
break;
case v6v4:
dnsServers.addAll(ipv6DnsServer);
dnsServers.addAll(ipv4DnsServer);
break;
case v4only:
dnsServers.addAll(ipv4DnsServer);
break;
case v6only:
dnsServers.addAll(ipv6DnsServer);
break;
}
return dnsServers;
}
public static void addDnsServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) {
if (!dnsServerLookup.isAvailable()) {
LOGGER.fine("Not adding " + dnsServerLookup.getName() + " as it is not available.");
return;
}
synchronized (LOOKUP_MECHANISMS) {
// We can't use Collections.sort(CopyOnWriteArrayList) with Java 7. So we first create a temp array, sort it, and replace
// LOOKUP_MECHANISMS with the result. For more information about the Java 7 Collections.sort(CopyOnWriteArarayList) issue see
// http://stackoverflow.com/a/34827492/194894
// TODO: Remove that workaround once MiniDNS is Java 8 only.
ArrayList<DnsServerLookupMechanism> tempList = new ArrayList<>(LOOKUP_MECHANISMS.size() + 1);
tempList.addAll(LOOKUP_MECHANISMS);
tempList.add(dnsServerLookup);
// Sadly, this Collections.sort() does not with the CopyOnWriteArrayList on Java 7.
Collections.sort(tempList);
LOOKUP_MECHANISMS.clear();
LOOKUP_MECHANISMS.addAll(tempList);
}
}
public static boolean removeDNSServerLookupMechanism(DnsServerLookupMechanism dnsServerLookup) {
synchronized (LOOKUP_MECHANISMS) {
return LOOKUP_MECHANISMS.remove(dnsServerLookup);
}
}
public static boolean addBlacklistedDnsServer(String dnsServer) {
return blacklistedDnsServers.add(dnsServer);
}
public static boolean removeBlacklistedDnsServer(String dnsServer) {
return blacklistedDnsServers.remove(dnsServer);
}
public boolean isAskForDnssec() {
return askForDnssec;
}
public void setAskForDnssec(boolean askForDnssec) {
this.askForDnssec = askForDnssec;
}
public boolean isDisableResultFilter() {
return disableResultFilter;
}
public void setDisableResultFilter(boolean disableResultFilter) {
this.disableResultFilter = disableResultFilter;
}
public boolean isUseHardcodedDnsServersEnabled() {
return useHardcodedDnsServers;
}
public void setUseHardcodedDnsServers(boolean useHardcodedDnsServers) {
this.useHardcodedDnsServers = useHardcodedDnsServers;
}
public InetAddress getRandomHardcodedIpv4DnsServer() {
return CollectionsUtil.getRandomFrom(STATIC_IPV4_DNS_SERVERS, insecureRandom);
}
public InetAddress getRandomHarcodedIpv6DnsServer() {
return CollectionsUtil.getRandomFrom(STATIC_IPV6_DNS_SERVERS, insecureRandom);
}
private static Question getReverseIpLookupQuestionFor(DnsName dnsName) {
return new Question(dnsName, TYPE.PTR);
}
public static Question getReverseIpLookupQuestionFor(Inet4Address inet4Address) {
DnsName reversedIpAddress = InetAddressUtil.reverseIpAddressOf(inet4Address);
DnsName dnsName = DnsName.from(reversedIpAddress, DnsName.IN_ADDR_ARPA);
return getReverseIpLookupQuestionFor(dnsName);
}
public static Question getReverseIpLookupQuestionFor(Inet6Address inet6Address) {
DnsName reversedIpAddress = InetAddressUtil.reverseIpAddressOf(inet6Address);
DnsName dnsName = DnsName.from(reversedIpAddress, DnsName.IP6_ARPA);
return getReverseIpLookupQuestionFor(dnsName);
}
public static Question getReverseIpLookupQuestionFor(InetAddress inetAddress) {
if (inetAddress instanceof Inet4Address) {
return getReverseIpLookupQuestionFor((Inet4Address) inetAddress);
} else if (inetAddress instanceof Inet6Address) {
return getReverseIpLookupQuestionFor((Inet6Address) inetAddress);
} else {
throw new IllegalArgumentException("The provided inetAddress '" + inetAddress
+ "' is neither of type Inet4Address nor Inet6Address");
}
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns;
public class MiniDnsConfiguration {
public static String getVersion() {
return MiniDnsInitialization.VERSION;
}
}

View file

@ -0,0 +1,120 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns;
import java.io.IOException;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsqueryresult.DnsQueryResult;
public abstract class MiniDnsException extends IOException {
/**
*
*/
private static final long serialVersionUID = 1L;
protected MiniDnsException(String message) {
super(message);
}
public static class IdMismatch extends MiniDnsException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final DnsMessage request;
private final DnsMessage response;
public IdMismatch(DnsMessage request, DnsMessage response) {
super(getString(request, response));
assert request.id != response.id;
this.request = request;
this.response = response;
}
public DnsMessage getRequest() {
return request;
}
public DnsMessage getResponse() {
return response;
}
private static String getString(DnsMessage request, DnsMessage response) {
return "The response's ID doesn't matches the request ID. Request: " + request.id + ". Response: " + response.id;
}
}
public static class NullResultException extends MiniDnsException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final DnsMessage request;
public NullResultException(DnsMessage request) {
super("The request yielded a 'null' result while resolving.");
this.request = request;
}
public DnsMessage getRequest() {
return request;
}
}
public static class ErrorResponseException extends MiniDnsException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final DnsMessage request;
private final DnsQueryResult result;
public ErrorResponseException(DnsMessage request, DnsQueryResult result) {
super("Received " + result.response.responseCode + " error response\n" + result);
this.request = request;
this.result = result;
}
public DnsMessage getRequest() {
return request;
}
public DnsQueryResult getResult() {
return result;
}
}
public static class NoQueryPossibleException extends MiniDnsException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final DnsMessage request;
public NoQueryPossibleException(DnsMessage request) {
super("No DNS server could be queried");
this.request = request;
}
public DnsMessage getRequest() {
return request;
}
}
}

View file

@ -0,0 +1,282 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.minidns.util.CallbackRecipient;
import org.minidns.util.ExceptionCallback;
import org.minidns.util.MultipleIoException;
import org.minidns.util.SuccessCallback;
public abstract class MiniDnsFuture<V, E extends Exception> implements Future<V>, CallbackRecipient<V, E> {
private boolean cancelled;
protected V result;
protected E exception;
private SuccessCallback<V> successCallback;
private ExceptionCallback<E> exceptionCallback;
@Override
public synchronized boolean cancel(boolean mayInterruptIfRunning) {
if (isDone()) {
return false;
}
cancelled = true;
if (mayInterruptIfRunning) {
notifyAll();
}
return true;
}
@Override
public final synchronized boolean isCancelled() {
return cancelled;
}
@Override
public final synchronized boolean isDone() {
return hasResult() || hasException();
}
public final synchronized boolean hasResult() {
return result != null;
}
public final synchronized boolean hasException() {
return exception != null;
}
@Override
public CallbackRecipient<V, E> onSuccess(SuccessCallback<V> successCallback) {
this.successCallback = successCallback;
maybeInvokeCallbacks();
return this;
}
@Override
public CallbackRecipient<V, E> onError(ExceptionCallback<E> exceptionCallback) {
this.exceptionCallback = exceptionCallback;
maybeInvokeCallbacks();
return this;
}
private V getOrThrowExecutionException() throws ExecutionException {
assert result != null || exception != null || cancelled;
if (result != null) {
return result;
}
if (exception != null) {
throw new ExecutionException(exception);
}
assert cancelled;
throw new CancellationException();
}
@Override
public final synchronized V get() throws InterruptedException, ExecutionException {
while (result == null && exception == null && !cancelled) {
wait();
}
return getOrThrowExecutionException();
}
public final synchronized V getOrThrow() throws E {
while (result == null && exception == null && !cancelled) {
try {
wait();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
if (exception != null) {
throw exception;
}
if (cancelled) {
throw new CancellationException();
}
assert result != null;
return result;
}
@Override
public final synchronized V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
final long deadline = System.currentTimeMillis() + unit.toMillis(timeout);
while (result != null && exception != null && !cancelled) {
final long waitTimeRemaining = deadline - System.currentTimeMillis();
if (waitTimeRemaining > 0) {
wait(waitTimeRemaining);
}
}
if (cancelled) {
throw new CancellationException();
}
if (result == null || exception == null) {
throw new TimeoutException();
}
return getOrThrowExecutionException();
}
private static final ExecutorService EXECUTOR_SERVICE;
static {
ThreadFactory threadFactory = new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
thread.setName("MiniDnsFuture Thread");
return thread;
}
};
BlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(128);
RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
r.run();
}
};
int cores = Runtime.getRuntime().availableProcessors();
int maximumPoolSize = cores <= 4 ? 2 : cores;
ExecutorService executorService = new ThreadPoolExecutor(0, maximumPoolSize, 60L, TimeUnit.SECONDS, blockingQueue, threadFactory,
rejectedExecutionHandler);
EXECUTOR_SERVICE = executorService;
}
@SuppressWarnings("FutureReturnValueIgnored")
protected final synchronized void maybeInvokeCallbacks() {
if (cancelled) {
return;
}
if (result != null && successCallback != null) {
EXECUTOR_SERVICE.submit(new Runnable() {
@Override
public void run() {
successCallback.onSuccess(result);
}
});
} else if (exception != null && exceptionCallback != null) {
EXECUTOR_SERVICE.submit(new Runnable() {
@Override
public void run() {
exceptionCallback.processException(exception);
}
});
}
}
public static class InternalMiniDnsFuture<V, E extends Exception> extends MiniDnsFuture<V, E> {
public final synchronized void setResult(V result) {
if (isDone()) {
return;
}
this.result = result;
this.notifyAll();
maybeInvokeCallbacks();
}
public final synchronized void setException(E exception) {
if (isDone()) {
return;
}
this.exception = exception;
this.notifyAll();
maybeInvokeCallbacks();
}
}
public static <V, E extends Exception> MiniDnsFuture<V, E> from(V result) {
InternalMiniDnsFuture<V, E> future = new InternalMiniDnsFuture<>();
future.setResult(result);
return future;
}
public static <V> MiniDnsFuture<V, IOException> anySuccessfulOf(Collection<MiniDnsFuture<V, IOException>> futures) {
return anySuccessfulOf(futures, exceptions -> MultipleIoException.toIOException(exceptions));
}
public interface ExceptionsWrapper<EI extends Exception, EO extends Exception> {
EO wrap(List<EI> exceptions);
}
public static <V, EI extends Exception, EO extends Exception> MiniDnsFuture<V, EO> anySuccessfulOf(
Collection<MiniDnsFuture<V, EI>> futures,
ExceptionsWrapper<EI, EO> exceptionsWrapper) {
InternalMiniDnsFuture<V, EO> returnedFuture = new InternalMiniDnsFuture<>();
final List<EI> exceptions = Collections.synchronizedList(new ArrayList<>(futures.size()));
for (MiniDnsFuture<V, EI> future : futures) {
future.onSuccess(new SuccessCallback<V>() {
@Override
public void onSuccess(V result) {
// Cancel all futures. Yes, this includes the future which just returned the
// result and futures which already failed with an exception, but then cancel
// will be a no-op.
for (MiniDnsFuture<V, EI> futureToCancel : futures) {
futureToCancel.cancel(true);
}
returnedFuture.setResult(result);
}
});
future.onError(new ExceptionCallback<EI>() {
@Override
public void processException(EI exception) {
exceptions.add(exception);
// Signal the main future about the exceptions, but only if all sub-futures returned an exception.
if (exceptions.size() == futures.size()) {
EO returnedException = exceptionsWrapper.wrap(exceptions);
returnedFuture.setException(returnedException);
}
}
});
}
return returnedFuture;
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.logging.Level;
import java.util.logging.Logger;
public class MiniDnsInitialization {
private static final Logger LOGGER = Logger.getLogger(MiniDnsInitialization.class.getName());
static final String VERSION;
static {
String miniDnsVersion;
BufferedReader reader = null;
try {
InputStream is = MiniDnsInitialization.class.getClassLoader().getResourceAsStream("org.minidns/version");
reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8));
miniDnsVersion = reader.readLine();
} catch (Exception e) {
LOGGER.log(Level.SEVERE, "Could not determine MiniDNS version", e);
miniDnsVersion = "unkown";
} finally {
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "IOException closing stream", e);
}
}
}
VERSION = miniDnsVersion;
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.minidns.dnsname.DnsName;
import org.minidns.record.Data;
import org.minidns.record.Record;
import org.minidns.record.Record.CLASS;
import org.minidns.record.Record.TYPE;
public final class RrSet {
public final DnsName name;
public final TYPE type;
public final CLASS clazz;
public final Set<Record<? extends Data>> records;
private RrSet(DnsName name, TYPE type, CLASS clazz, Set<Record<? extends Data>> records) {
this.name = name;
this.type = type;
this.clazz = clazz;
this.records = Collections.unmodifiableSet(records);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(name).append('\t').append(clazz).append('\t').append(type).append('\n');
for (Record<?> record : records) {
sb.append(record).append('\n');
}
return sb.toString();
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private DnsName name;
private TYPE type;
private CLASS clazz;
Set<Record<? extends Data>> records = new LinkedHashSet<>(8);
private Builder() {
}
public Builder addRecord(Record<? extends Data> record) {
if (name == null) {
name = record.name;
type = record.type;
clazz = record.clazz;
} else if (!couldContain(record)) {
throw new IllegalArgumentException(
"Can not add " + record + " to RRSet " + name + ' ' + type + ' ' + clazz);
}
boolean didNotExist = records.add(record);
assert didNotExist;
return this;
}
public boolean couldContain(Record<? extends Data> record) {
if (name == null) {
return true;
}
return name.equals(record.name) && type == record.type && clazz == record.clazz;
}
public boolean addIfPossible(Record<? extends Data> record) {
if (!couldContain(record)) {
return false;
}
addRecord(record);
return true;
}
public RrSet build() {
if (name == null) {
// There is no RR added to this builder.
throw new IllegalStateException();
}
return new RrSet(name, type, clazz, records);
}
}
}

View file

@ -0,0 +1,129 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.cache;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsqueryresult.CachedDnsQueryResult;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.dnsqueryresult.SynthesizedCachedDnsQueryResult;
import org.minidns.record.Data;
import org.minidns.record.Record;
/**
* A variant of {@link LruCache} also using the data found in the sections for caching.
*/
public class ExtendedLruCache extends LruCache {
public ExtendedLruCache() {
this(DEFAULT_CACHE_SIZE);
}
public ExtendedLruCache(int capacity) {
super(capacity);
}
public ExtendedLruCache(int capacity, long maxTTL) {
super(capacity, maxTTL);
}
@SuppressWarnings("UnsynchronizedOverridesSynchronized")
@Override
protected void putNormalized(DnsMessage q, DnsQueryResult result) {
super.putNormalized(q, result);
DnsMessage message = result.response;
Map<DnsMessage, List<Record<? extends Data>>> extraCaches = new HashMap<>(message.additionalSection.size());
gather(extraCaches, q, message.answerSection, null);
gather(extraCaches, q, message.authoritySection, null);
gather(extraCaches, q, message.additionalSection, null);
putExtraCaches(result, extraCaches);
}
@Override
public void offer(DnsMessage query, DnsQueryResult result, DnsName authoritativeZone) {
DnsMessage reply = result.response;
// The reply shouldn't be an authoritative answers when offer() is used. That would be a case for put().
assert !reply.authoritativeAnswer;
Map<DnsMessage, List<Record<? extends Data>>> extraCaches = new HashMap<>(reply.additionalSection.size());
// N.B. not gathering from reply.answerSection here. Since it is a non authoritativeAnswer it shouldn't contain anything.
gather(extraCaches, query, reply.authoritySection, authoritativeZone);
gather(extraCaches, query, reply.additionalSection, authoritativeZone);
putExtraCaches(result, extraCaches);
}
private void gather(Map<DnsMessage, List<Record<?extends Data>>> extraCaches, DnsMessage q, List<Record<? extends Data>> records, DnsName authoritativeZone) {
for (Record<? extends Data> extraRecord : records) {
if (!shouldGather(extraRecord, q.getQuestion(), authoritativeZone))
continue;
DnsMessage.Builder additionalRecordQuestionBuilder = extraRecord.getQuestionMessage();
if (additionalRecordQuestionBuilder == null)
continue;
additionalRecordQuestionBuilder.copyFlagsFrom(q);
additionalRecordQuestionBuilder.setAdditionalResourceRecords(q.additionalSection);
DnsMessage additionalRecordQuestion = additionalRecordQuestionBuilder.build();
if (additionalRecordQuestion.equals(q)) {
// No need to cache the additional question if it is the same as the original question.
continue;
}
List<Record<? extends Data>> additionalRecords = extraCaches.get(additionalRecordQuestion);
if (additionalRecords == null) {
additionalRecords = new ArrayList<>();
extraCaches.put(additionalRecordQuestion, additionalRecords);
}
additionalRecords.add(extraRecord);
}
}
private void putExtraCaches(DnsQueryResult synthesynthesizationSource, Map<DnsMessage, List<Record<? extends Data>>> extraCaches) {
DnsMessage reply = synthesynthesizationSource.response;
for (Entry<DnsMessage, List<Record<? extends Data>>> entry : extraCaches.entrySet()) {
DnsMessage question = entry.getKey();
DnsMessage answer = reply.asBuilder()
.setQuestion(question.getQuestion())
.setAuthoritativeAnswer(true)
.addAnswers(entry.getValue())
.build();
CachedDnsQueryResult cachedDnsQueryResult = new SynthesizedCachedDnsQueryResult(question, answer, synthesynthesizationSource);
synchronized (this) {
backend.put(question, cachedDnsQueryResult);
}
}
}
protected boolean shouldGather(Record<? extends Data> extraRecord, Question question, DnsName authoritativeZone) {
boolean extraRecordIsChildOfQuestion = extraRecord.name.isChildOf(question.name);
boolean extraRecordIsChildOfAuthoritativeZone = false;
if (authoritativeZone != null) {
extraRecordIsChildOfAuthoritativeZone = extraRecord.name.isChildOf(authoritativeZone);
}
return extraRecordIsChildOfQuestion || extraRecordIsChildOfAuthoritativeZone;
}
}

View file

@ -0,0 +1,39 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.cache;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.record.Data;
import org.minidns.record.Record;
/**
* An <b>insecure</b> variant of {@link LruCache} also using all the data found in the sections of an answer.
*/
public class FullLruCache extends ExtendedLruCache {
public FullLruCache() {
this(DEFAULT_CACHE_SIZE);
}
public FullLruCache(int capacity) {
super(capacity);
}
public FullLruCache(int capacity, long maxTTL) {
super(capacity, maxTTL);
}
@Override
protected boolean shouldGather(Record<? extends Data> extraRecord, Question question, DnsName authoritativeZone) {
return true;
}
}

View file

@ -0,0 +1,169 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.cache;
import java.util.LinkedHashMap;
import java.util.Map.Entry;
import org.minidns.DnsCache;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsqueryresult.CachedDnsQueryResult;
import org.minidns.dnsqueryresult.DirectCachedDnsQueryResult;
import org.minidns.dnsqueryresult.DnsQueryResult;
/**
* LRU based DNSCache backed by a LinkedHashMap.
*/
public class LruCache extends DnsCache {
/**
* Internal miss count.
*/
protected long missCount = 0L;
/**
* Internal expire count (subset of misses that was caused by expire).
*/
protected long expireCount = 0L;
/**
* Internal hit count.
*/
protected long hitCount = 0L;
/**
* The internal capacity of the backend cache.
*/
protected int capacity;
/**
* The upper bound of the ttl. All longer TTLs will be capped by this ttl.
*/
protected long maxTTL;
/**
* The backend cache.
*/
protected LinkedHashMap<DnsMessage, CachedDnsQueryResult> backend;
/**
* Create a new LRUCache with given capacity and upper bound ttl.
* @param capacity The internal capacity.
* @param maxTTL The upper bound for any ttl.
*/
@SuppressWarnings("serial")
public LruCache(final int capacity, final long maxTTL) {
this.capacity = capacity;
this.maxTTL = maxTTL;
backend = new LinkedHashMap<DnsMessage, CachedDnsQueryResult>(
Math.min(capacity + (capacity + 3) / 4 + 2, 11), 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Entry<DnsMessage, CachedDnsQueryResult> eldest) {
return size() > capacity;
}
};
}
/**
* Create a new LRUCache with given capacity.
* @param capacity The capacity of this cache.
*/
public LruCache(final int capacity) {
this(capacity, Long.MAX_VALUE);
}
public LruCache() {
this(DEFAULT_CACHE_SIZE);
}
@Override
protected synchronized void putNormalized(DnsMessage q, DnsQueryResult result) {
if (result.response.receiveTimestamp <= 0L) {
return;
}
backend.put(q, new DirectCachedDnsQueryResult(q, result));
}
@Override
protected synchronized CachedDnsQueryResult getNormalized(DnsMessage q) {
CachedDnsQueryResult result = backend.get(q);
if (result == null) {
missCount++;
return null;
}
DnsMessage message = result.response;
// RFC 2181 § 5.2 says that all TTLs in a RRSet should be equal, if this isn't the case, then we assume the
// shortest TTL to be the effective one.
final long answersMinTtl = message.getAnswersMinTtl();
final long ttl = Math.min(answersMinTtl, maxTTL);
final long expiryDate = message.receiveTimestamp + (ttl * 1000);
final long now = System.currentTimeMillis();
if (expiryDate < now) {
missCount++;
expireCount++;
backend.remove(q);
return null;
} else {
hitCount++;
return result;
}
}
/**
* Clear all entries in this cache.
*/
public synchronized void clear() {
backend.clear();
missCount = 0L;
hitCount = 0L;
expireCount = 0L;
}
/**
* Get the miss count of this cache which is the number of fruitless
* get calls since this cache was last resetted.
* @return The number of cache misses.
*/
public long getMissCount() {
return missCount;
}
/**
* The number of expires (cache hits that have had a ttl to low to be
* retrieved).
* @return The expire count.
*/
public long getExpireCount() {
return expireCount;
}
/**
* The cache hit count (all successful calls to get).
* @return The hit count.
*/
public long getHitCount() {
return hitCount;
}
@Override
public String toString() {
return "LRUCache{usage=" + backend.size() + "/" + capacity + ", hits=" + hitCount + ", misses=" + missCount + ", expires=" + expireCount + "}";
}
@Override
public void offer(DnsMessage query, DnsQueryResult result, DnsName knownAuthoritativeZone) {
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.cache;
import org.minidns.DnsCache;
public interface MiniDnsCacheFactory {
DnsCache newCache();
}

View file

@ -0,0 +1,107 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.constants;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
public class DnsRootServer {
private static final Map<Character, Inet4Address> IPV4_ROOT_SERVER_MAP = new HashMap<>();
private static final Map<Character, Inet6Address> IPV6_ROOT_SERVER_MAP = new HashMap<>();
protected static final Inet4Address[] IPV4_ROOT_SERVERS = new Inet4Address[] {
rootServerInet4Address('a', 198, 41, 0, 4),
rootServerInet4Address('b', 192, 228, 79, 201),
rootServerInet4Address('c', 192, 33, 4, 12),
rootServerInet4Address('d', 199, 7, 91 , 13),
rootServerInet4Address('e', 192, 203, 230, 10),
rootServerInet4Address('f', 192, 5, 5, 241),
rootServerInet4Address('g', 192, 112, 36, 4),
rootServerInet4Address('h', 198, 97, 190, 53),
rootServerInet4Address('i', 192, 36, 148, 17),
rootServerInet4Address('j', 192, 58, 128, 30),
rootServerInet4Address('k', 193, 0, 14, 129),
rootServerInet4Address('l', 199, 7, 83, 42),
rootServerInet4Address('m', 202, 12, 27, 33),
};
protected static final Inet6Address[] IPV6_ROOT_SERVERS = new Inet6Address[] {
rootServerInet6Address('a', 0x2001, 0x0503, 0xba3e, 0x0000, 0x0000, 0x000, 0x0002, 0x0030),
rootServerInet6Address('b', 0x2001, 0x0500, 0x0084, 0x0000, 0x0000, 0x000, 0x0000, 0x000b),
rootServerInet6Address('c', 0x2001, 0x0500, 0x0002, 0x0000, 0x0000, 0x000, 0x0000, 0x000c),
rootServerInet6Address('d', 0x2001, 0x0500, 0x002d, 0x0000, 0x0000, 0x000, 0x0000, 0x000d),
rootServerInet6Address('f', 0x2001, 0x0500, 0x002f, 0x0000, 0x0000, 0x000, 0x0000, 0x000f),
rootServerInet6Address('h', 0x2001, 0x0500, 0x0001, 0x0000, 0x0000, 0x000, 0x0000, 0x0053),
rootServerInet6Address('i', 0x2001, 0x07fe, 0x0000, 0x0000, 0x0000, 0x000, 0x0000, 0x0053),
rootServerInet6Address('j', 0x2001, 0x0503, 0x0c27, 0x0000, 0x0000, 0x000, 0x0002, 0x0030),
rootServerInet6Address('l', 0x2001, 0x0500, 0x0003, 0x0000, 0x0000, 0x000, 0x0000, 0x0042),
rootServerInet6Address('m', 0x2001, 0x0dc3, 0x0000, 0x0000, 0x0000, 0x000, 0x0000, 0x0035),
};
private static Inet4Address rootServerInet4Address(char rootServerId, int addr0, int addr1, int addr2, int addr3) {
Inet4Address inetAddress;
String name = rootServerId + ".root-servers.net";
try {
inetAddress = (Inet4Address) InetAddress.getByAddress(name, new byte[] { (byte) addr0, (byte) addr1, (byte) addr2,
(byte) addr3 });
IPV4_ROOT_SERVER_MAP.put(rootServerId, inetAddress);
} catch (UnknownHostException e) {
// This should never happen, if it does it's our fault!
throw new RuntimeException(e);
}
return inetAddress;
}
private static Inet6Address rootServerInet6Address(char rootServerId, int addr0, int addr1, int addr2, int addr3, int addr4, int addr5, int addr6, int addr7) {
Inet6Address inetAddress;
String name = rootServerId + ".root-servers.net";
try {
inetAddress = (Inet6Address) InetAddress.getByAddress(name, new byte[] {
// @formatter:off
(byte) (addr0 >> 8), (byte) addr0, (byte) (addr1 >> 8), (byte) addr1,
(byte) (addr2 >> 8), (byte) addr2, (byte) (addr3 >> 8), (byte) addr3,
(byte) (addr4 >> 8), (byte) addr4, (byte) (addr5 >> 8), (byte) addr5,
(byte) (addr6 >> 8), (byte) addr6, (byte) (addr7 >> 8), (byte) addr7
// @formatter:on
});
IPV6_ROOT_SERVER_MAP.put(rootServerId, inetAddress);
} catch (UnknownHostException e) {
// This should never happen, if it does it's our fault!
throw new RuntimeException(e);
}
return inetAddress;
}
public static Inet4Address getRandomIpv4RootServer(Random random) {
return IPV4_ROOT_SERVERS[random.nextInt(IPV4_ROOT_SERVERS.length)];
}
public static Inet6Address getRandomIpv6RootServer(Random random) {
return IPV6_ROOT_SERVERS[random.nextInt(IPV6_ROOT_SERVERS.length)];
}
public static Inet4Address getIpv4RootServerById(char id) {
return IPV4_ROOT_SERVER_MAP.get(id);
}
public static Inet6Address getIpv6RootServerById(char id) {
return IPV6_ROOT_SERVER_MAP.get(id);
}
}

View file

@ -0,0 +1,99 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.constants;
import java.util.HashMap;
import java.util.Map;
public final class DnssecConstants {
/**
* Do not allow to instantiate DNSSECConstants
*/
private DnssecConstants() {
}
private static final Map<Byte, SignatureAlgorithm> SIGNATURE_ALGORITHM_LUT = new HashMap<>();
/**
* DNSSEC Signature Algorithms.
*
* @see <a href=
* "http://www.iana.org/assignments/dns-sec-alg-numbers/dns-sec-alg-numbers.xhtml">
* IANA DNSSEC Algorithm Numbers</a>
*/
public enum SignatureAlgorithm {
@Deprecated
RSAMD5(1, "RSA/MD5"),
DH(2, "Diffie-Hellman"),
DSA(3, "DSA/SHA1"),
RSASHA1(5, "RSA/SHA-1"),
DSA_NSEC3_SHA1(6, "DSA_NSEC3-SHA1"),
RSASHA1_NSEC3_SHA1(7, "RSASHA1-NSEC3-SHA1"),
RSASHA256(8, "RSA/SHA-256"),
RSASHA512(10, "RSA/SHA-512"),
ECC_GOST(12, "GOST R 34.10-2001"),
ECDSAP256SHA256(13, "ECDSA Curve P-256 with SHA-256"),
ECDSAP384SHA384(14, "ECDSA Curve P-384 with SHA-384"),
INDIRECT(252, "Reserved for Indirect Keys"),
PRIVATEDNS(253, "private algorithm"),
PRIVATEOID(254, "private algorithm oid"),
;
SignatureAlgorithm(int number, String description) {
if (number < 0 || number > 255) {
throw new IllegalArgumentException();
}
this.number = (byte) number;
this.description = description;
SIGNATURE_ALGORITHM_LUT.put(this.number, this);
}
public final byte number;
public final String description;
public static SignatureAlgorithm forByte(byte b) {
return SIGNATURE_ALGORITHM_LUT.get(b);
}
}
private static final Map<Byte, DigestAlgorithm> DELEGATION_DIGEST_LUT = new HashMap<>();
/**
* DNSSEC Digest Algorithms.
*
* @see <a href=
* "https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml">
* IANA Delegation Signer (DS) Resource Record (RR)</a>
*/
public enum DigestAlgorithm {
SHA1(1, "SHA-1"),
SHA256(2, "SHA-256"),
GOST(3, "GOST R 34.11-94"),
SHA384(4, "SHA-384"),
;
DigestAlgorithm(int value, String description) {
if (value < 0 || value > 255) {
throw new IllegalArgumentException();
}
this.value = (byte) value;
this.description = description;
DELEGATION_DIGEST_LUT.put(this.value, this);
}
public final byte value;
public final String description;
public static DigestAlgorithm forByte(byte b) {
return DELEGATION_DIGEST_LUT.get(b);
}
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dane;
import java.security.cert.CertificateException;
import java.util.Collections;
import java.util.List;
import org.minidns.record.TLSA;
public abstract class DaneCertificateException extends CertificateException {
/**
*
*/
private static final long serialVersionUID = 1L;
protected DaneCertificateException() {
}
protected DaneCertificateException(String message) {
super(message);
}
public static class CertificateMismatch extends DaneCertificateException {
/**
*
*/
private static final long serialVersionUID = 1L;
public final TLSA tlsa;
public final byte[] computed;
public CertificateMismatch(TLSA tlsa, byte[] computed) {
super("The TLSA RR does not match the certificate");
this.tlsa = tlsa;
this.computed = computed;
}
}
public static class MultipleCertificateMismatchExceptions extends DaneCertificateException {
/**
*
*/
private static final long serialVersionUID = 1L;
public final List<CertificateMismatch> certificateMismatchExceptions;
public MultipleCertificateMismatchExceptions(List<CertificateMismatch> certificateMismatchExceptions) {
super("There where multiple CertificateMismatch exceptions because none of the TLSA RR does match the certificate");
assert !certificateMismatchExceptions.isEmpty();
this.certificateMismatchExceptions = Collections.unmodifiableList(certificateMismatchExceptions);
}
}
}

View file

@ -0,0 +1,272 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dane;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsname.DnsName;
import org.minidns.dnssec.DnssecClient;
import org.minidns.dnssec.DnssecQueryResult;
import org.minidns.dnssec.DnssecUnverifiedReason;
import org.minidns.record.Data;
import org.minidns.record.Record;
import org.minidns.record.TLSA;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLPeerUnverifiedException;
import javax.net.ssl.SSLSession;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import java.io.IOException;
import java.security.KeyManagementException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Logger;
/**
* A helper class to validate the usage of TLSA records.
*/
public class DaneVerifier {
private static final Logger LOGGER = Logger.getLogger(DaneVerifier.class.getName());
private final DnssecClient client;
public DaneVerifier() {
this(new DnssecClient());
}
public DaneVerifier(DnssecClient client) {
this.client = client;
}
/**
* Verifies the certificate chain in an active {@link SSLSocket}. The socket must be connected.
*
* @param socket A connected {@link SSLSocket} whose certificate chain shall be verified using DANE.
* @return Whether the DANE verification is the only requirement according to the TLSA record.
* If this method returns {@code false}, additional PKIX validation is required.
* @throws CertificateException if the certificate chain provided differs from the one enforced using DANE.
*/
public boolean verify(SSLSocket socket) throws CertificateException {
if (!socket.isConnected()) {
throw new IllegalStateException("Socket not yet connected.");
}
return verify(socket.getSession());
}
/**
* Verifies the certificate chain in an active {@link SSLSession}.
*
* @param session An active {@link SSLSession} whose certificate chain shall be verified using DANE.
* @return Whether the DANE verification is the only requirement according to the TLSA record.
* If this method returns {@code false}, additional PKIX validation is required.
* @throws CertificateException if the certificate chain provided differs from the one enforced using DANE.
*/
public boolean verify(SSLSession session) throws CertificateException {
try {
return verifyCertificateChain(convert(session.getPeerCertificates()), session.getPeerHost(), session.getPeerPort());
} catch (SSLPeerUnverifiedException e) {
throw new CertificateException("Peer not verified", e);
}
}
/**
* Verifies a certificate chain to be valid when used with the given connection details using DANE.
*
* @param chain A certificate chain that should be verified using DANE.
* @param hostName The DNS name of the host this certificate chain belongs to.
* @param port The port number that was used to reach the server providing the certificate chain in question.
* @return Whether the DANE verification is the only requirement according to the TLSA record.
* If this method returns {@code false}, additional PKIX validation is required.
* @throws CertificateException if the certificate chain provided differs from the one enforced using DANE.
*/
public boolean verifyCertificateChain(X509Certificate[] chain, String hostName, int port) throws CertificateException {
DnsName req = DnsName.from("_" + port + "._tcp." + hostName);
DnssecQueryResult result;
try {
result = client.queryDnssec(req, Record.TYPE.TLSA);
} catch (IOException e) {
throw new RuntimeException(e);
}
DnsMessage res = result.dnsQueryResult.response;
// TODO: We previously used the AD bit here. This allowed non-DNSSEC aware clients to be plugged into
// DaneVerifier, which, in turn, allows to use a trusted forward as DNSSEC validator. Is this a good idea?
if (!result.isAuthenticData()) {
String msg = "Got TLSA response from DNS server, but was not signed properly.";
msg += " Reasons:";
for (DnssecUnverifiedReason reason : result.getUnverifiedReasons()) {
msg += " " + reason;
}
LOGGER.info(msg);
return false;
}
List<DaneCertificateException.CertificateMismatch> certificateMismatchExceptions = new ArrayList<>();
boolean verified = false;
for (Record<? extends Data> record : res.answerSection) {
if (record.type == Record.TYPE.TLSA && record.name.equals(req)) {
TLSA tlsa = (TLSA) record.payloadData;
try {
verified |= checkCertificateMatches(chain[0], tlsa, hostName);
} catch (DaneCertificateException.CertificateMismatch certificateMismatchException) {
// Record the mismatch and only throw an exception if no
// TLSA RR is able to verify the cert. This allows for TLSA
// certificate rollover.
certificateMismatchExceptions.add(certificateMismatchException);
}
if (verified) break;
}
}
if (!verified && !certificateMismatchExceptions.isEmpty()) {
throw new DaneCertificateException.MultipleCertificateMismatchExceptions(certificateMismatchExceptions);
}
return verified;
}
private static boolean checkCertificateMatches(X509Certificate cert, TLSA tlsa, String hostName) throws CertificateException {
if (tlsa.certUsage == null) {
LOGGER.warning("TLSA certificate usage byte " + tlsa.certUsageByte + " is not supported while verifying " + hostName);
return false;
}
switch (tlsa.certUsage) {
case serviceCertificateConstraint: // PKIX-EE
case domainIssuedCertificate: // DANE-EE
break;
case caConstraint: // PKIX-TA
case trustAnchorAssertion: // DANE-TA
default:
LOGGER.warning("TLSA certificate usage " + tlsa.certUsage + " (" + tlsa.certUsageByte + ") not supported while verifying " + hostName);
return false;
}
if (tlsa.selector == null) {
LOGGER.warning("TLSA selector byte " + tlsa.selectorByte + " is not supported while verifying " + hostName);
return false;
}
byte[] comp = null;
switch (tlsa.selector) {
case fullCertificate:
comp = cert.getEncoded();
break;
case subjectPublicKeyInfo:
comp = cert.getPublicKey().getEncoded();
break;
default:
LOGGER.warning("TLSA selector " + tlsa.selector + " (" + tlsa.selectorByte + ") not supported while verifying " + hostName);
return false;
}
if (tlsa.matchingType == null) {
LOGGER.warning("TLSA matching type byte " + tlsa.matchingTypeByte + " is not supported while verifying " + hostName);
return false;
}
switch (tlsa.matchingType) {
case noHash:
break;
case sha256:
try {
comp = MessageDigest.getInstance("SHA-256").digest(comp);
} catch (NoSuchAlgorithmException e) {
throw new CertificateException("Verification using TLSA failed: could not SHA-256 for matching", e);
}
break;
case sha512:
try {
comp = MessageDigest.getInstance("SHA-512").digest(comp);
} catch (NoSuchAlgorithmException e) {
throw new CertificateException("Verification using TLSA failed: could not SHA-512 for matching", e);
}
break;
default:
LOGGER.warning("TLSA matching type " + tlsa.matchingType + " not supported while verifying " + hostName);
return false;
}
boolean matches = tlsa.certificateAssociationEquals(comp);
if (!matches) {
throw new DaneCertificateException.CertificateMismatch(tlsa, comp);
}
// domain issued certificate does not require further verification,
// service certificate constraint does.
return tlsa.certUsage == TLSA.CertUsage.domainIssuedCertificate;
}
/**
* Invokes {@link HttpsURLConnection#connect()} in a DANE verified fashion.
* This method must be called before {@link HttpsURLConnection#connect()} is invoked.
*
* If a SSLSocketFactory was set on this HttpsURLConnection, it will be ignored. You can use
* {@link #verifiedConnect(HttpsURLConnection, X509TrustManager)} to inject a custom {@link TrustManager}.
*
* @param conn connection to be connected.
* @return The {@link HttpsURLConnection} after being connected.
* @throws IOException when the connection could not be established.
* @throws CertificateException if there was an exception while verifying the certificate.
*/
public HttpsURLConnection verifiedConnect(HttpsURLConnection conn) throws IOException, CertificateException {
return verifiedConnect(conn, null);
}
/**
* Invokes {@link HttpsURLConnection#connect()} in a DANE verified fashion.
* This method must be called before {@link HttpsURLConnection#connect()} is invoked.
*
* If a SSLSocketFactory was set on this HttpsURLConnection, it will be ignored.
*
* @param conn connection to be connected.
* @param trustManager A non-default {@link TrustManager} to be used.
* @return The {@link HttpsURLConnection} after being connected.
* @throws IOException when the connection could not be established.
* @throws CertificateException if there was an exception while verifying the certificate.
*/
public HttpsURLConnection verifiedConnect(HttpsURLConnection conn, X509TrustManager trustManager) throws IOException, CertificateException {
try {
SSLContext context = SSLContext.getInstance("TLS");
ExpectingTrustManager expectingTrustManager = new ExpectingTrustManager(trustManager);
context.init(null, new TrustManager[] {expectingTrustManager}, null);
conn.setSSLSocketFactory(context.getSocketFactory());
conn.connect();
boolean fullyVerified = verifyCertificateChain(convert(conn.getServerCertificates()), conn.getURL().getHost(),
conn.getURL().getPort() < 0 ? conn.getURL().getDefaultPort() : conn.getURL().getPort());
// If fullyVerified is true then it's the DANE verification performed by verifiyCertificateChain() is
// sufficient to verify the certificate and we ignore possible pending exceptions of ExpectingTrustManager.
if (!fullyVerified && expectingTrustManager.hasException()) {
throw new IOException("Peer verification failed using PKIX", expectingTrustManager.getException());
}
return conn;
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
private static X509Certificate[] convert(Certificate[] certificates) {
List<X509Certificate> certs = new ArrayList<>();
for (Certificate certificate : certificates) {
if (certificate instanceof X509Certificate) {
certs.add((X509Certificate) certificate);
}
}
return certs.toArray(new X509Certificate[certs.size()]);
}
}

View file

@ -0,0 +1,63 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dane;
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
public class ExpectingTrustManager implements X509TrustManager {
private CertificateException exception;
private final X509TrustManager trustManager;
/**
* Creates a new instance of ExpectingTrustManager.
*
* @param trustManager The {@link X509TrustManager} to be used for verification.
* {@code null} to use the system default.
*/
public ExpectingTrustManager(X509TrustManager trustManager) {
this.trustManager = trustManager == null ? X509TrustManagerUtil.getDefault() : trustManager;
}
public boolean hasException() {
return exception != null;
}
public CertificateException getException() {
CertificateException e = exception;
exception = null;
return e;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
trustManager.checkClientTrusted(chain, authType);
} catch (CertificateException e) {
exception = e;
}
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
try {
trustManager.checkServerTrusted(chain, authType);
} catch (CertificateException e) {
exception = e;
}
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return trustManager.getAcceptedIssuers();
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dane;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import javax.net.ssl.TrustManager;
import javax.net.ssl.TrustManagerFactory;
import javax.net.ssl.X509TrustManager;
public class X509TrustManagerUtil {
public static X509TrustManager getDefault() {
return getDefault(null);
}
public static X509TrustManager getDefault(KeyStore keyStore) {
String defaultAlgorithm = TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory trustManagerFactory;
try {
trustManagerFactory = TrustManagerFactory.getInstance(defaultAlgorithm);
trustManagerFactory.init(keyStore);
} catch (NoSuchAlgorithmException | KeyStoreException e) {
throw new AssertionError(e);
}
for (TrustManager trustManager : trustManagerFactory.getTrustManagers()) {
if (trustManager instanceof X509TrustManager) {
return (X509TrustManager) trustManager;
}
}
throw new AssertionError("No trust manager for the default algorithm " + defaultAlgorithm + " found");
}
}

View file

@ -0,0 +1,172 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dane.java7;
import org.minidns.dane.DaneVerifier;
import org.minidns.dane.X509TrustManagerUtil;
import org.minidns.dnssec.DnssecClient;
import org.minidns.util.InetAddressUtil;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLSocket;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509ExtendedTrustManager;
import javax.net.ssl.X509TrustManager;
import java.net.Socket;
import java.security.KeyManagementException;
import java.security.NoSuchAlgorithmException;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.logging.Logger;
public class DaneExtendedTrustManager extends X509ExtendedTrustManager {
private static final Logger LOGGER = Logger.getLogger(DaneExtendedTrustManager.class.getName());
private final X509TrustManager base;
private final DaneVerifier verifier;
public static void inject() {
inject(new DaneExtendedTrustManager());
}
public static void inject(DaneExtendedTrustManager trustManager) {
try {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(null, new TrustManager[] {trustManager}, null);
SSLContext.setDefault(sslContext);
} catch (NoSuchAlgorithmException | KeyManagementException e) {
throw new RuntimeException(e);
}
}
public DaneExtendedTrustManager() {
this(X509TrustManagerUtil.getDefault());
}
public DaneExtendedTrustManager(DnssecClient client) {
this(client, X509TrustManagerUtil.getDefault());
}
public DaneExtendedTrustManager(X509TrustManager base) {
this(new DaneVerifier(), base);
}
public DaneExtendedTrustManager(DnssecClient client, X509TrustManager base) {
this(new DaneVerifier(client), base);
}
public DaneExtendedTrustManager(DaneVerifier verifier, X509TrustManager base) {
this.verifier = verifier;
this.base = base;
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
if (base == null) {
LOGGER.warning("DaneExtendedTrustManager invalidly used for client certificate check and no fallback X509TrustManager specified");
return;
}
LOGGER.info("DaneExtendedTrustManager invalidly used for client certificate check forwarding request to fallback X509TrustManage");
if (base instanceof X509ExtendedTrustManager) {
((X509ExtendedTrustManager) base).checkClientTrusted(chain, authType, socket);
} else {
base.checkClientTrusted(chain, authType);
}
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, Socket socket) throws CertificateException {
boolean verificationSuccessful = false;
if (socket instanceof SSLSocket) {
final SSLSocket sslSocket = (SSLSocket) socket;
final String hostname = sslSocket.getHandshakeSession().getPeerHost();
if (hostname == null) {
LOGGER.warning("Hostname returned by sslSocket.getHandshakeSession().getPeerHost() is null");
} else if (InetAddressUtil.isIpAddress(hostname)) {
LOGGER.warning(
"Hostname returned by sslSocket.getHandshakeSession().getPeerHost() '" + hostname
+ "' is an IP address");
} else {
final int port = socket.getPort();
verificationSuccessful = verifier.verifyCertificateChain(chain, hostname, port);
}
} else {
throw new IllegalStateException("The provided socket '" + socket + "' is not of type SSLSocket");
}
if (verificationSuccessful) {
// Verification successful, no need to delegate to base trust manager.
return;
}
if (base instanceof X509ExtendedTrustManager) {
((X509ExtendedTrustManager) base).checkServerTrusted(chain, authType, socket);
} else {
base.checkServerTrusted(chain, authType);
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
if (base == null) {
LOGGER.warning("DaneExtendedTrustManager invalidly used for client certificate check and no fallback X509TrustManager specified");
return;
}
LOGGER.info("DaneExtendedTrustManager invalidly used for client certificate check, forwarding request to fallback X509TrustManage");
if (base instanceof X509ExtendedTrustManager) {
((X509ExtendedTrustManager) base).checkClientTrusted(chain, authType, engine);
} else {
base.checkClientTrusted(chain, authType);
}
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType, SSLEngine engine) throws CertificateException {
if (verifier.verifyCertificateChain(chain, engine.getPeerHost(), engine.getPeerPort())) {
// Verification successful, no need to delegate to base trust manager.
return;
}
if (base instanceof X509ExtendedTrustManager) {
((X509ExtendedTrustManager) base).checkServerTrusted(chain, authType, engine);
} else {
base.checkServerTrusted(chain, authType);
}
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
if (base == null) {
LOGGER.warning("DaneExtendedTrustManager invalidly used for client certificate check and no fallback X509TrustManager specified");
return;
}
LOGGER.info("DaneExtendedTrustManager invalidly used for client certificate check, forwarding request to fallback X509TrustManage");
base.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
LOGGER.info("DaneExtendedTrustManager cannot be used without hostname information, forwarding request to fallback X509TrustManage");
base.checkServerTrusted(chain, authType);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return base.getAcceptedIssuers();
}
}

View file

@ -0,0 +1,25 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
import org.minidns.idna.MiniDnsIdna;
public final class ALabel extends XnLabel {
ALabel(String label) {
super(label);
}
@Override
protected String getInternationalizedRepresentationInternal() {
return MiniDnsIdna.toUnicode(label);
}
}

View file

@ -0,0 +1,280 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
import org.minidns.util.SafeCharSequence;
/**
* A DNS label is an individual component of a DNS name. Labels are usually shown separated by dots.
* <p>
* This class implements {@link Comparable} which compares DNS labels according to the Canonical DNS Name Order as
* specified in <a href="https://tools.ietf.org/html/rfc4034#section-6.1">RFC 4034 § 6.1</a>.
* </p>
* <p>
* Note that as per <a href="https://tools.ietf.org/html/rfc2181#section-11">RFC 2181 § 11</a> DNS labels may contain
* any byte.
* </p>
*
* @see <a href="https://tools.ietf.org/html/rfc5890#section-2.2">RFC 5890 § 2.2. DNS-Related Terminology</a>
* @author Florian Schmaus
*
*/
public abstract class DnsLabel extends SafeCharSequence implements Comparable<DnsLabel> {
/**
* The maximum length of a DNS label in octets.
*
* @see <a href="https://tools.ietf.org/html/rfc1035">RFC 1035 § 2.3.4.</a>
*/
public static final int MAX_LABEL_LENGTH_IN_OCTETS = 63;
public static final DnsLabel WILDCARD_LABEL = DnsLabel.from("*");
/**
* Whether or not the DNS label is validated on construction.
*/
public static boolean VALIDATE = true;
public final String label;
protected DnsLabel(String label) {
this.label = label;
if (!VALIDATE) {
return;
}
setBytesIfRequired();
if (byteCache.length > MAX_LABEL_LENGTH_IN_OCTETS) {
throw new LabelToLongException(label);
}
}
private transient String internationalizedRepresentation;
public final String getInternationalizedRepresentation() {
if (internationalizedRepresentation == null) {
internationalizedRepresentation = getInternationalizedRepresentationInternal();
}
return internationalizedRepresentation;
}
protected String getInternationalizedRepresentationInternal() {
return label;
}
public final String getLabelType() {
return getClass().getSimpleName();
}
private transient String safeToStringRepresentation;
@Override
public final String toString() {
if (safeToStringRepresentation == null) {
safeToStringRepresentation = toSafeRepesentation(label);
}
return safeToStringRepresentation;
}
/**
* Get the raw label. Note that this may return a String containing null bytes.
* Those Strings are notoriously difficult to handle from a security
* perspective. Therefore it is recommended to use {@link #toString()} instead,
* which will return a sanitized String.
*
* @return the raw label.
* @since 1.1.0
*/
public final String getRawLabel() {
return label;
}
@Override
public final boolean equals(Object other) {
if (!(other instanceof DnsLabel)) {
return false;
}
DnsLabel otherDnsLabel = (DnsLabel) other;
return label.equals(otherDnsLabel.label);
}
@Override
public final int hashCode() {
return label.hashCode();
}
private transient DnsLabel lowercasedVariant;
public final DnsLabel asLowercaseVariant() {
if (lowercasedVariant == null) {
String lowercaseLabel = label.toLowerCase(Locale.US);
lowercasedVariant = DnsLabel.from(lowercaseLabel);
}
return lowercasedVariant;
}
private transient byte[] byteCache;
private void setBytesIfRequired() {
if (byteCache == null) {
byteCache = label.getBytes(StandardCharsets.US_ASCII);
}
}
public final void writeToBoas(ByteArrayOutputStream byteArrayOutputStream) {
setBytesIfRequired();
byteArrayOutputStream.write(byteCache.length);
byteArrayOutputStream.write(byteCache, 0, byteCache.length);
}
@Override
public final int compareTo(DnsLabel other) {
String myCanonical = asLowercaseVariant().label;
String otherCanonical = other.asLowercaseVariant().label;
return myCanonical.compareTo(otherCanonical);
}
public static DnsLabel from(String label) {
if (label == null || label.isEmpty()) {
throw new IllegalArgumentException("Label is null or empty");
}
if (LdhLabel.isLdhLabel(label)) {
return LdhLabel.fromInternal(label);
}
return NonLdhLabel.fromInternal(label);
}
public static DnsLabel[] from(String[] labels) {
DnsLabel[] res = new DnsLabel[labels.length];
for (int i = 0; i < labels.length; i++) {
res[i] = DnsLabel.from(labels[i]);
}
return res;
}
public static boolean isIdnAcePrefixed(String string) {
return string.toLowerCase(Locale.US).startsWith("xn--");
}
public static String toSafeRepesentation(String dnsLabel) {
if (consistsOnlyOfLettersDigitsHypenAndUnderscore(dnsLabel)) {
// This label is safe, nothing to do.
return dnsLabel;
}
StringBuilder sb = new StringBuilder(2 * dnsLabel.length());
for (int i = 0; i < dnsLabel.length(); i++) {
char c = dnsLabel.charAt(i);
if (isLdhOrMaybeUnderscore(c, true)) {
sb.append(c);
continue;
}
// Let's see if we found and unsafe char we want to replace.
switch (c) {
case '.':
sb.append('●'); // U+25CF BLACK CIRCLE;
break;
case '\\':
sb.append('⧷'); // U+29F7 REVERSE SOLIDUS WITH HORIZONTAL STROKE
break;
case '\u007f':
// Convert DEL to U+2421 SYMBOL FOR DELETE
sb.append('␡');
break;
case ' ':
sb.append('␣'); // U+2423 OPEN BOX
break;
default:
if (c < 32) {
// First convert the ASCI control codes to the Unicode Control Pictures
int substituteAsInt = c + '\u2400';
assert substituteAsInt <= Character.MAX_CODE_POINT;
char substitute = (char) substituteAsInt;
sb.append(substitute);
} else if (c < 127) {
// Everything smaller than 127 is now safe to directly append.
sb.append(c);
} else if (c > 255) {
throw new IllegalArgumentException("The string '" + dnsLabel
+ "' contains characters outside the 8-bit range: " + c + " at position " + i);
} else {
// Everything that did not match the previous conditions is explicitly escaped.
sb.append(""); // U+301A
// Transform the char to hex notation. Note that we have ensure that c is <= 255
// here, hence only two hexadecimal places are ok.
String hex = String.format("%02X", (int) c);
sb.append(hex);
sb.append(""); // U+301B
}
}
}
return sb.toString();
}
private static boolean isLdhOrMaybeUnderscore(char c, boolean underscore) {
// CHECKSTYLE:OFF
return (c >= 'a' && c <= 'z')
|| (c >= 'A' && c <= 'Z')
|| (c >= '0' && c <= '9')
|| c == '-'
|| (underscore && c == '_')
;
// CHECKSTYLE:ON
}
private static boolean consistsOnlyOfLdhAndMaybeUnderscore(String string, boolean underscore) {
for (int i = 0; i < string.length(); i++) {
char c = string.charAt(i);
if (isLdhOrMaybeUnderscore(c, underscore)) {
continue;
}
return false;
}
return true;
}
public static boolean consistsOnlyOfLettersDigitsAndHypen(String string) {
return consistsOnlyOfLdhAndMaybeUnderscore(string, false);
}
public static boolean consistsOnlyOfLettersDigitsHypenAndUnderscore(String string) {
return consistsOnlyOfLdhAndMaybeUnderscore(string, true);
}
public static class LabelToLongException extends IllegalArgumentException {
/**
*
*/
private static final long serialVersionUID = 1L;
public final String label;
LabelToLongException(String label) {
this.label = label;
}
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
public final class FakeALabel extends XnLabel {
FakeALabel(String label) {
super(label);
}
}

View file

@ -0,0 +1,81 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
/**
* A LDH (<b>L</b>etters, <b>D</b>igits, <b>H</b>yphen) label, which is the
* classical label form.
* <p>
* Note that it is a common misconception that LDH labels can not start with a
* digit. The origin of this misconception is likely that
* <a href="https://datatracker.ietf.org/doc/html/rfc1034#section-3.5">RFC 1034
* § 3.5</a> specified
* </p>
* <blockquote>
* They [i.e, DNS labels] must start with a letter, end with a letter or digit,
* and have as interior characters only letters, digits, and hyphen.
* </blockquote>.
* However, this was relaxed in
* <a href="https://datatracker.ietf.org/doc/html/rfc1123#page-13">RFC 1123 §
* 2.1</a>
* <blockquote>
* One aspect of host name syntax is hereby changed: the restriction on the first
* character is relaxed to allow either a letter or a digit.
* </blockquote>
* and later summarized in
* <a href="https://datatracker.ietf.org/doc/html/rfc3696#section-2">RFC 3696 §
* 2</a>:
* <blockquote>
* If the hyphen is used, it is not permitted to appear at either the beginning
* or end of a label.
* </blockquote>
* Furthermore
* <a href="https://datatracker.ietf.org/doc/html/rfc5890#section-2.3.1">RFC
* 5890 § 2.3.1</a> only mentions the requirement that hyphen must not be the
* first or last character of a LDH label.
*
* @see <a href="https://tools.ietf.org/html/rfc5890#section-2.3.1">RFC 5890 §
* 2.3.1. LDH Label</a>
*
*/
public abstract class LdhLabel extends DnsLabel {
protected LdhLabel(String label) {
super(label);
}
public static boolean isLdhLabel(String label) {
if (label.isEmpty()) {
return false;
}
if (LeadingOrTrailingHyphenLabel.isLeadingOrTrailingHypenLabelInternal(label)) {
return false;
}
return consistsOnlyOfLettersDigitsAndHypen(label);
}
protected static LdhLabel fromInternal(String label) {
assert isLdhLabel(label);
if (ReservedLdhLabel.isReservedLdhLabel(label)) {
// Label starts with '??--'. Now let us see if it is a XN-Label, starting with 'xn--', but be aware that the
// 'xn' part is case insensitive. The XnLabel.isXnLabelInternal(String) method takes care of this.
if (XnLabel.isXnLabelInternal(label)) {
return XnLabel.fromInternal(label);
} else {
return new ReservedLdhLabel(label);
}
}
return new NonReservedLdhLabel(label);
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
/**
* A DNS label with a leading or trailing hyphen ('-').
*/
public final class LeadingOrTrailingHyphenLabel extends NonLdhLabel {
LeadingOrTrailingHyphenLabel(String label) {
super(label);
}
static boolean isLeadingOrTrailingHypenLabelInternal(String label) {
if (label.isEmpty()) {
return false;
}
if (label.charAt(0) == '-') {
return true;
}
if (label.charAt(label.length() - 1) == '-') {
return true;
}
return false;
}
}

View file

@ -0,0 +1,35 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
/**
* A DNS label which contains more than just letters, digits and a hyphen.
*
*/
public abstract class NonLdhLabel extends DnsLabel {
protected NonLdhLabel(String label) {
super(label);
}
protected static DnsLabel fromInternal(String label) {
if (UnderscoreLabel.isUnderscoreLabelInternal(label)) {
return new UnderscoreLabel(label);
}
if (LeadingOrTrailingHyphenLabel.isLeadingOrTrailingHypenLabelInternal(label)) {
return new LeadingOrTrailingHyphenLabel(label);
}
return new OtherNonLdhLabel(label);
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
/**
* A Non-Reserved LDH label (NR-LDH label), which do <em>not</em> have "--" in the third and fourth position.
*
*/
public final class NonReservedLdhLabel extends LdhLabel {
NonReservedLdhLabel(String label) {
super(label);
assert isNonReservedLdhLabelInternal(label);
}
public static boolean isNonReservedLdhLabel(String label) {
if (!isLdhLabel(label)) {
return false;
}
return isNonReservedLdhLabelInternal(label);
}
static boolean isNonReservedLdhLabelInternal(String label) {
return !ReservedLdhLabel.isReservedLdhLabelInternal(label);
}
}

View file

@ -0,0 +1,23 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
/**
* A Non-LDH label which does <em>not</em> begin with an underscore ('_'), hyphen ('-') or ends with an hyphen.
*
*/
public final class OtherNonLdhLabel extends NonLdhLabel {
OtherNonLdhLabel(String label) {
super(label);
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
/**
* A reserved LDH label (R-LDH label), which have the property that they contain "--" in the third and fourth characters.
*
*/
public class ReservedLdhLabel extends LdhLabel {
protected ReservedLdhLabel(String label) {
super(label);
assert isReservedLdhLabelInternal(label);
}
public static boolean isReservedLdhLabel(String label) {
if (!isLdhLabel(label)) {
return false;
}
return isReservedLdhLabelInternal(label);
}
static boolean isReservedLdhLabelInternal(String label) {
return label.length() >= 4
&& label.charAt(2) == '-'
&& label.charAt(3) == '-';
}
}

View file

@ -0,0 +1,26 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
/**
* A DNS label which begins with an underscore ('_').
*
*/
public final class UnderscoreLabel extends NonLdhLabel {
UnderscoreLabel(String label) {
super(label);
}
static boolean isUnderscoreLabelInternal(String label) {
return label.charAt(0) == '_';
}
}

View file

@ -0,0 +1,50 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnslabel;
import java.util.Locale;
import org.minidns.idna.MiniDnsIdna;
/**
* A label that begins with "xn--" and follows the LDH rule.
*/
public abstract class XnLabel extends ReservedLdhLabel {
protected XnLabel(String label) {
super(label);
}
protected static LdhLabel fromInternal(String label) {
assert isIdnAcePrefixed(label);
String uLabel = MiniDnsIdna.toUnicode(label);
if (label.equals(uLabel)) {
// No Punycode conversation to Unicode was performed, this is a fake A-label!
return new FakeALabel(label);
} else {
return new ALabel(label);
}
}
public static boolean isXnLabel(String label) {
if (!isReservedLdhLabel(label)) {
return false;
}
return isXnLabelInternal(label);
}
static boolean isXnLabelInternal(String label) {
// Note that we already ensure the minimum label length here, since reserved LDH
// labels must start with "xn--".
return label.substring(0, 2).toLowerCase(Locale.US).equals("xn");
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,180 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsmessage;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.Arrays;
import org.minidns.dnsname.DnsName;
import org.minidns.record.Record.CLASS;
import org.minidns.record.Record.TYPE;
/**
* A DNS question (request).
*/
public class Question {
/**
* The question string (e.g. "measite.de").
*/
public final DnsName name;
/**
* The question type (e.g. A).
*/
public final TYPE type;
/**
* The question class (usually IN for Internet).
*/
public final CLASS clazz;
/**
* UnicastQueries have the highest bit of the CLASS field set to 1.
*/
private final boolean unicastQuery;
/**
* Cache for the serialized object.
*/
private byte[] byteArray;
/**
* Create a dns question for the given name/type/class.
* @param name The name e.g. "measite.de".
* @param type The type, e.g. A.
* @param clazz The class, usually IN (internet).
* @param unicastQuery True if this is a unicast query.
*/
public Question(CharSequence name, TYPE type, CLASS clazz, boolean unicastQuery) {
this(DnsName.from(name), type, clazz, unicastQuery);
}
public Question(DnsName name, TYPE type, CLASS clazz, boolean unicastQuery) {
assert name != null;
assert type != null;
assert clazz != null;
this.name = name;
this.type = type;
this.clazz = clazz;
this.unicastQuery = unicastQuery;
}
/**
* Create a dns question for the given name/type/class.
* @param name The name e.g. "measite.de".
* @param type The type, e.g. A.
* @param clazz The class, usually IN (internet).
*/
public Question(DnsName name, TYPE type, CLASS clazz) {
this(name, type, clazz, false);
}
/**
* Create a dns question for the given name/type/IN (internet class).
* @param name The name e.g. "measite.de".
* @param type The type, e.g. A.
*/
public Question(DnsName name, TYPE type) {
this(name, type, CLASS.IN);
}
/**
* Create a dns question for the given name/type/class.
* @param name The name e.g. "measite.de".
* @param type The type, e.g. A.
* @param clazz The class, usually IN (internet).
*/
public Question(CharSequence name, TYPE type, CLASS clazz) {
this(DnsName.from(name), type, clazz);
}
/**
* Create a dns question for the given name/type/IN (internet class).
* @param name The name e.g. "measite.de".
* @param type The type, e.g. A.
*/
public Question(CharSequence name, TYPE type) {
this(DnsName.from(name), type);
}
/**
* Parse a byte array and rebuild the dns question from it.
* @param dis The input stream.
* @param data The plain data (for dns name references).
* @throws IOException On errors (read outside of packet).
*/
public Question(DataInputStream dis, byte[] data) throws IOException {
name = DnsName.parse(dis, data);
type = TYPE.getType(dis.readUnsignedShort());
clazz = CLASS.getClass(dis.readUnsignedShort());
unicastQuery = false;
}
/**
* Generate a binary paket for this dns question.
* @return The dns question.
*/
public byte[] toByteArray() {
if (byteArray == null) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(512);
DataOutputStream dos = new DataOutputStream(baos);
try {
name.writeToStream(dos);
dos.writeShort(type.getValue());
dos.writeShort(clazz.getValue() | (unicastQuery ? (1 << 15) : 0));
dos.flush();
} catch (IOException e) {
// Should never happen
throw new RuntimeException(e);
}
byteArray = baos.toByteArray();
}
return byteArray;
}
@Override
public int hashCode() {
return Arrays.hashCode(toByteArray());
}
@Override
public boolean equals(Object other) {
if (this == other) {
return true;
}
if (!(other instanceof Question)) {
return false;
}
byte[] t = toByteArray();
byte[] o = ((Question) other).toByteArray();
return Arrays.equals(t, o);
}
@Override
public String toString() {
return name.getRawAce() + ".\t" + clazz + '\t' + type;
}
public DnsMessage.Builder asMessageBuilder() {
DnsMessage.Builder builder = DnsMessage.builder();
builder.setQuestion(this);
return builder;
}
public DnsMessage asQueryMessage() {
return asMessageBuilder().build();
}
}

View file

@ -0,0 +1,646 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsname;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Locale;
import org.minidns.dnslabel.DnsLabel;
import org.minidns.idna.MiniDnsIdna;
import org.minidns.util.SafeCharSequence;
/**
* A DNS name, also called "domain name". A DNS name consists of multiple 'labels' (see {@link DnsLabel}) and is subject to certain restrictions (see
* for example <a href="https://tools.ietf.org/html/rfc3696#section-2">RFC 3696 § 2.</a>).
* <p>
* Instances of this class can be created by using {@link #from(String)}.
* </p>
* <p>
* This class holds three representations of a DNS name: ACE, raw ACE and IDN. ACE (ASCII Compatible Encoding), which
* can be accessed via {@link #ace}, represents mostly the data that got send over the wire. But since DNS names are
* case insensitive, the ACE value is normalized to lower case. You can use {@link #getRawAce()} to get the raw ACE data
* that was received, which possibly includes upper case characters. The IDN (Internationalized Domain Name), that is
* the DNS name as it should be shown to the user, can be retrieved using {@link #asIdn()}.
* </p>
* More information about Internationalized Domain Names can be found at:
* <ul>
* <li><a href="https://unicode.org/reports/tr46/">UTS #46 - Unicode IDNA Compatibility Processing</a>
* <li><a href="https://tools.ietf.org/html/rfc8753">RFC 8753 - Internationalized Domain Names for Applications (IDNA) Review for New Unicode Versions</a>
* </ul>
*
* @see <a href="https://tools.ietf.org/html/rfc3696">RFC 3696</a>
* @see DnsLabel
* @author Florian Schmaus
*
*/
public final class DnsName extends SafeCharSequence implements Serializable, Comparable<DnsName> {
/**
*
*/
private static final long serialVersionUID = 1L;
/**
* @see <a href="https://www.ietf.org/rfc/rfc3490.txt">RFC 3490 § 3.1 1.</a>
*/
private static final String LABEL_SEP_REGEX = "[.\u3002\uFF0E\uFF61]";
/**
* See <a href="https://tools.ietf.org/html/rfc1035">RFC 1035 § 2.3.4.</a>
*/
static final int MAX_DNSNAME_LENGTH_IN_OCTETS = 255;
public static final int MAX_LABELS = 128;
public static final DnsName ROOT = new DnsName(".");
public static final DnsName IN_ADDR_ARPA = new DnsName("in-addr.arpa");
public static final DnsName IP6_ARPA = new DnsName("ip6.arpa");
/**
* Whether or not the DNS name is validated on construction.
*/
public static boolean VALIDATE = true;
/**
* The DNS name in ASCII Compatible Encoding (ACE).
*/
public final String ace;
/**
* The DNS name in raw format, i.e. as it was received from the remote server. This means that compared to
* {@link #ace}, this String may not be lower-cased.
*/
private final String rawAce;
private transient byte[] bytes;
private transient byte[] rawBytes;
private transient String idn;
private transient String domainpart;
private transient String hostpart;
/**
* The labels in <b>reverse</b> order.
*/
private transient DnsLabel[] labels;
private transient DnsLabel[] rawLabels;
private transient int hashCode;
private int size = -1;
private DnsName(String name) {
this(name, true);
}
private DnsName(String name, boolean inAce) {
if (name.isEmpty()) {
rawAce = ROOT.rawAce;
} else {
final int nameLength = name.length();
final int nameLastPos = nameLength - 1;
// Strip potential trailing dot. N.B. that we require nameLength > 2, because we don't want to strip the one
// character string containing only a single dot to the empty string.
if (nameLength >= 2 && name.charAt(nameLastPos) == '.') {
name = name.subSequence(0, nameLastPos).toString();
}
if (inAce) {
// Name is already in ACE format.
rawAce = name;
} else {
rawAce = MiniDnsIdna.toASCII(name);
}
}
ace = rawAce.toLowerCase(Locale.US);
if (!VALIDATE) {
return;
}
// Validate the DNS name.
validateMaxDnsnameLengthInOctets();
}
private DnsName(DnsLabel[] rawLabels, boolean validateMaxDnsnameLength) {
this.rawLabels = rawLabels;
this.labels = new DnsLabel[rawLabels.length];
int size = 0;
for (int i = 0; i < rawLabels.length; i++) {
size += rawLabels[i].length() + 1;
labels[i] = rawLabels[i].asLowercaseVariant();
}
rawAce = labelsToString(rawLabels, size);
ace = labelsToString(labels, size);
// The following condition is deliberately designed that VALIDATE=false causes the validation to be skipped even
// if validateMaxDnsnameLength is set to true. There is no need to validate even if this constructor is called
// with validateMaxDnsnameLength set to true if VALIDATE is globally set to false.
if (!validateMaxDnsnameLength || !VALIDATE) {
return;
}
validateMaxDnsnameLengthInOctets();
}
private static String labelsToString(DnsLabel[] labels, int stringLength) {
StringBuilder sb = new StringBuilder(stringLength);
for (int i = labels.length - 1; i >= 0; i--) {
sb.append(labels[i]).append('.');
}
sb.setLength(sb.length() - 1);
return sb.toString();
}
private void validateMaxDnsnameLengthInOctets() {
setBytesIfRequired();
if (bytes.length > MAX_DNSNAME_LENGTH_IN_OCTETS) {
throw new InvalidDnsNameException.DNSNameTooLongException(ace, bytes);
}
}
public void writeToStream(OutputStream os) throws IOException {
setBytesIfRequired();
os.write(bytes);
}
/**
* Serialize a domain name under IDN rules.
*
* @return The binary domain name representation.
*/
public byte[] getBytes() {
setBytesIfRequired();
return bytes.clone();
}
public byte[] getRawBytes() {
if (rawBytes == null) {
setLabelsIfRequired();
rawBytes = toBytes(rawLabels);
}
return rawBytes.clone();
}
private void setBytesIfRequired() {
if (bytes != null)
return;
setLabelsIfRequired();
bytes = toBytes(labels);
}
private static byte[] toBytes(DnsLabel[] labels) {
ByteArrayOutputStream baos = new ByteArrayOutputStream(64);
for (int i = labels.length - 1; i >= 0; i--) {
labels[i].writeToBoas(baos);
}
baos.write(0);
assert baos.size() <= MAX_DNSNAME_LENGTH_IN_OCTETS;
return baos.toByteArray();
}
private void setLabelsIfRequired() {
if (labels != null && rawLabels != null) return;
if (isRootLabel()) {
rawLabels = labels = new DnsLabel[0];
return;
}
labels = getLabels(ace);
rawLabels = getLabels(rawAce);
}
private static DnsLabel[] getLabels(String ace) {
String[] labels = ace.split(LABEL_SEP_REGEX, MAX_LABELS);
// Reverse the labels, so that 'foo, example, org' becomes 'org, example, foo'.
for (int i = 0; i < labels.length / 2; i++) {
String t = labels[i];
int j = labels.length - i - 1;
labels[i] = labels[j];
labels[j] = t;
}
try {
return DnsLabel.from(labels);
} catch (DnsLabel.LabelToLongException e) {
throw new InvalidDnsNameException.LabelTooLongException(ace, e.label);
}
}
/**
* Return the ACE (ASCII Compatible Encoding) version of this DNS name. Note
* that this method may return a String containing null bytes. Those Strings are
* notoriously difficult to handle from a security perspective. Therefore it is
* recommended to use {@link #toString()} instead, which will return a sanitized
* String.
*
* @return the ACE version of this DNS name.
* @since 1.1.0
*/
public String getAce() {
return ace;
}
/**
* Returns the raw ACE version of this DNS name. That is, the version as it was
* received over the wire. Most notably, this version may include uppercase
* letters.
*
* <b>Please refer to {@link #getAce()} for a discussion of the security
* implications when working with the ACE representation of a DNS name.</b>
*
* @return the raw ACE version of this DNS name.
* @see #getAce()
*/
public String getRawAce() {
return rawAce;
}
public String asIdn() {
if (idn != null)
return idn;
idn = MiniDnsIdna.toUnicode(ace);
return idn;
}
/**
* Domainpart in ACE representation.
*
* @return the domainpart in ACE representation.
*/
public String getDomainpart() {
setHostnameAndDomainpartIfRequired();
return domainpart;
}
/**
* Hostpart in ACE representation.
*
* @return the hostpart in ACE representation.
*/
public String getHostpart() {
setHostnameAndDomainpartIfRequired();
return hostpart;
}
public DnsLabel getHostpartLabel() {
setLabelsIfRequired();
return labels[labels.length - 1];
}
private void setHostnameAndDomainpartIfRequired() {
if (hostpart != null) return;
String[] parts = ace.split(LABEL_SEP_REGEX, 2);
hostpart = parts[0];
if (parts.length > 1) {
domainpart = parts[1];
} else {
domainpart = "";
}
}
public int size() {
if (size < 0) {
if (isRootLabel()) {
size = 1;
} else {
size = ace.length() + 2;
}
}
return size;
}
private transient String safeToStringRepresentation;
@Override
public String toString() {
if (safeToStringRepresentation == null) {
setLabelsIfRequired();
if (labels.length == 0) {
return ".";
}
StringBuilder sb = new StringBuilder();
for (int i = labels.length - 1; i >= 0; i--) {
// Note that it is important that we append the result of DnsLabel.toString() to
// the StringBuilder. As only the result of toString() is the safe label
// representation.
String safeLabelRepresentation = labels[i].toString();
sb.append(safeLabelRepresentation);
if (i != 0) {
sb.append('.');
}
}
safeToStringRepresentation = sb.toString();
}
return safeToStringRepresentation;
}
public static DnsName from(CharSequence name) {
return from(name.toString());
}
public static DnsName from(String name) {
return new DnsName(name, false);
}
/**
* Create a DNS name by "concatenating" the child under the parent name. The child can also be seen as the "left"
* part of the resulting DNS name and the parent is the "right" part.
* <p>
* For example using "i.am.the.child" as child and "of.this.parent.example" as parent, will result in a DNS name:
* "i.am.the.child.of.this.parent.example".
* </p>
*
* @param child the child DNS name.
* @param parent the parent DNS name.
* @return the resulting of DNS name.
*/
public static DnsName from(DnsName child, DnsName parent) {
child.setLabelsIfRequired();
parent.setLabelsIfRequired();
DnsLabel[] rawLabels = new DnsLabel[child.rawLabels.length + parent.rawLabels.length];
System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length);
System.arraycopy(child.rawLabels, 0, rawLabels, parent.rawLabels.length, child.rawLabels.length);
return new DnsName(rawLabels, true);
}
public static DnsName from(CharSequence child, DnsName parent) {
DnsLabel childLabel = DnsLabel.from(child.toString());
return DnsName.from(childLabel, parent);
}
public static DnsName from(DnsLabel child, DnsName parent) {
parent.setLabelsIfRequired();
DnsLabel[] rawLabels = new DnsLabel[parent.rawLabels.length + 1];
System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length);
rawLabels[parent.rawLabels.length] = child;
return new DnsName(rawLabels, true);
}
public static DnsName from(DnsLabel grandchild, DnsLabel child, DnsName parent) {
parent.setBytesIfRequired();
DnsLabel[] rawLabels = new DnsLabel[parent.rawLabels.length + 2];
System.arraycopy(parent.rawLabels, 0, rawLabels, 0, parent.rawLabels.length);
rawLabels[parent.rawLabels.length] = child;
rawLabels[parent.rawLabels.length + 1] = grandchild;
return new DnsName(rawLabels, true);
}
public static DnsName from(DnsName... nameComponents) {
int labelCount = 0;
for (DnsName component : nameComponents) {
component.setLabelsIfRequired();
labelCount += component.rawLabels.length;
}
DnsLabel[] rawLabels = new DnsLabel[labelCount];
int destLabelPos = 0;
for (int i = nameComponents.length - 1; i >= 0; i--) {
DnsName component = nameComponents[i];
System.arraycopy(component.rawLabels, 0, rawLabels, destLabelPos, component.rawLabels.length);
destLabelPos += component.rawLabels.length;
}
return new DnsName(rawLabels, true);
}
public static DnsName from(String[] parts) {
DnsLabel[] rawLabels = DnsLabel.from(parts);
return new DnsName(rawLabels, true);
}
/**
* Parse a domain name starting at the current offset and moving the input
* stream pointer past this domain name (even if cross references occure).
*
* @param dis The input stream.
* @param data The raw data (for cross references).
* @return The domain name string.
* @throws IOException Should never happen.
*/
public static DnsName parse(DataInputStream dis, byte[] data)
throws IOException {
int c = dis.readUnsignedByte();
if ((c & 0xc0) == 0xc0) {
c = ((c & 0x3f) << 8) + dis.readUnsignedByte();
HashSet<Integer> jumps = new HashSet<Integer>();
jumps.add(c);
return parse(data, c, jumps);
}
if (c == 0) {
return DnsName.ROOT;
}
byte[] b = new byte[c];
dis.readFully(b);
String childLabelString = new String(b, StandardCharsets.US_ASCII);
DnsName child = new DnsName(childLabelString);
DnsName parent = parse(dis, data);
return DnsName.from(child, parent);
}
/**
* Parse a domain name starting at the given offset.
*
* @param data The raw data.
* @param offset The offset.
* @param jumps The list of jumps (by now).
* @return The parsed domain name.
* @throws IllegalStateException on cycles.
*/
@SuppressWarnings("NonApiType")
private static DnsName parse(byte[] data, int offset, HashSet<Integer> jumps)
throws IllegalStateException {
int c = data[offset] & 0xff;
if ((c & 0xc0) == 0xc0) {
c = ((c & 0x3f) << 8) + (data[offset + 1] & 0xff);
if (jumps.contains(c)) {
throw new IllegalStateException("Cyclic offsets detected.");
}
jumps.add(c);
return parse(data, c, jumps);
}
if (c == 0) {
return DnsName.ROOT;
}
String childLabelString = new String(data, offset + 1, c, StandardCharsets.US_ASCII);
DnsName child = new DnsName(childLabelString);
DnsName parent = parse(data, offset + 1 + c, jumps);
return DnsName.from(child, parent);
}
@Override
public int compareTo(DnsName other) {
return ace.compareTo(other.ace);
}
@Override
public boolean equals(Object other) {
if (other == null) return false;
if (other instanceof DnsName) {
DnsName otherDnsName = (DnsName) other;
setBytesIfRequired();
otherDnsName.setBytesIfRequired();
return Arrays.equals(bytes, otherDnsName.bytes);
}
return false;
}
@Override
public int hashCode() {
if (hashCode == 0 && !isRootLabel()) {
setBytesIfRequired();
hashCode = Arrays.hashCode(bytes);
}
return hashCode;
}
public boolean isDirectChildOf(DnsName parent) {
setLabelsIfRequired();
parent.setLabelsIfRequired();
int parentLabelsCount = parent.labels.length;
if (labels.length - 1 != parentLabelsCount)
return false;
for (int i = 0; i < parent.labels.length; i++) {
if (!labels[i].equals(parent.labels[i]))
return false;
}
return true;
}
public boolean isChildOf(DnsName parent) {
setLabelsIfRequired();
parent.setLabelsIfRequired();
if (labels.length < parent.labels.length)
return false;
for (int i = 0; i < parent.labels.length; i++) {
if (!labels[i].equals(parent.labels[i]))
return false;
}
return true;
}
public int getLabelCount() {
setLabelsIfRequired();
return labels.length;
}
/**
* Get a copy of the labels of this DNS name. The resulting array will contain the labels in reverse order, that is,
* the top-level domain will be at res[0].
*
* @return an array of the labels in reverse order.
*/
public DnsLabel[] getLabels() {
setLabelsIfRequired();
return labels.clone();
}
public DnsLabel getLabel(int labelNum) {
setLabelsIfRequired();
return labels[labelNum];
}
/**
* Get a copy of the raw labels of this DNS name. The resulting array will contain the labels in reverse order, that is,
* the top-level domain will be at res[0].
*
* @return an array of the raw labels in reverse order.
*/
public DnsLabel[] getRawLabels() {
setLabelsIfRequired();
return rawLabels.clone();
}
public DnsName stripToLabels(int labelCount) {
setLabelsIfRequired();
if (labelCount > labels.length) {
throw new IllegalArgumentException();
}
if (labelCount == labels.length) {
return this;
}
if (labelCount == 0) {
return ROOT;
}
DnsLabel[] stripedLabels = Arrays.copyOfRange(rawLabels, 0, labelCount);
return new DnsName(stripedLabels, false);
}
/**
* Return the parent of this DNS label. Will return the root label if this label itself is the root label (because there is no parent of root).
* <p>
* For example:
* </p>
* <ul>
* <li><code>"foo.bar.org".getParent() == "bar.org"</code></li>
* <li><code> ".".getParent() == "."</code></li>
* </ul>
* @return the parent of this DNS label.
*/
public DnsName getParent() {
if (isRootLabel()) return ROOT;
return stripToLabels(getLabelCount() - 1);
}
public boolean isRootLabel() {
return ace.isEmpty() || ace.equals(".");
}
}

View file

@ -0,0 +1,66 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsname;
import org.minidns.dnslabel.DnsLabel;
public abstract class InvalidDnsNameException extends IllegalStateException {
private static final long serialVersionUID = 1L;
protected final String ace;
protected InvalidDnsNameException(String ace) {
this.ace = ace;
}
public static class LabelTooLongException extends InvalidDnsNameException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final String label;
public LabelTooLongException(String ace, String label) {
super(ace);
this.label = label;
}
@Override
public String getMessage() {
return "The DNS name '" + ace + "' contains the label '" + label
+ "' which exceeds the maximum label length of " + DnsLabel.MAX_LABEL_LENGTH_IN_OCTETS + " octets by "
+ (label.length() - DnsLabel.MAX_LABEL_LENGTH_IN_OCTETS) + " octets.";
}
}
public static class DNSNameTooLongException extends InvalidDnsNameException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final byte[] bytes;
public DNSNameTooLongException(String ace, byte[] bytes) {
super(ace);
this.bytes = bytes;
}
@Override
public String getMessage() {
return "The DNS name '" + ace + "' exceeds the maximum name length of "
+ DnsName.MAX_DNSNAME_LENGTH_IN_OCTETS + " octets by "
+ (bytes.length - DnsName.MAX_DNSNAME_LENGTH_IN_OCTETS) + " octets.";
}
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsqueryresult;
import org.minidns.dnsmessage.DnsMessage;
public abstract class CachedDnsQueryResult extends DnsQueryResult {
protected final DnsQueryResult cachedDnsQueryResult;
protected CachedDnsQueryResult(DnsMessage query, DnsQueryResult cachedDnsQueryResult) {
super(QueryMethod.cachedDirect, query, cachedDnsQueryResult.response);
this.cachedDnsQueryResult = cachedDnsQueryResult;
}
protected CachedDnsQueryResult(DnsMessage query, DnsMessage response, DnsQueryResult synthesynthesizationSource) {
super(QueryMethod.cachedSynthesized, query, response);
this.cachedDnsQueryResult = synthesynthesizationSource;
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsqueryresult;
import org.minidns.dnsmessage.DnsMessage;
public class DirectCachedDnsQueryResult extends CachedDnsQueryResult {
public DirectCachedDnsQueryResult(DnsMessage query, DnsQueryResult cachedDnsQueryResult) {
super(query, cachedDnsQueryResult);
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsqueryresult;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
public abstract class DnsQueryResult {
public enum QueryMethod {
udp,
tcp,
asyncUdp,
asyncTcp,
cachedDirect,
cachedSynthesized,
testWorld,
}
public final QueryMethod queryMethod;
public final DnsMessage query;
public final DnsMessage response;
protected DnsQueryResult(QueryMethod queryMethod, DnsMessage query, DnsMessage response) {
assert queryMethod != null;
assert query != null;
assert response != null;
this.queryMethod = queryMethod;
this.query = query;
this.response = response;
}
@Override
public String toString() {
return response.toString();
}
public boolean wasSuccessful() {
return response.responseCode == RESPONSE_CODE.NO_ERROR;
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsqueryresult;
import java.net.InetAddress;
import org.minidns.dnsmessage.DnsMessage;
public class StandardDnsQueryResult extends DnsQueryResult {
public final InetAddress serverAddress;
public final int port;
public StandardDnsQueryResult(InetAddress serverAddress, int port, QueryMethod queryMethod, DnsMessage query, DnsMessage responseDnsMessage) {
super(queryMethod, query, responseDnsMessage);
this.serverAddress = serverAddress;
this.port = port;
}
}

View file

@ -0,0 +1,21 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsqueryresult;
import org.minidns.dnsmessage.DnsMessage;
public class SynthesizedCachedDnsQueryResult extends CachedDnsQueryResult {
public SynthesizedCachedDnsQueryResult(DnsMessage query, DnsMessage response, DnsQueryResult synthesynthesizationSource) {
super(query, response, synthesynthesizationSource);
}
}

View file

@ -0,0 +1,15 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec;
public interface DigestCalculator {
byte[] digest(byte[] bytes);
}

View file

@ -0,0 +1,573 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec;
import org.minidns.DnsCache;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.dnssec.DnssecUnverifiedReason.NoActiveSignaturesReason;
import org.minidns.dnssec.DnssecUnverifiedReason.NoSecureEntryPointReason;
import org.minidns.dnssec.DnssecUnverifiedReason.NoSignaturesReason;
import org.minidns.dnssec.DnssecUnverifiedReason.NoTrustAnchorReason;
import org.minidns.dnssec.DnssecValidationFailedException.AuthorityDoesNotContainSoa;
import org.minidns.iterative.ReliableDnsClient;
import org.minidns.record.DLV;
import org.minidns.record.DNSKEY;
import org.minidns.record.DS;
import org.minidns.record.Data;
import org.minidns.record.DelegatingDnssecRR;
import org.minidns.record.NSEC;
import org.minidns.record.NSEC3;
import org.minidns.record.RRSIG;
import org.minidns.record.Record;
import org.minidns.record.Record.CLASS;
import org.minidns.record.Record.TYPE;
import java.io.IOException;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
public class DnssecClient extends ReliableDnsClient {
/**
* The root zone's KSK.
* The ID of the current key is "Klajeyz", and the key tag value is "20326".
*/
private static final BigInteger rootEntryKey = new BigInteger("1628686155461064465348252249725010996177649738666492500572664444461532807739744536029771810659241049343994038053541290419968870563183856865780916376571550372513476957870843322273120879361960335192976656756972171258658400305760429696147778001233984421619267530978084631948434496468785021389956803104620471232008587410372348519229650742022804219634190734272506220018657920136902014393834092648785514548876370028925405557661759399901378816916683122474038734912535425670533237815676134840739565610963796427401855723026687073600445461090736240030247906095053875491225879656640052743394090544036297390104110989318819106653199917493");
private static final DnsName DEFAULT_DLV = DnsName.from("dlv.isc.org");
/**
* Create a new DNSSEC aware DNS client using the global default cache.
*/
public DnssecClient() {
this(DEFAULT_CACHE);
}
/**
* Create a new DNSSEC aware DNS client with the given DNS cache.
*
* @param cache The backend DNS cache.
*/
public DnssecClient(DnsCache cache) {
super(cache);
addSecureEntryPoint(DnsName.ROOT, rootEntryKey.toByteArray());
}
/**
* Known secure entry points (SEPs).
*/
private final Map<DnsName, byte[]> knownSeps = new ConcurrentHashMap<>();
private boolean stripSignatureRecords = true;
/**
* The active DNSSEC Look-aside Validation Registry. May be <code>null</code>.
*/
private DnsName dlv;
@Override
public DnsQueryResult query(Question q) throws IOException {
DnssecQueryResult dnssecQueryResult = queryDnssec(q);
if (!dnssecQueryResult.isAuthenticData()) {
// TODO: Refine exception.
throw new IOException();
}
return dnssecQueryResult.dnsQueryResult;
}
public DnssecQueryResult queryDnssec(CharSequence name, TYPE type) throws IOException {
Question q = new Question(name, type, CLASS.IN);
return queryDnssec(q);
}
public DnssecQueryResult queryDnssec(Question q) throws IOException {
DnsQueryResult dnsQueryResult = super.query(q);
DnssecQueryResult dnssecQueryResult = performVerification(dnsQueryResult);
return dnssecQueryResult;
}
private DnssecQueryResult performVerification(DnsQueryResult dnsQueryResult) throws IOException {
if (dnsQueryResult == null) return null;
DnsMessage dnsMessage = dnsQueryResult.response;
DnsMessage.Builder messageBuilder = dnsMessage.asBuilder();
Set<DnssecUnverifiedReason> unverifiedReasons = verify(dnsMessage);
messageBuilder.setAuthenticData(unverifiedReasons.isEmpty());
List<Record<? extends Data>> answers = dnsMessage.answerSection;
List<Record<? extends Data>> nameserverRecords = dnsMessage.authoritySection;
List<Record<? extends Data>> additionalResourceRecords = dnsMessage.additionalSection;
Set<Record<RRSIG>> signatures = new HashSet<>();
Record.filter(signatures, RRSIG.class, answers);
Record.filter(signatures, RRSIG.class, nameserverRecords);
Record.filter(signatures, RRSIG.class, additionalResourceRecords);
if (stripSignatureRecords) {
messageBuilder.setAnswers(stripSignatureRecords(answers));
messageBuilder.setNameserverRecords(stripSignatureRecords(nameserverRecords));
messageBuilder.setAdditionalResourceRecords(stripSignatureRecords(additionalResourceRecords));
}
return new DnssecQueryResult(messageBuilder.build(), dnsQueryResult, signatures, unverifiedReasons);
}
private static List<Record<? extends Data>> stripSignatureRecords(List<Record<? extends Data>> records) {
if (records.isEmpty()) return records;
List<Record<? extends Data>> recordList = new ArrayList<>(records.size());
for (Record<? extends Data> record : records) {
if (record.type != TYPE.RRSIG) {
recordList.add(record);
}
}
return recordList;
}
private Set<DnssecUnverifiedReason> verify(DnsMessage dnsMessage) throws IOException {
if (!dnsMessage.answerSection.isEmpty()) {
return verifyAnswer(dnsMessage);
} else {
return verifyNsec(dnsMessage);
}
}
private Set<DnssecUnverifiedReason> verifyAnswer(DnsMessage dnsMessage) throws IOException {
Question q = dnsMessage.questions.get(0);
List<Record<? extends Data>> answers = dnsMessage.answerSection;
List<Record<? extends Data>> toBeVerified = dnsMessage.copyAnswers();
VerifySignaturesResult verifiedSignatures = verifySignatures(q, answers, toBeVerified);
Set<DnssecUnverifiedReason> result = verifiedSignatures.reasons;
if (!result.isEmpty()) {
return result;
}
// Keep SEPs separated, we only need one valid SEP.
boolean sepSignatureValid = false;
Set<DnssecUnverifiedReason> sepReasons = new HashSet<>();
for (Iterator<Record<? extends Data>> iterator = toBeVerified.iterator(); iterator.hasNext(); ) {
Record<DNSKEY> record = iterator.next().ifPossibleAs(DNSKEY.class);
if (record == null) {
continue;
}
// Verify all DNSKEYs as if it was a SEP. If we find a single SEP we are safe.
Set<DnssecUnverifiedReason> reasons = verifySecureEntryPoint(record);
if (reasons.isEmpty()) {
sepSignatureValid = true;
} else {
sepReasons.addAll(reasons);
}
if (!verifiedSignatures.sepSignaturePresent) {
LOGGER.finer("SEP key is not self-signed.");
}
iterator.remove();
}
if (verifiedSignatures.sepSignaturePresent && !sepSignatureValid) {
result.addAll(sepReasons);
}
if (verifiedSignatures.sepSignatureRequired && !verifiedSignatures.sepSignaturePresent) {
result.add(new NoSecureEntryPointReason(q.name));
}
if (!toBeVerified.isEmpty()) {
if (toBeVerified.size() != answers.size()) {
throw new DnssecValidationFailedException(q, "Only some records are signed!");
} else {
result.add(new NoSignaturesReason(q));
}
}
return result;
}
private Set<DnssecUnverifiedReason> verifyNsec(DnsMessage dnsMessage) throws IOException {
Set<DnssecUnverifiedReason> result = new HashSet<>();
Question q = dnsMessage.questions.get(0);
boolean validNsec = false;
boolean nsecPresent = false;
// Get the SOA RR that has to be in the authority section. Note that we will verify its signature later, after
// we have verified the NSEC3 RR. And although the data form the SOA RR is only required for NSEC3 we check for
// its existence here, since it would be invalid if there is none.
// TODO: Add a reference to the relevant RFC parts which specify that there has to be a SOA RR in X.
DnsName zone = null;
List<Record<? extends Data>> authoritySection = dnsMessage.authoritySection;
for (Record<? extends Data> authorityRecord : authoritySection) {
if (authorityRecord.type == TYPE.SOA) {
zone = authorityRecord.name;
break;
}
}
if (zone == null)
throw new AuthorityDoesNotContainSoa(dnsMessage);
// TODO Examine if it is better to verify the RRs in the authority section *before* we verify NSEC(3). We
// currently do it the other way around.
// TODO: This whole logic needs to be changed. It currently checks one NSEC(3) record after another, when it
// should first determine if we are dealing with NSEC or NSEC3 and the verify the whole response.
for (Record<? extends Data> record : authoritySection) {
DnssecUnverifiedReason reason;
switch (record.type) {
case NSEC:
nsecPresent = true;
Record<NSEC> nsecRecord = record.as(NSEC.class);
reason = Verifier.verifyNsec(nsecRecord, q);
break;
case NSEC3:
nsecPresent = true;
Record<NSEC3> nsec3Record = record.as(NSEC3.class);
reason = Verifier.verifyNsec3(zone, nsec3Record, q);
break;
default:
continue;
}
if (reason != null) {
result.add(reason);
} else {
validNsec = true;
}
}
// TODO: Shouldn't we also throw if !nsecPresent?
if (nsecPresent && !validNsec) {
throw new DnssecValidationFailedException(q, "Invalid NSEC!");
}
List<Record<? extends Data>> toBeVerified = dnsMessage.copyAuthority();
VerifySignaturesResult verifiedSignatures = verifySignatures(q, authoritySection, toBeVerified);
if (validNsec && verifiedSignatures.reasons.isEmpty()) {
result.clear();
} else {
result.addAll(verifiedSignatures.reasons);
}
if (!toBeVerified.isEmpty() && toBeVerified.size() != authoritySection.size()) {
// TODO Refine this exception and include the missing toBeVerified RRs and the whole DnsMessage into it.
throw new DnssecValidationFailedException(q, "Only some resource records from the authority section are signed!");
}
return result;
}
private static final class VerifySignaturesResult {
boolean sepSignatureRequired = false;
boolean sepSignaturePresent = false;
Set<DnssecUnverifiedReason> reasons = new HashSet<>();
}
@SuppressWarnings("JavaUtilDate")
private VerifySignaturesResult verifySignatures(Question q, Collection<Record<? extends Data>> reference, List<Record<? extends Data>> toBeVerified) throws IOException {
final Date now = new Date();
final List<RRSIG> outdatedRrSigs = new ArrayList<>();
VerifySignaturesResult result = new VerifySignaturesResult();
final List<Record<RRSIG>> rrsigs = new ArrayList<>(toBeVerified.size());
for (Record<? extends Data> recordToBeVerified : toBeVerified) {
Record<RRSIG> record = recordToBeVerified.ifPossibleAs(RRSIG.class);
if (record == null) continue;
RRSIG rrsig = record.payloadData;
if (rrsig.signatureExpiration.compareTo(now) < 0 || rrsig.signatureInception.compareTo(now) > 0) {
// This RRSIG is out of date, but there might be one that is not.
outdatedRrSigs.add(rrsig);
continue;
}
rrsigs.add(record);
}
if (rrsigs.isEmpty()) {
if (!outdatedRrSigs.isEmpty()) {
result.reasons.add(new NoActiveSignaturesReason(q, outdatedRrSigs));
} else {
// TODO: Check if QNAME results should have signatures and add a different reason if there are RRSIGs
// expected compared to when not.
result.reasons.add(new NoSignaturesReason(q));
}
return result;
}
for (Record<RRSIG> sigRecord : rrsigs) {
RRSIG rrsig = sigRecord.payloadData;
List<Record<? extends Data>> records = new ArrayList<>(reference.size());
for (Record<? extends Data> record : reference) {
if (record.type == rrsig.typeCovered && record.name.equals(sigRecord.name)) {
records.add(record);
}
}
Set<DnssecUnverifiedReason> reasons = verifySignedRecords(q, rrsig, records);
result.reasons.addAll(reasons);
if (q.name.equals(rrsig.signerName) && rrsig.typeCovered == TYPE.DNSKEY) {
for (Iterator<Record<? extends Data>> iterator = records.iterator(); iterator.hasNext(); ) {
Record<DNSKEY> dnsKeyRecord = iterator.next().ifPossibleAs(DNSKEY.class);
// dnsKeyRecord should never be null here.
DNSKEY dnskey = dnsKeyRecord.payloadData;
// DNSKEYs are verified separately, so don't mark them verified now.
iterator.remove();
if (dnskey.getKeyTag() == rrsig.keyTag) {
result.sepSignaturePresent = true;
}
}
// DNSKEY's should be signed by a SEP
result.sepSignatureRequired = true;
}
if (!isParentOrSelf(sigRecord.name.ace, rrsig.signerName.ace)) {
LOGGER.finer("Records at " + sigRecord.name + " are cross-signed with a key from " + rrsig.signerName);
} else {
toBeVerified.removeAll(records);
}
toBeVerified.remove(sigRecord);
}
return result;
}
private static boolean isParentOrSelf(String child, String parent) {
if (child.equals(parent)) return true;
if (parent.isEmpty()) return true;
String[] childSplit = child.split("\\.");
String[] parentSplit = parent.split("\\.");
if (parentSplit.length > childSplit.length) return false;
for (int i = 1; i <= parentSplit.length; i++) {
if (!parentSplit[parentSplit.length - i].equals(childSplit[childSplit.length - i])) {
return false;
}
}
return true;
}
private Set<DnssecUnverifiedReason> verifySignedRecords(Question q, RRSIG rrsig, List<Record<? extends Data>> records) throws IOException {
Set<DnssecUnverifiedReason> result = new HashSet<>();
DNSKEY dnskey = null;
if (rrsig.typeCovered == TYPE.DNSKEY) {
// Key must be present
List<Record<DNSKEY>> dnskeyRrs = Record.filter(DNSKEY.class, records);
for (Record<DNSKEY> dnsKeyRecord : dnskeyRrs) {
if (dnsKeyRecord.payloadData.getKeyTag() == rrsig.keyTag) {
dnskey = dnsKeyRecord.payloadData;
break;
}
}
} else if (q.type == TYPE.DS && rrsig.signerName.equals(q.name)) {
// We should not probe for the self signed DS negative response, as it will be an endless loop.
result.add(new NoTrustAnchorReason(q.name));
return result;
} else {
DnssecQueryResult dnskeyRes = queryDnssec(rrsig.signerName, TYPE.DNSKEY);
result.addAll(dnskeyRes.getUnverifiedReasons());
List<Record<DNSKEY>> dnskeyRrs = dnskeyRes.dnsQueryResult.response.filterAnswerSectionBy(DNSKEY.class);
for (Record<DNSKEY> dnsKeyRecord : dnskeyRrs) {
if (dnsKeyRecord.payloadData.getKeyTag() == rrsig.keyTag) {
dnskey = dnsKeyRecord.payloadData;
break;
}
}
}
if (dnskey == null) {
throw new DnssecValidationFailedException(q, records.size() + " " + rrsig.typeCovered + " record(s) are signed using an unknown key.");
}
DnssecUnverifiedReason unverifiedReason = Verifier.verify(records, rrsig, dnskey);
if (unverifiedReason != null) {
result.add(unverifiedReason);
}
return result;
}
private Set<DnssecUnverifiedReason> verifySecureEntryPoint(final Record<DNSKEY> sepRecord) throws IOException {
final DNSKEY dnskey = sepRecord.payloadData;
Set<DnssecUnverifiedReason> unverifiedReasons = new HashSet<>();
Set<DnssecUnverifiedReason> activeReasons = new HashSet<>();
if (knownSeps.containsKey(sepRecord.name)) {
if (dnskey.keyEquals(knownSeps.get(sepRecord.name))) {
return unverifiedReasons;
} else {
unverifiedReasons.add(new DnssecUnverifiedReason.ConflictsWithSep(sepRecord));
return unverifiedReasons;
}
}
// If we are looking for the SEP of the root zone at this point, then the client was not
// configured with one. Hence we can abort and state the reason why we aborted.
if (sepRecord.name.isRootLabel()) {
unverifiedReasons.add(new DnssecUnverifiedReason.NoRootSecureEntryPointReason());
return unverifiedReasons;
}
DelegatingDnssecRR delegation = null;
DnssecQueryResult dsResp = queryDnssec(sepRecord.name, TYPE.DS);
unverifiedReasons.addAll(dsResp.getUnverifiedReasons());
List<Record<DS>> dsRrs = dsResp.dnsQueryResult.response.filterAnswerSectionBy(DS.class);
for (Record<DS> dsRecord : dsRrs) {
DS ds = dsRecord.payloadData;
if (dnskey.getKeyTag() == ds.keyTag) {
delegation = ds;
activeReasons = dsResp.getUnverifiedReasons();
break;
}
}
if (delegation == null) {
LOGGER.fine("There is no DS record for \'" + sepRecord.name + "\', server gives empty result");
}
if (delegation == null && dlv != null && !dlv.isChildOf(sepRecord.name)) {
DnssecQueryResult dlvResp = queryDnssec(DnsName.from(sepRecord.name, dlv), TYPE.DLV);
unverifiedReasons.addAll(dlvResp.getUnverifiedReasons());
List<Record<DLV>> dlvRrs = dlvResp.dnsQueryResult.response.filterAnswerSectionBy(DLV.class);
for (Record<DLV> dlvRecord : dlvRrs) {
if (sepRecord.payloadData.getKeyTag() == dlvRecord.payloadData.keyTag) {
LOGGER.fine("Found DLV for " + sepRecord.name + ", awesome.");
delegation = dlvRecord.payloadData;
activeReasons = dlvResp.getUnverifiedReasons();
break;
}
}
}
if (delegation != null) {
DnssecUnverifiedReason unverifiedReason = Verifier.verify(sepRecord, delegation);
if (unverifiedReason != null) {
unverifiedReasons.add(unverifiedReason);
} else {
unverifiedReasons = activeReasons;
}
} else if (unverifiedReasons.isEmpty()) {
unverifiedReasons.add(new NoTrustAnchorReason(sepRecord.name));
}
return unverifiedReasons;
}
@Override
protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) {
message.getEdnsBuilder().setUdpPayloadSize(dataSource.getUdpPayloadSize()).setDnssecOk();
message.setCheckingDisabled(true);
return super.newQuestion(message);
}
@Override
protected String isResponseAcceptable(DnsMessage response) {
boolean dnssecOk = response.isDnssecOk();
if (!dnssecOk) {
// This is a deliberate violation of RFC 6840 § 5.6. I doubt that
// "resolvers MUST ignore the DO bit in responses" does any good. Also we basically ignore the DO bit after
// the fall back to iterative mode.
return "DNSSEC OK (DO) flag not set in response";
}
boolean checkingDisabled = response.checkingDisabled;
if (!checkingDisabled) {
return "CHECKING DISABLED (CD) flag not set in response";
}
return super.isResponseAcceptable(response);
}
/**
* Add a new secure entry point to the list of known secure entry points.
*
* A secure entry point acts as a trust anchor. By default, the only secure entry point is the key signing key
* provided by the root zone.
*
* @param name The domain name originating the key. Once the secure entry point for this domain is requested,
* the resolver will use this key without further verification instead of using the DNS system to
* verify the key.
* @param key The secure entry point corresponding to the domain name. This key can be retrieved by requesting
* the DNSKEY record for the domain and using the key with first flags bit set
* (also called key signing key)
*/
public final void addSecureEntryPoint(DnsName name, byte[] key) {
knownSeps.put(name, key);
}
/**
* Remove the secure entry point stored for a domain name.
*
* @param name The domain name of which the corresponding secure entry point shall be removed. For the root zone,
* use the empty string here.
*/
public void removeSecureEntryPoint(DnsName name) {
knownSeps.remove(name);
}
/**
* Clears the list of known secure entry points.
*
* This will also remove the secure entry point of the root zone and
* thus render this instance useless until a new secure entry point is added.
*/
public void clearSecureEntryPoints() {
knownSeps.clear();
}
/**
* Whether signature records (RRSIG) are stripped from the resulting {@link DnsMessage}.
*
* Default is {@code true}.
*
* @return Whether signature records are stripped.
*/
public boolean isStripSignatureRecords() {
return stripSignatureRecords;
}
/**
* Enable or disable stripping of signature records (RRSIG) from the result {@link DnsMessage}.
* @param stripSignatureRecords Whether signature records shall be stripped.
*/
public void setStripSignatureRecords(boolean stripSignatureRecords) {
this.stripSignatureRecords = stripSignatureRecords;
}
/**
* Enables DNSSEC Lookaside Validation (DLV) using the default DLV service at dlv.isc.org.
*/
public void enableLookasideValidation() {
configureLookasideValidation(DEFAULT_DLV);
}
/**
* Disables DNSSEC Lookaside Validation (DLV).
* DLV is disabled by default, this is only required if {@link #enableLookasideValidation()} was used before.
*/
public void disableLookasideValidation() {
configureLookasideValidation(null);
}
/**
* Enables DNSSEC Lookaside Validation (DLV) using the given DLV service.
*
* @param dlv The domain name of the DLV service to be used or {@code null} to disable DLV.
*/
public void configureLookasideValidation(DnsName dlv) {
this.dlv = dlv;
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec;
import java.util.Collections;
import java.util.Set;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.record.RRSIG;
import org.minidns.record.Record;
public class DnssecQueryResult {
public final DnsMessage synthesizedResponse;
public final DnsQueryResult dnsQueryResult;
private final Set<Record<RRSIG>> signatures;
private final Set<DnssecUnverifiedReason> dnssecUnverifiedReasons;
DnssecQueryResult(DnsMessage synthesizedResponse, DnsQueryResult dnsQueryResult, Set<Record<RRSIG>> signatures,
Set<DnssecUnverifiedReason> dnssecUnverifiedReasons) {
this.synthesizedResponse = synthesizedResponse;
this.dnsQueryResult = dnsQueryResult;
this.signatures = Collections.unmodifiableSet(signatures);
if (dnssecUnverifiedReasons == null) {
this.dnssecUnverifiedReasons = Collections.emptySet();
} else {
this.dnssecUnverifiedReasons = Collections.unmodifiableSet(dnssecUnverifiedReasons);
}
}
public boolean isAuthenticData() {
return dnssecUnverifiedReasons.isEmpty();
}
public Set<Record<RRSIG>> getSignatures() {
return signatures;
}
public Set<DnssecUnverifiedReason> getUnverifiedReasons() {
return dnssecUnverifiedReasons;
}
}

View file

@ -0,0 +1,48 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec;
import java.util.Collections;
import java.util.Set;
import org.minidns.MiniDnsException;
public final class DnssecResultNotAuthenticException extends MiniDnsException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final Set<DnssecUnverifiedReason> unverifiedReasons;
private DnssecResultNotAuthenticException(String message, Set<DnssecUnverifiedReason> unverifiedReasons) {
super(message);
if (unverifiedReasons.isEmpty()) {
throw new IllegalArgumentException();
}
this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons);
}
public static DnssecResultNotAuthenticException from(Set<DnssecUnverifiedReason> unverifiedReasons) {
StringBuilder sb = new StringBuilder();
sb.append("DNSSEC result not authentic. Reasons: ");
for (DnssecUnverifiedReason reason : unverifiedReasons) {
sb.append(reason).append('.');
}
return new DnssecResultNotAuthenticException(sb.toString(), unverifiedReasons);
}
public Set<DnssecUnverifiedReason> getUnverifiedReasons() {
return unverifiedReasons;
}
}

View file

@ -0,0 +1,175 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec;
import java.util.Collections;
import java.util.List;
import org.minidns.constants.DnssecConstants.DigestAlgorithm;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.record.DNSKEY;
import org.minidns.record.Data;
import org.minidns.record.RRSIG;
import org.minidns.record.Record;
import org.minidns.record.Record.TYPE;
public abstract class DnssecUnverifiedReason {
public abstract String getReasonString();
@Override
public String toString() {
return getReasonString();
}
@Override
public int hashCode() {
return getReasonString().hashCode();
}
@Override
public boolean equals(Object obj) {
return obj instanceof DnssecUnverifiedReason && ((DnssecUnverifiedReason) obj).getReasonString().equals(getReasonString());
}
public static class AlgorithmNotSupportedReason extends DnssecUnverifiedReason {
private final String algorithm;
private final TYPE type;
private final Record<? extends Data> record;
public AlgorithmNotSupportedReason(byte algorithm, TYPE type, Record<? extends Data> record) {
this.algorithm = Integer.toString(algorithm & 0xff);
this.type = type;
this.record = record;
}
@Override
public String getReasonString() {
return type.name() + " algorithm " + algorithm + " required to verify " + record.name + " is unknown or not supported by platform";
}
}
public static class AlgorithmExceptionThrownReason extends DnssecUnverifiedReason {
private final int algorithmNumber;
private final String kind;
private final Exception reason;
private final Record<? extends Data> record;
public AlgorithmExceptionThrownReason(DigestAlgorithm algorithm, String kind, Record<? extends Data> record, Exception reason) {
this.algorithmNumber = algorithm.value;
this.kind = kind;
this.record = record;
this.reason = reason;
}
@Override
public String getReasonString() {
return kind + " algorithm " + algorithmNumber + " threw exception while verifying " + record.name + ": " + reason;
}
}
public static class ConflictsWithSep extends DnssecUnverifiedReason {
private final Record<DNSKEY> record;
public ConflictsWithSep(Record<DNSKEY> record) {
this.record = record;
}
@Override
public String getReasonString() {
return "Zone " + record.name.ace + " is in list of known SEPs, but DNSKEY from response mismatches!";
}
}
public static class NoTrustAnchorReason extends DnssecUnverifiedReason {
private final DnsName zone;
public NoTrustAnchorReason(DnsName zone) {
this.zone = zone;
}
@Override
public String getReasonString() {
return "No trust anchor was found for zone " + zone + ". Try enabling DLV";
}
}
public static class NoSecureEntryPointReason extends DnssecUnverifiedReason {
private final DnsName zone;
public NoSecureEntryPointReason(DnsName zone) {
this.zone = zone;
}
@Override
public String getReasonString() {
return "No secure entry point was found for zone " + zone;
}
}
public static class NoRootSecureEntryPointReason extends DnssecUnverifiedReason {
public NoRootSecureEntryPointReason() {
}
@Override
public String getReasonString() {
return "No secure entry point was found for the root zone (\"Did you forget to configure a root SEP?\")";
}
}
public static class NoSignaturesReason extends DnssecUnverifiedReason {
private final Question question;
public NoSignaturesReason(Question question) {
this.question = question;
}
@Override
public String getReasonString() {
return "No signatures were attached to answer on question for " + question.type + " at " + question.name;
}
}
public static class NoActiveSignaturesReason extends DnssecUnverifiedReason {
private final Question question;
private final List<RRSIG> outdatedRrSigs;
public NoActiveSignaturesReason(Question question, List<RRSIG> outdatedRrSigs) {
this.question = question;
assert !outdatedRrSigs.isEmpty();
this.outdatedRrSigs = Collections.unmodifiableList(outdatedRrSigs);
}
@Override
public String getReasonString() {
return "No currently active signatures were attached to answer on question for " + question.type + " at " + question.name;
}
public List<RRSIG> getOutdatedRrSigs() {
return outdatedRrSigs;
}
}
public static class NSECDoesNotMatchReason extends DnssecUnverifiedReason {
private final Question question;
private final Record<? extends Data> record;
public NSECDoesNotMatchReason(Question question, Record<? extends Data> record) {
this.question = question;
this.record = record;
}
@Override
public String getReasonString() {
return "NSEC " + record.name + " does nat match question for " + question.type + " at " + question.name;
}
}
}

View file

@ -0,0 +1,153 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.Question;
import org.minidns.record.Data;
import org.minidns.record.DelegatingDnssecRR;
import org.minidns.record.Record;
import java.io.IOException;
import java.math.BigInteger;
import java.security.spec.InvalidKeySpecException;
import java.util.List;
import java.util.Locale;
public class DnssecValidationFailedException extends IOException {
private static final long serialVersionUID = 5413184667629832742L;
public DnssecValidationFailedException(Question question, String reason) {
super("Validation of request to " + question + " failed: " + reason);
}
public DnssecValidationFailedException(String message) {
super(message);
}
public DnssecValidationFailedException(String message, Throwable cause) {
super(message, cause);
}
public DnssecValidationFailedException(Record<? extends Data> record, String reason) {
super("Validation of record " + record + " failed: " + reason);
}
public DnssecValidationFailedException(List<Record<? extends Data>> records, String reason) {
super("Validation of " + records.size() + " " + records.get(0).type + " record" + (records.size() > 1 ? "s" : "") + " failed: " + reason);
}
public static class DataMalformedException extends DnssecValidationFailedException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final byte[] data;
public DataMalformedException(IOException exception, byte[] data) {
super("Malformed data", exception);
this.data = data;
}
public DataMalformedException(String message, IOException exception, byte[] data) {
super(message, exception);
this.data = data;
}
public byte[] getData() {
return data;
}
}
public static class DnssecInvalidKeySpecException extends DnssecValidationFailedException {
/**
*
*/
private static final long serialVersionUID = 1L;
public DnssecInvalidKeySpecException(InvalidKeySpecException exception) {
super("Invalid key spec", exception);
}
public DnssecInvalidKeySpecException(String message, InvalidKeySpecException exception, byte[] data) {
super(message, exception);
}
}
public static class AuthorityDoesNotContainSoa extends DnssecValidationFailedException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final DnsMessage response;
public AuthorityDoesNotContainSoa(DnsMessage response) {
super("Autority does not contain SOA");
this.response = response;
}
public DnsMessage getResponse() {
return response;
}
}
public static final class DigestComparisonFailedException extends DnssecValidationFailedException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final Record<? extends Data> record;
private final DelegatingDnssecRR ds;
private final byte[] digest;
private final String digestHex;
private DigestComparisonFailedException(String message, Record<? extends Data> record, DelegatingDnssecRR ds, byte[] digest, String digestHex) {
super(message);
this.record = record;
this.ds = ds;
this.digest = digest;
this.digestHex = digestHex;
}
public Record<? extends Data> getRecord() {
return record;
}
public DelegatingDnssecRR getDelegaticDnssecRr() {
return ds;
}
public byte[] getDigest() {
return digest.clone();
}
public String getDigestHex() {
return digestHex;
}
public static DigestComparisonFailedException from(Record<? extends Data> record, DelegatingDnssecRR ds, byte[] digest) {
BigInteger digestBigInteger = new BigInteger(1, digest);
String digestHex = digestBigInteger.toString(16).toUpperCase(Locale.ROOT);
String message = "Digest for " + record + " does not match. Digest of delegating DNSSEC RR " + ds + " is '"
+ ds.getDigestHex() + "' while we calculated '" + digestHex + "'";
return new DigestComparisonFailedException(message, record, ds, digest, digestHex);
}
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec;
public class DnssecValidatorInitializationException extends RuntimeException {
private static final long serialVersionUID = -1464257268053507791L;
public DnssecValidatorInitializationException(String message, Throwable cause) {
super(message, cause);
}
}

View file

@ -0,0 +1,18 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec;
import org.minidns.record.DNSKEY;
import org.minidns.record.RRSIG;
public interface SignatureVerifier {
boolean verify(byte[] content, RRSIG rrsig, DNSKEY key) throws DnssecValidationFailedException;
}

View file

@ -0,0 +1,219 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec;
import org.minidns.dnslabel.DnsLabel;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnssec.DnssecUnverifiedReason.AlgorithmExceptionThrownReason;
import org.minidns.dnssec.DnssecUnverifiedReason.AlgorithmNotSupportedReason;
import org.minidns.dnssec.DnssecUnverifiedReason.NSECDoesNotMatchReason;
import org.minidns.dnssec.DnssecValidationFailedException.DigestComparisonFailedException;
import org.minidns.dnssec.algorithms.AlgorithmMap;
import org.minidns.record.DNSKEY;
import org.minidns.record.Data;
import org.minidns.record.DelegatingDnssecRR;
import org.minidns.record.NSEC;
import org.minidns.record.NSEC3;
import org.minidns.record.RRSIG;
import org.minidns.record.Record;
import org.minidns.util.Base32;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
class Verifier {
private static final AlgorithmMap algorithmMap = AlgorithmMap.INSTANCE;
public static DnssecUnverifiedReason verify(Record<DNSKEY> dnskeyRecord, DelegatingDnssecRR ds) throws DnssecValidationFailedException {
DNSKEY dnskey = dnskeyRecord.payloadData;
DigestCalculator digestCalculator = algorithmMap.getDsDigestCalculator(ds.digestType);
if (digestCalculator == null) {
return new AlgorithmNotSupportedReason(ds.digestTypeByte, ds.getType(), dnskeyRecord);
}
byte[] dnskeyData = dnskey.toByteArray();
byte[] dnskeyOwner = dnskeyRecord.name.getBytes();
byte[] combined = new byte[dnskeyOwner.length + dnskeyData.length];
System.arraycopy(dnskeyOwner, 0, combined, 0, dnskeyOwner.length);
System.arraycopy(dnskeyData, 0, combined, dnskeyOwner.length, dnskeyData.length);
byte[] digest;
try {
digest = digestCalculator.digest(combined);
} catch (Exception e) {
return new AlgorithmExceptionThrownReason(ds.digestType, "DS", dnskeyRecord, e);
}
if (!ds.digestEquals(digest)) {
throw DigestComparisonFailedException.from(dnskeyRecord, ds, digest);
}
return null;
}
public static DnssecUnverifiedReason verify(List<Record<? extends Data>> records, RRSIG rrsig, DNSKEY key) throws IOException {
SignatureVerifier signatureVerifier = algorithmMap.getSignatureVerifier(rrsig.algorithm);
if (signatureVerifier == null) {
return new AlgorithmNotSupportedReason(rrsig.algorithmByte, rrsig.getType(), records.get(0));
}
byte[] combine = combine(rrsig, records);
if (signatureVerifier.verify(combine, rrsig, key)) {
return null;
} else {
throw new DnssecValidationFailedException(records, "Signature is invalid.");
}
}
public static DnssecUnverifiedReason verifyNsec(Record<NSEC> nsecRecord, Question q) {
NSEC nsec = nsecRecord.payloadData;
if (nsecRecord.name.equals(q.name) && !nsec.types.contains(q.type)) {
// records with same name but different types exist
return null;
} else if (nsecMatches(q.name, nsecRecord.name, nsec.next)) {
return null;
}
return new NSECDoesNotMatchReason(q, nsecRecord);
}
public static DnssecUnverifiedReason verifyNsec3(DnsName zone, Record<NSEC3> nsec3record, Question q) {
NSEC3 nsec3 = nsec3record.payloadData;
DigestCalculator digestCalculator = algorithmMap.getNsecDigestCalculator(nsec3.hashAlgorithm);
if (digestCalculator == null) {
return new AlgorithmNotSupportedReason(nsec3.hashAlgorithmByte, nsec3.getType(), nsec3record);
}
byte[] bytes = nsec3hash(digestCalculator, nsec3, q.name, nsec3.iterations);
String s = Base32.encodeToString(bytes);
DnsName computedNsec3Record = DnsName.from(s + "." + zone);
if (nsec3record.name.equals(computedNsec3Record)) {
if (nsec3.types.contains(q.type)) {
// TODO: Refine exception thrown in this case.
return new NSECDoesNotMatchReason(q, nsec3record);
}
return null;
}
if (nsecMatches(s, nsec3record.name.getHostpart(), Base32.encodeToString(nsec3.getNextHashed()))) {
return null;
}
return new NSECDoesNotMatchReason(q, nsec3record);
}
static byte[] combine(RRSIG rrsig, List<Record<? extends Data>> records) {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
// Write RRSIG without signature
try {
rrsig.writePartialSignature(dos);
DnsName sigName = records.get(0).name;
if (!sigName.isRootLabel()) {
if (sigName.getLabelCount() < rrsig.labels) {
// TODO: This is currently not covered by the unit tests.
throw new DnssecValidationFailedException("Invalid RRsig record");
}
if (sigName.getLabelCount() > rrsig.labels) {
// TODO: This is currently not covered by the unit tests.
// Expand wildcards
sigName = DnsName.from(DnsLabel.WILDCARD_LABEL, sigName.stripToLabels(rrsig.labels));
}
}
List<byte[]> recordBytes = new ArrayList<>(records.size());
for (Record<? extends Data> record : records) {
Record<Data> ref = new Record<Data>(sigName, record.type, record.clazzValue, rrsig.originalTtl, record.payloadData);
recordBytes.add(ref.toByteArray());
}
// Sort correctly (cause they might be ordered randomly) as per RFC 4034 § 6.3.
final int offset = sigName.size() + 10; // Where the RDATA begins
Collections.sort(recordBytes, new Comparator<byte[]>() {
@Override
public int compare(byte[] b1, byte[] b2) {
for (int i = offset; i < b1.length && i < b2.length; i++) {
if (b1[i] != b2[i]) {
return (b1[i] & 0xFF) - (b2[i] & 0xFF);
}
}
return b1.length - b2.length;
}
});
for (byte[] recordByte : recordBytes) {
dos.write(recordByte);
}
dos.flush();
} catch (IOException e) {
// Never happens
throw new RuntimeException(e);
}
return bos.toByteArray();
}
static boolean nsecMatches(String test, String lowerBound, String upperBound) {
return nsecMatches(DnsName.from(test), DnsName.from(lowerBound), DnsName.from(upperBound));
}
/**
* Tests if a nsec domain name is part of an NSEC record.
*
* @param test test domain name
* @param lowerBound inclusive lower bound
* @param upperBound exclusive upper bound
* @return test domain name is covered by NSEC record
*/
static boolean nsecMatches(DnsName test, DnsName lowerBound, DnsName upperBound) {
int lowerParts = lowerBound.getLabelCount();
int upperParts = upperBound.getLabelCount();
int testParts = test.getLabelCount();
if (testParts > lowerParts && !test.isChildOf(lowerBound) && test.stripToLabels(lowerParts).compareTo(lowerBound) < 0)
return false;
if (testParts <= lowerParts && test.compareTo(lowerBound.stripToLabels(testParts)) < 0)
return false;
if (testParts > upperParts && !test.isChildOf(upperBound) && test.stripToLabels(upperParts).compareTo(upperBound) > 0)
return false;
if (testParts <= upperParts && test.compareTo(upperBound.stripToLabels(testParts)) >= 0)
return false;
return true;
}
static byte[] nsec3hash(DigestCalculator digestCalculator, NSEC3 nsec3, DnsName ownerName, int iterations) {
return nsec3hash(digestCalculator, nsec3.getSalt(), ownerName.getBytes(), iterations);
}
/**
* Derived from RFC 5155 Section 5.
*
* @param digestCalculator the digest calculator.
* @param salt the salt.
* @param data the data.
* @param iterations the number of iterations.
* @return the NSEC3 hash.
*/
static byte[] nsec3hash(DigestCalculator digestCalculator, byte[] salt, byte[] data, int iterations) {
while (iterations-- >= 0) {
byte[] combined = new byte[data.length + salt.length];
System.arraycopy(data, 0, combined, 0, data.length);
System.arraycopy(salt, 0, combined, data.length, salt.length);
data = digestCalculator.digest(combined);
}
return data;
}
}

View file

@ -0,0 +1,122 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec.algorithms;
import org.minidns.constants.DnssecConstants.DigestAlgorithm;
import org.minidns.constants.DnssecConstants.SignatureAlgorithm;
import org.minidns.dnssec.DnssecValidatorInitializationException;
import org.minidns.dnssec.DigestCalculator;
import org.minidns.dnssec.SignatureVerifier;
import org.minidns.record.NSEC3.HashAlgorithm;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
public final class AlgorithmMap {
private Logger LOGGER = Logger.getLogger(AlgorithmMap.class.getName());
public static final AlgorithmMap INSTANCE = new AlgorithmMap();
private final Map<DigestAlgorithm, DigestCalculator> dsDigestMap = new HashMap<>();
private final Map<SignatureAlgorithm, SignatureVerifier> signatureMap = new HashMap<>();
private final Map<HashAlgorithm, DigestCalculator> nsecDigestMap = new HashMap<>();
@SuppressWarnings("deprecation")
private AlgorithmMap() {
try {
dsDigestMap.put(DigestAlgorithm.SHA1, new JavaSecDigestCalculator("SHA-1"));
nsecDigestMap.put(HashAlgorithm.SHA1, new JavaSecDigestCalculator("SHA-1"));
} catch (NoSuchAlgorithmException e) {
// SHA-1 is MANDATORY
throw new DnssecValidatorInitializationException("SHA-1 is mandatory", e);
}
try {
dsDigestMap.put(DigestAlgorithm.SHA256, new JavaSecDigestCalculator("SHA-256"));
} catch (NoSuchAlgorithmException e) {
// SHA-256 is MANDATORY
throw new DnssecValidatorInitializationException("SHA-256 is mandatory", e);
}
try {
dsDigestMap.put(DigestAlgorithm.SHA384, new JavaSecDigestCalculator("SHA-384"));
} catch (NoSuchAlgorithmException e) {
// SHA-384 is OPTIONAL
LOGGER.log(Level.FINE, "Platform does not support SHA-384", e);
}
try {
signatureMap.put(SignatureAlgorithm.RSAMD5, new RsaSignatureVerifier("MD5withRSA"));
} catch (NoSuchAlgorithmException e) {
// RSA/MD5 is DEPRECATED
LOGGER.log(Level.FINER, "Platform does not support RSA/MD5", e);
}
try {
DsaSignatureVerifier sha1withDSA = new DsaSignatureVerifier("SHA1withDSA");
signatureMap.put(SignatureAlgorithm.DSA, sha1withDSA);
signatureMap.put(SignatureAlgorithm.DSA_NSEC3_SHA1, sha1withDSA);
} catch (NoSuchAlgorithmException e) {
// DSA/SHA-1 is OPTIONAL
LOGGER.log(Level.FINE, "Platform does not support DSA/SHA-1", e);
}
try {
RsaSignatureVerifier sha1withRSA = new RsaSignatureVerifier("SHA1withRSA");
signatureMap.put(SignatureAlgorithm.RSASHA1, sha1withRSA);
signatureMap.put(SignatureAlgorithm.RSASHA1_NSEC3_SHA1, sha1withRSA);
} catch (NoSuchAlgorithmException e) {
throw new DnssecValidatorInitializationException("Platform does not support RSA/SHA-1", e);
}
try {
signatureMap.put(SignatureAlgorithm.RSASHA256, new RsaSignatureVerifier("SHA256withRSA"));
} catch (NoSuchAlgorithmException e) {
// RSA/SHA-256 is RECOMMENDED
LOGGER.log(Level.INFO, "Platform does not support RSA/SHA-256", e);
}
try {
signatureMap.put(SignatureAlgorithm.RSASHA512, new RsaSignatureVerifier("SHA512withRSA"));
} catch (NoSuchAlgorithmException e) {
// RSA/SHA-512 is RECOMMENDED
LOGGER.log(Level.INFO, "Platform does not support RSA/SHA-512", e);
}
try {
signatureMap.put(SignatureAlgorithm.ECC_GOST, new EcgostSignatureVerifier());
} catch (NoSuchAlgorithmException e) {
// GOST R 34.10-2001 is OPTIONAL
LOGGER.log(Level.FINE, "Platform does not support GOST R 34.10-2001", e);
}
try {
signatureMap.put(SignatureAlgorithm.ECDSAP256SHA256, new EcdsaSignatureVerifier.P256SHA256());
} catch (NoSuchAlgorithmException e) {
// ECDSA/SHA-256 is RECOMMENDED
LOGGER.log(Level.INFO, "Platform does not support ECDSA/SHA-256", e);
}
try {
signatureMap.put(SignatureAlgorithm.ECDSAP384SHA384, new EcdsaSignatureVerifier.P384SHA284());
} catch (NoSuchAlgorithmException e) {
// ECDSA/SHA-384 is RECOMMENDED
LOGGER.log(Level.INFO, "Platform does not support ECDSA/SHA-384", e);
}
}
public DigestCalculator getDsDigestCalculator(DigestAlgorithm algorithm) {
return dsDigestMap.get(algorithm);
}
public SignatureVerifier getSignatureVerifier(SignatureAlgorithm algorithm) {
return signatureMap.get(algorithm);
}
public DigestCalculator getNsecDigestCalculator(HashAlgorithm algorithm) {
return nsecDigestMap.get(algorithm);
}
}

View file

@ -0,0 +1,132 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec.algorithms;
import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException;
import org.minidns.record.DNSKEY;
import org.minidns.record.RRSIG;
import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
class DsaSignatureVerifier extends JavaSecSignatureVerifier {
private static final int LENGTH = 20;
DsaSignatureVerifier(String algorithm) throws NoSuchAlgorithmException {
super("DSA", algorithm);
}
@Override
protected byte[] getSignature(RRSIG rrsig) throws DataMalformedException {
DataInput dis = rrsig.getSignatureAsDataInputStream();
ByteArrayOutputStream bos;
try {
// Convert RFC 2536 to ASN.1
@SuppressWarnings("unused")
byte t = dis.readByte();
byte[] r = new byte[LENGTH];
dis.readFully(r);
int roff = 0;
final int rlen;
if (r[0] == 0) {
while (roff < LENGTH && r[roff] == 0) {
roff++;
}
rlen = r.length - roff;
} else if (r[0] < 0) {
rlen = r.length + 1;
} else {
rlen = r.length;
}
byte[] s = new byte[LENGTH];
dis.readFully(s);
int soff = 0;
final int slen;
if (s[0] == 0) {
while (soff < LENGTH && s[soff] == 0) {
soff++;
}
slen = s.length - soff;
} else if (s[0] < 0) {
slen = s.length + 1;
} else {
slen = s.length;
}
bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
dos.writeByte(0x30);
dos.writeByte(rlen + slen + 4);
dos.writeByte(0x2);
dos.writeByte(rlen);
if (rlen > LENGTH)
dos.writeByte(0);
dos.write(r, roff, LENGTH - roff);
dos.writeByte(0x2);
dos.writeByte(slen);
if (slen > LENGTH)
dos.writeByte(0);
dos.write(s, soff, LENGTH - soff);
} catch (IOException e) {
throw new DataMalformedException(e, rrsig.getSignature());
}
return bos.toByteArray();
}
@Override
protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException {
DataInput dis = key.getKeyAsDataInputStream();
BigInteger subPrime, prime, base, pubKey;
try {
int t = dis.readUnsignedByte();
byte[] subPrimeBytes = new byte[LENGTH];
dis.readFully(subPrimeBytes);
subPrime = new BigInteger(1, subPrimeBytes);
byte[] primeBytes = new byte[64 + t * 8];
dis.readFully(primeBytes);
prime = new BigInteger(1, primeBytes);
byte[] baseBytes = new byte[64 + t * 8];
dis.readFully(baseBytes);
base = new BigInteger(1, baseBytes);
byte[] pubKeyBytes = new byte[64 + t * 8];
dis.readFully(pubKeyBytes);
pubKey = new BigInteger(1, pubKeyBytes);
} catch (IOException e) {
throw new DataMalformedException(e, key.getKey());
}
try {
return getKeyFactory().generatePublic(new DSAPublicKeySpec(pubKey, prime, subPrime, base));
} catch (InvalidKeySpecException e) {
throw new DnssecInvalidKeySpecException(e);
}
}
}

View file

@ -0,0 +1,133 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec.algorithms;
import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException;
import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException;
import org.minidns.record.DNSKEY;
import org.minidns.record.RRSIG;
import java.io.ByteArrayOutputStream;
import java.io.DataInput;
import java.io.DataOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.EllipticCurve;
import java.security.spec.InvalidKeySpecException;
abstract class EcdsaSignatureVerifier extends JavaSecSignatureVerifier {
private final ECParameterSpec spec;
private final int length;
EcdsaSignatureVerifier(BigInteger[] spec, int length, String algorithm) throws NoSuchAlgorithmException {
this(new ECParameterSpec(new EllipticCurve(new ECFieldFp(spec[0]), spec[1], spec[2]), new ECPoint(spec[3], spec[4]), spec[5], 1), length, algorithm);
}
EcdsaSignatureVerifier(ECParameterSpec spec, int length, String algorithm) throws NoSuchAlgorithmException {
super("EC", algorithm);
this.length = length;
this.spec = spec;
}
@Override
protected byte[] getSignature(RRSIG rrsig) throws DataMalformedException {
DataInput dis = rrsig.getSignatureAsDataInputStream();
ByteArrayOutputStream bos = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(bos);
try {
byte[] r = new byte[length];
dis.readFully(r);
int rlen = (r[0] < 0) ? length + 1 : length;
byte[] s = new byte[length];
dis.readFully(s);
int slen = (s[0] < 0) ? length + 1 : length;
dos.writeByte(0x30);
dos.writeByte(rlen + slen + 4);
dos.writeByte(0x2);
dos.writeByte(rlen);
if (rlen > length) dos.writeByte(0);
dos.write(r);
dos.writeByte(0x2);
dos.writeByte(slen);
if (slen > length) dos.writeByte(0);
dos.write(s);
} catch (IOException e) {
throw new DataMalformedException(e, rrsig.getSignature());
}
return bos.toByteArray();
}
@Override
protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException {
DataInput dis = key.getKeyAsDataInputStream();
BigInteger x, y;
try {
byte[] xBytes = new byte[length];
dis.readFully(xBytes);
x = new BigInteger(1, xBytes);
byte[] yBytes = new byte[length];
dis.readFully(yBytes);
y = new BigInteger(1, yBytes);
} catch (IOException e) {
throw new DataMalformedException(e, key.getKey());
}
try {
return getKeyFactory().generatePublic(new ECPublicKeySpec(new ECPoint(x, y), spec));
} catch (InvalidKeySpecException e) {
throw new DnssecInvalidKeySpecException(e);
}
}
public static class P256SHA256 extends EcdsaSignatureVerifier {
private static BigInteger[] SPEC = {
new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16),
new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC", 16),
new BigInteger("5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B", 16),
new BigInteger("6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296", 16),
new BigInteger("4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5", 16),
new BigInteger("FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551", 16)
};
P256SHA256() throws NoSuchAlgorithmException {
super(SPEC, 32, "SHA256withECDSA");
}
}
public static class P384SHA284 extends EcdsaSignatureVerifier {
private static BigInteger[] SPEC = {
new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", 16),
new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC", 16),
new BigInteger("B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF", 16),
new BigInteger("AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7", 16),
new BigInteger("3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F", 16),
new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973", 16)
};
P384SHA284() throws NoSuchAlgorithmException {
super(SPEC, 48, "SHA384withECDSA");
}
}
}

View file

@ -0,0 +1,86 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec.algorithms;
import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException;
import org.minidns.record.DNSKEY;
import org.minidns.record.RRSIG;
import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException;
import java.io.DataInput;
import java.io.IOException;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.ECFieldFp;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.EllipticCurve;
import java.security.spec.InvalidKeySpecException;
class EcgostSignatureVerifier extends JavaSecSignatureVerifier {
private static final int LENGTH = 32;
private static final ECParameterSpec SPEC = new ECParameterSpec(
new EllipticCurve(
new ECFieldFp(new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD97", 16)),
new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFD94", 16),
new BigInteger("A6", 16)
),
new ECPoint(BigInteger.ONE, new BigInteger("8D91E471E0989CDA27DF505A453F2B7635294F2DDF23E3B122ACC99C9E9F1E14", 16)),
new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF6C611070995AD10045841B09B761B893", 16),
1
);
EcgostSignatureVerifier() throws NoSuchAlgorithmException {
super("ECGOST3410", "GOST3411withECGOST3410");
}
@Override
protected byte[] getSignature(RRSIG rrsig) {
return rrsig.getSignature();
}
@Override
protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException {
DataInput dis = key.getKeyAsDataInputStream();
BigInteger x, y;
try {
byte[] xBytes = new byte[LENGTH];
dis.readFully(xBytes);
reverse(xBytes);
x = new BigInteger(1, xBytes);
byte[] yBytes = new byte[LENGTH];
dis.readFully(yBytes);
reverse(yBytes);
y = new BigInteger(1, yBytes);
} catch (IOException e) {
throw new DataMalformedException(e, key.getKey());
}
try {
return getKeyFactory().generatePublic(new ECPublicKeySpec(new ECPoint(x, y), SPEC));
} catch (InvalidKeySpecException e) {
throw new DnssecInvalidKeySpecException(e);
}
}
private static void reverse(byte[] array) {
for (int i = 0; i < array.length / 2; i++) {
int j = array.length - i - 1;
byte tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec.algorithms;
import org.minidns.dnssec.DigestCalculator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class JavaSecDigestCalculator implements DigestCalculator {
private MessageDigest md;
public JavaSecDigestCalculator(String algorithm) throws NoSuchAlgorithmException {
md = MessageDigest.getInstance(algorithm);
}
@Override
public byte[] digest(byte[] bytes) {
return md.digest(bytes);
}
}

View file

@ -0,0 +1,62 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec.algorithms;
import org.minidns.dnssec.DnssecValidationFailedException;
import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException;
import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException;
import org.minidns.dnssec.SignatureVerifier;
import org.minidns.record.DNSKEY;
import org.minidns.record.RRSIG;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
public abstract class JavaSecSignatureVerifier implements SignatureVerifier {
private final KeyFactory keyFactory;
private final String signatureAlgorithm;
public JavaSecSignatureVerifier(String keyAlgorithm, String signatureAlgorithm) throws NoSuchAlgorithmException {
keyFactory = KeyFactory.getInstance(keyAlgorithm);
this.signatureAlgorithm = signatureAlgorithm;
// Verify signature algorithm to be valid
Signature.getInstance(signatureAlgorithm);
}
public KeyFactory getKeyFactory() {
return keyFactory;
}
@Override
public boolean verify(byte[] content, RRSIG rrsig, DNSKEY key) throws DnssecValidationFailedException {
try {
PublicKey publicKey = getPublicKey(key);
Signature signature = Signature.getInstance(signatureAlgorithm);
signature.initVerify(publicKey);
signature.update(content);
return signature.verify(getSignature(rrsig));
} catch (NoSuchAlgorithmException e) {
// We checked against this before, it should never happen!
throw new AssertionError(e);
} catch (InvalidKeyException | SignatureException | ArithmeticException e) {
throw new DnssecValidationFailedException("Validating signature failed", e);
}
}
protected abstract byte[] getSignature(RRSIG rrsig) throws DataMalformedException;
protected abstract PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException;
}

View file

@ -0,0 +1,67 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnssec.algorithms;
import org.minidns.dnssec.DnssecValidationFailedException.DnssecInvalidKeySpecException;
import org.minidns.record.DNSKEY;
import org.minidns.record.RRSIG;
import org.minidns.dnssec.DnssecValidationFailedException.DataMalformedException;
import java.io.DataInput;
import java.io.IOException;
import java.math.BigInteger;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
class RsaSignatureVerifier extends JavaSecSignatureVerifier {
RsaSignatureVerifier(String algorithm) throws NoSuchAlgorithmException {
super("RSA", algorithm);
}
@Override
protected PublicKey getPublicKey(DNSKEY key) throws DataMalformedException, DnssecInvalidKeySpecException {
DataInput dis = key.getKeyAsDataInputStream();
BigInteger exponent, modulus;
try {
int exponentLength = dis.readUnsignedByte();
int bytesRead = 1;
if (exponentLength == 0) {
bytesRead += 2;
exponentLength = dis.readUnsignedShort();
}
byte[] exponentBytes = new byte[exponentLength];
dis.readFully(exponentBytes);
bytesRead += exponentLength;
exponent = new BigInteger(1, exponentBytes);
byte[] modulusBytes = new byte[key.getKeyLength() - bytesRead];
dis.readFully(modulusBytes);
modulus = new BigInteger(1, modulusBytes);
} catch (IOException e) {
throw new DataMalformedException(e, key.getKey());
}
try {
return getKeyFactory().generatePublic(new RSAPublicKeySpec(modulus, exponent));
} catch (InvalidKeySpecException e) {
throw new DnssecInvalidKeySpecException(e);
}
}
@Override
protected byte[] getSignature(RRSIG rrsig) {
return rrsig.getSignature();
}
}

View file

@ -0,0 +1,60 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsserverlookup;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.logging.Logger;
public abstract class AbstractDnsServerLookupMechanism implements DnsServerLookupMechanism {
protected static final Logger LOGGER = Logger.getLogger(AbstractDnsServerLookupMechanism.class.getName());
private final String name;
private final int priority;
protected AbstractDnsServerLookupMechanism(String name, int priority) {
this.name = name;
this.priority = priority;
}
@Override
public final String getName() {
return name;
}
@Override
public final int getPriority() {
return priority;
}
@Override
public final int compareTo(DnsServerLookupMechanism other) {
int myPriority = getPriority();
int otherPriority = other.getPriority();
return Integer.compare(myPriority, otherPriority);
}
@Override
public abstract List<String> getDnsServerAddresses();
protected static List<String> toListOfStrings(Collection<? extends InetAddress> inetAddresses) {
List<String> result = new ArrayList<>(inetAddresses.size());
for (InetAddress inetAddress : inetAddresses) {
String address = inetAddress.getHostAddress();
result.add(address);
}
return result;
}
}

View file

@ -0,0 +1,115 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsserverlookup;
import org.minidns.util.PlatformDetection;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
/**
* Try to retrieve the list of DNS server by executing getprop.
*/
public final class AndroidUsingExec extends AbstractDnsServerLookupMechanism {
public static final DnsServerLookupMechanism INSTANCE = new AndroidUsingExec();
public static final int PRIORITY = AndroidUsingReflection.PRIORITY - 1;
private AndroidUsingExec() {
super(AndroidUsingExec.class.getSimpleName(), PRIORITY);
}
@Override
public List<String> getDnsServerAddresses() {
try {
Process process = Runtime.getRuntime().exec("getprop");
InputStream inputStream = process.getInputStream();
LineNumberReader lnr = new LineNumberReader(
new InputStreamReader(inputStream, StandardCharsets.UTF_8));
Set<String> server = parseProps(lnr, true);
if (server.size() > 0) {
List<String> res = new ArrayList<>(server.size());
res.addAll(server);
return res;
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Exception in findDNSByExec", e);
}
return null;
}
@Override
public boolean isAvailable() {
return PlatformDetection.isAndroid();
}
private static final String PROP_DELIM = "]: [";
static Set<String> parseProps(BufferedReader lnr, boolean logWarning) throws UnknownHostException, IOException {
String line = null;
Set<String> server = new HashSet<String>(6);
while ((line = lnr.readLine()) != null) {
int split = line.indexOf(PROP_DELIM);
if (split == -1) {
continue;
}
String property = line.substring(1, split);
int valueStart = split + PROP_DELIM.length();
int valueEnd = line.length() - 1;
if (valueEnd < valueStart) {
// This can happen if a newline sneaks in as the first character of the property value. For example
// "[propName]: [\n…]".
if (logWarning) {
LOGGER.warning("Malformed property detected: \"" + line + '"');
}
continue;
}
String value = line.substring(valueStart, valueEnd);
if (value.isEmpty()) {
continue;
}
if (property.endsWith(".dns") || property.endsWith(".dns1") ||
property.endsWith(".dns2") || property.endsWith(".dns3") ||
property.endsWith(".dns4")) {
// normalize the address
InetAddress ip = InetAddress.getByName(value);
if (ip == null) continue;
value = ip.getHostAddress();
if (value == null) continue;
if (value.length() == 0) continue;
server.add(value);
}
}
return server;
}
}

View file

@ -0,0 +1,98 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsserverlookup;
import org.minidns.util.PlatformDetection;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
/**
* Try to retrieve the list of DNS server by calling SystemProperties.
*/
public class AndroidUsingReflection extends AbstractDnsServerLookupMechanism {
public static final DnsServerLookupMechanism INSTANCE = new AndroidUsingReflection();
public static final int PRIORITY = 1000;
private final Method systemPropertiesGet;
protected AndroidUsingReflection() {
super(AndroidUsingReflection.class.getSimpleName(), PRIORITY);
Method systemPropertiesGet = null;
if (PlatformDetection.isAndroid()) {
try {
Class<?> SystemProperties = Class.forName("android.os.SystemProperties");
systemPropertiesGet = SystemProperties.getMethod("get", new Class<?>[] { String.class });
} catch (ClassNotFoundException | NoSuchMethodException | SecurityException e) {
// This is not unexpected, as newer Android versions do not provide access to it any more.
LOGGER.log(Level.FINE, "Can not get method handle for android.os.SystemProperties.get(String).", e);
}
}
this.systemPropertiesGet = systemPropertiesGet;
}
@Override
public List<String> getDnsServerAddresses() {
ArrayList<String> servers = new ArrayList<String>(5);
for (String propKey : new String[] {
"net.dns1", "net.dns2", "net.dns3", "net.dns4"}) {
String value;
try {
value = (String) systemPropertiesGet.invoke(null, propKey);
} catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
LOGGER.log(Level.WARNING, "Exception in findDNSByReflection", e);
return null;
}
if (value == null) continue;
if (value.length() == 0) continue;
if (servers.contains(value)) continue;
InetAddress ip;
try {
ip = InetAddress.getByName(value);
} catch (UnknownHostException e) {
LOGGER.log(Level.WARNING, "Exception in findDNSByReflection", e);
continue;
}
if (ip == null) continue;
value = ip.getHostAddress();
if (value == null) continue;
if (value.length() == 0) continue;
if (servers.contains(value)) continue;
servers.add(value);
}
if (servers.size() > 0) {
return servers;
}
return null;
}
@Override
public boolean isAvailable() {
return systemPropertiesGet != null;
}
}

View file

@ -0,0 +1,34 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsserverlookup;
import java.util.List;
public interface DnsServerLookupMechanism extends Comparable<DnsServerLookupMechanism> {
String getName();
int getPriority();
boolean isAvailable();
/**
* Returns a List of String representing ideally IP addresses. The list must be modifiable.
* <p>
* Note that the lookup mechanisms are not required to assure that only IP addresses are returned. This verification is performed in
* when using {@link org.minidns.DnsClient#findDNS()}.
* </p>
*
* @return a List of Strings presenting hopefully IP addresses.
*/
List<String> getDnsServerAddresses();
}

View file

@ -0,0 +1,110 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsserverlookup;
import org.minidns.util.PlatformDetection;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public final class UnixUsingEtcResolvConf extends AbstractDnsServerLookupMechanism {
public static final DnsServerLookupMechanism INSTANCE = new UnixUsingEtcResolvConf();
public static final int PRIORITY = 2000;
private static final Logger LOGGER = Logger.getLogger(UnixUsingEtcResolvConf.class.getName());
private static final String RESOLV_CONF_FILE = "/etc/resolv.conf";
private static final Pattern NAMESERVER_PATTERN = Pattern.compile("^nameserver\\s+(.*)$");
private static List<String> cached;
private static long lastModified;
private UnixUsingEtcResolvConf() {
super(UnixUsingEtcResolvConf.class.getSimpleName(), PRIORITY);
}
@Override
public List<String> getDnsServerAddresses() {
File file = new File(RESOLV_CONF_FILE);
if (!file.exists()) {
// Not very unixoid systems
return null;
}
long currentLastModified = file.lastModified();
if (currentLastModified == lastModified && cached != null) {
return cached;
}
List<String> servers = new ArrayList<>();
BufferedReader reader = null;
try {
reader = new BufferedReader(new InputStreamReader(new FileInputStream(file), StandardCharsets.UTF_8));
String line;
while ((line = reader.readLine()) != null) {
Matcher matcher = NAMESERVER_PATTERN.matcher(line);
if (matcher.matches()) {
servers.add(matcher.group(1).trim());
}
}
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Could not read from " + RESOLV_CONF_FILE, e);
return null;
} finally {
if (reader != null) try {
reader.close();
} catch (IOException e) {
LOGGER.log(Level.WARNING, "Could not close reader", e);
}
}
if (servers.isEmpty()) {
LOGGER.fine("Could not find any nameservers in " + RESOLV_CONF_FILE);
return null;
}
cached = servers;
lastModified = currentLastModified;
return cached;
}
@Override
public boolean isAvailable() {
if (PlatformDetection.isAndroid()) {
// Don't rely on resolv.conf when on Android
return false;
}
File file = new File(RESOLV_CONF_FILE);
boolean resolvConfFileExists;
try {
resolvConfFileExists = file.exists();
} catch (SecurityException securityException) {
LOGGER.log(Level.FINE, "Access to /etc/resolv.conf not possible", securityException);
return false;
}
return resolvConfFileExists;
}
}

View file

@ -0,0 +1,135 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.dnsserverlookup.android21;
import android.annotation.TargetApi;
import android.content.Context;
import android.net.ConnectivityManager;
import android.net.LinkProperties;
import android.net.Network;
import android.net.RouteInfo;
import android.os.Build;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.List;
import org.minidns.DnsClient;
import org.minidns.dnsserverlookup.AbstractDnsServerLookupMechanism;
import org.minidns.dnsserverlookup.AndroidUsingExec;
/**
* A DNS server lookup mechanism using Android's Link Properties method available on Android API 21 or higher. Use
* {@link #setup(Context)} to setup this mechanism.
* <p>
* Requires the ACCESS_NETWORK_STATE permission.
* </p>
*/
public class AndroidUsingLinkProperties extends AbstractDnsServerLookupMechanism {
private final ConnectivityManager connectivityManager;
/**
* Setup this DNS server lookup mechanism. You need to invoke this method only once, ideally before you do your
* first DNS lookup.
*
* @param context a Context instance.
* @return the instance of the newly setup mechanism
*/
public static AndroidUsingLinkProperties setup(Context context) {
AndroidUsingLinkProperties androidUsingLinkProperties = new AndroidUsingLinkProperties(context);
DnsClient.addDnsServerLookupMechanism(androidUsingLinkProperties);
return androidUsingLinkProperties;
}
/**
* Construct this DNS server lookup mechanism.
*
* @param context an Android context.
*/
public AndroidUsingLinkProperties(Context context) {
super(AndroidUsingLinkProperties.class.getSimpleName(), AndroidUsingExec.PRIORITY - 1);
connectivityManager = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
}
@Override
public boolean isAvailable() {
return Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP;
}
@TargetApi(Build.VERSION_CODES.M)
private List<String> getDnsServerAddressesOfActiveNetwork() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
return null;
}
// ConnectivityManager.getActiveNetwork() is API 23.
Network activeNetwork = connectivityManager.getActiveNetwork();
if (activeNetwork == null) {
return null;
}
LinkProperties linkProperties = connectivityManager.getLinkProperties(activeNetwork);
if (linkProperties == null) {
return null;
}
List<InetAddress> dnsServers = linkProperties.getDnsServers();
return toListOfStrings(dnsServers);
}
@Override
@TargetApi(21)
public List<String> getDnsServerAddresses() {
// First, try the API 23 approach using ConnectivityManager.getActiveNetwork().
List<String> servers = getDnsServerAddressesOfActiveNetwork();
if (servers != null) {
return servers;
}
Network[] networks = connectivityManager.getAllNetworks();
if (networks == null) {
return null;
}
servers = new ArrayList<>(networks.length * 2);
for (Network network : networks) {
LinkProperties linkProperties = connectivityManager.getLinkProperties(network);
if (linkProperties == null) {
continue;
}
// Prioritize the DNS servers of links which have a default route
if (hasDefaultRoute(linkProperties)) {
servers.addAll(0, toListOfStrings(linkProperties.getDnsServers()));
} else {
servers.addAll(toListOfStrings(linkProperties.getDnsServers()));
}
}
if (servers.isEmpty()) {
return null;
}
return servers;
}
@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private static boolean hasDefaultRoute(LinkProperties linkProperties) {
for (RouteInfo route : linkProperties.getRoutes()) {
if (route.isDefaultRoute()) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,235 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.edns;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import org.minidns.dnsname.DnsName;
import org.minidns.record.Data;
import org.minidns.record.OPT;
import org.minidns.record.Record;
import org.minidns.record.Record.TYPE;
/**
* EDNS - Extension Mechanism for DNS.
*
* @see <a href="https://tools.ietf.org/html/rfc6891">RFC 6891 - Extension Mechanisms for DNS (EDNS(0))</a>
*
*/
public class Edns {
/**
* Inform the dns server that the client supports DNSSEC.
*/
public static final int FLAG_DNSSEC_OK = 0x8000;
/**
* The EDNS option code.
*
* @see <a href="http://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-11">IANA - DNS EDNS0 Option Codes (OPT)</a>
*/
public enum OptionCode {
UNKNOWN(-1, UnknownEdnsOption.class),
NSID(3, Nsid.class),
;
private static Map<Integer, OptionCode> INVERSE_LUT = new HashMap<>(OptionCode.values().length);
static {
for (OptionCode optionCode : OptionCode.values()) {
INVERSE_LUT.put(optionCode.asInt, optionCode);
}
}
public final int asInt;
public final Class<? extends EdnsOption> clazz;
OptionCode(int optionCode, Class<? extends EdnsOption> clazz) {
this.asInt = optionCode;
this.clazz = clazz;
}
public static OptionCode from(int optionCode) {
OptionCode res = INVERSE_LUT.get(optionCode);
if (res == null) res = OptionCode.UNKNOWN;
return res;
}
}
public final int udpPayloadSize;
/**
* 8-bit extended return code.
*
* RFC 6891 § 6.1.3 EXTENDED-RCODE
*/
public final int extendedRcode;
/**
* 8-bit version field.
*
* RFC 6891 § 6.1.3 VERSION
*/
public final int version;
/**
* 16-bit flags.
*
* RFC 6891 § 6.1.4
*/
public final int flags;
public final List<EdnsOption> variablePart;
public final boolean dnssecOk;
private Record<OPT> optRecord;
public Edns(Record<OPT> optRecord) {
assert optRecord.type == TYPE.OPT;
udpPayloadSize = optRecord.clazzValue;
extendedRcode = (int) ((optRecord.ttl >> 8) & 0xff);
version = (int) ((optRecord.ttl >> 16) & 0xff);
flags = (int) optRecord.ttl & 0xffff;
dnssecOk = (optRecord.ttl & FLAG_DNSSEC_OK) > 0;
OPT opt = optRecord.payloadData;
variablePart = opt.variablePart;
this.optRecord = optRecord;
}
public Edns(Builder builder) {
udpPayloadSize = builder.udpPayloadSize;
extendedRcode = builder.extendedRcode;
version = builder.version;
int flags = 0;
if (builder.dnssecOk) {
flags |= FLAG_DNSSEC_OK;
}
dnssecOk = builder.dnssecOk;
this.flags = flags;
if (builder.variablePart != null) {
variablePart = builder.variablePart;
} else {
variablePart = Collections.emptyList();
}
}
@SuppressWarnings({"unchecked", "TypeParameterUnusedInFormals"})
public <O extends EdnsOption> O getEdnsOption(OptionCode optionCode) {
for (EdnsOption o : variablePart) {
if (o.getOptionCode().equals(optionCode)) {
return (O) o;
}
}
return null;
}
public Record<OPT> asRecord() {
if (optRecord == null) {
long optFlags = flags;
optFlags |= extendedRcode << 8;
optFlags |= version << 16;
optRecord = new Record<OPT>(DnsName.ROOT, Record.TYPE.OPT, udpPayloadSize, optFlags, new OPT(variablePart));
}
return optRecord;
}
private String terminalOutputCache;
public String asTerminalOutput() {
if (terminalOutputCache == null) {
StringBuilder sb = new StringBuilder();
sb.append("EDNS: version: ").append(version).append(", flags:");
if (dnssecOk)
sb.append(" do");
sb.append("; udp: ").append(udpPayloadSize);
if (!variablePart.isEmpty()) {
sb.append('\n');
Iterator<EdnsOption> it = variablePart.iterator();
while (it.hasNext()) {
EdnsOption edns = it.next();
sb.append(edns.getOptionCode()).append(": ");
sb.append(edns.asTerminalOutput());
if (it.hasNext()) {
sb.append('\n');
}
}
}
terminalOutputCache = sb.toString();
}
return terminalOutputCache;
}
@Override
public String toString() {
return asTerminalOutput();
}
public static Edns fromRecord(Record<? extends Data> record) {
if (record.type != TYPE.OPT) return null;
@SuppressWarnings("unchecked")
Record<OPT> optRecord = (Record<OPT>) record;
return new Edns(optRecord);
}
public static Builder builder() {
return new Builder();
}
public static final class Builder {
private int udpPayloadSize;
private int extendedRcode;
private int version;
private boolean dnssecOk;
private List<EdnsOption> variablePart;
private Builder() {
}
public Builder setUdpPayloadSize(int udpPayloadSize) {
if (udpPayloadSize > 0xffff) {
throw new IllegalArgumentException("UDP payload size must not be greater than 65536, was " + udpPayloadSize);
}
this.udpPayloadSize = udpPayloadSize;
return this;
}
public Builder setDnssecOk(boolean dnssecOk) {
this.dnssecOk = dnssecOk;
return this;
}
public Builder setDnssecOk() {
dnssecOk = true;
return this;
}
public Builder addEdnsOption(EdnsOption ednsOption) {
if (variablePart == null) {
variablePart = new ArrayList<>(4);
}
variablePart.add(ednsOption);
return this;
}
public Edns build() {
return new Edns(this);
}
}
}

View file

@ -0,0 +1,83 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.edns;
import java.io.DataOutputStream;
import java.io.IOException;
import org.minidns.edns.Edns.OptionCode;
public abstract class EdnsOption {
public final int optionCode;
public final int optionLength;
protected final byte[] optionData;
protected EdnsOption(int optionCode, byte[] optionData) {
this.optionCode = optionCode;
this.optionLength = optionData.length;
this.optionData = optionData;
}
@SuppressWarnings("this-escape")
protected EdnsOption(byte[] optionData) {
this.optionCode = getOptionCode().asInt;
this.optionLength = optionData.length;
this.optionData = optionData;
}
public final void writeToDos(DataOutputStream dos) throws IOException {
dos.writeShort(optionCode);
dos.writeShort(optionLength);
dos.write(optionData);
}
public abstract OptionCode getOptionCode();
private String toStringCache;
@Override
public final String toString() {
if (toStringCache == null) {
toStringCache = toStringInternal().toString();
}
return toStringCache;
}
protected abstract CharSequence toStringInternal();
private String terminalOutputCache;
public final String asTerminalOutput() {
if (terminalOutputCache == null) {
terminalOutputCache = asTerminalOutputInternal().toString();
}
return terminalOutputCache;
}
protected abstract CharSequence asTerminalOutputInternal();
public static EdnsOption parse(int intOptionCode, byte[] optionData) {
OptionCode optionCode = OptionCode.from(intOptionCode);
EdnsOption res;
switch (optionCode) {
case NSID:
res = new Nsid(optionData);
break;
default:
res = new UnknownEdnsOption(intOptionCode, optionData);
break;
}
return res;
}
}

View file

@ -0,0 +1,47 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.edns;
import java.nio.charset.StandardCharsets;
import org.minidns.edns.Edns.OptionCode;
import org.minidns.util.Hex;
public class Nsid extends EdnsOption {
public static final Nsid REQUEST = new Nsid();
private Nsid() {
this(new byte[0]);
}
public Nsid(byte[] payload) {
super(payload);
}
@Override
public OptionCode getOptionCode() {
return OptionCode.NSID;
}
@Override
protected CharSequence toStringInternal() {
String res = OptionCode.NSID + ": ";
res += new String(optionData, StandardCharsets.US_ASCII);
return res;
}
@Override
protected CharSequence asTerminalOutputInternal() {
return Hex.from(optionData);
}
}

View file

@ -0,0 +1,37 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.edns;
import org.minidns.edns.Edns.OptionCode;
import org.minidns.util.Hex;
public class UnknownEdnsOption extends EdnsOption {
protected UnknownEdnsOption(int optionCode, byte[] optionData) {
super(optionCode, optionData);
}
@Override
public OptionCode getOptionCode() {
return OptionCode.UNKNOWN;
}
@Override
protected CharSequence asTerminalOutputInternal() {
return Hex.from(optionData);
}
@Override
protected CharSequence toStringInternal() {
return asTerminalOutputInternal();
}
}

View file

@ -0,0 +1,124 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.hla;
import java.io.IOException;
import java.util.Set;
import org.minidns.DnsCache;
import org.minidns.MiniDnsException.NullResultException;
import org.minidns.cache.LruCache;
import org.minidns.cache.MiniDnsCacheFactory;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnssec.DnssecClient;
import org.minidns.dnssec.DnssecQueryResult;
import org.minidns.dnssec.DnssecUnverifiedReason;
import org.minidns.iterative.ReliableDnsClient.Mode;
import org.minidns.record.Data;
import org.minidns.record.Record.TYPE;
public class DnssecResolverApi extends ResolverApi {
public static final DnssecResolverApi INSTANCE = new DnssecResolverApi();
private final DnssecClient dnssecClient;
private final DnssecClient iterativeOnlyDnssecClient;
private final DnssecClient recursiveOnlyDnssecClient;
public DnssecResolverApi() {
this(new MiniDnsCacheFactory() {
@Override
public DnsCache newCache() {
return new LruCache();
}
});
}
public DnssecResolverApi(MiniDnsCacheFactory cacheFactory) {
this(new DnssecClient(cacheFactory.newCache()), cacheFactory);
}
private DnssecResolverApi(DnssecClient dnssecClient, MiniDnsCacheFactory cacheFactory) {
super(dnssecClient);
this.dnssecClient = dnssecClient;
// Set the *_ONLY_DNSSEC ResolverApi. It is important that the two do *not* share the same cache, since we
// probably fall back to iterativeOnly and in that case do not want the cached results of the recursive result.
iterativeOnlyDnssecClient = new DnssecClient(cacheFactory.newCache());
iterativeOnlyDnssecClient.setMode(Mode.iterativeOnly);
recursiveOnlyDnssecClient = new DnssecClient(cacheFactory.newCache());
recursiveOnlyDnssecClient.setMode(Mode.recursiveOnly);
}
@Override
public <D extends Data> ResolverResult<D> resolve(Question question) throws IOException {
DnssecQueryResult dnssecMessage = dnssecClient.queryDnssec(question);
return toResolverResult(question, dnssecMessage);
}
/**
* Resolve the given name and type which is expected to yield DNSSEC authenticated results.
*
* @param name the DNS name to resolve.
* @param type the class of the RR type to resolve.
* @param <D> the RR type to resolve.
* @return the resolver result.
* @throws IOException in case an exception happens while resolving.
* @see #resolveDnssecReliable(Question)
*/
public <D extends Data> ResolverResult<D> resolveDnssecReliable(String name, Class<D> type) throws IOException {
return resolveDnssecReliable(DnsName.from(name), type);
}
/**
* Resolve the given name and type which is expected to yield DNSSEC authenticated results.
*
* @param name the DNS name to resolve.
* @param type the class of the RR type to resolve.
* @param <D> the RR type to resolve.
* @return the resolver result.
* @throws IOException in case an exception happens while resolving.
* @see #resolveDnssecReliable(Question)
*/
public <D extends Data> ResolverResult<D> resolveDnssecReliable(DnsName name, Class<D> type) throws IOException {
TYPE t = TYPE.getType(type);
Question q = new Question(name, t);
return resolveDnssecReliable(q);
}
/**
* Resolve the given question which is expected to yield DNSSEC authenticated results.
*
* @param question the question to resolve.
* @param <D> the RR type to resolve.
* @return the resolver result.
* @throws IOException in case an exception happens while resolving.
*/
public <D extends Data> ResolverResult<D> resolveDnssecReliable(Question question) throws IOException {
DnssecQueryResult dnssecMessage = recursiveOnlyDnssecClient.queryDnssec(question);
if (dnssecMessage == null || !dnssecMessage.isAuthenticData()) {
dnssecMessage = iterativeOnlyDnssecClient.queryDnssec(question);
}
return toResolverResult(question, dnssecMessage);
}
public DnssecClient getDnssecClient() {
return dnssecClient;
}
private static <D extends Data> ResolverResult<D> toResolverResult(Question question, DnssecQueryResult dnssecMessage) throws NullResultException {
Set<DnssecUnverifiedReason> unverifiedReasons = dnssecMessage.getUnverifiedReasons();
return new ResolverResult<D>(question, dnssecMessage.dnsQueryResult, unverifiedReasons);
}
}

View file

@ -0,0 +1,32 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.hla;
import org.minidns.MiniDnsException;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
public class ResolutionUnsuccessfulException extends MiniDnsException {
/**
*
*/
private static final long serialVersionUID = 1L;
public final Question question;
public final RESPONSE_CODE responseCode;
public ResolutionUnsuccessfulException(Question question, RESPONSE_CODE responseCode) {
super("Asking for " + question + " yielded an error response " + responseCode);
this.question = question;
this.responseCode = responseCode;
}
}

View file

@ -0,0 +1,222 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.hla;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import org.minidns.AbstractDnsClient;
import org.minidns.DnsClient;
import org.minidns.dnslabel.DnsLabel;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.hla.srv.SrvProto;
import org.minidns.hla.srv.SrvService;
import org.minidns.hla.srv.SrvServiceProto;
import org.minidns.hla.srv.SrvType;
import org.minidns.iterative.ReliableDnsClient;
import org.minidns.record.Data;
import org.minidns.record.PTR;
import org.minidns.record.SRV;
import org.minidns.record.Record.TYPE;
/**
* The high-level MiniDNS resolving API. It is designed to be easy to use.
* <p>
* A simple exammple how to resolve the IPv4 address of a given domain:
* </p>
* <pre>
* {@code
* ResolverResult<A> result = DnssecResolverApi.INSTANCE.resolve("verteiltesysteme.net", A.class);
* if (!result.wasSuccessful()) {
* RESPONSE_CODE responseCode = result.getResponseCode();
* // Perform error handling.
*
* return;
* }
* if (!result.isAuthenticData()) {
* // Response was not secured with DNSSEC.
*
* return;
* }
* Set<A> answers = result.getAnswers();
* for (A a : answers) {
* InetAddress inetAddress = a.getInetAddress();
* // Do someting with the InetAddress, e.g. connect to.
*
* }
* }
* </pre>
* <p>
* MiniDNS also supports SRV resource records as first class citizens:
* </p>
* <pre>
* {@code
* SrvResolverResult result = DnssecResolverApi.INSTANCE.resolveSrv(SrvType.xmpp_client, "example.org")
* if (!result.wasSuccessful()) {
* RESPONSE_CODE responseCode = result.getResponseCode();
* // Perform error handling.
*
* return;
* }
* if (!result.isAuthenticData()) {
* // Response was not secured with DNSSEC.
*
* return;
* }
* List<ResolvedSrvRecord> srvRecords = result.getSortedSrvResolvedAddresses();
* // Loop over the domain names pointed by the SRV RR. MiniDNS will return the list
* // correctly sorted by the priority and weight of the related SRV RR.
* for (ResolvedSrvRecord srvRecord : srvRecord) {
* // Loop over the Internet Address RRs resolved for the SRV RR. The order of
* // the list depends on the prefered IP version setting of MiniDNS.
* for (InternetAddressRR inetAddressRR : srvRecord.addresses) {
* InetAddress inetAddress = inetAddressRR.getInetAddress();
* int port = srvAddresses.port;
* // Try to connect to inetAddress at port.
*
* }
* }
* }
* </pre>
*
* @author Florian Schmaus
*
*/
public class ResolverApi {
public static final ResolverApi INSTANCE = new ResolverApi(new ReliableDnsClient());
private final AbstractDnsClient dnsClient;
public ResolverApi(AbstractDnsClient dnsClient) {
this.dnsClient = dnsClient;
}
public final <D extends Data> ResolverResult<D> resolve(String name, Class<D> type) throws IOException {
return resolve(DnsName.from(name), type);
}
public final <D extends Data> ResolverResult<D> resolve(DnsName name, Class<D> type) throws IOException {
TYPE t = TYPE.getType(type);
Question q = new Question(name, t);
return resolve(q);
}
public <D extends Data> ResolverResult<D> resolve(Question question) throws IOException {
DnsQueryResult dnsQueryResult = dnsClient.query(question);
return new ResolverResult<D>(question, dnsQueryResult, null);
}
public SrvResolverResult resolveSrv(SrvType type, String serviceName) throws IOException {
return resolveSrv(type.service, type.proto, DnsName.from(serviceName));
}
public SrvResolverResult resolveSrv(SrvType type, DnsName serviceName) throws IOException {
return resolveSrv(type.service, type.proto, serviceName);
}
public SrvResolverResult resolveSrv(SrvService service, SrvProto proto, String name) throws IOException {
return resolveSrv(service.dnsLabel, proto.dnsLabel, DnsName.from(name));
}
public SrvResolverResult resolveSrv(SrvService service, SrvProto proto, DnsName name) throws IOException {
return resolveSrv(service.dnsLabel, proto.dnsLabel, name);
}
public SrvResolverResult resolveSrv(DnsLabel service, DnsLabel proto, DnsName name) throws IOException {
SrvServiceProto srvServiceProto = new SrvServiceProto(service, proto);
return resolveSrv(name, srvServiceProto);
}
public SrvResolverResult resolveSrv(String name) throws IOException {
return resolveSrv(DnsName.from(name));
}
public ResolverResult<PTR> reverseLookup(CharSequence inetAddressCs) throws IOException {
InetAddress inetAddress = InetAddress.getByName(inetAddressCs.toString());
return reverseLookup(inetAddress);
}
public ResolverResult<PTR> reverseLookup(InetAddress inetAddress) throws IOException {
if (inetAddress instanceof Inet4Address) {
return reverseLookup((Inet4Address) inetAddress);
} else if (inetAddress instanceof Inet6Address) {
return reverseLookup((Inet6Address) inetAddress);
} else {
throw new IllegalArgumentException("The given InetAddress '" + inetAddress + "' is neither of type Inet4Address or Inet6Address");
}
}
public ResolverResult<PTR> reverseLookup(Inet4Address inet4Address) throws IOException {
Question question = DnsClient.getReverseIpLookupQuestionFor(inet4Address);
return resolve(question);
}
public ResolverResult<PTR> reverseLookup(Inet6Address inet6Address) throws IOException {
Question question = DnsClient.getReverseIpLookupQuestionFor(inet6Address);
return resolve(question);
}
/**
* Resolve the {@link SRV} resource record for the given name. After ensuring that the resolution was successful
* with {@link SrvResolverResult#wasSuccessful()} , and, if DNSSEC was used, that the results could be verified with
* {@link SrvResolverResult#isAuthenticData()}, simply use {@link SrvResolverResult#getSortedSrvResolvedAddresses()} to
* retrieve the resolved IP addresses.
* <p>
* The name of SRV records is "_[service]._[protocol].[serviceDomain]", for example "_xmpp-client._tcp.example.org".
* </p>
*
* @param srvDnsName the name to resolve.
* @return a <code>SrvResolverResult</code> instance which can be used to retrieve the IP addresses.
* @throws IOException if an IO exception occurs.
*/
public SrvResolverResult resolveSrv(DnsName srvDnsName) throws IOException {
final int labelCount = srvDnsName.getLabelCount();
if (labelCount < 3) {
throw new IllegalArgumentException();
}
DnsLabel service = srvDnsName.getLabel(labelCount - 1);
DnsLabel proto = srvDnsName.getLabel(labelCount - 2);
DnsName name = srvDnsName.stripToLabels(labelCount - 2);
SrvServiceProto srvServiceProto = new SrvServiceProto(service, proto);
return resolveSrv(name, srvServiceProto);
}
/**
* Resolve the {@link SRV} resource record for the given service name, service and protcol. After ensuring that the
* resolution was successful with {@link SrvResolverResult#wasSuccessful()} , and, if DNSSEC was used, that the
* results could be verified with {@link SrvResolverResult#isAuthenticData()}, simply use
* {@link SrvResolverResult#getSortedSrvResolvedAddresses()} to retrieve the resolved IP addresses.
*
* @param name the DNS name of the service.
* @param srvServiceProto the service and protocol to lookup.
* @return a <code>SrvResolverResult</code> instance which can be used to retrieve the IP addresses.
* @throws IOException if an I/O error occurs.
*/
public SrvResolverResult resolveSrv(DnsName name, SrvServiceProto srvServiceProto) throws IOException {
DnsName srvDnsName = DnsName.from(srvServiceProto.service, srvServiceProto.proto, name);
ResolverResult<SRV> result = resolve(srvDnsName, SRV.class);
return new SrvResolverResult(result, srvServiceProto, this);
}
public final AbstractDnsClient getClient() {
return dnsClient;
}
}

View file

@ -0,0 +1,178 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.hla;
import java.util.Collections;
import java.util.Set;
import org.minidns.MiniDnsException;
import org.minidns.MiniDnsException.NullResultException;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
import org.minidns.dnssec.DnssecResultNotAuthenticException;
import org.minidns.dnssec.DnssecUnverifiedReason;
import org.minidns.record.Data;
public class ResolverResult<D extends Data> {
protected final Question question;
private final RESPONSE_CODE responseCode;
private final Set<D> data;
private final boolean isAuthenticData;
protected final Set<DnssecUnverifiedReason> unverifiedReasons;
protected final DnsMessage answer;
protected final DnsQueryResult result;
ResolverResult(Question question, DnsQueryResult result, Set<DnssecUnverifiedReason> unverifiedReasons) throws NullResultException {
// TODO: Is this null check still needed?
if (result == null) {
throw new MiniDnsException.NullResultException(question.asMessageBuilder().build());
}
this.result = result;
DnsMessage answer = result.response;
this.question = question;
this.responseCode = answer.responseCode;
this.answer = answer;
Set<D> r = answer.getAnswersFor(question);
if (r == null) {
this.data = Collections.emptySet();
} else {
this.data = Collections.unmodifiableSet(r);
}
if (unverifiedReasons == null) {
this.unverifiedReasons = null;
isAuthenticData = false;
} else {
this.unverifiedReasons = Collections.unmodifiableSet(unverifiedReasons);
isAuthenticData = this.unverifiedReasons.isEmpty();
}
}
public boolean wasSuccessful() {
return responseCode == RESPONSE_CODE.NO_ERROR;
}
public Set<D> getAnswers() {
throwIseIfErrorResponse();
return data;
}
public Set<D> getAnswersOrEmptySet() {
return data;
}
public RESPONSE_CODE getResponseCode() {
return responseCode;
}
public boolean isAuthenticData() {
throwIseIfErrorResponse();
return isAuthenticData;
}
/**
* Get the reasons the result could not be verified if any exists.
*
* @return The reasons the result could not be verified or <code>null</code>.
*/
public Set<DnssecUnverifiedReason> getUnverifiedReasons() {
throwIseIfErrorResponse();
return unverifiedReasons;
}
public Question getQuestion() {
return question;
}
public void throwIfErrorResponse() throws ResolutionUnsuccessfulException {
ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException();
if (resolutionUnsuccessfulException != null) throw resolutionUnsuccessfulException;
}
private ResolutionUnsuccessfulException resolutionUnsuccessfulException;
public ResolutionUnsuccessfulException getResolutionUnsuccessfulException() {
if (wasSuccessful()) return null;
if (resolutionUnsuccessfulException == null) {
resolutionUnsuccessfulException = new ResolutionUnsuccessfulException(question, responseCode);
}
return resolutionUnsuccessfulException;
}
private DnssecResultNotAuthenticException dnssecResultNotAuthenticException;
public DnssecResultNotAuthenticException getDnssecResultNotAuthenticException() {
if (!wasSuccessful())
return null;
if (isAuthenticData)
return null;
if (dnssecResultNotAuthenticException == null) {
dnssecResultNotAuthenticException = DnssecResultNotAuthenticException.from(getUnverifiedReasons());
}
return dnssecResultNotAuthenticException;
}
/**
* Get the raw answer DNS message we received. <b>This is likely not what you want</b>, try {@link #getAnswers()} instead.
*
* @return the raw answer DNS Message.
* @see #getAnswers()
*/
public DnsMessage getRawAnswer() {
return answer;
}
public DnsQueryResult getDnsQueryResult() {
return result;
}
@Override
public final String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getName()).append('\n')
.append("Question: ").append(question).append('\n')
.append("Response Code: ").append(responseCode).append('\n');
if (responseCode == RESPONSE_CODE.NO_ERROR) {
if (isAuthenticData) {
sb.append("Results verified via DNSSEC\n");
}
if (hasUnverifiedReasons()) {
sb.append(unverifiedReasons).append('\n');
}
sb.append(answer.answerSection);
}
return sb.toString();
}
boolean hasUnverifiedReasons() {
return unverifiedReasons != null && !unverifiedReasons.isEmpty();
}
protected void throwIseIfErrorResponse() {
ResolutionUnsuccessfulException resolutionUnsuccessfulException = getResolutionUnsuccessfulException();
if (resolutionUnsuccessfulException != null)
throw new IllegalStateException("Can not perform operation because the DNS resolution was unsuccessful",
resolutionUnsuccessfulException);
}
}

View file

@ -0,0 +1,204 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.hla;
import java.io.IOException;
import java.net.InetAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.IdentityHashMap;
import java.util.List;
import java.util.Set;
import org.minidns.AbstractDnsClient.IpVersionSetting;
import org.minidns.MiniDnsException.NullResultException;
import org.minidns.dnsname.DnsName;
import org.minidns.hla.srv.SrvServiceProto;
import org.minidns.record.A;
import org.minidns.record.AAAA;
import org.minidns.record.InternetAddressRR;
import org.minidns.record.SRV;
import org.minidns.util.SrvUtil;
public class SrvResolverResult extends ResolverResult<SRV> {
private final ResolverApi resolver;
private final IpVersionSetting ipVersion;
private final SrvServiceProto srvServiceProto;
private List<ResolvedSrvRecord> sortedSrvResolvedAddresses;
SrvResolverResult(ResolverResult<SRV> srvResult, SrvServiceProto srvServiceProto, ResolverApi resolver) throws NullResultException {
super(srvResult.question, srvResult.result, srvResult.unverifiedReasons);
this.resolver = resolver;
this.ipVersion = resolver.getClient().getPreferedIpVersion();
this.srvServiceProto = srvServiceProto;
}
/**
* Get a list ordered by priority and weight of the resolved SRV records. This method will throw if there was an
* error response or if subsequent {@link A} or {@link AAAA} resource record lookups fail. It will return
* {@code null} in case the service is decidedly not available at this domain.
*
* @return a list ordered by priority and weight of the related SRV records.
* @throws IOException in case an I/O error occurs.
*/
public List<ResolvedSrvRecord> getSortedSrvResolvedAddresses() throws IOException {
if (sortedSrvResolvedAddresses != null) {
return sortedSrvResolvedAddresses;
}
throwIseIfErrorResponse();
if (isServiceDecidedlyNotAvailableAtThisDomain()) {
return null;
}
List<SRV> srvRecords = SrvUtil.sortSrvRecords(getAnswers());
List<ResolvedSrvRecord> res = new ArrayList<>(srvRecords.size());
for (SRV srvRecord : srvRecords) {
ResolverResult<A> aRecordsResult = null;
ResolverResult<AAAA> aaaaRecordsResult = null;
Set<A> aRecords = Collections.emptySet();
if (ipVersion.v4) {
aRecordsResult = resolver.resolve(srvRecord.target, A.class);
if (aRecordsResult.wasSuccessful() && !aRecordsResult.hasUnverifiedReasons()) {
aRecords = aRecordsResult.getAnswers();
}
}
Set<AAAA> aaaaRecords = Collections.emptySet();
if (ipVersion.v6) {
aaaaRecordsResult = resolver.resolve(srvRecord.target, AAAA.class);
if (aaaaRecordsResult.wasSuccessful() && !aaaaRecordsResult.hasUnverifiedReasons()) {
aaaaRecords = aaaaRecordsResult.getAnswers();
}
}
if (aRecords.isEmpty() && aaaaRecords.isEmpty()) {
// TODO Possibly check for (C|D)NAME usage and throw a meaningful exception that it is not allowed for
// the target of an SRV to be an alias as per RFC 2782.
/*
ResolverResult<CNAME> cnameRecordResult = resolve(srvRecord.name, CNAME.class);
if (cnameRecordResult.wasSuccessful()) {
}
*/
continue;
}
List<InternetAddressRR<? extends InetAddress>> srvAddresses = new ArrayList<>(aRecords.size() + aaaaRecords.size());
switch (ipVersion) {
case v4only:
srvAddresses.addAll(aRecords);
break;
case v6only:
srvAddresses.addAll(aaaaRecords);
break;
case v4v6:
srvAddresses.addAll(aRecords);
srvAddresses.addAll(aaaaRecords);
break;
case v6v4:
srvAddresses.addAll(aaaaRecords);
srvAddresses.addAll(aRecords);
break;
}
ResolvedSrvRecord resolvedSrvAddresses = new ResolvedSrvRecord(question.name, srvServiceProto, srvRecord, srvAddresses,
aRecordsResult, aaaaRecordsResult);
res.add(resolvedSrvAddresses);
}
sortedSrvResolvedAddresses = res;
return res;
}
public boolean isServiceDecidedlyNotAvailableAtThisDomain() {
Set<SRV> answers = getAnswers();
if (answers.size() != 1) {
return false;
}
SRV singleAnswer = answers.iterator().next();
return !singleAnswer.isServiceAvailable();
}
public static final class ResolvedSrvRecord {
public final DnsName name;
public final SrvServiceProto srvServiceProto;
public final SRV srv;
public final List<InternetAddressRR<? extends InetAddress>> addresses;
public final ResolverResult<A> aRecordsResult;
public final ResolverResult<AAAA> aaaaRecordsResult;
/**
* The port announced by the SRV RR. This is simply a shortcut for <code>srv.port</code>.
*/
public final int port;
private ResolvedSrvRecord(DnsName name, SrvServiceProto srvServiceProto, SRV srv,
List<InternetAddressRR<? extends InetAddress>> addresses, ResolverResult<A> aRecordsResult,
ResolverResult<AAAA> aaaaRecordsResult) {
this.name = name;
this.srvServiceProto = srvServiceProto;
this.srv = srv;
this.addresses = Collections.unmodifiableList(addresses);
this.port = srv.port;
this.aRecordsResult = aRecordsResult;
this.aaaaRecordsResult = aaaaRecordsResult;
}
}
/**
* Convenience method to sort multiple resolved SRV RRs. This is for example required by XEP-0368, where
* {@link org.minidns.hla.srv.SrvService#xmpp_client} and {@link org.minidns.hla.srv.SrvService#xmpps_client} may be
* sorted together.
*
* @param resolvedSrvRecordCollections a collection of resolved SRV records.
* @return a list ordered by priority and weight of the related SRV records.
*/
@SafeVarargs
public static List<ResolvedSrvRecord> sortMultiple(Collection<ResolvedSrvRecord>... resolvedSrvRecordCollections) {
int srvRecordsCount = 0;
for (Collection<ResolvedSrvRecord> resolvedSrvRecords : resolvedSrvRecordCollections) {
if (resolvedSrvRecords == null) {
continue;
}
srvRecordsCount += resolvedSrvRecords.size();
}
List<SRV> srvToSort = new ArrayList<>(srvRecordsCount);
IdentityHashMap<SRV, ResolvedSrvRecord> identityMap = new IdentityHashMap<>(srvRecordsCount);
for (Collection<ResolvedSrvRecord> resolvedSrvRecords : resolvedSrvRecordCollections) {
if (resolvedSrvRecords == null) {
continue;
}
for (ResolvedSrvRecord resolvedSrvRecord : resolvedSrvRecords) {
srvToSort.add(resolvedSrvRecord.srv);
identityMap.put(resolvedSrvRecord.srv, resolvedSrvRecord);
}
}
List<SRV> sortedSrvs = SrvUtil.sortSrvRecords(srvToSort);
assert sortedSrvs.size() == srvRecordsCount;
List<ResolvedSrvRecord> res = new ArrayList<>(srvRecordsCount);
for (SRV sortedSrv : sortedSrvs) {
ResolvedSrvRecord resolvedSrvRecord = identityMap.get(sortedSrv);
res.add(resolvedSrvRecord);
}
return res;
}
}

View file

@ -0,0 +1,29 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.hla.srv;
import org.minidns.dnslabel.DnsLabel;
public enum SrvProto {
// @formatter:off
tcp,
udp,
;
// @formatter:on
@SuppressWarnings("ImmutableEnumChecker")
public final DnsLabel dnsLabel;
SrvProto() {
dnsLabel = DnsLabel.from('_' + name());
}
}

View file

@ -0,0 +1,44 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.hla.srv;
import org.minidns.dnslabel.DnsLabel;
public enum SrvService {
// @formatter:off
xmpp_client,
xmpp_server,
/**
* XMPP client-to-server (c2s) connections using implicit TLS (also known as "Direct TLS").
*
* @see <a href="https://xmpp.org/extensions/xep-0368.html">XEP-0368: SRV records for XMPP over TLS</a>
*/
xmpps_client,
/**
* XMPP server-to-server (s2s) connections using implicit TLS (also known as "Direct TLS").
*
* @see <a href="https://xmpp.org/extensions/xep-0368.html">XEP-0368: SRV records for XMPP over TLS</a>
*/
xmpps_server,
;
// @formatter:on
@SuppressWarnings("ImmutableEnumChecker")
public final DnsLabel dnsLabel;
SrvService() {
String enumName = name().replaceAll("_", "-");
dnsLabel = DnsLabel.from('_' + enumName);
}
}

View file

@ -0,0 +1,27 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.hla.srv;
import org.minidns.dnslabel.DnsLabel;
/**
* The Serivce and Protocol part of a SRV owner name. The format of a SRV owner name is "_Service._Proto.Name".
*/
public class SrvServiceProto {
public final DnsLabel service;
public final DnsLabel proto;
public SrvServiceProto(DnsLabel service, DnsLabel proto) {
this.service = service;
this.proto = proto;
}
}

View file

@ -0,0 +1,28 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.hla.srv;
public enum SrvType {
// @formatter:off
xmpp_client(SrvService.xmpp_client, SrvProto.tcp),
xmpp_server(SrvService.xmpp_server, SrvProto.tcp),
;
// @formatter:on
public final SrvService service;
public final SrvProto proto;
SrvType(SrvService service, SrvProto proto) {
this.service = service;
this.proto = proto;
}
}

View file

@ -0,0 +1,36 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.idna;
import java.net.IDN;
import org.minidns.dnsname.DnsName;
public class DefaultIdnaTransformator implements IdnaTransformator {
@Override
public String toASCII(String input) {
// Special case if input is ".", i.e. a string containing only a single dot character. This is a workaround for
// IDN.toASCII() implementations throwing an IllegalArgumentException on this input string (for example Android
// APIs level 26, see https://issuetracker.google.com/issues/113070416).
if (DnsName.ROOT.ace.equals(input)) {
return DnsName.ROOT.ace;
}
return IDN.toASCII(input);
}
@Override
public String toUnicode(String input) {
return IDN.toUnicode(input);
}
}

View file

@ -0,0 +1,19 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.idna;
public interface IdnaTransformator {
String toASCII(String input);
String toUnicode(String input);
}

View file

@ -0,0 +1,31 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.idna;
public class MiniDnsIdna {
private static IdnaTransformator idnaTransformator = new DefaultIdnaTransformator();
public static String toASCII(String string) {
return idnaTransformator.toASCII(string);
}
public static String toUnicode(String string) {
return idnaTransformator.toUnicode(string);
}
public static void setActiveTransformator(IdnaTransformator idnaTransformator) {
if (idnaTransformator == null) {
throw new IllegalArgumentException();
}
MiniDnsIdna.idnaTransformator = idnaTransformator;
}
}

View file

@ -0,0 +1,56 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.integrationtest;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.IOException;
import org.minidns.DnsClient;
import org.minidns.record.Record;
import org.minidns.MiniDnsFuture;
import org.minidns.dnsmessage.DnsMessage.RESPONSE_CODE;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.source.AbstractDnsDataSource;
import org.minidns.source.AbstractDnsDataSource.QueryMode;
import org.minidns.source.async.AsyncNetworkDataSource;
public class AsyncApiTest {
public static void main(String[] args) throws IOException {
tcpAsyncApiTest();
}
public static void simpleAsyncApiTest() throws IOException {
DnsClient client = new DnsClient();
client.setDataSource(new AsyncNetworkDataSource());
client.getDataSource().setTimeout(60 * 60 * 1000);
MiniDnsFuture<DnsQueryResult, IOException> future = client.queryAsync("example.com", Record.TYPE.NS);
DnsQueryResult result = future.getOrThrow();
assertEquals(RESPONSE_CODE.NO_ERROR, result.response.responseCode);
}
public static void tcpAsyncApiTest() throws IOException {
AbstractDnsDataSource dataSource = new AsyncNetworkDataSource();
dataSource.setTimeout(60 * 60 * 1000);
dataSource.setUdpPayloadSize(256);
dataSource.setQueryMode(QueryMode.tcp);
DnsClient client = new DnsClient();
client.setDataSource(dataSource);
client.setAskForDnssec(true);
MiniDnsFuture<DnsQueryResult, IOException> future = client.queryAsync("google.com", Record.TYPE.AAAA);
DnsQueryResult result = future.getOrThrow();
assertEquals(RESPONSE_CODE.NO_ERROR, result.response.responseCode);
}
}

View file

@ -0,0 +1,58 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.integrationtest;
import org.minidns.DnsClient;
import org.minidns.cache.LruCache;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.record.Data;
import org.minidns.record.Record;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
public class CoreTest {
@IntegrationTest
public static void testExampleCom() throws IOException {
DnsClient client = new DnsClient(new LruCache(1024));
String exampleIp4 = "93.184.216.34"; // stable?
String exampleIp6 = "2606:2800:220:1:248:1893:25c8:1946"; // stable?
assertEquals(client.query("example.com", Record.TYPE.A).response.answerSection.get(0).payloadData.toString(), exampleIp4);
assertEquals(client.query("www.example.com", Record.TYPE.A).response.answerSection.get(0).payloadData.toString(), exampleIp4);
assertEquals(client.query("example.com", Record.TYPE.AAAA).response.answerSection.get(0).payloadData.toString(), exampleIp6);
assertEquals(client.query("www.example.com", Record.TYPE.AAAA).response.answerSection.get(0).payloadData.toString(), exampleIp6);
DnsQueryResult nsResult = client.query("example.com", Record.TYPE.NS);
List<String> values = new ArrayList<>();
for (Record<? extends Data> record : nsResult.response.answerSection) {
values.add(record.payloadData.toString());
}
Collections.sort(values);
assertEquals(values.get(0), "a.iana-servers.net.");
assertEquals(values.get(1), "b.iana-servers.net.");
}
@IntegrationTest
public static void testTcpAnswer() throws IOException {
DnsClient client = new DnsClient(new LruCache(1024));
client.setAskForDnssec(true);
client.setDisableResultFilter(true);
DnsQueryResult result = client.query("www-nsec.example.com", Record.TYPE.A);
assertNotNull(result);
assertTrue(result.response.toArray().length > 512);
}
}

View file

@ -0,0 +1,45 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.integrationtest;
import org.minidns.dane.DaneVerifier;
import javax.net.ssl.HttpsURLConnection;
import org.junit.Ignore;
import java.io.IOException;
import java.net.URL;
import java.security.cert.CertificateException;
public class DaneTest {
@Ignore
@IntegrationTest
public static void testOarcDaneGood() throws IOException, CertificateException {
DaneVerifier daneVerifier = new DaneVerifier();
daneVerifier.verifiedConnect((HttpsURLConnection) new URL("https://good.dane.dns-oarc.net/").openConnection());
}
@Ignore
@IntegrationTest()
public static void testOarcDaneBadHash() throws IOException, CertificateException {
DaneVerifier daneVerifier = new DaneVerifier();
daneVerifier.verifiedConnect((HttpsURLConnection) new URL("https://bad-hash.dane.dns-oarc.net/").openConnection());
}
@Ignore
@IntegrationTest
public static void testOarcDaneBadParams() throws IOException, CertificateException {
DaneVerifier daneVerifier = new DaneVerifier();
daneVerifier.verifiedConnect((HttpsURLConnection) new URL("https://bad-params.dane.dns-oarc.net/").openConnection());
}
}

View file

@ -0,0 +1,65 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.integrationtest;
import java.io.IOException;
import java.util.Iterator;
import org.junit.Ignore;
import org.minidns.cache.LruCache;
import org.minidns.dnssec.DnssecClient;
import org.minidns.dnssec.DnssecQueryResult;
import org.minidns.dnssec.DnssecUnverifiedReason;
import org.minidns.dnssec.DnssecValidationFailedException;
import org.minidns.record.Record;
import static org.junit.jupiter.api.Assertions.assertFalse;
public class DnssecTest {
@Ignore
@IntegrationTest
public static void testOarcDaneBadSig() throws Exception {
DnssecClient client = new DnssecClient(new LruCache(1024));
assertFalse(client.queryDnssec("_443._tcp.bad-sig.dane.dns-oarc.net", Record.TYPE.TLSA).isAuthenticData());
}
@IntegrationTest
public static void testUniDueSigOk() throws IOException {
DnssecClient client = new DnssecClient(new LruCache(1024));
assertAuthentic(client.queryDnssec("sigok.verteiltesysteme.net", Record.TYPE.A));
}
@IntegrationTest(expected = DnssecValidationFailedException.class)
public static void testUniDueSigFail() throws IOException {
DnssecClient client = new DnssecClient(new LruCache(1024));
client.query("sigfail.verteiltesysteme.net", Record.TYPE.A);
}
@IntegrationTest
public static void testCloudFlare() throws IOException {
DnssecClient client = new DnssecClient(new LruCache(1024));
assertAuthentic(client.queryDnssec("www.cloudflare-dnssec-auth.com", Record.TYPE.A));
}
private static void assertAuthentic(DnssecQueryResult dnssecMessage) {
if (dnssecMessage.isAuthenticData()) return;
StringBuilder sb = new StringBuilder();
sb.append("Answer should contain authentic data while it does not. Reasons:\n");
for (Iterator<DnssecUnverifiedReason> it = dnssecMessage.getUnverifiedReasons().iterator(); it.hasNext(); ) {
DnssecUnverifiedReason unverifiedReason = it.next();
sb.append(unverifiedReason);
if (it.hasNext()) sb.append('\n');
}
throw new AssertionError(sb.toString());
}
}

View file

@ -0,0 +1,57 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.integrationtest;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import java.util.Set;
import org.minidns.hla.ResolverApi;
import org.minidns.hla.ResolverResult;
import org.minidns.hla.SrvResolverResult;
import org.minidns.record.A;
import org.minidns.record.SRV;
public class HlaTest {
@IntegrationTest
public static void resolverTest() throws IOException {
ResolverResult<A> res = ResolverApi.INSTANCE.resolve("geekplace.eu", A.class);
assertEquals(true, res.wasSuccessful());
Set<A> answers = res.getAnswers();
assertEquals(1, answers.size());
assertArrayEquals(new A(5, 45, 100, 158).toByteArray(), answers.iterator().next().toByteArray());
}
@IntegrationTest
public static void idnSrvTest() throws IOException {
ResolverResult<SRV> res = ResolverApi.INSTANCE.resolve("_xmpp-client._tcp.im.plä.net", SRV.class);
Set<SRV> answers = res.getAnswers();
assertEquals(1, answers.size());
SRV srv = answers.iterator().next();
ResolverResult<A> aRes = ResolverApi.INSTANCE.resolve(srv.target, A.class);
assertTrue(aRes.wasSuccessful());
}
@IntegrationTest
public static void resolveSrvTest() throws IOException {
SrvResolverResult resolverResult = ResolverApi.INSTANCE.resolveSrv("_xmpp-client._tcp.jabber.org");
Set<SRV> answers = resolverResult.getAnswers();
assertFalse(answers.isEmpty());
}
}

View file

@ -0,0 +1,22 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.integrationtest;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface IntegrationTest {
Class<?> expected() default Class.class;
}

View file

@ -0,0 +1,133 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.integrationtest;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.junit.Ignore;
import org.minidns.dnsname.DnsName;
import org.minidns.jul.MiniDnsJul;
import org.minidns.record.Record.TYPE;
public class IntegrationTestHelper {
public static final DnsName DNSSEC_DOMAIN = DnsName.from("verteiltesysteme.net");
public static final TYPE RR_TYPE = TYPE.A;
private static Set<Class<?>> testClasses = new HashSet<>();
private static Logger LOGGER = Logger.getLogger(IntegrationTestHelper.class.getName());
enum TestResult {
Success,
Failure,
}
static {
testClasses.add(CoreTest.class);
testClasses.add(DnssecTest.class);
testClasses.add(DaneTest.class);
testClasses.add(HlaTest.class);
testClasses.add(NsidTest.class);
testClasses.add(IterativeDnssecTest.class);
}
private static final String MINTTEST = "minttest.";
public static void main(String[] args) {
Properties systemProperties = System.getProperties();
String debugString = systemProperties.getProperty(MINTTEST + "debug", Boolean.toString(false));
boolean debug = Boolean.parseBoolean(debugString);
if (debug) {
LOGGER.info("Enabling debug and trace output");
MiniDnsJul.enableMiniDnsTrace();
}
int testsRun = 0;
List<Method> successfulTests = new ArrayList<>();
List<Method> failedTests = new ArrayList<>();
List<Method> ignoredTests = new ArrayList<>();
for (final Class<?> aClass : testClasses) {
for (final Method method : aClass.getDeclaredMethods()) {
if (!method.isAnnotationPresent(IntegrationTest.class)) {
continue;
}
if (method.isAnnotationPresent(Ignore.class)) {
ignoredTests.add(method);
continue;
}
TestResult result = invokeTest(method, aClass);
testsRun++;
switch (result) {
case Success:
successfulTests.add(method);
break;
case Failure:
failedTests.add(method);
break;
}
}
}
StringBuilder resultMessage = new StringBuilder();
resultMessage.append("MiniDNS Integration Test Result: [").append(successfulTests.size()).append('/').append(testsRun).append("] ");
if (!ignoredTests.isEmpty()) {
resultMessage.append("(Ignored: ").append(ignoredTests.size()).append(") ");
}
int exitStatus = 0;
if (failedTests.isEmpty()) {
resultMessage.append("SUCCESS \\o/");
} else {
resultMessage.append("FAILURE :(");
exitStatus = 2;
}
LOGGER.info(resultMessage.toString());
System.exit(exitStatus);
}
public static TestResult invokeTest(Method method, Class<?> aClass) {
Class<?> expected = method.getAnnotation(IntegrationTest.class).expected();
if (!Exception.class.isAssignableFrom(expected)) expected = null;
String testClassName = method.getDeclaringClass().getSimpleName();
String testMethodName = method.getName();
LOGGER.logp(Level.INFO, testClassName, testMethodName, "Test start.");
try {
method.invoke(null);
if (expected != null) {
LOGGER.logp(Level.WARNING, testClassName, testMethodName, "Test failed: expected exception " + expected + " was not thrown!");
return TestResult.Failure;
} else {
LOGGER.logp(Level.INFO, testClassName, testMethodName, "Test suceeded.");
return TestResult.Success;
}
} catch (InvocationTargetException e) {
if (expected != null && expected.isAssignableFrom(e.getTargetException().getClass())) {
LOGGER.logp(Level.INFO, testClassName, testMethodName, "Test suceeded.");
return TestResult.Success;
} else {
LOGGER.logp(Level.WARNING, testClassName, testMethodName, "Test failed: unexpected exception was thrown: ", e.getTargetException());
return TestResult.Failure;
}
} catch (IllegalAccessException | NullPointerException e) {
LOGGER.logp(Level.SEVERE, testClassName, testMethodName, "Test failed: could not invoke test, is it public static?");
return TestResult.Failure;
}
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.integrationtest;
import org.minidns.DnsCache;
import org.minidns.cache.ExtendedLruCache;
import org.minidns.cache.FullLruCache;
import org.minidns.cache.LruCache;
import org.minidns.dnssec.DnssecClient;
import org.minidns.source.NetworkDataSourceWithAccounting;
public class IntegrationTestTools {
public enum CacheConfig {
without,
normal,
extended,
full,
}
public static DnssecClient getClient(CacheConfig cacheConfig) {
DnsCache cache;
switch (cacheConfig) {
case without:
cache = null;
break;
case normal:
cache = new LruCache();
break;
case extended:
cache = new ExtendedLruCache();
break;
case full:
cache = new FullLruCache();
break;
default:
throw new IllegalStateException();
}
DnssecClient client = new DnssecClient(cache);
client.setDataSource(new NetworkDataSourceWithAccounting());
return client;
}
}

View file

@ -0,0 +1,55 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.integrationtest;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.IOException;
import org.minidns.dnsname.DnsName;
import org.minidns.dnssec.DnssecClient;
import org.minidns.dnssec.DnssecQueryResult;
import org.minidns.integrationtest.IntegrationTestTools.CacheConfig;
import org.minidns.iterative.ReliableDnsClient.Mode;
import org.minidns.record.Record.TYPE;
import org.minidns.source.NetworkDataSourceWithAccounting;
public class IterativeDnssecTest {
private static final DnsName DNSSEC_DOMAIN = IntegrationTestHelper.DNSSEC_DOMAIN;
private static final TYPE RR_TYPE = IntegrationTestHelper.RR_TYPE;
@IntegrationTest
public static void shouldRequireLessQueries() throws IOException {
DnssecClient normalCacheClient = getClient(CacheConfig.normal);
DnssecQueryResult normalCacheResult = normalCacheClient.queryDnssec(DNSSEC_DOMAIN, RR_TYPE);
assertTrue(normalCacheResult.isAuthenticData());
NetworkDataSourceWithAccounting normalCacheNdswa = NetworkDataSourceWithAccounting.from(normalCacheClient);
DnssecClient extendedCacheClient = getClient(CacheConfig.extended);
DnssecQueryResult extendedCacheResult = extendedCacheClient.queryDnssec(DNSSEC_DOMAIN, RR_TYPE);
assertTrue(extendedCacheResult.isAuthenticData());
NetworkDataSourceWithAccounting extendedCacheNdswa = NetworkDataSourceWithAccounting.from(extendedCacheClient);
final int normalCacheSuccessfulQueries = normalCacheNdswa.getStats().successfulQueries;
final int extendedCacheSuccessfulQueries = extendedCacheNdswa.getStats().successfulQueries;
assertTrue(
normalCacheSuccessfulQueries > extendedCacheSuccessfulQueries,
"Extend cache successful query count " + extendedCacheSuccessfulQueries
+ " is not less than normal cache successful query count " + normalCacheSuccessfulQueries);
}
private static DnssecClient getClient(CacheConfig cacheConfig) {
DnssecClient client = IntegrationTestTools.getClient(cacheConfig);
client.setMode(Mode.iterativeOnly);
return client;
}
}

View file

@ -0,0 +1,52 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.integrationtest;
import java.io.IOException;
import java.net.InetAddress;
import org.minidns.DnsClient;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.edns.Nsid;
import org.minidns.edns.Edns.OptionCode;
import org.minidns.iterative.IterativeDnsClient;
import org.minidns.record.Record.TYPE;
import static org.junit.jupiter.api.Assertions.assertNotNull;
public class NsidTest {
@IntegrationTest
public static Nsid testNsidLRoot() {
DnsClient client = new DnsClient(null) {
@Override
protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) {
message.getEdnsBuilder().addEdnsOption(Nsid.REQUEST);
return super.newQuestion(message);
}
};
DnsQueryResult result = null;
Question q = new Question("de", TYPE.NS);
for (InetAddress lRoot : IterativeDnsClient.getRootServer('l')) {
try {
result = client.query(q, lRoot);
} catch (IOException e) {
continue;
}
break;
}
Nsid nsid = result.response.getEdns().getEdnsOption(OptionCode.NSID);
assertNotNull(nsid);
return nsid;
}
}

View file

@ -0,0 +1,93 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.iterative;
import java.net.InetAddress;
import org.minidns.MiniDnsException;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsqueryresult.DnsQueryResult;
public abstract class IterativeClientException extends MiniDnsException {
/**
*
*/
private static final long serialVersionUID = 1L;
protected IterativeClientException(String message) {
super(message);
}
public static class LoopDetected extends IterativeClientException {
/**
*
*/
private static final long serialVersionUID = 1L;
public final InetAddress address;
public final Question question;
public LoopDetected(InetAddress address, Question question) {
super("Resolution loop detected: We already asked " + address + " about " + question);
this.address = address;
this.question = question;
}
}
public static class MaxIterativeStepsReached extends IterativeClientException {
/**
*
*/
private static final long serialVersionUID = 1L;
public MaxIterativeStepsReached() {
super("Maxmimum steps reached");
}
}
public static class NotAuthoritativeNorGlueRrFound extends IterativeClientException {
/**
*
*/
private static final long serialVersionUID = 1L;
private final DnsMessage request;
private final DnsQueryResult result;
private final DnsName authoritativeZone;
public NotAuthoritativeNorGlueRrFound(DnsMessage request, DnsQueryResult result, DnsName authoritativeZone) {
super("Did not receive an authoritative answer, nor did the result contain any glue records");
this.request = request;
this.result = result;
this.authoritativeZone = authoritativeZone;
}
public DnsMessage getRequest() {
return request;
}
public DnsQueryResult getResult() {
return result;
}
public DnsName getAuthoritativeZone() {
return authoritativeZone;
}
}
}

View file

@ -0,0 +1,502 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.iterative;
import static org.minidns.constants.DnsRootServer.getIpv4RootServerById;
import static org.minidns.constants.DnsRootServer.getIpv6RootServerById;
import static org.minidns.constants.DnsRootServer.getRandomIpv4RootServer;
import static org.minidns.constants.DnsRootServer.getRandomIpv6RootServer;
import org.minidns.AbstractDnsClient;
import org.minidns.DnsCache;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsname.DnsName;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.iterative.IterativeClientException.LoopDetected;
import org.minidns.iterative.IterativeClientException.NotAuthoritativeNorGlueRrFound;
import org.minidns.record.A;
import org.minidns.record.AAAA;
import org.minidns.record.RRWithTarget;
import org.minidns.record.Record;
import org.minidns.record.Record.TYPE;
import org.minidns.record.Data;
import org.minidns.record.InternetAddressRR;
import org.minidns.record.NS;
import org.minidns.util.MultipleIoException;
import java.io.IOException;
import java.net.Inet4Address;
import java.net.Inet6Address;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
import java.util.logging.Level;
public class IterativeDnsClient extends AbstractDnsClient {
int maxSteps = 128;
/**
* Create a new recursive DNS client using the global default cache.
*/
public IterativeDnsClient() {
super();
}
/**
* Create a new recursive DNS client with the given DNS cache.
*
* @param cache The backend DNS cache.
*/
public IterativeDnsClient(DnsCache cache) {
super(cache);
}
/**
* Recursively query the DNS system for one entry.
*
* @param queryBuilder The query DNS message builder.
* @return The response (or null on timeout/error).
* @throws IOException if an IO error occurs.
*/
@Override
protected DnsQueryResult query(DnsMessage.Builder queryBuilder) throws IOException {
DnsMessage q = queryBuilder.build();
ResolutionState resolutionState = new ResolutionState(this);
DnsQueryResult result = queryRecursive(resolutionState, q);
return result;
}
private static InetAddress[] getTargets(Collection<? extends InternetAddressRR<? extends InetAddress>> primaryTargets,
Collection<? extends InternetAddressRR<? extends InetAddress>> secondaryTargets) {
InetAddress[] res = new InetAddress[2];
for (InternetAddressRR<? extends InetAddress> arr : primaryTargets) {
if (res[0] == null) {
res[0] = arr.getInetAddress();
// If secondaryTargets is empty, then try to get the second target out of the set of primaryTargets.
if (secondaryTargets.isEmpty()) {
continue;
}
}
if (res[1] == null) {
res[1] = arr.getInetAddress();
}
break;
}
for (InternetAddressRR<? extends InetAddress> arr : secondaryTargets) {
if (res[0] == null) {
res[0] = arr.getInetAddress();
continue;
}
if (res[1] == null) {
res[1] = arr.getInetAddress();
}
break;
}
return res;
}
private DnsQueryResult queryRecursive(ResolutionState resolutionState, DnsMessage q) throws IOException {
InetAddress primaryTarget = null, secondaryTarget = null;
Question question = q.getQuestion();
DnsName parent = question.name.getParent();
switch (ipVersionSetting) {
case v4only:
for (A a : getCachedIPv4NameserverAddressesFor(parent)) {
if (primaryTarget == null) {
primaryTarget = a.getInetAddress();
continue;
}
secondaryTarget = a.getInetAddress();
break;
}
break;
case v6only:
for (AAAA aaaa : getCachedIPv6NameserverAddressesFor(parent)) {
if (primaryTarget == null) {
primaryTarget = aaaa.getInetAddress();
continue;
}
secondaryTarget = aaaa.getInetAddress();
break;
}
break;
case v4v6:
InetAddress[] v4v6targets = getTargets(getCachedIPv4NameserverAddressesFor(parent), getCachedIPv6NameserverAddressesFor(parent));
primaryTarget = v4v6targets[0];
secondaryTarget = v4v6targets[1];
break;
case v6v4:
InetAddress[] v6v4targets = getTargets(getCachedIPv6NameserverAddressesFor(parent), getCachedIPv4NameserverAddressesFor(parent));
primaryTarget = v6v4targets[0];
secondaryTarget = v6v4targets[1];
break;
default:
throw new AssertionError();
}
DnsName authoritativeZone = parent;
if (primaryTarget == null) {
authoritativeZone = DnsName.ROOT;
switch (ipVersionSetting) {
case v4only:
primaryTarget = getRandomIpv4RootServer(insecureRandom);
break;
case v6only:
primaryTarget = getRandomIpv6RootServer(insecureRandom);
break;
case v4v6:
primaryTarget = getRandomIpv4RootServer(insecureRandom);
secondaryTarget = getRandomIpv6RootServer(insecureRandom);
break;
case v6v4:
primaryTarget = getRandomIpv6RootServer(insecureRandom);
secondaryTarget = getRandomIpv4RootServer(insecureRandom);
break;
}
}
List<IOException> ioExceptions = new ArrayList<>();
try {
return queryRecursive(resolutionState, q, primaryTarget, authoritativeZone);
} catch (IOException ioException) {
abortIfFatal(ioException);
ioExceptions.add(ioException);
}
if (secondaryTarget != null) {
try {
return queryRecursive(resolutionState, q, secondaryTarget, authoritativeZone);
} catch (IOException ioException) {
ioExceptions.add(ioException);
}
}
MultipleIoException.throwIfRequired(ioExceptions);
return null;
}
private DnsQueryResult queryRecursive(ResolutionState resolutionState, DnsMessage q, InetAddress address, DnsName authoritativeZone) throws IOException {
resolutionState.recurse(address, q);
DnsQueryResult dnsQueryResult = query(q, address);
DnsMessage resMessage = dnsQueryResult.response;
if (resMessage.authoritativeAnswer) {
return dnsQueryResult;
}
if (cache != null) {
cache.offer(q, dnsQueryResult, authoritativeZone);
}
List<Record<? extends Data>> authorities = resMessage.copyAuthority();
List<IOException> ioExceptions = new ArrayList<>();
// Glued NS first
for (Iterator<Record<? extends Data>> iterator = authorities.iterator(); iterator.hasNext(); ) {
Record<NS> record = iterator.next().ifPossibleAs(NS.class);
if (record == null) {
iterator.remove();
continue;
}
DnsName name = record.payloadData.target;
IpResultSet gluedNs = searchAdditional(resMessage, name);
for (Iterator<InetAddress> addressIterator = gluedNs.addresses.iterator(); addressIterator.hasNext(); ) {
InetAddress target = addressIterator.next();
DnsQueryResult recursive = null;
try {
recursive = queryRecursive(resolutionState, q, target, record.name);
} catch (IOException e) {
abortIfFatal(e);
LOGGER.log(Level.FINER, "Exception while recursing", e);
resolutionState.decrementSteps();
ioExceptions.add(e);
if (!addressIterator.hasNext()) {
iterator.remove();
}
continue;
}
return recursive;
}
}
// Try non-glued NS
for (Record<? extends Data> record : authorities) {
final Question question = q.getQuestion();
DnsName name = ((NS) record.payloadData).target;
// Loop prevention: If this non-glued NS equals the name we question for and if the question is about a A or
// AAAA RR, then we should not continue here as it would result in an endless loop.
if (question.name.equals(name) && (question.type == TYPE.A || question.type == TYPE.AAAA))
continue;
IpResultSet res = null;
try {
res = resolveIpRecursive(resolutionState, name);
} catch (IOException e) {
resolutionState.decrementSteps();
ioExceptions.add(e);
}
if (res == null) {
continue;
}
for (InetAddress target : res.addresses) {
DnsQueryResult recursive = null;
try {
recursive = queryRecursive(resolutionState, q, target, record.name);
} catch (IOException e) {
resolutionState.decrementSteps();
ioExceptions.add(e);
continue;
}
return recursive;
}
}
MultipleIoException.throwIfRequired(ioExceptions);
// Reaching this point means we did not receive an authoritative answer, nor
// where we able to find glue records or the IPs of the next nameservers.
throw new NotAuthoritativeNorGlueRrFound(q, dnsQueryResult, authoritativeZone);
}
private IpResultSet resolveIpRecursive(ResolutionState resolutionState, DnsName name) throws IOException {
IpResultSet.Builder res = newIpResultSetBuilder();
if (ipVersionSetting.v4) {
// TODO Try to retrieve A records for name out from cache.
Question question = new Question(name, TYPE.A);
final DnsMessage query = getQueryFor(question);
DnsQueryResult aDnsQueryResult = queryRecursive(resolutionState, query);
// TODO: queryRecurisve() should probably never return null. Verify that and then remove the follwing null check.
DnsMessage aMessage = aDnsQueryResult != null ? aDnsQueryResult.response : null;
if (aMessage != null) {
for (Record<? extends Data> answer : aMessage.answerSection) {
if (answer.isAnswer(question)) {
InetAddress inetAddress = inetAddressFromRecord(name.ace, (A) answer.payloadData);
res.ipv4Addresses.add(inetAddress);
} else if (answer.type == TYPE.CNAME && answer.name.equals(name)) {
return resolveIpRecursive(resolutionState, ((RRWithTarget) answer.payloadData).target);
}
}
}
}
if (ipVersionSetting.v6) {
// TODO Try to retrieve AAAA records for name out from cache.
Question question = new Question(name, TYPE.AAAA);
final DnsMessage query = getQueryFor(question);
DnsQueryResult aDnsQueryResult = queryRecursive(resolutionState, query);
// TODO: queryRecurisve() should probably never return null. Verify that and then remove the follwing null check.
DnsMessage aMessage = aDnsQueryResult != null ? aDnsQueryResult.response : null;
if (aMessage != null) {
for (Record<? extends Data> answer : aMessage.answerSection) {
if (answer.isAnswer(question)) {
InetAddress inetAddress = inetAddressFromRecord(name.ace, (AAAA) answer.payloadData);
res.ipv6Addresses.add(inetAddress);
} else if (answer.type == TYPE.CNAME && answer.name.equals(name)) {
return resolveIpRecursive(resolutionState, ((RRWithTarget) answer.payloadData).target);
}
}
}
}
return res.build();
}
@SuppressWarnings("incomplete-switch")
private IpResultSet searchAdditional(DnsMessage message, DnsName name) {
IpResultSet.Builder res = newIpResultSetBuilder();
for (Record<? extends Data> record : message.additionalSection) {
if (!record.name.equals(name)) {
continue;
}
switch (record.type) {
case A:
res.ipv4Addresses.add(inetAddressFromRecord(name.ace, (A) record.payloadData));
break;
case AAAA:
res.ipv6Addresses.add(inetAddressFromRecord(name.ace, (AAAA) record.payloadData));
break;
default:
break;
}
}
return res.build();
}
private static InetAddress inetAddressFromRecord(String name, A recordPayload) {
try {
return InetAddress.getByAddress(name, recordPayload.getIp());
} catch (UnknownHostException e) {
// This will never happen
throw new RuntimeException(e);
}
}
private static InetAddress inetAddressFromRecord(String name, AAAA recordPayload) {
try {
return InetAddress.getByAddress(name, recordPayload.getIp());
} catch (UnknownHostException e) {
// This will never happen
throw new RuntimeException(e);
}
}
public static List<InetAddress> getRootServer(char rootServerId) {
return getRootServer(rootServerId, DEFAULT_IP_VERSION_SETTING);
}
public static List<InetAddress> getRootServer(char rootServerId, IpVersionSetting setting) {
Inet4Address ipv4Root = getIpv4RootServerById(rootServerId);
Inet6Address ipv6Root = getIpv6RootServerById(rootServerId);
List<InetAddress> res = new ArrayList<>(2);
switch (setting) {
case v4only:
if (ipv4Root != null) {
res.add(ipv4Root);
}
break;
case v6only:
if (ipv6Root != null) {
res.add(ipv6Root);
}
break;
case v4v6:
if (ipv4Root != null) {
res.add(ipv4Root);
}
if (ipv6Root != null) {
res.add(ipv6Root);
}
break;
case v6v4:
if (ipv6Root != null) {
res.add(ipv6Root);
}
if (ipv4Root != null) {
res.add(ipv4Root);
}
break;
}
return res;
}
@Override
protected boolean isResponseCacheable(Question q, DnsQueryResult result) {
return result.response.authoritativeAnswer;
}
@Override
protected DnsMessage.Builder newQuestion(DnsMessage.Builder message) {
message.setRecursionDesired(false);
message.getEdnsBuilder().setUdpPayloadSize(dataSource.getUdpPayloadSize());
return message;
}
private IpResultSet.Builder newIpResultSetBuilder() {
return new IpResultSet.Builder(this.insecureRandom);
}
private static final class IpResultSet {
final List<InetAddress> addresses;
private IpResultSet(List<InetAddress> ipv4Addresses, List<InetAddress> ipv6Addresses, Random random) {
int size;
switch (DEFAULT_IP_VERSION_SETTING) {
case v4only:
size = ipv4Addresses.size();
break;
case v6only:
size = ipv6Addresses.size();
break;
case v4v6:
case v6v4:
default:
size = ipv4Addresses.size() + ipv6Addresses.size();
break;
}
if (size == 0) {
// Fast-path in case there were no addresses, which could happen e.g., if the NS records where not
// glued.
addresses = Collections.emptyList();
} else {
// Shuffle the addresses first, so that the load is better balanced.
if (DEFAULT_IP_VERSION_SETTING.v4) {
Collections.shuffle(ipv4Addresses, random);
}
if (DEFAULT_IP_VERSION_SETTING.v6) {
Collections.shuffle(ipv6Addresses, random);
}
List<InetAddress> addresses = new ArrayList<>(size);
// Now add the shuffled addresses to the result list.
switch (DEFAULT_IP_VERSION_SETTING) {
case v4only:
addresses.addAll(ipv4Addresses);
break;
case v6only:
addresses.addAll(ipv6Addresses);
break;
case v4v6:
addresses.addAll(ipv4Addresses);
addresses.addAll(ipv6Addresses);
break;
case v6v4:
addresses.addAll(ipv6Addresses);
addresses.addAll(ipv4Addresses);
break;
}
this.addresses = Collections.unmodifiableList(addresses);
}
}
private static final class Builder {
private final Random random;
private final List<InetAddress> ipv4Addresses = new ArrayList<>(8);
private final List<InetAddress> ipv6Addresses = new ArrayList<>(8);
private Builder(Random random) {
this.random = random;
}
public IpResultSet build() {
return new IpResultSet(ipv4Addresses, ipv6Addresses, random);
}
}
}
protected static void abortIfFatal(IOException ioException) throws IOException {
if (ioException instanceof LoopDetected) {
throw ioException;
}
}
}

View file

@ -0,0 +1,190 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.iterative;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.logging.Level;
import org.minidns.AbstractDnsClient;
import org.minidns.DnsCache;
import org.minidns.DnsClient;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.Question;
import org.minidns.dnsqueryresult.DnsQueryResult;
import org.minidns.source.DnsDataSource;
import org.minidns.util.MultipleIoException;
/**
* A DNS client using a reliable strategy. First the configured resolver of the
* system are used, then, in case there is no answer, a fall back to iterative
* resolving is performed.
*/
public class ReliableDnsClient extends AbstractDnsClient {
public enum Mode {
/**
* Try the recursive servers first and fallback to iterative resolving if it fails. This is the default mode.
*/
recursiveWithIterativeFallback,
/**
* Only try the recursive servers. This makes {@code ReliableDnsClient} behave like a {@link DnsClient}.
*/
recursiveOnly,
/**
* Only use iterative resolving. This makes {@code ReliableDnsClient} behave like a {@link IterativeDnsClient}.
*/
iterativeOnly,
}
private final IterativeDnsClient recursiveDnsClient;
private final DnsClient dnsClient;
private Mode mode = Mode.recursiveWithIterativeFallback;
public ReliableDnsClient(DnsCache dnsCache) {
super(dnsCache);
recursiveDnsClient = new IterativeDnsClient(dnsCache) {
@Override
protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
questionMessage = super.newQuestion(questionMessage);
return ReliableDnsClient.this.newQuestion(questionMessage);
}
// TODO: Rename dnsMessage to result.
@Override
protected boolean isResponseCacheable(Question q, DnsQueryResult dnsMessage) {
boolean res = super.isResponseCacheable(q, dnsMessage);
return ReliableDnsClient.this.isResponseCacheable(q, dnsMessage) && res;
}
};
dnsClient = new DnsClient(dnsCache) {
@Override
protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
questionMessage = super.newQuestion(questionMessage);
return ReliableDnsClient.this.newQuestion(questionMessage);
}
// TODO: Rename dnsMessage to result.
@Override
protected boolean isResponseCacheable(Question q, DnsQueryResult dnsMessage) {
boolean res = super.isResponseCacheable(q, dnsMessage);
return ReliableDnsClient.this.isResponseCacheable(q, dnsMessage) && res;
}
};
}
public ReliableDnsClient() {
this(DEFAULT_CACHE);
}
@Override
protected DnsQueryResult query(DnsMessage.Builder q) throws IOException {
DnsQueryResult dnsMessage = null;
String unacceptableReason = null;
List<IOException> ioExceptions = new ArrayList<>();
if (mode != Mode.iterativeOnly) {
// Try a recursive query.
try {
dnsMessage = dnsClient.query(q);
if (dnsMessage != null) {
unacceptableReason = isResponseAcceptable(dnsMessage.response);
if (unacceptableReason == null) {
return dnsMessage;
}
}
} catch (IOException ioException) {
ioExceptions.add(ioException);
}
}
// Abort if we the are in "recursive only" mode.
if (mode == Mode.recursiveOnly) return dnsMessage;
// Eventually log that we fall back to iterative mode.
final Level FALLBACK_LOG_LEVEL = Level.FINE;
if (LOGGER.isLoggable(FALLBACK_LOG_LEVEL) && mode != Mode.iterativeOnly) {
String logString = "Resolution fall back to iterative mode because: ";
if (!ioExceptions.isEmpty()) {
logString += ioExceptions.get(0);
} else if (dnsMessage == null) {
logString += " DnsClient did not return a response";
} else if (unacceptableReason != null) {
logString += unacceptableReason + ". Response:\n" + dnsMessage;
} else {
throw new AssertionError("This should never been reached");
}
LOGGER.log(FALLBACK_LOG_LEVEL, logString);
}
try {
dnsMessage = recursiveDnsClient.query(q);
assert dnsMessage != null;
} catch (IOException ioException) {
ioExceptions.add(ioException);
}
if (dnsMessage == null) {
assert !ioExceptions.isEmpty();
MultipleIoException.throwIfRequired(ioExceptions);
}
return dnsMessage;
}
@Override
protected DnsMessage.Builder newQuestion(DnsMessage.Builder questionMessage) {
return questionMessage;
}
@Override
protected boolean isResponseCacheable(Question q, DnsQueryResult result) {
return isResponseAcceptable(result.response) == null;
}
/**
* Check if the response from the system's nameserver is acceptable. Must return <code>null</code> if the response
* is acceptable, or a String describing why it is not acceptable. If the response is not acceptable then
* {@link ReliableDnsClient} will fall back to resolve the query iteratively.
*
* @param response the response we got from the system's nameserver.
* @return <code>null</code> if the response is acceptable, or a String if not.
*/
protected String isResponseAcceptable(DnsMessage response) {
return null;
}
@Override
public void setDataSource(DnsDataSource dataSource) {
super.setDataSource(dataSource);
recursiveDnsClient.setDataSource(dataSource);
dnsClient.setDataSource(dataSource);
}
/**
* Set the mode used when resolving queries.
*
* @param mode the mode to use.
*/
public void setMode(Mode mode) {
if (mode == null) {
throw new IllegalArgumentException("Mode must not be null.");
}
this.mode = mode;
}
public void setUseHardcodedDnsServers(boolean useHardcodedDnsServers) {
dnsClient.setUseHardcodedDnsServers(useHardcodedDnsServers);
}
}

View file

@ -0,0 +1,53 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.iterative;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import org.minidns.dnsmessage.DnsMessage;
import org.minidns.dnsmessage.Question;
import org.minidns.iterative.IterativeClientException.LoopDetected;
import org.minidns.iterative.IterativeClientException.MaxIterativeStepsReached;
public class ResolutionState {
private final IterativeDnsClient recursiveDnsClient;
private final HashMap<InetAddress, Set<Question>> map = new HashMap<>();
private int steps;
ResolutionState(IterativeDnsClient recursiveDnsClient) {
this.recursiveDnsClient = recursiveDnsClient;
}
void recurse(InetAddress address, DnsMessage query) throws LoopDetected, MaxIterativeStepsReached {
Question question = query.getQuestion();
if (!map.containsKey(address)) {
map.put(address, new HashSet<Question>());
} else if (map.get(address).contains(question)) {
throw new IterativeClientException.LoopDetected(address, question);
}
if (++steps > recursiveDnsClient.maxSteps) {
throw new IterativeClientException.MaxIterativeStepsReached();
}
boolean isNew = map.get(address).add(question);
assert isNew;
}
void decrementSteps() {
steps--;
}
}

View file

@ -0,0 +1,121 @@
/*
* Copyright 2015-2024 the original author or authors
*
* This software is licensed under the Apache License, Version 2.0,
* the GNU Lesser General Public License version 2 or later ("LGPL")
* and the WTFPL.
* You may choose either license to govern your use of this software only
* upon the condition that you accept all of the terms of either
* the Apache License 2.0, the LGPL 2.1+ or the WTFPL.
*/
package org.minidns.jul;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.nio.charset.StandardCharsets;
import java.time.Instant;
import java.time.format.DateTimeFormatter;
import java.util.logging.ConsoleHandler;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogManager;
import java.util.logging.LogRecord;
import java.util.logging.Logger;
@SuppressWarnings("DateFormatConstant")
public class MiniDnsJul {
private static final Logger LOGGER = Logger.getLogger(MiniDnsJul.class.getName());
private static final InputStream LOG_MANAGER_CONFIG = new ByteArrayInputStream((
// @formatter:off
"org.minidns.level=FINEST" + '\n'
).getBytes(StandardCharsets.UTF_8)
);
// @formatter:on
private static final DateTimeFormatter LONG_LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("hh:mm:ss.SSS");
private static final DateTimeFormatter SHORT_LOG_TIME_FORMAT = DateTimeFormatter.ofPattern("mm:ss.SSS");
private static final Handler CONSOLE_HANDLER = new ConsoleHandler();
private static boolean shortLog = true;
static {
try {
LogManager.getLogManager().readConfiguration(LOG_MANAGER_CONFIG);
} catch (SecurityException | IOException e) {
LOGGER.log(Level.SEVERE, "Could not apply MiniDNS JUL configuration", e);
}
CONSOLE_HANDLER.setLevel(Level.OFF);
CONSOLE_HANDLER.setFormatter(new Formatter() {
@Override
public String format(LogRecord logRecord) {
StringBuilder sb = new StringBuilder(256);
Instant date = Instant.ofEpochMilli(logRecord.getMillis());
String dateString;
if (shortLog) {
dateString = SHORT_LOG_TIME_FORMAT.format(date);
} else {
dateString = LONG_LOG_TIME_FORMAT.format(date);
}
sb.append(dateString).append(' ');
String level = logRecord.getLevel().toString();
if (shortLog) {
level = level.substring(0, 1);
}
sb.append(level).append(' ');
String loggerName = logRecord.getLoggerName();
if (shortLog) {
String[] parts = loggerName.split("\\.");
loggerName = parts[parts.length > 1 ? parts.length - 1 : 0];
}
sb.append(loggerName);
sb.append(' ').append(logRecord.getSourceMethodName());
if (shortLog) {
sb.append(' ');
} else {
sb.append('\n');
}
sb.append(formatMessage(logRecord));
if (logRecord.getThrown() != null) {
StringWriter sw = new StringWriter();
PrintWriter pw = new PrintWriter(sw);
// CHECKSTYLE:OFF
pw.println();
logRecord.getThrown().printStackTrace(pw);
// CHECKSTYLE:ON
pw.close();
sb.append(sw);
}
sb.append('\n');
return sb.toString();
}
});
Logger.getLogger("org.minidns").addHandler(CONSOLE_HANDLER);
}
public static void enableMiniDnsTrace() {
enableMiniDnsTrace(true);
}
public static void enableMiniDnsTrace(boolean shortLog) {
MiniDnsJul.shortLog = shortLog;
CONSOLE_HANDLER.setLevel(Level.FINEST);
}
public static void disableMiniDnsTrace() {
CONSOLE_HANDLER.setLevel(Level.OFF);
}
}

Some files were not shown because too many files have changed in this diff Show more