/*
* SIP Communicator, the OpenSource Java VoIP and Instant Messaging client.
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package net.java.sip.communicator.impl.netaddr;
import java.net.*;
import net.java.sip.communicator.util.*;
import net.java.stun4j.*;
import net.java.stun4j.attribute.*;
import net.java.stun4j.message.*;
/**
* Runs a separate thread of diagnostics for a given network address. The
* diagnostics thread would discover NAT bindings through stun, update bindings
* lifetime test connectivity and etc.
*
* @author Emil Ivov
*/
public class AddressDiagnosticsKit
extends Thread
{
private static final Logger logger =
Logger.getLogger(AddressDiagnosticsKit.class);
public static final int DIAGNOSTICS_STATUS_OFF = 1;
public static final int DIAGNOSTICS_STATUS_DISOVERING_CONFIG = 2;
public static final int DIAGNOSTICS_STATUS_RESOLVING = 3;
public static final int DIAGNOSTICS_STATUS_COMPLETED = 4;
public static final int DIAGNOSTICS_STATUS_DISOVERING_BIND_LIFETIME = 5;
public static final int DIAGNOSTICS_STATUS_TERMINATED = 6;
private int diagnosticsStatus = DIAGNOSTICS_STATUS_OFF;
/**
* These are used by (to my knowledge) mac and windows boxes when dhcp
* fails and are only usable with other boxes using the same address
* in the same net segment. That's why they get their low preference.
*/
private static final AddressPreference ADDR_PREF_LOCAL_IPV4_AUTOCONF
= new AddressPreference(40);
/**
* Local IPv6 addresses are assigned by default to any network iface running
* an ipv6 stack. Theya are one of our last resorts since an internet
* connected node would have generally configured sth else as well.
*/
private static final AddressPreference ADDR_PREF_LOCAL_IPV6
= new AddressPreference(40);
/**
* Local IPv4 addresses are either assigned by DHCP or manually configured
* which means that even if they're unresolved to a globally routable
* address they're still there for a reason (let the reason be ...) and this
* reason might very well be purposeful so they should get a preference
* higher than local IPv6 (even though I'm an IPv6 fan :) )
*/
private static final AddressPreference ADDR_PREF_PRIVATE_IPV4
= new AddressPreference(50);
/**
* Global IPv4 Addresses are a good think when they work. We are therefore
* setting a high preference that will then be corrected by.
*/
private static final AddressPreference ADDR_PREF_GLOBAL_IPV4
= new AddressPreference(60);
/**
* There are many reasons why global IPv6 addresses should have the highest
* preference. A global IPv6 address is most often delivered through
* stateless address autoconfiguration which means an active router and
* might also mean an active net connection.
*/
private static final AddressPreference ADDR_PREF_GLOBAL_IPV6
= new AddressPreference(70);
/**
* The address of the stun server to query
*/
private StunAddress primaryStunServerAddress =
new StunAddress("stun01.sipphone.com", 3478);
/**
* The address pool entry that this kit is diagnosing.
*/
private AddressPoolEntry addressEntry = null;
/**
* Specifies whether stun should be used or not.
* This field is updated during runtime to conform to the configuration.
*/
private boolean useStun = true;
private StunClient stunClient = null;
/**
* The port to be used locally for sending generic stun queries.
*/
static final int LOCAL_STUN_PORT = 55126;
private int bindRetries = 10;
public AddressDiagnosticsKit(AddressPoolEntry addressEntry)
{
this.addressEntry = addressEntry;
setDiagnosticsStatus(DIAGNOSTICS_STATUS_OFF);
}
/**
* Sets the current status of the address diagnostics process
* @param status int
*/
private void setDiagnosticsStatus(int status)
{
this.diagnosticsStatus = status;
}
/**
* Returns the current status of this diagnosics process.
* @return int
*/
public int getDiagnosticsStatus()
{
return this.diagnosticsStatus;
}
/**
* The diagnostics code itself.
*/
public void run()
{
logger.debug("Started a diag kit for entry: " + addressEntry);
//implements the algorithm from AssigningAddressPreferences.png
setDiagnosticsStatus(this.DIAGNOSTICS_STATUS_DISOVERING_CONFIG);
InetAddress address = addressEntry.getInetAddress();
//is this an ipv6 address
if (addressEntry.isIPv6())
{
if (addressEntry.isLinkLocal())
{
addressEntry.setAddressPreference(ADDR_PREF_LOCAL_IPV6);
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
return;
}
if (addressEntry.is6to4())
{
//right now we don't support these. we should though ... one day
addressEntry.setAddressPreference(AddressPreference.MIN);
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
return;
}
//if we get here then we are a globally routable ipv6 addr
addressEntry.setAddressPreference(ADDR_PREF_GLOBAL_IPV6);
setDiagnosticsStatus(DIAGNOSTICS_STATUS_COMPLETED);
//should do some connectivity testing here and proceed with firewall
//discovery but since stun4j does not support ipv6 yet, this too
//will happen another day.
return;
}
//from now on we're only dealing with IPv4
if (addressEntry.isIPv4LinkLocalAutoconf())
{
//not sure whether these are used for anything.
addressEntry.setAddressPreference(AddressPreference.MIN);
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
return;
}
//first try and see what we can infer from just looking at the
//address
if (addressEntry.isLinkLocalIPv4Address())
{
addressEntry.setAddressPreference(ADDR_PREF_PRIVATE_IPV4);
}
else
{
//public address
addressEntry.setAddressPreference(ADDR_PREF_GLOBAL_IPV4);
}
if (!useStun)
{
//if we're configured not to run stun - we're done.
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
return;
}
//start stunning
for(int i = 0; i < bindRetries; i++){
StunAddress localStunAddress = new StunAddress(
address, 1024 + (int) (Math.random() * 64512));
try
{
stunClient = new StunClient(localStunAddress);
stunClient.start();
logger.debug("Successfully started StunClient for "
+ localStunAddress + ".");
break;
}
catch (StunException ex)
{
if (ex.getCause() instanceof SocketException
&& i < bindRetries)
{
logger.debug("Failed to bind to "
+ localStunAddress + ". Retrying ...");
logger.debug("Exception was ", ex);
continue;
}
logger.error("Failed to start a stun client for address entry ["
+ addressEntry.toString()+"]:"
+localStunAddress.getPort() + ". Ceasing attempts",
ex);
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
return;
}
}
//De Stun Test I
StunMessageEvent event = null;
try
{
event = stunClient.doStunTestI(
primaryStunServerAddress);
}
catch (StunException ex)
{
logger.error("Failed to perform STUN Test I for address entry"
+ addressEntry.toString(), ex);
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
stunClient.shutDown();
return;
}
if(event == null)
{
//didn't get a response - we either don't have connectivity or the
//server is down
/** @todo if possible try another stun server here. we should
* support multiple stun servers*/
logger.debug("There seems to be no inet connectivity for "
+ addressEntry);
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
stunClient.shutDown();
logger.debug("stun test 1 failed");
return;
}
//the moment of the truth - are we behind a NAT?
boolean isPublic;
Message stunResponse = event.getMessage();
Attribute mappedAttr = stunResponse.getAttribute(Attribute.MAPPED_ADDRESS);
StunAddress mappedAddrFromTestI = ((MappedAddressAttribute)mappedAttr).getAddress();
Attribute changedAddressAttributeFromTestI
= stunResponse.getAttribute(Attribute.CHANGED_ADDRESS);
StunAddress secondaryStunServerAddress =
((ChangedAddressAttribute)changedAddressAttributeFromTestI).
getAddress();
/** @todo verify whether the stun server returned the same address for
* the primary and secondary server and act accordingly
* */
if(mappedAddrFromTestI == null){
logger.error(
"Stun Server did not return a mapped address for entry "
+ addressEntry.toString());
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
return;
}
if(mappedAddrFromTestI.equals(event.getSourceAccessPoint().getAddress()))
{
isPublic = true;
}
else
{
isPublic = false;
}
//do STUN Test II
try
{
event = stunClient.doStunTestII(primaryStunServerAddress);
}
catch (StunException ex)
{
logger.error("Failed to perform STUN Test II for address entry"
+ addressEntry.toString(), ex);
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
stunClient.shutDown();
logger.debug("stun test 2 failed");
return;
}
if(event != null){
logger.error("Secondary STUN server is down"
+ addressEntry.toString());
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
stunClient.shutDown();
return;
}
//might mean that either the secondary stun server is down
//or that we are behind a restrictive firewall. Let's find out
//which.
try
{
event = stunClient.doStunTestI(secondaryStunServerAddress);
logger.debug("stun test 1 succeeded with s server 2");
}
catch (StunException ex)
{
logger.error("Failed to perform STUN Test I for address entry"
+ addressEntry.toString(), ex);
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
stunClient.shutDown();
return;
}
if (event == null)
{
//secondary stun server is down
logger.error("Secondary STUN server is down"
+ addressEntry.toString());
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
stunClient.shutDown();
return;
}
//we are at least behind a port restricted nat
stunResponse = event.getMessage();
mappedAttr = stunResponse.getAttribute(Attribute.MAPPED_ADDRESS);
StunAddress mappedAddrFromSecServer =
((MappedAddressAttribute)mappedAttr).getAddress();
if(!mappedAddrFromTestI.equals(mappedAddrFromSecServer))
{
//secondary stun server is down
logger.debug("We are behind a symmetric nat"
+ addressEntry.toString());
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
stunClient.shutDown();
return;
}
//now let's run test III so that we could guess whether or not we're
//behind a port restricted nat/fw or simply a restricted one.
try
{
event = stunClient.doStunTestIII(primaryStunServerAddress);
logger.debug("stun test 3 succeeded with s server 1");
}
catch (StunException ex)
{
logger.error("Failed to perform STUN Test III for address entry"
+ addressEntry.toString(), ex);
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
stunClient.shutDown();
return;
}
if (event == null)
{
logger.debug("We are behind a port restricted NAT or fw"
+ addressEntry.toString());
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
stunClient.shutDown();
return;
}
logger.debug("We are behind a restricted NAT or fw"
+ addressEntry.toString());
setDiagnosticsStatus(DIAGNOSTICS_STATUS_TERMINATED);
stunClient.shutDown();
}
}