問題(TL;DR)
當為遠端轉送動態分配連接埠(也稱為-R
選項)時,遠端電腦上的腳本(例如源自.bashrc
)如何確定 OpenSSH 選擇了哪些連接埠?
背景
我使用 OpenSSH(兩端)連接到我與多個其他用戶共享的中央伺服器。對於我的遠端會話(目前),我想轉送 X、cups 和pulseaudio。
最簡單的是使用該-X
選項轉送 X。分配的 X 位址儲存在環境變數中DISPLAY
,在大多數情況下,我可以從中確定對應的 TCP 連接埠。但我幾乎不需要這樣做,因為 Xlib 尊重DISPLAY
.
我需要一個類似的機制用於杯子和脈衝音頻。這兩種服務的基礎知識分別以環境變數CUPS_SERVER
和 的形式存在PULSE_SERVER
。以下是使用範例:
ssh -X -R12345:localhost:631 -R54321:localhost:4713 datserver
export CUPS_SERVER=localhost:12345
lowriter #and I can print using my local printer
lpr -P default -o Duplex=DuplexNoTumble minutes.pdf #printing through the tunnel
lpr -H localhost:631 -P default -o Duplex=DuplexNoTumble minutes.pdf #printing remotely
mpg123 mp3s/van_halen/jump.mp3 #annoy co-workers
PULSE_SERVER=localhost:54321 mpg123 mp3s/van_halen/jump.mp3 #listen to music through the tunnel
問題在於設定CUPS_SERVER
和PULSE_SERVER
正確。
我們經常使用連接埠轉發,因此我需要動態連接埠分配。靜態連接埠分配不是一種選擇。
OpenSSH 有一種在遠端伺服器上動態分配連接埠的機制,透過指定0
為遠端轉送的綁定連接埠(-R
選項)。透過使用以下命令,OpenSSH將為cups和脈衝轉送動態分配連接埠。
ssh -X -R0:localhost:631 -R0:localhost:4713 datserver
當我使用該命令時,ssh
會將以下內容列印到STDERR
:
Allocated port 55710 for remote forward to 127.0.0.1:4713
Allocated port 41273 for remote forward to 127.0.0.1:631
有我想要的資料!最終我想生成:
export CUPS_SERVER=localhost:41273
export PULSE_SERVER=localhost:55710
但是,「分配的連接埠...」訊息是在我的本機電腦上建立的,並發送到STDERR
我無法在遠端電腦上存取的。奇怪的是 OpenSSH 似乎沒有辦法檢索有關連接埠轉送的資訊。
如何獲取該資訊並將其放入 shell 腳本中以進行充分設定CUPS_SERVER
並PULSE_SERVER
放在遠端主機上?
死胡同
我能找到的唯一簡單的事情是增加冗長的信息,sshd
直到可以從日誌中讀取該信息。這是不可行的,因為該資訊披露的資訊遠遠超過非 root 用戶可以存取的合理資訊。
我正在考慮修補 OpenSSH 以支援額外的轉義序列,該序列可以列印內部 struct 的良好表示permitted_opens
,但即使這是我想要的,我仍然無法編寫從伺服器端存取客戶端轉義序列的腳本。
一定會有更好的辦法
以下方法似乎非常不穩定,並且僅限於每個使用者一個此類 SSH 會話。但是,我需要至少兩個並發的此類會話,而其他用戶則需要更多。但我嘗試過...
當星星正確對齊時,犧牲一兩隻雞,我可以濫用這樣一個事實:它sshd
不是以我的用戶身份啟動,而是在成功登入後放棄特權,以執行以下操作:
取得屬於我的使用者的所有偵聽套接字的連接埠號碼列表
netstat -tlpen | grep ${UID} | sed -e 's/^.*:\([0-9]\+\) .*$/\1/'
取得屬於我的使用者啟動的程序的所有偵聽套接字的連接埠號碼列表
lsof -u ${UID} 2>/dev/null | grep LISTEN | sed -e 's/.*:\([0-9]\+\) (LISTEN).*$/\1/'
第一個集合中但不在第二個集合中的所有端口很可能是我的轉發端口,並且實際上減去集合會產生
41273
、55710
和6010
;分別是杯子、脈衝和X。6010
使用 標識為 X 連接埠DISPLAY
。41273
是 cups 端口,因為lpstat -h localhost:41273 -a
返回0
。55710
是脈衝端口,因為pactl -s localhost:55710 stat
返回0
。 (它甚至列印我客戶端的主機名稱!)
(要進行設定減法,我sort -u
儲存上述命令列的輸出並用於comm
進行減法。)
Pulseaudio 讓我可以識別客戶端,並且出於所有意圖和目的,這可以作為分離需要分離的 SSH 會話的錨點。但是,我還沒有找到一種方法將41273
,55710
和6010
綁定到同一sshd
進程。netstat
不會向非 root 用戶透露該資訊。我只在我想閱讀的列-
中得到一個(在這個特定的例子中)。很近 ...PID/Program name
2339/54
答案1
不幸的是,我之前沒有找到你的問題,但我剛剛從 kamil-maciorowski 那裡得到了一個非常好的答案:
https://unix.stackexchange.com/a/584505/251179
總之,首先建立主連接,將其保留在後台,然後發出第二個命令,-O *ctl_cmd*
設定forward
為請求/設定連接埠轉送:
ssh -fNMS /path/to/socket user@server
port="$(ssh -S /path/to/socket -O forward -R 0:localhost:22 placeholder)"
這將為您提供$port
本機電腦和後台連線。
然後就可以$port
在本地使用;或者ssh
再次使用在遠端伺服器上執行命令,您可以在其中使用相同的控制套接字。
至於標誌,這些是:
- -F= 請求 ssh 進入後台。
- -N= 不執行遠端命令。
- -M= 將客戶端置於「主」模式,以實現連線共享。
- -S= 用於連接共享的控制套接字的位置。
- -O= 控制活動連線重複使用主程序。
就我而言,我添加了更多內容來繼續檢查連接:
#!/bin/bash
#--------------------------------------------------
# Setup
#--------------------------------------------------
set -u;
tunnel_user="user";
tunnel_host="1.1.1.1";
local_port="22";
local_name="my-name";
path_key="$HOME/.ssh/tunnel_ed25519";
path_lock="/tmp/tunnel.${tunnel_host}.pid"
path_port="/tmp/tunnel.${tunnel_host}.port"
path_log="/tmp/tunnel.${tunnel_host}.log"
path_socket="/tmp/tunnel.${tunnel_host}.socket"
#--------------------------------------------------
# Key file
#--------------------------------------------------
if [ ! -f "${path_key}" ]; then
ssh-keygen -q -t ed25519 -f "${path_key}" -N "";
/usr/local/bin/tunnel-client-key.sh
# Sends the public key to a central server, also run via cron, so it can be added to ~/.ssh/authorized_keys
# curl -s --form-string "pass=${pass}" --form-string "name=$(local_name)" -F "key=@${path_key}.pub" "https://example.com/key/";
fi
#--------------------------------------------------
# Lock
#--------------------------------------------------
if [ -e "${path_lock}" ]; then
c=$(pgrep -F "${path_lock}" 2>/dev/null | wc -l);
# MacOS 10.15.4 does not support "-c" to count processes, or the full "--pidfile" flag.
else
c=0;
fi
if [[ "${c}" -gt 0 ]]; then
if tty -s; then
echo "Already running";
fi;
exit;
fi;
echo "$$" > "${path_lock}";
#--------------------------------------------------
# Port forward
#--------------------------------------------------
retry=0;
while true; do
#--------------------------------------------------
# Log cleanup
#--------------------------------------------------
if [ ! -f "${path_log}" ]; then
touch "${path_log}";
fi
tail -n 30 "${path_log}" > "${path_log}.tmp";
mv "${path_log}.tmp" "${path_log}";
#--------------------------------------------------
# Exit old sockets
#--------------------------------------------------
if [ -S "${path_socket}" ]; then
echo "$(date) : Exit" >> "${path_log}";
ssh -S "${path_socket}" -O exit placeholder;
fi
#--------------------------------------------------
# Lost lock
#--------------------------------------------------
if [ ! -e "${path_lock}" ] || ! grep -q "$$" "${path_lock}"; then
echo "$(date) : Lost Lock" >> "${path_log}";
exit;
fi
#--------------------------------------------------
# Master connection
#--------------------------------------------------
echo "$(date) : Connect ${retry}" >> "${path_log}";
ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 -o ServerAliveInterval=30 -o ExitOnForwardFailure=yes -fNTMS "${path_socket}" -i "${path_key}" "${tunnel_user}@${tunnel_host}" >> "${path_log}" 2>&1;
#--------------------------------------------------
# Setup and keep checking the port forwarding
#--------------------------------------------------
old_port=0;
while ssh -S "${path_socket}" -O check placeholder 2>/dev/null; do
new_port=$(ssh -S "${path_socket}" -O forward -R "0:localhost:${local_port}" placeholder 2>&1);
if [[ "${new_port}" -gt 0 ]]; then
retry=0;
if [[ "${new_port}" -ne "${old_port}" ]]; then
ssh -i "${path_key}" "${tunnel_user}@${tunnel_host}" "tunnel.port.sh '${new_port}' '${local_name}'" >> "${path_log}" 2>&1;
# Tell remote server what the port is, and local_name.
# Don't use socket, it used "-N"; if done, a lost connection keeps sshd running on the remote host, even with ClientAliveInterval/ClientAliveCountMax.
echo "$(date) : ${new_port}" >> "${path_log}";
echo "${new_port}" > "${path_port}";
old_port="${new_port}";
sleep 1;
else
sleep 300; # Looks good, check again in 5 minutes.
fi
else # Not a valid port number (0, empty string, number followed by an error message, etc?)
ssh -S "${path_socket}" -O exit placeholder 2>/dev/null;
fi
done
#--------------------------------------------------
# Cleanup
#--------------------------------------------------
if [ ! -f "${path_port}" ]; then
rm "${path_port}";
fi
echo "$(date) : Disconnected" >> "${path_log}";
#--------------------------------------------------
# Delay before next re-try
#--------------------------------------------------
retry=$((retry+1));
if [[ $retry -gt 10 ]]; then
sleep 180; # Too many connection failures, try again in 3 minutes
else
sleep 5;
fi
done
答案2
我透過在本機用戶端上建立一個管道,然後將 stderr 重定向到該管道來實現相同的目的,該管道也被重定向到 ssh 的輸入。它不需要多個 ssh 連線來假定可能失敗的可用已知連接埠。這樣,登入橫幅和「分配的連接埠 ### ...」文字將會重定向到遠端主機。
我在主機上有一個簡單的腳本,getsshport.sh
該腳本在遠端主機上運行,該腳本讀取重定向的輸入並解析連接埠。只要此腳本沒有結束,ssh 遠端轉送就會保持開啟。
本地端
mkfifo pipe
ssh -R "*:0:localhost:22" user@remotehost "~/getsshport.sh" 3>&1 1>&2 2>&3 < pipe | cat > pipe
3>&1 1>&2 2>&3
交換 stderr 和 stdout 是一個小技巧,這樣 stderr 就可以透過管道傳輸到 cat,並且 ssh 的所有正常輸出都顯示在 stderr 上。
遠端~/getsshport.sh
#!/bin/sh
echo "Connection from $SSH_CLIENT"
while read line
do
echo "$line" # echos everything sent back to the client
echo "$line" | sed -n "s/Allocated port \([0-9]*\) for remote forward to \(.*\)\:\([0-9]*\).*/client port \3 is on local port \1/p" >> /tmp/allocatedports
done
在透過 ssh 發送訊息之前,我確實嘗試grep
先在本地發送「已分配的連接埠」訊息,但似乎 ssh 會阻止等待管道在標準輸入上開啟。 grep 在收到某些內容之前不會打開管道進行寫入,因此這基本上會陷入死鎖。cat
但似乎沒有相同的行為,並打開管道立即寫入,允許 ssh 打開連接。
這在遠端也是同樣的問題,為什麼要read
逐行而不是僅從 stdin 進行 grep - 否則在 ssh 隧道關閉之前不會寫出“/tmp/allocatedports”,這違背了整個目的
首選將 ssh 的 stderr 透過管道傳輸到類似的命令中~/getsshport.sh
,因為如果不指定命令,則橫幅文字或管道中的任何其他內容都會在遠端 shell 上執行。
答案3
取兩個(請參閱歷史版本,該版本SCP從伺服器端來看,有點簡單),這應該可以做到。其要點是這樣的:
- 將環境變數從客戶端傳遞到伺服器,告訴伺服器如何偵測連接埠資訊何時可用,然後取得並使用它。
- 一旦連接埠資訊可用,將其從客戶端複製到伺服器,允許伺服器獲取它(在上面第 1 部分的幫助下),並使用它
首先,在遠端進行設置,您需要啟用發送環境變量sshd配置:
sudo yourfavouriteeditor /etc/ssh/sshd_config
找到帶有的行AcceptEnv
並將其添加到其中(如果還沒有,MY_PORT_FILE
則在右側部分下方添加該行)。Host
對我來說,這條線變成了這樣:
AcceptEnv LANG LC_* MY_PORT_FILE
還記得重啟一下sshd以使此生效。
此外,要使以下腳本正常運作,請mkdir ~/portfiles
在遠端執行!
然後在本地,一個腳本片段將
- 為 stderr 重定向建立暫存檔案名
- 留下後台作業以等待文件包含內容
- 在重定向時將檔案名稱作為環境變數傳遞給伺服器SSH標準錯誤到文件
- 後台作業繼續使用單獨的方法將 stderr 暫存檔案複製到伺服器端SCP
- 後台作業也會將標誌檔案複製到伺服器以指示 stderr 檔案已準備好
腳本片段:
REMOTE=$USER@datserver
PORTFILE=`mktemp /tmp/sshdataserverports-$(hostname)-XXXXX`
test -e $PORTFILE && rm -v $PORTFILE
# EMPTYFLAG servers both as empty flag file for remote side,
# and safeguard for background job termination on this side
EMPTYFLAG=$PORTFILE-empty
cp /dev/null $EMPTYFLAG
# this variable has the file name sent over ssh connection
export MY_PORT_FILE=$(basename $PORTFILE)
# background job loop to wait for the temp file to have data
( while [ -f $EMPTYFLAG -a \! -s $PORTFILE ] ; do
sleep 1 # check once per sec
done
sleep 1 # make sure temp file gets the port data
# first copy temp file, ...
scp $PORTFILE $REMOTE:portfiles/$MY_PORT_FILE
# ...then copy flag file telling temp file contents are up to date
scp $EMPTYFLAG $REMOTE:portfiles/$MY_PORT_FILE.flag
) &
# actual ssh terminal connection
ssh -X -o "SendEnv MY_PORT_FILE" -R0:localhost:631 -R0:localhost:4713 $REMOTE 2> $PORTFILE
# remove files after connection is over
rm -v $PORTFILE $EMPTYFLAG
然後是遠端的片段,適合.bashrc:
# only do this if subdir has been created and env variable set
if [ -d ~/portfiles -a "$MY_PORT_FILE" ] ; then
PORTFILE=~/portfiles/$(basename "$MY_PORT_FILE")
FLAGFILE=$PORTFILE.flag
# wait for FLAGFILE to get copied,
# after which PORTFILE should be complete
while [ \! -f "$FLAGFILE" ] ; do
echo "Waiting for $FLAGFILE..."
sleep 1
done
# use quite exact regexps and head to make this robust
export CUPS_SERVER=localhost:$(grep '^Allocated port [0-9]\+ .* localhost:631[[:space:]]*$' "$PORTFILE" | head -1 | cut -d" " -f3)
export PULSE_SERVER=localhost:$(grep '^Allocated port [0-9]\+ .* localhost:4713[[:space:]]*$' "$PORTFILE" | head -1 | cut -d" " -f3)
echo "Set CUPS_SERVER and PULSE_SERVER"
# copied files served their purpose, and can be removed right away
rm -v -- "$PORTFILE" "$FLAGFILE"
fi
筆記:上面的程式碼當然沒有經過非常徹底的測試,可能包含各種錯誤、複製貼上錯誤等。使用風險自負!我僅使用本地主機連接對其進行了測試,並且在我的測試環境中它對我有用。 YMMV。
答案4
這是一個棘手的問題,沿著SSH_CONNECTION
或 的方式進行額外的伺服器端處理DISPLAY
會很棒,但添加起來並不容易:部分問題是只有客戶ssh
端知道本地目的地,請求資料包(到伺服器)包含僅遠端地址和連接埠。
這裡的其他答案有各種不漂亮的解決方案來捕獲此客戶端並將其發送到伺服器。這是另一種方法,說實話,它並不是很漂亮,但至少這個醜陋的聚會保留在客戶端;-)
- 客戶端,新增/修改
SendEnv
,以便我們可以透過 ssh 本機發送一些環境變數(可能不是預設值) - 伺服器端,新增/修改
AcceptEnv
以接受相同的(預設可能未啟用) - 使用動態載入的庫監視
ssh
客戶端 stderr 輸出,並更新 ssh 用戶端環境在連線建立期間 - 在設定檔/登入腳本中選擇伺服器端的環境變數
這有效(幸運的是,無論如何現在),因為遠端轉送是在交換環境之前設定和記錄的(用 確認ssh -vv ...
)。動態載入的函式庫必須捕捉libc函數write()
(ssh_confirm_remote_forward()
→→→ )。在 ELF 二進位檔案中重定向或包裝函數(無需重新編譯)比對動態庫中的函數執行相同的操作要複雜幾個數量級。logit()
do_log()
write()
在客戶端.ssh/config
(或命令列-o SendEnv ...
)
Host somehost
user whatever
SendEnv SSH_RFWD_*
在伺服器上sshd_config
(需要 root/管理更改)
AcceptEnv LC_* SSH_RFWD_*
這種方法適用於 Linux 用戶端,並且不需要在伺服器上做任何特殊的事情,它應該適用於其他 *nix,只需進行一些小的調整。至少適用於 OpenSSH 5.8p1 至 7.5p1。
編譯gcc -Wall -shared -ldl -Wl,-soname,rfwd -o rfwd.so rfwd.c
調用:
LD_PRELOAD=./rfwd.so ssh -R0:127.0.0.1:4713 -R0:localhost:631 somehost
代碼:
#define _GNU_SOURCE
#include <stdio.h>
#include <dlfcn.h>
#include <string.h>
#include <stdlib.h>
// gcc -Wall -shared -ldl -Wl,-soname,rfwd -o rfwd.so rfwd.c
#define DEBUG 0
#define dfprintf(fmt, ...) \
do { if (DEBUG) fprintf(stderr, "[%14s#%04d:%8s()] " fmt, \
__FILE__, __LINE__, __func__,##__VA_ARGS__); } while (0)
typedef ssize_t write_fp(int fd, const void *buf, size_t count);
static write_fp *real_write;
void myinit(void) __attribute__((constructor));
void myinit(void)
{
void *dl;
dfprintf("It's alive!\n");
if ((dl=dlopen(NULL,RTLD_NOW))) {
real_write=dlsym(RTLD_NEXT,"write");
if (!real_write) dfprintf("error: %s\n",dlerror());
dfprintf("found %p write()\n", (void *)real_write);
} else {
dfprintf(stderr,"dlopen() failed\n");
}
}
ssize_t write(int fd, const void *buf, size_t count)
{
static int nenv=0;
// debug1: Remote connections from 192.168.0.1:0 forwarded to local address 127.0.0.1:1000
// Allocated port 44284 for remote forward to 127.0.0.1:1000
// debug1: All remote forwarding requests processed
if ( (fd==2) && (!strncmp(buf,"Allocated port ",15)) ) {
char envbuf1[256],envbuf2[256];
unsigned int rport;
char lspec[256];
int rc;
rc=sscanf(buf,"Allocated port %u for remote forward to %256s",
&rport,lspec);
if ( (rc==2) && (nenv<32) ) {
snprintf(envbuf1,sizeof(envbuf1),"SSH_RFWD_%i",nenv++);
snprintf(envbuf2,sizeof(envbuf2),"%u %s",rport,lspec);
setenv(envbuf1,envbuf2,1);
dfprintf("setenv(%s,%s,1)\n",envbuf1,envbuf2);
}
}
return real_write(fd,buf,count);
}
(有一些與使用此方法的符號版本控制相關的 glibc 熊陷阱,但write()
不存在此問題。)
如果您有勇氣,您可以獲得setenv()
相關程式碼並將其修補到ssh.c
ssh_confirm_remote_forward()
回調函數中。
這會設定名為 的環境變量SSH_RFWD_nnn
,在您的設定檔中檢查這些變量,例如bash
for fwd in ${!SSH_RFWD_*}; do
IFS=" :" read lport rip rport <<< ${!fwd}
[[ $rport -eq "631" ]] && export CUPS_SERVER=localhost:$lport
# ...
done
注意事項:
- 程式碼中沒有太多錯誤檢查
- 改變環境可能導致與線程相關的問題,PAM 使用線程,我預計不會出現問題,但我還沒有測試過
ssh
目前沒有明確記錄表單 * local:port:remote:port* 的完整轉送(如果需要,需要進一步解析debug1
訊息ssh -v
),但您的用例不需要這個
奇怪的是 OpenSSH 似乎沒有辦法檢索有關連接埠轉送的資訊。
您可以(部分)與 escape 互動地執行此操作~#
,奇怪的是,該實作會跳過正在偵聽的通道,它只列出開啟的(即TCP ESTABLISHED)通道,並且在任何情況下都不會列印有用的字段。看channels.c
channel_open_message()
您可以修補該函數以列印插槽的詳細信息SSH_CHANNEL_PORT_LISTENER
,但這只能獲得本地轉發(頻道與實際情況不同前鋒)。或者,您可以修補它以從全域結構中轉儲兩個轉發表options
:
#include "readconf.h"
Options options; /* extern */
[...]
snprintf(buf, sizeof buf, "Local forwards:\r\n");
buffer_append(&buffer, buf, strlen(buf));
for (i = 0; i < options.num_local_forwards; i++) {
snprintf(buf, sizeof buf, " #%d listen %s:%d connect %s:%d\r\n",i,
options.local_forwards[i].listen_host,
options.local_forwards[i].listen_port,
options.local_forwards[i].connect_host,
options.local_forwards[i].connect_port);
buffer_append(&buffer, buf, strlen(buf));
}
snprintf(buf, sizeof buf, "Remote forwards:\r\n");
buffer_append(&buffer, buf, strlen(buf));
for (i = 0; i < options.num_remote_forwards; i++) {
snprintf(buf, sizeof buf, " #%d listen %s:%d connect %s:%d\r\n",i,
options.remote_forwards[i].listen_host,
options.remote_forwards[i].listen_port,
options.remote_forwards[i].connect_host,
options.remote_forwards[i].connect_port);
buffer_append(&buffer, buf, strlen(buf));
}
這工作正常,儘管它不是一個「編程」解決方案,但需要注意的是,當您動態添加/刪除轉發時,客戶端程式碼不會(但在原始程式碼中標記為 XXX)更新清單 ( ~C
)
如果伺服器是 Linux,那麼您還有一種選擇,這是我通常使用的一種,不過用於本地轉送而不是遠端轉送。lo
是127.0.0.1/8,在Linux上你可以透明綁定到127/8中的任何地址,因此如果您使用唯一的 127.xyz 位址,則可以使用固定端口,例如:
mr@local:~$ ssh -R127.53.50.55:44284:127.0.0.1:44284 remote
[...]
mr@remote:~$ ss -atnp src 127.53.50.55
State Recv-Q Send-Q Local Address:Port Peer Address:Port
LISTEN 0 128 127.53.50.55:44284 *:*
這取決於綁定特權連接埠 <1024,OpenSSH 不支援 Linux 功能,並且在大多數平台上都有硬編碼的 UID 檢查。
明智地選擇八位元組(在我的例子中是 ASCII 序數助記符)有助於在一天結束時理清混亂的局面。