OpenSSH RemoteForwardに動的に割り当てられたポートを決定する


13

質問(TL; DR)

リモート転送(-Rオプション)のためにポートを動的に割り当てる場合、リモートマシン上のスクリプト(たとえば、からソース.bashrc)は、どのポートがOpenSSHによって選択されたかをどのように判断できますか?


バックグラウンド

私は(両端で)OpenSSHを使用して、他の複数のユーザーと共有している中央サーバーに接続します。私のリモートセッション(今のところ)では、X、cups、pulseaudioを転送したいと思います。

最も簡単なのは、-Xオプションを使用してXを転送することです。割り当てられたXアドレスは環境変数に保存され、DISPLAYそこからほとんどの場合、対応するTCPポートを特定できます。しかし、Xlibが名誉を与えられてDISPLAYいるので、私はほとんど必要としません。

cupsと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_SERVERPULSE_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番目のセットにはないすべてのポートは、転送ポートである可能性が高く、実際にセットを差し引くと 41273557106010; カップ、パルス、X。

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

  • 41273はをlpstat -h localhost:41273 -a返すため、cupsポート0です。
  • 55710はをpactl -s localhost:55710 stat返すため、パルスポート0です。(クライアントのホスト名も表示されます!)

(設定sort -u減算を実行し、上記のコマンドラインからの出力を保存し、減算commを実行するために使用します。)

Pulseaudioを使用すると、クライアントを識別できます。これは、すべての目的と目的で、分離が必要なSSHセッションを分離するためのアンカーとして機能する場合があります。しかし、私はネクタイする方法を発見していない41273557106010同じにsshdプロセス。netstat非rootユーザーにその情報を開示しません。(この特定のインスタンスでは)読み取りたい列にのみが表示さ-れます。とても近い...PID/Program name2339/54


fwiw、netstat所有していない、またはカーネル空間のプロセスのPIDを表示しないと言う方が正確です。
ブラッチリー2014

最も堅牢な方法は、sshdにパッチを適用することです...クイック&ダーティパッチは、サーバーがOSからローカルポートを取得し、ポート番号をファイルに書き込み、ユーザー、リモートホスト、および港。サーバーがクライアント側のポートを知っていると想定しますが、これは確かではありません。
ハイド

@hyde:まさに。リモートサーバーは転送されたポートを認識しません。リスニングソケットをいくつか作成するだけで、データはssh接続を介して転送されます。ローカル宛先ポートについては認識しません。
Bananguin、2014年

回答:


1

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. 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変数を送信する場合)。いくつかのテストの後で回答を更新します(おそらく明日まで時間がありません)。
ハイド

@Bananguin新しいバージョンが完了しました。私にはうまくいくようですので、あなたのユースケースに適応できるはずです。「ナイスアプローチ」についてはそうですが、ここには本当に良いアプローチがあるとは思いません。情報は何らかの方法で渡される必要があり、sshクライアントとサーバーの両方にパッチを適用して単一の接続でクリーンに実行しない限り、常にハックになります。
ハイド

そして、openshにパッチを当てることについてますます考えています。大したことではないようです。情報はすでに利用可能です。サーバーに送信するだけです。サーバーがそのような情報を受信するたびに、それを書き込みます~/.ssh-${PID}-forwards
Bananguin

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

ローカルクライアントでパイプを作成し、sderの入力にリダイレクトされるパイプにstderrをリダイレクトすることで、同じことを実現しました。失敗する可能性のある既知の空きポートを推定するために複数の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

grepsshを介して送信する前に、ローカル側の「allocated port」メッセージを最初に試みましたが、sdinがstdinでパイプが開くのを待つのをブロックしているようです。grepは何かを受け取るまで書き込み用のパイプを開かないため、これは基本的にデッドロックになります。catただし、これと同じ動作をするようには見えず、書き込み用のパイプを開いて、sshが接続を開くことをすぐに許可します。

これはリモート側と同じ問題であり、なぜreadstdinからのgrepの代わりに行ごとに行なうのか-それ以外の場合、 `/ tmp / allocatedports 'は、sshトンネルが閉じて目的全体を無効にするまで書き出されない

sshのstderrを次のようなコマンドにパイプすることをお勧めします。これ~/getsshport.shは、コマンド、バナーテキスト、またはパイプ内にある他のものはすべてリモートシェルで実行されるためです。


いいね リソースを節約するためにrenice +10 $$; exec cat前に追加しましたdone
Spongman

0

これは、の線に沿ってトリッキーな1、余分なサーバー側の処理であるSSH_CONNECTIONか、DISPLAY素晴らしいことだが、それは追加することは容易ではありません。問題の一部のみということでssh、クライアントが含まれています(サーバーへの)ローカル宛先、要求パケットを知っていますリモートアドレスとポートのみ。

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

  • クライアント側で、SendEnvsshを介してネイティブにいくつかの環境変数を送信できるように追加/修正します(おそらくデフォルトではありません)
  • サーバー側、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にはポート転送に関する情報を取得する手段がないようです。

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

その関数にパッチを適用してSSH_CHANNEL_PORT_LISTENERスロットの詳細を出力できますが、これはローカル転送のみを取得します(チャネルは実際の転送とは異なります)。または、パッチを適用して、グローバル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の通常のニーモニック)は、一日の終わりに混乱を解くのに役立ちます。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.