🐱

socatを使ってさまざまな通信をリレーする

2023/05/05に公開

はじめに

socat を最近知りました。TCP や標準入出力といった通信を変換することができるツールです。機能はシンプルながら、様々なことに応用できそうです。

確認環境

  • Ubuntu 22.04

基本の使い方

ほぼ必須と言えるオプションがいくつかあるので、それらオプションの必要性について説明します。

TCP → TCP

予め、TCP:80 に httpd か何か http サーバが立っているとします。
以下のコマンドは、TCP:80 に http サーバが立っていることを確認しています。

$ curl -s localhost:80 | head -n 3

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">

TCP:8080 を listen し、TCP:80 に接続する socat を起動します。

$ socat tcp-listen:8080 tcp-connect:localhost:80

別の端末で curl を使って TCP:8080 にアクセスすると、確かに TCP:80 のコンテンツが取得できています。

$ curl -s localhost:8080 | head -n 3

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">

TCP(fork) → TCP

ところで socat は、接続が切断されると、プロセスを終了してしまいます(もう一度 listen しようとしない)。fork を付加すると、通信を受け付ける度にプロセスを fork するようになります。fork 元のプロセスは listen を維持、fork 先のプロセスは受け付けた接続をハンドルします。

$ socat tcp-listen:8080,fork tcp-connect:localhost:80

TCP(fork,reuseaddr) → TCP

socat を再起動した際に Address already in use と言われることがあります。これは終了したTCPセッションが、TIME_WAITCLOSE_WAIT の状態にあり、ポート番号が再利用できないためです。Linux?[1] では、SO_REUSEADDR を有効にすると、TIME_WAITCLOSE_WAIT 状態で残っている TCP セッションがあっても bind できるようになります。socat では、 reuseaddr を指定することで、SO_REUSEADDR を有効にできます。

$ socat tcp-listen:8080,fork,reuseaddr tcp-connect:example.com:80

unix domain socket → exec ls

socat -h の通り、様々な通信に対応しています。次の例は、unix socket でリレーしたり、起動した実行ファイルの標準入出力とつなげたりしています。

以下は、foo.sock な unix domain socket を作成し、接続が来たら、ls -l を実行します。

$ mkdir /tmp/work ; cd /tmp/work ; touch nekochan.sh
$ socat unix-listen:foo.sock,fork exec:"ls -l --time-style=\"+\""

unix domain socket の通信の確認には netcat nc -U が使えます。

$ cd /tmp/work
$ nc -U foo.sock
total 0
srwxrwxr-x 1 mai mai 0  foo.sock
-rw-rw-r-- 1 mai mai 0  nekochan.sh

ところで、unix domain socket のパーミッションは、rwxrwxr-x になっているので、別のユーザからは書き込むことができません

$ sudo -u www-data nc -U foo.sock
nc: unix connect failed: Permission denied
nc: foo.sock: Permission denied

user を指定すると、自動的に chown します。mode でパーミッションを変えることもできます。

$ sudo socat unix-listen:foo.sock,user=www-data,mode=700 exec:"ls -l --time-style=\"+\""
# # # 別の端末から
$ sudo -u www-data nc -U foo.sock
total 0
srwx------ 1 www-data root 0  foo.sock
-rw-rw-r-- 1 mai      mai  0  nekochan.sh

exec vs system

exec と似たものに system があります。違いはコマンドプロセッサを使い | などを解釈させるかどうか。大抵の場合は bash スクリプトに保存すれば exec で十分です。system には、 ,!! を含めてはならない等の特殊ルールもあるので、あまり使いません。

$ socat unix-listen:foo.sock system:"ls | wc -l"
$ socat unix-listen:foo.sock exec:"ls | wc -l"
ls: cannot access '|': No such file or directory
ls: cannot access 'wc': No such file or directory
2023/05/05 17:09:31 socat[438644] E waitpid(): child 438646 exited with status 2
$ echo "ls | wc -l" > lswc.sh
$ socat unix-listen:foo.sock exec:"bash lswc.sh"

応用例

cat で http サーバ

とりあえず何か返す TCP サーバを立てたい時に便利かと思います。

$ cat > http.txt
HTTP/1.1 200 OK
Content-Length: 19
Content-Type: text/html

<html>200 ok</html>
$ socat tcp-listen:8080,fork,reuseaddr exec:"cat http.txt"

ポートマッピングなしで docker コンテナと通信

ポートマッピングをしない代わりに、bind mount と unix domain socket を使って通信できます。

docker コンテナからホストの http サーバにアクセス

socat を 2つ使って、<コンテナ TCP:8080> → unix domain socket → <ホスト TCP:80> とリレーしてみます。

ホスト側:

$ socat unix-listen:foo.sock,fork,mode=777 tcp-connect:localhost:80

コンテナ側:

# コンテナの起動
$ docker run --name temp --rm -it -v /tmp/work:/mnt/work ubuntu:22.04
# 以降コンテナ内
$ apt update -qq && apt install -qq -y socat curl
$ socat tcp-listen:8080,reuseaddr unix-connect:/mnt/work/foo.sock &
[1] 3823
$ curl -s localhost:8080 | head -n 3

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
[1]+  Done                    socat tcp-listen:8080,reuseaddr unix-connect:/mnt/work/foo.sock

ホストから docker コンテナのコマンドを実行

ホストから、docker 内へと通信する例です。ポートマッピングが使えない事情がある場合は便利かもしれないですね?

コンテナ側:

# コンテナの起動
$ docker run --name temp --rm -it -v /tmp/work:/mnt/work ubuntu:22.04
# 以降コンテナ内
$ apt update -qq && apt install -qq -y socat
$ socat unix-listen:/mnt/work/foo.sock,user=1000,mode=700 exec:"bash"

ホスト側

$ echo "cd /bin; ls | wc -l" | nc -U foo.sock
316
脚注
  1. POSIX の仕様かもしれない… ↩︎

Discussion