1 ポイント 投稿者 GN⁺ 4 시간 전 | 1件のコメント | WhatsAppで共有
  • 最小構成のコンテナイメージでは 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 である dashzsh では使えず、bash を明示的に呼び出す必要がある
  • Bash のビルド時に --enable-net-redirections で有効化されるコンパイル時オプションである
  • 要するに curl を置き換える汎用ツールというより、追加インストールできない 小さなコンテナで素早く接続性を確認する用途に向いている

1件のコメント

 
GN⁺ 4 시간 전
Hacker Newsのコメント
  • 90年代末の子ども時代に、telnet80、25、110番ポートへ接続してサーバーと直接やり取りできると知って衝撃を受けた
    単純な GET / HTTP/1.1 リクエストを自分で打ち込んだり、25番ポートで HELOmail-frommail-to を使ってメールを送ったり、POP3でメールボックス一覧や個々のメッセージを取得できたりした
    この体験が「魔法なんてない」という気づきの始まりで、コンピューターのあらゆる部分は人間が作ったもので、努力すればある程度までは理解できるのだと知った
    将来は大半をエージェントに任せることになるだろうが、モデルや安全装置のフィルターなしに実際の動作を学びたい人のために、さまざまなシステムには面白い抜け道が残る気がする

    • DKIM/SPF もなく、SMTPサーバーの認証も緩かった時代には、jacques.chirac@elysee.fr のようなアドレスでメールを送って友人たちにハッカーっぽく見せることができた
    • 当時は DKIM や SPF がなかっただけでなく、ほとんどの SMTP サーバーが誰から誰へでもメールを受け付ける オープンリレー だった
    • 結局は全部テキストファイルにすぎない
      構造化されたテキストファイルを作り、送り、読み取るさまざまな方法の上に略語が大量に積み重なった構造だった
      ある日、データベースですらテキストファイルなのだと気づいて、しばらく座り込んでしまった
    • 前世紀には、会社で個人メールを読んだり送ったりするとき、それぞれ telnetPOP3 と SMTP に接続して処理していた
    • HTTP/2 ではこういうやり方はできないが、幸いほぼすべてのサーバーはまだ HTTP/1 も話せる
      TLS も telnet では無理で、多くのサーバーは HTTP リクエストにはリダイレクトしか返さない
      telnet の代わりに openssl s_client を使えば TLS の中にテキストをトンネリングすることはできるが、少し裏技っぽく感じる
      最近のプロトコルの多くがバイナリエンコーディングを好むため、専用ツールなしでは wire レベルで触りにくくなったのも残念だ
      それでも将来もこういうものを掘り下げる人はいる気がするし、棒で火を起こしたり粘土レンガを焼いたりするような古い技術は楽しく、ときには実際に役立つ
      むしろ AI のおかげで実験はしやすくなっていて、RFC を漁らなくても LLM に聞きながら、たとえば一般的な IMAP コマンドの大半を学べる
  • zsh には Bash の /dev/tcp とは別に zsh/net/tcpzsh/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 に行き着く

    • 公共 Wi-Fi のキャプティブポータル が不調なときにも 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で完全なHTTPサーバー」という表現は、正確には誤り
      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サーバーを実装するのも同様だが、素早いテスト用としてはかなり良い
    • 誰かは 純粋なBash Minecraftサーバー まで作っていた
      https://sdomi.pl/weblog/15-witchcraft-minecraft-server-in-ba...
    • Bash用のRails風フレームワークもある: https://github.com/jneen/balls
  • BauhiniaチームがCTFの問題を解くときにこれを使っているのを見て学んだ
    複数段階にまたがるCTFで、最初はROPチェーンで system シェルを得るが、実質的にはBash以外ほとんど何も実行できない牢獄のような環境だった
    使えるのは readcat くらいだったので、cat /dev/tcp を使ってからそれを 仮想端末 にリダイレクトし、その内容を読んで内部システムのURLを得てフラグを見つけた

  • 内部 Dockerネットワーク でコンテナ間接続を確認していたとき、イメージに curlwget もなくてこの方法を見つけた
    驚いたのは、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として話しかけたいポートである

    • 面白いが、単に curl対応のイメージ を使わないことに何かデメリットがあるのだろうか
      思いつくデメリットはなく、運用用イメージでもほぼ必須だと思う
  • 昔のDebianとDebian派生ディストリビューションではこの機能は動作しなかった。仮想ファイル経由の TCPアクセス がデフォルトで無効化されていたためである
    理解している限りでは、2009年に方針が変わって機能が有効化され、Bug #146464 に議論とリンクがある
    <https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=146464#37>
    シェルツールでネットワーク機能に直接アクセスする方法はこれ以外にも、curlwget、Perlの HEADGET コマンド、netcat/ncsocattelnet などいろいろある

  • 10代のころ、他人の /dev/ptty に不気味なメッセージを echo で送りつけて驚かせていたのを思い出す
    自分が送ったメッセージが相手の開いている端末に魔法のように現れた
    コンピュータ室でクライアントごとに別アカウントを使わせ、ロックもさせていなかった理由はいまだに分からないし、当時の VAXの限界 だったのかもしれない