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