美文网首页
使用JSch实现ssh隧道建立

使用JSch实现ssh隧道建立

作者: 瑾兰 | 来源:发表于2018-10-15 12:50 被阅读259次

    前言:

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

    问题情景描述:

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

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

    利用堡垒机建立隧道链接.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 root@192.168.0.85 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<String> 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/目录下文件:


    内部服务器文件.png

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

    控制台输出 内部服务器IP.png

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

    存在该文件.png
    测试二、上传单个文件

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

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

    文件验证.png

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

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


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


    参考资料:

    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隧道建立

          本文链接:https://www.haomeiwen.com/subject/hwhfzftx.html