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

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

问题

考虑这样的命令:

<binary_input ssh user@server 'sudo tool' >binary_output 2>error.log

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

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


研究:原因

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

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

研究:与有效命令的比较

  • 该本地命令是参考点。假设它成功处理了一些二进制数据:

    <binary_input tool >binary_output
    
  • 如果我需要tool在服务器上运行,我可以这样做。即使ssh询问我的密码,这将起作用:

    <binary_input ssh user@server tool >binary_output
    

    在这种情况下ssh对于二进制数据是透明的。

  • 同样本地也sudo可以是透明的。即使sudo询问我的密码,以下命令也不会破坏数据:

    <binary_input sudo tool >binary_output
    
  • tool但是在服务器上运行sudo就麻烦了:

    <binary_input ssh user@server 'sudo tool' >binary_output
    

    在此配置中sshsudo 一起一般情况下不能做到透明。找到一种使它们透明的方法是这个问题的要点。


研究:类似问题

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


我明确提出的问题

在以下命令中:

<binary_input ssh user@server 'requires-tty' >binary_output 2>error.log

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

requires-tty可以sudo …,但我不想要特定于sudo.

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

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

答案1

脚本

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

#!/bin/sh -

# the name of the script
me="${0##*/}"

# error handling functions
scream() { printf '%s\n' >&2 "$1"; }
die()    { scream "$2"; exit "$1"; }

# initialization of variables
redir0=''
redir1=''
redir2=''
tty="/dev/$(ps -p "$$" -o tty=)"

# edge cases
[ "$tty" = '/dev/?' ] && { scream "$me: no tty detected, falling back to regular ssh"
   exec ssh "$@"; }
[ "$#" -lt 2 ] && die 1 "usage: $me [options] [user@]hostname command"

# see what needs to be redirected
exec 7>&1
if [ "$(<&0 tty 2>/dev/null)" != "$tty" ]; then redir0=y; fi
if [ "$(<&7 tty 2>/dev/null)" != "$tty" ]; then redir1=y; fi
if [ "$(<&2 tty 2>/dev/null)" != "$tty" ]; then redir2=y; fi
exec 7>&-

# edge case
[ "$redir0$redir1$redir2" ] || { scream "$me: no redirection detected, falling back to ssh -t"
   exec ssh -t "$@"; }

# command line parsing, extract two last arguments: ... host command
z="$#"
n="$z"
for arg do
   if [ "$n" -eq "$z" ]; then
      set --
   fi
   case "$n" in
      1) command="$arg"
   ;;
      2) host="$arg"
   ;;
      *)
      set -- "$@" "$arg"
   esac
   n="$(($n - 1))"
done

# prepare to clean on exit
trap 'status="$?"; rm -r "$tmpd" 2>/dev/null; trap - EXIT; exit "$status"' EXIT HUP INT QUIT PIPE TERM

# temporary directory and socket
tmpd="$(mktemp -d)"
[ "$?" -eq 0 ] || exit 1
sock="$tmpd/sock"

# main pipe: ssh master connection -> background cat
(
[ "$redir0" ] || exec 0</dev/null
# ssh master connection, it will report the remote PID of the remote shell via its stdout
ssh -M -S "$sock" "$@" -T "$host" '</dev/null echo "$$"; exec sleep 2147483647'
) | {

# read the remote PID
IFS= read -r rpid || exit 1

# background process to pass data
exec 6<&0
cat <&6 2>/dev/null &

# move original descriptors out of the way
exec </dev/tty >/dev/tty 6>&-

# prepare remote redirections
if [ "$redir0" ]; then redir0="<&6";  fi
if [ "$redir1" ]; then redir1=">&7";  fi
if [ "$redir2" ]; then redir2="2>&8"; fi

# ssh to run the command, with remote tty
ssh -S "$sock" -t "$host" "
 trap 'status=\"\$?\"; kill $rpid 2>/dev/null; trap - EXIT; exit \"\$status\"' EXIT HUP INT QUIT PIPE TERM
 exec 6</proc/$rpid/fd/0 7>/proc/$rpid/fd/1 8>/proc/$rpid/fd/2 9>/dev/tty $redir0 $redir1 $redir2 || exit 3;
 $command"
}


一般免责声明

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

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

  • 像这样的命令:

    <binary_input sshe user@server 'sudo tool' >binary_output 2>error.log
    

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

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

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


用法

sshe像这样使用:

sshe … [user@]hostname command

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

重要的事物:

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

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

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

ssh user@server -p 1234 echo foo

应重新排列为:

sshe -p 1234 user@server 'echo foo'

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

下面有一些例子。


要求、可移植性问题

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

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

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

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

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


手术

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

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

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

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

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

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

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

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

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

  • 本地 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

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

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


障碍和注意事项

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

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

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

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

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

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

sshe … command | whatever

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

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

sshe … command | whatever >some_file

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

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

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

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


例子

  • 读取或写入需要访问的远程块设备sudo

    • 阅读:

      sshe user@server 'sudo cat /dev/sdx1' >local_file
      # or
      sshe user@server 'sudo pv  /dev/sdx1' >local_file
      
    • 写作:

      <local_file sshe user@server 'sudo tee /dev/sdx1 >/dev/null'
      

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

      pv local_file | sshe user@server 'sudo tee /dev/sdx1 >/dev/null'
      

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

  • 本地sudo和远程sudo一起。

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

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

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

      sudo -v   # input local password if needed
      sshe user@server 'sudo cat /dev/sdx1' | sudo tee /dev/sdy1 >/dev/null
      
    • 从本地设备复制到远程设备:

      sudo -v   # input local password if needed
      sudo cat /dev/sdy1 | sshe user@server 'sudo tee /dev/sdx1 >/dev/null'
      
  • 正是问题所要求的。

    • sudo

      <binary_input sshe user@server 'sudo tool' >binary_output 2>error.log
      
    • 或者更一般地说:

      <binary_input sshe user@server 'requires-tty' >binary_output 2>error.log
      

最后说明

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

相关内容