From c6b3632784048dcf7a0d22b0995fd3693352357b Mon Sep 17 00:00:00 2001 From: dalbodeule <11470513+dalbodeule@users.noreply.github.com> Date: Wed, 3 Dec 2025 01:34:34 +0900 Subject: [PATCH] [feat](protocol): introduce stream-based DTLS tunneling and body size handling - Designed a stream/frame-based protocol leveraging `StreamOpen`, `StreamData`, and `StreamClose` fields for chunked transmission. - Addressed DTLS/UDP MTU limits by capping tunneled body sizes to 48 KiB and replacing oversized responses with `502 Bad Gateway`. - Updated `internal/protocol` to enable safe handling of large HTTP bodies via streaming. - Documented future work on replacing JSON with binary encoding for improved performance. --- ARCHITECTURE.md | 23 ++++++++++++++-------- README.md | 41 +++++++++++++++++++++++----------------- internal/proxy/client.go | 37 +++++++++++++++++++++++++++++++++++- progress.md | 39 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 114 insertions(+), 26 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 1ed1adb..da0dbe4 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -101,17 +101,22 @@ This document describes the overall architecture of the HopGate system. (en) ### 2.5 `internal/protocol` -- 서버와 클라이언트가 DTLS 위에서 주고받는 HTTP 요청/응답 메시지 포맷을 정의합니다. (ko) +- 서버와 클라이언트가 DTLS 위에서 주고받는 HTTP 요청/응답 메시지 포맷을 정의합니다. (ko) - Defines HTTP request/response message formats exchanged over DTLS between server and clients. (en) -- 요청 메시지 / Request message: (ko/en) +- 요청 메시지 / Request message: (ko/en) - `RequestID`, `ClientID`, `ServiceName`, `Method`, `URL`, `Header`, `Body`. (ko/en) -- 응답 메시지 / Response message: (ko/en) +- 응답 메시지 / Response message: (ko/en) - `RequestID`, `Status`, `Header`, `Body`, `Error`. (ko/en) -- 인코딩은 초기에는 JSON을 사용하고, 필요 시 MsgPack/Protobuf 등으로 확장 가능합니다. (ko) -- Encoding starts with JSON and may be extended to MsgPack/Protobuf later. (en) +- 인코딩은 현재 JSON 을 사용하며, 각 HTTP 요청/응답을 하나의 Envelope 로 감싸 DTLS 위에서 전송합니다. (ko) +- Encoding currently uses JSON, wrapping each HTTP request/response in a single Envelope sent over DTLS. (en) + +- 향후에는 `Envelope.StreamOpen` / `StreamData` / `StreamClose` 필드를 활용한 **스트림/프레임 기반 프로토콜**로 전환하여, + 대용량 HTTP 바디도 DTLS/UDP MTU 한계를 넘지 않도록 chunk 단위로 안전하게 전송할 계획입니다. (ko) +- In the future, the plan is to move to a **stream/frame-based protocol** using `Envelope.StreamOpen` / `StreamData` / `StreamClose`, + so that large HTTP bodies can be safely chunked under DTLS/UDP MTU limits. (en) --- @@ -217,11 +222,13 @@ The server decodes the `protocol.Response`, converts it back into an HTTP respon - `internal/acme` 에 ACME 클라이언트(certmagic 또는 lego 등)를 연결해 TLS 인증서 발급/갱신을 구현합니다. (ko) - Wire an ACME client (certmagic, lego, etc.) into `internal/acme` to implement TLS certificate issuance/renewal. (en) -- `internal/dtls` 에서 pion/dtls 기반 DTLS 전송 계층 및 핸드셰이크를 안정화합니다. (ko) +- `internal/dtls` 에서 pion/dtls 기반 DTLS 전송 계층 및 핸드셰이크를 안정화합니다. (ko) - Stabilize the pion/dtls-based DTLS transport and handshake logic in `internal/dtls`. (en) -- `internal/protocol` 과 `internal/proxy` 를 통해 실제 HTTP 터널링을 구현하고, 라우팅 규칙을 구성합니다. (ko) -- Implement real HTTP tunneling and routing rules via `internal/protocol` and `internal/proxy`. (en) +- `internal/protocol` 과 `internal/proxy` 를 통해 실제 HTTP 터널링을 구현하고, + 단일 JSON Envelope 기반 모델에서 `StreamOpen` / `StreamData` / `StreamClose` 중심의 스트림 기반 DTLS 터널링으로 전환합니다. (ko) +- Implement real HTTP tunneling and routing rules via `internal/protocol` and `internal/proxy`, + and move from a single JSON-Envelope model to a stream-based DTLS tunneling model built around `StreamOpen` / `StreamData` / `StreamClose`. (en) - `internal/admin` + `ent` + PostgreSQL 을 사용해 Domain 등록/해제 및 클라이언트 API Key 발급을 완성합니다. (ko) - Complete domain registration/unregistration and client API key issuing using `internal/admin` + `ent` + PostgreSQL. (en) diff --git a/README.md b/README.md index 4bb7b35..8db0e04 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,23 @@ ## 1. 프로젝트 개요 (Project Overview) -HopGate는 공인 서버와 여러 프라이빗 네트워크 클라이언트 사이에 **DTLS 기반 HTTP 터널**을 제공하는 게이트웨이입니다. +HopGate는 공인 서버와 여러 프라이빗 네트워크 클라이언트 사이에 **DTLS 기반 HTTP 터널**을 제공하는 게이트웨이입니다. HopGate is a gateway that provides a **DTLS-based HTTP tunnel** between a public server and multiple private-network clients. 주요 특징 (Key features): -- 서버는 80/443 포트를 점유하고, ACME(Let's Encrypt 등)로 TLS 인증서를 자동 발급/갱신합니다. +- 서버는 80/443 포트를 점유하고, ACME(Let's Encrypt 등)로 TLS 인증서를 자동 발급/갱신합니다. The server listens on ports 80/443 and automatically issues/renews TLS certificates via ACME (e.g. Let's Encrypt). -- 서버–클라이언트 간 전송은 DTLS 위에서 이루어지며, HTTP 요청/응답을 메시지로 터널링합니다. - Transport between server and clients uses DTLS, tunneling HTTP request/response messages. -- 관리 Plane(REST API)을 통해 도메인 등록/해제 및 클라이언트 API Key 발급을 수행합니다. +- 서버–클라이언트 간 전송은 DTLS 위에서 이루어지며, 현재는 HTTP 요청/응답을 JSON 기반 메시지로 터널링합니다. + Transport between server and clients uses DTLS; currently HTTP requests/responses are tunneled as JSON-based messages. +- 관리 Plane(REST API)을 통해 도메인 등록/해제 및 클라이언트 API Key 발급을 수행합니다. An admin management plane (REST API) handles domain registration/unregistration and client API key issuance. -- 로그는 JSON 구조 형태로 stdout 에 출력되며, Prometheus + Loki + Grafana 스택에 친화적으로 설계되었습니다. +- 로그는 JSON 구조 형태로 stdout 에 출력되며, Prometheus + Loki + Grafana 스택에 친화적으로 설계되었습니다. Logs are JSON-structured and designed to work well with a Prometheus + Loki + Grafana stack. +> 참고: 대용량 HTTP 바디에 대해서는 DTLS/UDP MTU 한계 때문에 스트림/프레임 기반 프로토콜로의 전환을 계획하고 있습니다. 자세한 내용은 `progress.md` 의 3.3A 섹션을 참고하세요. (ko) +> Note: For very large HTTP bodies, we plan to move to a stream/frame-based protocol over DTLS due to UDP MTU limits. See section 3.3A in `progress.md` for details. (en) + 아키텍처 세부 내용은 [`ARCHITECTURE.md`](ARCHITECTURE.md)에 정리되어 있습니다. Detailed architecture is documented in [`ARCHITECTURE.md`](ARCHITECTURE.md). @@ -38,10 +41,10 @@ Detailed architecture is documented in [`ARCHITECTURE.md`](ARCHITECTURE.md). ### 3.1 의존성 (Dependencies) -- Go 1.21+ 권장 (go.mod 상 버전보다 최신 Go 사용을 추천) +- Go 1.21+ 권장 (go.mod 상 버전보다 최신 Go 사용을 추천) Go 1.21+ is recommended (even if go.mod specifies an older minor). -- PostgreSQL (추후 DomainValidator 실제 구현 시 필요) - PostgreSQL (only required when implementing real domain validation). +- PostgreSQL (관리 Plane + 실제 DomainValidator 에 필수) + PostgreSQL (required for the admin plane and the real DomainValidator). Go 모듈 의존성 설치 / 정리는 다음으로 수행할 수 있습니다: You can install/cleanup Go module deps via: @@ -104,11 +107,11 @@ HOP_CLIENT_LOCAL_TARGET=127.0.0.1:8080 HOP_CLIENT_DEBUG=true ``` -- `HOP_CLIENT_SERVER_ADDR` : DTLS 서버 주소 (예: `localhost:8443`) +- `HOP_CLIENT_SERVER_ADDR` : DTLS 서버 주소 (예: `localhost:8443`) DTLS server address, e.g. `localhost:8443`. -- `HOP_CLIENT_DOMAIN` / `HOP_CLIENT_API_KEY` : 관리 Plane 에서 발급받은 도메인/키 (현재는 DummyValidator 로 아무 값이나 허용) - Domain and API key issued by the admin plane (currently any values are accepted by DummyValidator). -- `HOP_CLIENT_LOCAL_TARGET` : 실제로 HTTP 요청을 보낼 로컬 서버 주소 +- `HOP_CLIENT_DOMAIN` / `HOP_CLIENT_API_KEY` : 관리 Plane 에서 발급받은 도메인/키 (실제 ent + PostgreSQL 기반 DomainValidator 에 의해 검증) + Domain and API key issued by the admin plane (validated by a real ent + PostgreSQL based DomainValidator). +- `HOP_CLIENT_LOCAL_TARGET` : 실제로 HTTP 요청을 보낼 로컬 서버 주소 Local HTTP target address. - `HOP_CLIENT_DEBUG=true` : 서버 인증서 체인 검증을 스킵(InsecureSkipVerify)하여 self-signed 인증서를 신뢰 Skips server certificate chain verification (InsecureSkipVerify) and trusts the self-signed cert. @@ -162,10 +165,14 @@ For implementation skeleton, see [`internal/admin`](internal/admin) and [`ent/sc ## 6. 주의사항 (Caveats) -- `Debug=true` 설정은 **개발/테스트 용도**입니다. self-signed 인증서 및 InsecureSkipVerify 사용은 프로덕션 환경에서 절대 사용하지 마세요. +- `Debug=true` 설정은 **개발/테스트 용도**입니다. self-signed 인증서 및 InsecureSkipVerify 사용은 프로덕션 환경에서 절대 사용하지 마세요. `Debug=true` is strictly for development/testing. Do not use self-signed certs or InsecureSkipVerify in production. -- 실제 운영 시에는 ACME 기반 인증서, PostgreSQL + ent 기반 DomainValidator, Proxy 레이어 연동 등을 완성해야 합니다. - For production you must wire ACME certificates, a PostgreSQL+ent-based DomainValidator, and the proxy layer. +- 현재 버전은 ACME 기반 인증서, PostgreSQL + ent 기반 DomainValidator, Proxy 레이어가 기본적으로 연동되어 있으나, + 대용량 HTTP 바디에 대해서는 JSON 단일 메시지 기반 터널링 특성상 DTLS/UDP MTU 한계에 부딪힐 수 있습니다. + 스트림/프레임 기반 DTLS 터널링으로의 전환 및 하드닝 작업은 `progress.md` 에 정의된 다음 단계에 포함되어 있습니다. (ko) + The current version wires ACME certificates, a PostgreSQL+ent-based DomainValidator, and the proxy layer by default, + but for very large HTTP bodies the JSON single-message tunneling model can still hit DTLS/UDP MTU limits. + Moving to a stream/frame-based DTLS tunneling model and further hardening are tracked as next steps in `progress.md`. (en) -HopGate는 아직 초기 단계의 실험적 프로젝트입니다. API 및 동작은 언제든지 변경될 수 있습니다. +HopGate는 아직 초기 단계의 실험적 프로젝트입니다. API 및 동작은 언제든지 변경될 수 있습니다. HopGate is still experimental; APIs and behavior may change at any time. diff --git a/internal/proxy/client.go b/internal/proxy/client.go index d7fc38c..b50dc52 100644 --- a/internal/proxy/client.go +++ b/internal/proxy/client.go @@ -192,10 +192,45 @@ func (p *ClientProxy) forwardToLocal(ctx context.Context, preq *protocol.Request for k, vs := range res.Header { presp.Header[k] = append([]string(nil), vs...) } - body, err := io.ReadAll(res.Body) + + // DTLS over UDP has an upper bound on packet size (~64KiB). 전체 HTTP 바디를 + // 하나의 JSON Envelope 로 감싸 전송하는 현재 설계에서는 바디가 너무 크면 + // OS 레벨에서 "message too long" (EMSGSIZE) 가 발생할 수 있습니다. (ko) + // + // 이를 피하기 위해, 터널링 가능한 바디 크기에 상한을 두고, 이를 초과하는 + // 응답은 502 Bad Gateway + HopGate 전용 에러 메시지로 대체합니다. (ko) + // + // DTLS over UDP has an upper bound on datagram size (~64KiB). With the current + // design (wrapping the entire HTTP body into a single JSON envelope), very + // large bodies can trigger "message too long" (EMSGSIZE) at the OS level. + // To avoid this, we cap the tunneled body size and replace oversized responses + // with a 502 Bad Gateway + HopGate-specific error body. (en) + const maxTunnelBodyBytes = 48 * 1024 // 48KiB, conservative under UDP limits + + limited := &io.LimitedReader{ + R: res.Body, + N: maxTunnelBodyBytes + 1, // read up to limit+1 to detect overflow + } + body, err := io.ReadAll(limited) if err != nil { return fmt.Errorf("read http response body: %w", err) } + if len(body) > maxTunnelBodyBytes { + // 응답 바디가 너무 커서 DTLS/UDP 로 안전하게 전송하기 어렵기 때문에, + // 원본 바디 대신 HopGate 에러 응답으로 대체합니다. (ko) + // + // The response body is too large to be safely tunneled over DTLS/UDP. + // Replace it with a HopGate error response instead of attempting to + // send an oversized datagram. (en) + presp.Status = http.StatusBadGateway + presp.Header = map[string][]string{ + "Content-Type": {"text/plain; charset=utf-8"}, + } + presp.Body = []byte("HopGate: response body too large for DTLS tunnel (over max_tunnel_body_bytes)") + presp.Error = "response body too large for DTLS tunnel" + return nil + } + presp.Body = body return nil diff --git a/progress.md b/progress.md index 5fe2135..11377f4 100644 --- a/progress.md +++ b/progress.md @@ -240,6 +240,45 @@ This document tracks implementation progress against the HopGate architecture an - [x] 클라이언트 main 에 Proxy loop wiring 추가: [`cmd/client/main.go`](cmd/client/main.go) - handshake 성공 후 `proxy.ClientProxy.StartLoop` 실행. +#### 3.3A Stream-based DTLS Tunneling / 스트림 기반 DTLS 터널링 + +현재 HTTP 터널링은 **단일 JSON Envelope + 단일 DTLS 쓰기** 방식(요청/응답 바디 전체를 한 번에 전송)이므로, +대용량 응답 바디에서 UDP MTU 한계로 인한 `sendto: message too long` 문제가 발생할 수 있습니다. +프로덕션 전 단계에서 이 한계를 제거하기 위해, DTLS 위 애플리케이션 프로토콜을 **완전히 스트림/프레임 기반**으로 재설계합니다. + +- [ ] 스트림 프로토콜 설계 및 단일 Envelope 방식 치환: [`internal/protocol/protocol.go`](internal/protocol/protocol.go:50) + - `Envelope` 타입의 `StreamOpen` / `StreamData` / `StreamClose` 필드를 사용해 HTTP 요청/응답을 스트림으로 모델링: + - 서버 → 클라이언트: + - `StreamOpen`: HTTP 요청 라인/헤더 전달. + - `StreamData`: 요청 바디를 여러 chunk 로 분할 전송. + - `StreamClose`: 요청 바디 종료/스트림 종료 알림. + - 클라이언트 → 서버: + - `StreamOpen`: HTTP 응답 상태/헤더 전달. + - `StreamData`: 응답 바디를 여러 chunk 로 분할 전송. + - `StreamClose`: 응답 바디 종료/스트림 종료 알림. + - 각 `StreamData.Data` 는 DTLS/UDP MTU 를 고려한 안전한 크기(예: 4–8KiB)로 제한하여, + 단일 datagram 이 MTU 를 넘지 않도록 함. + - 기존 `MessageTypeHTTP` 기반 단일 요청/응답 방식은 스트림 경로가 완성되면 제거하거나 내부용/테스트용으로만 유지. + +- [ ] 클라이언트 Proxy 스트림 모드 구현: [`internal/proxy/client.go`](internal/proxy/client.go:55) + - Stream ID ↔ 로컬 HTTP 요청/응답을 연결하기 위한 `io.Pipe` 또는 버퍼링 구조 도입. + - 서버에서 수신한 `StreamOpen/StreamData/StreamClose` 프레임을 사용해: + - 로컬 HTTP 요청을 streaming body 로 구성. + - 로컬 HTTP 응답은 반대 방향 스트림으로 전송: + - 상태/헤더 → `StreamOpen`. + - 바디 chunk → 연속 `StreamData`. + - 응답 종료 → `StreamClose`. + +- [ ] 서버 측 스트림 처리기 도입: [`cmd/server/main.go`](cmd/server/main.go:160) + - 스트림 모드에서는 `ForwardHTTP` 가 전체 `*protocol.Response` 를 반환하는 대신, + 특정 Stream ID 에 대한 응답을 `http.ResponseWriter` 에 직접 chunk 단위로 중계하는 스트리밍 경로를 구현. + - 필요 시 스트림 전용 터널 타입(예: `dtlsStreamTunnel`)을 도입하여, + 터널링 레이어와 HTTP 레이어를 명확히 분리. + +- [ ] JSON 인코딩 유지 여부 검토 + - 초기에는 JSON 기반 스트림 프레임으로 구현하되, + 이후 성능/오버헤드 측정을 바탕으로 length-prefix 이진 프레임(MsgPack/Protobuf 등)으로 전환 여부를 재평가한다. + --- ### 3.4 ACME Integration / ACME 연동