/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Copyright @ 2015 Atlassian Pty Ltd
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.java.sip.communicator.impl.dns;
import java.beans.*;
import java.io.*;
import java.net.*;
import java.util.*;
import java.util.concurrent.*;
import net.java.sip.communicator.service.dns.*;
import net.java.sip.communicator.util.*;
import org.xbill.DNS.*;
/**
* The purpose of this class is to help avoid the significant delays that occur
* in networks where DNS servers would ignore SRV, NAPTR, and sometimes even
* A/AAAA queries (i.e. without even sending an error response). We also try to
* handle cases where DNS servers may return empty responses to some records.
* <p>
* We achieve this by entering a redundant mode whenever we detect an abnormal
* delay (longer than <tt>DNS_PATIENCE</tt>) while waiting for a DNS resonse,
* or when that response is not considered satisfying.
* <p>
* Once we enter redundant mode, we start duplicating all queries and sending
* them to both our primary and backup resolvers (in case we have any). We then
* always return the first response we get, regardless of who sent it.
* <p>
* We exit redundant mode after receiving <tt>DNS_REDEMPTION</tt> consecutive
* timely and correct responses from our primary resolver.
*
* @author Emil Ivov
*/
public class ParallelResolverImpl
implements CustomResolver, PropertyChangeListener
{
/**
* The <tt>Logger</tt> used by the <tt>ParallelResolver</tt>
* class and its instances for logging output.
*/
private static final Logger logger = Logger
.getLogger(ParallelResolverImpl.class);
/**
* Indicates whether we are currently in a mode where all DNS queries are
* sent to both the primary and the backup DNS servers.
*/
private volatile static boolean redundantMode = false;
/**
* The currently configured number of milliseconds that we need to wait
* before entering redundant mode.
*/
private static long currentDnsPatience = DNS_PATIENCE;
/**
* The currently configured number of times that the primary DNS would have
* to provide a faster response than the backup resolver before we consider
* it safe enough to exit redundant mode.
*/
public static int currentDnsRedemption = DNS_REDEMPTION;
/**
* The number of fast responses that we need to get from the primary
* resolver before we exit redundant mode. <tt>0</tt> indicates that we are
* no longer in redundant mode
*/
private static int redemptionStatus = 0;
/**
* A lock that we use while determining whether we've completed redemption
* and can exit redundant mode.
*/
private final static Object redemptionLock = new Object();
/**
* The default resolver that we use if everything works properly.
*/
private Resolver defaultResolver;
/**
* An extended resolver that would be encapsulating all backup resolvers.
*/
private ExtendedResolver backupResolver;
/** Thread pool that processes the backup queries. */
private ExecutorService backupQueriesPool;
/**
* Creates a new instance of this class.
*/
ParallelResolverImpl()
{
backupQueriesPool = Executors.newCachedThreadPool();
DnsUtilActivator.getConfigurationService()
.addPropertyChangeListener(this);
initProperties();
reset();
}
private void initProperties()
{
String rslvrAddrStr
= DnsUtilActivator.getConfigurationService().getString(
DnsUtilActivator.PNAME_BACKUP_RESOLVER,
DnsUtilActivator.DEFAULT_BACKUP_RESOLVER);
String customResolverIP
= DnsUtilActivator.getConfigurationService().getString(
DnsUtilActivator.PNAME_BACKUP_RESOLVER_FALLBACK_IP,
DnsUtilActivator.getResources().getSettingsString(
DnsUtilActivator.PNAME_BACKUP_RESOLVER_FALLBACK_IP));
InetAddress resolverAddress = null;
try
{
resolverAddress = NetworkUtils.getInetAddress(rslvrAddrStr);
}
catch(UnknownHostException exc)
{
logger.warn(
"Seems like the primary DNS is down, trying fallback to "
+ customResolverIP);
}
if(resolverAddress == null)
{
// name resolution failed for backup DNS resolver,
// try with the IP address of the default backup resolver
try
{
resolverAddress = NetworkUtils.getInetAddress(customResolverIP);
}
catch (UnknownHostException e)
{
// this shouldn't happen, but log anyway
logger.error(e);
}
}
int resolverPort = DnsUtilActivator.getConfigurationService().getInt(
DnsUtilActivator.PNAME_BACKUP_RESOLVER_PORT,
SimpleResolver.DEFAULT_PORT);
InetSocketAddress resolverSockAddr
= new InetSocketAddress(resolverAddress, resolverPort);
setBackupServers(new InetSocketAddress[]{ resolverSockAddr });
currentDnsPatience = DnsUtilActivator.getConfigurationService()
.getLong(PNAME_DNS_PATIENCE, DNS_PATIENCE);
currentDnsRedemption
= DnsUtilActivator.getConfigurationService()
.getInt(PNAME_DNS_REDEMPTION, DNS_REDEMPTION);
}
/**
* Sets the specified array of <tt>backupServers</tt> used if the default
* DNS doesn't seem to be doing that well.
*
* @param backupServers the list of backup DNS servers that we should use
* if, and only if, the default servers don't seem to work that well.
*/
private void setBackupServers(InetSocketAddress[] backupServers)
{
try
{
backupResolver = new ExtendedResolver(new SimpleResolver[0]);
for(InetSocketAddress backupServer : backupServers)
{
SimpleResolver sr = new SimpleResolver();
sr.setAddress(backupServer);
backupResolver.addResolver(sr);
}
}
catch (UnknownHostException e)
{
// this shouldn't be thrown since we don't do any DNS querying in
// here. this is why we take an InetSocketAddress as a param.
throw new IllegalStateException(
"The impossible just happened: we could not initialize our"
+ " backup DNS resolver.");
}
}
/**
* Sends a message and waits for a response.
*
* @param query The query to send.
* @return The response
*
* @throws IOException An error occurred while sending or receiving.
*/
public Message send(Message query)
throws IOException
{
ParallelResolution resolution = new ParallelResolution(query);
resolution.sendFirstQuery();
//if we are not in redundant mode we should wait a bit and see how this
//goes. if we get a reply we could return bravely.
if(!redundantMode)
{
if(resolution.waitForResponse(currentDnsPatience))
{
//we are done.
return resolution.returnResponseOrThrowUp();
}
else
{
synchronized(redemptionLock)
{
redundantMode = true;
redemptionStatus = currentDnsRedemption;
logger.info("Primary DNS seems laggy: "
+ "no response for " + query.getQuestion().getName()
+ "/" + Type.string(query.getQuestion().getType())
+ " after " + currentDnsPatience + "ms. "
+ "Enabling redundant mode.");
}
}
}
//we are definitely in redundant mode now
resolution.sendBackupQueries();
resolution.waitForResponse(0);
//check if it is time to end redundant mode.
synchronized(redemptionLock)
{
if(!resolution.primaryResolverRespondedFirst)
{
//primary DNS is still feeling shaky. we reinit redemption
//status in case we were about to cut the server some slack
redemptionStatus = currentDnsRedemption;
}
else
{
//primary server replied first. we let him redeem some dignity
redemptionStatus --;
//yup, it's now time to end DNS redundant mode;
if(redemptionStatus <= 0)
{
redundantMode = false;
logger.info("Primary DNS seems back in biz. "
+ "Disabling redundant mode.");
}
}
}
return resolution.returnResponseOrThrowUp();
}
/**
* Supposed to asynchronously send messages but not currently implemented.
*
* @param query The query to send
* @param listener The object containing the callbacks.
* @return An identifier, which is also a parameter in the callback
*/
public Object sendAsync(final Message query, final ResolverListener listener)
{
throw new UnsupportedOperationException("Not implemented");
}
/**
* Sets the port to communicate on with the default servers.
*
* @param port The port to send messages to
*/
public void setPort(int port)
{
defaultResolver.setPort(port);
}
/**
* Sets whether TCP connections will be sent by default with the default
* resolver. Backup servers would always be contacted the same way.
*
* @param flag Indicates whether TCP connections are made
*/
public void setTCP(boolean flag)
{
defaultResolver.setTCP(flag);
}
/**
* Sets whether truncated responses will be ignored. If not, a truncated
* response over UDP will cause a retransmission over TCP. Backup servers
* would always be contacted the same way.
*
* @param flag Indicates whether truncated responses should be ignored.
*/
public void setIgnoreTruncation(boolean flag)
{
defaultResolver.setIgnoreTruncation(flag);
}
/**
* Sets the EDNS version used on outgoing messages.
*
* @param level The EDNS level to use. 0 indicates EDNS0 and -1 indicates no
* EDNS.
* @throws IllegalArgumentException An invalid level was indicated.
*/
public void setEDNS(int level)
{
defaultResolver.setEDNS(level);
}
/**
* Sets the EDNS information on outgoing messages.
*
* @param level The EDNS level to use. 0 indicates EDNS0 and -1 indicates no
* EDNS.
* @param payloadSize The maximum DNS packet size that this host is capable
* of receiving over UDP. If 0 is specified, the default (1280) is used.
* @param flags EDNS extended flags to be set in the OPT record.
* @param options EDNS options to be set in the OPT record, specified as a
* List of OPTRecord.Option elements.
*
* @throws IllegalArgumentException An invalid field was specified.
* @see OPTRecord
*/
@SuppressWarnings("rawtypes") // that's the way it is in dnsjava
public void setEDNS(int level, int payloadSize, int flags, List options)
{
defaultResolver.setEDNS(level, payloadSize, flags, options);
}
/**
* Specifies the TSIG key that messages will be signed with
* @param key The key
*/
public void setTSIGKey(TSIG key)
{
defaultResolver.setTSIGKey(key);
}
/**
* Sets the amount of time to wait for a response before giving up.
*
* @param secs The number of seconds to wait.
* @param msecs The number of milliseconds to wait.
*/
public void setTimeout(int secs, int msecs)
{
defaultResolver.setTimeout(secs, msecs);
}
/**
* Sets the amount of time to wait for a response before giving up.
*
* @param secs The number of seconds to wait.
*/
public void setTimeout(int secs)
{
defaultResolver.setTimeout(secs);
}
/**
* Resets resolver configuration and populate our default resolver
* with the newly configured servers.
*/
public final void reset()
{
Lookup.refreshDefault();
// populate with new servers after refreshing configuration
try
{
Lookup.setDefaultResolver(this);
ExtendedResolver temp = new ExtendedResolver();
temp.setTimeout(10);
defaultResolver = temp;
}
catch (UnknownHostException e)
{
// should never happen
throw new RuntimeException("Failed to initialize resolver");
}
}
/**
* Determines if <tt>response</tt> can be considered a satisfactory DNS
* response and returns accordingly.
* <p>
* We consider non-satisfactory responses that may indicate that the local
* DNS does not work properly and that we may hence need to fall back to
* the backup resolver.
* <p>
* Basically the goal here is to be able to go into redundant mode when we
* come across DNS servers that send empty responses to SRV and NAPTR
* requests.
*
* @param response the dnsjava {@link Message} that we'd like to inspect.
*
* @return <tt>true</tt> if <tt>response</tt> appears as a satisfactory
* response and <tt>false</tt> otherwise.
*/
private boolean isResponseSatisfactory(Message response)
{
if ( response == null )
return false;
Record[] answerRR = response.getSectionArray(Section.ANSWER);
Record[] authorityRR = response.getSectionArray(Section.AUTHORITY);
Record[] additionalRR = response.getSectionArray(Section.ADDITIONAL);
if ( (answerRR != null && answerRR.length > 0)
|| (authorityRR != null && authorityRR.length > 0)
|| (additionalRR != null && additionalRR.length > 0))
{
return true;
}
int rcode = response.getRcode();
//we didn't find any responses and the answer is NXDOMAIN then
//we may want to check with the backup resolver for a second opinion
if(rcode == Rcode.NXDOMAIN)
return false;
//if we received NODATA (same as NOERROR and no response records) for
// an AAAA or a NAPTR query then it makes sense since many existing
//domains come without those two.
Record question = response.getQuestion();
int questionType = (question == null) ? 0 : question.getType();
if( rcode == Rcode.NOERROR
&& question != null
&& (questionType == Type.AAAA || questionType == Type.NAPTR))
{
return true;
}
//nope .. this doesn't make sense ...
return false;
}
/**
* The class that listens for responses to any of the queries we send to
* our default and backup servers and returns as soon as we get one or until
* our default resolver fails.
*/
private class ParallelResolution implements Runnable
{
/**
* The query that we have sent to the default and backup DNS servers.
*/
private final Message query;
/**
* The field where we would store the first incoming response to our
* query.
*/
private volatile Message response;
/**
* The field where we would store the first error we receive from a DNS
* or a backup resolver.
*/
private Throwable exception;
/**
* Indicates whether we are still waiting for an answer from someone
*/
private volatile boolean done = false;
/**
* Indicates that a response was received from the primary resolver.
*/
private volatile boolean primaryResolverRespondedFirst = true;
/**
* Creates a {@link ParallelResolution} for the specified <tt>query</tt>
*
* @param query the DNS query that we'd like to send to our primary
* and backup resolvers.
*/
public ParallelResolution(final Message query)
{
this.query = query;
}
/**
* Starts this collector which would cause it to send its query to the
* default resolver.
*/
public void sendFirstQuery()
{
ParallelResolverImpl.this.backupQueriesPool.execute(this);
}
/**
* Sends this collector's query to the default resolver.
*/
@Override
public void run()
{
Message localResponse = null;
try
{
localResponse = defaultResolver.send(query);
}
catch (SocketTimeoutException exc)
{
logger.info("Default DNS resolver timed out.");
exception = exc;
}
catch (Throwable exc)
{
logger.info("Default DNS resolver failed", exc);
exception = exc;
}
//if the backup resolvers had already replied we ignore the
//reply of the primary one whatever it was.
if(done)
return;
synchronized(this)
{
//if there was a response we're only done if it is satisfactory
if( localResponse != null
&& isResponseSatisfactory(localResponse))
{
response = localResponse;
done = true;
}
notify();
}
}
/**
* Asynchronously sends this collector's query to all backup resolvers.
*/
public void sendBackupQueries()
{
//yes. a second thread in the thread ... it's ugly but it works
//and i do want to keep code simple to read ... this whole parallel
//resolving is complicated enough as it is.
backupQueriesPool.execute(new Runnable(){
@Override
public void run()
{
if (done)
{
return;
}
Message localResponse = null;
try
{
logger.info("Sending query for "
+ query.getQuestion().getName() + "/"
+ Type.string(query.getQuestion().getType())
+ " to backup resolvers");
localResponse = backupResolver.send(query);
}
catch (Throwable exc)
{
logger.info(
"Exception occurred during backup DNS resolving "
+ exc);
//keep this so that we can rethrow it
exception = exc;
}
//if the default resolver has already replied we
//ignore the reply of the backup ones.
if(done)
{
return;
}
synchronized(ParallelResolution.this)
{
//contrary to responses from the primary resolver,
//in this case we don't care whether the response is
//satisfying: if it isn't, there's nothing we can do
if (response == null)
{
response = localResponse;
primaryResolverRespondedFirst = false;
}
done = true;
ParallelResolution.this.notify();
}
}
});
}
/**
* Waits for a response or an error to occur during <tt>waitFor</tt>
* milliseconds.If neither happens, we return false.
*
* @param waitFor the number of milliseconds to wait for a response or
* an error or <tt>0</tt> if we'd like to wait until either of these
* happen.
*
* @return <tt>true</tt> if we returned because we received a response
* from a resolver or errors from everywhere, and <tt>false</tt> that
* didn't happen.
*/
public boolean waitForResponse(long waitFor)
{
synchronized(this)
{
if(done)
return done;
try
{
wait(waitFor);
}
catch (InterruptedException e)
{
//we don't care
}
return done;
}
}
/**
* Waits for resolution to complete (if necessary) and then either
* returns the response we received or throws whatever exception we
* saw.
*
* @return the response {@link Message} we received from the DNS.
*
* @throws IOException if this resolution ended badly because of a
* network IO error
* @throws RuntimeException if something unexpected happened
* during resolution.
* @throws IllegalArgumentException if something unexpected happened
* during resolution or if there was no response.
*/
public Message returnResponseOrThrowUp()
throws IOException, RuntimeException, IllegalArgumentException
{
if(!done)
waitForResponse(0);
if(response != null)
{
return response;
}
else if (exception instanceof SocketTimeoutException)
{
logger.warn("DNS resolver timed out");
throw (IOException) exception;
}
else if (exception instanceof IOException)
{
logger.warn("IO exception while using DNS resolver", exception);
throw (IOException) exception;
}
else if (exception instanceof RuntimeException)
{
logger.warn("RunTimeException while using DNS resolver",
exception);
throw (RuntimeException) exception;
}
else if (exception instanceof Error)
{
logger.warn("Error while using DNS resolver", exception);
throw (Error) exception;
}
else
{
logger.warn("Received a bad response from primary DNS resolver",
exception);
throw new IllegalStateException("ExtendedResolver failure");
}
}
}
@SuppressWarnings("serial")
private final Set<String> configNames = new HashSet<String>(5)
{{
add(DnsUtilActivator.PNAME_BACKUP_RESOLVER_ENABLED);
add(DnsUtilActivator.PNAME_BACKUP_RESOLVER);
add(DnsUtilActivator.PNAME_BACKUP_RESOLVER_FALLBACK_IP);
add(DnsUtilActivator.PNAME_BACKUP_RESOLVER_PORT);
add(CustomResolver.PNAME_DNS_PATIENCE);
add(CustomResolver.PNAME_DNS_REDEMPTION);
}};
public void propertyChange(PropertyChangeEvent evt)
{
if (!configNames.contains(evt.getPropertyName()))
{
return;
}
initProperties();
}
}