標準入出力を使って OpenSSH の -L や -R のようなポートフォワードを行うツールを作った

標準入出力で OpenSSH の -L や -R のようなポートフォワードを行うツールを作成しました。

ローカルへのインストールは go get github.com/ngyuki/somux で、Docker イメージは docker pull ngyuki/somux で取得できます。

使い方

主な用途は 「ローカルのポートをリモートの Docker ホスト上のコンテナのポートへ転送、およびその逆」 です。

# リモートの Docker ホストを指定
export DOCKER_HOST=example.com

# nginx を実行
docker run --name=nginx --rm -d nginx:alpine

# ローカルとコンテナの双方で somux を実行してポートフォワードを実行
# - ローカルの 8080 ポート -> コンテナの 80 ポート
# - コンテナの 9000 ポート -> ローカルの 9000 ポート
somux -v \
  -L 8080:nginx:80 \
  -R 9000:localhost:9000 \
  docker run --name=somux --rm -i --link=nginx ngyuki/somux -v &

# ローカルの 8080 からリモートのコンテナのポートに転送される
curl http://localhost:8080/

# ローカルで 9000 ポートでリッスンしてみる
nc -lk 9000 &

# コンテナの 9000 ポートからローカルのポートに転送される
echo hello | docker exec -i somux nc localhost 9000

なぜこんなものが必要か

普段 Docker Desktop で開発を行っているのですが、WSL1 + Docker Desktop(Hyper-V) という構成だったため(※1)、コンテナにホストのディレクトリをマウントしても実際には CIFS となり(※2)、I/O性能が非常に悪く、また、inotify などのネイティブのディレクトリ変更監視が効かないという問題がありました。

ので、Docker Desktop はやめてリモートの Docker ホストに unison でディレクトリを同期して開発をすることにしました。

リモートの Docker ホストを使う場合、コンテナでポートを expose するだけでは localhost ではアクセスできないし、コンテナからローカルの Windows のポートに接続しようとしても host.docker.internal は利用できず Windows 機のIPアドレスをベタに指定する必要があり、素の Docker Desktop と比べてかなり体験が損なわれます。

そこで、下記の記事でもチラッと書いていたように、ポートの転送は sshd のコンテナを追加して OpenSSH のポートフォワードを利用していました。

次のような感じです。

# docker-compose.yml
version: "3.7"
services:
  app:
    # ...snip...
  sshd:
    image: ngyuki/insecure-sshd
    networks:
      default:
        aliases:
          - host.docker.internal
# Makefile
all:
    make -j up fwd

up:
    docker-compose up

fwd:
    while ! docker-compose exec -T sshd nc -zv localhost 22; do sleep 1; done
    ssh root@localhost -C -N -g \
        -o ProxyCommand="docker-compose exec -T sshd nc -v localhost 22" \
        -o ExitOnForwardFailure=yes \
        -o StrictHostKeyChecking=no \
        -o UserKnownHostsFile=/dev/null \
        -L 8080:app:80 -R 9000:localhost:9000

これでローカルからリモートへは Docker でさえ接続できれば、あとは docker exec の標準入出力上で SSH を通して双方向にポートフォワードができます。

これでも十分でしたが「これだけのために sshd は過剰では」という気もしたので、もっとシンプルに docker exec の標準入出力を使ってポートフォワードができるツールがありそう・・と思って探したのですが、ぱっと見つからなかったので作りました。

somux を使えば次のようにできます。

# docker-compose.yml
version: "3.7"
services:
  app:
    # ...snip...
  somux:
    image: ngyuki/somux
    init: true
    networks:
      default:
        aliases:
          - host.docker.internal
    command: [tail, -f, /dev/null]
# Makefile
all:
    make -j up fwd

up:
    docker-compose up

fwd:
    while ! docker-compose exec -T somux true; do sleep 1; done
    somux -L 8080:app:80 -R 9000:localhost:9000 \
        docker-compose exec -T somux somux

さいごに

諸般の事情で PC が新しくなり、OS が Windows 10 Pro → Windows 10 Home になったことで Hyper-V が使えなくなったので、普段使いのディストリ(fedora)と Docker Desktop をともに WSL2 に変更しました。

WSL2 の場合、NTFS 上のディレクトリをワークスペースにすると WSL2 からは 9p でのアクセスになってしまい、WSL1 の DrvFs と比べて性能の劣化が激しすぎて使い物にならないのと、inotify も効かなくなってしまうので、WSL2 の ext4 領域をワークスペースをすることにしました。

すると、以前の構成と比べて Docker Desktop でもかなり快適になりました。WSL2 の fedora と Docker Desktop では別のディストリなわけなので ext4 領域はマウントはできないか(WSL1 の VolFS は Docker Desktop でマウントできないですよね)、なにかしらネットワークファイルシステムになると思っていたのですが、そんなこともなく、WSL2 の fedora の ext4 上の領域が Docker Desktop でもそのまま ext4 として見えています。なので I/O も早いし、inotify などの変更監視も問題ありません。

ので、別にこんなことしなくても普通に Docker Desktop で良いのでは・・・という気もしています。なにか別の用途で使えればいいんですけど、うーん、思いつかない。