Ssh

具有單獨的標準輸入、標準輸出、標準錯誤和 tty 的 ssh

  • October 4, 2021

問題

考慮這樣的命令:

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

wheretool是任意的,ssh是一個包裝器或一些ssh-like-contraption允許上述工作的包裝器。定期ssh它不起作用。

sudo在這裡使用過,但這只是需要 tty 的命令的範例。我想要一個通用的解決方案,而不是特定於sudo.


研究:原因

使用正常ssh它不起作用,因為:

  • sudo需要 tty 來詢問密碼(或根本工作),所以我需要ssh -t;實際上在這種情況下我需要ssh -tt.
  • 另一方面,ssh -tt將從中sudo讀取密碼binary_input。我想通過我的本地 tty 提供密碼。即使sudo配置為在沒有密碼的情況下工作,或者如果我將密碼注入到 tty 中binary_inputssh -tt也會從遠端 tty 讀取並寫入輸出sudo錯誤和提示到遠端 tty。我不僅無法在本地區分輸出和錯誤/提示。所有的流都將由遠端 tty 處理,這將破壞二進制數據。tool

研究:與有效命令的比較

  • 該本地命令是參考點。假設它成功處理了一些二進制數據:
<binary_input tool >binary_output
<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 在一起通常不能是透明的。找到一種使它們透明的方法是這個問題的要點。


研究:類似問題

我發現了幾個類似的問題:

這個問題只關心標準輸出。現有的答案(來自問題的作者)建議sudo -S使用標準輸入。我真的不想改變我的binary_input. 我希望有一個不特定於sudo.

這集中在傳遞Ctrl+c和背景是 GNU parallel。僅在沒有遠端 tty 的情況下使Ctrl+c工作的解決方法對我來說是不夠的。

這是一個好的開始(我認為尤其是這個答案)。但是在這裡我想強調 tty 的必要性。我想要一個自動化的解決方案,並允許我使用遠端sudo(或其他),就好像它是本地的一樣。


我的明確問題

在以下命令中:

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

requires-tty是需要 tty 但處理從其標準輸入到其標準輸出的二進制數據的程式碼的佔位符。看來我需要ssh -tt,否則requires-tty將無法工作;同時我不能使用ssh -tt,否則二進制數據會被破壞。我怎樣才能以方便的方式解決這個問題?

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

我想理想的(?)解決方案將是一個腳本/工具,它ssh在上述呼叫中替換並且可以正常工作。它應該(?)將遠端標準輸入、標準輸出和標準錯誤分別連接到其本地對應項,並將遠端 tty 連接到本地 tty。

如果可能的話,我更喜歡不需要任何伺服器端配套程序的客戶端解決方案。

腳本

我是這個問題的作者,這是我嘗試建構一個解決問題的腳本。該腳本旨在在客戶端工作,它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"; }

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

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

# 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

where表示如果執行檔是ssh. 沒有必要把-tnor -tt(nor -T) 放在這裡。該腳本假定您希望 tty 在遠端端(否則只需使用ssh)。該腳本期望本地 stdin、stdout、stderr 中的至少一個被重定向離開本地 tty。ssh -t如果一切都連接到本地 tty ,腳本將回退到。

重要的事情:

  • command是您要在伺服器上執行的 shell 程式碼。它必須是單個參數,是 的最後一個參數sshe。不能省略。
  • hostnameuser@hostname必須是倒數第二個參數。不能省略。

在內部,腳本需要知道command在前面添加一些程式碼。它需要知道[user@]hostname,因為它使用了兩次。該腳本僅分別選擇最後一個和倒數第二個參數,因此存在上述限制。

並非每個有效ssh的呼叫都可以sshe通過替換sshsshe. 但我相信任何ssh執行程式碼的有效呼叫(而不是生成互動式 shell)都可以重新排列為有效sshe命令。例子:

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 標準的外殼。
  • 靜默啟動腳本(比較SCP 在不起作用echo``.bashrcsshe情況類似)。

在我的測試中,我成功地從 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。在此之後,master 的 stdoutssh將進入後台cat,其唯一目的是將其中繼到sshe. 2. 後來ssh … -t …是一個從屬連接,它確實在遠端端分配 tty。已經從主連接知道遠端 PID,它設置重定向,因此提供給ssheas 的程式碼command可以在遠端端使用單獨的 stdin、stdout、stderr(通過主ssh連接)和 tty(通過從ssh連接)。從站ssh不使用原始的標準輸入或標準輸出sshe,而是使用本地的/dev/tty

這個想法類似於這個答案(已經在問題中連結到)。連結答案中的程式碼執行ssh(隱式ssh -T)兩次以提供額外的描述符。我的腳本執行ssh -Tssh -t提供標準描述符和 tty。並且它使用 的主從功能ssh,因此它只進行一次身份驗證(例如要求輸入密碼)。

如果本地 stdin、stdout、stderr 都不是本地 tty,那麼這就是數據流動的方式:

  • 本地標準輸入到 master ssh,沒有其他本地程序從腳本的標準輸入中讀取。通過從(遠端)讀取/proc/nnnn/fd/0遠端程序可以訪問本地標準輸入。從ssh連接預先重定向到command,因此遠端端的 shell/proc/nnnn/fd/0用作其標準輸入。
  • 類似地,遠端端的 shell/proc/nnnn/fd/1用作其標準輸出。無論去那裡,都會從當地的主人那裡出來ssh。這是在主機ssh從它執行的(遠端)shell 程式碼中檢索到正確的 PID 之後。PID 被消耗,隨後的任何數據都通過後台read進入原始標準輸出。sshe``cat
  • 類似地,遠端端的 shell/proc/nnnn/fd/2用作它的標準錯誤。流將直接從本地 mastersshsshe. 腳本生成的一些本地程序使用腳本的 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從 slave 遠端執行ssh)將不會被重定向到/proc/nnnn/fd/N,它將保持與遠端 tty 的連接。無論哪種方式,它都會到達本地 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

儘管 stderr fromwhatever不會被“煮熟”。預計診斷消息看起來很奇怪。請注意,您可以將它們重定向到一個文件(或另一個將“烹飪”它們的本地 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一起。

如果本地sudo要詢問您的密碼,sudo … | sshe … 'sudo …'或者sshe … 'sudo …' | sudo …不是一個好主意,因為本地sudosshe將同時從本地 tty 讀取。完全本地sudo … | sudo …工作,因為sudo實現了鎖定機制,所以兩個 localsudo不會同時與同一個終端互動。這不適用於本地和遠端sudo的混合。

希望您的本地sudo 允許 timeout。如果是這樣,請事先呼叫sudo -v以提供本地密碼(如果需要)而不受干擾;然後用管道:

  • 從遠端設備複製到本地設備:
sudo -v   # input local password if needed
sshe user@server 'sudo cat /dev/sdx1' | sudo tee /dev/sdy1 >/dev/null
從本地設備複製到遠端設備:
do -v   # input local password if needed
do cat /dev/sdy1 | sshe user@server 'sudo tee /dev/sdx1 >/dev/null'
* 正是問題所要求的。


與`sudo`:
t;binary_input sshe user@server 'sudo tool' >binary_output 2>error.log
或更一般地說:
t;binary_input sshe user@server 'requires-tty' >binary_output 2>error.log




---


### 最後說明


我曾經認為將 stdin 隧道傳輸到 stdin、stdout 到 stdout、stderr 到 stderr*和* `/dev/tty`to`/dev/tty`是微不足道的。我曾經想知道為什麼`ssh`不提供一個選項(類似於`-t`)來做到這一點。現在我知道這不是那麼簡單;我懷疑*也許*我仍然缺少一些東西。

引用自:https://unix.stackexchange.com/questions/653431