You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用JSch通过SSH隧道与远程进程通信的异常问题排查

用JSch实现SSH隧道通信的异常问题解决

我需要用JSch复现Bash中正常运行的SSH隧道功能:建立隧道后每10秒向远程发送认证字符串,并接收持续数据流。目前已成功建立SSH会话,但遇到两个异常:


异常1:本地端口转发后连接失败,抛出channel is not opened

调用session.setPortForwardingL配置本地端口转发,随后连接localhost:tunnelLocalPort时,DirectTCPIP线程抛出JSchException: channel is not opened。

相关代码

JSch.setConfig("server_host_key", JSch.getConfig("server_host_key") + ",ssh-rsa")
JSch.setConfig("PubkeyAcceptedAlgorithms", JSch.getConfig("PubkeyAcceptedAlgorithms") + ",ssh-rsa")
val jsch = new JSch()
jsch.addIdentity(relPathPrivateKey)
val session = jsch.getSession(usernameAtRemote, remoteIp, 22)
session.setConfig("StrictHostKeyChecking", "no")
session.connect()   // 确认会话已连接并完成认证
val assignedPort: Int = session.setPortForwardingL(tunnelLocalPort, localhost, remotePort)
val socket = new Socket(localhost, assignedPort)
val inputStream = socket.getInputStream
val outputStream = socket.getOutputStream
val bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
val writer = new PrintWriter(outputStream)
writer.println(auth)
readLines(bufferedReader)
socket.close()
session.disconnect()

错误触发位置

com.jcraft.jsch.Channel类的sendChannelOpen方法:

protected void sendChannelOpen() throws Exception {
  // ... 第819-821行 ...
  if (this.open_confirmation == false) { // SSH_MSG_CHANNEL_OPEN_FAILURE
    throw new JSchException("channel is not opened.");  // <== DirectTCPIP线程在此失败
  }
  // ...
}

异常2:使用getStreamForwarder时抛出session is down

调用session.getStreamForwarder获取通道后,执行channel.connect()时,主线程抛出JSchException: session is down。

相关代码

JSch.setConfig("server_host_key", JSch.getConfig("server_host_key") + ",ssh-rsa")
JSch.setConfig("PubkeyAcceptedAlgorithms", JSch.getConfig("PubkeyAcceptedAlgorithms") + ",ssh-rsa")
val jsch = new JSch()
jsch.addIdentity(relPathPrivateKey)
val session = jsch.getSession(usernameAtRemote, remoteIp, 22)
session.setConfig("StrictHostKeyChecking", "no")
session.connect()   // 确认会话已连接并完成认证
val channel = session.getStreamForwarder(remoteIp, remotePort)
channel.connect()   // 在此失败
val inputStream = channel.getInputStream
val outputStream = channel.getOutputStream
val bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))
val writer = new PrintWriter(outputStream)
writer.println(auth)
readLines(bufferedReader)
channel.disconnect()
session.disconnect()

错误触发位置

com.jcraft.jsch.Channel类的sendChannelOpen方法:

protected void sendChannelOpen() throws Exception {
  // ... 第816-818行 ...
  if (!_session.isConnected()) {
    throw new JSchException("session is down");  // <== 主线程在此失败
  }
  // ...
}

原Bash参考命令

$ ssh -l username -L LOCALPORT:127.0.0.1:REMOTEPORT REMOTEHOST
$ ID='{ "id": 1234, "auth": "abcdefghijklmnopqrstuvwxyz" }'
$ while true; do echo $ID; sleep 10; done | nc localhost LOCALPORT

修正后的JSch实现方案

问题核心是对JSch端口转发和通道模型的误解:

  1. 本地端口转发(对应ssh -L)的正确流程是:建立会话→设置转发规则→通过本地端口创建Socket连接,而非直接操作direct-tcpip通道。
  2. getStreamForwarder是底层流转发接口,需严格保证会话状态和通道初始化顺序。

以下是可正常运行的代码:

import com.jcraft.jsch._
import java.io._
import java.nio.charset.StandardCharsets

object Main extends Log4JLogging {
  def main(args: Array[String]): Unit = {
    val remoteIpaddr = "123.45.54.321"
    val localhost = "127.0.0.1"
    val sshPort = 22
    val tunnelLocalPort = LOCALPORT // 替换为实际本地端口
    val tunnelRemotePort = REMOTEPORT // 替换为实际远程服务端口

    val usernameAtRemoteHost = "username"
    val privateKeyForRemoteHost = "ssh/for-remotehost/id_rsa"

    val auth = "{ \"id\": 1234, \"auth\": \"abcdefghijklmnopqrstuvwxyz\" }\n"
    try {
      val currentWorkingDirectory: String = System.getProperty("user.dir")
      val pathPrivateKeyForRemoteHost: String = s"$currentWorkingDirectory/$privateKeyForRemoteHost"
      
      // 配置JSch支持ssh-rsa算法
      JSch.setConfig("server_host_key", JSch.getConfig("server_host_key") + ",ssh-rsa")
      JSch.setConfig("PubkeyAcceptedAlgorithms", JSch.getConfig("PubkeyAcceptedAlgorithms") + ",ssh-rsa")
      
      val jsch = new JSch()
      jsch.addIdentity(pathPrivateKeyForRemoteHost)
      
      // 建立SSH会话
      val session = jsch.getSession(usernameAtRemoteHost, remoteIpaddr, sshPort)
      session.setConfig("StrictHostKeyChecking", "no")
      session.connect()
      log.info("SSH会话已连接")

      // 设置本地端口转发:本地端口 -> 远程主机的127.0.0.1:REMOTEPORT
      val assignedPort = session.setPortForwardingL(tunnelLocalPort, localhost, tunnelRemotePort)
      log.info(s"本地端口转发已建立,本地端口:$assignedPort")

      // 连接本地转发端口,建立与远程服务的通信
      val socket = new Socket(localhost, assignedPort)
      val outputStream = new PrintWriter(socket.getOutputStream, true)
      val inputStream = new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8))

      // 独立线程定期发送认证字符串,避免阻塞数据流读取
      val senderThread = new Thread(() => {
        try {
          while (!socket.isClosed) {
            outputStream.println(auth)
            Thread.sleep(10000) // 每10秒发送一次
          }
        } catch {
          case e: InterruptedException => log.info("发送线程已中断")
          case e: IOException => log.error("发送数据失败", e)
        }
      })
      senderThread.start()

      // 持续读取远程返回的数据
      log.info("开始接收远程数据...")
      var line: String = null
      while ({line = inputStream.readLine(); line != null}) {
        log.info(s"收到数据:$line")
      }

      // 清理资源
      senderThread.interrupt()
      socket.close()
      session.disconnect()
      log.info("资源已清理,会话断开")
    } catch {
      case e: Exception =>
        log.error("执行失败", e)
        System.exit(1)
    }
  }
}

关键修正点

  • 端口转发流程:设置setPortForwardingL后,通过Socket连接本地端口,这是模拟ssh -L的标准方式。
  • 线程分离:将认证字符串发送逻辑放在独立线程,避免阻塞主线程的数据流读取。
  • 资源管理:确保Socket、流和会话在结束时正确关闭,避免资源泄漏。

内容的提问来源于stack exchange,提问作者gknauth

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.24 05:32:04