mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2025-12-09 05:15:44 +09:00
[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.
This commit is contained in:
@@ -110,8 +110,13 @@ This document describes the overall architecture of the HopGate system. (en)
|
|||||||
- 응답 메시지 / Response message: (ko/en)
|
- 응답 메시지 / Response message: (ko/en)
|
||||||
- `RequestID`, `Status`, `Header`, `Body`, `Error`. (ko/en)
|
- `RequestID`, `Status`, `Header`, `Body`, `Error`. (ko/en)
|
||||||
|
|
||||||
- 인코딩은 초기에는 JSON을 사용하고, 필요 시 MsgPack/Protobuf 등으로 확장 가능합니다. (ko)
|
- 인코딩은 현재 JSON 을 사용하며, 각 HTTP 요청/응답을 하나의 Envelope 로 감싸 DTLS 위에서 전송합니다. (ko)
|
||||||
- Encoding starts with JSON and may be extended to MsgPack/Protobuf later. (en)
|
- 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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -220,8 +225,10 @@ The server decodes the `protocol.Response`, converts it back into an HTTP respon
|
|||||||
- `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)
|
- Stabilize the pion/dtls-based DTLS transport and handshake logic in `internal/dtls`. (en)
|
||||||
|
|
||||||
- `internal/protocol` 과 `internal/proxy` 를 통해 실제 HTTP 터널링을 구현하고, 라우팅 규칙을 구성합니다. (ko)
|
- `internal/protocol` 과 `internal/proxy` 를 통해 실제 HTTP 터널링을 구현하고,
|
||||||
- Implement real HTTP tunneling and routing rules via `internal/protocol` and `internal/proxy`. (en)
|
단일 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)
|
- `internal/admin` + `ent` + PostgreSQL 을 사용해 Domain 등록/해제 및 클라이언트 API Key 발급을 완성합니다. (ko)
|
||||||
- Complete domain registration/unregistration and client API key issuing using `internal/admin` + `ent` + PostgreSQL. (en)
|
- Complete domain registration/unregistration and client API key issuing using `internal/admin` + `ent` + PostgreSQL. (en)
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -11,13 +11,16 @@ HopGate is a gateway that provides a **DTLS-based HTTP tunnel** between a public
|
|||||||
|
|
||||||
- 서버는 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).
|
The server listens on ports 80/443 and automatically issues/renews TLS certificates via ACME (e.g. Let's Encrypt).
|
||||||
- 서버–클라이언트 간 전송은 DTLS 위에서 이루어지며, HTTP 요청/응답을 메시지로 터널링합니다.
|
- 서버–클라이언트 간 전송은 DTLS 위에서 이루어지며, 현재는 HTTP 요청/응답을 JSON 기반 메시지로 터널링합니다.
|
||||||
Transport between server and clients uses DTLS, tunneling HTTP request/response messages.
|
Transport between server and clients uses DTLS; currently HTTP requests/responses are tunneled as JSON-based messages.
|
||||||
- 관리 Plane(REST API)을 통해 도메인 등록/해제 및 클라이언트 API Key 발급을 수행합니다.
|
- 관리 Plane(REST API)을 통해 도메인 등록/해제 및 클라이언트 API Key 발급을 수행합니다.
|
||||||
An admin management plane (REST API) handles domain registration/unregistration and client API key issuance.
|
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.
|
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)에 정리되어 있습니다.
|
아키텍처 세부 내용은 [`ARCHITECTURE.md`](ARCHITECTURE.md)에 정리되어 있습니다.
|
||||||
Detailed architecture is documented in [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
Detailed architecture is documented in [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
||||||
|
|
||||||
@@ -40,8 +43,8 @@ Detailed architecture is documented in [`ARCHITECTURE.md`](ARCHITECTURE.md).
|
|||||||
|
|
||||||
- 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).
|
Go 1.21+ is recommended (even if go.mod specifies an older minor).
|
||||||
- PostgreSQL (추후 DomainValidator 실제 구현 시 필요)
|
- PostgreSQL (관리 Plane + 실제 DomainValidator 에 필수)
|
||||||
PostgreSQL (only required when implementing real domain validation).
|
PostgreSQL (required for the admin plane and the real DomainValidator).
|
||||||
|
|
||||||
Go 모듈 의존성 설치 / 정리는 다음으로 수행할 수 있습니다:
|
Go 모듈 의존성 설치 / 정리는 다음으로 수행할 수 있습니다:
|
||||||
You can install/cleanup Go module deps via:
|
You can install/cleanup Go module deps via:
|
||||||
@@ -106,8 +109,8 @@ 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`.
|
DTLS server address, e.g. `localhost:8443`.
|
||||||
- `HOP_CLIENT_DOMAIN` / `HOP_CLIENT_API_KEY` : 관리 Plane 에서 발급받은 도메인/키 (현재는 DummyValidator 로 아무 값이나 허용)
|
- `HOP_CLIENT_DOMAIN` / `HOP_CLIENT_API_KEY` : 관리 Plane 에서 발급받은 도메인/키 (실제 ent + PostgreSQL 기반 DomainValidator 에 의해 검증)
|
||||||
Domain and API key issued by the admin plane (currently any values are accepted by DummyValidator).
|
Domain and API key issued by the admin plane (validated by a real ent + PostgreSQL based DomainValidator).
|
||||||
- `HOP_CLIENT_LOCAL_TARGET` : 실제로 HTTP 요청을 보낼 로컬 서버 주소
|
- `HOP_CLIENT_LOCAL_TARGET` : 실제로 HTTP 요청을 보낼 로컬 서버 주소
|
||||||
Local HTTP target address.
|
Local HTTP target address.
|
||||||
- `HOP_CLIENT_DEBUG=true` : 서버 인증서 체인 검증을 스킵(InsecureSkipVerify)하여 self-signed 인증서를 신뢰
|
- `HOP_CLIENT_DEBUG=true` : 서버 인증서 체인 검증을 스킵(InsecureSkipVerify)하여 self-signed 인증서를 신뢰
|
||||||
@@ -164,8 +167,12 @@ For implementation skeleton, see [`internal/admin`](internal/admin) and [`ent/sc
|
|||||||
|
|
||||||
- `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.
|
`Debug=true` is strictly for development/testing. Do not use self-signed certs or InsecureSkipVerify in production.
|
||||||
- 실제 운영 시에는 ACME 기반 인증서, PostgreSQL + ent 기반 DomainValidator, Proxy 레이어 연동 등을 완성해야 합니다.
|
- 현재 버전은 ACME 기반 인증서, PostgreSQL + ent 기반 DomainValidator, Proxy 레이어가 기본적으로 연동되어 있으나,
|
||||||
For production you must wire ACME certificates, a PostgreSQL+ent-based DomainValidator, and the proxy layer.
|
대용량 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.
|
HopGate is still experimental; APIs and behavior may change at any time.
|
||||||
|
|||||||
@@ -192,10 +192,45 @@ func (p *ClientProxy) forwardToLocal(ctx context.Context, preq *protocol.Request
|
|||||||
for k, vs := range res.Header {
|
for k, vs := range res.Header {
|
||||||
presp.Header[k] = append([]string(nil), vs...)
|
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 {
|
if err != nil {
|
||||||
return fmt.Errorf("read http response body: %w", err)
|
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
|
presp.Body = body
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
39
progress.md
39
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)
|
- [x] 클라이언트 main 에 Proxy loop wiring 추가: [`cmd/client/main.go`](cmd/client/main.go)
|
||||||
- handshake 성공 후 `proxy.ClientProxy.StartLoop` 실행.
|
- 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 연동
|
### 3.4 ACME Integration / ACME 연동
|
||||||
|
|||||||
Reference in New Issue
Block a user