diff --git a/pom.xml b/pom.xml index acead98..92fd48e 100644 --- a/pom.xml +++ b/pom.xml @@ -49,6 +49,7 @@ jDnsProxy + xmpp-dox ${project.artifactId} diff --git a/readme.md b/readme.md index 5e686e0..f59d31e 100644 --- a/readme.md +++ b/readme.md @@ -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: ``` diff --git a/xmpp-dox/dox.md b/xmpp-dox/dox.md new file mode 100644 index 0000000..8edfdfa --- /dev/null +++ b/xmpp-dox/dox.md @@ -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 + + vOIBIAABAAAAAAABB2V4YW1wbGUDb3JnAAABAAEAACkQAAAAAAAADAAKAAj5HO5JuEe+mA + +``` + +response: +```xml + + vOKBoAABAAEAAAABB2V4YW1wbGUDb3JnAAABAAHADAABAAEAAAhjAARduNgiAAApEAAAAAAAAAA + +``` + +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 diff --git a/xmpp-dox/jdnsproxy.xmpp.listener.properties b/xmpp-dox/jdnsproxy.xmpp.listener.properties new file mode 100644 index 0000000..dd0fd46 --- /dev/null +++ b/xmpp-dox/jdnsproxy.xmpp.listener.properties @@ -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 diff --git a/xmpp-dox/jdnsproxy.xmpp.resolver.properties b/xmpp-dox/jdnsproxy.xmpp.resolver.properties new file mode 100644 index 0000000..c1d6e5d --- /dev/null +++ b/xmpp-dox/jdnsproxy.xmpp.resolver.properties @@ -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 diff --git a/xmpp-dox/pom.xml b/xmpp-dox/pom.xml new file mode 100644 index 0000000..24b71e2 --- /dev/null +++ b/xmpp-dox/pom.xml @@ -0,0 +1,34 @@ + + + + com.moparisthebest.dns + jDnsProxy-parent + 1.0-SNAPSHOT + + 4.0.0 + xmpp-dox + jar + 1.0-SNAPSHOT + ${project.artifactId} + + + ${project.groupId} + jDnsProxy + ${project.version} + provided + + + org.igniterealtime.smack + smack-tcp + 4.3.2 + + + org.igniterealtime.smack + smack-java7 + 4.3.2 + + + + ${project.artifactId} + + diff --git a/xmpp-dox/src/main/java/com/moparisthebest/dns/listen/XmppListener.java b/xmpp-dox/src/main/java/com/moparisthebest/dns/listen/XmppListener.java new file mode 100644 index 0000000..ada9bf7 --- /dev/null +++ b/xmpp-dox/src/main/java/com/moparisthebest/dns/listen/XmppListener.java @@ -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(); + } + } +} diff --git a/xmpp-dox/src/main/java/com/moparisthebest/dns/listen/XmppServices.java b/xmpp-dox/src/main/java/com/moparisthebest/dns/listen/XmppServices.java new file mode 100644 index 0000000..8272e31 --- /dev/null +++ b/xmpp-dox/src/main/java/com/moparisthebest/dns/listen/XmppServices.java @@ -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; + } +} diff --git a/xmpp-dox/src/main/java/com/moparisthebest/dns/resolve/XmppResolver.java b/xmpp-dox/src/main/java/com/moparisthebest/dns/resolve/XmppResolver.java new file mode 100644 index 0000000..d7729e0 --- /dev/null +++ b/xmpp-dox/src/main/java/com/moparisthebest/dns/resolve/XmppResolver.java @@ -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); + } +} diff --git a/xmpp-dox/src/main/java/com/moparisthebest/dns/resolve/XmppServices.java b/xmpp-dox/src/main/java/com/moparisthebest/dns/resolve/XmppServices.java new file mode 100644 index 0000000..b84ff07 --- /dev/null +++ b/xmpp-dox/src/main/java/com/moparisthebest/dns/resolve/XmppServices.java @@ -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; + } +} diff --git a/xmpp-dox/src/main/java/com/moparisthebest/dns/xmpp/ConnectionDetails.java b/xmpp-dox/src/main/java/com/moparisthebest/dns/xmpp/ConnectionDetails.java new file mode 100644 index 0000000..0493e17 --- /dev/null +++ b/xmpp-dox/src/main/java/com/moparisthebest/dns/xmpp/ConnectionDetails.java @@ -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; + } +} diff --git a/xmpp-dox/src/main/java/com/moparisthebest/dns/xmpp/DnsIq.java b/xmpp-dox/src/main/java/com/moparisthebest/dns/xmpp/DnsIq.java new file mode 100644 index 0000000..310003d --- /dev/null +++ b/xmpp-dox/src/main/java/com/moparisthebest/dns/xmpp/DnsIq.java @@ -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; + } +} diff --git a/xmpp-dox/src/main/resources/META-INF/services/com.moparisthebest.dns.listen.Services b/xmpp-dox/src/main/resources/META-INF/services/com.moparisthebest.dns.listen.Services new file mode 100644 index 0000000..9f0754a --- /dev/null +++ b/xmpp-dox/src/main/resources/META-INF/services/com.moparisthebest.dns.listen.Services @@ -0,0 +1 @@ +com.moparisthebest.dns.listen.XmppServices \ No newline at end of file diff --git a/xmpp-dox/src/main/resources/META-INF/services/com.moparisthebest.dns.resolve.Services b/xmpp-dox/src/main/resources/META-INF/services/com.moparisthebest.dns.resolve.Services new file mode 100644 index 0000000..2514dcc --- /dev/null +++ b/xmpp-dox/src/main/resources/META-INF/services/com.moparisthebest.dns.resolve.Services @@ -0,0 +1 @@ +com.moparisthebest.dns.resolve.XmppServices \ No newline at end of file