/**
   * 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();
  }