Raspberry Pi × socat の便利な使い方|netcatの先へ進む万能ツール

以前、ncコマンドの使い方をブログにまとめてみました。

関連

uepon.hatenadiary.com

このツールは「ネットワークのスイスアーミーナイフ」と呼ばれるだけあって便利なのですが、使ってみると「暗号化はできないの?」「シリアルポートと組み合わせると便利なんだけど…」みたいな要求もでてきます。つまり、ncだけでは届かない場面 が見えてきたんですよね。

今回紹介するsocatです。ncの上位互換とも言える存在で、ncで困っていた部分にピタッと答えてくれるのです。

今回のエントリでは自分の理解の整理も兼ねてまとめておきたいと思います。


socatとは?

socat(SOcket CAT)は、2つのデータストリーム間で双方向にデータを転送する多機能リレーツールです。netcat(nc)の強化版とも言われるようで、TCP/UDP・UNIXソケット・シリアルポート・ファイル・標準入出力・SSL・PTYなど、いろいろな「アドレス(エンドポイント)」を扱えます。かなりの豪華版といえます。

つまり、IoT・組み込み・ネットワークデバッグ・トンネリングなど、エッジ用途でも重宝するツールです。

基本構文!

socatの基本構文はシンプルで、次の形をしています。

$ socat [オプション] <address1> <address2>

オプションで2つのアドレスを指定して、その間を双方向につなぐイメージです。

インストール

# Debian/Ubuntu (Raspberry Pi OSも同様)
$ sudo apt install socat

自分はRaspberryPi OSで試しています。

代表的なアドレス種別

socatの「アドレス」として指定できるものはかなり幅広いです。使いそうなものをまとめてみました。

アドレス 説明
- 標準入出力 (stdin/stdout)
TCP:host:port TCPクライアントとして接続
TCP-LISTEN:port TCPサーバとして待ち受け
UDP:host:port UDP送信
UDP-LISTEN:port UDPで待ち受け
UNIX-CONNECT:/path UNIXドメインソケットへ接続
UNIX-LISTEN:/path UNIXドメインソケットで待ち受け
OPEN:/path/to/file ファイルを開く
EXEC:"command" プロセスを起動して接続
SYSTEM:"command" シェル経由でコマンド実行
PTY 仮想擬似端末
/dev/ttyUSB0,b115200 シリアルポート
OPENSSL:host:port SSL/TLSクライアント
OPENSSL-LISTEN:port SSL/TLSサーバ

「ファイルもプロセスもシリアルポートも、全部同じ文法で書ける」点がsocatの面白いところです。

よく使う例

1. シンプルなTCP echoサーバ

まずは受信したデータをそのまま返すechoサーバを1行で書けます。

$ socat -v TCP-LISTEN:8080,fork,reuseaddr EXEC:cat
  • fork … 接続ごとに子プロセスを生成(複数同時接続OK)
  • reuseaddr … ポート再利用を許可
  • -v … 通信内容を標準エラー出力に表示(デバッグ用)

別の端末からnc localhost 8080で接続して、文字を入力すると、そのまま返ってきます。

socatコマンドでechoサーバを起動する

ncコマンドで別ターミナルからアクセス

2. ポートフォワーディング(TCPリレー)

ローカル8080番への接続を、内部サーバ192.168.1.10:80に転送する例:

$ socat TCP-LISTEN:8080,fork,reuseaddr TCP:192.168.1.10:80

ssh -Lと似た形でTCP通信を中継できます。ただし、この例だけでは 暗号化や認証は一切行われない平文中継 なので、信頼できるネットワーク内での検証用途や、上位でTLS終端する前提の踏み台用途に向いています(本格的にトンネリングしたい場合はSSHやVPNを併用してください)。

実機の内部サーバをわざわざ用意しなくても、Pythonの簡易HTTPサーバを「中継先の本物のサーバ役」に見立てて、手元のマシン1台だけで動作確認できます。ターミナルを3つ開いて、次の順で起動するだけです。

# ① 中継先サーバ役: Pythonの簡易HTTPサーバ(ポート8000)を別ターミナルで起動
$ python3 -m http.server 8000

# ② socatでローカル8080→8000に転送(別ターミナル)
$ socat -v TCP-LISTEN:8080,fork,reuseaddr TCP:localhost:8000

# ③ クライアント側からアクセス(さらに別ターミナル)
$ curl http://localhost:8080/

① 中継先サーバ役: Pythonの簡易HTTPサーバ(ポート8000)を別ターミナルで起動

② socatでローカル8080→8000に転送(別ターミナル)

③ クライアント側からアクセス(さらに別ターミナル)

-vオプションを付けているので、socatを起動した端末側にリクエスト・レスポンスがそのまま流れて見えます。 中継先を別マシンのTCP:192.168.1.10:8000に差し替えれば、そのまま実機相手のフォワーディングに移行が可能です。便利🤗

3. シリアルポートをTCPで公開(IoTで便利)

これを知ってsocatを使いたくなりました🤩

Raspberry Piの/dev/ttyUSB0に繋がったマイコンと、 ネットワーク越しに通信したいときに、このパターンが便利🤩

# Raspberry Pi側 (LAN内のIPを bind= で指定して公開範囲を絞っています)
$ socat TCP-LISTEN:5000,reuseaddr,bind=192.168.x.x /dev/ttyUSB0,b115200,rawer

クライアント側からは

# クライアント側
$ socat - TCP:raspberrypi.local:5000

ここで先頭にある-は、オプションではなく 「標準入出力(stdin/stdout)」を表すアドレス です。socatは「2つのアドレスをつなぐ」のが基本動作なので、ここでは「手元のターミナル(-)」と「TCPの向こう側(TCP:raspberrypi.local:5000)」を双方向につないでいるという意味になります。

これで手元のPCの端末からRaspberryPiに接続されたデバイスのシリアルが叩けます。マイコンのデバッグにすごく便利🤗

サーバ側の指定でちょっと変えているところがあるので補足しておきます。

  • rawerを使っています…以前はraw,echo=0と並べて書く例をよく見かけましたが、socat 1.7.4以降は端末処理をまとめて全部無効化するrawerという指定が用意されているのでそちらが楽です(古いバージョンしか入っていない環境では従来通りraw,echo=0で代用してください)
  • forkはあえて付けていません…シリアルポートは「1本の物理デバイス」なので、forkで複数クライアントが同時に開くと読み書きが混ざって動作がわかりにくくなります。
  • bind=で公開範囲を明示TCP-LISTEN:5000だけだと全インターフェースで待ち受けになります。社内LANや家庭内ネットワーク向けでも、bind=でアドレスを絞っておくのが安心です(検証用にローカルだけで試したいならbind=127.0.0.1)

4. 仮想シリアルポートのペアを作る

開発用に、双方向につながった仮想シリアルポートを2本作れます。これはちょっと不思議な使い方かも。

$ socat -d -d PTY,raw,echo=0,link=/tmp/ttyV0 PTY,raw,echo=0,link=/tmp/ttyV1

/tmp/ttyV0/tmp/ttyV1が対になり、 物理ハードウェアなしでシリアル通信のテストができます 。マイコンを持っていないときの動作確認や、シリアル通信を扱うアプリケーションのテストにも使えます。

5. UDPとTCPの単純中継

UDPで受け取ったデータをTCP側に流す、というプロトコルをまたいだ単純中継もsocatなら一発で書けます。

$ socat UDP-LISTEN:5000,fork TCP:backend.example.com:6000

⚠️ この場合「ペイロードのバイト列を双方向に流すだけ」で、UDPのデータグラム境界はTCP側では保たれません。データグラム単位でメッセージを区切る前提のアプリケーションでは、上位プロトコルでフレーミングを自前で持っておかないと、つながったように見えて受信側で意味が正常ではなくなるといったことが起こり得ます。

6. SSL/TLSでラップする

ncだとできなかった暗号化通信が、socatなら素直に書けます。 平文のサーバの前段にTLSをかぶせる(TLS終端) という、リバースプロキシで言うところの SSL ターミネーションが1行で書けてしまいます。 ここでは Raspberry Pi 1台で完結する形で試してみます。 ターミナルを3つ 使うので、SSH越しの場合は tmux や別セッションを用意しておくと楽です。

事件準備として自己署名証明書を作る

socatのcert=に渡すPEMファイルが必要となるので作成を行います。

$ cd ~
$ mkdir -p socat-tls-test && cd socat-tls-test

# 自己署名証明書を作成
$ openssl req -x509 -newkey rsa:2048 -nodes -days 365 \
    -keyout server.key -out server.crt \
    -subj "/CN=localhost" \
    -addext "subjectAltName=DNS:localhost,IP:127.0.0.1"

# socatが扱える形式(鍵と証明書を結合したPEM)にまとめる
$ cat server.key server.crt > server.pem
$ chmod 600 server.pem      # 秘密鍵を含むので権限を絞っておく
$ ls -la server.pem

⚠️ ポイントは -addext "subjectAltName=..." です。これがないと、後でクライアント側で証明書をきちんと検証しようとした際に「hostname mismatch」で蹴られます。

また、socatのcert=に渡すファイルは 秘密鍵と証明書を連結した1ファイル にしておく必要があります(cert=server.crt,key=server.keyのように別々に指定する書き方もあります)。

バックエンドの平文HTTPサーバ(8080番)を起動する

中継先になる「素のHTTPサーバ」役です。Pythonの簡易サーバを使えば実機で何も用意せずに試せます。

$ python3 -m http.server 8080

socatでTLS終端(8443番 → 8080番)

別のターミナルを開き以下を実行します。

$ socat OPENSSL-LISTEN:8443,cert=server.pem,verify=0,reuseaddr,fork TCP:localhost:8080

サーバ側に付けているverify=0は 「クライアント証明書を要求しない」 という意味です(クライアント側に付ける場合の意味とは別物なので注意)。検証用ならこれでOKです。

クライアントから叩く

更に別のターミナルで以下を実行します。

まずは検証無効化(-k)で疎通確認:

$ curl -k https://127.0.0.1:8443/

疎通が成功していたら、以下を実行します。

$ curl --cacert server.crt https://127.0.0.1:8443/

これによりディレクトリ一覧のHTMLが、 TLSで暗号化された経路を経由して 返ってくれば成功となります🤩

7. ファイル送受信(netcat風)

ncのエントリでも行なったシンプルなファイル送受信です。コマンドに出てくるファイル名とアドレスを先に整理しておきます。

  • sendme.bin送信側のローカルにある「これから送りたいファイル」。事前に手元に置いておきます
  • received.bin受信側のカレントディレクトリに作られる「受け取り先ファイル」。受信側コマンドではOPEN:received.bin,creat,truncとしています。creatは「無ければ新規作成」、truncは「ファイルを開く際に既存サイズを0バイトに切り詰めてから書く」の意味です。truncを付けないと先頭から上書きするだけになるので、 以前のファイルの方が長かった場合に古い末尾部分が残ってしまい、結果としてファイルが壊れます 。1回ごとに上書きしたいときはtruncを入れておくのが安全です
  • TCP:receiver.local:9000 … 送信側から見た受信先マシンのホスト名とポート。receiver.localの部分は実環境のIPアドレスやホスト名に読み替えてください。ローカルでのテストではlocalhostを指定すれば大丈夫です。

処理としては「送信側のsendme.bin → TCP:9000 → 受信側でreceived.binとして書き出される」というデータの流れになります。

受信側

$ socat -u TCP-LISTEN:9000,reuseaddr OPEN:received.bin,creat,trunc

送信側

$ socat -u OPEN:sendme.bin TCP:receiver.local:9000

-uは単方向(unidirectional)モード。 送信専用/受信専用が明示される ので、後から読み返してもわかりやすいです。

よく使うオプション

アドレスに付けるサブオプション(よく使うもの)

サブオプション 説明
fork 接続ごとに子プロセスを生成
reuseaddr TIME_WAITでもポートを再利用
bind=IP ローカルバインドアドレス指定
raw rawモード(端末処理なし)
echo=0 ローカルエコーOFF
b115200 シリアルのボーレート
crnl LFをCRLFに変換

netcat(nc)と比較

ここで一旦、ncと比べてみます。

比較項目 netcat(nc) socat
双方向リレー ◯(基本のみ)
シリアルポート/PTY ×
SSL/TLS ×(※ncatなら◎、本家OpenBSD nc -cは対応)
UNIXドメインソケット ◯(-Uで対応)
文法 シンプル やや複雑(慣れが必要)

ちょっとした動作確認はnc、本格的なリレー・変換・トンネリングまで行うのであればsocatという使い分けがよさそうです。

ncの過去エントリーのユースケースをsocatで書き直してみると…

ここまでで概要は伝わったと思うので、最後にncの過去エントリーで紹介したユースケースを、socatで書き直したらどうなるかを並べてみます。同じことをやろうとしたときの 書き方の違い を見ると、両者の役割分担がよりはっきりすると思います。

①ポート疎通確認

nc

$ nc -zv example.com 80

socat

$ socat -u /dev/null TCP:example.com:80
# 接続できれば即終了、できなければエラー出力

これは素直にncのほうが簡潔です。

②簡易チャット

nc

# 受信側
$ nc -l -p 5555
# 送信側
$ nc 192.168.1.100 5555

socat

# 受信側
$ socat - TCP-LISTEN:5555,reuseaddr
# 送信側
$ socat - TCP:192.168.1.100:5555

機能はほぼ同等です。socatなら、 受信側をOPENSSL-LISTEN+サーバ証明書、送信側をOPENSSLに揃える ことで、ここからTLS暗号化チャットへ発展させられます(OPENSSL-LISTEN側ではcert=server.pemなどで証明書を指定するのが推奨です)。TCP-LISTENを単純に書き換えるだけで済むわけではありませんが、ncで暗号化チャットをやろうとしたときの「そもそもどうやるんだ?」と比べれば、ぐっと現実的な距離感になります。

③ファイル転送

nc

# 受信側
$ nc -l -p 1234 > received.zip
# 送信側
$ nc 192.168.1.100 1234 < send.zip

socat

# 受信側
$ socat -u TCP-LISTEN:1234,reuseaddr OPEN:received.zip,creat,trunc
# 送信側
$ socat -u OPEN:send.zip TCP:192.168.1.100:1234

socatのほうが冗長ですが、 -u(単方向)が明示される ので、後から読み返しても「これは送信専用/受信専用」がわかります。即席共有ならnc、運用スクリプトに組み込むならsocatというイメージがいいかもしれません。

④HTTPクライアントとして使う

nc

$ printf "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" | nc example.com 80

socat

$ printf "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" | socat - TCP:example.com:80

平文HTTPならほぼ同じ。ただし HTTPS にしたい場合、ncはそのままでは無理ですが、socatであれば簡単に書けます。

$ printf "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n" | socat - OPENSSL:example.com:443

これ一発で対応できます。ncの過去エントリーでは触れられなかった領域に踏み込めるのが分かりやすい例ですね。

⑤簡易Webサーバー

nc

$ while true; do { printf 'HTTP/1.1 200 OK\r\n\r\n'; cat index.html; } | nc -l -p 8080; done

socat

$ socat TCP-LISTEN:8080,fork,reuseaddr SYSTEM:"printf 'HTTP/1.1 200 OK\r\n\r\n'; cat index.html"

⚠️ echo -eよりもprintfを使うのが無難です。printf 'HTTP/1.1 200 OK\r\n\r\n'はステータス行のあとにヘッダとボディの区切りの空行(\r\n\r\n) を確実に出力できます。

socatの方は一発で書ける&forkで複数同時接続が可能。ncの場合はwhile trueループを使用して1接続ずつ処理する素朴な実装でしたが、socatはforkオプションを書くだけでプログラム内で並列対応してくれます。プロトタイピングも快適です。

ncとsocatの使い分け早見表

Ubuntuのnetcat基準でまとめると、こんな住み分けになります。

ユースケース Ubuntuの netcat socat
ポート疎通確認
即席チャット/ファイル転送 ◎(速い) ◎(明示的)
暗号化通信(TLS) ×(ncatなら可)
シリアルポート/PTY ×
UNIXドメインソケット ◯(-U)
並列接続対応の簡易サーバー ◎(fork)
ワンライナーで全部済ます手軽さ △(構文がやや長い)

ncで手軽に試して、暗号化や本番運用が見えてきたらsocatに置き換えるという流れが現実的ではないでしょうか。

おわりに

最初は「ncの上位互換」くらいの印象で触り始めたsocatですが、特に シリアルポートをネットワーク越しに延長できる のが個人的にはツボでした。Raspberry Piのデバッグや、PCに直接挿せないマイコンとの対話が一気に楽になりそうです。

若干、引数の文法は最初ちょっと取っ付きにくい印象もありますが、2つのアドレスを書くだけ という原則を思い出せば、だいたい読めると思います。

参考リンク

参考記事(ncコマンドの使い方)

uepon.hatenadiary.com