web-dev-qa-db-ja.com

OpenSSHRemoteForwardに動的に割り当てられたポートを決定します

質問(TL; DR)

リモート転送用にポートを動的に割り当てる場合(別名_-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_SERVER_と_Pulse_SERVER_を正しく設定することです。

ポート転送を頻繁に使用するため、動的ポート割り当てが必要です。静的ポート割り当てはオプションではありません。

OpenSSHには、リモート転送のバインドポートとして_0_を指定することにより、リモートサーバーで動的ポートを割り当てるメカニズムがあります(_-R_オプション)。次のコマンドを使用することにより、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にはポート転送に関する情報を取得する手段がないようです。

リモートホストで_CUPS_SERVER_と_Pulse_SERVER_を適切に設定するために、その情報をフェッチしてシェルスクリプトに書き込むにはどうすればよいですか?


行き止まり

私が見つけた唯一の簡単なことは、ログから情報を読み取ることができるまでsshdの詳細度を上げることでした。その情報は、root以外のユーザーがアクセスできるようにするために賢明な情報よりもはるかに多くの情報を開示しているため、これは実行可能ではありません。

OpenSSHにパッチを適用して、内部構造体_permitted_opens_のニース表現を出力する追加のエスケープシーケンスをサポートすることを考えていましたが、それでもいいのに、サーバーからクライアントエスケープシーケンスにアクセスするスクリプトを実行できません側。


より良い方法があるはずです

次のアプローチは非常に不安定に見え、ユーザーごとに1つのそのようなSSHセッションに制限されています。ただし、少なくとも2つのそのようなセッションと、他のユーザーが同時に必要です。しかし、私は試しました...

星が正しく配置され、1羽か2羽の鶏を犠牲にした場合、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/'

  • 最初のセットにはあるが2番目のセットにはないすべてのポートは、転送ポートである可能性が高く、実際にセットを差し引くと、_41273_、_55710_および_6010_;が得られます。カップ、それぞれパルスとX。

  • _6010_は、DISPLAYを使用してXポートとして識別されます。

  • _41273_は_lpstat -h localhost:41273 -a_を返すため、_0_はcupsポートです。
  • _55710_は_pactl -s localhost:55710 stat_を返すため、_0_はPulseポートです。 (クライアントのホスト名も表示されます!)

(設定された減算を行うには、I _sort -u_を実行し、上記のコマンドラインからの出力を保存し、commを使用して減算を実行します。)

Pulseaudioを使用すると、クライアントを識別できます。これは、すべての目的と目的で、分離が必要なSSHセッションを分離するためのアンカーとして機能する場合があります。ただし、_41273_、_55710_、および_6010_を同じsshdプロセスに関連付ける方法が見つかりませんでした。 netstatはその情報をroot以外のユーザーに開示しません。 _-_を読みたい_PID/Program name_列に_2339/54_しか表示されません(この特定のインスタンスでは)。とても近い...

13
Bananguin

.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
1
ToxeH

2つ取ってください(サーバー側から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. sshstderrをファイルにリダイレクトしながら、ファイル名をenv変数としてサーバーに渡します
  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

:もちろん、上記のコードは十分にテストされておらず、あらゆる種類のバグ、コピー/貼り付けエラーなどを含む可能性があります。それを使用する人は、それをよりよく理解することもできます自己責任で使用してください!ローカルホスト接続のみを使用してテストしましたが、テスト環境で機能しました。 YMMV。

1
hyde

ローカルクライアントでパイプを作成し、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はstdinでパイプが開くのを待つのをブロックするようです。 grepは何かを受け取るまで書き込み用のパイプを開かないため、これは基本的にデッドロックになります。 catはこれと同じ動作をしているようには見えず、書き込み用のパイプを開き、sshが接続を開くことをすぐに許可します。

これはリモート側でも同じ問題であり、なぜstdinからのgrepではなく1行ずつreadが出力されます。目的

コマンドやバナーテキストなどを指定せずにリモートシェルで実行されるため、sshのstderrを~/getsshport.shなどのコマンドにパイプすることをお勧めします。

0
JesseMcL

これはトリッキーなもので、_SSH_CONNECTION_またはDISPLAYの行に沿った追加のサーバー側処理は素晴らしいでしょうが、追加するのは簡単ではありません。問題の一部はsshクライアントはローカル宛先を知っており、(サーバーへの)要求パケットにはリモートアドレスとポートのみが含まれています。

ここでの他の回答には、このクライアント側をキャプチャしてサーバーに送信するためのさまざまな解決策があります。これは正直に言うとそれほどきれいではない別のアプローチですが、少なくともこの醜いパーティーはクライアント側にとどまっています;-)

  • クライアント側、SendEnvを追加/修正して、sshを介していくつかの環境変数をネイティブに送信できるようにする(おそらくデフォルトではない)
  • サーバー側で、AcceptEnvを追加/修正して同じものを受け入れます(おそらくデフォルトでは有効になっていません)
  • 動的にロードされたライブラリを使用してssh client 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_(ルート/管理上の変更が必要)

_AcceptEnv LC_* SSH_RFWD_*
_

このアプローチはLinuxクライアントで機能し、サーバー上で特別なことは何も必要ありません。いくつかのマイナーな調整を加えた他の* nixでも機能するはずです。少なくともOpenSSH5.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
_

警告:

  • コードにエラーチェックはあまりありません
  • 環境の変更mayスレッド関連の問題を引き起こし、PAMはスレッドを使用します。問題は予想されませんが、テストはしていません。
  • sshは現在、* local:port:remote:port *という形式の完全な転送を明確に記録していません(必要に応じて、_debug1_を含む_ssh -v_メッセージの解析がさらに必要になります)。ユースケースではこれは必要ありません

奇妙なことに、OpenSSHにはポート転送に関する情報を取得する手段がないようです。

エスケープ_~#_を使用して(部分的に)これをインタラクティブに行うことができます。奇妙なことに、実装はリッスンしているチャネルをスキップし、開いている(つまり、TCP ESTABLISHED)チャネルのみをリストします。いずれの場合も、有用なフィールドを出力しません。 _channels.c_channel_open_message()を参照してください。

その関数にパッチを当てて_SSH_CHANNEL_PORT_LISTENER_スロットの詳細を出力することはできますが、それはローカル転送のみを取得します(channelsは実際のforwardsと同じではありません) 。または、パッチを適用して、グローバルoptions構造体から2つの転送テーブルをダンプすることもできます。

_#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の場合、もう1つのオプションがあります。これは、リモートではなくローカル転送用ですが、私が一般的に使用するオプションです。 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序数ニーモニック)は、一日の終わりに混乱を解くのに役立ちます。

0
mr.spuratic