protected boolean isEquivalent(SshdSocketAddress that) {
   if (that == null) {
     return false;
   } else if (that == this) {
     return true;
   } else {
     return (this.getPort() == that.getPort())
         && Objects.equals(this.getHostName(), that.getHostName());
   }
 }
  @Override
  public synchronized void stopDynamicPortForwarding(SshdSocketAddress local) throws IOException {
    Closeable obj;
    synchronized (dynamicLocal) {
      obj = dynamicLocal.remove(local.getPort());
    }

    if (obj != null) {
      if (log.isDebugEnabled()) {
        log.debug("stopDynamicPortForwarding(" + local + ") unbinding");
      }
      obj.close(true);
      acceptor.unbind(local.toInetSocketAddress());
    } else {
      if (log.isDebugEnabled()) {
        log.debug("stopDynamicPortForwarding(" + local + ") no binding found");
      }
    }
  }
  @Override
  public synchronized SshdSocketAddress localPortForwardingRequested(SshdSocketAddress local)
      throws IOException {
    ValidateUtils.checkNotNull(local, "Local address is null");
    ValidateUtils.checkTrue(local.getPort() >= 0, "Invalid local port: %s", local);

    FactoryManager manager = session.getFactoryManager();
    ForwardingFilter filter = manager.getTcpipForwardingFilter();
    if ((filter == null) || (!filter.canListen(local, session))) {
      if (log.isDebugEnabled()) {
        log.debug(
            "localPortForwardingRequested("
                + session
                + ")["
                + local
                + "][haveFilter="
                + (filter != null)
                + "] rejected");
      }
      throw new IOException("Rejected address: " + local);
    }
    InetSocketAddress bound = doBind(local, staticIoHandlerFactory);
    SshdSocketAddress result = new SshdSocketAddress(bound.getHostString(), bound.getPort());
    if (log.isDebugEnabled()) {
      log.debug("localPortForwardingRequested(" + local + "): " + result);
    }

    boolean added;
    synchronized (localForwards) {
      // NOTE !!! it is crucial to use the bound address host name first
      added =
          localForwards.add(
              new LocalForwardingEntry(
                  result.getHostName(), local.getHostName(), result.getPort()));
    }

    if (!added) {
      throw new IOException(
          "Failed to add local port forwarding entry for " + local + " -> " + result);
    }
    return result;
  }
  @Override
  public synchronized void stopLocalPortForwarding(SshdSocketAddress local) throws IOException {
    ValidateUtils.checkNotNull(local, "Local address is null");

    SshdSocketAddress bound;
    synchronized (localToRemote) {
      bound = localToRemote.remove(local.getPort());
    }

    if ((bound != null) && (acceptor != null)) {
      if (log.isDebugEnabled()) {
        log.debug("stopLocalPortForwarding(" + local + ") unbind " + bound);
      }
      acceptor.unbind(bound.toInetSocketAddress());
    } else {
      if (log.isDebugEnabled()) {
        log.debug("stopLocalPortForwarding(" + local + ") no mapping/acceptor for " + bound);
      }
    }
  }
  @Override
  public synchronized SshdSocketAddress startRemotePortForwarding(
      SshdSocketAddress remote, SshdSocketAddress local) throws IOException {
    ValidateUtils.checkNotNull(local, "Local address is null");
    ValidateUtils.checkNotNull(remote, "Remote address is null");

    Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_GLOBAL_REQUEST);
    buffer.putString("tcpip-forward");
    buffer.putBoolean(true);
    buffer.putString(remote.getHostName());
    buffer.putInt(remote.getPort());
    Buffer result = session.request(buffer);
    if (result == null) {
      throw new SshException("Tcpip forwarding request denied by server");
    }
    int port = (remote.getPort() == 0) ? result.getInt() : remote.getPort();
    // TODO: Is it really safe to only store the local address after the request ?
    SshdSocketAddress prev;
    synchronized (remoteToLocal) {
      prev = remoteToLocal.put(port, local);
    }

    if (prev != null) {
      throw new IOException(
          "Multiple remote port forwarding bindings on port="
              + port
              + ": current="
              + remote
              + ", previous="
              + prev);
    }

    SshdSocketAddress bound = new SshdSocketAddress(remote.getHostName(), port);
    if (log.isDebugEnabled()) {
      log.debug("startRemotePortForwarding(" + remote + " -> " + local + "): " + bound);
    }

    return bound;
  }
  @Override
  public synchronized void localPortForwardingCancelled(SshdSocketAddress local)
      throws IOException {
    LocalForwardingEntry entry;
    synchronized (localForwards) {
      entry =
          LocalForwardingEntry.findMatchingEntry(
              local.getHostName(), local.getPort(), localForwards);
      if (entry != null) {
        localForwards.remove(entry);
      }
    }

    if ((entry != null) && (acceptor != null)) {
      if (log.isDebugEnabled()) {
        log.debug("localPortForwardingCancelled(" + local + ") unbind " + entry);
      }
      acceptor.unbind(entry.toInetSocketAddress());
    } else {
      if (log.isDebugEnabled()) {
        log.debug("localPortForwardingCancelled(" + local + ") no match/acceptor: " + entry);
      }
    }
  }
  @Override
  public synchronized void stopRemotePortForwarding(SshdSocketAddress remote) throws IOException {
    SshdSocketAddress bound;
    synchronized (remoteToLocal) {
      bound = remoteToLocal.remove(remote.getPort());
    }

    if (bound != null) {
      if (log.isDebugEnabled()) {
        log.debug("stopRemotePortForwarding(" + remote + ") cancel forwarding to " + bound);
      }

      Buffer buffer = session.createBuffer(SshConstants.SSH_MSG_GLOBAL_REQUEST);
      buffer.putString("cancel-tcpip-forward");
      buffer.putBoolean(false);
      buffer.putString(remote.getHostName());
      buffer.putInt(remote.getPort());
      session.writePacket(buffer);
    } else {
      if (log.isDebugEnabled()) {
        log.debug("stopRemotePortForwarding(" + remote + ") no binding found");
      }
    }
  }
  /**
   * @param address The request bind address
   * @param handlerFactory A {@link Factory} to create an {@link IoHandler} if necessary
   * @return The {@link InetSocketAddress} to which the binding occurred
   * @throws IOException If failed to bind
   */
  private InetSocketAddress doBind(
      SshdSocketAddress address, Factory<? extends IoHandler> handlerFactory) throws IOException {
    if (acceptor == null) {
      FactoryManager manager = session.getFactoryManager();
      IoServiceFactory factory = manager.getIoServiceFactory();
      IoHandler handler = handlerFactory.create();
      acceptor = factory.createAcceptor(handler);
    }

    // TODO find a better way to determine the resulting bind address - what if multi-threaded
    // calls...
    Set<SocketAddress> before = acceptor.getBoundAddresses();
    try {
      InetSocketAddress bindAddress = address.toInetSocketAddress();
      acceptor.bind(bindAddress);

      Set<SocketAddress> after = acceptor.getBoundAddresses();
      if (GenericUtils.size(after) > 0) {
        after.removeAll(before);
      }
      if (GenericUtils.isEmpty(after)) {
        throw new IOException(
            "Error binding to " + address + "[" + bindAddress + "]: no local addresses bound");
      }

      if (after.size() > 1) {
        throw new IOException(
            "Multiple local addresses have been bound for " + address + "[" + bindAddress + "]");
      }
      return (InetSocketAddress) after.iterator().next();
    } catch (IOException bindErr) {
      Set<SocketAddress> after = acceptor.getBoundAddresses();
      if (GenericUtils.isEmpty(after)) {
        close();
      }
      throw bindErr;
    }
  }
  @Override
  public synchronized SshdSocketAddress startDynamicPortForwarding(SshdSocketAddress local)
      throws IOException {
    ValidateUtils.checkNotNull(local, "Local address is null");
    ValidateUtils.checkTrue(local.getPort() >= 0, "Invalid local port: %s", local);

    if (isClosed()) {
      throw new IllegalStateException("TcpipForwarder is closed");
    }
    if (isClosing()) {
      throw new IllegalStateException("TcpipForwarder is closing");
    }

    SocksProxy socksProxy = new SocksProxy(service);
    SocksProxy prev;
    InetSocketAddress bound = doBind(local, socksProxyIoHandlerFactory);
    int port = bound.getPort();
    synchronized (dynamicLocal) {
      prev = dynamicLocal.put(port, socksProxy);
    }

    if (prev != null) {
      throw new IOException(
          "Multiple dynamic port mappings found for port="
              + port
              + ": current="
              + socksProxy
              + ", previous="
              + prev);
    }

    SshdSocketAddress result = new SshdSocketAddress(bound.getHostString(), port);
    if (log.isDebugEnabled()) {
      log.debug("startDynamicPortForwarding(" + local + "): " + result);
    }

    return result;
  }
  @Override
  public synchronized SshdSocketAddress startLocalPortForwarding(
      SshdSocketAddress local, SshdSocketAddress remote) throws IOException {
    ValidateUtils.checkNotNull(local, "Local address is null");
    ValidateUtils.checkTrue(local.getPort() >= 0, "Invalid local port: %s", local);
    ValidateUtils.checkNotNull(remote, "Remote address is null");

    if (isClosed()) {
      throw new IllegalStateException("TcpipForwarder is closed");
    }
    if (isClosing()) {
      throw new IllegalStateException("TcpipForwarder is closing");
    }

    InetSocketAddress bound = doBind(local, staticIoHandlerFactory);
    int port = bound.getPort();
    SshdSocketAddress prev;
    synchronized (localToRemote) {
      prev = localToRemote.put(port, remote);
    }

    if (prev != null) {
      throw new IOException(
          "Multiple local port forwarding bindings on port="
              + port
              + ": current="
              + remote
              + ", previous="
              + prev);
    }

    SshdSocketAddress result = new SshdSocketAddress(bound.getHostString(), port);
    if (log.isDebugEnabled()) {
      log.debug("startLocalPortForwarding(" + local + " -> " + remote + "): " + result);
    }
    return result;
  }