Add xmpp-dox module, support xmpp:// listeners/resolvers

This commit is contained in:
Travis Burtrum 2019-03-13 00:45:49 -04:00
parent f118f6410b
commit 9c925d2046
14 changed files with 515 additions and 2 deletions

View File

@ -49,6 +49,7 @@
</properties>
<modules>
<module>jDnsProxy</module>
<module>xmpp-dox</module>
</modules>
<build>
<finalName>${project.artifactId}</finalName>

View File

@ -14,18 +14,22 @@ Sample/default configuration is in [jdnsproxy.properties](https://github.com/mop
Build/run like so:
```
mvn clean package
java -jar target/jDnsProxy.jar ./jdnsproxy.properties
java -jar jDnsProxy/target/jDnsProxy.jar ./jdnsproxy.properties
# or with xmpp:// listener+resolver support:
java -cp jDnsProxy/target/jDnsProxy.jar:xmpp-dox/target/xmpp-dox.jar com.moparisthebest.dns.DnsProxy xmpp-dox/jdnsproxy.xmpp.resolver.properties
```
Implemented specs:
* [RFC-1035: DOMAIN NAMES - IMPLEMENTATION AND SPECIFICATION](https://tools.ietf.org/html/rfc1035)
* [RFC-7858: Specification for DNS over Transport Layer Security (TLS)](https://tools.ietf.org/html/rfc7858)
* [Draft: DNS Queries over HTTPS](https://tools.ietf.org/html/draft-hoffman-dns-over-https)
* [RFC 8484: DNS Queries over HTTPS (DoH)](http://tools.ietf.org/html/rfc8484)
* [Draft: Serving Stale Data to Improve DNS Resiliency](https://tools.ietf.org/html/draft-ietf-dnsop-serve-stale)
* [RFC-6891: Extension Mechanisms for DNS (EDNS(0))](https://tools.ietf.org/html/rfc6891)
* [DNS EDNS0 Option Codes (OPT)](https://www.iana.org/assignments/dns-parameters/dns-parameters.xhtml#dns-parameters-11)
* [RFC-3225: Indicating Resolver Support of DNSSEC](https://tools.ietf.org/html/rfc3225)
* [XEP-xxxx: DNS Queries over XMPP (DoX)](https://xmpp.org/extensions/inbox/dox.html)
Use these for quick testing:
```

34
xmpp-dox/dox.md Normal file
View File

@ -0,0 +1,34 @@
XEP-XXXX DNS Queries over XMPP (DoX)
------------------------------------
Submitted [XEP](https://xmpp.org/extensions/inbox/dox.html)
```
# put your jid+pass details in jdns.xmpp.resolver.properties
$ java -jar jDnsProxy.jar jdns.xmpp.resolver.properties &
$ dig -p5353 @127.0.0.1 +short +tcp example.org
93.184.216.34
```
wire format of protocol (this is the request+response from the query above, A record for example.org):
request:
```xml
<iq to='dns@example.org/listener' id='27tZp-7' type='get'>
<dns xmlns='urn:xmpp:dox:0'>vOIBIAABAAAAAAABB2V4YW1wbGUDb3JnAAABAAEAACkQAAAAAAAADAAKAAj5HO5JuEe+mA</dns>
</iq>
```
response:
```xml
<iq to='dns@example.org/resolver' id='27tZp-7' type='result'>
<dns xmlns='urn:xmpp:dox:0'>vOKBoAABAAEAAAABB2V4YW1wbGUDb3JnAAABAAHADAABAAEAAAhjAARduNgiAAApEAAAAAAAAAA</dns>
</iq>
```
The content of the dns element is the DNS on-the-wire format is defined in [RFC1035].
The body MUST be encoded with base64 [RFC4648].
Padding characters for base64 MUST NOT be included.
[RFC1035]: https://tools.ietf.org/html/rfc1035
[RFC4648]: https://tools.ietf.org/html/rfc4648

View File

@ -0,0 +1,52 @@
# minTtl: rewrite TTLs lower than this to this value, default 600, 0 disables this feature
minTtl=600
# staleResponseTimeout: milliseconds to wait for response to query before serving a stale record if we have it, default 1000
staleResponseTimeout=1000
# staleResponseTtl: TTL to apply to stale record when above timeout is met and stale record is served, default 10
staleResponseTtl=10
# cacheFile: path to file to persist cache to at an interval
cacheFile=dnscache.map
# cacheDelayMinutes: how often to write the cache to disk
cacheDelayMinutes=60
# packetQueueLength: maximum requests queued waiting for responses from upstream, all resolvers specified process from this queue, cached responses don't enter this queue, default 100, 0 means unlimited
packetQueueLength=100
# listeners: list of listeners, currently supports tcp:// and udp:// with no options, default 'tcp://127.0.0.1:5353 udp://127.0.0.1:5353'
# also supports xmpp:// (DNS-over-XMPP), put IP:port of XMPP server, along with username/password to login with
listeners=tcp://127.0.0.1:5353 udp://127.0.0.1:5353 xmpp://208.68.163.210:5222#user=anyjid@example.org/listener;pass=y0urPa55W0rDHere
# resolvers: list of resolvers with or without options, whitespace separated, options are in fragment separated by ;
# currently support tcp:// (regular DNS-over-TCP), tls:// (DNS-over-TLS), http:// https:// (DNS-over-HTTPS)
# both tls:// and https:// support option pubKeyPinsSha256 with a comma-separated list of base64 public key hashes like HPKP, not supplying this causes TLS connections to be unauthenticated (vulnerable to MITM)
# https:// also validates the hostname for now like a browser would
# default 'https://dns.google.com/experimental?ct#name=dns.google.com'
resolvers=\
tls://89.233.43.71#name=unicast.censurfridns.dk;pubKeyPinsSha256=wikE3jYAA6jQmXYTr/rbHeEPmC78dQwZbQp6WdrseEs= \
tls://145.100.185.15#name=dnsovertls.sinodun.com;pubKeyPinsSha256=62lKu9HsDVbyiPenApnc4sfmSYTHOVfFgL3pyB+cBL4= \
tls://145.100.185.16#name=dnsovertls1.sinodun.com;pubKeyPinsSha256=cE2ecALeE5B+urJhDrJlVFmf38cJLAvqekONvjvpqUA= \
tls://185.49.141.37#name=getdnsapi.net;pubKeyPinsSha256=foxZRnIh9gZpWnl+zEiKa0EJ2rdCGroMWm02gaxSc9Q= \
https://dns.google.com/experimental?ct#name=dns.google.com
#resolvers=https://dns.google.com/experimental?ct
#resolvers=tcp://8.8.4.4:53
#resolvers=tls://89.233.43.71:853#pubKeyPinsSha256=wikE3jYAA6jQmXYTr/rbHeEPmC78dQwZbQp6WdrseEs=
# below here are resolver options that may be defined here and/or at the resolver level, if both resolver level wins
# proxy: defines a proxy to use for all connections to this resolver, supports socks:// and http://, default none
#proxy=socks://127.0.0.1:9050
# pubKeyPinsSha256: should be on an individual resolver level, specify comma-seperated base64 public key hashes like HPKP, not supplying this causes TLS connections to be unauthenticated (vulnerable to MITM), default none
# https:// also validates the hostname for now like a browser would
#pubKeyPinsSha256=wikE3jYAA6jQmXYTr/rbHeEPmC78dQwZbQp6WdrseEs=
# maxRetries: maximum number of times a request is re-queued to be resolved upstream due to failure before giving up, this is maximum retries total, not per-resolver, default resolvers.length * 2
#maxRetries=5
# name: human-readable name of resolver, might end up in logs, default full resolver URI
#name=somename
# connectTimeout: TCP connection timeout in milliseconds to upstream resolver, default 500
connectTimeout=500

View File

@ -0,0 +1,48 @@
# minTtl: rewrite TTLs lower than this to this value, default 600, 0 disables this feature
minTtl=600
# staleResponseTimeout: milliseconds to wait for response to query before serving a stale record if we have it, default 1000
staleResponseTimeout=1000
# staleResponseTtl: TTL to apply to stale record when above timeout is met and stale record is served, default 10
staleResponseTtl=10
# cacheFile: path to file to persist cache to at an interval
cacheFile=dnscache.map
# cacheDelayMinutes: how often to write the cache to disk
cacheDelayMinutes=60
# packetQueueLength: maximum requests queued waiting for responses from upstream, all resolvers specified process from this queue, cached responses don't enter this queue, default 100, 0 means unlimited
packetQueueLength=100
# listeners: list of listeners, currently supports tcp:// and udp:// with no options, default 'tcp://127.0.0.1:5353 udp://127.0.0.1:5353'
listeners=tcp://127.0.0.1:5353 udp://127.0.0.1:5353
# resolvers: list of resolvers with or without options, whitespace separated, options are in fragment separated by ;
# currently support tcp:// (regular DNS-over-TCP), tls:// (DNS-over-TLS), http:// https:// (DNS-over-HTTPS)
# both tls:// and https:// support option pubKeyPinsSha256 with a comma-separated list of base64 public key hashes like HPKP, not supplying this causes TLS connections to be unauthenticated (vulnerable to MITM)
# https:// also validates the hostname for now like a browser would
# default 'https://dns.google.com/experimental?ct#name=dns.google.com'
# also supports xmpp:// (DNS-over-XMPP), put IP:port of XMPP server, along with username/password to login with, and a resolverJid
resolvers=\
xmpp://208.68.163.210:5222#user=anyjid@example.org/resolver;pass=y0urPa55W0rDHere;resolverJid=dns@moparisthebest.com/listener
#resolvers=https://dns.google.com/experimental?ct
#resolvers=tcp://8.8.4.4:53
#resolvers=tls://89.233.43.71:853#pubKeyPinsSha256=wikE3jYAA6jQmXYTr/rbHeEPmC78dQwZbQp6WdrseEs=
# below here are resolver options that may be defined here and/or at the resolver level, if both resolver level wins
# proxy: defines a proxy to use for all connections to this resolver, supports socks:// and http://, default none
#proxy=socks://127.0.0.1:9050
# pubKeyPinsSha256: should be on an individual resolver level, specify comma-seperated base64 public key hashes like HPKP, not supplying this causes TLS connections to be unauthenticated (vulnerable to MITM), default none
# https:// also validates the hostname for now like a browser would
#pubKeyPinsSha256=wikE3jYAA6jQmXYTr/rbHeEPmC78dQwZbQp6WdrseEs=
# maxRetries: maximum number of times a request is re-queued to be resolved upstream due to failure before giving up, this is maximum retries total, not per-resolver, default resolvers.length * 2
#maxRetries=5
# name: human-readable name of resolver, might end up in logs, default full resolver URI
#name=somename
# connectTimeout: TCP connection timeout in milliseconds to upstream resolver, default 500
connectTimeout=500

34
xmpp-dox/pom.xml Normal file
View File

@ -0,0 +1,34 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<parent>
<groupId>com.moparisthebest.dns</groupId>
<artifactId>jDnsProxy-parent</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>xmpp-dox</artifactId>
<packaging>jar</packaging>
<version>1.0-SNAPSHOT</version>
<name>${project.artifactId}</name>
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>jDnsProxy</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-tcp</artifactId>
<version>4.3.2</version>
</dependency>
<dependency>
<groupId>org.igniterealtime.smack</groupId>
<artifactId>smack-java7</artifactId>
<version>4.3.2</version>
</dependency>
</dependencies>
<build>
<finalName>${project.artifactId}</finalName>
</build>
</project>

View File

@ -0,0 +1,153 @@
package com.moparisthebest.dns.listen;
import com.moparisthebest.dns.dto.Packet;
import com.moparisthebest.dns.net.ParsedUrl;
import com.moparisthebest.dns.resolve.BaseRequestResponse;
import com.moparisthebest.dns.resolve.Resolver;
import com.moparisthebest.dns.xmpp.ConnectionDetails;
import com.moparisthebest.dns.xmpp.DnsIq;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.iqrequest.IQRequestHandler;
import org.jivesoftware.smack.packet.*;
import org.jxmpp.jid.Jid;
import java.io.IOException;
import java.util.concurrent.ExecutorService;
public class XmppListener implements Listener {
private final ConnectionDetails cd;
private final Resolver resolver;
private final ExecutorService executor;
private boolean running = true;
private Thread thisThread = null;
public XmppListener(final ParsedUrl parsedUrl, final Resolver resolver, final ExecutorService executor) {
this.cd = new ConnectionDetails(parsedUrl);
this.resolver = resolver;
this.executor = executor;
}
@Override
public void run() {
while (running)
try {
final AbstractXMPPConnection connection = cd.login();
thisThread = Thread.currentThread();
connection.registerIQRequestHandler(new IQRequestHandler() {
@Override
public IQ handleIQRequest(final IQ req) {
//System.out.println("request: " + req);
//System.out.println("request XML: " + req.toXML(StreamOpen.CLIENT_NAMESPACE));
final byte[] request = DnsIq.parseDnsIq(req);
if (request != null) {
//System.out.println("good request: " + req);
final XmppRequestResponse requestResponse = new XmppRequestResponse(req.getFrom(), new Packet(request));
resolver.resolveAsync(requestResponse).whenCompleteAsync((urr, t) -> {
if (t != null) {
t.printStackTrace();
return;
}
//debugPacket(urr.getResponse().getBuf());
final IQ resp = DnsIq.responseFor(req, urr.getResponse().getBuf());
try {
//System.out.println("dns response: " + resp.toString());
//System.out.println("dns response XML: " + resp.toXML(StreamOpen.CLIENT_NAMESPACE));
connection.sendStanza(resp);
} catch (SmackException.NotConnectedException | InterruptedException e) {
e.printStackTrace();
}
}, executor);
}
// todo: respond with error Iq?
return null;
}
@Override
public Mode getMode() {
return Mode.sync;
}
@Override
public IQ.Type getType() {
return IQ.Type.get;
}
@Override
public String getElement() {
return DnsIq.ELEMENT;
}
@Override
public String getNamespace() {
return DnsIq.NAMESPACE;
}
});
while (running)
Thread.sleep(Long.MAX_VALUE);
} catch (IOException | XMPPException | SmackException e) {
e.printStackTrace();
try {
// let's not burn the CPU
Thread.sleep(1000);
} catch (InterruptedException e1) {
//ignore
}
} catch (InterruptedException e) {
if (running) {
// not good
e.printStackTrace();
try {
// let's not burn the CPU
Thread.sleep(1000);
} catch (InterruptedException e1) {
//ignore
}
} else {
// being shutdown
return;
}
}
}
@Override
public void close() {
running = false;
if (thisThread != null)
thisThread.interrupt();
}
public class XmppRequestResponse extends BaseRequestResponse {
private final Jid requester;
public XmppRequestResponse(final Jid requester, final Packet request) {
super(request);
this.requester = requester;
}
public Jid getRequester() {
return requester;
}
@Override
public String toString() {
return "XmppRequestResponse{" +
"requester=" + requester +
"} " + super.toString();
}
}
}

View File

@ -0,0 +1,16 @@
package com.moparisthebest.dns.listen;
import com.moparisthebest.dns.net.ParsedUrl;
import com.moparisthebest.dns.resolve.Resolver;
import java.util.concurrent.ExecutorService;
public class XmppServices implements Services {
@Override
public Listener getListener(ParsedUrl parsedUrl, final Resolver resolver, final ExecutorService executor) {
if ("xmpp".equals(parsedUrl.getProtocol())) {
return new XmppListener(parsedUrl, resolver, executor);
}
return null;
}
}

View File

@ -0,0 +1,43 @@
package com.moparisthebest.dns.resolve;
import com.moparisthebest.dns.dto.Packet;
import com.moparisthebest.dns.net.ParsedUrl;
import com.moparisthebest.dns.xmpp.ConnectionDetails;
import com.moparisthebest.dns.xmpp.DnsIq;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.packet.IQ;
import org.jxmpp.jid.Jid;
import org.jxmpp.jid.impl.JidCreate;
import java.io.*;
public class XmppResolver implements Resolver {
private final Jid to;
private final AbstractXMPPConnection connection;
public XmppResolver(final ParsedUrl parsedUrl) {
try {
to = JidCreate.from(parsedUrl.getProps().get("resolverJid"));
this.connection = new ConnectionDetails(parsedUrl).login();
} catch (InterruptedException | XMPPException | SmackException | IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
@Override
public Packet resolve(final Packet request) throws Exception {
// todo: should do this async
final IQ req = DnsIq.requestTo(to, request.getBuf());
//System.out.println("dns request: " + req.toString());
//System.out.println("dns request XML: " + req.toXML(StreamOpen.CLIENT_NAMESPACE));
final IQ resp = connection.sendIqRequestAndWaitForResponse(req);
final byte[] ret = DnsIq.parseDnsIq(resp);
if(ret == null)
throw new Exception("XMPP request failed");
return new Packet(ret);
}
}

View File

@ -0,0 +1,13 @@
package com.moparisthebest.dns.resolve;
import com.moparisthebest.dns.net.ParsedUrl;
public class XmppServices implements Services {
@Override
public Resolver getResolver(ParsedUrl parsedUrl) {
if ("xmpp".equals(parsedUrl.getProtocol())) {
return new XmppResolver(parsedUrl);
}
return null;
}
}

View File

@ -0,0 +1,57 @@
package com.moparisthebest.dns.xmpp;
import com.moparisthebest.dns.net.ParsedUrl;
import org.jivesoftware.smack.AbstractXMPPConnection;
import org.jivesoftware.smack.SmackException;
import org.jivesoftware.smack.XMPPException;
import org.jivesoftware.smack.tcp.XMPPTCPConnection;
import org.jivesoftware.smack.tcp.XMPPTCPConnectionConfiguration;
import org.jxmpp.util.XmppStringUtils;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Objects;
public class ConnectionDetails {
private final String jid;
private final String password;
private final InetSocketAddress isa;
private final long replyTimeout;
public ConnectionDetails(final String jid, final String password, final InetSocketAddress isa, final long replyTimeout) {
Objects.requireNonNull(jid, "user must be non-null");
Objects.requireNonNull(isa, "isa must be non-null");
this.jid = jid;
this.password = password;
this.isa = isa;
this.replyTimeout = replyTimeout;
}
public ConnectionDetails(final ParsedUrl parsedUrl) {
this(parsedUrl.getProps().get("user"), parsedUrl.getProps().get("pass"),
(InetSocketAddress) parsedUrl.getAddr(),
Long.parseLong(parsedUrl.getProps().getOrDefault("replyTimeout", "5000")));
}
public AbstractXMPPConnection login() throws InterruptedException, XMPPException, SmackException, IOException {
final XMPPTCPConnectionConfiguration.Builder builder = XMPPTCPConnectionConfiguration.builder()
.setUsernameAndPassword(XmppStringUtils.parseLocalpart(jid), password)
.setXmppDomain(XmppStringUtils.parseDomain(jid))
.setHostAddress(isa.getAddress())
.setPort(isa.getPort());
final String resource = XmppStringUtils.parseResource(jid);
if(resource != null && !resource.isEmpty()) {
builder.setResource(resource);
}
final AbstractXMPPConnection connection = new XMPPTCPConnection(builder.build());
connection.setReplyTimeout(replyTimeout);
connection.connect().login();
return connection;
}
}

View File

@ -0,0 +1,56 @@
package com.moparisthebest.dns.xmpp;
import org.jivesoftware.smack.packet.IQ;
import org.jivesoftware.smack.packet.UnparsedIQ;
import org.jxmpp.jid.Jid;
import java.nio.ByteBuffer;
import java.util.Base64;
public class DnsIq extends IQ {
private static final Base64.Decoder decoder = Base64.getDecoder();
private static final Base64.Encoder encoder = Base64.getEncoder().withoutPadding();
public static final String ELEMENT = "dns";
public static final String NAMESPACE = "urn:xmpp:dox:0";
private final ByteBuffer bb;
private DnsIq(final ByteBuffer bb) {
super(ELEMENT, NAMESPACE);
this.bb = bb;
}
public static DnsIq responseFor(final IQ iq, final ByteBuffer bb) {
final DnsIq ret = new DnsIq(bb);
ret.setStanzaId(iq.getStanzaId());
ret.setTo(iq.getFrom());
ret.setType(Type.result);
return ret;
}
public static DnsIq requestTo(final Jid to, final ByteBuffer bb) {
final DnsIq ret = new DnsIq(bb);
ret.setTo(to);
ret.setType(Type.get);
return ret;
}
public static byte[] parseDnsIq(final IQ iq) {
// todo: yikes this whole method is awful, what happened to XML ? investigate this later
if(!(iq instanceof UnparsedIQ))
return null;
final UnparsedIQ uiq = (UnparsedIQ) iq;
final String actualRequest = uiq.getContent().toString().replaceAll("<[^>]+>", "");
//System.out.println("actualRequest: " + actualRequest);
return decoder.decode(actualRequest);
}
@Override
protected IQChildElementXmlStringBuilder getIQChildElementBuilder(final IQChildElementXmlStringBuilder buf) {
buf.rightAngleBracket();
buf.append(encoder.encodeToString(bb.array()));
return buf;
}
}

View File

@ -0,0 +1 @@
com.moparisthebest.dns.listen.XmppServices

View File

@ -0,0 +1 @@
com.moparisthebest.dns.resolve.XmppServices