You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2020/10/29 17:30:08 UTC
[mina-sshd] branch master updated: [SSHD-1066] Allow multiple
binding to local port tunnel on different addresses
This is an automated email from the ASF dual-hosted git repository.
lgoldstein pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/mina-sshd.git
The following commit(s) were added to refs/heads/master by this push:
new 1498b76 [SSHD-1066] Allow multiple binding to local port tunnel on different addresses
1498b76 is described below
commit 1498b762d95c8118f903f89f92dbb79e9617af68
Author: Lyor Goldstein <lg...@apache.org>
AuthorDate: Fri Oct 23 18:22:59 2020 +0300
[SSHD-1066] Allow multiple binding to local port tunnel on different addresses
---
CHANGES.md | 1 +
.../sshd/common/util/net/SshdSocketAddress.java | 95 ++++++++++-
.../sshd/client/session/AbstractClientSession.java | 2 +-
.../apache/sshd/client/session/ClientSession.java | 16 ++
.../sshd/common/forward/DefaultForwarder.java | 139 ++++++++++-------
.../sshd/common/forward/LocalForwardingEntry.java | 173 ++++++++++++++++-----
.../forward/PortForwardingInformationProvider.java | 19 +--
.../sshd/common/forward/PortForwardingManager.java | 12 ++
.../sshd/common/forward/TcpipClientChannel.java | 2 +-
.../sshd/common/session/helpers/SessionHelper.java | 10 +-
...calForwardingEntryCombinedBoundAddressTest.java | 121 ++++++++++++++
.../common/forward/LocalForwardingEntryTest.java | 97 +++++++++++-
.../sshd/common/forward/PortForwardingTest.java | 98 +++++++++++-
.../org/apache/sshd/util/test/BaseTestSupport.java | 24 +++
.../sftp/client/AbstractSftpClientTestSupport.java | 20 +--
15 files changed, 680 insertions(+), 149 deletions(-)
diff --git a/CHANGES.md b/CHANGES.md
index 001fc9e..e47f90d 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -57,5 +57,6 @@ or `-key-file` command line option.
* [SSHD-1058](https://issues.apache.org/jira/browse/SSHD-1058) Improve exception logging strategy.
* [SSHD-1059](https://issues.apache.org/jira/browse/SSHD-1059) Do not send heartbeat if KEX state not DONE
* [SSHD-1063](https://issues.apache.org/jira/browse/SSHD-1063) Fixed known-hosts file server key verifier matching of same host with different ports
+* [SSHD-1066](https://issues.apache.org/jira/browse/SSHD-1066) Allow multiple binding to local port tunnel on different addresses
* [SSHD-1070](https://issues.apache.org/jira/browse/SSHD-1070) OutOfMemoryError when use async port forwarding
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java b/sshd-common/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java
index e19f4b7..701e761 100644
--- a/sshd-common/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java
+++ b/sshd-common/src/main/java/org/apache/sshd/common/util/net/SshdSocketAddress.java
@@ -31,6 +31,7 @@ import java.util.Comparator;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
+import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -191,7 +192,7 @@ public class SshdSocketAddress extends SocketAddress {
return true;
} else {
return (this.getPort() == that.getPort())
- && (GenericUtils.safeCompare(this.getHostName(), that.getHostName(), false) == 0);
+ && isEquivalentHostName(this.getHostName(), that.getHostName(), false);
}
}
@@ -208,12 +209,12 @@ public class SshdSocketAddress extends SocketAddress {
@Override
public int hashCode() {
- return GenericUtils.hashCode(getHostName(), Boolean.FALSE) + getPort();
+ return GenericUtils.hashCode(getHostName(), Boolean.FALSE) + 31 * Integer.hashCode(getPort());
}
/**
* Returns the first external network address assigned to this machine or null if one is not found.
- *
+ *
* @return Inet4Address associated with an external interface DevNote: We actually return InetAddress here, as
* Inet4Addresses are final and cannot be mocked.
*/
@@ -292,7 +293,6 @@ public class SshdSocketAddress extends SocketAddress {
}
return !isLoopback(addr);
-
}
/**
@@ -325,11 +325,22 @@ public class SshdSocketAddress extends SocketAddress {
return false;
}
- if (LOCALHOST_NAME.equals(ip) || LOCALHOST_IPV4.equals(ip)) {
+ if (LOCALHOST_NAME.equals(ip)) {
return true;
}
- // TODO add support for IPv6 - see SSHD-746
+ return isIPv4LoopbackAddress(ip) || isIPv6LoopbackAddress(ip);
+ }
+
+ public static boolean isIPv4LoopbackAddress(String ip) {
+ if (GenericUtils.isEmpty(ip)) {
+ return false;
+ }
+
+ if (LOCALHOST_IPV4.equals(ip)) {
+ return true; // most used
+ }
+
String[] values = GenericUtils.split(ip, '.');
if (GenericUtils.length(values) != 4) {
return false;
@@ -352,6 +363,34 @@ public class SshdSocketAddress extends SocketAddress {
return true;
}
+ public static boolean isIPv6LoopbackAddress(String ip) {
+ // TODO add more patterns - e.g., https://tools.ietf.org/id/draft-smith-v6ops-larger-ipv6-loopback-prefix-04.html
+ return IPV6_LONG_LOCALHOST.equals(ip) || IPV6_SHORT_LOCALHOST.equals(ip);
+ }
+
+ public static boolean isEquivalentHostName(String h1, String h2, boolean allowWildcard) {
+ if (GenericUtils.safeCompare(h1, h2, false) == 0) {
+ return true;
+ }
+
+ if (allowWildcard) {
+ return isWildcardAddress(h1) || isWildcardAddress(h2);
+ }
+
+ return false;
+ }
+
+ public static boolean isLoopbackAlias(String h1, String h2) {
+ return (LOCALHOST_NAME.equals(h1) && isLoopback(h2))
+ || (LOCALHOST_NAME.equals(h2) && isLoopback(h1));
+ }
+
+ public static boolean isWildcardAddress(String addr) {
+ return IPV4_ANYADDR.equalsIgnoreCase(addr)
+ || IPV6_LONG_ANY_ADDRESS.equalsIgnoreCase(addr)
+ || IPV6_SHORT_ANY_ADDRESS.equalsIgnoreCase(addr);
+ }
+
public static SshdSocketAddress toSshdSocketAddress(SocketAddress addr) {
if (addr == null) {
return null;
@@ -457,7 +496,7 @@ public class SshdSocketAddress extends SocketAddress {
/**
* Checks if the address is one of the allocated private blocks
- *
+ *
* @param addr The address string
* @return {@code true} if this is one of the allocated private blocks. <B>Note:</B> it assumes that the
* address string is indeed an IPv4 address
@@ -533,7 +572,7 @@ public class SshdSocketAddress extends SocketAddress {
* <LI>Has at most 3 <U>digits</U></LI>
* <LI>Its value is ≤ 255</LI>
* </UL>
- *
+ *
* @param c The {@link CharSequence} to be validate
* @return {@code true} if valid IPv4 address component
*/
@@ -652,4 +691,44 @@ public class SshdSocketAddress extends SocketAddress {
}
return true;
}
+
+ public static <V> V findByOptionalWildcardAddress(Map<SshdSocketAddress, ? extends V> map, SshdSocketAddress address) {
+ Map.Entry<SshdSocketAddress, ? extends V> entry = findMatchingOptionalWildcardEntry(map, address);
+ return (entry == null) ? null : entry.getValue();
+ }
+
+ public static <V> V removeByOptionalWildcardAddress(Map<SshdSocketAddress, ? extends V> map, SshdSocketAddress address) {
+ Map.Entry<SshdSocketAddress, ? extends V> entry = findMatchingOptionalWildcardEntry(map, address);
+ return (entry == null) ? null : map.remove(entry.getKey());
+ }
+
+ public static <V> Map.Entry<SshdSocketAddress, ? extends V> findMatchingOptionalWildcardEntry(
+ Map<SshdSocketAddress, ? extends V> map, SshdSocketAddress address) {
+ if (GenericUtils.isEmpty(map) || (address == null)) {
+ return null;
+ }
+
+ String hostName = address.getHostName();
+ Map.Entry<SshdSocketAddress, ? extends V> candidate = null;
+ for (Map.Entry<SshdSocketAddress, ? extends V> e : map.entrySet()) {
+ SshdSocketAddress a = e.getKey();
+ if (a.getPort() != address.getPort()) {
+ continue;
+ }
+
+ String candidateName = a.getHostName();
+ if (hostName.equalsIgnoreCase(candidateName)) {
+ return e; // If found exact match then use it
+ }
+
+ if (isEquivalentHostName(hostName, candidateName, true)) {
+ if (candidate != null) {
+ throw new IllegalStateException("Multiple candidate matches for " + address + ": " + candidate + ", " + e);
+ }
+ candidate = e;
+ }
+ }
+
+ return candidate;
+ }
}
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
index a1748f4..c7c8d5e 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/AbstractClientSession.java
@@ -328,7 +328,7 @@ public abstract class AbstractClientSession extends AbstractSession implements C
} else if (Channel.CHANNEL_SUBSYSTEM.equals(type)) {
return createSubsystemChannel(subType);
} else {
- throw new IllegalArgumentException("Unsupported channel type " + type);
+ throw new IllegalArgumentException("Unsupported channel type requested: " + type);
}
}
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
index 075d026..2de7695 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSession.java
@@ -312,6 +312,22 @@ public interface ClientSession
* Starts a local port forwarding and returns a tracker that stops the forwarding when the {@code close()} method is
* called. This tracker can be used in a {@code try-with-resource} block to ensure cleanup of the set up forwarding.
*
+ * @param localPort The local port - if zero one is allocated
+ * @param remote The remote address
+ * @return The tracker instance
+ * @throws IOException If failed to set up the requested forwarding
+ * @see #startLocalPortForwarding(SshdSocketAddress, SshdSocketAddress)
+ */
+ default ExplicitPortForwardingTracker createLocalPortForwardingTracker(
+ int localPort, SshdSocketAddress remote)
+ throws IOException {
+ return createLocalPortForwardingTracker(new SshdSocketAddress(localPort), remote);
+ }
+
+ /**
+ * Starts a local port forwarding and returns a tracker that stops the forwarding when the {@code close()} method is
+ * called. This tracker can be used in a {@code try-with-resource} block to ensure cleanup of the set up forwarding.
+ *
* @param local The local address
* @param remote The remote address
* @return The tracker instance
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultForwarder.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultForwarder.java
index 26a14c7..c1d24f1 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultForwarder.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/DefaultForwarder.java
@@ -19,6 +19,7 @@
package org.apache.sshd.common.forward;
import java.io.IOException;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.SocketAddress;
import java.time.Duration;
@@ -26,15 +27,14 @@ import java.util.AbstractMap.SimpleImmutableEntry;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.Comparator;
import java.util.EnumSet;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
import java.util.Objects;
import java.util.Set;
-import java.util.TreeMap;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.concurrent.atomic.AtomicLong;
import java.util.function.Consumer;
@@ -86,13 +86,13 @@ public class DefaultForwarder
private final Session sessionInstance;
private final Object localLock = new Object();
- private final Map<Integer, SshdSocketAddress> localToRemote = new TreeMap<>(Comparator.naturalOrder());
- private final Map<Integer, InetSocketAddress> boundLocals = new TreeMap<>(Comparator.naturalOrder());
+ private final Map<SshdSocketAddress, SshdSocketAddress> localToRemote = new HashMap<>();
+ private final Map<SshdSocketAddress, InetSocketAddress> boundLocals = new HashMap<>();
private final Object dynamicLock = new Object();
- private final Map<Integer, SshdSocketAddress> remoteToLocal = new TreeMap<>(Comparator.naturalOrder());
- private final Map<Integer, SocksProxy> dynamicLocal = new TreeMap<>(Comparator.naturalOrder());
- private final Map<Integer, InetSocketAddress> boundDynamic = new TreeMap<>(Comparator.naturalOrder());
+ private final Map<Integer, SshdSocketAddress> remoteToLocal = new HashMap<>();
+ private final Map<Integer, SocksProxy> dynamicLocal = new HashMap<>();
+ private final Map<Integer, InetSocketAddress> boundDynamic = new HashMap<>();
private final Set<LocalForwardingEntry> localForwards = new HashSet<>();
private final IoHandlerFactory staticIoHandlerFactory = StaticIoHandler::new;
@@ -186,29 +186,32 @@ public class DefaultForwarder
throw new IllegalStateException("TcpipForwarder is closed or closing: " + state);
}
- InetSocketAddress bound = null;
- int port;
signalEstablishingExplicitTunnel(local, remote, true);
+
+ InetSocketAddress bound = null;
+ SshdSocketAddress result;
try {
bound = doBind(local, getLocalIoAcceptor());
- port = bound.getPort();
+ int port = bound.getPort();
+ result = new SshdSocketAddress(bound.getHostString(), port);
+
synchronized (localLock) {
- SshdSocketAddress prevRemote = localToRemote.get(port);
+ SshdSocketAddress prevRemote = SshdSocketAddress.findByOptionalWildcardAddress(localToRemote, result);
if (prevRemote != null) {
throw new IOException(
- "Multiple local port forwarding addressing on port=" + port
+ "Multiple local port forwarding addressing on port=" + result
+ ": current=" + remote + ", previous=" + prevRemote);
}
- InetSocketAddress prevBound = boundLocals.get(port);
+ InetSocketAddress prevBound = SshdSocketAddress.findByOptionalWildcardAddress(boundLocals, result);
if (prevBound != null) {
throw new IOException(
- "Multiple local port forwarding bindings on port=" + port
+ "Multiple local port forwarding bindings on port=" + result
+ ": current=" + bound + ", previous=" + prevBound);
}
- localToRemote.put(port, remote);
- boundLocals.put(port, bound);
+ localToRemote.put(result, remote);
+ boundLocals.put(result, bound);
}
} catch (IOException | RuntimeException e) {
try {
@@ -221,7 +224,6 @@ public class DefaultForwarder
}
try {
- SshdSocketAddress result = new SshdSocketAddress(bound.getHostString(), port);
if (log.isDebugEnabled()) {
log.debug("startLocalPortForwarding(" + local + " -> " + remote + "): " + result);
}
@@ -239,10 +241,9 @@ public class DefaultForwarder
SshdSocketAddress remote;
InetSocketAddress bound;
- int port = local.getPort();
synchronized (localLock) {
- remote = localToRemote.remove(port);
- bound = boundLocals.remove(port);
+ remote = SshdSocketAddress.removeByOptionalWildcardAddress(localToRemote, local);
+ bound = SshdSocketAddress.removeByOptionalWildcardAddress(boundLocals, local);
}
unbindLocalForwarding(local, remote, bound);
@@ -723,29 +724,29 @@ public class DefaultForwarder
}
signalEstablishingExplicitTunnel(local, null, true);
+
SshdSocketAddress result;
try {
InetSocketAddress bound = doBind(local, getLocalIoAcceptor());
- result = new SshdSocketAddress(bound.getHostString(), bound.getPort());
+ result = new SshdSocketAddress(bound);
if (log.isDebugEnabled()) {
log.debug("localPortForwardingRequested(" + local + "): " + result);
}
boolean added;
+ LocalForwardingEntry localEntry = new LocalForwardingEntry(local, result);
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()));
+ added = localForwards.add(localEntry);
}
if (!added) {
throw new IOException("Failed to add local port forwarding entry for " + local + " -> " + result);
}
- } catch (IOException | RuntimeException e) {
+ } catch (IOException | RuntimeException | Error e) {
try {
localPortForwardingCancelled(local);
- } catch (IOException | RuntimeException err) {
- e.addSuppressed(e);
+ } catch (IOException | RuntimeException | Error err) {
+ e.addSuppressed(err);
}
signalEstablishedExplicitTunnel(local, null, true, null, e);
throw e;
@@ -763,7 +764,8 @@ public class DefaultForwarder
public synchronized void localPortForwardingCancelled(SshdSocketAddress local) throws IOException {
LocalForwardingEntry entry;
synchronized (localForwards) {
- entry = LocalForwardingEntry.findMatchingEntry(local.getHostName(), local.getPort(), localForwards);
+ entry = LocalForwardingEntry.findMatchingEntry(
+ local.getHostName(), local.getPort(), localForwards);
if (entry != null) {
localForwards.remove(entry);
}
@@ -774,15 +776,18 @@ public class DefaultForwarder
log.debug("localPortForwardingCancelled(" + local + ") unbind " + entry);
}
- signalTearingDownExplicitTunnel(entry, true, null);
+ SshdSocketAddress reportedBoundAddress = entry.getCombinedBoundAddress();
+ signalTearingDownExplicitTunnel(reportedBoundAddress, true, null);
+
+ SshdSocketAddress boundAddress = entry.getBoundAddress();
try {
- localAcceptor.unbind(entry.toInetSocketAddress());
- } catch (RuntimeException e) {
- signalTornDownExplicitTunnel(entry, true, null, e);
+ localAcceptor.unbind(boundAddress.toInetSocketAddress());
+ } catch (RuntimeException | Error e) {
+ signalTornDownExplicitTunnel(reportedBoundAddress, true, null, e);
throw e;
}
- signalTornDownExplicitTunnel(entry, true, null, null);
+ signalTornDownExplicitTunnel(reportedBoundAddress, true, null, null);
} else {
if (log.isDebugEnabled()) {
log.debug("localPortForwardingCancelled(" + local + ") no match/acceptor: " + entry);
@@ -993,12 +998,12 @@ public class DefaultForwarder
protected InetSocketAddress doBind(SshdSocketAddress address, IoAcceptor acceptor)
throws IOException {
// TODO find a better way to determine the resulting bind address - what if multi-threaded calls...
- Set<SocketAddress> before = acceptor.getBoundAddresses();
+ Collection<SocketAddress> before = acceptor.getBoundAddresses();
try {
InetSocketAddress bindAddress = address.toInetSocketAddress();
acceptor.bind(bindAddress);
- Set<SocketAddress> after = acceptor.getBoundAddresses();
+ Collection<SocketAddress> after = acceptor.getBoundAddresses();
if (GenericUtils.size(after) > 0) {
after.removeAll(before);
}
@@ -1009,7 +1014,9 @@ public class DefaultForwarder
if (after.size() > 1) {
throw new IOException("Multiple local addresses have been bound for " + address + "[" + bindAddress + "]");
}
- return (InetSocketAddress) GenericUtils.head(after);
+
+ InetSocketAddress boundAddress = (InetSocketAddress) GenericUtils.head(after);
+ return boundAddress;
} catch (IOException bindErr) {
Collection<SocketAddress> after = acceptor.getBoundAddresses();
if (GenericUtils.isEmpty(after)) {
@@ -1034,9 +1041,13 @@ public class DefaultForwarder
@Override
public void sessionCreated(IoSession session) throws Exception {
- InetSocketAddress local = (InetSocketAddress) session.getLocalAddress();
- int localPort = local.getPort();
- SshdSocketAddress remote = localToRemote.get(localPort);
+ InetSocketAddress localAddress = (InetSocketAddress) session.getLocalAddress();
+ SshdSocketAddress local = new SshdSocketAddress(localAddress);
+ SshdSocketAddress remote;
+ synchronized (localLock) {
+ remote = SshdSocketAddress.findByOptionalWildcardAddress(localToRemote, local);
+ }
+
TcpipClientChannel.Type channelType = (remote == null)
? TcpipClientChannel.Type.Forwarded
: TcpipClientChannel.Type.Direct;
@@ -1048,9 +1059,12 @@ public class DefaultForwarder
SocketAddress accepted = session.getAcceptanceAddress();
LocalForwardingEntry localEntry = null;
if (accepted instanceof InetSocketAddress) {
+ InetSocketAddress inetSocketAddress = (InetSocketAddress) accepted;
+ InetAddress inetAddress = inetSocketAddress.getAddress();
synchronized (localForwards) {
localEntry = LocalForwardingEntry.findMatchingEntry(
- ((InetSocketAddress) accepted).getHostString(), localPort, localForwards);
+ inetSocketAddress.getHostString(), inetAddress.isAnyLocalAddress(), local.getPort(),
+ localForwards);
}
}
@@ -1162,18 +1176,33 @@ public class DefaultForwarder
}
@Override
- public SshdSocketAddress getBoundLocalPortForward(int port) {
- ValidateUtils.checkTrue(port > 0, "Invalid local port: %d", port);
+ public List<SshdSocketAddress> getBoundLocalPortForwards(int port) {
+ synchronized (localLock) {
+ return localToRemote.isEmpty()
+ ? Collections.emptyList()
+ : localToRemote.keySet()
+ .stream()
+ .filter(k -> k.getPort() == port)
+ .collect(Collectors.toList());
+ }
+ }
- Integer portKey = Integer.valueOf(port);
- synchronized (localToRemote) {
- return localToRemote.get(portKey);
+ @Override
+ public boolean isLocalPortForwardingStartedForPort(int port) {
+ synchronized (localLock) {
+ return localToRemote.isEmpty()
+ ? false
+ : localToRemote.keySet()
+ .stream()
+ .filter(e -> e.getPort() == port)
+ .findAny()
+ .isPresent();
}
}
@Override
- public List<Map.Entry<Integer, SshdSocketAddress>> getLocalForwardsBindings() {
- synchronized (localToRemote) {
+ public List<Map.Entry<SshdSocketAddress, SshdSocketAddress>> getLocalForwardsBindings() {
+ synchronized (localLock) {
return localToRemote.isEmpty()
? Collections.emptyList()
: localToRemote.entrySet()
@@ -1184,13 +1213,9 @@ public class DefaultForwarder
}
@Override
- public NavigableSet<Integer> getStartedLocalPortForwards() {
- synchronized (localToRemote) {
- if (localToRemote.isEmpty()) {
- return Collections.emptyNavigableSet();
- }
-
- return GenericUtils.asSortedSet(localToRemote.keySet());
+ public List<SshdSocketAddress> getStartedLocalPortForwards() {
+ synchronized (localLock) {
+ return localToRemote.isEmpty() ? Collections.emptyList() : new ArrayList<>(localToRemote.keySet());
}
}
@@ -1219,11 +1244,7 @@ public class DefaultForwarder
@Override
public NavigableSet<Integer> getStartedRemotePortForwards() {
synchronized (remoteToLocal) {
- if (remoteToLocal.isEmpty()) {
- return Collections.emptyNavigableSet();
- }
-
- return GenericUtils.asSortedSet(remoteToLocal.keySet());
+ return remoteToLocal.isEmpty() ? Collections.emptyNavigableSet() : GenericUtils.asSortedSet(remoteToLocal.keySet());
}
}
}
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/LocalForwardingEntry.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/LocalForwardingEntry.java
index 13ed473..033d86a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/LocalForwardingEntry.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/LocalForwardingEntry.java
@@ -21,82 +21,185 @@ package org.apache.sshd.common.forward;
import java.net.InetSocketAddress;
import java.util.Collection;
+import java.util.Objects;
import org.apache.sshd.common.util.GenericUtils;
-import org.apache.sshd.common.util.ValidateUtils;
import org.apache.sshd.common.util.net.SshdSocketAddress;
/**
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
-public class LocalForwardingEntry extends SshdSocketAddress {
- private static final long serialVersionUID = 423661570180889621L;
- private final String alias;
+public class LocalForwardingEntry {
+ private final SshdSocketAddress local;
+ private final SshdSocketAddress bound;
+ private final SshdSocketAddress combined;
- // NOTE !!! it is crucial to use the bound address host name first
public LocalForwardingEntry(SshdSocketAddress local, InetSocketAddress bound) {
- this(local, new SshdSocketAddress(bound.getHostString(), bound.getPort()));
+ this(local, new SshdSocketAddress(bound));
}
- // NOTE !!! it is crucial to use the bound address host name first
public LocalForwardingEntry(SshdSocketAddress local, SshdSocketAddress bound) {
- this(bound.getHostName(), local.getHostName(), bound.getPort());
+ this.local = Objects.requireNonNull(local, "No local address provided");
+ this.bound = Objects.requireNonNull(bound, "No bound address provided");
+ this.combined = resolveCombinedBoundAddress(local, bound);
}
- public LocalForwardingEntry(String hostName, String alias, int port) {
- super(hostName, port);
- this.alias = ValidateUtils.checkNotNullAndNotEmpty(alias, "No host alias");
+ /**
+ * @return The original requested local address for binding
+ */
+ public SshdSocketAddress getLocalAddress() {
+ return local;
}
- public String getAlias() {
- return alias;
+ /**
+ * @return The actual bound address
+ */
+ public SshdSocketAddress getBoundAddress() {
+ return bound;
}
- @Override
- protected boolean isEquivalent(SshdSocketAddress that) {
- if (super.isEquivalent(that) && (that instanceof LocalForwardingEntry)) {
- LocalForwardingEntry entry = (LocalForwardingEntry) that;
- if (GenericUtils.safeCompare(this.getAlias(), entry.getAlias(), false) == 0) {
- return true;
- }
- }
-
- return false;
+ /**
+ * A combined address using the following logic:
+ * <UL>
+ * <LI>If original requested local binding has a specific port and non-wildcard address then use the local binding
+ * as-is</LI>
+ *
+ * <LI>If original requested local binding has a specific address but no specific port, then combine its address
+ * with the actual auto-allocated port at binding.</LI>
+ *
+ * <LI>If original requested local binding has neither a specific address nor a specific port then use the effective
+ * bound address.</LI>
+ * <UL>
+ *
+ * @return Combined result
+ */
+ public SshdSocketAddress getCombinedBoundAddress() {
+ return combined;
}
@Override
public boolean equals(Object o) {
- return super.equals(o);
+ if (o == null) {
+ return false;
+ }
+ if (o == this) {
+ return true;
+ }
+ if (getClass() != o.getClass()) {
+ return false;
+ }
+
+ LocalForwardingEntry other = (LocalForwardingEntry) o;
+ return Objects.equals(getCombinedBoundAddress(), other.getCombinedBoundAddress());
}
@Override
public int hashCode() {
- return super.hashCode() + GenericUtils.hashCode(getAlias(), Boolean.FALSE);
+ return Objects.hashCode(getCombinedBoundAddress());
}
@Override
public String toString() {
- return super.toString() + " - " + getAlias();
+ return getClass().getSimpleName()
+ + "[local=" + getLocalAddress()
+ + ", bound=" + getBoundAddress()
+ + ", combined=" + getCombinedBoundAddress() + "]";
+ }
+
+ public static SshdSocketAddress resolveCombinedBoundAddress(SshdSocketAddress local, SshdSocketAddress bound) {
+ int localPort = local.getPort();
+ int boundPort = bound.getPort();
+ if ((localPort > 0) && (localPort != boundPort)) {
+ throw new IllegalArgumentException("Mismatched ports for local (" + local + ") vs. bound (" + bound + ") entry");
+ }
+
+ if (Objects.equals(local, bound)) {
+ return local;
+ }
+
+ String localName = local.getHostName();
+ boolean wildcardLocal = SshdSocketAddress.isWildcardAddress(localName);
+ if (wildcardLocal) {
+ return bound;
+ }
+
+ if (localPort > 0) {
+ return local; // have a specific local address
+ }
+
+ // Missing the port from local address
+ return new SshdSocketAddress(localName, boundPort);
+ }
+
+ public static LocalForwardingEntry findMatchingEntry(
+ String host, int port, Collection<? extends LocalForwardingEntry> entries) {
+ return findMatchingEntry(host, SshdSocketAddress.isWildcardAddress(host), port, entries);
}
/**
- * @param host The host - ignored if {@code null}/empty - i.e., no match reported
- * @param port The port - ignored if non-positive - i.e., no match reported
- * @param entries The {@link Collection} of {@link LocalForwardingEntry} to check - ignored if {@code null}/empty -
- * i.e., no match reported
- * @return The <U>first</U> entry whose host or alias matches the host name - case <U>insensitive</U>
- * <B>and</B> has a matching port - {@code null} if no match found
+ * @param host The host - ignored if {@code null}/empty and not wildcard address match - i.e., no match
+ * reported
+ * @param anyLocalAddress Is host the wildcard address - in which case, we try an exact match first for the host,
+ * and if that fails then only the port is matched
+ * @param port The port - ignored if non-positive - i.e., no match reported
+ * @param entries The {@link Collection} of {@link LocalForwardingEntry} to check - ignored if
+ * {@code null}/empty - i.e., no match reported
+ * @return The <U>first</U> entry whose local or bound address matches the host name - case
+ * <U>insensitive</U> <B>and</B> has a matching bound port - {@code null} if no match found
*/
public static LocalForwardingEntry findMatchingEntry(
- String host, int port, Collection<? extends LocalForwardingEntry> entries) {
- if (GenericUtils.isEmpty(host) || (port <= 0) || (GenericUtils.isEmpty(entries))) {
+ String host, boolean anyLocalAddress, int port, Collection<? extends LocalForwardingEntry> entries) {
+ if ((port <= 0) || (GenericUtils.isEmpty(entries))) {
return null;
}
+ if (GenericUtils.isEmpty(host) && (!anyLocalAddress)) {
+ return null;
+ }
+
+ LocalForwardingEntry candidate = null;
for (LocalForwardingEntry e : entries) {
- if ((port == e.getPort()) && (host.equalsIgnoreCase(e.getHostName()) || host.equalsIgnoreCase(e.getAlias()))) {
+ SshdSocketAddress bound = e.getBoundAddress();
+ /*
+ * Note we don't check the local port since it could be zero.
+ * If it isn't then it must be equal to the bound port (enforced in constructor)
+ */
+ if (port != bound.getPort()) {
+ continue;
+ }
+
+ /*
+ * We first try an exact match - if not found, declare this
+ * a candidate and return it if host is any local address
+ */
+
+ String boundName = bound.getHostName();
+ if (SshdSocketAddress.isEquivalentHostName(host, boundName, false)) {
+ return e;
+ }
+
+ SshdSocketAddress local = e.getLocalAddress();
+ String localName = local.getHostName();
+ if (SshdSocketAddress.isEquivalentHostName(host, localName, false)) {
return e;
}
+
+ if (SshdSocketAddress.isLoopbackAlias(host, boundName)
+ || SshdSocketAddress.isLoopbackAlias(host, localName)) {
+ return e;
+ }
+
+ if (anyLocalAddress) {
+ if (candidate != null) {
+ throw new IllegalStateException(
+ "Multiple candidate matches for " + host + "@" + port + ": " + candidate + ", " + e);
+ }
+ candidate = e;
+ }
+ }
+
+ if (anyLocalAddress) {
+ return candidate;
}
return null; // no match found
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/PortForwardingInformationProvider.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/PortForwardingInformationProvider.java
index 0d39182..c9c8217 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/PortForwardingInformationProvider.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/PortForwardingInformationProvider.java
@@ -23,6 +23,7 @@ import java.util.List;
import java.util.Map;
import java.util.NavigableSet;
+import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.net.SshdSocketAddress;
/**
@@ -30,33 +31,33 @@ import org.apache.sshd.common.util.net.SshdSocketAddress;
*/
public interface PortForwardingInformationProvider {
/**
- * @return A {@link NavigableSet} <u>snapshot</u> of the currently started local port forwards
+ * @return A {@link List} <u>snapshot</u> of the currently started local port forward bindings
*/
- NavigableSet<Integer> getStartedLocalPortForwards();
+ List<SshdSocketAddress> getStartedLocalPortForwards();
/**
* @param port The port number
- * @return The local bound {@link SshdSocketAddress} for the port - {@code null} if none bound
+ * @return The local bound {@link SshdSocketAddress}-es for the port
* @see #isLocalPortForwardingStartedForPort(int) isLocalPortForwardingStartedForPort
* @see #getStartedLocalPortForwards()
*/
- SshdSocketAddress getBoundLocalPortForward(int port);
+ List<SshdSocketAddress> getBoundLocalPortForwards(int port);
/**
- * @return A <u>snapshot</u> of the currently bound forwarded local ports as "pairs" of port + bound
- * {@link SshdSocketAddress}
+ * @return A <u>snapshot</u> of the currently bound forwarded local ports as "pairs" of local/remote
+ * {@link SshdSocketAddress}-es
*/
- List<Map.Entry<Integer, SshdSocketAddress>> getLocalForwardsBindings();
+ List<Map.Entry<SshdSocketAddress, SshdSocketAddress>> getLocalForwardsBindings();
/**
* Test if local port forwarding is started
*
* @param port The local port
* @return {@code true} if local port forwarding is started
- * @see #getBoundLocalPortForward(int) getBoundLocalPortForward
+ * @see #getBoundLocalPortForwards(int) getBoundLocalPortForwards
*/
default boolean isLocalPortForwardingStartedForPort(int port) {
- return getBoundLocalPortForward(port) != null;
+ return GenericUtils.isNotEmpty(getBoundLocalPortForwards(port));
}
/**
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/PortForwardingManager.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/PortForwardingManager.java
index 8cc647d..e71eabd 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/PortForwardingManager.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/PortForwardingManager.java
@@ -28,6 +28,18 @@ import org.apache.sshd.common.util.net.SshdSocketAddress;
*/
public interface PortForwardingManager extends PortForwardingInformationProvider {
/**
+ * Start forwarding the given local port on the client to the given address on the server.
+ *
+ * @param localPort The local port - if zero then one will be allocated
+ * @param remote The remote address
+ * @return The bound {@link SshdSocketAddress}
+ * @throws IOException If failed to create the requested binding
+ */
+ default SshdSocketAddress startLocalPortForwarding(int localPort, SshdSocketAddress remote) throws IOException {
+ return startLocalPortForwarding(new SshdSocketAddress(localPort), remote);
+ }
+
+ /**
* Start forwarding the given local address on the client to the given address on the server.
*
* @param local The local address
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java b/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
index 2282b9a..853581b 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/forward/TcpipClientChannel.java
@@ -105,7 +105,7 @@ public class TcpipClientChannel extends AbstractClientChannel implements Forward
public void updateLocalForwardingEntry(LocalForwardingEntry entry) {
Objects.requireNonNull(entry, "No local forwarding entry provided");
- localEntry = new SshdSocketAddress(entry.getAlias(), entry.getPort());
+ localEntry = entry.getBoundAddress();
}
@Override
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
index 6883451..c62719a 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/session/helpers/SessionHelper.java
@@ -1199,7 +1199,7 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
}
@Override
- public List<Map.Entry<Integer, SshdSocketAddress>> getLocalForwardsBindings() {
+ public List<Map.Entry<SshdSocketAddress, SshdSocketAddress>> getLocalForwardsBindings() {
Forwarder forwarder = getForwarder();
return (forwarder == null) ? Collections.emptyList() : forwarder.getLocalForwardsBindings();
}
@@ -1211,15 +1211,15 @@ public abstract class SessionHelper extends AbstractKexFactoryManager implements
}
@Override
- public NavigableSet<Integer> getStartedLocalPortForwards() {
+ public List<SshdSocketAddress> getStartedLocalPortForwards() {
Forwarder forwarder = getForwarder();
- return (forwarder == null) ? Collections.emptyNavigableSet() : forwarder.getStartedLocalPortForwards();
+ return (forwarder == null) ? Collections.emptyList() : forwarder.getStartedLocalPortForwards();
}
@Override
- public SshdSocketAddress getBoundLocalPortForward(int port) {
+ public List<SshdSocketAddress> getBoundLocalPortForwards(int port) {
Forwarder forwarder = getForwarder();
- return (forwarder == null) ? null : forwarder.getBoundLocalPortForward(port);
+ return (forwarder == null) ? Collections.emptyList() : forwarder.getBoundLocalPortForwards(port);
}
@Override
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/forward/LocalForwardingEntryCombinedBoundAddressTest.java b/sshd-core/src/test/java/org/apache/sshd/common/forward/LocalForwardingEntryCombinedBoundAddressTest.java
new file mode 100644
index 0000000..cc5e431
--- /dev/null
+++ b/sshd-core/src/test/java/org/apache/sshd/common/forward/LocalForwardingEntryCombinedBoundAddressTest.java
@@ -0,0 +1,121 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+package org.apache.sshd.common.forward;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.util.test.JUnit4ClassRunnerWithParametersFactory;
+import org.apache.sshd.util.test.JUnitTestSupport;
+import org.apache.sshd.util.test.NoIoTestCase;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.experimental.categories.Category;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized.UseParametersRunnerFactory;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+@UseParametersRunnerFactory(JUnit4ClassRunnerWithParametersFactory.class)
+@Category({ NoIoTestCase.class })
+public class LocalForwardingEntryCombinedBoundAddressTest extends JUnitTestSupport {
+ private final LocalForwardingEntry entry;
+ private final SshdSocketAddress expected;
+
+ public LocalForwardingEntryCombinedBoundAddressTest(
+ SshdSocketAddress local, SshdSocketAddress bound,
+ SshdSocketAddress expected) {
+ this.entry = new LocalForwardingEntry(local, bound);
+ this.expected = expected;
+ }
+
+ @Parameters(name = "local={0}, bound={1}, expected={2}")
+ public static List<Object[]> parameters() {
+ return new ArrayList<Object[]>() {
+ // Not serializing it
+ private static final long serialVersionUID = 1L;
+
+ {
+ SshdSocketAddress bound = new SshdSocketAddress("10.10.10.10", 7365);
+ addTestCase(bound, bound, bound);
+
+ SshdSocketAddress specificLocal = new SshdSocketAddress("specificLocal", bound.getPort());
+ addTestCase(specificLocal, bound, specificLocal);
+
+ SshdSocketAddress noLocalPort = new SshdSocketAddress(specificLocal.getHostName(), 0);
+ addTestCase(noLocalPort, bound, new SshdSocketAddress(specificLocal.getHostName(), bound.getPort()));
+
+ for (String address : new String[] {
+ "", SshdSocketAddress.IPV4_ANYADDR,
+ SshdSocketAddress.IPV6_LONG_ANY_ADDRESS,
+ SshdSocketAddress.IPV6_SHORT_ANY_ADDRESS
+ }) {
+ SshdSocketAddress wildcard = new SshdSocketAddress(address, bound.getPort());
+ addTestCase(wildcard, bound, bound);
+ }
+ }
+
+ private void addTestCase(
+ SshdSocketAddress local, SshdSocketAddress bound, SshdSocketAddress expected) {
+ add(new Object[] { local, bound, expected });
+ }
+ };
+ }
+
+ @Test
+ public void testResolvedValue() {
+ assertEquals(expected, entry.getCombinedBoundAddress());
+ }
+
+ @Test
+ public void testHashCode() {
+ assertEquals(expected.hashCode(), entry.hashCode());
+ }
+
+ @Test
+ public void testSameInstanceReuse() {
+ SshdSocketAddress combined = entry.getCombinedBoundAddress();
+ SshdSocketAddress local = entry.getLocalAddress();
+ SshdSocketAddress bound = entry.getBoundAddress();
+ boolean eqLocal = Objects.equals(combined, local);
+ boolean eqBound = Objects.equals(combined, bound);
+ if (eqLocal) {
+ assertSame("Not same local reference", combined, local);
+ } else if (eqBound) {
+ assertSame("Not same bound reference", combined, bound);
+ } else {
+ assertNotSame("Unexpected same local reference", combined, local);
+ assertNotSame("Unexpected same bound reference", combined, bound);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[entry=" + entry + ", expected=" + expected + "]";
+ }
+}
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/forward/LocalForwardingEntryTest.java b/sshd-core/src/test/java/org/apache/sshd/common/forward/LocalForwardingEntryTest.java
index d88ff82..a61441c 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/forward/LocalForwardingEntryTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/forward/LocalForwardingEntryTest.java
@@ -25,9 +25,12 @@ import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.IntStream;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.NoIoTestCase;
import org.junit.FixMethodOrder;
import org.junit.Test;
+import org.junit.experimental.categories.Category;
import org.junit.runners.MethodSorters;
/**
@@ -36,6 +39,7 @@ import org.junit.runners.MethodSorters;
* @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@Category({ NoIoTestCase.class })
public class LocalForwardingEntryTest extends BaseTestSupport {
public LocalForwardingEntryTest() {
super();
@@ -43,12 +47,16 @@ public class LocalForwardingEntryTest extends BaseTestSupport {
@Test // NOTE: this also checks indirectly SshSocketAddress host comparison case-insensitive
public void testCaseInsensitiveMatching() {
- LocalForwardingEntry expected = new LocalForwardingEntry(getClass().getSimpleName(), getCurrentTestName(), 7365);
- String hostname = expected.getHostName();
- String alias = expected.getAlias();
- int port = expected.getPort();
+ SshdSocketAddress local = new SshdSocketAddress(getClass().getSimpleName(), 0);
+ SshdSocketAddress bound = new SshdSocketAddress(getCurrentTestName(), 7365);
+ LocalForwardingEntry expected = new LocalForwardingEntry(local, bound);
+ String hostname = local.getHostName();
+ String alias = bound.getHostName();
+ int port = bound.getPort();
List<LocalForwardingEntry> entries = IntStream.rangeClosed(1, 4)
- .mapToObj(seed -> new LocalForwardingEntry(hostname + "-" + seed, alias + "-" + seed, port + seed))
+ .mapToObj(seed -> new LocalForwardingEntry(
+ new SshdSocketAddress(hostname + "-" + seed, 0),
+ new SshdSocketAddress(alias + "-" + seed, port + seed)))
.collect(Collectors.toCollection(ArrayList::new));
entries.add(expected);
@@ -63,4 +71,83 @@ public class LocalForwardingEntryTest extends BaseTestSupport {
}
}
}
+
+ @Test
+ public void testSingleWildcardMatching() {
+ SshdSocketAddress address = new SshdSocketAddress(getCurrentTestName(), 7365);
+ LocalForwardingEntry expected = new LocalForwardingEntry(address, address);
+ int port = address.getPort();
+ List<LocalForwardingEntry> entries = IntStream.rangeClosed(1, 4)
+ .mapToObj(seed -> {
+ String hostname = address.getHostName();
+ SshdSocketAddress other = new SshdSocketAddress(hostname + "-" + seed, port + seed);
+ return new LocalForwardingEntry(other, other);
+ }).collect(Collectors.toCollection(ArrayList::new));
+ entries.add(expected);
+
+ for (String host : new String[] {
+ SshdSocketAddress.IPV4_ANYADDR,
+ SshdSocketAddress.IPV6_LONG_ANY_ADDRESS,
+ SshdSocketAddress.IPV6_SHORT_ANY_ADDRESS
+ }) {
+ LocalForwardingEntry actual = LocalForwardingEntry.findMatchingEntry(host, port, entries);
+ assertSame("Host=" + host, expected, actual);
+ }
+ }
+
+ @Test
+ public void testLoopbackMatching() {
+ int port = 7365;
+ List<LocalForwardingEntry> entries = IntStream.rangeClosed(1, 4)
+ .mapToObj(seed -> {
+ String hostname = getCurrentTestName();
+ SshdSocketAddress other = new SshdSocketAddress(hostname + "-" + seed, port + seed);
+ return new LocalForwardingEntry(other, other);
+ }).collect(Collectors.toCollection(ArrayList::new));
+ int numEntries = entries.size();
+ for (String host : new String[] {
+ SshdSocketAddress.LOCALHOST_IPV4,
+ SshdSocketAddress.IPV6_LONG_LOCALHOST,
+ SshdSocketAddress.IPV6_SHORT_LOCALHOST
+ }) {
+ SshdSocketAddress bound = new SshdSocketAddress(host, port);
+ LocalForwardingEntry expected = new LocalForwardingEntry(bound, bound);
+ entries.add(expected);
+
+ LocalForwardingEntry actual
+ = LocalForwardingEntry.findMatchingEntry(SshdSocketAddress.LOCALHOST_NAME, port, entries);
+ entries.remove(numEntries);
+ assertSame("Host=" + host, expected, actual);
+ }
+ }
+
+ @Test
+ public void testMultipleWildcardCandidates() {
+ int port = 7365;
+ List<LocalForwardingEntry> entries = IntStream.rangeClosed(1, 4)
+ .mapToObj(seed -> {
+ String hostname = getCurrentTestName();
+ SshdSocketAddress other = new SshdSocketAddress(hostname + "-" + seed, port + seed);
+ return new LocalForwardingEntry(other, other);
+ }).collect(Collectors.toCollection(ArrayList::new));
+ for (int index = 0; index < 4; index++) {
+ SshdSocketAddress duplicate = new SshdSocketAddress(getClass().getSimpleName() + "-" + index, port);
+ entries.add(new LocalForwardingEntry(duplicate, duplicate));
+ }
+
+ for (String host : new String[] {
+ SshdSocketAddress.IPV4_ANYADDR,
+ SshdSocketAddress.IPV6_LONG_ANY_ADDRESS,
+ SshdSocketAddress.IPV6_SHORT_ANY_ADDRESS
+ }) {
+ try {
+ LocalForwardingEntry actual = LocalForwardingEntry.findMatchingEntry(host, port, entries);
+ fail("Unexpected success for host=" + host + ": " + actual);
+ } catch (IllegalStateException e) {
+ String msg = e.getMessage();
+ assertTrue("Bad exception message: " + msg,
+ msg.startsWith("Multiple candidate matches for " + host + "@" + port + ":"));
+ }
+ }
+ }
}
diff --git a/sshd-core/src/test/java/org/apache/sshd/common/forward/PortForwardingTest.java b/sshd-core/src/test/java/org/apache/sshd/common/forward/PortForwardingTest.java
index 51bcb80..08c7adf 100644
--- a/sshd-core/src/test/java/org/apache/sshd/common/forward/PortForwardingTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/common/forward/PortForwardingTest.java
@@ -18,18 +18,30 @@
*/
package org.apache.sshd.common.forward;
+import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
+import java.io.InputStreamReader;
import java.io.OutputStream;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
+import java.net.HttpURLConnection;
+import java.net.Inet4Address;
+import java.net.InetAddress;
import java.net.InetSocketAddress;
+import java.net.NetworkInterface;
+import java.net.Proxy;
import java.net.Socket;
+import java.net.SocketException;
+import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
+import java.util.ArrayList;
import java.util.Collection;
+import java.util.Enumeration;
import java.util.HashSet;
+import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
@@ -38,6 +50,7 @@ import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
+import java.util.stream.Collectors;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.JSchException;
@@ -55,6 +68,7 @@ import org.apache.sshd.common.session.ConnectionService;
import org.apache.sshd.common.util.GenericUtils;
import org.apache.sshd.common.util.MapEntryUtils.NavigableMapBuilder;
import org.apache.sshd.common.util.ProxyUtils;
+import org.apache.sshd.common.util.io.IoUtils;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.core.CoreModuleProperties;
import org.apache.sshd.server.SshServer;
@@ -66,6 +80,7 @@ import org.apache.sshd.util.test.CoreTestSupportUtils;
import org.apache.sshd.util.test.JSchLogger;
import org.apache.sshd.util.test.SimpleUserInfo;
import org.junit.AfterClass;
+import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.Test;
@@ -77,6 +92,7 @@ import org.slf4j.LoggerFactory;
* Port forwarding tests
*/
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+@SuppressWarnings("checkstyle:MethodCount")
public class PortForwardingTest extends BaseTestSupport {
public static final int SO_TIMEOUT = (int) TimeUnit.SECONDS.toMillis(13L);
@@ -198,7 +214,13 @@ public class PortForwardingTest extends BaseTestSupport {
@SuppressWarnings("synthetic-access")
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
- Object result = method.invoke(forwarder, args);
+ Object result;
+ try {
+ result = method.invoke(forwarder, args);
+ } catch (Throwable t) {
+ throw ProxyUtils.unwrapInvocationThrowable(t);
+ }
+
String name = method.getName();
String request = method2req.get(name);
if (GenericUtils.length(request) > 0) {
@@ -246,7 +268,14 @@ public class PortForwardingTest extends BaseTestSupport {
}
}
- private void waitForForwardingRequest(String expected, Duration timeout) throws InterruptedException {
+ @Before
+ public void setUp() {
+ if (!REQUESTS_QUEUE.isEmpty()) {
+ REQUESTS_QUEUE.clear();
+ }
+ }
+
+ private static void waitForForwardingRequest(String expected, Duration timeout) throws InterruptedException {
for (long remaining = timeout.toMillis(); remaining > 0L;) {
long waitStart = System.currentTimeMillis();
String actual = REQUESTS_QUEUE.poll(remaining, TimeUnit.MILLISECONDS);
@@ -622,6 +651,8 @@ public class PortForwardingTest extends BaseTestSupport {
byte[] buf = new byte[bytes.length + Long.SIZE];
int n = input.read(buf);
+ assertTrue("No data read from tunnel", n > 0);
+
String res = new String(buf, 0, n, StandardCharsets.UTF_8);
assertEquals("Mismatched data", expected, res);
} finally {
@@ -672,6 +703,8 @@ public class PortForwardingTest extends BaseTestSupport {
output.flush();
int n = input.read(buf);
+ assertTrue("No data read from tunnel", n > 0);
+
String res = new String(buf, 0, n, StandardCharsets.UTF_8);
assertEquals("Mismatched data at iteration #" + i, expected, res);
}
@@ -754,6 +787,61 @@ public class PortForwardingTest extends BaseTestSupport {
}
}
+ @Test // see SSHD-1066
+ public void testLocalBindingOnDifferentInterfaces() throws Exception {
+ InetSocketAddress addr = (InetSocketAddress) GenericUtils.head(sshd.getBoundAddresses());
+ log.info("{} - using bound address={}", getCurrentTestName(), addr);
+
+ List<String> allAddresses = getHostAddresses();
+ log.info("{} - test on addresses={}", getCurrentTestName(), allAddresses);
+
+ try (ClientSession session = createNativeSession(null)) {
+ List<ExplicitPortForwardingTracker> trackers = new ArrayList<>();
+ try {
+ for (String host : allAddresses) {
+ ExplicitPortForwardingTracker tracker = session.createLocalPortForwardingTracker(
+ new SshdSocketAddress(host, 8080),
+ new SshdSocketAddress("test.javastack.org", 80));
+ SshdSocketAddress boundAddress = tracker.getBoundAddress();
+ log.info("{} - test for binding={}", getCurrentTestName(), boundAddress);
+ testRemoteURL(new Proxy(Proxy.Type.HTTP, boundAddress.toInetSocketAddress()),
+ "http://test.javastack.org/");
+ trackers.add(tracker);
+ }
+ } finally {
+ IoUtils.closeQuietly(trackers);
+ }
+ }
+ }
+
+ private static List<String> getHostAddresses() throws SocketException {
+ List<String> addresses = new ArrayList<>();
+ Enumeration<NetworkInterface> eni = NetworkInterface.getNetworkInterfaces();
+ while (eni.hasMoreElements()) {
+ NetworkInterface networkInterface = eni.nextElement();
+ Enumeration<InetAddress> eia = networkInterface.getInetAddresses();
+ while (eia.hasMoreElements()) {
+ InetAddress ia = eia.nextElement();
+ if (ia instanceof Inet4Address) {
+ addresses.add(ia.getHostAddress());
+ }
+ }
+ }
+ return addresses;
+ }
+
+ private static void testRemoteURL(Proxy proxy, String url) throws IOException {
+ HttpURLConnection connection = (HttpURLConnection) new URL(url).openConnection(proxy);
+ connection.setConnectTimeout((int) DEFAULT_TIMEOUT.toMillis());
+ connection.setReadTimeout((int) DEFAULT_TIMEOUT.toMillis());
+ String result;
+ try (InputStream inputStream = connection.getInputStream();
+ BufferedReader in = new BufferedReader(new InputStreamReader(inputStream))) {
+ result = in.lines().collect(Collectors.joining(System.lineSeparator()));
+ }
+ assertEquals("Unexpected server response", "OK", result);
+ }
+
/**
* Close the socket inside this JSCH session. Use reflection to find it and just close it.
*
@@ -848,10 +936,6 @@ public class PortForwardingTest extends BaseTestSupport {
client.addPortForwardingEventListener(listener);
}
- ClientSession session
- = client.connect(getCurrentTestName(), TEST_LOCALHOST, sshPort).verify(CONNECT_TIMEOUT).getSession();
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(AUTH_TIMEOUT);
- return session;
+ return createAuthenticatedClientSession(client, sshPort);
}
}
diff --git a/sshd-core/src/test/java/org/apache/sshd/util/test/BaseTestSupport.java b/sshd-core/src/test/java/org/apache/sshd/util/test/BaseTestSupport.java
index 94765ef..5bc19cc 100644
--- a/sshd-core/src/test/java/org/apache/sshd/util/test/BaseTestSupport.java
+++ b/sshd-core/src/test/java/org/apache/sshd/util/test/BaseTestSupport.java
@@ -18,10 +18,12 @@
*/
package org.apache.sshd.util.test;
+import java.io.IOException;
import java.time.Duration;
import java.util.Collection;
import org.apache.sshd.client.SshClient;
+import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.helpers.AbstractFactoryManager;
import org.apache.sshd.common.io.BuiltinIoServiceFactoryFactories;
import org.apache.sshd.common.io.DefaultIoServiceFactoryFactory;
@@ -119,6 +121,28 @@ public abstract class BaseTestSupport extends JUnitTestSupport {
assumeNotIoServiceProvider(getCurrentTestName(), excluded);
}
+ protected ClientSession createClientSession(SshClient client, int port) throws IOException {
+ return client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
+ .verify(CONNECT_TIMEOUT)
+ .getSession();
+ }
+
+ protected ClientSession createAuthenticatedClientSession(SshClient client, int port) throws IOException {
+ ClientSession session = createClientSession(client, port);
+ try {
+ session.addPasswordIdentity(getCurrentTestName());
+ session.auth().verify(AUTH_TIMEOUT);
+
+ ClientSession authSession = session;
+ session = null; // avoid auto-close at finally clause
+ return authSession;
+ } finally {
+ if (session != null) {
+ session.close();
+ }
+ }
+ }
+
public static IoServiceFactoryFactory getIoServiceProvider() {
DefaultIoServiceFactoryFactory factory = DefaultIoServiceFactoryFactory.getDefaultIoServiceFactoryFactoryInstance();
return factory.getIoServiceProvider();
diff --git a/sshd-sftp/src/test/java/org/apache/sshd/sftp/client/AbstractSftpClientTestSupport.java b/sshd-sftp/src/test/java/org/apache/sshd/sftp/client/AbstractSftpClientTestSupport.java
index 4d7b14e..ed3dbad 100644
--- a/sshd-sftp/src/test/java/org/apache/sshd/sftp/client/AbstractSftpClientTestSupport.java
+++ b/sshd-sftp/src/test/java/org/apache/sshd/sftp/client/AbstractSftpClientTestSupport.java
@@ -87,26 +87,8 @@ public abstract class AbstractSftpClientTestSupport extends BaseTestSupport {
sshd.setFileSystemFactory(fileSystemFactory);
}
- protected ClientSession createClientSession() throws IOException {
- return client.connect(getCurrentTestName(), TEST_LOCALHOST, port)
- .verify(CONNECT_TIMEOUT)
- .getSession();
- }
-
protected ClientSession createAuthenticatedClientSession() throws IOException {
- ClientSession session = createClientSession();
- try {
- session.addPasswordIdentity(getCurrentTestName());
- session.auth().verify(AUTH_TIMEOUT);
-
- ClientSession authSession = session;
- session = null; // avoid auto-close at finally clause
- return authSession;
- } finally {
- if (session != null) {
- session.close();
- }
- }
+ return createAuthenticatedClientSession(client, port);
}
protected SftpClient createSingleSessionClient() throws IOException {