ssh 具有单独的 stdin、stdout、stderr 和 tty

Kam*_*ski 7 ssh sudo io-redirection tty

问题

\n

考虑这样的命令:

\n
<binary_input ssh user@server \'sudo tool\' >binary_output 2>error.log\n
Run Code Online (Sandbox Code Playgroud)\n

其中tool是任意的,ssh是一个包装器或一些ssh-like-contraption允许上述工作的包装器。常规的ssh话是行不通的。

\n

sudo在这里使用,但这只是需要 tty 的命令的示例。我想要一个通用的解决方案,而不是特定于sudo.

\n
\n

研究:原因

\n

常规情况下ssh它不起作用,因为:

\n
    \n
  • sudo需要 tty 询问密码(或者根本无法工作),所以我需要ssh -t; 实际上在这种情况下我需要ssh -tt.
  • \n
  • 另一方面ssh -tt将使sudo读取密码binary_input。我想通过本地 tty 提供密码。即使sudo配置为无需密码即可工作,或者如果我将密码注入到binary_inputssh -tt也会从远程 tty 进行读取sudo和写入,并将输出错误以及提示写入远程 tty。我不仅无法在本地区分输出和错误/提示。所有流都将由远程 tty 处理,这将破坏数据(您可以在我的这个答案的一些示例中看到这一点,在标题为“一些实践”的部分中)。tool
  • \n
\n
\n

研究:与有效命令的比较

\n\n
\n

研究:类似问题

\n

我发现了几个类似的问题:

\n\n
\n

我的明确问题

\n

在以下命令中:

\n
<binary_input ssh user@server \'requires-tty\' >binary_output 2>error.log\n
Run Code Online (Sandbox Code Playgroud)\n

requires-tty是需要 tty 但将二进制数据从其 stdin 处理到其 stdout 的代码的占位符。看来我需要ssh -tt,否则requires-tty不行;同时我不能使用ssh -tt,否则二进制数据将被破坏。我怎样才能方便地解决这个问题呢?

\n

requires-tty可以,sudo \xe2\x80\xa6但我不想要特定于sudo.

\n

我想理想的(?)解决方案将是一个脚本/工具,它可以替换ssh上面的调用并且可以正常工作。它应该(?)将远程 stdin、stdout 和 stderr 分别连接到其本地对应项,并将远程 tty 连接到本地 tty。

\n

如果可能的话,我更喜欢不需要任何服务器端配套程序的客户端解决方案。

\n

Kam*_*ski 4

脚本

\n

我是这个问题的作者,这是我尝试构建一个解决问题的脚本。该脚本旨在在客户端工作,它替换ssh有问题的命令。这是实验性的。我称之为sshe。这是脚本:

\n
#!/bin/sh -\n\n# the name of the script\nme="${0##*/}"\n\n# error handling functions\nscream() { printf \'%s\\n\' >&2 "$1"; }\ndie()    { scream "$2"; exit "$1"; }\n\n# initialization of variables\nredir0=\'\'\nredir1=\'\'\nredir2=\'\'\ntty="/dev/$(ps -p "$$" -o tty=)"\n\n# edge cases\n[ "$tty" = \'/dev/?\' ] && { scream "$me: no tty detected, falling back to regular ssh"\n   exec ssh "$@"; }\n[ "$#" -lt 2 ] && die 1 "usage: $me [options] [user@]hostname command"\n\n# see what needs to be redirected\nexec 7>&1\nif [ "$(<&0 tty 2>/dev/null)" != "$tty" ]; then redir0=y; fi\nif [ "$(<&7 tty 2>/dev/null)" != "$tty" ]; then redir1=y; fi\nif [ "$(<&2 tty 2>/dev/null)" != "$tty" ]; then redir2=y; fi\nexec 7>&-\n\n# edge case\n[ "$redir0$redir1$redir2" ] || { scream "$me: no redirection detected, falling back to ssh -t"\n   exec ssh -t "$@"; }\n\n# command line parsing, extract two last arguments: ... host command\nz="$#"\nn="$z"\nfor arg do\n   if [ "$n" -eq "$z" ]; then\n      set --\n   fi\n   case "$n" in\n      1) command="$arg"\n   ;;\n      2) host="$arg"\n   ;;\n      *)\n      set -- "$@" "$arg"\n   esac\n   n="$(($n - 1))"\ndone\n\n# prepare to clean on exit\ntrap \'status="$?"; rm -r "$tmpd" 2>/dev/null; trap - EXIT; exit "$status"\' EXIT HUP INT QUIT PIPE TERM\n\n# temporary directory and socket\ntmpd="$(mktemp -d)"\n[ "$?" -eq 0 ] || exit 1\nsock="$tmpd/sock"\n\n# main pipe: ssh master connection -> background cat\n(\n[ "$redir0" ] || exec 0</dev/null\n# ssh master connection, it will report the remote PID of the remote shell via its stdout\nssh -M -S "$sock" "$@" -T "$host" \'</dev/null echo "$$"; exec sleep 2147483647\'\n) | {\n\n# read the remote PID\nIFS= read -r rpid || exit 1\n\n# background process to pass data\nexec 6<&0\ncat <&6 2>/dev/null &\n\n# move original descriptors out of the way\nexec </dev/tty >/dev/tty 6>&-\n\n# prepare remote redirections\nif [ "$redir0" ]; then redir0="<&6";  fi\nif [ "$redir1" ]; then redir1=">&7";  fi\nif [ "$redir2" ]; then redir2="2>&8"; fi\n\n# ssh to run the command, with remote tty\nssh -S "$sock" -t "$host" "\n trap \'status=\\"\\$?\\"; kill $rpid 2>/dev/null; trap - EXIT; exit \\"\\$status\\"\' EXIT HUP INT QUIT PIPE TERM\n exec 6</proc/$rpid/fd/0 7>/proc/$rpid/fd/1 8>/proc/$rpid/fd/2 9>/dev/tty $redir0 $redir1 $redir2 || exit 3;\n $command"\n}\n\n
Run Code Online (Sandbox Code Playgroud)\n
\n

一般免责声明

\n
    \n
  • 该脚本在许多情况下都运行良好。我并不是说它会随机失败。它不会随机失败。我说它无法处理某些特定数据。它处理任意数据。

    \n

    它“无法正常工作”的情况仅由其与本地 tty 的交互引起。通过不包含任何 tty 的通道流动的数据(包括任意二进制数据)始终没问题。请阅读本答案的其余部分,尤其是“障碍和警告”部分,以了解问题并了解应避免的内容。

    \n
  • \n
  • 像这样的命令:

    \n
    <binary_input sshe user@server \'sudo tool\' >binary_output 2>error.log\n
    Run Code Online (Sandbox Code Playgroud)\n

    避免了这个问题。只要满足技术要求(请参阅下面的“要求”),它就应该可以正常工作。

    \n
  • \n
  • 该脚本是实验性的,我尝试设置traps,以保留退出状态并以理智(?)的方式在退出时进行清理。我不确定我是否成功了。

    \n
  • \n
  • 该脚本从来就不是为了万无一失。将其视为概念证明。

    \n
  • \n
\n
\n

用法

\n

sshe像这样使用:

\n
sshe \xe2\x80\xa6 [user@]hostname command\n
Run Code Online (Sandbox Code Playgroud)\n

其中\xe2\x80\xa6表示如果可执行文件是ssh. 这里不需要放置-tnor -tt(nor -T)。该脚本假设您希望在远程端使用 tty (否则只需使用ssh)。该脚本期望至少本地 stdin、stdout、stderr 之一从本地 tty 重定向出去。ssh -t如果一切都连接到本地 tty,则脚本将回退。

\n

重要的事情:

\n
    \n
  • command是您要在服务器上运行的 shell 代码。它必须是单个参数,即 的最后一个参数sshe。不能省略。
  • \n
  • hostname或者user@hostname必须是倒数第二个参数。不能省略。
  • \n
\n

在内部,脚本需要知道command在前面添加一些代码。它需要知道,[user@]hostname因为它使用了两次。该脚本仅分别选择最后一个和倒数第二个参数,因此存在上述限制。

\n

并非每个有效的调用都可以通过仅替换为 来ssh转换为sshe调用。但我相信任何运行代码的有效调用(而不是生成交互式 shell)都可以重新排列为有效命令。例子:sshsshesshsshe

\n
ssh user@server -p 1234 echo foo\n
Run Code Online (Sandbox Code Playgroud)\n

应重新排列为:

\n
sshe -p 1234 user@server \'echo foo\'\n
Run Code Online (Sandbox Code Playgroud)\n

sshe(除非在这种情况下你并不真正需要;它只是正确语法的一个示例)。如果您使用了sshe user@server -p 1234 echo foo脚本,那么该脚本将echo作为服务器和foo命令,因为它不会像ssh以前那样解析其参数。

\n

下面有一些例子。

\n
\n

要求、可移植性问题

\n

当地要求(其中sshe运行地点):

\n
    \n
  • /dev/$(ps -p "$$" -o tty=)假定为控制终端的“真实姓名”。比较一下这个问题
  • \n
  • mktemp -d
  • \n
  • ssh支持-M-S;该脚本创建主连接和从连接。
  • \n
\n

远程要求(在服务器上):

\n
    \n
  • SSH 服务器能够处理主从连接。
  • \n
  • /proc伪文件系统。
  • \n
  • 使用能力/proc/nnnn/fd/N属于同一用户的另一个进程。
  • \n
  • 符合 POSIX 标准的外壳。
  • \n
  • 无提示启动脚本(比较SCP时不起作用echo.bashrc,与sshe情况类似)\n。
  • \n
\n

在测试期间,我成功从 Kubuntu (18.04.5 LTS) 连接到各种 Debian 或 Debian 衍生服务器。我的sshsshd来自 OpenSSH。

\n
\n

手术

\n

sshe(除非它决定回退到sshssh -t)运行ssh两次:

\n
    \n
  1. ssh -M \xe2\x80\xa6 -T \xe2\x80\xa6是一个主连接,不在远程端分配 tty。它在那里运行的 shell 代码通过 stdout 和execs 向长期运行的sleep(大约 68 年)报告其 PID。该进程的标准文件描述符将由另一个进程使用。

    \n

    从主站报告的 PIDssh由 获取read。此后,主站的标准输出ssh将转到后台cat,其唯一目的是将其中继到 的(本地)标准输出sshe

    \n
  2. \n
  3. 稍后ssh \xe2\x80\xa6 -t \xe2\x80\xa6是一个从属连接,它在远程端分配 tty。已经从主连接知道远程 PID 后,它会设置重定向,因此提供给ssheas 的代码可以在远程端command使用单独的 stdin、stdout、stderr(通过主ssh连接)和 tty(通过从连接)。ssh从机ssh不使用原始的 stdin 或 stdout sshe,而是使用本地/dev/tty

    \n
  4. \n
\n

这个想法类似于这个答案(已经链接到问题中)的作用。链接答案中的代码运行ssh(隐式ssh -T)两次以提供额外的描述符。我的脚本运行ssh -Tssh -t提供标准描述符和 tty。并且它使用了主从功能ssh,因此它会进行一次身份验证(例如要求密码)。

\n

如果本地 stdin、stdout、stderr 都不是本地 tty,则数据流向如下:

\n
    \n
  • 本地 stdin 转到 master ssh,没有其他本地进程从脚本的 stdin 读取。通过从(远程)读取,/proc/nnnn/fd/0远程进程可以访问本地标准输入。从属ssh连接预先重定向到command,因此远程端的 shell 用作/proc/nnnn/fd/0其标准输入。

    \n
  • \n
  • 类似地,远程端的 shell 用作/proc/nnnn/fd/1其标准输出。无论那里发生什么,都会从当地的主人那里出来ssh。这是在主机ssh从它运行的(远程)shell 代码中检索到正确的 PID 之后。PID 被消耗,随后的任何数据都会通过后台read发送到原始标准输出。sshecat

    \n
  • \n
  • 类似地,远程端的 shell 使用/proc/nnnn/fd/2它的 stderr。该流将直接从本地 master 发送ssh到 的 stderr sshe。脚本生成的一些本地进程使用脚本的 stderr 作为它们的 stderr,因此如果您sshe \xe2\x80\xa6 2>error.log这样做,那么日志也将包含它们的错误消息。特别期待Shared connection to server closed.。这类似于ssh -T \xe2\x80\xa6 2>error.log日志从远程命令和ssh自身收集消息的情况。我认为可以制作一个变体,sshe通过与另一个的 stdout 关联的通道从远程命令传递 stderrssh;在这种情况下,人们将能够区分远程 stderr 和本地工具生成的诊断消息。但脚本并没有这样做。

    \n
  • \n
  • 本地 tty 可用于 master ssh(如果需要询问密码),然后可用于 Slave ssh。(坦率地说,脚本使用的更多本地工具可以访问 local /dev/tty,但他们只是不使用它。)从属设备ssh -t用作/dev/tty其标准输入和标准输出。通过这种方式,它可以连接本地和远程/dev/tty,尽管有其他重定向(例如ssh -t在没有重定向的终端中运行)。远程进程从它们读取的内容/dev/tty将得到本地从属ssh从本地读取的内容/dev/tty。远程进程写入它们/dev/tty将使本地从站ssh写入本地/dev/tty

    \n
  • \n
\n

如果本地 stdin、stdout 或 stderr 是本地 tty,则其在远程端(由command从机远程运行ssh)的相应对应部分将不会被重定向到/proc/nnnn/fd/N,并且将保持与远程 tty 的连接。不管怎样,它都会到达本地终端。重点是它不应该绕过远程 tty。其原因很快就会清楚。

\n

很少有本地和远程重定向不一定需要sshe工作。这是因为我的其他实验性的东西。我决定保留额外的重定向,以防万一它们对鞋底来说sshe比我记忆中更重要。

\n
\n

障碍和注意事项

\n

整个概念并不像看起来那么容易。tty 可以处理您输入的内容(例如,转换^M^J)以及将要打印的内容(例如,如果我cat在终端上使用 *nix 行结尾的文件,则每个换行符将像carriage_return + newline 一样工作)。调用stty -a即可查看大量设置。

\n

这就是为什么在处理任意二进制数据时不需要 tty 的原因。而且你在互动时确实需要它。

\n

进程可以配置 tty 以满足它们的需要。查看

\n

当您ssh以某种方式在服务器上分配 tty 时,那里的进程会将其视为它们的 tty。如果他们需要配置他们的 tty,他们将配置他们在服务器上看到的 tty。他们没有办法直接配置本地tty ssh。所有(?)“烹饪”都是由远程 tty 完成的,并ssh配置本地 tty,因此它不会干扰。

\n

sshe这就是在重定向最终应连接到本地 tty 的远程描述符时不应绕过远程 tty 的原因。如果远程 tty 被绕过,那么将没有实体来“烹饪”流。通过将 stdin、stdout 或 stderr 连接sshe到本地 tty,您表明您希望它“煮熟”。这sshe类似于ssh \xe2\x80\xa6 command在交互式 shell 中运行,即所有标准流都由本地 tty“烹调”的情况(注意这ssh就像ssh -T并且它不会将本地终端置于“原始”模式)。

\n

所以sshe“煮”你显然想“煮”的东西。当您执行以下操作时会出现问题:

\n
sshe \xe2\x80\xa6 command | whatever\n
Run Code Online (Sandbox Code Playgroud)\n

数据将从远程流向command本地,whatever而不会被“煮熟”(正如人们所期望的那样ssh \xe2\x80\xa6 command | whatever),但 的输出whatever不会在本地“煮熟”。sshe可以重新配置本地 tty,使其“烹饪”,但如果command碰巧打印到其 tty(即打印到可能或可能不“烹饪”的远程 tty,具体取决于其设置),则本地 tty 不应“烹饪”。

\n

sshe不尝试解决这个问题。它基本上是为了支持最终输出到终端以外的其他地方(例如到常规文件或块设备)的情况。在这个问题上,下面的代码更好:

\n
sshe \xe2\x80\xa6 command | whatever >some_file\n
Run Code Online (Sandbox Code Playgroud)\n

尽管 stderr 不会whatever被“煮熟”。预计诊断消息看起来很奇怪。请注意,您可以将它们重定向到一个文件(或另一个将“烹饪”它们的本地 tty)。

\n

输入端的情况更糟。如果另一个本地进程尝试从本地 tty 读取数据,它不仅会读取原始数据,还会竞争输入sshe。这是两个进程从同一终端读取数据的常见问题。

\n

总结一下:构建一个本地命令(管道),因此只有(一个)sshe想要从本地tty读取;不要让除sshe打印之外的工具打印到本地 tty,除非您可以忍受“原始”输出。

\n

我开发了sshe能够传递或处理二进制数据的能力。就我而言,几乎不需要从本地 tty 读取数据或将数据写入本地 tty。我可以忍受来自本地工具的诊断消息不够“成熟”。作为回报,我可以像本地一样sshe使用远程。sudo

\n
\n

例子

\n
    \n
  • 读取或写入需要的远程块设备sudo

    \n
      \n
    • 阅读:

      \n
      sshe user@server \'sudo cat /dev/sdx1\' >local_file\n# or\nsshe user@server \'sudo pv  /dev/sdx1\' >local_file\n
      Run Code Online (Sandbox Code Playgroud)\n
    • \n
    • 写作:

      \n
      <local_file sshe user@server \'sudo tee /dev/sdx1 >/dev/null\'\n
      Run Code Online (Sandbox Code Playgroud)\n

      在我的测试中,本地pv显然不介意本地 tty 是“原始”的;或者更确切地说,它强加的配置和强加的配置sshe并不矛盾,并且哪个工具首先配置本地 tty 并不重要。所以这似乎有效:

      \n
      pv local_file | sshe user@server \'sudo tee /dev/sdx1 >/dev/null\'\n
      Run Code Online (Sandbox Code Playgroud)\n

      请注意,如果设置受到pv干扰sshe,那么您可能无法向遥控器提供密码sudo。如果设置受到sshe干扰pv,那么pv打印到终端的内容可能看起来被破坏了。即使在这些假设的情况下,内容local_file也会一字不差地传到远程/dev/sdx1

      \n
    • \n
    \n
  • \n
  • 本地sudo和远程sudo一起。

    \n

    如果 localsudo会询问你的密码,sudo \xe2\x80\xa6 | sshe \xe2\x80\xa6 \'sudo \xe2\x80\xa6\'或者sshe \xe2\x80\xa6 \'sudo \xe2\x80\xa6\' | sudo \xe2\x80\xa6不是一个好主意,因为 localsudosshe都会同时从本地 tty 读取。完全本地sudo \xe2\x80\xa6 | sudo \xe2\x80\xa6工作是因为sudo实现了锁定机制,因此两个本地sudo不会同时与同一终端交互。这不适用于本地和远程的混合sudo

    \n

    希望您当地sudo 允许超时。如果是这样,请sudo -v提前调用以提供本地密码(如果需要),而不会受到干扰;然后用管道:

    \n
      \n
    • 从远程设备复制到本地设备:

      \n
      sudo -v   # input local password if needed\nsshe user@server \'sudo cat /dev/sdx1\' | sudo tee /dev/sdy1 >/dev/null\n
      Run Code Online (Sandbox Code Playgroud)\n
    • \n
    • 从本地设备复制到远程设备:

      \n
      sudo -v   # input local password if needed\nsudo cat /dev/sdy1 | sshe user@server \'sudo tee /dev/sdx1 >/dev/null\'\n
      Run Code Online (Sandbox Code Playgroud)\n
    • \n
    \n
  • \n
  • 正是问题所要求的。

    \n\n
  • \n
\n
\n

最后说明

\n

我曾经认为通过隧道将 stdin 传输到 stdin、将 stdout 传输到 stdout、将 stderr 传输到 stderr以及 /dev/tty传输到 stderr/dev/tty是微不足道的。我曾经想知道为什么ssh不提供一个选项(类似于-t)来执行此操作。现在我知道事情没那么简单;我怀疑也许我还遗漏了一些东西。

\n


归档时间:

查看次数:

3420 次

最近记录:

2 年,11 月 前