使用JSch实现ssh隧道建立

前言:

本篇文章记录我近期研究的问题:如何利用java实现堡垒机与内部机器建立隧道问题。

问题情景描述:

在生产环境中的集群往往在一个局域网中,而该局域网只能通过某台特定的堡垒机来访问。

即:为了更加安全,所以线上的服务器都无法直接访问,它必须通过一台堡垒机来访问。示意如下:

使用JSch实现ssh隧道建立_第1张图片
利用堡垒机建立隧道链接.png

用户想直接访问内部服务器,但是这些内部服务器并没有外网。 而堡垒机和这些服务器在一个局域网中,堡垒机可以和内部服务器通信,同时堡垒机拥有外网,可以直接被用户访问到。那么我们便可以先由ssh到堡垒机,然后再ssh到内部服务器才能够访问。这样做自然可以减少攻击,但是每次要到内部机器上去执行命令,都需要经历2次ssh,对线上的调试与监控效率影响非常大。下面就来全面介绍一下用java如何来解决该问题。

通过ProxyCommand+Netcat

一、前提条件 流程介绍
  1. 本机、跳板机、目标机器(内部服务器)三者都需要已经做过公钥认证。
  • tip:如果不做秘钥认证就会提示分别输入跳板机和目标机器的密码,需要输入两次密码,非常繁琐。而且要利用java做交互式命令输入。这个问题最终我找到解决的办法,不需要手动进行交互式命令输入,Jsch中UIKeyboardInteractive 能够进行赋值,解决这个问题。

  • 利用java做公钥认证的方式,暂时我没有解决掉。我改为上面的 利用命令做交互式赋值来解决的。

  1. linux服务器安装Netcat
  • 以CentOS Linux 为例:yum install nc
  1. 配置本机ssh config
  • 运行命令:vim ~/.ssh/config

  • 内容vim ~/.ssh/config 如下:

Host foo  #目标主机(内网服务器)别名,也可写成目标服务器IP,可使用通配符,如:Host 10.208.*

HostName 192.168.0.11  #目标机域名或IP地址

User root  #SSH用户名

Port 22   #SSH端口

ProxyCommand ssh -q -p 22 [email protected] nc %h %p  #这地方的用户名, ip 是指的 堡垒机的

IdentityFile ~/.ssh/id_rsa  #登陆堡垒机的私钥所在位置,如默认位置可不用显示指定

4、原理

通过ProxyCommand ,可以在开启ssh之前执行一个命令打开代理隧道,这个命令 nc %h % p是在堡垒机上使用nc开启了远程隧道。

ProxyCommand 参数 中的-q 是为了防止和堡垒机的ssh连接产生多余的输出,比如 不加 -q 就会导致每次断开连接时会多一句 killed by signal 1


代码搬上来:

清单一 :gradle项目中导入使用的包
// ssh

compile 'com.jcraft:jsch:0.1.54'

// google json

compile 'com.google.code.gson:gson:2.8.5'

注意:如果你们使用的是maven构建的项目,那就去maven官网中去找对应的包,引入。

清单二:MyUserInfo.java 用户信息类

实现 Jsch包中的UserInfo,UIKeyboardInteractive,用来存用户信息,以及进行交互式命令的赋值。

注意:promptYesNo()方法,要手动改为true,只用这样,在运行的时候才能,赋值为yes,便不会再提示输入跳板机和目标机器的密码。

import com.jcraft.jsch.UIKeyboardInteractive;

import com.jcraft.jsch.UserInfo;

/**

* @author wangchunlan

* @Description

* @date 2018/10/12 14:46

**/

public abstract class MyUserInfo implements UserInfo,UIKeyboardInteractive {

@Override

public String[] promptKeyboardInteractive(String destination, String name, String instruction, String[] prompt, boolean[] echo) {

return new String[0];

}

@Override

public String getPassphrase() {

return null;

}

@Override

public String getPassword() {

return null;

}

@Override

public boolean promptPassword(String message) {

return false;

}

@Override

public boolean promptPassphrase(String message) {

return false;

}

@Override

public boolean promptYesNo(String message) {

// 注意此处改为true

return true;

}

@Override

public void showMessage(String message) {

}

}
清单三、 SSHInfo.java 堡垒机与目标机器的常用属性封装

注意:

1、我在setCommandOutput()方法中做了修改,添加了一句reader = new BufferedReader(new InputStreamReader(commandOutput));。

2、 SSHInfo()构造方法, 创建了对象:this.ssh =new JSch();

import com.jcraft.jsch.Channel;

import com.jcraft.jsch.JSch;

import com.jcraft.jsch.Session;

import java.io.BufferedReader;

import java.io.InputStream;

import java.io.InputStreamReader;

/**

* 跳板机 与目标机器的 常用属性 封装

* @author wangchunlan

* @Description

* @date 2018/10/12 14:52

**/

public class SSHInfo {

private Session session;

private JSch ssh;

// 目标机器

private String targer_username;

private String targer_password;

private String targer_host;

// 堡垒机

private String jump_username;

private String jump_password;

private String jump_host;

private int port = 22;

private InputStream commandOutput;

private BufferedReader reader;

private Channel channel;

private boolean ready;

public SSHInfo(){

}

public SSHInfo(String targer_username, String targer_password, String targer_host, String jump_username, String jump_password, String jump_host, int port) {

this.ssh =new JSch();

this.targer_username = targer_username;

this.targer_password = targer_password;

this.targer_host = targer_host;

this.jump_username = jump_username;

this.jump_password = jump_password;

this.jump_host = jump_host;

this.port = port;

}

public SSHInfo(String targer_username, String targer_password, String targer_host, String jump_username, String jump_password, String jump_host, int port, InputStream commandOutput) {

this.ssh =new JSch();

this.targer_username = targer_username;

this.targer_password = targer_password;

this.targer_host = targer_host;

this.jump_username = jump_username;

this.jump_password = jump_password;

this.jump_host = jump_host;

this.port = port;

this.commandOutput = commandOutput;

}

public Session getSession() {

return session;

}

public void setSession(Session session) {

this.session = session;

}

public JSch getSsh() {

return ssh;

}

public void setSsh(JSch ssh) {

this.ssh = ssh;

}

public String getTarger_username() {

return targer_username;

}

public void setTarger_username(String targer_username) {

this.targer_username = targer_username;

}

public String getTarger_password() {

return targer_password;

}

public void setTarger_password(String targer_password) {

this.targer_password = targer_password;

}

public String getTarger_host() {

return targer_host;

}

public void setTarger_host(String targer_host) {

this.targer_host = targer_host;

}

public String getJump_username() {

return jump_username;

}

public void setJump_username(String jump_username) {

this.jump_username = jump_username;

}

public String getJump_password() {

return jump_password;

}

public void setJump_password(String jump_password) {

this.jump_password = jump_password;

}

public String getJump_host() {

return jump_host;

}

public void setJump_host(String jump_host) {

this.jump_host = jump_host;

}

public int getPort() {

return port;

}

public void setPort(int port) {

this.port = port;

}

public InputStream getCommandOutput() {

return commandOutput;

}

public void setCommandOutput(InputStream commandOutput) {

this.commandOutput = commandOutput;

reader = new BufferedReader(new InputStreamReader(commandOutput));

}

public BufferedReader getReader() {

return reader;

}

public void setReader(BufferedReader reader) {

this.reader = reader;

}

public Channel getChannel() {

return channel;

}

public void setChannel(Channel channel) {

this.channel = channel;

}

public boolean isReady() {

return ready;

}

public void setReady(boolean ready) {

this.ready = ready;

}

}
清单四、SSHConnection.java 链接工具类

import com.jcraft.jsch.*;

import top.smartpos.itom.utils.LogUtils;

import java.io.File;

import java.util.List;

/**

* SSH链接工具类

* @author wangchunlan

* @Description

* @date 2018/10/12 15:43

**/

public class SSHConnection {

// private SSHInfo sshInfo=new SSHInfo();

private SSHInfo sshInfo=new SSHInfo("root","targer_password","192.168.0.11","root","jump_password","192.168.0.85",22);

public boolean connect(){

try {

String config=config(sshInfo.getPort(),sshInfo.getTarger_username(),sshInfo.getTarger_host(),sshInfo.getJump_username(),sshInfo.getJump_host());

System.out.println(config);

ConfigRepository configRepository=OpenSSHConfig.parse(config);

sshInfo.getSsh().setConfigRepository(configRepository);

Session session=sshInfo.getSsh().getSession("foo");

session.setPassword(sshInfo.getTarger_password());

session.setUserInfo(new MyUserInfo() {});

session.connect(30000);

sshInfo.setSession(session);

sshInfo.setReady(true);

return true;

} catch (Exception e) {

e.printStackTrace();

return false;

}

}

public String write(String command) {

try {

sshInfo.setChannel(sshInfo.getSession().openChannel("exec"));

Channel channel= sshInfo.getChannel();

((ChannelExec) channel).setCommand(command);

sshInfo.setCommandOutput(channel.getInputStream());

channel.connect(3000);

StringBuilder sBuilder = new StringBuilder();

String lido = sshInfo.getReader().readLine();

while (lido != null) {

sBuilder.append(lido);

sBuilder.append("\n");

lido = sshInfo.getReader().readLine();

}

System.out.println("The remote command is: " + command);

return sBuilder.toString();

} catch (Exception e) {

e.printStackTrace();

}

return null;

}

// 断开 通道和会话

public void close() {

if (sshInfo.getChannel() != null)

sshInfo.getChannel().disconnect();

if (sshInfo.getSession() != null)

sshInfo.getSession() .disconnect();

sshInfo.setReady(false);

}

public String config(int port,String targer_username,String targer_host,String jump_username,String jump_host){

// todo :foo 要改为final 常量

String bastion=jump_username+"@"+jump_host+":"+port;

String config="";

config=

"Port "+port+"\n"+

"\n"+

"Host foo"+"\n"+

" User "+targer_username+"\n"+

" Hostname "+targer_host+"\n"+

" ProxyJump "+bastion+"\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";

return config;

}

/**

* 上传文件

* @param sourceFile 本地路径

* @param dirDestino 上传文件绝对路径 如:/root/kvm2.xml

* @return

*/

/**

* 上传文件

* @param sourceFile 本地文件绝对路径 如:c:/kvm.xml

* @param targetDirFileLocation 上传文件所在目录 如:/root/

* @return

*/

public boolean upload(String sourceFile,String targetDirFileLocation) {

try {

File origem_ = new File(sourceFile);

targetDirFileLocation = targetDirFileLocation.replace(" ", "_");

String targetFile = targetDirFileLocation.concat("/").concat(origem_.getName());

return upload(sourceFile, targetFile, targetDirFileLocation);

} catch (Exception e) {

throw new SSHException(e);

}

}

/**

* 上传文件

* @param sourceFile 本地文件绝对路径 如:c:/kvm.xml

* @param targetFile 目标文件绝对路径 如:/root/kvm2.xml

* @param targetDirFileLocation 上传文件所在目录 如:/root/

* @return

*/

public boolean upload(String sourceFile,String targetFile,String targetDirFileLocation) {

try {

ChannelSftp sftp = (ChannelSftp) sshInfo.getSession().openChannel("sftp");

sftp.connect();

targetDirFileLocation = targetDirFileLocation.replace(" ", "_");

sftp.cd(targetDirFileLocation);

sftp.put(sourceFile, targetFile);

sftp.disconnect();

return true;

} catch (Exception e) {

e.printStackTrace();

}

return false;

}

/**

* 下载文件

* @param sourceFile 下载文件绝对路径名称 如:/root/kvm2.xml

* @param targetFile 下载文件目标位置绝对路径名称 如:C:\Users\kvm2.xml

* @return

*/

public boolean download(String sourceFile, String targetFile){

try {

ChannelSftp sftp = (ChannelSftp) sshInfo.getSession().openChannel("sftp");

sftp.connect();

sftp.get(sourceFile, targetFile);

sftp.disconnect();

return true;

} catch (Exception e) {

e.printStackTrace();

}

return false;

}

/**

* 判断单个源文件[上传文件] 是否存在

* @param sourceFile 源文件

* @return

*/

public boolean prepareUpload(String sourceFile) {

File file = new File(sourceFile);

if (file.exists() && file.isFile()) {

return true;

}

return false;

}

/**

* 判断多个源文件[上传文件] 是否存在

* @param sourceFiles 源文件

* @return

*/

public boolean prepareUpload(List sourceFiles) {

for(String item:sourceFiles){

File file = new File(item);

boolean isTrue=file.exists() && file.isFile();

if (!isTrue) {

return false;

}

continue;

}

return true;

}

public SSHInfo getSshInfo() {

return sshInfo;

}

public void setSshInfo(SSHInfo sshInfo) {

this.sshInfo = sshInfo;

}

}

清单五、TestDemo.java 测试用例
/**

* 测试

* @author wangchunlan

* @Description

* @date 2018/10/12 16:29

**/

public class TestDemo {

public static void main(String[] args) {

// 测试一、创建目录

createDir("wangchunlan");

// 测试二、 上传单个文件

uploadTo("/root/kvm2.txt","/root/","C:\\Users\\Administrator\\Desktop\\kvm2.xml");

}

/**

* 创建文件夹

* tip:当文件夹存在时,不报错。

* @param targetDirFileLocation 创建(目标)文件夹的绝对路径 如:/root/ma

*/

public static void createDir(String targetDirFileLocation) {

SSHConnection ssh = new SSHConnection();

try {

ssh.connect();

if (ssh.getSshInfo().isReady()) {

ssh.write("mkdir -p " + targetDirFileLocation);

String out = ssh.write("ifconfig");

System.out.print(out);

ssh.close();

}

} catch (Exception e) {

e.printStackTrace();

}

}

/**

* 上传单个文件

*

* @param targetFile 目标文件的绝对路径 如 :/root/kvm2.txt"

* @param targetDirFileLocation 目标文件所在绝对路径 如:/root/

* @param sourceFile 源文件的绝对路径完整名称 如:C:\Users\kvm2.txt

* @return

*/

public static boolean uploadTo(String targetFile, String targetDirFileLocation, String sourceFile) {

SSHConnection ssh = new SSHConnection();

try {

ssh.connect();

if (ssh.getSshInfo().isReady()) {

boolean isExist = ssh.prepareUpload(sourceFile);

if (!isExist) {

System.out.print("本地不存在此文件");

return false;

}

if (ssh.upload(sourceFile, targetFile, targetDirFileLocation)) {

System.out.print("文件上传成功");

ssh.close();

return true;

}

}

} catch (Exception e) {

e.printStackTrace();

}

return false;

}

/**

* 创建文件夹

* tip:当文件夹存在时,不报错。

*

* @param targetDirFileLocation 创建(目标)文件夹的绝对路径 如:/root/ma

*/

public static void createDir(SSHConnection ssh, String targetDirFileLocation) {

ssh.write("mkdir -p " + targetDirFileLocation);

}

}

说明一下:我用的堡垒机的IP :192.168.0.85,内部服务器的IP是192.168.0.11。

运行TestDemo.java文件,我们来测试一下。

测试一、创建目录

createDir("wangchunlan");

先去linux内部服务器(192.168.0.11)中查看一下/root/目录下文件:


使用JSch实现ssh隧道建立_第2张图片
内部服务器文件.png

然后运行结果,控制台输出:

使用JSch实现ssh隧道建立_第3张图片
控制台输出 内部服务器IP.png

我们到内部服务器中查看一下,是否真的有这个文件wangchunlan

使用JSch实现ssh隧道建立_第4张图片
存在该文件.png
测试二、上传单个文件

uploadTo("/root/kvm2.txt","/root/","C:\Users\Administrator\Desktop\kvm2.xml");

同理可验证:文件上传也是可以用的。

使用JSch实现ssh隧道建立_第5张图片
文件验证.png

以上代码,已经完成了用java实现 Jsch建立隧道问题。

下面是我的一点文章扩展。


通过搜集资料和实践,最终确定了用JSch来链接到服务器并进行系列远程命令操作。而在建立2层链接时,开始是利用普通2层session,太浪费时间,每次用都要链接2次,关闭也是如此。后又改用端口转发,虽然能够实现,如果服务器堡垒机的端口意外泄露,会造成 恶意攻击。最后,确定下使用Proxycommand。

  • 关于使用Proxy 建立隧道,有两种方式 ProxyCommand 和ProxyJump.我使用的是linux命令实现的。可以看我的另一篇文章 《用java 运行proxyCommand 命令,带来命令交互式问题》。

  • 关于使用端口转发,也请参考我的另一篇文章《 java 利用jsch端口转发 建立连接》。

  • 关于OpenSSH/Cookbook/Proxies and Jump Hosts 文章,get到的知识点,提取整理一下《 OpenSSH / Cookbook / Proxies和Jump Hosts 知识点提取》。


参考资料:

1、OpenSSH/Cookbook/Proxies and Jump Hosts 重点推荐:对于socks代理,堡垒机转发链接网关,以及利用netcat隧道连接,解释的非常通透,我也是因为这篇文章才豁然开朗的;

2、SSH ProxyCommand example: Going through one host to reach another server :通过这篇文章的示例,在linux测试,初步理解了实现原理。

3、透过SSH代理穿越跳板机的方法 : 说清楚了 问题场景以及实现步骤,所以我参考了其中的ProxyCommand+Netcat 部分的叙述。

4、JSch - Examples - OpenSSHConfig.java :官网的示例,拯救了对于Jsch使用的疑惑。

5、Secure Shell (SSH) and Java : 重点推荐: 说清了 java实现 ssh的的思路,需要翻墙才能阅读。

你可能感兴趣的:(使用JSch实现ssh隧道建立)