This is an automated email from the ASF dual-hosted git repository.

gnodet 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 204c0bc  [SSHD-1047] Support for SSH jumps (#154)
204c0bc is described below

commit 204c0bc48de5a32c43a3b201f8c739bb38f161f8
Author: Guillaume Nodet <gno...@gmail.com>
AuthorDate: Fri Jul 31 14:55:28 2020 +0200

    [SSHD-1047] Support for SSH jumps (#154)
---
 CHANGES.md                                         |   9 +-
 docs/proxies.md                                    |  22 ++
 .../org/apache/sshd/cli/client/ScpCommandMain.java |   2 +-
 .../apache/sshd/cli/client/SftpCommandMain.java    |   2 +-
 .../sshd/cli/client/SshClientCliSupport.java       |  33 ++-
 .../org/apache/sshd/cli/client/SshClientMain.java  |   2 +-
 .../apache/sshd/cli/client/ChannelExecMain.java    |   2 +-
 .../config/hosts/ConfigFileHostEntryResolver.java  |  22 +-
 .../hosts/DefaultConfigFileHostEntryResolver.java  |  11 +-
 .../sshd/client/config/hosts/HostConfigEntry.java  |  58 +++++-
 .../config/hosts/HostConfigEntryResolver.java      |   6 +-
 .../hosts/ConfigFileHostEntryResolverTest.java     |   8 +-
 .../java/org/apache/sshd/client/SshClient.java     | 226 +++++++++++++-------
 .../sshd/client/session/AbstractClientSession.java |   4 +
 .../sshd/client/session/ClientSessionCreator.java  |  14 ++
 .../simple/AbstractSimpleClientSessionCreator.java |  15 ++
 .../sshd/client/simple/SimpleSessionClient.java    |  20 ++
 .../java/org/apache/sshd/client/ProxyTest.java     | 228 ++++++++++++++++++---
 .../config/hosts/HostConfigEntryResolverTest.java  |   4 +-
 19 files changed, 550 insertions(+), 138 deletions(-)

diff --git a/CHANGES.md b/CHANGES.md
index 6477914..fc54a85 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -13,16 +13,17 @@
 ## Major code re-factoring
 
 * [SSHD-506](https://issues.apache.org/jira/browse/SSHD-506) Added support for 
AES-GCM ciphers.
+* [SSHD-1004](https://issues.apache.org/jira/browse/SSHD-1004) Deprecate DES, 
RC4 and Blowfish ciphers from default setup.
 * [SSHD-1034](https://issues.apache.org/jira/browse/SSHD-1034) Rename 
`org.apache.sshd.common.ForwardingFilter` to `Forwarder`.
 * [SSHD-1035](https://issues.apache.org/jira/browse/SSHD-1035) Move property 
definitions to common locations.
-* [SSHD-1038](https://issues.apache.org/jira/browse/SSHD-1035) Refactor 
packages from a module into a cleaner hierarchy.
-* [SSHD-1004](https://issues.apache.org/jira/browse/SSHD-1035) Deprecate DES, 
RC4 and Blowfish ciphers from default setup.
+* [SSHD-1038](https://issues.apache.org/jira/browse/SSHD-1038) Refactor 
packages from a module into a cleaner hierarchy.
+* [SSHD-1047](https://issues.apache.org/jira/browse/SSHD-1047) Support for SSH 
jumps.
 
 ## Minor code helpers
 
-* [SSHD-1040](https://issues.apache.org/jira/browse/SSHD-1040) Make server key 
available after KEX completed.
 * [SSHD-1030](https://issues.apache.org/jira/browse/SSHD-1030) Added a 
NoneFileSystemFactory implementation
-* [SSHD-1042](https://issues.apache.org/jira/browse/SSHD-1030) Added more 
callbacks to SftpEventListener
+* [SSHD-1042](https://issues.apache.org/jira/browse/SSHD-1042) Added more 
callbacks to SftpEventListener
+* [SSHD-1040](https://issues.apache.org/jira/browse/SSHD-1040) Make server key 
available after KEX completed.
 
 ## Behavioral changes and enhancements
 
diff --git a/docs/proxies.md b/docs/proxies.md
new file mode 100644
index 0000000..4047179
--- /dev/null
+++ b/docs/proxies.md
@@ -0,0 +1,22 @@
+# Proxies
+
+## SSH Jumps
+
+The SSH client can be configured to use SSH jumps.  A *jump host* (also known 
as a *jump server*) is an 
+intermediary host or an SSH gateway to a remote network, through which a 
connection can be made to another 
+host in a dissimilar security zone, for example a demilitarized zone (DMZ). It 
bridges two dissimilar 
+security zones and offers controlled access between them.
+
+Starting from SSHD 2.6.0, the *ProxyJump* host configuration entry is honored 
when using the `SshClient`
+to connect to a host.  The `SshClient` built by default reads the 
`~/.ssh/config` file. The various CLI clients
+also honor the `-J` command line option to specify one or more jumps.
+
+In order to manually configure jumps, you need to build a `HostConfigEntry` 
with a `proxyJump` and use it
+to connect to the server:
+```
+ConnectFuture future = client.connect(new HostConfigEntry(
+        "", host, port, user,
+        proxyUser + "@" + proxyHost + ":" + proxyPort));
+```
+
+The configuration options specified in the configuration file for the jump 
hosts are also honored. 
diff --git 
a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java 
b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
index a0a842b..a23eff5 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/ScpCommandMain.java
@@ -184,7 +184,7 @@ public class ScpCommandMain extends SshClientCliSupport {
             if (session == null) {
                 stderr.println("usage: scp [" + SCP_PORT_OPTION + " port] [-i 
identity] [-io nio2|mina|netty]"
                                + " [-v[v][v]] [-E logoutput] [-r] [-p] [-q] 
[-o option=value] [-o creator=class name]"
-                               + " [-c cipherlist] [-m maclist] [-w password] 
[-C] <source> <target>");
+                               + " [-c cipherlist] [-m maclist] [-J proxyJump] 
[-w password] [-C] <source> <target>");
                 stderr.println();
                 stderr.println("Where <source> or <target> are either 
'user@host:file' or a local file path");
                 stderr.println("NOTE: exactly ONE of the source or target must 
be remote and the other one local");
diff --git 
a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java 
b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
index a1501b9..399208f 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SftpCommandMain.java
@@ -329,7 +329,7 @@ public class SftpCommandMain extends SshClientCliSupport 
implements Channel {
                     : setupClientSession(SFTP_PORT_OPTION, stdin, level, 
stdout, stderr, args);
             if (session == null) {
                 System.err.println("usage: sftp [-v[v][v]] [-E logoutput] [-i 
identity] [-io nio2|mina|netty]"
-                                   + " [-l login] [" + SFTP_PORT_OPTION + " 
port] [-o option=value]"
+                                   + " [-J proxyJump] [-l login] [" + 
SFTP_PORT_OPTION + " port] [-o option=value]"
                                    + " [-w password] [-c cipherlist]  [-m 
maclist] [-C] hostname/user@host");
                 System.exit(-1);
                 return;
diff --git 
a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java 
b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
index 2eff66d..fca0403 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientCliSupport.java
@@ -51,6 +51,7 @@ import org.apache.sshd.client.SshClient;
 import org.apache.sshd.client.auth.keyboard.UserInteraction;
 import org.apache.sshd.client.config.SshClientConfigFileReader;
 import org.apache.sshd.client.config.hosts.HostConfigEntry;
+import org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
 import org.apache.sshd.client.config.keys.ClientIdentity;
 import org.apache.sshd.client.keyverifier.DefaultKnownHostsServerKeyVerifier;
 import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
@@ -79,6 +80,7 @@ import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.common.util.OsUtils;
 import org.apache.sshd.common.util.ValidateUtils;
 import org.apache.sshd.common.util.io.NoCloseOutputStream;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 import org.apache.sshd.common.util.threads.ThreadUtils;
 import org.apache.sshd.core.CoreModuleProperties;
 
@@ -106,10 +108,12 @@ public abstract class SshClientCliSupport extends 
CliSupport {
                 || "-w".equals(argName)
                 || "-c".equals(argName)
                 || "-m".equals(argName)
-                || "-E".equals(argName);
+                || "-E".equals(argName)
+                || "-J".equals(argName);
     }
 
     // NOTE: ClientSession#getFactoryManager is the SshClient
+    // CHECKSTYLE:OFF
     public static ClientSession setupClientSession(
             String portOption, BufferedReader stdin, Level level,
             PrintStream stdout, PrintStream stderr, String... args)
@@ -117,6 +121,7 @@ public abstract class SshClientCliSupport extends 
CliSupport {
         int port = -1;
         String host = null;
         String login = null;
+        String proxyJump = null;
         String password = null;
         boolean error = false;
         List<Path> identities = new ArrayList<>();
@@ -149,6 +154,12 @@ public abstract class SshClientCliSupport extends 
CliSupport {
                     error = showError(stderr, "Bad option value for " + 
argName + ": " + port);
                     break;
                 }
+            } else if ("-J".equals(argName)) {
+                if (proxyJump != null) {
+                    error = showError(stderr, argName + " option value 
re-specified: " + proxyJump);
+                    break;
+                }
+                proxyJump = argVal;
             } else if ("-w".equals(argName)) {
                 if (GenericUtils.length(password) > 0) {
                     error = showError(stderr, argName + " option value 
re-specified: " + password);
@@ -245,8 +256,9 @@ public abstract class SshClientCliSupport extends 
CliSupport {
                 port = SshConstants.DEFAULT_PORT;
             }
 
+            HostConfigEntry entry = resolveHost(client, login, host, port, 
proxyJump);
             // TODO use a configurable wait time
-            ClientSession session = client.connect(login, host, port)
+            ClientSession session = client.connect(entry, null, null)
                     .verify()
                     .getSession();
             try {
@@ -264,6 +276,23 @@ public abstract class SshClientCliSupport extends 
CliSupport {
             throw e;
         }
     }
+    // CHECKSTYLE:ON
+
+    public static HostConfigEntry resolveHost(SshClient client, String 
username, String host, int port, String proxyJump)
+            throws IOException {
+        HostConfigEntryResolver resolver = client.getHostConfigEntryResolver();
+        HostConfigEntry entry = resolver.resolveEffectiveHost(host, port, 
null, username, proxyJump, null);
+        if (entry == null) {
+            // IPv6 addresses have a format which means they need special 
treatment, separate from pattern validation
+            if (SshdSocketAddress.isIPv6Address(host)) {
+                // Not using a pattern as the host name passed in was a valid 
IPv6 address
+                entry = new HostConfigEntry("", host, port, username, null);
+            } else {
+                entry = new HostConfigEntry(host, host, port, username, 
proxyJump);
+            }
+        }
+        return entry;
+    }
 
     public static Path resolveIdentityFile(String id) throws IOException {
         BuiltinIdentities identity = BuiltinIdentities.fromName(id);
diff --git 
a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java 
b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java
index 75d28e2..7c1265e 100644
--- a/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java
+++ b/sshd-cli/src/main/java/org/apache/sshd/cli/client/SshClientMain.java
@@ -135,7 +135,7 @@ public class SshClientMain extends SshClientCliSupport {
 
             if (error) {
                 System.err.println("usage: ssh [-A|-a] [-v[v][v]] [-E 
logoutputfile] [-D socksPort]"
-                                   + " [-l login] [" + SSH_CLIENT_PORT_OPTION 
+ " port] [-o option=value]"
+                                   + " [-J proxyJump] [-l login] [" + 
SSH_CLIENT_PORT_OPTION + " port] [-o option=value]"
                                    + " [-w password] [-c cipherslist] [-m 
maclist] [-C]"
                                    + " hostname/user@host [command]");
                 System.exit(-1);
diff --git 
a/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java 
b/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java
index 93da3ba..45424d4 100644
--- a/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java
+++ b/sshd-cli/src/test/java/org/apache/sshd/cli/client/ChannelExecMain.java
@@ -82,7 +82,7 @@ public class ChannelExecMain extends BaseTestSupport {
                     CliSupport.resolveLoggingVerbosity(args), stdout, stderr, 
args);
             if (session == null) {
                 System.err.println("usage: channelExec [-i identity] [-l 
login] [-P port] [-o option=value]"
-                                   + " [-w password] [-c cipherlist]  [-m 
maclist] [-C] hostname/user@host");
+                                   + " [-J proxyJump] [-w password] [-c 
cipherlist]  [-m maclist] [-C] hostname/user@host");
                 System.exit(-1);
                 return;
             }
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
 
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
index 1159e50..0ee867c 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolver.java
@@ -52,21 +52,21 @@ public class ConfigFileHostEntryResolver extends 
ModifiableFileWatcher implement
 
     @Override
     public HostConfigEntry resolveEffectiveHost(
-            String host, int port, SocketAddress localAddress, String 
username, AttributeRepository context)
+            String host, int port, SocketAddress localAddress, String 
username, String proxyJump, AttributeRepository context)
             throws IOException {
         try {
             HostConfigEntryResolver delegate
-                    = Objects.requireNonNull(resolveEffectiveResolver(host, 
port, username), "No delegate");
-            HostConfigEntry entry = delegate.resolveEffectiveHost(host, port, 
localAddress, username, context);
+                    = Objects.requireNonNull(resolveEffectiveResolver(host, 
port, username, proxyJump), "No delegate");
+            HostConfigEntry entry = delegate.resolveEffectiveHost(host, port, 
localAddress, username, proxyJump, context);
             if (log.isDebugEnabled()) {
-                log.debug("resolveEffectiveHost({}@{}:{}) => {}", username, 
host, port, entry);
+                log.debug("resolveEffectiveHost({}@{}:{}/{}) => {}", username, 
host, port, proxyJump, entry);
             }
 
             return entry;
         } catch (Throwable e) {
             if (log.isDebugEnabled()) {
-                log.debug("resolveEffectiveHost({}@{}:{}) failed ({}) to 
resolve: {}",
-                        username, host, port, e.getClass().getSimpleName(), 
e.getMessage());
+                log.debug("resolveEffectiveHost({}@{}:{}/{}) failed ({}) to 
resolve: {}",
+                        username, host, port, proxyJump, 
e.getClass().getSimpleName(), e.getMessage());
             }
 
             if (log.isTraceEnabled()) {
@@ -80,18 +80,20 @@ public class ConfigFileHostEntryResolver extends 
ModifiableFileWatcher implement
         }
     }
 
-    protected HostConfigEntryResolver resolveEffectiveResolver(String host, 
int port, String username) throws IOException {
+    protected HostConfigEntryResolver resolveEffectiveResolver(String host, 
int port, String username, String proxyJump)
+            throws IOException {
         if (checkReloadRequired()) {
             delegateHolder.set(HostConfigEntryResolver.EMPTY); // start fresh
 
             Path path = getPath();
             if (exists()) {
-                Collection<HostConfigEntry> entries = 
reloadHostConfigEntries(path, host, port, username);
+                Collection<HostConfigEntry> entries = 
reloadHostConfigEntries(path, host, port, username, proxyJump);
                 if (GenericUtils.size(entries) > 0) {
                     
delegateHolder.set(HostConfigEntry.toHostConfigEntryResolver(entries));
                 }
             } else {
-                log.info("resolveEffectiveResolver({}@{}:{}) no configuration 
file at {}", username, host, port, path);
+                log.info("resolveEffectiveResolver({}@{}:{}/{}) no 
configuration file at {}", username, host, port, proxyJump,
+                        path);
             }
         }
 
@@ -99,7 +101,7 @@ public class ConfigFileHostEntryResolver extends 
ModifiableFileWatcher implement
     }
 
     protected List<HostConfigEntry> reloadHostConfigEntries(
-            Path path, String host, int port, String username)
+            Path path, String host, int port, String username, String 
proxyJump)
             throws IOException {
         List<HostConfigEntry> entries = 
HostConfigEntry.readHostConfigEntries(path);
         log.info("resolveEffectiveResolver({}@{}:{}) loaded {} entries from 
{}", username, host, port,
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
 
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
index 6b44026..010d28a 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/DefaultConfigFileHostEntryResolver.java
@@ -64,22 +64,23 @@ public class DefaultConfigFileHostEntryResolver extends 
ConfigFileHostEntryResol
     }
 
     @Override
-    protected List<HostConfigEntry> reloadHostConfigEntries(Path path, String 
host, int port, String username)
+    protected List<HostConfigEntry> reloadHostConfigEntries(Path path, String 
host, int port, String username, String proxyJump)
             throws IOException {
         if (isStrict()) {
             if (log.isDebugEnabled()) {
-                log.debug("reloadHostConfigEntries({}@{}:{}) check permissions 
of {}", username, host, port, path);
+                log.debug("reloadHostConfigEntries({}@{}:{}/{}) check 
permissions of {}", username, host, port, proxyJump,
+                        path);
             }
 
             Map.Entry<String, ?> violation = 
validateStrictConfigFilePermissions(path);
             if (violation != null) {
-                log.warn("reloadHostConfigEntries({}@{}:{}) invalid file={} 
permissions: {}",
-                        username, host, port, path, violation.getKey());
+                log.warn("reloadHostConfigEntries({}@{}:{}/{}) invalid file={} 
permissions: {}",
+                        username, host, port, proxyJump, path, 
violation.getKey());
                 updateReloadAttributes();
                 return Collections.emptyList();
             }
         }
 
-        return super.reloadHostConfigEntries(path, host, port, username);
+        return super.reloadHostConfigEntries(path, host, port, username, 
proxyJump);
     }
 }
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
 
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
index 6a5bb70..26f839b 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntry.java
@@ -77,6 +77,7 @@ public class HostConfigEntry extends HostPatternsHolder 
implements MutableUserHo
     public static final String HOST_NAME_CONFIG_PROP = "HostName";
     public static final String PORT_CONFIG_PROP = 
ConfigFileReaderSupport.PORT_CONFIG_PROP;
     public static final String USER_CONFIG_PROP = "User";
+    public static final String PROXY_JUMP_CONFIG_PROP = "ProxyJump";
     public static final String IDENTITY_FILE_CONFIG_PROP = "IdentityFile";
     /**
      * Use only the identities specified in the host entry (if any)
@@ -116,6 +117,7 @@ public class HostConfigEntry extends HostPatternsHolder 
implements MutableUserHo
     private String hostName;
     private int port;
     private String username;
+    private String proxyJump;
     private Boolean exclusiveIdentites;
     private Collection<String> identities = Collections.emptyList();
     private Map<String, String> properties = Collections.emptyMap();
@@ -125,10 +127,15 @@ public class HostConfigEntry extends HostPatternsHolder 
implements MutableUserHo
     }
 
     public HostConfigEntry(String pattern, String host, int port, String 
username) {
+        this(pattern, host, port, username, null);
+    }
+
+    public HostConfigEntry(String pattern, String host, int port, String 
username, String proxyJump) {
         setHost(pattern);
         setHostName(host);
         setPort(port);
         setUsername(username);
+        setProxyJump(proxyJump);
     }
 
     /**
@@ -211,6 +218,29 @@ public class HostConfigEntry extends HostPatternsHolder 
implements MutableUserHo
     }
 
     /**
+     * @return the host to use as a proxy
+     */
+    public String getProxyJump() {
+        return proxyJump;
+    }
+
+    public void setProxyJump(String proxyJump) {
+        this.proxyJump = proxyJump;
+    }
+
+    /**
+     * Resolves the effective proxyJump
+     *
+     * @param  originalProxyJump The original requested proxyJump
+     * @return                   If the configured host entry proxyJump is not 
{@code null}/empty then it is used,
+     *                           otherwise the original one.
+     * @see                      #resolveUsername(String)
+     */
+    public String resolveProxyJump(String originalProxyJump) {
+        return resolveProxyJump(originalProxyJump, getProxyJump());
+    }
+
+    /**
      * @return The current identities file paths - may be {@code null}/empty
      */
     public Collection<String> getIdentities() {
@@ -472,6 +502,11 @@ public class HostConfigEntry extends HostPatternsHolder 
implements MutableUserHo
             setIdentitiesOnly(
                     ConfigFileReaderSupport.parseBooleanValue(
                             ValidateUtils.checkNotNullAndNotEmpty(joinedValue, 
"No identities option value")));
+        } else if (PROXY_JUMP_CONFIG_PROP.equalsIgnoreCase(key)) {
+            String curValue = getProxyJump();
+            ValidateUtils.checkTrue(GenericUtils.isEmpty(curValue) || 
ignoreAlreadyInitialized, "Already initialized %s: %s",
+                    key, curValue);
+            setProxyJump(joinedValue);
         }
     }
 
@@ -659,7 +694,7 @@ public class HostConfigEntry extends HostPatternsHolder 
implements MutableUserHo
         if (GenericUtils.isEmpty(entries)) {
             return HostConfigEntryResolver.EMPTY;
         } else {
-            return (host1, port1, lclAddress, username1, ctx) -> {
+            return (host1, port1, lclAddress, username1, proxyJump1, ctx) -> {
                 List<HostConfigEntry> matches = findMatchingEntries(host1, 
entries);
                 int numMatches = GenericUtils.size(matches);
                 if (numMatches <= 0) {
@@ -672,7 +707,7 @@ public class HostConfigEntry extends HostPatternsHolder 
implements MutableUserHo
                             host1, port1, numMatches);
                 }
 
-                return normalizeEntry(match, host1, port1, username1);
+                return normalizeEntry(match, host1, port1, username1, 
proxyJump1);
             };
         }
     }
@@ -691,7 +726,7 @@ public class HostConfigEntry extends HostPatternsHolder 
implements MutableUserHo
      * @see                #resolveIdentityFilePath(String, String, int, 
String)
      */
     public static HostConfigEntry normalizeEntry(
-            HostConfigEntry entry, String host, int port, String username)
+            HostConfigEntry entry, String host, int port, String username, 
String proxyJump)
             throws IOException {
         if (entry == null) {
             return null;
@@ -702,6 +737,7 @@ public class HostConfigEntry extends HostPatternsHolder 
implements MutableUserHo
         normal.setHostName(entry.resolveHostName(host));
         normal.setPort(entry.resolvePort(port));
         normal.setUsername(entry.resolveUsername(username));
+        normal.setProxyJump(entry.resolveProxyJump(proxyJump));
 
         Map<String, String> props = entry.getProperties();
         if (GenericUtils.size(props) > 0) {
@@ -772,6 +808,22 @@ public class HostConfigEntry extends HostPatternsHolder 
implements MutableUserHo
         }
     }
 
+    /**
+     * Resolves the effective proxyJump
+     *
+     * @param  originalProxyJump The original requested proxyJump
+     * @param  entryProxyJump    The configured host entry proxyJump
+     * @return                   If the configured host entry proxyJump is not 
{@code null}/empty then it is used,
+     *                           otherwise the original one.
+     */
+    public static String resolveProxyJump(String originalProxyJump, String 
entryProxyJump) {
+        if (GenericUtils.isEmpty(entryProxyJump)) {
+            return originalProxyJump;
+        } else {
+            return entryProxyJump;
+        }
+    }
+
     public static List<HostConfigEntry> readHostConfigEntries(Path path, 
OpenOption... options) throws IOException {
         try (InputStream input = Files.newInputStream(path, options)) {
             return readHostConfigEntries(input, true);
diff --git 
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
 
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
index d27c053..cc0766a 100644
--- 
a/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
+++ 
b/sshd-common/src/main/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolver.java
@@ -36,7 +36,8 @@ public interface HostConfigEntryResolver {
     HostConfigEntryResolver EMPTY = new HostConfigEntryResolver() {
         @Override
         public HostConfigEntry resolveEffectiveHost(
-                String host, int port, SocketAddress localAddress, String 
username, AttributeRepository context)
+                String host, int port, SocketAddress localAddress, String 
username, String proxyJump,
+                AttributeRepository context)
                 throws IOException {
             return null;
         }
@@ -54,6 +55,7 @@ public interface HostConfigEntryResolver {
      * @param  port         The requested port
      * @param  localAddress Optional binding endpoint for the local peer
      * @param  username     The requested username
+     * @param  proxyJump    The requested proxyJump
      * @param  context      An optional &quot;context&quot; provided during 
the connection request (to be attached to
      *                      the established session if successfully connected)
      * @return              A {@link HostConfigEntry} for the actual target - 
{@code null} if use original parameters.
@@ -63,6 +65,6 @@ public interface HostConfigEntryResolver {
      * @throws IOException  If failed to resolve the configuration
      */
     HostConfigEntry resolveEffectiveHost(
-            String host, int port, SocketAddress localAddress, String 
username, AttributeRepository context)
+            String host, int port, SocketAddress localAddress, String 
username, String proxyJump, AttributeRepository context)
             throws IOException;
 }
diff --git 
a/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
 
b/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
index 4a9a070..54085ca 100644
--- 
a/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
+++ 
b/sshd-common/src/test/java/org/apache/sshd/client/config/hosts/ConfigFileHostEntryResolverTest.java
@@ -53,10 +53,11 @@ public class ConfigFileHostEntryResolverTest extends 
JUnitTestSupport {
         ConfigFileHostEntryResolver resolver = new ConfigFileHostEntryResolver(
                 
assertHierarchyTargetFolderExists(dir).resolve(getCurrentTestName() + 
".config.txt")) {
             @Override
-            protected List<HostConfigEntry> reloadHostConfigEntries(Path path, 
String host, int port, String username)
+            protected List<HostConfigEntry> reloadHostConfigEntries(
+                    Path path, String host, int port, String username, String 
proxyJump)
                     throws IOException {
                 reloadCount.incrementAndGet();
-                return super.reloadHostConfigEntries(path, host, port, 
username);
+                return super.reloadHostConfigEntries(path, host, port, 
username, proxyJump);
             }
         };
         Path path = resolver.getPath();
@@ -116,7 +117,8 @@ public class ConfigFileHostEntryResolverTest extends 
JUnitTestSupport {
 
         for (int index = 1; index < Byte.SIZE; index++) {
             HostConfigEntry actual
-                    = resolver.resolveEffectiveHost(query.getHostName(), 
query.getPort(), null, query.getUsername(), null);
+                    = resolver.resolveEffectiveHost(query.getHostName(), 
query.getPort(), null, query.getUsername(), null,
+                            null);
 
             if (entries == null) {
                 assertEquals(phase + "[" + index + "]: mismatched reload 
count", 0, reloadCount.get());
diff --git a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java 
b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
index 8633268..049edca 100644
--- a/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
+++ b/sshd-core/src/main/java/org/apache/sshd/client/SshClient.java
@@ -22,6 +22,8 @@ import java.io.IOException;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
 import java.net.SocketTimeoutException;
+import java.net.URI;
+import java.nio.channels.UnsupportedAddressTypeException;
 import java.nio.file.LinkOption;
 import java.nio.file.Path;
 import java.nio.file.Paths;
@@ -52,6 +54,7 @@ import 
org.apache.sshd.client.config.hosts.HostConfigEntryResolver;
 import org.apache.sshd.client.config.keys.ClientIdentity;
 import org.apache.sshd.client.config.keys.ClientIdentityLoader;
 import org.apache.sshd.client.config.keys.DefaultClientIdentitiesWatcher;
+import org.apache.sshd.client.future.AuthFuture;
 import org.apache.sshd.client.future.ConnectFuture;
 import org.apache.sshd.client.future.DefaultConnectFuture;
 import org.apache.sshd.client.keyverifier.ServerKeyVerifier;
@@ -62,6 +65,7 @@ import org.apache.sshd.client.session.ClientSession;
 import org.apache.sshd.client.session.ClientSessionCreator;
 import org.apache.sshd.client.session.ClientUserAuthServiceFactory;
 import org.apache.sshd.client.session.SessionFactory;
+import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
 import org.apache.sshd.client.simple.AbstractSimpleClientSessionCreator;
 import org.apache.sshd.client.simple.SimpleClient;
 import org.apache.sshd.common.AttributeRepository;
@@ -466,100 +470,120 @@ public class SshClient extends AbstractFactoryManager 
implements ClientFactoryMa
     }
 
     @Override
-    public ConnectFuture connect(
-            String username, String host, int port, AttributeRepository 
context, SocketAddress localAddress)
-            throws IOException {
-        HostConfigEntryResolver resolver = getHostConfigEntryResolver();
-        HostConfigEntry entry = resolver.resolveEffectiveHost(host, port, 
localAddress, username, context);
-        if (entry == null) {
-            // generate a synthetic entry
-            if (log.isDebugEnabled()) {
-                log.debug("connect({}@{}:{}) no overrides", username, host, 
port);
-            }
-
-            // IPv6 addresses have a format which means they need special 
treatment, separate from pattern validation
-            if (SshdSocketAddress.isIPv6Address(host)) {
-                // Not using a pattern as the host name passed in was a valid 
IPv6 address
-                entry = new HostConfigEntry("", host, port, username);
-            } else {
-                entry = new HostConfigEntry(host, host, port, username);
-            }
-        } else {
-            if (log.isDebugEnabled()) {
-                log.debug("connect({}@{}:{}) effective: {}", username, host, 
port, entry);
-            }
+    public ConnectFuture connect(String uriStr) throws IOException {
+        Objects.requireNonNull(uriStr, "No uri address");
+        URI uri = URI.create(uriStr.contains("//") ? uriStr : "ssh://" + 
uriStr);
+        if (GenericUtils.isNotEmpty(uri.getScheme()) && 
!"ssh".equals(uri.getScheme())) {
+            throw new IllegalArgumentException("Unsupported scheme for uri: " 
+ uri);
         }
-
-        return connect(entry, context, localAddress);
+        String host = uri.getHost();
+        int port = uri.getPort();
+        String userInfo = uri.getUserInfo();
+        return connect(userInfo, host, port);
     }
 
     @Override
     public ConnectFuture connect(
-            String username, SocketAddress targetAddress, AttributeRepository 
context, SocketAddress localAddress)
+            String username, SocketAddress targetAddress,
+            AttributeRepository context, SocketAddress localAddress)
             throws IOException {
         Objects.requireNonNull(targetAddress, "No target address");
-        if (targetAddress instanceof InetSocketAddress) {
-            InetSocketAddress inetAddress = (InetSocketAddress) targetAddress;
-            String host = 
ValidateUtils.checkNotNullAndNotEmpty(inetAddress.getHostString(), "No host");
-            int port = inetAddress.getPort();
-            ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port);
-
-            HostConfigEntryResolver resolver = getHostConfigEntryResolver();
-            HostConfigEntry entry = resolver.resolveEffectiveHost(host, port, 
localAddress, username, context);
-            if (entry == null) {
-                if (log.isDebugEnabled()) {
-                    log.debug("connect({}@{}:{}) no overrides", username, 
host, port);
-                }
-
-                return doConnect(
-                        username, targetAddress, context, localAddress, 
KeyIdentityProvider.EMPTY_KEYS_PROVIDER, true);
-            } else {
-                if (log.isDebugEnabled()) {
-                    log.debug("connect({}@{}:{}) effective: {}", username, 
host, port, entry);
-                }
-
-                return connect(entry, context, localAddress);
-            }
-        } else {
-            if (log.isDebugEnabled()) {
-                log.debug("connect({}@{}) not an InetSocketAddress: {}",
-                        username, targetAddress, 
targetAddress.getClass().getName());
-            }
-            return doConnect(
-                    username, targetAddress, context, localAddress, 
KeyIdentityProvider.EMPTY_KEYS_PROVIDER, true);
+        if (!(targetAddress instanceof InetSocketAddress)) {
+            throw new UnsupportedAddressTypeException();
         }
+        InetSocketAddress inetAddress = (InetSocketAddress) targetAddress;
+        String host = 
ValidateUtils.checkNotNullAndNotEmpty(inetAddress.getHostString(), "No host");
+        int port = inetAddress.getPort();
+        ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port);
+        return connect(username, host, port, context, localAddress);
+    }
+
+    @Override
+    public ConnectFuture connect(
+            String username, String host, int port,
+            AttributeRepository context, SocketAddress localAddress)
+            throws IOException {
+        HostConfigEntry entry = resolveHost(username, host, port, context, 
localAddress);
+        return connect(entry, context, localAddress);
     }
 
     @Override
     public ConnectFuture connect(
             HostConfigEntry hostConfig, AttributeRepository context, 
SocketAddress localAddress)
             throws IOException {
+        List<HostConfigEntry> jumps = 
parseProxyJumps(hostConfig.getProxyJump(), context);
+        return doConnect(hostConfig, jumps, context, localAddress);
+    }
+
+    protected ConnectFuture doConnect(
+            HostConfigEntry hostConfig, List<HostConfigEntry> jumps,
+            AttributeRepository context, SocketAddress localAddress)
+            throws IOException {
         Objects.requireNonNull(hostConfig, "No host configuration");
         String host = 
ValidateUtils.checkNotNullAndNotEmpty(hostConfig.getHostName(), "No target 
host");
         int port = hostConfig.getPort();
         ValidateUtils.checkTrue(port > 0, "Invalid port: %d", port);
-
         Collection<String> hostIds = hostConfig.getIdentities();
-        Collection<PathResource> idFiles = GenericUtils.isEmpty(hostIds)
-                ? Collections.emptyList()
-                : hostIds.stream()
-                        .map(Paths::get)
-                        .map(PathResource::new)
-                        .collect(Collectors.toCollection(() -> new 
ArrayList<>(hostIds.size())));
+        Collection<PathResource> idFiles = GenericUtils.stream(hostIds)
+                .map(Paths::get)
+                .map(PathResource::new)
+                .collect(Collectors.toCollection(() -> new 
ArrayList<>(hostIds.size())));
         KeyIdentityProvider keys = preloadClientIdentities(idFiles);
-        return doConnect(hostConfig.getUsername(), new InetSocketAddress(host, 
port),
-                context, localAddress, keys, !hostConfig.isIdentitiesOnly());
-    }
-
-    protected KeyIdentityProvider preloadClientIdentities(
-            Collection<? extends NamedResource> locations)
-            throws IOException {
-        return GenericUtils.isEmpty(locations)
-                ? KeyIdentityProvider.EMPTY_KEYS_PROVIDER
-                : ClientIdentityLoader.asKeyIdentityProvider(
-                        Objects.requireNonNull(getClientIdentityLoader(), "No 
ClientIdentityLoader"),
-                        locations, getFilePasswordProvider(),
-                        
CoreModuleProperties.IGNORE_INVALID_IDENTITIES.getRequired(this));
+        String username = hostConfig.getUsername();
+        InetSocketAddress targetAddress = new 
InetSocketAddress(hostConfig.getHostName(), hostConfig.getPort());
+        if (GenericUtils.isNotEmpty(jumps)) {
+            ConnectFuture connectFuture = new DefaultConnectFuture(username + 
"@" + targetAddress, null);
+            HostConfigEntry jump = jumps.remove(0);
+            ConnectFuture f1 = doConnect(jump, jumps, context, null);
+            f1.addListener(f2 -> {
+                if (f2.isConnected()) {
+                    ClientSession proxySession = f2.getClientSession();
+                    try {
+                        AuthFuture auth = proxySession.auth();
+                        auth.addListener(f3 -> {
+                            if (f3.isSuccess()) {
+                                try {
+                                    SshdSocketAddress address
+                                            = new 
SshdSocketAddress(hostConfig.getHostName(), hostConfig.getPort());
+                                    ExplicitPortForwardingTracker tracker = 
proxySession
+                                            
.createLocalPortForwardingTracker(SshdSocketAddress.LOCALHOST_ADDRESS, address);
+                                    SshdSocketAddress bound = 
tracker.getBoundAddress();
+                                    ConnectFuture f4 = 
doConnect(hostConfig.getUsername(), bound.toInetSocketAddress(),
+                                            context, localAddress, keys, 
!hostConfig.isIdentitiesOnly());
+                                    f4.addListener(f5 -> {
+                                        if (f5.isConnected()) {
+                                            ClientSession clientSession = 
f5.getClientSession();
+                                            
clientSession.setAttribute(TARGET_SERVER, address);
+                                            
connectFuture.setSession(clientSession);
+                                            
proxySession.addCloseFutureListener(f6 -> clientSession.close(true));
+                                            
clientSession.addCloseFutureListener(f6 -> proxySession.close(true));
+                                        } else {
+                                            proxySession.close(true);
+                                            
connectFuture.setException(f5.getException());
+                                        }
+                                    });
+                                } catch (IOException e) {
+                                    proxySession.close(true);
+                                    connectFuture.setException(e);
+                                }
+                            } else {
+                                proxySession.close(true);
+                                connectFuture.setException(f3.getException());
+                            }
+                        });
+                    } catch (IOException e) {
+                        proxySession.close(true);
+                        connectFuture.setException(e);
+                    }
+                } else {
+                    connectFuture.setException(f2.getException());
+                }
+            });
+            return connectFuture;
+        } else {
+            return doConnect(hostConfig.getUsername(), new 
InetSocketAddress(host, port),
+                    context, localAddress, keys, 
!hostConfig.isIdentitiesOnly());
+        }
     }
 
     protected ConnectFuture doConnect(
@@ -580,6 +604,60 @@ public class SshClient extends AbstractFactoryManager 
implements ClientFactoryMa
         return connectFuture;
     }
 
+    protected List<HostConfigEntry> parseProxyJumps(String proxyJump, 
AttributeRepository context) throws IOException {
+        List<HostConfigEntry> jumps = new ArrayList<>();
+        for (String jump : GenericUtils.split(proxyJump, ',')) {
+            String j = jump.trim();
+            URI uri = URI.create(j.contains("//") ? j : "ssh://" + j);
+            if (GenericUtils.isNotEmpty(uri.getScheme()) && 
!"ssh".equals(uri.getScheme())) {
+                throw new IllegalArgumentException("Unsupported scheme for 
proxy jump: " + jump);
+            }
+            String host = uri.getHost();
+            int port = uri.getPort();
+            String userInfo = uri.getUserInfo();
+            HostConfigEntry entry = resolveHost(userInfo, host, port, context, 
null);
+            jumps.add(entry);
+        }
+        return jumps;
+    }
+
+    protected HostConfigEntry resolveHost(
+            String username, String host, int port, AttributeRepository 
context, SocketAddress localAddress)
+            throws IOException {
+        HostConfigEntryResolver resolver = getHostConfigEntryResolver();
+        HostConfigEntry entry = resolver.resolveEffectiveHost(host, port, 
localAddress, username, null, context);
+        if (entry == null) {
+            // generate a synthetic entry
+            if (log.isDebugEnabled()) {
+                log.debug("connect({}@{}:{}) no overrides", username, host, 
port);
+            }
+
+            // IPv6 addresses have a format which means they need special 
treatment, separate from pattern validation
+            if (SshdSocketAddress.isIPv6Address(host)) {
+                // Not using a pattern as the host name passed in was a valid 
IPv6 address
+                entry = new HostConfigEntry("", host, port, username, null);
+            } else {
+                entry = new HostConfigEntry(host, host, port, username, null);
+            }
+        } else {
+            if (log.isDebugEnabled()) {
+                log.debug("connect({}@{}:{}) effective: {}", username, host, 
port, entry);
+            }
+        }
+        return entry;
+    }
+
+    protected KeyIdentityProvider preloadClientIdentities(
+            Collection<? extends NamedResource> locations)
+            throws IOException {
+        return GenericUtils.isEmpty(locations)
+                ? KeyIdentityProvider.EMPTY_KEYS_PROVIDER
+                : ClientIdentityLoader.asKeyIdentityProvider(
+                        Objects.requireNonNull(getClientIdentityLoader(), "No 
ClientIdentityLoader"),
+                        locations, getFilePasswordProvider(),
+                        
CoreModuleProperties.IGNORE_INVALID_IDENTITIES.getRequired(this));
+    }
+
     protected SshFutureListener<IoConnectFuture> 
createConnectCompletionListener(
             ConnectFuture connectFuture, String username, SocketAddress 
address,
             KeyIdentityProvider identities, boolean useDefaultIdentities) {
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 5421ee6..805df8b 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
@@ -560,6 +560,10 @@ public abstract class AbstractClientSession extends 
AbstractSession implements C
         IoSession networkSession = getIoSession();
         SocketAddress remoteAddress = networkSession.getRemoteAddress();
         PublicKey serverKey = Objects.requireNonNull(getServerKey(), "No 
server key to verify");
+        SshdSocketAddress targetServerAddress = 
getAttribute(ClientSessionCreator.TARGET_SERVER);
+        if (targetServerAddress != null) {
+            remoteAddress = targetServerAddress.toInetSocketAddress();
+        }
 
         boolean verified = false;
         if (serverKey instanceof OpenSshCertificate) {
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionCreator.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionCreator.java
index 6f13584..d0e3c7e 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionCreator.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/session/ClientSessionCreator.java
@@ -24,11 +24,25 @@ import java.net.SocketAddress;
 import org.apache.sshd.client.config.hosts.HostConfigEntry;
 import org.apache.sshd.client.future.ConnectFuture;
 import org.apache.sshd.common.AttributeRepository;
+import org.apache.sshd.common.util.net.SshdSocketAddress;
 
 /**
  * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
  */
 public interface ClientSessionCreator {
+
+    AttributeRepository.AttributeKey<SshdSocketAddress> TARGET_SERVER = new 
AttributeRepository.AttributeKey<>();
+
+    /**
+     * Resolves the <U>effective</U> {@link HostConfigEntry} and connects to it
+     *
+     * @param  uri         The server uri to connect to
+     * @return             A {@link ConnectFuture}
+     * @throws IOException If failed to resolve the effective target or 
connect to it
+     * @see                #connect(HostConfigEntry)
+     */
+    ConnectFuture connect(String uri) throws IOException;
+
     /**
      * Resolves the <U>effective</U> {@link HostConfigEntry} and connects to it
      *
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClientSessionCreator.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClientSessionCreator.java
index a5045aa..e32bf28 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClientSessionCreator.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/simple/AbstractSimpleClientSessionCreator.java
@@ -82,6 +82,16 @@ public abstract class AbstractSimpleClientSessionCreator 
extends AbstractSimpleC
         return loginSession(connect(username, target), identity);
     }
 
+    @Override
+    public ClientSession sessionLogin(String uri, String password) throws 
IOException {
+        return loginSession(connect(uri), password);
+    }
+
+    @Override
+    public ClientSession sessionLogin(String uri, KeyPair identity) throws 
IOException {
+        return loginSession(connect(uri), identity);
+    }
+
     protected ClientSession loginSession(ConnectFuture future, String 
password) throws IOException {
         return authSession(future.verify(getConnectTimeout()), password);
     }
@@ -143,6 +153,11 @@ public abstract class AbstractSimpleClientSessionCreator 
extends AbstractSimpleC
         Objects.requireNonNull(channel, "No channel");
         return new AbstractSimpleClientSessionCreator() {
             @Override
+            public ConnectFuture connect(String uri) throws IOException {
+                return creator.connect(uri);
+            }
+
+            @Override
             public ConnectFuture connect(String username, String host, int 
port) throws IOException {
                 return creator.connect(username, host, port);
             }
diff --git 
a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleSessionClient.java
 
b/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleSessionClient.java
index a83fbaa..658abe1 100644
--- 
a/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleSessionClient.java
+++ 
b/sshd-core/src/main/java/org/apache/sshd/client/simple/SimpleSessionClient.java
@@ -167,4 +167,24 @@ public interface SimpleSessionClient extends 
SimpleClientConfigurator, Channel {
      * @throws IOException If failed to login or authenticate
      */
     ClientSession sessionLogin(SocketAddress target, String username, KeyPair 
identity) throws IOException;
+
+    /**
+     * Creates a session and logs in using the provided credentials
+     *
+     * @param  uri         The target uri
+     * @param  password    Password
+     * @return             Created {@link ClientSession}
+     * @throws IOException If failed to login or authenticate
+     */
+    ClientSession sessionLogin(String uri, String password) throws IOException;
+
+    /**
+     * Creates a session and logs in using the provided credentials
+     *
+     * @param  uri         The target uri
+     * @param  identity    The {@link KeyPair} identity
+     * @return             Created {@link ClientSession}
+     * @throws IOException If failed to login or authenticate
+     */
+    ClientSession sessionLogin(String uri, KeyPair identity) throws 
IOException;
 }
diff --git a/sshd-core/src/test/java/org/apache/sshd/client/ProxyTest.java 
b/sshd-core/src/test/java/org/apache/sshd/client/ProxyTest.java
index 1500729..2a8297b 100644
--- a/sshd-core/src/test/java/org/apache/sshd/client/ProxyTest.java
+++ b/sshd-core/src/test/java/org/apache/sshd/client/ProxyTest.java
@@ -18,21 +18,38 @@
  */
 package org.apache.sshd.client;
 
+import java.io.BufferedWriter;
 import java.io.ByteArrayOutputStream;
+import java.io.File;
+import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.StandardOpenOption;
+import java.rmi.RemoteException;
+import java.security.KeyPair;
+import java.util.Arrays;
+import java.util.concurrent.TimeUnit;
 
+import org.apache.sshd.client.config.hosts.HostConfigEntry;
+import org.apache.sshd.client.config.hosts.KnownHostHashValue;
+import org.apache.sshd.client.keyverifier.KnownHostsServerKeyVerifier;
+import org.apache.sshd.client.keyverifier.RejectAllServerKeyVerifier;
 import org.apache.sshd.client.session.ClientSession;
-import org.apache.sshd.client.session.forward.ExplicitPortForwardingTracker;
-import org.apache.sshd.common.util.io.IoUtils;
-import org.apache.sshd.common.util.net.SshdSocketAddress;
+import org.apache.sshd.common.config.keys.PublicKeyEntry;
+import org.apache.sshd.common.util.GenericUtils;
 import org.apache.sshd.server.SshServer;
 import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
 import org.apache.sshd.util.test.BaseTestSupport;
 import org.apache.sshd.util.test.CommandExecutionHelper;
 import org.junit.FixMethodOrder;
+import org.junit.Ignore;
+import org.junit.Rule;
 import org.junit.Test;
+import org.junit.rules.TemporaryFolder;
 import org.junit.runners.MethodSorters;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * @author <a href="mailto:d...@mina.apache.org";>Apache MINA SSHD Project</a>
@@ -40,6 +57,11 @@ import org.junit.runners.MethodSorters;
 @FixMethodOrder(MethodSorters.NAME_ASCENDING)
 public class ProxyTest extends BaseTestSupport {
 
+    @Rule
+    public TemporaryFolder tmpClientDir = new TemporaryFolder();
+
+    protected final Logger logger = LoggerFactory.getLogger(getClass());
+
     private ClientSession proxySession;
 
     public ProxyTest() {
@@ -69,45 +91,193 @@ public class ProxyTest extends BaseTestSupport {
             // setup client
             client.start();
 
-            String command = "ls -la";
-            String result;
+            logger.info("Proxy: " + proxy.getPort() + ", server: " + 
server.getPort());
+
+            // Connect through to the proxy.
+            client.addPasswordIdentity("user2");
             try (ClientSession session = createSession(
                     client,
                     "localhost", server.getPort(), "user1", "user1",
-                    "localhost", proxy.getPort(), "user2", "user2")) {
-                ByteArrayOutputStream out = new ByteArrayOutputStream();
-                ByteArrayOutputStream errors = new ByteArrayOutputStream();
-                session.executeRemoteCommand(command, out, errors, 
StandardCharsets.UTF_8);
-                result = out.toString();
+                    "user2@localhost:" + proxy.getPort())) {
+                assertTrue(session.isOpen());
+                doTestCommand(session, "ls -al");
+            }
+            assertTrue(proxySession == null || proxySession.isClosing() || 
proxySession.isClosed());
+        }
+    }
+
+    @Test
+    public void testDirectWithHostKeyVerification() throws Exception {
+        // This test exists only to show that the knownhosts setup is correct
+        try (SshServer server = setupTestServer();
+             SshServer proxy = setupTestServer();
+             SshClient client = setupTestClient()) {
+
+            File knownHosts = prepareHostKeySetup(server, proxy);
+            // Setup client with a standard ServerKeyVerifier
+            client.setServerKeyVerifier(
+                    new 
KnownHostsServerKeyVerifier(RejectAllServerKeyVerifier.INSTANCE, 
knownHosts.toPath()));
+            client.start();
+
+            logger.info("Proxy: " + proxy.getPort() + ", server: " + 
server.getPort());
+
+            // Connect to the server directly to verify the knownhosts setup.
+            try (ClientSession session = createSession(
+                    client, "localhost", server.getPort(), "user1", "user1", 
null)) {
+                assertTrue(session.isOpen());
+                doTestCommand(session, "ls -al");
+            }
+            assertTrue(proxySession == null || proxySession.isClosing() || 
proxySession.isClosed());
+
+            // Connect through to the proxy.
+            try (ClientSession session = createSession(
+                    client, "localhost", proxy.getPort(), "user2", "user2", 
null)) {
+                assertTrue(session.isOpen());
+                assertThrows(RemoteException.class,
+                        () -> doTestCommand(session, "ls -al"));
+            }
+            assertTrue(proxySession == null || proxySession.isClosing() || 
proxySession.isClosed());
+        }
+    }
+
+    @Test
+    public void testProxyWithHostKeyVerification() throws Exception {
+        try (SshServer server = setupTestServer();
+             SshServer proxy = setupTestServer();
+             SshClient client = setupTestClient()) {
+
+            File knownHosts = prepareHostKeySetup(server, proxy);
+            // Setup client with a standard ServerKeyVerifier
+            client.setServerKeyVerifier(
+                    new 
KnownHostsServerKeyVerifier(RejectAllServerKeyVerifier.INSTANCE, 
knownHosts.toPath()));
+            client.start();
+
+            logger.info("Proxy: " + proxy.getPort() + ", server: " + 
server.getPort());
+
+            // Connect via the proxy
+            client.addPasswordIdentity("user2");
+            try (ClientSession session = createSession(
+                    client, "localhost", server.getPort(), "user1", "user1",
+                    "user2@localhost:" + proxy.getPort())) {
+
+                assertTrue(session.isOpen());
+                doTestCommand(session, "ls -la");
             }
-            assertEquals(command, result);
             // make sure the proxy session is closed / closing
             assertTrue(proxySession == null || proxySession.isClosing() || 
proxySession.isClosed());
         }
     }
 
+    @Test
+    public void testProxyWithHostKeyVerificationAndCustomConfig() throws 
Exception {
+        try (SshServer server = setupTestServer();
+             SshServer proxy = setupTestServer();
+             SshClient client = setupTestClient()) {
+
+            File knownHosts = prepareHostKeySetup(server, proxy);
+            // Setup client with a standard ServerKeyVerifier
+            client.setServerKeyVerifier(
+                    new 
KnownHostsServerKeyVerifier(RejectAllServerKeyVerifier.INSTANCE, 
knownHosts.toPath()));
+            client.start();
+            
client.setHostConfigEntryResolver(HostConfigEntry.toHostConfigEntryResolver(Arrays.asList(
+                    new HostConfigEntry("server", "localhost", 
server.getPort(), "user1", "proxy"),
+                    new HostConfigEntry("proxy", "localhost", proxy.getPort(), 
"user2"))));
+
+            logger.info("Proxy: " + proxy.getPort() + ", server: " + 
server.getPort());
+
+            // Connect via the proxy
+            client.addPasswordIdentity("user1");
+            client.addPasswordIdentity("user2");
+            try (ClientSession session = client.connect("server")
+                    .verify(CONNECT_TIMEOUT).getSession()) {
+                session.auth().verify(AUTH_TIMEOUT);
+
+                assertTrue(session.isOpen());
+                doTestCommand(session, "ls -la");
+            }
+            // make sure the proxy session is closed / closing
+            assertTrue(proxySession == null || proxySession.isClosing() || 
proxySession.isClosed());
+        }
+    }
+
+    @Test
+    @Ignore
+    public void testExternal() throws Exception {
+        try (SshServer server = setupTestServer();
+             SshServer proxy = setupTestServer()) {
+
+            server.setCommandFactory((session, command) -> new 
CommandExecutionHelper(command) {
+                @Override
+                protected boolean handleCommandLine(String command) throws 
Exception {
+                    OutputStream stdout = getOutputStream();
+                    stdout.write(command.getBytes(StandardCharsets.US_ASCII));
+                    stdout.flush();
+                    return false;
+                }
+            });
+            server.start();
+            // setup proxy with a forwarding filter to allow the local port 
forwarding
+            proxy.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
+            proxy.start();
+
+            logger.info("Proxy: " + proxy.getPort() + ", server: " + 
server.getPort());
+            Thread.sleep(TimeUnit.MINUTES.toMillis(5));
+        }
+    }
+
+    protected void doTestCommand(ClientSession session, String command) throws 
IOException {
+        String result;
+        ByteArrayOutputStream out = new ByteArrayOutputStream();
+        ByteArrayOutputStream errors = new ByteArrayOutputStream();
+        session.executeRemoteCommand(command, out, errors, 
StandardCharsets.UTF_8);
+        result = out.toString();
+        assertEquals(command, result);
+    }
+
+    protected File prepareHostKeySetup(SshServer server, SshServer proxy) 
throws Exception {
+        // setup server with an echo command
+        server.setCommandFactory((session, command) -> new 
CommandExecutionHelper(command) {
+            @Override
+            protected boolean handleCommandLine(String command) throws 
Exception {
+                OutputStream stdout = getOutputStream();
+                stdout.write(command.getBytes(StandardCharsets.US_ASCII));
+                stdout.flush();
+                return true;
+            }
+        });
+
+        server.start();
+        File knownHosts = tmpClientDir.newFile("knownhosts");
+        writeKnownHosts(server, knownHosts);
+        // setup proxy with a forwarding filter to allow the local port 
forwarding
+        proxy.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
+        proxy.start();
+        writeKnownHosts(proxy, knownHosts);
+        return knownHosts;
+    }
+
+    protected File writeKnownHosts(SshServer server, File knownHosts) throws 
Exception {
+        KeyPair serverHostKey = 
GenericUtils.head(server.getKeyPairProvider().loadKeys(null));
+        try (BufferedWriter writer = 
Files.newBufferedWriter(knownHosts.toPath(), StandardCharsets.US_ASCII,
+                StandardOpenOption.CREATE, StandardOpenOption.WRITE, 
StandardOpenOption.APPEND)) {
+            KnownHostHashValue.appendHostPattern(writer, "localhost", 
server.getPort());
+            writer.append(' ');
+            PublicKeyEntry.appendPublicKeyEntry(writer, 
serverHostKey.getPublic());
+            writer.append('\n');
+        }
+        return knownHosts;
+    }
+
     @SuppressWarnings("checkstyle:ParameterNumber")
     protected ClientSession createSession(
             SshClient client,
             String host, int port, String user, String password,
-            String proxyHost, int proxyPort, String proxyUser, String 
proxyPassword)
-            throws java.io.IOException {
-        ClientSession session;
-        if (proxyHost != null) {
-            proxySession = client.connect(proxyUser, proxyHost, proxyPort)
-                    .verify(CONNECT_TIMEOUT).getSession();
-            proxySession.addPasswordIdentity(proxyPassword);
-            proxySession.auth().verify(AUTH_TIMEOUT);
-            SshdSocketAddress address = new SshdSocketAddress(host, port);
-            ExplicitPortForwardingTracker tracker = 
proxySession.createLocalPortForwardingTracker(
-                    SshdSocketAddress.LOCALHOST_ADDRESS, address);
-            SshdSocketAddress bound = tracker.getBoundAddress();
-            session = client.connect(user, bound.getHostName(), 
bound.getPort())
-                    .verify(CONNECT_TIMEOUT).getSession();
-            session.addCloseFutureListener(f -> IoUtils.closeQuietly(tracker, 
proxySession));
-        } else {
-            session = client.connect(user, host, 
port).verify(CONNECT_TIMEOUT).getSession();
-        }
+            String proxyJump)
+            throws IOException {
+        ClientSession session = client.connect(new HostConfigEntry(
+                "", host, port, user,
+                proxyJump))
+                .verify(CONNECT_TIMEOUT).getSession();
         session.addPasswordIdentity(password);
         session.auth().verify(AUTH_TIMEOUT);
         return session;
diff --git 
a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
 
b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
index 495ba82..c45f8e0 100644
--- 
a/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
+++ 
b/sshd-core/src/test/java/org/apache/sshd/client/config/hosts/HostConfigEntryResolverTest.java
@@ -92,7 +92,7 @@ public class HostConfigEntryResolverTest extends 
BaseTestSupport {
     @Test
     public void testEffectiveHostConfigResolution() throws Exception {
         HostConfigEntry entry = new HostConfigEntry(getCurrentTestName(), 
TEST_LOCALHOST, port, getCurrentTestName());
-        client.setHostConfigEntryResolver((host, portValue, lclAddress, 
username, context) -> entry);
+        client.setHostConfigEntryResolver((host, portValue, lclAddress, 
username, proxy, context) -> entry);
         client.start();
 
         try (ClientSession session = client.connect(
@@ -169,7 +169,7 @@ public class HostConfigEntryResolverTest extends 
BaseTestSupport {
         String host = getClass().getSimpleName();
         HostConfigEntry entry = new HostConfigEntry(host, TEST_LOCALHOST, 
port, user);
         entry.addIdentity(clientIdentity);
-        client.setHostConfigEntryResolver((host1, portValue, lclAddress, 
username, context) -> entry);
+        client.setHostConfigEntryResolver((host1, portValue, lclAddress, 
username, proxy, context) -> entry);
 
         client.start();
         try (ClientSession session = client.connect(

Reply via email to