socatを使ってさまざまな通信をリレーする
はじめに
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_WAIT
、CLOSE_WAIT
の状態にあり、ポート番号が再利用できないためです。Linux?[1] では、SO_REUSEADDR
を有効にすると、TIME_WAIT
、CLOSE_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
-
POSIX の仕様かもしれない… ↩︎
Discussion