/* * 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.service.configuration.*; import net.java.sip.communicator.service.configuration.event.*; import net.java.sip.communicator.service.netaddr.*; import net.java.sip.communicator.util.*; import net.java.stun4j.*; import net.java.stun4j.client.*; import java.util.*; import java.io.*; /** * 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) { if(logger.isTraceEnabled()) { logger.trace("Querying a localhost addr for dst=" + intendedDestination); } //no point in making sure that the localHostFinderSocket is initialized. //better let it through a NullPointerException. InetAddress localHost = null; localHostFinderSocket.connect(intendedDestination , 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()) { logger.trace("Socket returned the AnyLocalAddress. "+ "Trying a workaround."); 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<NetworkInterface> interfaces = NetworkInterface.getNetworkInterfaces(); while (interfaces.hasMoreElements()) { NetworkInterface iface = interfaces.nextElement(); Enumeration<InetAddress> addresses = iface.getInetAddresses(); while(addresses.hasMoreElements()) { InetAddress address = addresses.nextElement(); if(address instanceof Inet6Address) { if(!address.isAnyLocalAddress() && !address.isLinkLocalAddress() && !address.isSiteLocalAddress() && !address.isLoopbackAddress()) { if(logger.isTraceEnabled()) { logger.trace("will return ipv6 addr " + address); } 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); } } if(logger.isTraceEnabled()) { logger.trace("Will return the following localhost address: " + localHost); } 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 disable it //and use algorithms in getLocalHost(). ... We should probably //even 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(); } }