确定为OpenSSH RemoteForward动态分配的端口


13

问题(TL; DR)

动态分配端口以进行远程转发(aka -R选项)时,远程计算机上的脚本(例如来自的源.bashrc)如何确定OpenSSH选择了哪些端口?


背景

我使用OpenSSH(在两端)连接到我们与多个其他用户共享的中央服务器。对于远程会话(暂时而言),我想转发X,cups和pulseaudio。

最简单的是使用-X选项转发X。分配的X地址存储在环境变量中DISPLAY,从此我可以在大多数情况下确定相应的TCP端口。但是我几乎不需要,因为Xlib非常荣幸DISPLAY

我需要类似的机制来制作杯子和Pulseaudio。两种服务的基础都以环境变量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_SERVERPULSE_SERVER正确。

我们经常使用端口转发,因此我需要动态端口分配。静态端口分配不是一种选择。

通过指定0为远程转发的绑定端口(该-R选项),OpenSSH具有在远程服务器上动态分配端口的机制。通过使用以下命令,OpenSSH将为杯子和脉冲转发动态分配端口。

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_SERVERPULSE_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/'

  • 所有端口是在第一盘,但不是在第二盘有很高的情形产生是我转发端口,确实减去套的产量 41273557106010; 杯,脉冲和X。

  • 6010使用标识为X端口DISPLAY

  • 41273是cups端口,因为lpstat -h localhost:41273 -areturn 0
  • 55710是脉冲端口,因为pactl -s localhost:55710 statreturn 0。(它甚至会打印我的客户端的主机名!)

(要执行设置的减法运算I,sort -u并存储上述命令行的输出,并用于comm进行减法运算。)

Pulseaudio让我可以识别客户端,并且出于所有目的和目的,它可以用作分离需要分离的SSH会话的锚点。但是,我还没有找到一种方法来配合41273557106010以同样的sshd过程。netstat不会将该信息透露给非root用户。我只-PID/Program name要阅读的列中得到一个2339/54(在此特定情况下)。很近 ...


首先,更准确地说是netstat不会显示您不拥有或内核空间的进程的PID。例如
布拉奇利(Bratchley)2014年

最健壮的方法是对sshd进行补丁...快速而肮脏的补丁仅是服务器从OS获得其本地端口,将端口号写入文件,用户,远程主机和港口。假设服务器知道客户端的端口,则不确定(甚至不可能)(否则该功能将已经存在)。
海德2014年

@海德:完全正确。远程服务器不知道转发的端口。它只是创建了几个监听套接字,并且数据通过ssh连接转发。它不知道本地目标端口。
Bananguin 2014年

Answers:


1

取两个(从服务器端执行scp的版本,请参阅历史记录,它要简单一些),这应该可以做到。要点是:

  1. 将环境变量从客户端传递到服务器,告诉服务器如何检测端口信息何时可用,然后获取并使用它。
  2. 一旦端口信息可用,将其从客户端复制到服务器,允许服务器获取它(在上述第1部分的帮助下)并使用它

首先,在远程端进行设置,您需要启用在sshd配置中发送环境变量:

sudo yourfavouriteeditor /etc/ssh/sshd_config

查找行AcceptEnv并将MY_PORT_FILE其添加到行中(Host如果尚未添加,则在右侧部分的下方添加行)。对我来说,这行变成了:

AcceptEnv LANG LC_* MY_PORT_FILE

还要记住重新启动sshd才能生效。

此外,要使以下脚本正常工作,请mkdir ~/portfiles在远程执行!


然后在本地,脚本片段

  1. 创建用于stderr重定向的临时文件名
  2. 留下后台作业以等待文件包含内容
  3. 将文件名作为环境变量传递给服务器,同时将ssh stderr 重定向到文件
  4. 后台作业继续使用单独的scp将stderr临时文件复制到服务器端
  5. 后台作业还将标记文件复制到服务器,以指示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

注意:上面的代码当然没有经过充分的测试,可能包含各种错误,复制粘贴错误等。任何使用它的人都应该也理解它,风险自负!我仅使用localhost连接对其进行了测试,并且在我的测试环境中对我有用。YMMV。


当然,哪一个要求我可以scp从远程端到本地端,而我不能。我也有类似的方法,但是ssh在建立连接后,我会将其包装为后台,然后通过本地将该文件从本地发送到远程scp,然后将ssh客户端拉到前台,然后在远程端运行脚本。我还没有弄清楚如何很好地脚本化本地和远程进程的后台和前台脚本。用ssh类似的远程脚本包装和集成本地客户端似乎不是一个好方法。
Bananguin 2014年

啊。我认为您应该仅将客户端scp作为背景:(while [ ... ] ; do sleep 1 ; done ; scp ... )&。然后在服务器的前台等待.bashrc(假设客户端发送正确的env变量),使文件出现。经过一些测试(稍后可能直到明天),我将在稍后更新答案。
海德2014年

@Bananguin新版本完成。似乎对我有用,因此应该适合您的用例。关于“不错的方法”,是的,但是我认为这里真的没有一个很好的方法。信息需要以某种方式传递,并且除非您同时修补ssh客户端和服务器以通过单个连接进行干净处理,否则始终都是黑客。
海德2014年

而且我正在越来越多地考虑修补openssh。似乎没什么大不了的。该信息已经可用。我只需要将其发送到服务器即可。只要服务器收到此类信息,就会将其写入~/.ssh-${PID}-forwards
Bananguin 2014年

1

适用于.bashrc的本地代码段:

#!/bin/bash

user=$1
host=$2

sshr() {
# 1. connect, get dynamic port, disconnect  
port=`echo "exit" | ssh -R '*:0:127.0.0.1:52698' -t $1 2>&1 | grep 'Allocated port' | awk '/port/ {print $3;}'`
# 2. reconnect with this port and set remote variable
cmds="ssh -R $port:127.0.0.1:52698 -t $1 bash -c \"export RMATE_PORT=$port; bash\""
($cmds)
}

sshr $user@$host

0

通过在本地客户端上创建管道,然后将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

grep在通过ssh发送消息之前,我确实先尝试了本地的“已分配端口”消息,但是ssh似乎会阻止等待管道在stdin上打开。grep在收到东西之前不会打开写入的管道,因此这基本上是死锁。cat但是似乎没有相同的行为,并立即打开管道进行写入,从而允许ssh打开连接。

这在远程端是相同的问题,为什么为什么read逐行而不是从stdin只是grep-否则在关闭ssh隧道之前不会写出`/ tmp / allocatedports',这会破坏整个目的

最好将ssh的stderr插入命令之类的命令中~/getsshport.sh,因为无需指定命令,标语文本或管道中的其他任何内容即可在远程shell上执行。


很好 我在添加renice +10 $$; exec cat之前done要节省资源。
Spongman '18

0

这是一个棘手的问题,可能会SSH_CONNECTIONDISPLAY会带来额外的服务器端处理,但这并不是一件容易的事:部分问题是只有ssh客户端知道本地目的地,请求数据包(到服务器)包含仅远程地址和端口。

此处的其他答案对于捕获此客户端并将其发送到服务器具有各种不完善的解决方案。这是另一种方法,说实话并不太漂亮,但是至少这个丑陋的聚会在客户端进行;-)

  • 客户端,添加/修改,SendEnv以便我们可以通过ssh本地发送一些环境变量(可能不是默认值)
  • 服务器端,添加/修改AcceptEnv以接受相同内容(默认情况下可能未启用)
  • ssh使用动态加载的库监视客户端stderr输出,并在建立连接期间更新ssh客户端环境
  • 在配置文件/登录脚本中获取服务器端的环境变量

这是可行的(无论如何,现在还是可以的),因为在交换环境(与确认ssh -vv ...)之前已设置并记录了远程转发。动态加载库具有捕捉write()libc函数(ssh_confirm_remote_forward()→交通logit()→交通do_log()→交通write())。重定向或包装ELF二进制文件中的函数(不进行重新编译)比对动态库中的函数进行复杂度高几个数量级。

在客户端.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源)当您添加更新列表/删除forwardings上即时(~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序数助记符)有助于消除混乱。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.