Java SSH Tunnel with Dynamic Port Forwarding

If you have ever worked with deployment teams, you have done SSH through some clients like putty to your server and check whether a particular service reachable from there or verify something else.

I stuck with a problem where my production team has blocked access to production URLs of individual nodes. Now I cannot check if a particular node in my cluster is working or not. After a lot of efforts, they gave us a bastion server (a server machine that is allowed with special rights, more) that allows us access to these nodes.

Basically, my application URL is www.myurl.com that runs 10 nodes on 8080 port. Then I am allowed to access www.myurl.com URL over internet/VPN but not allowed to check individual nodes like www.node1.myurl.com:8080 etc. Now I have to use the new bastion server and log in through SSH then use dynamic port forward on that server. After that use SOCKS proxy of my browser to access these URLs.

Although, I am able to access these URLs from my browser but what about my JAVA utility that was doing all the verification without me doing all the manual work? So juggled around couple of libraries to see if I can still use that utitily and do all this.

There are couple of libraries but I explored two of them

JSCH (Doesn’t support dynamic port forward as of now)
Apache MINA
JSCH Example
Dependencies

<dependency>
  <groupId>com.jcraft</groupId>
  <artifactId>jsch</artifactId>
  <version>0.1.55</version>
</dependency>

Example

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URL;
import com.jcraft.jsch.JSch;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.UserInfo;
public class TunnelJSCH {
  public static void main(String[] args) {
    TunnelJSCH t = new TunnelJSCH();
    try {
      t.go();
    } catch (Exception ex) {
      ex.printStackTrace();
    }
  }
  public void go() throws Exception {
    String host = "bastion-server-host";
    String user = "user";
    String password = "password";
    int port = 22;
    int tunnelLocalPort = 8000;
    String tunnelRemoteHost = "remote-host";
    int tunnelRemotePort = 9080;
    JSch jsch = new JSch();
    Session session = jsch.getSession(user, host, port);
    session.setPassword(password);
    localUserInfo lui = new localUserInfo();
    session.setUserInfo(lui);
    System.out.println("Creating connection to server:" + host);
    session.connect();
    System.out.println("Connected to server, initializing Port forwarding ");
    session.setPortForwardingL(tunnelLocalPort, tunnelRemoteHost, tunnelRemotePort);
    
    System.out.println("Port forward successful forwarding: localhost:" + tunnelLocalPort + " -> "
        + tunnelRemoteHost + ":" + tunnelRemotePort);
    try {
      URL url = new URL("http://localhost:8000");
      System.out.println("Reading data from URL: " + url);
      BufferedReader in = new BufferedReader(new InputStreamReader(url.openStream()));
      System.out.println("================== Data From URL ==================\n");
      String inputLine;
      while ((inputLine = in.readLine()) != null)
        System.out.println(inputLine);
      in.close();
      System.out.println("================== Data From URL ==================\n");
    } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
    if (session != null && session.isConnected()) {
      System.out.println("Closing SSH Connection");
      session.disconnect();
    }
  }
  class localUserInfo implements UserInfo {
    String passwd;
    public String getPassword() {
      return passwd;
    }
    public boolean promptYesNo(String str) {
      return true;
    }
    public String getPassphrase() {
      return null;
    }
    public boolean promptPassphrase(String message) {
      return true;
    }
    public boolean promptPassword(String message) {
      return true;
    }
    public void showMessage(String message) {
    }
  }
}

Apache MINA
Dependencies

<!-- https://mvnrepository.com/artifact/org.apache.mina/mina-core -->
    <dependency>
      <groupId>org.apache.mina</groupId>
      <artifactId>mina-core</artifactId>
      <version>3.0.0-M2</version>
    </dependency>
    <!-- https://mvnrepository.com/artifact/org.apache.sshd/sshd-core -->
    <dependency>
      <groupId>org.apache.sshd</groupId>
      <artifactId>sshd-core</artifactId>
      <version>2.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.sshd</groupId>
      <artifactId>sshd-putty</artifactId>
      <version>2.1.0</version>
    </dependency>
    <dependency>
      <groupId>org.apache.sshd</groupId>
      <artifactId>sshd-common</artifactId>
      <version>2.1.0</version>
    </dependency>

Example

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.Proxy;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.file.Paths;
import java.security.GeneralSecurityException;
import java.security.KeyPair;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import org.apache.sshd.client.SshClient;
import org.apache.sshd.client.auth.hostbased.HostKeyIdentityProvider;
import org.apache.sshd.client.keyverifier.AcceptAllServerKeyVerifier;
import org.apache.sshd.client.session.ClientSession;
import org.apache.sshd.common.NamedFactory;
import org.apache.sshd.common.config.keys.loader.pem.PEMResourceParserUtils;
import org.apache.sshd.common.config.keys.loader.putty.PuttyKeyUtils;
import org.apache.sshd.common.forward.PortForwardingEventListener;
import org.apache.sshd.common.session.Session;
import org.apache.sshd.common.util.net.SshdSocketAddress;
import org.apache.sshd.server.channel.PuttyRequestHandler;
import org.apache.sshd.server.forward.AcceptAllForwardingFilter;
import org.apache.sshd.server.keyprovider.SimpleGeneratorHostKeyProvider;
/**
 * This class
 * 
 * @author Ankit Katiyar
 *
 */
public class AmazonTest {
  private static String BASTION_SERVER_PASSWORD = "[email protected]";
  private static final String BASTION_SERVER_USER = "ec2-user";
  private static final String BASTION_SEREVR_HOST = "ec2-18-191-207-91.us-east-2.compute.amazonaws.com";
  private static final String URL_TO_ACCESS = "http://www.google.com";
  public static void main(String[] args) {
    try {
      
      
      Collection<KeyPair> keys = null;
      // OPtional loading keys from a PEM file
      //keys=PEMResourceParserUtils.getPEMResourceParserByAlgorithm("RSA").loadKeyPairs(ClassLoader.getSystemResource("local-ps-test.pem").toURI().toURL(), null);
      
      // Optional: Using Putty key for login 
       keys=PuttyKeyUtils.DEFAULT_INSTANCE.loadKeyPairs(ClassLoader.getSystemResource("local-ps-private-key.ppk").toURI().toURL(), null);
       
      SshClient client = SshClient.setUpDefaultClient();
      client.setForwardingFilter(AcceptAllForwardingFilter.INSTANCE);
      client.setServerKeyVerifier(AcceptAllServerKeyVerifier.INSTANCE);
      client.start();
      // using the client for multiple sessions...
      try (ClientSession session = client.connect(BASTION_SERVER_USER, BASTION_SEREVR_HOST, 22).verify()
          .getSession()) {
        // IF you use password to login provide here
        // session.addPasswordIdentity(BASTION_SERVER_PASSWORD); // for password-based
        // authentication
        
        session.addPublicKeyIdentity(keys.iterator().next());
        // authentication
        // Note: can add BOTH password AND public key identities - depends on the
        // client/server security setup
        session.auth().verify(10000);
        // start using the session to run commands, do SCP/SFTP, create local/remote
        // port forwarding, etc...
        session.addPortForwardingEventListener(new PortForwardingEventListener() {
          @Override
          public void establishedDynamicTunnel(Session session, SshdSocketAddress local,
              SshdSocketAddress boundAddress, Throwable reason) throws IOException {
            // TODO Auto-generated method stub
            PortForwardingEventListener.super.establishedDynamicTunnel(session, local, boundAddress, reason);
            System.out.println("Dynamic Forword Tunnel is Ready");
          }
        });
        SshdSocketAddress sshdSocketAddress = session
            .startDynamicPortForwarding(new SshdSocketAddress("localhost", 8000));
        System.out.println("Host: " + sshdSocketAddress.getHostName());
        System.out.println("Port: " + sshdSocketAddress.getPort());
        // Create a Proxy object to work with
        Proxy proxy = new Proxy(Proxy.Type.SOCKS,
            new InetSocketAddress(sshdSocketAddress.getHostName(), sshdSocketAddress.getPort()));
        /**
         * Now you can use this proxy instance into any URL until this SSH session is active. 
         */
        
        // TEST one URL
        HttpURLConnection connection = (HttpURLConnection) new URL(URL_TO_ACCESS).openConnection(proxy);
        System.out.println("Proxy work:" + connection.getURL());
        BufferedReader in = new BufferedReader(new InputStreamReader(connection.getInputStream()));
        System.out.println("================== Data From URL ==================\n");
        String inputLine;
        while ((inputLine = in.readLine()) != null)
          System.out.println(inputLine);
        in.close();
        System.out.println("================== Data From URL ==================\n");
      } catch (IOException e1) {
        // TODO Auto-generated catch block
        e1.printStackTrace();
      } catch (Exception e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
      }
    } catch (Exception e) {
      // TODO Auto-generated catch block
      e.printStackTrace();
    }
  }
}

View complete project at GitHub

Ref: https://github.com/ankitkatiyar91/java-framework-examples/tree/master/java-tunneling

package jsch.java_tunneling;

import javax.swing.JOptionPane;

import com.jcraft.jsch.*;

public class OpenSSHConfig {
	  public static void main(String[] arg){

	    try{
	      JSch jsch=new JSch();

	      String host=null;
	      if(arg.length>0){
	        host=arg[0];
	      }
	      else{
	        host=JOptionPane.showInputDialog("Enter [email protected]",
	                                         System.getProperty("user.name")+
	                                         "@localhost");
	      }
	      String user=host.substring(0, host.indexOf('@'));
	      host=host.substring(host.indexOf('@')+1);

	      String config =
	        "Port 22\n"+
	        "\n"+
	        "Host foo\n"+
	        "  User "+user+"\n"+
	        "  Hostname "+host+"\n"+
	        "Host *\n"+
	        "  ConnectTime 30000\n"+
	        "  PreferredAuthentications keyboard-interactive,password,publickey\n"+
	        "  #ForwardAgent yes\n"+
	        "  #StrictHostKeyChecking no\n"+
	        "  #IdentityFile ~/.ssh/id_rsa\n"+
	        "  #UserKnownHostsFile ~/.ssh/known_hosts";

	      System.out.println("Generated configurations:");
	      System.out.println(config);

	      ConfigRepository configRepository =
	        com.jcraft.jsch.OpenSSHConfig.parse(config);
	        //com.jcraft.jsch.OpenSSHConfig.parseFile("~/.ssh/config");

	      jsch.setConfigRepository(configRepository);

	      // "foo" is from "Host foo" in the above config.
	      Session session=jsch.getSession("foo");

	      String passwd = JOptionPane.showInputDialog("Enter password");
	      session.setPassword(passwd);

	      UserInfo ui = new MyUserInfo(){
	        public void showMessage(String message){
	          JOptionPane.showMessageDialog(null, message);
	        }
	        public boolean promptYesNo(String message){
	          Object[] options={ "yes", "no" };
	          int foo=JOptionPane.showOptionDialog(null,
	                                               message,
	                                               "Warning",
	                                               JOptionPane.DEFAULT_OPTION,
	                                               JOptionPane.WARNING_MESSAGE,
	                                               null, options, options[0]);
	          return foo==0;
	        }

	        // If password is not given before the invocation of Session#connect(),
	        // implement also following methods,
	        //   * UserInfo#getPassword(),
	        //   * UserInfo#promptPassword(String message) and
	        //   * UIKeyboardInteractive#promptKeyboardInteractive()

	      };

	      session.setUserInfo(ui);

	      session.connect(); // making a connection with timeout as defined above.

	      Channel channel=session.openChannel("shell");

	      channel.setInputStream(System.in);
	      /*
	      // a hack for MS-DOS prompt on Windows.
	      channel.setInputStream(new FilterInputStream(System.in){
	          public int read(byte[] b, int off, int len)throws IOException{
	            return in.read(b, off, (len>1024?1024:len));
	          }
	        });
	       */

	      channel.setOutputStream(System.out);

	      /*
	      // Choose the pty-type "vt102".
	      ((ChannelShell)channel).setPtyType("vt102");
	      */

	      /*
	      // Set environment variable "LANG" as "ja_JP.eucJP".
	      ((ChannelShell)channel).setEnv("LANG", "ja_JP.eucJP");
	      */

	      //channel.connect();
	      channel.connect(3*1000);
	    }
	    catch(Exception e){
	      System.out.println(e);
	    }
	  }

	  public static abstract class MyUserInfo
	                          implements UserInfo, UIKeyboardInteractive{
	    public String getPassword(){ return null; }
	    public boolean promptYesNo(String str){ return false; }
	    public String getPassphrase(){ return null; }
	    public boolean promptPassphrase(String message){ return false; }
	    public boolean promptPassword(String message){ return false; }
	    public void showMessage(String message){ }
	    public String[] promptKeyboardInteractive(String destination,
	                                              String name,
	                                              String instruction,
	                                              String[] prompt,
	                                              boolean[] echo){
	      return null;
	    }
	  }
	}

Refer: http://www.jcraft.com/jsch/examples/OpenSSHConfig.java.html

package ssh.ssh;


import java.awt.*;
import javax.swing.*;

import com.jcraft.jsch.*;
import com.jcraft.jsch.UserInfo;


public class OpenSSHConfig {
	  private static final String STRICT_HOST_KEY_CHECKING_KEY = "StrictHostKeyChecking";
	  private static final String STRICT_HOST_KEY_CHECKING_VALUE = "no";
	  
	  public static void main(String[] arg){

	    try{
			  OpenSSHConfig t = new OpenSSHConfig();
			  t.go();
	    }
	    catch(Exception e){
	      System.out.println(e);
	    }
	  }
	  
	  public void go() throws Exception {
		  JSch jsch=new JSch();

		  int tunnelLocalPort = 8000;
		  String tunnelRemoteHost = "172.31.83.16";
		  int tunnelRemotePort = 22;

	      ConfigRepository configRepository =
	        com.jcraft.jsch.OpenSSHConfig.parseFile("~/.ssh/config");

	      jsch.setConfigRepository(configRepository);
	      jsch.addIdentity("~/.ssh/nhannvt.rsa");
	      jsch.setConfig(STRICT_HOST_KEY_CHECKING_KEY, STRICT_HOST_KEY_CHECKING_VALUE);


	      
	      Session session=jsch.getSession("hdev-bastion");	      

	      MyUserInfo ui = new MyUserInfo(); 

	      session.setUserInfo(ui);
	      System.out.println("Before....");

	      session.connect(); // making a connection with timeout as defined above.
	      System.out.println("Connect....");
		  session.setPortForwardingL(tunnelLocalPort, tunnelRemoteHost, tunnelRemotePort);
	      System.out.println("setPortForwardingL....");
	      session = jsch.getSession("nhannvt", "127.0.0.1", tunnelLocalPort);

	      session.setHostKeyAlias(tunnelRemoteHost);
	      session.connect();
	      System.out.println("Connect Forward....");
	      Channel channel = session.openChannel("sftp");
          channel.setInputStream(System.in);
          channel.setOutputStream(System.out);
          channel.connect();
          System.out.println("shell channel connected....");

          ChannelSftp c = (ChannelSftp) channel;

          String fileName = "/home/nhannvt/test.txt";
          c.put(fileName, "/home/nhannvt/");
          c.exit();
          System.out.println("done");
	  }
	  
	
	  public static class MyUserInfo implements UserInfo, UIKeyboardInteractive{
		    public String getPassword(){ return null; }
		    public boolean promptYesNo(String str){
		      Object[] options={ "yes", "no" };
		      int foo=JOptionPane.showOptionDialog(null, 
		             str,
		             "Warning", 
		             JOptionPane.DEFAULT_OPTION, 
		             JOptionPane.WARNING_MESSAGE,
		             null, options, options[0]);
		       return foo==0;
		    }
		  
		    String passphrase;
		    JTextField passphraseField=(JTextField)new JPasswordField(20);

		    public String getPassphrase(){ return passphrase; }
		    public boolean promptPassphrase(String message){
		      Object[] ob={passphraseField};
		      int result=
			JOptionPane.showConfirmDialog(null, ob, message,
						      JOptionPane.OK_CANCEL_OPTION);
		      if(result==JOptionPane.OK_OPTION){
		        passphrase=passphraseField.getText();
		        return true;
		      }
		      else{ return false; }
		    }
		    public boolean promptPassword(String message){ return true; }
		    public void showMessage(String message){
		      JOptionPane.showMessageDialog(null, message);
		    }
		    final GridBagConstraints gbc = 
		      new GridBagConstraints(0,0,1,1,1,1,
		                             GridBagConstraints.NORTHWEST,
		                             GridBagConstraints.NONE,
		                             new Insets(0,0,0,0),0,0);
		    private Container panel;
		    public String[] promptKeyboardInteractive(String destination,
		                                              String name,
		                                              String instruction,
		                                              String[] prompt,
		                                              boolean[] echo){
		      panel = new JPanel();
		      panel.setLayout(new GridBagLayout());

		      gbc.weightx = 1.0;
		      gbc.gridwidth = GridBagConstraints.REMAINDER;
		      gbc.gridx = 0;
		      panel.add(new JLabel(instruction), gbc);
		      gbc.gridy++;

		      gbc.gridwidth = GridBagConstraints.RELATIVE;

		      JTextField[] texts=new JTextField[prompt.length];
		      for(int i=0; i<prompt.length; i++){
		        gbc.fill = GridBagConstraints.NONE;
		        gbc.gridx = 0;
		        gbc.weightx = 1;
		        panel.add(new JLabel(prompt[i]),gbc);

		        gbc.gridx = 1;
		        gbc.fill = GridBagConstraints.HORIZONTAL;
		        gbc.weighty = 1;
		        if(echo[i]){
		          texts[i]=new JTextField(20);
		        }
		        else{
		          texts[i]=new JPasswordField(20);
		        }
		        panel.add(texts[i], gbc);
		        gbc.gridy++;
		      }

		      if(JOptionPane.showConfirmDialog(null, panel, 
		                                       destination+": "+name,
		                                       JOptionPane.OK_CANCEL_OPTION,
		                                       JOptionPane.QUESTION_MESSAGE)
		         ==JOptionPane.OK_OPTION){
		        String[] response=new String[prompt.length];
		        for(int i=0; i<prompt.length; i++){
		          response[i]=texts[i].getText();
		        }
			return response;
		      }
		      else{
		        return null;  // cancel
		      }
		    }
		  }

		
}