首页 > 解决方案 > 如何通过现有 TCP 连接运行 ssh

问题描述

我希望能够在不同的 NAT 后面一次通过 SSH 连接到多个 linux 设备。我无法配置他们所在的网络。但是,我无法让 ssh 通过现有连接。

我可以完全控制我的客户端和设备。到目前为止的过程如下:在我的客户端上,我首先运行

socat TCP-LISTEN:5001,pktinfo,fork EXEC:./create_socket.sh,fdin=3,fdout=4,nofork

内容./create_socket.sh

ssh -N -M -S "~/sockets/${SOCAT_PEERADDR}" -o "ProxyCommand=socat - FD:3!!FD:4" "root@${SOCAT_PEERADDR}"

在设备上,我正在运行

socat TCP:my_host:4321 TCP:localhost:22

但是,我认为没有任何东西进出FD:3!!FD:4,因为 ProxyCommand 是一个子进程。我也尝试过设置fdin=3,fdout=3并更改./create_socket.sh为:

ssh -N -M -S "~/sockets/${SOCAT_PEERADDR}" -o "ProxyUseFdpass=yes" -o "ProxyCommand=echo 3" "root@${host}"

这会打印一个错误:

mm_receive_fd: no message header
proxy dialer did not pass back a connection

我相信这是因为 fd 应该以某种方式使用 发送sendmsg,但 fd 无论如何都不是源自子进程。我想让它尽可能简单,这感觉接近可行。

标签: linuxssh

解决方案


您想将客户端/服务器模型颠倒过来,并制作一个通用服务器来按需生成客户端并响应来自网络边界的传入未经身份验证的 TCP 连接,然后告诉新生成的客户端使用未经身份验证的 TCP 会话。我认为这可能有你没有想到的安全考虑。如果恶意人员向您的计算机发送垃圾邮件连接,您的计算机将生成大量SSH 实例以进行连接,并且这些进程在进行身份验证时会占用大量本地系统资源。您实际上是在尝试将 SSH 设置为跨网络边界自动连接到不受信任(未经验证)的远程启动的机器。我不能强调如何这可能对您的客户端计算机很危险。使用错误的选项可能会暴露您拥有的任何凭据,甚至可能让恶意人员完全访问您的计算机。

还值得注意的是,您要求做的场景是在多个设备之间建立隧道以跨不受信任的网络边界多路复用其他连接,这正是 VPN 软件的目的。是的,SSH 可以建立隧道。VPN 软件可以更好地建立隧道。这个概念是您将在您的客户端计算机上运行一个 VPN 服务器。VPN 服务器将创建一个代表您的设备的新(虚拟)网络接口。这些设备将连接到 VPN 服务器并被分配一个 IP 地址。然后,从客户端机器上,您只需启动 SSH 到设备的 VPN 地址,它将通过虚拟网络接口路由并到达设备并由其 SSH 守护程序服务器处理。那你不socat或用于端口转发的 SSH 选项。您将获得围绕 VPN 存在的所有工具和教程。我强烈建议您查看 VPN 软件

如果您真的想使用 SSH,那么我强烈建议您学习如何保护 SSH 服务器。您已声明设备跨越网络边界 (NAT),并且您的客户端系统不受保护。我不会阻止你在脚上开枪,但在你所说的情况下很容易做到这一点。如果你在工作环境中,你应该和你的系统管理员讨论防火墙规则、堡垒主机之类的东西。

是的,你可以做你所说的。我强烈建议谨慎。我强烈建议它,我不会建议任何可以与上述内容一起使用的东西。我将建议一个具有相同概念但更多身份验证的变体。

首先,您已经有效地设置了自己的 SSH 反弹服务器,但没有任何与 SSH 服务器兼容的通用工具。所以这是我要解决的第一件事:使用 SSH 服务器软件来验证传入的隧道请求,方法是使用ssh客户端软件从设备而不是socat. ssh已经有很多功能可以在两个方向上创建隧道,并且您会获得与它捆绑socat的身份验证(使用 ,没有身份验证)。设备应该能够使用加密密钥进行身份验证(ssh 调用这些身份)。您需要手动连接一次从设备验证和授权远程加密密钥指纹。您还需要将公钥文件(不是私钥文件)复制到客户端计算机并将其添加到您的 authorized_keys 文件中。如果需要,您可以单独寻求帮助。

第二个问题是您似乎正在使用 fd3 和 fd4。我不知道你为什么这样做。如果有的话,您应该使用 fd0 和 fd1 ,因为它们分别是stdinstdoutsocat但是,如果您用于启动连接,您甚至不需要这样做。只需使用-wherestdinstdoutare mean。它应该完全兼容-o ProxyCommand而不指定任何文件描述符。这个答案的末尾有一个例子。

来自设备端的调用可能如下所示(将其放入脚本文件中):

IDENTITY=/home/WavesAtParticles/.ssh/tunnel.id_rsa # on device
REMOTE_SOCKET=/home/WavesAtParticles/.ssh/$(hostname).sock # on client
REMOTEUSER=WavesAtParticles # on client
REMOTEHOST=remotehost # client hostname or IP address accessible from device
while true
do
  echo "$(date -Is) connecting"
  #
  # Set up your SSH tunnel. Check stderr for known issues.
  ssh \
    -i "${IDENTITY}" \
    -R "${REMOTE_SOCKET}:127.0.0.1:22" \
    -o ExitOnForwardFailure=yes \
    -o PasswordAuthentication=no \
    -o IdentitiesOnly=yes \
    -l "${REMOTEUSER}" \
    "${REMOTEHOST}" \
      "sleep inf" \
  2> >(
    read -r line
    if echo "${line}" | grep -q "Error: remote port forwarding failed"
    then
      ssh \
        -i "${IDENTITY}" \
        -o PasswordAuthentication=no \
        -o IdentitiesOnly=yes \
        -l "${REMOTEUSER}" \
        "${REMOTEHOST}" \
          "rm ${REMOTE_SOCKET}" \
      2>/dev/null # convince me this is wrong
      echo "$(date -Is) removed stale socket"
    fi
    #
    # Re-print stderr to the terminal
    >&2 echo "${line}" # the stderr line we checked
    >&2 cat - # and any unused stderr messages
  )

  echo "disconnected"
  sleep 30
done

请记住,就 shell 脚本而言,复制和粘贴是不好的。至少,我建议您阅读man sshman ssh_config, 并对照 shellcheck.net 检查脚本。该脚本的意图是:

  1. 在一个循环中,让您的设备(重新)连接到您的客户端以维护您的隧道。
  2. 如果连接断开或失败,则每 30 秒重新连接一次。
  3. ssh使用以下参数 运行:
    • -i "${IDENTITY}":指定用于身份验证的私钥。
    • -R "${REMOTE_SOCKET}:127.0.0.1:22":指定一个连接请求转发器,它接受远程端的连接, /home/WavesAtParticles/$(hostname).sock然后通过连接将它们转发到本地端127.0.0.1:22
    • -o ExitOnForwardFailure=yes:如果远程端无法设置连接转发器,那么本地端应该发出一个错误并死掉(我们在子shell中检查这个错误)。
    • -o PasswordAuthentication=no:不要回退到密码请求,特别是因为本地用户不在这里输入密码
    • -o IdentitiesOnly=yes:不要使用任何默认身份,也不要使用任何本地代理提供的任何身份。仅使用 指定的一种-i
    • -l "${REMOTEUSER}": 以指定用户登录。
    • remotehost,例如您希望设备连接到的客户端计算机。
  4. 永远沉睡
  5. 如果连接由于过时的套接字而失败,请通过以下方式解决此问题:

    • 单独登录
    • 删除(陈旧的)套接字
    • 打印今天的日期,表明它何时被删除
    • 再次循环

    有一个选项旨在使这种错误处理变得多余:StreamLocalBindUnlink. 但是,该选项无法正常工作,并且存在多年的错误。我想那是因为确实没有多少人使用 ssh 通过 unix 域套接字进行转发。这很烦人,但解决方法并不难。

使用 unix 域套接字应该将连接限制为可​​以访问套接字文件的任何人(应该只有您,并且root如果它放置在您的${HOME}/.ssh目录中并且该目录具有正确的权限)。我不知道这对你的案子是否重要。

另一方面,如果您愿意为每个设备打开一个 TCP 端口,您也可以大大简化这一点。127.0.0.1但是同一系统上的任何其他用户也可以连接。您应该专门监听127.0.0.1哪个只会接受来自同一主机的连接,以防止外部机器到达转发端口。例如,您可以将${REMOTE_SOCKET}变量更改127.0.0.1:4567为侦听端口 4567 并且只接受本地连接。因此,您将失去命名套接字功能并允许客户端计算机上的任何其他用户连接到您的设备,但获得一个更简单的隧道脚本(因为您可以删除有关解析 stderr 以删除陈旧套接字文件的全部内容)。

只要您的设备在线(可以访问您的工作站的传入端口)并且正在运行该脚本,并且身份验证有效,那么隧道也应该在线或即将上线。但是,在网络连接丢失(和恢复)后需要一些时间才能恢复。您可以使用ConnectTimeoutTCPKeepAliveServerAliveInterval选项以及sleep 30循环的一部分对其进行调整。tmux即使您没有运行登录会话,您也可以在会话中运行它以使其继续运行。即使在从电源故障中恢复后,您也可以将其作为设备上的系统服务运行以使其联机。

然后从您的客户端,您可以反向连接:

ssh -o ProxyCommand='socat - unix-connect:/home/WavesAtParticles/remotehost.sock' -l WavesAtParticles .

在此调用中,您将开始ssh. 然后它将使用socat. 它将获取其 stdin/stdout 并通过连接的 AF_UNIX 套接字在提供的路径上中继它。您需要更新您期望的远程主机的路径。但是根本不需要指定文件描述符。如果ssh投诉:

2019/08/26 18:09:52 socat[29914] E connect(5, AF=1 "/home/WavesAtParticles/remotehost.sock", 7): Connection refused
ssh_exchange_identification: Connection closed by remote host

那么隧道当前处于关闭状态,您应该调查remotehost设备的连接性。

如果您将远程转发选项与 TCP 端口侦听而不是 unix 域套接字一起使用,则客户端通过隧道到远程调用变得更加容易:ssh -p 4567 WavesAtParticles@localhost.

同样,您正在尝试反转客户端/服务器模型,我认为这对 SSH 来说不是一个好主意。


推荐阅读