/** * Tries to obtain a mapped/public address for the specified port (possibly by executing a STUN * query). * * @param dst the destination that we'd like to use this address with. * @param port the port whose mapping we are interested in. * @return a public address corresponding to the specified port or null if all attempts to * retrieve such an address have failed. * @throws IOException if an error occurs while stun4j is using sockets. * @throws BindException if the port is already in use. */ public InetSocketAddress getPublicAddressFor(InetAddress dst, int port) throws IOException, BindException { if (!useStun || (dst instanceof Inet6Address)) { logger.debug( "Stun is disabled for destination " + dst + ", skipping mapped address recovery (useStun=" + useStun + ", IPv6@=" + (dst instanceof Inet6Address) + ")."); // we'll still try to bind though so that we could notify the caller // if the port has been taken already. DatagramSocket bindTestSocket = new DatagramSocket(port); bindTestSocket.close(); // if we're here then the port was free. return new InetSocketAddress(getLocalHost(dst), port); } StunAddress mappedAddress = queryStunServer(port); InetSocketAddress result = null; if (mappedAddress != null) result = mappedAddress.getSocketAddress(); else { // Apparently STUN failed. Let's try to temporarily disble it // and use algorithms in getLocalHost(). ... We should probably // eveng think about completely disabling stun, and not only // temporarily. // Bug report - John J. Barton - IBM InetAddress localHost = getLocalHost(dst); result = new InetSocketAddress(localHost, port); } if (logger.isDebugEnabled()) logger.debug("Returning mapping for port:" + port + " as follows: " + result); return result; }
/** * Initializes this network address manager service implementation and starts all * processes/threads associated with this address manager, such as a stun firewall/nat detector, * keep alive threads, binding lifetime discovery threads and etc. The method may also be used * after a call to stop() as a reinitialization technique. */ public void start() { // init stun String stunAddressStr = null; int port = -1; stunAddressStr = NetaddrActivator.getConfigurationService().getString(PROP_STUN_SERVER_ADDRESS); String portStr = NetaddrActivator.getConfigurationService().getString(PROP_STUN_SERVER_PORT); this.localHostFinderSocket = initRandomPortSocket(); if (stunAddressStr == null || portStr == null) { useStun = false; // we use the default stun server address only for chosing a public // route and not for stun queries. stunServerAddress = new StunAddress(DEFAULT_STUN_SERVER_ADDRESS, DEFAULT_STUN_SERVER_PORT); logger.info( "Stun server address(" + stunAddressStr + ")/port(" + portStr + ") not set (or invalid). Disabling STUN."); } else { try { port = Integer.valueOf(portStr).intValue(); } catch (NumberFormatException ex) { logger.error(portStr + " is not a valid port number. " + "Defaulting to 3478", ex); port = 3478; } stunServerAddress = new StunAddress(stunAddressStr, port); detector = new SimpleAddressDetector(stunServerAddress); if (logger.isDebugEnabled()) { logger.debug( "Created a STUN Address detector for the following " + "STUN server: " + stunAddressStr + ":" + port); } detector.start(); logger.debug("STUN server detector started;"); // make sure that someone doesn't set invalid stun address and port NetaddrActivator.getConfigurationService() .addVetoableChangeListener(PROP_STUN_SERVER_ADDRESS, this); NetaddrActivator.getConfigurationService() .addVetoableChangeListener(PROP_STUN_SERVER_PORT, this); // now start a thread query to the stun server and only set the // useStun flag to true if it succeeds. launchStunServerTest(); } }
/** * The method queries a Stun server for a binding for the specified port. * * @param port the port to resolve (the stun message gets sent trhough that port) * @return StunAddress the address returned by the stun server or null if an error occurred or no * address was returned * @throws IOException if an error occurs while stun4j is using sockets. * @throws BindException if the port is already in use. */ private StunAddress queryStunServer(int port) throws IOException, BindException { StunAddress mappedAddress = null; if (detector != null && useStun) { mappedAddress = detector.getMappingFor(port); if (logger.isDebugEnabled()) logger.debug( "For port:" + port + "a Stun server returned the " + "following mapping [" + mappedAddress); } return mappedAddress; }
/** * Initializes and binds a socket that on a random port number. The method would try to bind on a * random port and retry 5 times until a free port is found. * * @return the socket that we have initialized on a randomport number. */ private DatagramSocket initRandomPortSocket() { DatagramSocket resultSocket = null; String bindRetriesStr = NetaddrActivator.getConfigurationService().getString(BIND_RETRIES_PROPERTY_NAME); int bindRetries = 5; if (bindRetriesStr != null) { try { bindRetries = Integer.parseInt(bindRetriesStr); } catch (NumberFormatException ex) { logger.error( bindRetriesStr + " does not appear to be an integer. " + "Defaulting port bind retries to " + bindRetries, ex); } } int currentlyTriedPort = NetworkUtils.getRandomPortNumber(); // we'll first try to bind to a random port. if this fails we'll try // again (bindRetries times in all) until we find a free local port. for (int i = 0; i < bindRetries; i++) { try { resultSocket = new DatagramSocket(currentlyTriedPort); // we succeeded - break so that we don't try to bind again break; } catch (SocketException exc) { if (exc.getMessage().indexOf("Address already in use") == -1) { logger.fatal( "An exception occurred while trying to create" + "a local host discovery socket.", exc); resultSocket = null; return null; } // port seems to be taken. try another one. logger.debug("Port " + currentlyTriedPort + " seems in use."); currentlyTriedPort = NetworkUtils.getRandomPortNumber(); logger.debug("Retrying bind on port " + currentlyTriedPort); } } return resultSocket; }
/** * The method queries a Stun server for a binding for the port and address that <tt>sock</tt> is * bound on. * * @param sock the socket whose port and address we'dlike to resolve (the stun message gets sent * trhough that socket) * @return StunAddress the address returned by the stun server or null if an error occurred or no * address was returned * @throws IOException if an error occurs while stun4j is using sockets. * @throws BindException if the port is already in use. */ private StunAddress queryStunServer(DatagramSocket sock) throws IOException, BindException { StunAddress mappedAddress = null; if (detector != null && useStun) { mappedAddress = detector.getMappingFor(sock); if (logger.isTraceEnabled()) { logger.trace( "For socket with address " + sock.getLocalAddress().getHostAddress() + " and port " + sock.getLocalPort() + " the stun server returned the " + "following mapping [" + mappedAddress + "]"); } } return mappedAddress; }
/** * Kills all threads/processes lauched by this thread and prepares it for shutdown. You may use * this method as a reinitialization technique ( you'll have to call start afterwards) */ public void stop() { try { try { detector.shutDown(); } catch (Exception ex) { logger.debug("Failed to properly shutdown a stun detector: " + ex.getMessage()); } detector = null; useStun = false; // remove the listeners NetaddrActivator.getConfigurationService() .removeVetoableChangeListener(PROP_STUN_SERVER_ADDRESS, this); NetaddrActivator.getConfigurationService() .removeVetoableChangeListener(PROP_STUN_SERVER_PORT, this); } finally { logger.logExit(); } }
/** * Returns an InetAddress instance that represents the localhost, and that a socket can bind upon * or distribute to peers as a contact address. * * @param intendedDestination the destination that we'd like to use the localhost address with. * @return an InetAddress instance representing the local host, and that a socket can bind upon or * distribute to peers as a contact address. */ public synchronized InetAddress getLocalHost(InetAddress intendedDestination) { // no point in making sure that the localHostFinderSocket is initialized. // better let it through a NullPointerException. InetAddress localHost = null; localHostFinderSocket.connect(intendedDestination, this.RANDOM_ADDR_DISC_PORT); localHost = localHostFinderSocket.getLocalAddress(); localHostFinderSocket.disconnect(); // windows socket implementations return the any address so we need to // find something else here ... InetAddress.getLocalHost seems to work // better on windows so lets hope it'll do the trick. if (localHost.isAnyLocalAddress()) { try { // all that's inside the if is an ugly IPv6 hack // (good ol' IPv6 - always causing more problems than it solves.) if (intendedDestination instanceof Inet6Address) { // return the first globally routable ipv6 address we find // on the machine (and hope it's a good one) Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); while (interfaces.hasMoreElements()) { NetworkInterface iface = (NetworkInterface) interfaces.nextElement(); Enumeration addresses = iface.getInetAddresses(); while (addresses.hasMoreElements()) { InetAddress address = (InetAddress) addresses.nextElement(); if (address instanceof Inet6Address) { if (!address.isAnyLocalAddress() && !address.isLinkLocalAddress() && !address.isSiteLocalAddress() && !address.isLoopbackAddress()) { return address; } } } } } else localHost = InetAddress.getLocalHost(); /** @todo test on windows for ipv6 cases */ } catch (Exception ex) { // sigh ... ok return 0.0.0.0 logger.warn("Failed to get localhost ", ex); } } return localHost; }
/** * This implementation of the Network Address Manager allows you to intelligently retrieve the * address of your localhost according to preferences specified in a number of properties like: <br> * net.java.sip.communicator.STUN_SERVER_ADDRESS - the address of the stun server to use for NAT * traversal <br> * net.java.sip.communicator.STUN_SERVER_PORT - the port of the stun server to use for NAT traversal * <br> * java.net.preferIPv6Addresses - a system property specifying weather ipv6 addresses are to be * preferred in address resolution (default is false for backward compatibility) <br> * net.java.sip.communicator.common.PREFERRED_NETWORK_ADDRESS - the address that the user would like * to use. (If this is a valid address it will be returned in getLocalhost() calls) <br> * net.java.sip.communicator.common.PREFERRED_NETWORK_INTERFACE - the network interface that the * user would like to use for fommunication (addresses belonging to that interface will be prefered * when selecting a localhost address) * * @todo further explain the way the service works. explain address selection algorithms and * priorities. * @author Emil Ivov */ public class NetworkAddressManagerServiceImpl implements NetworkAddressManagerService, VetoableChangeListener { private static Logger logger = Logger.getLogger(NetworkAddressManagerServiceImpl.class); /** The name of the property containing the stun server address. */ private static final String PROP_STUN_SERVER_ADDRESS = "net.java.sip.communicator.impl.netaddr.STUN_SERVER_ADDRESS"; /** The port number of the stun server to use for NAT traversal */ private static final String PROP_STUN_SERVER_PORT = "net.java.sip.communicator.impl.netaddr.STUN_SERVER_PORT"; /** A stun4j address resolver */ private SimpleAddressDetector detector = null; /** Specifies whether or not STUN should be used for NAT traversal */ private boolean useStun = false; /** The address of the stun server that we're currently using. */ private StunAddress stunServerAddress = null; /** * The socket that we use for dummy connections during selection of a local address that has to be * used when communicating with a specific location. */ DatagramSocket localHostFinderSocket = null; /** * A random (unused)local port to use when trying to select a local host address to use when * sending messages to a specific destination. */ private static final int RANDOM_ADDR_DISC_PORT = 55721; /** * The prefix used for Dynamic Configuration of IPv4 Link-Local Addresses. <br> * {@link http://ietf.org/rfc/rfc3927.txt} */ private static final String DYNAMIC_CONF_FOR_IPV4_ADDR_PREFIX = "169.254"; /** * The name of the property containing the number of binds that we should should execute in case a * port is already bound to (each retry would be on a new random port). */ public static final String BIND_RETRIES_PROPERTY_NAME = "net.java.sip.communicator.service.netaddr.BIND_RETRIES"; /** Default STUN server address. */ public static final String DEFAULT_STUN_SERVER_ADDRESS = "stun.iptel.org"; /** Default STUN server port. */ public static final int DEFAULT_STUN_SERVER_PORT = 3478; /** * Initializes this network address manager service implementation and starts all * processes/threads associated with this address manager, such as a stun firewall/nat detector, * keep alive threads, binding lifetime discovery threads and etc. The method may also be used * after a call to stop() as a reinitialization technique. */ public void start() { // init stun String stunAddressStr = null; int port = -1; stunAddressStr = NetaddrActivator.getConfigurationService().getString(PROP_STUN_SERVER_ADDRESS); String portStr = NetaddrActivator.getConfigurationService().getString(PROP_STUN_SERVER_PORT); this.localHostFinderSocket = initRandomPortSocket(); if (stunAddressStr == null || portStr == null) { useStun = false; // we use the default stun server address only for chosing a public // route and not for stun queries. stunServerAddress = new StunAddress(DEFAULT_STUN_SERVER_ADDRESS, DEFAULT_STUN_SERVER_PORT); logger.info( "Stun server address(" + stunAddressStr + ")/port(" + portStr + ") not set (or invalid). Disabling STUN."); } else { try { port = Integer.valueOf(portStr).intValue(); } catch (NumberFormatException ex) { logger.error(portStr + " is not a valid port number. " + "Defaulting to 3478", ex); port = 3478; } stunServerAddress = new StunAddress(stunAddressStr, port); detector = new SimpleAddressDetector(stunServerAddress); if (logger.isDebugEnabled()) { logger.debug( "Created a STUN Address detector for the following " + "STUN server: " + stunAddressStr + ":" + port); } detector.start(); logger.debug("STUN server detector started;"); // make sure that someone doesn't set invalid stun address and port NetaddrActivator.getConfigurationService() .addVetoableChangeListener(PROP_STUN_SERVER_ADDRESS, this); NetaddrActivator.getConfigurationService() .addVetoableChangeListener(PROP_STUN_SERVER_PORT, this); // now start a thread query to the stun server and only set the // useStun flag to true if it succeeds. launchStunServerTest(); } } /** * Kills all threads/processes lauched by this thread and prepares it for shutdown. You may use * this method as a reinitialization technique ( you'll have to call start afterwards) */ public void stop() { try { try { detector.shutDown(); } catch (Exception ex) { logger.debug("Failed to properly shutdown a stun detector: " + ex.getMessage()); } detector = null; useStun = false; // remove the listeners NetaddrActivator.getConfigurationService() .removeVetoableChangeListener(PROP_STUN_SERVER_ADDRESS, this); NetaddrActivator.getConfigurationService() .removeVetoableChangeListener(PROP_STUN_SERVER_PORT, this); } finally { logger.logExit(); } } /** * Returns an InetAddress instance that represents the localhost, and that a socket can bind upon * or distribute to peers as a contact address. * * @param intendedDestination the destination that we'd like to use the localhost address with. * @return an InetAddress instance representing the local host, and that a socket can bind upon or * distribute to peers as a contact address. */ public synchronized InetAddress getLocalHost(InetAddress intendedDestination) { // no point in making sure that the localHostFinderSocket is initialized. // better let it through a NullPointerException. InetAddress localHost = null; localHostFinderSocket.connect(intendedDestination, this.RANDOM_ADDR_DISC_PORT); localHost = localHostFinderSocket.getLocalAddress(); localHostFinderSocket.disconnect(); // windows socket implementations return the any address so we need to // find something else here ... InetAddress.getLocalHost seems to work // better on windows so lets hope it'll do the trick. if (localHost.isAnyLocalAddress()) { try { // all that's inside the if is an ugly IPv6 hack // (good ol' IPv6 - always causing more problems than it solves.) if (intendedDestination instanceof Inet6Address) { // return the first globally routable ipv6 address we find // on the machine (and hope it's a good one) Enumeration interfaces = NetworkInterface.getNetworkInterfaces(); while (interfaces.hasMoreElements()) { NetworkInterface iface = (NetworkInterface) interfaces.nextElement(); Enumeration addresses = iface.getInetAddresses(); while (addresses.hasMoreElements()) { InetAddress address = (InetAddress) addresses.nextElement(); if (address instanceof Inet6Address) { if (!address.isAnyLocalAddress() && !address.isLinkLocalAddress() && !address.isSiteLocalAddress() && !address.isLoopbackAddress()) { return address; } } } } } else localHost = InetAddress.getLocalHost(); /** @todo test on windows for ipv6 cases */ } catch (Exception ex) { // sigh ... ok return 0.0.0.0 logger.warn("Failed to get localhost ", ex); } } return localHost; } /** * The method queries a Stun server for a binding for the specified port. * * @param port the port to resolve (the stun message gets sent trhough that port) * @return StunAddress the address returned by the stun server or null if an error occurred or no * address was returned * @throws IOException if an error occurs while stun4j is using sockets. * @throws BindException if the port is already in use. */ private StunAddress queryStunServer(int port) throws IOException, BindException { StunAddress mappedAddress = null; if (detector != null && useStun) { mappedAddress = detector.getMappingFor(port); if (logger.isDebugEnabled()) logger.debug( "For port:" + port + "a Stun server returned the " + "following mapping [" + mappedAddress); } return mappedAddress; } /** * The method queries a Stun server for a binding for the port and address that <tt>sock</tt> is * bound on. * * @param sock the socket whose port and address we'dlike to resolve (the stun message gets sent * trhough that socket) * @return StunAddress the address returned by the stun server or null if an error occurred or no * address was returned * @throws IOException if an error occurs while stun4j is using sockets. * @throws BindException if the port is already in use. */ private StunAddress queryStunServer(DatagramSocket sock) throws IOException, BindException { StunAddress mappedAddress = null; if (detector != null && useStun) { mappedAddress = detector.getMappingFor(sock); if (logger.isTraceEnabled()) { logger.trace( "For socket with address " + sock.getLocalAddress().getHostAddress() + " and port " + sock.getLocalPort() + " the stun server returned the " + "following mapping [" + mappedAddress + "]"); } } return mappedAddress; } /** * Tries to obtain a mapped/public address for the specified port (possibly by executing a STUN * query). * * @param dst the destination that we'd like to use this address with. * @param port the port whose mapping we are interested in. * @return a public address corresponding to the specified port or null if all attempts to * retrieve such an address have failed. * @throws IOException if an error occurs while stun4j is using sockets. * @throws BindException if the port is already in use. */ public InetSocketAddress getPublicAddressFor(InetAddress dst, int port) throws IOException, BindException { if (!useStun || (dst instanceof Inet6Address)) { logger.debug( "Stun is disabled for destination " + dst + ", skipping mapped address recovery (useStun=" + useStun + ", IPv6@=" + (dst instanceof Inet6Address) + ")."); // we'll still try to bind though so that we could notify the caller // if the port has been taken already. DatagramSocket bindTestSocket = new DatagramSocket(port); bindTestSocket.close(); // if we're here then the port was free. return new InetSocketAddress(getLocalHost(dst), port); } StunAddress mappedAddress = queryStunServer(port); InetSocketAddress result = null; if (mappedAddress != null) result = mappedAddress.getSocketAddress(); else { // Apparently STUN failed. Let's try to temporarily disble it // and use algorithms in getLocalHost(). ... We should probably // eveng think about completely disabling stun, and not only // temporarily. // Bug report - John J. Barton - IBM InetAddress localHost = getLocalHost(dst); result = new InetSocketAddress(localHost, port); } if (logger.isDebugEnabled()) logger.debug("Returning mapping for port:" + port + " as follows: " + result); return result; } /** * Tries to obtain a mapped/public address for the specified port (possibly by executing a STUN * query). * * @param port the port whose mapping we are interested in. * @return a public address corresponding to the specified port or null if all attempts to * retrieve such an address have failed. * @throws IOException if an error occurs while stun4j is using sockets. * @throws BindException if the port is already in use. */ public InetSocketAddress getPublicAddressFor(int port) throws IOException, BindException { return getPublicAddressFor(this.stunServerAddress.getSocketAddress().getAddress(), port); } /** * This method gets called when a bound property is changed. * * @param evt A PropertyChangeEvent object describing the event source and the property that has * changed. */ public void propertyChange(PropertyChangeEvent evt) { // there's no point in implementing this method as we have no way of // knowing whether the current property change event is the only event // we're going to get or whether another one is going to follow.. // in the case of a STUN_SERVER_ADDRESS property change for example // there's no way of knowing whether a STUN_SERVER_PORT property change // will follow or not. // Reinitializaion will therefore only happen if the reinitialize() // method is called. } /** * This method gets called when a property we're interested in is about to change. In case we * don't like the new value we throw a PropertyVetoException to prevent the actual change from * happening. * * @param evt a <tt>PropertyChangeEvent</tt> object describing the event source and the property * that will change. * @exception PropertyVetoException if we don't want the change to happen. */ public void vetoableChange(PropertyChangeEvent evt) throws PropertyVetoException { if (evt.getPropertyName().equals(PROP_STUN_SERVER_ADDRESS)) { // make sure that we have a valid fqdn or ip address. // null or empty port is ok since it implies turning STUN off. if (evt.getNewValue() == null) return; String host = evt.getNewValue().toString(); if (host.trim().length() == 0) return; boolean ipv6Expected = false; if (host.charAt(0) == '[') { // This is supposed to be an IPv6 litteral if (host.length() > 2 && host.charAt(host.length() - 1) == ']') { host = host.substring(1, host.length() - 1); ipv6Expected = true; } else { // This was supposed to be a IPv6 address, but it's not! throw new PropertyVetoException("Invalid address string" + host, evt); } } for (int i = 0; i < host.length(); i++) { char c = host.charAt(i); if (Character.isLetterOrDigit(c)) continue; if ((c != '.' && c != ':') || (c == '.' && ipv6Expected) || (c == ':' && !ipv6Expected)) throw new PropertyVetoException(host + " is not a valid address nor host name", evt); } } // is prop_stun_server_address else if (evt.getPropertyName().equals(PROP_STUN_SERVER_PORT)) { // null or empty port is ok since it implies turning STUN off. if (evt.getNewValue() == null) return; String port = evt.getNewValue().toString(); if (port.trim().length() == 0) return; try { Integer.valueOf(evt.getNewValue().toString()); } catch (NumberFormatException ex) { throw new PropertyVetoException(port + " is not a valid port! " + ex.getMessage(), evt); } } } /** * Initializes and binds a socket that on a random port number. The method would try to bind on a * random port and retry 5 times until a free port is found. * * @return the socket that we have initialized on a randomport number. */ private DatagramSocket initRandomPortSocket() { DatagramSocket resultSocket = null; String bindRetriesStr = NetaddrActivator.getConfigurationService().getString(BIND_RETRIES_PROPERTY_NAME); int bindRetries = 5; if (bindRetriesStr != null) { try { bindRetries = Integer.parseInt(bindRetriesStr); } catch (NumberFormatException ex) { logger.error( bindRetriesStr + " does not appear to be an integer. " + "Defaulting port bind retries to " + bindRetries, ex); } } int currentlyTriedPort = NetworkUtils.getRandomPortNumber(); // we'll first try to bind to a random port. if this fails we'll try // again (bindRetries times in all) until we find a free local port. for (int i = 0; i < bindRetries; i++) { try { resultSocket = new DatagramSocket(currentlyTriedPort); // we succeeded - break so that we don't try to bind again break; } catch (SocketException exc) { if (exc.getMessage().indexOf("Address already in use") == -1) { logger.fatal( "An exception occurred while trying to create" + "a local host discovery socket.", exc); resultSocket = null; return null; } // port seems to be taken. try another one. logger.debug("Port " + currentlyTriedPort + " seems in use."); currentlyTriedPort = NetworkUtils.getRandomPortNumber(); logger.debug("Retrying bind on port " + currentlyTriedPort); } } return resultSocket; } /** * Runs a test query agains the stun server. If it works we set useStun to true, otherwise we set * it to false. */ private void launchStunServerTest() { Thread stunServerTestThread = new Thread("StunServerTestThread") { public void run() { DatagramSocket randomSocket = initRandomPortSocket(); try { StunAddress stunAddress = detector.getMappingFor(randomSocket); randomSocket.disconnect(); if (stunAddress != null) { useStun = true; logger.trace( "StunServer check succeeded for server: " + detector.getServerAddress() + " and local port: " + randomSocket.getLocalPort()); } else { useStun = false; logger.trace( "StunServer check failed for server: " + detector.getServerAddress() + " and local port: " + randomSocket.getLocalPort() + ". No address returned by server."); } } catch (Throwable ex) { logger.error( "Failed to run a stun query against " + "server :" + detector.getServerAddress(), ex); if (randomSocket.isConnected()) randomSocket.disconnect(); useStun = false; } } }; stunServerTestThread.setDaemon(true); stunServerTestThread.start(); } }
/** * 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(); } }
/** 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(); }