问题(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 序数助记符)有助于在一天结束时理清混乱。