TIL: curlを使わずにBashの/dev/TCPでHTTPリクエストを送信できる
原題: TIL: You can make HTTP requests without curl using Bash /dev/TCP
日本語訳
# タイトル
TIL: curlを使わずにBashの/dev/tcpでHTTPリクエストを送る方法
# 本文
Dockerの内部ネットワークを通じて、あるコンテナから別のコンテナに到達できるか確認する必要がありました。共有ネットワーク上のサービスに対して、単純な `GET /health` を送るという作業です。当然、`curl http://service:8642/health` を使うのが一番手っ取り早い選択肢です。しかし、このアプリケーションイメージは極限まで軽量化されており、`curl` も `wget` も入っておらず、ソケットを開くために使える他の手段もありませんでした。
実は、bash単体でHTTPを扱うことができます。ホストとポートへの接続を開き、リクエストを手動で書き込むには、すでに存在するシェル以外に何も必要ありません。
```bash
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名など)。ご自身のホスト名とポートに置き換えて使用してください。
これにより、ステータスライン、ヘッダー、空行、そしてボディを含むレスポンス全体が表示されます。`Authorization: Bearer` トークンのようなヘッダーを追加するには、リクエストの末尾にある空行の前に、別の `\r\n` で区切られた行を追加します。
```bash
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` は実在するデバイスファイルではないということです。ディスク上にそのようなパスは存在しません。`ls /dev/tcp` を実行しても何も見つからず、別のシェルから `cat /dev/tcp/...` を実行してもエラーになります。これはbashが内部的に処理するリダイレクト機能です。Bashのマニュアルにはこうあります:
`/dev/tcp/host/port` – hostが有効なホスト名またはIPアドレスであり、portが整数値のポート番号またはサービス名である場合、bashは対応するTCPソケットを開こうと試みます。
この名前が選ばれた理由は、本物のUnixには `/dev/tcp` や `/dev/udp` という階層が存在しないため、既存の仕組みと衝突することがないからです。BashがDNSルックアップと `connect(2)` を代行してくれます。そして `exec 3<>` によって、他のファイルと同様に読み書き可能なファイル記述子(3)としてソケットが渡されます。
知っておくべき点がいくつかあります:
- **`Connection: close` ヘッダーは重要です。** これがないと、サーバーはレスポンスを返した後も接続を維持し続けます(これがHTTP/1.1のデフォルトです)。その結果、`cat <&3` は、二度と届かないバイトを待ち続けてしまいます。サーバーに接続を閉じるよう要求することで、`cat` はEOFに到達して終了します。念のため `timeout 6 bash -c '...'` でラップしておけば、どちらのケースでも安心です。
- **TLS(SSL)は使えません。** `/dev/tcp` は生のソケットを開くため、プレーンテキストのHTTPにしか対応していません。HTTPSを使用するには `openssl s_client` が必要になりますが、そうなると、最初から適切なツールを使ったほうがマシでしょう。
- **これはbashの機能であり、POSIX標準ではありません。** `dash`(Debianの `/bin/sh`)や `zsh` では利用できないため、`#!/bin/sh` スクリプトでは使えません。直接 `bash` を呼び出す必要があります。
- **これはコンパイル時のオプションであり、bashが `--enable-net-redirections` 付きでビルドされている場合に有効になります。** 主要なビルドのほとんどでは有効になっており、私が使用していたDebianベースのイメージでも問題なく動作しました。しかし、Debianでは長年この機能が無効化されていたこともあるため、古いシステムや非常に最小限のシステムでは、事前に確認する価値があります。
日常的な作業においては、依然として `curl` が最適なツールです。しかし、何もインストールできない、意図的に極限まで小さくされたコンテナ内では、パッケージを追加することなく、素早くチェックを行うためにこの方法が役立ちます。
原文(英語)を表示
I needed to check that one container could reach another over an internal Docker network: a plain GET /health
against a service on a shared network. The obvious move is curl http://service:8642/health
. But this app image was stripped right down, with no curl
or wget
and nothing else around that I could use to open a socket.
As it turns out, bash
can speak HTTP by itself. Opening a connection to a host and port and writing the request by hand needs nothing beyond the shell that’s already there:
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
here is just the hostname of whatever you’re talking to. It has to resolve and be reachable from wherever you run this, so it needs to be set up first: a container or service name on a Docker network you’ve configured, or any DNS name that resolves. Swap in your own host and port.
That prints the whole response: the status line, the headers, the blank line, and the body. To add a header, such as an Authorization: Bearer
token, put another \r\n
-terminated line before the blank line that ends the request:
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
What caught me out the first time is that /dev/tcp
isn’t a real device file. There’s no such path on disk; ls /dev/tcp
finds nothing, and cat /dev/tcp/...
from another shell just errors. It’s a redirection that bash
handles internally. From the Bash manual:
/dev/tcp/host/port
– If host is a valid hostname or Internet address, and port is an integer port number or service name, bash attempts to open the corresponding TCP socket.
The names were picked because no real Unix has a /dev/tcp
or /dev/udp
hierarchy, so there’s nothing to collide with. Bash does the DNS lookup and the connect(2)
for you, and exec 3<>
hands the socket a file descriptor (3
) you read from and write to like any other.
A few things worth knowing:
- The
Connection: close
header matters. Without it the server keeps the connection open after it responds, which is the HTTP/1.1 default, andcat <&3
then waits forever for bytes that never arrive. Asking the server to close meanscat
reaches EOF and returns. Wrapping the call intimeout 6 bash -c '...'
covers you either way. - There’s no TLS.
/dev/tcp
opens a raw socket, so this only works for plaintext HTTP. Forhttps
you’d needopenssl s_client
, and by then you may as well have the proper tools. - This is a
bash
feature, not POSIX.dash
(Debian’s/bin/sh
) andzsh
don’t have it, so a#!/bin/sh
script can’t use it. Callbash
directly. - It’s a compile-time option, switched on when
bash
is built with--enable-net-redirections
. Most mainstream builds enable it, and it worked without any fuss in the Debian-based image I was in, but Debian shipped it disabled for years, so on an old or very minimal system it’s worth checking first.
For day-to-day work curl
is still the right tool. But inside a deliberately small container where you can’t install anything, this gets a quick check done without adding a package.