Zeroserve: eBPF로 스크립트 가능한 제로 설정 웹 서버
출처: Hacker News
면책 조항: 이 글은 GPT‑5.5와 Claude Opus 4.8과 공동 집필되었습니다.
zeroserve 는 작고 빠르며 설정이 필요 없는 HTTPS 서버입니다. 웹사이트의 tarball 하나만 넘겨주면 HTTP/2와 TLS 1.3 위에서 바로 제공해 주며, 핫 리로드와 아주 작은 메모리 점유율을 갖습니다. 여기서 핵심은 tarball 안에 eBPF 프로그램을 넣을 수 있다는 점인데, 이 프로그램은 모든 요청마다 사용자 공간에서 샌드박스 미들웨어로 실행됩니다—요청을 재작성하거나 인증·속도 제한을 하거나, 백엔드에 역프록시하도록 할 수 있습니다.
요약하면:
- 빠름: 한 코어에서 대부분의 워크로드—작은 파일·큰 파일 정적 제공, 스크립트 미들웨어, 작은 응답 프록시—를 nginx보다 앞섭니다. 모두 HTTPS 위에서 동작합니다.
- 효율적인 eBPF 스크립팅: 스크립트는 JIT‑컴파일되어 네이티브 코드가 되고, 사용자 공간에서 샌드박스되므로 매 요청마다 실행해도 비용이 거의 없습니다.
- 프로그램이 곧 설정: eBPF 프로그램 자체가 전체 설정이며, 각 요청에 대해 무엇을 할지 결정합니다.
- 전역
io_uring사용: 모든 네트워크·디스크 작업이io_uring을 통해 제출됩니다. - 박스 안에 최신 TLS: TLS 1.3, HTTP/2, Encrypted Client Hello, SNI 기반 인증서 선택, JA4 지문 수집을 기본 제공.
- 운용이 간단: 하나의 tarball 로 전체 사이트를 제공하고,
SIGHUP하나로(그리고 TLS 자료도) 핫 리로드합니다.
zeroserve는 nginx와 Caddy의 대안으로 설계됐으며, 핵심 설계 포인트는 설정입니다. 기존 서버들은 선언형 설정 언어—location 블록, rewrite 규칙, map 지시문, try_files 등—를 제공하고, 선언형 언어가 한계에 다다르면 Lua나 Caddy 플러그인 같은 선택적 스크립트 런타임을 별도로 붙입니다. 결과적으로 동작은 두 층으로 나뉘게 됩니다: 자체 흐름 제어를 가진 지시문과, 요청 라이프사이클 어딘가에서 실행되는 스크립트. 이 두 층을 머릿속에 모두 유지해야 합니다.
zeroserve는 이를 하나로 합칩니다. 설정 파일이 없습니다. eBPF 프로그램이 곧 설정이며, 모든 요청을 보고 라우팅·헤더·인증·속도 제한·프록시 등을 결정하는 단일 샌드박스 프로그램입니다. 나는 전체 요청 흐름을 위에서 아래까지 한 눈에 읽을 수 있길 원합니다.
하나의 tarball, 제자리에서 제공
전체 사이트는 하나의 tar 파일입니다. zeroserve는 로드 시 이를 인덱싱해 경로 → 바이트‑범위 맵을 만들고, tarball 자체에 바이트‑범위 읽기를 수행해 파일을 제공합니다. 디스크에 풀어내는 일은 전혀 없습니다. 사이트가 하나의 파일에만 존재하므로, 노출될 수 있는 별도 location 규칙이 없고, 배포는 원자적인 파일 교체 한 번이면 됩니다. 디렉터리를 패키징하려면:
zeroserve --pack ./public > site.tar
zeroserve --addr 0.0.0.0:8080 site.tar
새 버전을 배포하는 방법은 “tarball을 교체하고 SIGHUP을 보내는 것”뿐입니다. 리로드는 사이트, 스크립트, TLS 자료를 모두 원자적으로 같은 프로세스 안에서 교체하므로 연결이 끊기지 않습니다:
killall -SIGHUP zeroserve
모든 네트워크·디스크 I/O는 io_uring을 통해 이루어집니다( monoio 런타임 사용). 각 인스턴스는 단일 스레드 이벤트 루프입니다. 이는 제한처럼 보일 수 있지만, 스케일링 단위가 “프로세스 수”일 때는 오히려 적합하며, 여러 인스턴스를 한 서버에 동시에 띄우는 것이 일반적입니다.
사용자 공간 eBPF 스크립팅
가장 재미있는 부분이 바로 여기입니다. .zeroserve/scripts/ 아래에 두는 모든 .c 파일은 패킹 시점에 (clang·llc 사용) eBPF 오브젝트로 컴파일되고, 매 요청마다 실행됩니다. eBPF는 완전히 사용자 공간에서 동작합니다: zeroserve는 바이트코드를 자체 비특권 프로세스 안의 런타임(async‑ebpf)에 로드하므로 커널 BPF 서브시스템과 CAP_BPF 권한은 전혀 관여하지 않습니다. async‑ebpf는 바이트코드를 네이티브 머신 코드로 JIT‑컴파일하는데(내부에 uBPF 포함), 따라서 “설정”이 네이티브 x86‑64 코드로 실행됩니다.
포인터 케이지가 커널 검증기가 보통 수행하는 역할을 대신합니다. JIT‑컴파일된 코드의 모든 메모리 접근은 프로그램 전용 영역으로 마스킹되므로, 실수로 다른 메모리를 읽거나 쓰는 일이 스크립트 메모리 안에 한정됩니다.
스크립트는 zeroserve의 단일 이벤트 루프 위에서 직접 실행됩니다. 하나의 느린 스크립트가 다른 모든 연결을 지연시키지 않도록, 런타임은 완전 선점 가능하도록 설계되었습니다: 타이머가 JIT‑컴파일된 네이티브 코드를 중간에 중단하고 제어권을 이벤트 루프로 되돌려 줍니다.
프로그래밍 모델은 파일명 순서대로 정렬된 스크립트 체인이며, 각 요청마다 메타데이터 맵을 공유합니다. 스크립트가 zs_respond 혹은 zs_reverse_proxy를 호출하면 체인이 즉시 종료됩니다. 아래는 가장 먼저 실행돼 모든 요청을 풍부하게 만드는 스크립트 예시입니다:
#include <zeroserve.h>
ZS_ENTRY
zs_u64 entry(void) {
char peer[64];
if (zs_req_peer(peer, sizeof(peer)) == 0) {
// 예시: 요청에 대한 로깅 등
}
// ... 추가 로직 ...
return 0;
}
스크립트가 호출할 수 있는 헬퍼 인터페이스는 다음과 같이 다양합니다:
- 요청 검사·변경: 메서드, 경로, 쿼리 파라미터, 헤더, 피어 주소를 읽고, 응답 전 URI를 재작성하거나 헤더를 추가·제거합니다.
- 암호·인코딩: SHA‑256, HMAC‑SHA256, base64, hex,
getrandom등을 지원합니다. - JSON: 요청 본문을 파싱하고, 문서 트리를 만들·수정한 뒤
zs_json_respond로 응답합니다. - 속도 제한: IP부터 API 키까지 임의의 키를 기준으로 토큰 버킷을 만들고, 핫 리로드 후에도 상태를 유지합니다.
- AWS SigV4: S3·그 외 AWS 서비스와 통신하기 위한 서명된
Authorization헤더와 프리사인드 URL을 생성합니다. - OIDC 로그인: 완전한 RP 흐름(Authorization Code + PKCE)을 제공하며, 로그인 세션을 XChaCha20‑Poly1305로 암호화한 쿠키에 담아 전송합니다. 이를 통해 “Google 로그인”으로 정적 사이트를 보호하면서 서버는 무상태(stateless)로 유지됩니다.
동적 엔드포인트는 단순히 응답을 반환하는 스크립트입니다:
ZS_ENTRY
zs_u64 entry(void) {
char path[64];
zs_req_path(path, sizeof(path));
if (zs_strcmp(path, "/health") != 0) return 0;
zs_meta_set(ZS_STR("zs.response.header.content-type"), ZS_STR("application/json"));
zs_respond(200, ZS_STR("{\"status\":\"ok\"}\n"));
return 0;
}
각 스크립트는 메모리 사용량 제한(기본 256 KB) 아래에서 실행되며, 런타임은 장시간 실행되는 스크립트를 타임슬라이스 방식으로 중단하고, 과도하게 실행되는 스크립트를 제한합니다. 스크립트는 zs_call 로 서로 호출할 수 있으며, 호출 깊이는 제한됩니다. 무한 루프에 빠진 스크립트는 해당 요청만 차단하고, 선점 타이머가 중단시키면 서버는 다른 요청을 계속 서비스합니다.
TLS 구현은 “설정이 필요 없는”이라는 외관을 넘어선 완전한 스택을 제공합니다. TLS 1.3 전용이며 BoringSSL 로 구현되고, Encrypted Client Hello 를 기본 지원해 실제 SNI가 평문으로 노출되지 않습니다. 디렉터리 기반 SNI 인증서 선택, JA4 클라이언트 지문을 스크립트에 노출, 그리고 투명 ECH 릴레이 모드가 있어 복호화되지 않는 핸드쉐이크를 실제 업스트림 서버로 그대로 전달함으로써 보호된 도메인이 공개 도메인 뒤에 숨겨질 수 있습니다. 이 정도 수준의 전송 보안을 단일 바이너리 하나에 담은 것이죠.
얼마나 빠른가?
zeroserve를 nginx 1.26 및 Caddy 2.11과 HTTPS 환경에서 비교 벤치마크했습니다. 테스트 머신은 8코어 Ryzen 7 3700X이며, 각 서버는 동일한 자체 서명 인증서를 사용해 같은 콘텐츠를 제공했습니다. zeroserve는 설계상 단일 스레드이므로 **코어당