mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2026-02-04 07:42:23 +09:00
[feat](docs): update ARCHITECTURE.md to reflect gRPC-based tunnel design
- Replaced legacy DTLS transport details with gRPC/HTTP2 tunnel architecture. - Updated server and client roles to describe gRPC bi-directional stream-based request/response handling. - Revised internal component descriptions and flow diagrams to align with gRPC-based implementation. - Marked DTLS sections as deprecated and documented planned removal in future versions.
This commit is contained in:
140
ARCHITECTURE.md
140
ARCHITECTURE.md
@@ -7,16 +7,21 @@ This document describes the overall architecture of the HopGate system. (en)
|
||||
|
||||
## 1. Overview / 전체 개요
|
||||
|
||||
- HopGate는 공인 서버와 여러 프라이빗 네트워크 클라이언트 사이에서 HTTP(S) 트래픽을 터널링하는 게이트웨이입니다. (ko)
|
||||
- HopGate는 공인 서버와 여러 프라이빗 네트워크 클라이언트 사이에서 HTTP(S) 트래픽을 터널링하는 게이트웨이입니다. (ko)
|
||||
- HopGate is a gateway that tunnels HTTP(S) traffic between a public server and multiple private-network clients. (en)
|
||||
|
||||
- 서버는 80/443 포트를 점유하고, ACME(Let's Encrypt 등)로 TLS 인증서를 자동 발급/갱신합니다. (ko)
|
||||
- 서버는 80/443 포트를 점유하고, ACME(Let's Encrypt 등)로 TLS 인증서를 자동 발급/갱신합니다. (ko)
|
||||
- The server listens on ports 80/443 and automatically issues/renews TLS certificates using ACME (e.g. Let's Encrypt). (en)
|
||||
|
||||
- 클라이언트는 DTLS를 통해 서버에 연결되고, 서버가 전달한 HTTP 요청을 로컬 서비스(127.0.0.1:PORT)에 대신 보내고 응답을 다시 서버로 전달합니다. (ko)
|
||||
- Clients connect to the server via DTLS, forward HTTP requests to local services (127.0.0.1:PORT), and send the responses back to the server. (en)
|
||||
- 전송 계층은 **TCP + TLS(HTTPS) + HTTP/2 + gRPC** 기반의 터널을 사용해 서버–클라이언트 간 HTTP 요청/응답을 멀티플렉싱합니다. (ko)
|
||||
- The transport layer uses a **TCP + TLS (HTTPS) + HTTP/2 + gRPC**-based tunnel to multiplex HTTP requests/responses between server and clients. (en)
|
||||
|
||||
- 관리 Plane(REST API)을 통해 도메인 등록/해제 및 클라이언트 API Key 발급을 수행합니다. (ko)
|
||||
- 클라이언트는 장기 유지 gRPC bi-directional stream 을 통해 서버와 터널을 형성하고,
|
||||
서버가 전달한 HTTP 요청을 로컬 서비스(127.0.0.1:PORT)에 대신 보내고 응답을 다시 서버로 전달합니다. (ko)
|
||||
- Clients establish long-lived gRPC bi-directional streams as tunnels to the server,
|
||||
forward HTTP requests to local services (127.0.0.1:PORT), and send responses back to the server. (en)
|
||||
|
||||
- 관리 Plane(REST API)을 통해 도메인 등록/해제 및 클라이언트 API Key 발급을 수행합니다. (ko)
|
||||
- An admin plane (REST API) is used to register/unregister domains and issue client API keys. (en)
|
||||
|
||||
---
|
||||
@@ -31,8 +36,7 @@ This document describes the overall architecture of the HopGate system. (en)
|
||||
├── internal/
|
||||
│ ├── config/ # shared configuration loader
|
||||
│ ├── acme/ # ACME certificate management
|
||||
│ ├── dtls/ # DTLS abstraction & implementation
|
||||
│ ├── proxy/ # HTTP proxy / tunneling core
|
||||
│ ├── proxy/ # HTTP proxy / tunneling core (gRPC tunnel)
|
||||
│ ├── protocol/ # server-client message protocol
|
||||
│ ├── admin/ # admin plane HTTP handlers
|
||||
│ └── logging/ # structured logging utilities
|
||||
@@ -46,11 +50,11 @@ This document describes the overall architecture of the HopGate system. (en)
|
||||
|
||||
### 2.1 `cmd/`
|
||||
|
||||
- [`cmd/server/main.go`](cmd/server/main.go) — 서버 실행 엔트리 포인트. 서버 설정 로딩, ACME/TLS 초기화, HTTP/HTTPS/DTLS 리스너 시작을 담당합니다. (ko)
|
||||
- [`cmd/server/main.go`](cmd/server/main.go) — Server entrypoint. Loads configuration, initializes ACME/TLS, and starts HTTP/HTTPS/DTLS listeners. (en)
|
||||
- [`cmd/server/main.go`](cmd/server/main.go) — 서버 실행 엔트리 포인트. 서버 설정 로딩, ACME/TLS 초기화, HTTP/HTTPS 리스너 및 gRPC 터널 엔드포인트 시작을 담당합니다. (ko)
|
||||
- [`cmd/server/main.go`](cmd/server/main.go) — Server entrypoint. Loads configuration, initializes ACME/TLS, and starts HTTP/HTTPS listeners plus the gRPC tunnel endpoint. (en)
|
||||
|
||||
- [`cmd/client/main.go`](cmd/client/main.go) — 클라이언트 실행 엔트리 포인트. 설정 로딩, DTLS 연결 및 핸드셰이크, 로컬 서비스 프록시 루프를 담당합니다. (ko)
|
||||
- [`cmd/client/main.go`](cmd/client/main.go) — Client entrypoint. Loads configuration, performs DTLS connection and handshake, and runs the local proxy loop. (en)
|
||||
- [`cmd/client/main.go`](cmd/client/main.go) — 클라이언트 실행 엔트리 포인트. 설정 로딩, gRPC/HTTP2 터널 연결, 로컬 서비스 프록시 루프를 담당합니다. (ko)
|
||||
- [`cmd/client/main.go`](cmd/client/main.go) — Client entrypoint. Loads configuration, establishes a gRPC/HTTP2 tunnel to the server, and runs the local proxy loop. (en)
|
||||
|
||||
---
|
||||
|
||||
@@ -72,37 +76,29 @@ This document describes the overall architecture of the HopGate system. (en)
|
||||
- ACME(예: Let's Encrypt) 클라이언트 래퍼 및 인증서 매니저를 구현하는 패키지입니다. (ko)
|
||||
- Package that will wrap an ACME client (e.g. Let's Encrypt) and manage certificates. (en)
|
||||
|
||||
- 역할 / Responsibilities: (ko/en)
|
||||
- 메인 도메인 및 프록시 서브도메인용 TLS 인증서 발급/갱신. (ko)
|
||||
- Issue/renew TLS certificates for main and proxy domains. (en)
|
||||
- HTTP-01 / TLS-ALPN-01 챌린지 처리 훅 제공. (ko)
|
||||
- Provide hooks for HTTP-01 / TLS-ALPN-01 challenges. (en)
|
||||
- HTTPS/DTLS 리스너에 사용할 `*tls.Config` 제공. (ko)
|
||||
- Provide `*tls.Config` for HTTPS/DTLS listeners. (en)
|
||||
- 역할 / Responsibilities: (ko/en)
|
||||
- 메인 도메인 및 프록시 서브도메인용 TLS 인증서 발급/갱신. (ko)
|
||||
- Issue/renew TLS certificates for main and proxy domains. (en)
|
||||
- HTTP-01 / TLS-ALPN-01 챌린지 처리 훅 제공. (ko)
|
||||
- Provide hooks for HTTP-01 / TLS-ALPN-01 challenges. (en)
|
||||
- HTTPS 및 gRPC 터널 리스너에 사용할 `*tls.Config` 제공. (ko)
|
||||
- Provide `*tls.Config` for HTTPS and gRPC tunnel listeners. (en)
|
||||
|
||||
---
|
||||
|
||||
### 2.4 `internal/dtls`
|
||||
### 2.4 (Reserved for legacy DTLS prototype)
|
||||
|
||||
- DTLS 통신을 추상화하고, pion/dtls 기반 구현 및 핸드셰이크 로직을 포함합니다. (ko)
|
||||
- Abstracts DTLS communication and includes a pion/dtls-based implementation plus handshake logic. (en)
|
||||
|
||||
- 주요 요소 / Main elements: (ko/en)
|
||||
- `Session`, `Server`, `Client` 인터페이스 — DTLS 위의 스트림과 서버/클라이언트를 추상화. (ko)
|
||||
- `Session`, `Server`, `Client` interfaces — abstract streams and server/client roles over DTLS. (en)
|
||||
- `NewPionServer`, `NewPionClient` — pion/dtls 를 사용하는 실제 구현. (ko)
|
||||
- `NewPionServer`, `NewPionClient` — concrete implementations using pion/dtls. (en)
|
||||
- `PerformServerHandshake`, `PerformClientHandshake` — 도메인 + 클라이언트 API Key 기반 애플리케이션 레벨 핸드셰이크. (ko)
|
||||
- `PerformServerHandshake`, `PerformClientHandshake` — application-level handshake based on domain + client API key. (en)
|
||||
- `NewSelfSignedLocalhostConfig` — 디버그용 localhost self-signed TLS 설정을 생성. (ko)
|
||||
- `NewSelfSignedLocalhostConfig` — generates a debug-only localhost self-signed TLS config. (en)
|
||||
> 초기 버전에서 DTLS 기반 터널을 실험했으나, 현재 설계에서는 **gRPC/HTTP2 터널만** 사용합니다.
|
||||
> DTLS 관련 코드는 점진적으로 제거하거나, 별도 브랜치/히스토리에서만 보존할 예정입니다. (ko)
|
||||
> Early iterations experimented with a DTLS-based tunnel, but the current design uses **gRPC/HTTP2 tunnels only**.
|
||||
> Any DTLS-related code is planned to be removed or kept only in historical branches. (en)
|
||||
|
||||
---
|
||||
|
||||
### 2.5 `internal/protocol`
|
||||
|
||||
- 서버와 클라이언트가 DTLS 위에서 주고받는 HTTP 요청/응답 메시지 포맷을 정의합니다. (ko)
|
||||
- Defines HTTP request/response message formats exchanged over DTLS between server and clients. (en)
|
||||
- 서버와 클라이언트가 **gRPC/HTTP2 터널 전송 계층** 위에서 주고받는 HTTP 요청/응답 및 스트림 메시지 포맷을 정의합니다. (ko)
|
||||
- Defines HTTP request/response and stream message formats exchanged over the gRPC/HTTP2 tunnel transport layer. (en)
|
||||
|
||||
- 요청 메시지 / Request message: (ko/en)
|
||||
- `RequestID`, `ClientID`, `ServiceName`, `Method`, `URL`, `Header`, `Body`. (ko/en)
|
||||
@@ -110,13 +106,15 @@ This document describes the overall architecture of the HopGate system. (en)
|
||||
- 응답 메시지 / Response message: (ko/en)
|
||||
- `RequestID`, `Status`, `Header`, `Body`, `Error`. (ko/en)
|
||||
|
||||
- 인코딩은 현재 JSON 을 사용하며, 각 HTTP 요청/응답을 하나의 Envelope 로 감싸 DTLS 위에서 전송합니다. (ko)
|
||||
- Encoding currently uses JSON, wrapping each HTTP request/response in a single Envelope sent over DTLS. (en)
|
||||
- 스트림 기반 터널링을 위한 Envelope/Stream 타입: (ko/en)
|
||||
- [`Envelope`](internal/protocol/protocol.go:64) — 상위 메시지 컨테이너. (ko/en)
|
||||
- [`StreamOpen`](internal/protocol/protocol.go:94) — 새로운 스트림 오픈 및 헤더/메타데이터 전달. (ko/en)
|
||||
- [`StreamData`](internal/protocol/protocol.go:104) — 시퀀스 번호(Seq)를 가진 바디 chunk 프레임. (ko/en)
|
||||
- [`StreamClose`](internal/protocol/protocol.go:143) — 스트림 종료 및 에러 정보 전달. (ko/en)
|
||||
- [`StreamAck`](internal/protocol/protocol.go:117) — 선택적 재전송(Selective Retransmission)을 위한 ACK/NACK 힌트. (ko/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)
|
||||
- 이 구조는 Protobuf 기반 length-prefix 프레이밍을 사용하며, gRPC bi-di stream 의 메시지 타입으로 매핑됩니다. (ko)
|
||||
- This structure uses protobuf-based length-prefixed framing and is mapped onto messages in a gRPC bi-di stream. (en)
|
||||
|
||||
---
|
||||
|
||||
@@ -127,28 +125,27 @@ This document describes the overall architecture of the HopGate system. (en)
|
||||
|
||||
#### 서버 측 역할 / Server-side role
|
||||
|
||||
- 공인 HTTPS 엔드포인트에서 들어오는 요청을 수신합니다. (ko)
|
||||
- 공인 HTTPS 엔드포인트에서 들어오는 요청을 수신합니다. (ko)
|
||||
- Receive incoming requests on the public HTTPS endpoint. (en)
|
||||
|
||||
- 도메인/패스 규칙에 따라 적절한 클라이언트와 서비스로 매핑합니다. (ko)
|
||||
- 도메인/패스 규칙에 따라 적절한 클라이언트와 서비스로 매핑합니다. (ko)
|
||||
- Map requests to appropriate clients and services based on domain/path rules. (en)
|
||||
|
||||
- 요청을 `protocol.Request` 로 직렬화하여 DTLS 세션을 통해 클라이언트로 전송합니다. (ko)
|
||||
- Serialize the request as `protocol.Request` and send it over a DTLS session to the client. (en)
|
||||
|
||||
- 클라이언트로부터 받은 `protocol.Response` 를 HTTP 응답으로 복원하여 외부 사용자에게 반환합니다. (ko)
|
||||
- Deserialize `protocol.Response` from the client and return it as an HTTP response to the external user. (en)
|
||||
- 요청/응답을 `internal/protocol` 의 스트림 메시지(`StreamOpen` / `StreamData` / `StreamClose` 등)로 직렬화하여
|
||||
서버–클라이언트 간 gRPC bi-di stream 위에서 주고받습니다. (ko)
|
||||
- Serialize requests/responses into stream messages from `internal/protocol` (`StreamOpen` / `StreamData` / `StreamClose`, etc.)
|
||||
and exchange them between server and clients over a gRPC bi-di stream. (en)
|
||||
|
||||
#### 클라이언트 측 역할 / Client-side role
|
||||
|
||||
- DTLS 채널을 통해 서버가 내려보낸 `protocol.Request` 를 수신합니다. (ko)
|
||||
- Receive `protocol.Request` objects sent by the server over DTLS. (en)
|
||||
- 서버가 gRPC 터널을 통해 내려보낸 스트림 메시지를 수신합니다. (ko)
|
||||
- Receive stream messages sent by the server over the gRPC tunnel. (en)
|
||||
|
||||
- 로컬 HTTP 서비스(예: 127.0.0.1:8080)에 요청을 전달하고 응답을 수신합니다. (ko)
|
||||
- 로컬 HTTP 서비스(예: 127.0.0.1:8080)에 요청을 전달하고 응답을 수신합니다. (ko)
|
||||
- Forward these requests to local HTTP services (e.g. 127.0.0.1:8080) and collect responses. (en)
|
||||
|
||||
- 응답을 `protocol.Response` 로 직렬화하여 DTLS 채널을 통해 서버로 전송합니다. (ko)
|
||||
- Serialize responses as `protocol.Response` and send them back to the server over DTLS. (en)
|
||||
- 응답을 동일한 gRPC bi-di stream 상의 역방향 스트림 메시지로 직렬화하여 서버로 전송합니다. (ko)
|
||||
- Serialize responses as reverse-direction stream messages on the same gRPC bi-di stream and send them back to the server. (en)
|
||||
|
||||
---
|
||||
|
||||
@@ -192,20 +189,24 @@ An external user sends an HTTPS request to `https://proxy.example.com/service-a/
|
||||
2. HopGate 서버의 HTTPS 리스너가 요청을 수신합니다. (ko)
|
||||
The HTTPS listener on the HopGate server receives the request. (en)
|
||||
|
||||
3. `proxy` 레이어가 도메인과 경로를 기반으로 이 요청을 처리할 클라이언트(예: client-1)와 해당 로컬 서비스(`service-a`)를 결정합니다. (ko)
|
||||
3. `proxy` 레이어가 도메인과 경로를 기반으로 이 요청을 처리할 클라이언트(예: client-1)와 해당 로컬 서비스(`service-a`)를 결정합니다. (ko)
|
||||
The `proxy` layer decides which client (e.g., client-1) and which local service (`service-a`) should handle the request, based on domain and path. (en)
|
||||
|
||||
4. 서버는 요청을 `protocol.Request` 구조로 직렬화하고, `dtls.Session` 을 통해 선택된 클라이언트로 전송합니다. (ko)
|
||||
The server serializes the request into a `protocol.Request` and sends it to the selected client over a `dtls.Session`. (en)
|
||||
4. 서버는 요청을 `internal/protocol` 의 스트림 메시지(예: `StreamOpen` + 여러 `StreamData` + `StreamClose`)로 직렬화하고,
|
||||
선택된 클라이언트와 맺은 gRPC bi-di stream 을 통해 전송합니다. (ko)
|
||||
The server serializes the request into stream messages from `internal/protocol` (e.g., `StreamOpen` + multiple `StreamData` + `StreamClose`)
|
||||
and sends them over a gRPC bi-di stream to the selected client. (en)
|
||||
|
||||
5. 클라이언트의 `proxy` 레이어는 `protocol.Request` 를 수신하고, 로컬 서비스(예: 127.0.0.1:8080)에 HTTP 요청을 수행합니다. (ko)
|
||||
The client’s `proxy` layer receives the `protocol.Request` and performs an HTTP request to a local service (e.g., 127.0.0.1:8080). (en)
|
||||
5. 클라이언트의 `proxy` 레이어는 이 스트림 메시지들을 수신해 로컬 서비스(예: 127.0.0.1:8080)에 HTTP 요청을 수행합니다. (ko)
|
||||
The client’s `proxy` layer receives these stream messages and performs an HTTP request to a local service (e.g., 127.0.0.1:8080). (en)
|
||||
|
||||
6. 클라이언트는 로컬 서비스로부터 HTTP 응답을 수신하고, 이를 `protocol.Response` 로 직렬화하여 DTLS 채널을 통해 서버로 다시 전송합니다. (ko)
|
||||
The client receives the HTTP response from the local service, serializes it as a `protocol.Response`, and sends it back to the server over DTLS. (en)
|
||||
6. 클라이언트는 로컬 서비스로부터 HTTP 응답을 수신하고, 이를 역방향 스트림 메시지(`StreamOpen` + 여러 `StreamData` + `StreamClose`)로 직렬화하여
|
||||
동일한 gRPC bi-di stream 을 통해 서버로 다시 전송합니다. (ko)
|
||||
The client receives the HTTP response from the local service, serializes it as reverse-direction stream messages
|
||||
(`StreamOpen` + multiple `StreamData` + `StreamClose`), and sends them back to the server over the same gRPC bi-di stream. (en)
|
||||
|
||||
7. 서버는 `protocol.Response` 를 디코딩하여 원래의 HTTPS 요청에 대한 HTTP 응답으로 변환한 뒤, 외부 사용자에게 반환합니다. (ko)
|
||||
The server decodes the `protocol.Response`, converts it back into an HTTP response, and returns it to the original external user. (en)
|
||||
7. 서버는 응답 스트림 메시지를 조립해 원래의 HTTPS 요청에 대한 HTTP 응답으로 변환한 뒤, 외부 사용자에게 반환합니다. (ko)
|
||||
The server reassembles the response stream messages into an HTTP response for the original HTTPS request and returns it to the external user. (en)
|
||||
|
||||

|
||||
|
||||
@@ -213,25 +214,26 @@ The server decodes the `protocol.Response`, converts it back into an HTTP respon
|
||||
|
||||
## 4. Next Steps / 다음 단계
|
||||
|
||||
- 위 아키텍처를 기반으로 디렉터리와 엔트리 포인트를 생성/정리합니다. (ko)
|
||||
- 위 아키텍처를 기반으로 디렉터리와 엔트리 포인트를 생성/정리합니다. (ko)
|
||||
- Use this architecture to create/organize directories and entrypoints. (en)
|
||||
|
||||
- `internal/config` 에 필요한 설정 필드와 `.env` 로더를 확장합니다. (ko)
|
||||
- `internal/config` 에 필요한 설정 필드와 `.env` 로더를 확장합니다. (ko)
|
||||
- Extend `internal/config` with required config fields and `.env` loaders. (en)
|
||||
|
||||
- `internal/acme` 에 ACME 클라이언트(certmagic 또는 lego 등)를 연결해 TLS 인증서 발급/갱신을 구현합니다. (ko)
|
||||
- `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)
|
||||
- Stabilize the pion/dtls-based DTLS transport and handshake logic in `internal/dtls`. (en)
|
||||
- gRPC/HTTP2 기반 터널 전송 계층을 설계/구현하고, 서버/클라이언트 모두에서 장기 유지 bi-di stream 위에
|
||||
HTTP 요청/응답을 멀티플렉싱하는 로직을 추가합니다. (ko)
|
||||
- Design and implement a gRPC/HTTP2-based tunnel transport layer, adding logic on both server and client to multiplex HTTP requests/responses over long-lived bi-di streams. (en)
|
||||
|
||||
- `internal/protocol` 과 `internal/proxy` 를 통해 실제 HTTP 터널링을 구현하고,
|
||||
단일 JSON Envelope 기반 모델에서 `StreamOpen` / `StreamData` / `StreamClose` 중심의 스트림 기반 DTLS 터널링으로 전환합니다. (ko)
|
||||
gRPC 기반 스트림 모델이 재사용할 수 있는 논리 프로토콜로 정리합니다. (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)
|
||||
organizing the logical protocol so that the gRPC-based stream model can reuse it. (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)
|
||||
|
||||
- 로깅/메트릭을 Prometheus + Loki + Grafana 스택과 연동하여 운영 가시성을 확보합니다. (ko)
|
||||
- 로깅/메트릭을 Prometheus + Loki + Grafana 스택과 연동하여 운영 가시성을 확보합니다. (ko)
|
||||
- Integrate logging/metrics with the Prometheus + Loki + Grafana stack to gain operational visibility. (en)
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 817 KiB After Width: | Height: | Size: 2.6 MiB |
@@ -4,7 +4,7 @@ Please draw a clean, modern system architecture diagram for a project called "Ho
|
||||
|
||||
=== High-level concept ===
|
||||
- HopGate is a reverse HTTP gateway.
|
||||
- A single public server terminates HTTPS and DTLS, and tunnels HTTP traffic to multiple clients.
|
||||
- A single public server terminates HTTPS and exposes a tunnel endpoint (gRPC/HTTP2) to tunnel HTTP traffic to multiple clients.
|
||||
- Each client runs in a private network and forwards HTTP requests to local services (127.0.0.1:PORT).
|
||||
|
||||
=== Main components to draw ===
|
||||
@@ -19,8 +19,8 @@ Please draw a clean, modern system architecture diagram for a project called "Ho
|
||||
- Terminates TLS using ACME certificates for main and proxy domains.
|
||||
b. "HTTP Listener (TCP 80)"
|
||||
- Handles ACME HTTP-01 challenges and redirects HTTP to HTTPS.
|
||||
c. "DTLS Listener (UDP 443 or 8443)"
|
||||
- Terminates DTLS sessions from multiple clients.
|
||||
c. "Tunnel Endpoint (gRPC)"
|
||||
- gRPC/HTTP2 listener on the same HTTPS port (TCP 443) tunnel streams.
|
||||
d. "Admin API / Management Plane"
|
||||
- REST API base path: /api/v1/admin
|
||||
- Endpoints:
|
||||
@@ -31,8 +31,9 @@ Please draw a clean, modern system architecture diagram for a project called "Ho
|
||||
- Routes incoming HTTP(S) requests to the correct client based on domain and path.
|
||||
f. "ACME Certificate Manager"
|
||||
- Automatically issues and renews TLS certificates (Let's Encrypt).
|
||||
g. "DTLS Session Manager"
|
||||
- Manages DTLS connections and per-domain sessions with clients.
|
||||
g. "Tunnel Session Manager"
|
||||
- Manages tunnel connections and per-domain sessions with clients
|
||||
(gRPC streams).
|
||||
h. "Metrics & Logging"
|
||||
- Structured JSON logs shipped to Prometheus / Loki / Grafana stack.
|
||||
|
||||
@@ -50,35 +51,34 @@ Please draw a clean, modern system architecture diagram for a project called "Ho
|
||||
- Draw 2–3 separate client boxes to show that multiple clients can connect.
|
||||
- Each box titled "HopGate Client".
|
||||
- Inside each client box, show:
|
||||
a. "DTLS Client"
|
||||
- Connects to HopGate Server via DTLS.
|
||||
- Performs handshake with:
|
||||
- domain
|
||||
- client_api_key
|
||||
a. "Tunnel Client"
|
||||
- gRPC client that opens a long-lived bi-directional gRPC stream over HTTPS (HTTP/2).
|
||||
b. "Client Proxy"
|
||||
- Receives HTTP requests from the server over DTLS.
|
||||
- Receives HTTP request frames from the server over the tunnel (gRPC stream).
|
||||
- Forwards them to local services such as:
|
||||
- 127.0.0.1:8080 (web)
|
||||
- 127.0.0.1:9000 (admin)
|
||||
c. "Local Services"
|
||||
- A small group of boxes representing local HTTP servers.
|
||||
|
||||
=== Flows to highlight ===
|
||||
1) User HTTP Flow
|
||||
- External user -> HTTPS Listener -> Reverse Proxy Core -> DTLS Session Manager -> Specific HopGate Client -> Local Service -> back through same path to the user.
|
||||
- External user -> HTTPS Listener -> Reverse Proxy Core ->
|
||||
gRPC Tunnel Endpoint -> specific HopGate Client (gRPC stream) -> Local Service ->
|
||||
back through same path to the user.
|
||||
|
||||
2) Admin Flow
|
||||
- Administrator -> Admin API (with Bearer admin key) -> PostgreSQL + ent ORM:
|
||||
- Register domain + memo -> returns client_api_key.
|
||||
- Unregister domain + client_api_key.
|
||||
|
||||
3) DTLS Handshake Flow
|
||||
- From client to server over DTLS:
|
||||
- Client sends {domain, client_api_key}.
|
||||
- Server validates against PostgreSQL Domain table.
|
||||
- On success, both sides log:
|
||||
- server: which domain is bound to the session.
|
||||
- client: success message, bound domain, and local_target (local service address).
|
||||
3) Tunnel Handshake / Session Establishment Flow
|
||||
- v1: DTLS Handshake Flow (legacy) - (REMOVED)
|
||||
- v2: gRPC Tunnel Establishment Flow:
|
||||
- From client to server over HTTPS (HTTP/2):
|
||||
- Client opens a long-lived bi-directional gRPC stream (e.g. OpenTunnel).
|
||||
- First frame includes {domain, client_api_key} and client metadata.
|
||||
- Server validates against PostgreSQL Domain table and associates the gRPC stream with that domain.
|
||||
- Subsequent frames carry HTTP request/response metadata and body chunks.
|
||||
|
||||
=== Visual style ===
|
||||
- Clean flat design, no 3D.
|
||||
@@ -93,4 +93,4 @@ Please draw a clean, modern system architecture diagram for a project called "Ho
|
||||
- Database near the server,
|
||||
- Multiple clients and their local services on the right or bottom.
|
||||
|
||||
Please output a single high-resolution architecture diagram that matches this description.
|
||||
Please output a single high-resolution architecture diagram that matches this description.
|
||||
|
||||
310
progress.md
310
progress.md
@@ -224,286 +224,46 @@ This document tracks implementation progress against the HopGate architecture an
|
||||
|
||||
---
|
||||
|
||||
### 3.3 Proxy Core / HTTP Tunneling
|
||||
### 3.3 Proxy Core / gRPC Tunneling
|
||||
|
||||
- [ ] 서버 측 Proxy 구현 확장: [`internal/proxy/server.go`](internal/proxy/server.go)
|
||||
- 현재 `ServerProxy` / `Router` 인터페이스와 `NewHTTPServer` 만 정의되어 있고,
|
||||
실제 HTTP/HTTPS 리스너와 DTLS 세션 매핑 로직은 [`cmd/server/main.go`](cmd/server/main.go) 의
|
||||
`newHTTPHandler` / `dtlsSessionWrapper.ForwardHTTP` 안에 위치합니다.
|
||||
- Proxy 코어 로직을 proxy 레이어로 이동하는 리팩터링은 아직 진행되지 않았습니다. (3.6 항목과 연동)
|
||||
HopGate 의 최종 목표는 **TCP + TLS(HTTPS) + HTTP/2 + gRPC** 기반 터널로 HTTP 트래픽을 전달하는 것입니다.
|
||||
이 섹션에서는 DTLS 기반 초기 설계를 정리만 남기고, 실제 구현/남은 작업은 gRPC 터널 기준으로 재정의합니다.
|
||||
|
||||
- [x] 클라이언트 측 Proxy 구현 확장: [`internal/proxy/client.go`](internal/proxy/client.go)
|
||||
- DTLS 세션에서 `protocol.Request` 수신 → 로컬 HTTP 호출 → `protocol.Response` 전송 루프 구현.
|
||||
- timeout/취소/에러 처리.
|
||||
- [ ] 서버 측 gRPC 터널 엔드포인트 설계/구현
|
||||
- 외부 사용자용 HTTPS(443/TCP)와 같은 포트에서:
|
||||
- 일반 HTTP 요청(브라우저/REST)은 기존 리버스 프록시 경로로,
|
||||
- `Content-Type: application/grpc` 인 요청은 클라이언트 터널용 gRPC 서버로
|
||||
라우팅하는 구조를 설계합니다.
|
||||
- 예시: `rpc OpenTunnel(stream TunnelFrame) returns (stream TunnelFrame)` (bi-directional streaming).
|
||||
- HTTP/2 + ALPN(h2)을 사용해 gRPC 스트림을 유지하고, 요청/응답 HTTP 메시지를 `TunnelFrame`으로 멀티플렉싱합니다.
|
||||
|
||||
- [x] 서버 main 에 Proxy wiring 추가: [`cmd/server/main.go`](cmd/server/main.go)
|
||||
- DTLS handshake 완료된 세션을 Proxy 라우팅 테이블에 등록.
|
||||
- HTTPS 서버와 Proxy 핸들러 연결.
|
||||
- [ ] 클라이언트 측 gRPC 터널 설계/구현
|
||||
- 클라이언트 프로세스는 HopGate 서버로 장기 유지 bi-di gRPC 스트림을 **하나(또는 소수 개)** 연 상태로 유지합니다.
|
||||
- 서버로부터 들어오는 `TunnelFrame`(요청 메타데이터 + 바디 chunk)을 수신해,
|
||||
로컬 HTTP 서비스(예: `127.0.0.1:8080`)로 proxy 하고, 응답을 다시 `TunnelFrame` 시퀀스로 전송합니다.
|
||||
- 기존 `internal/proxy/client.go` 의 HTTP 매핑/스트림 ARQ 경험을, gRPC 메시지 단위 chunk/flow-control 설계에 참고합니다.
|
||||
|
||||
- [x] 클라이언트 main 에 Proxy loop wiring 추가: [`cmd/client/main.go`](cmd/client/main.go)
|
||||
- handshake 성공 후 `proxy.ClientProxy.StartLoop` 실행.
|
||||
- [ ] HTTP ↔ gRPC 터널 매핑 규약 정의
|
||||
- 한 HTTP 요청/응답 쌍을 gRPC 스트림 상에서 어떻게 표현할지 스키마를 정의합니다:
|
||||
- 요청: `StreamID`, method, URL, headers, body chunks
|
||||
- 응답: `StreamID`, status, headers, body chunks, error
|
||||
- 현재 `internal/protocol/protocol.go`의 논리 모델(Envelope/StreamOpen/StreamData/StreamClose/StreamAck)을
|
||||
gRPC 메시지(oneof 필드 등)로 직렬화할지, 또는 새로운 gRPC 전용 메시지를 정의할지 결정합니다.
|
||||
- Back-pressure / flow-control 은 gRPC/HTTP2의 스트림 flow-control 을 최대한 활용하고,
|
||||
추가 application-level windowing 이 필요하면 최소한으로만 도입합니다.
|
||||
|
||||
#### 3.3A Stream-based DTLS Tunneling / 스트림 기반 DTLS 터널링
|
||||
|
||||
초기 HTTP 터널링 설계는 **단일 JSON Envelope + 단일 DTLS 쓰기** 방식(요청/응답 바디 전체를 한 번에 전송)이었고,
|
||||
대용량 응답 바디에서 UDP MTU 한계로 인한 `sendto: message too long` 문제가 발생할 수 있었습니다.
|
||||
이 한계를 제거하기 위해, 현재 코드는 DTLS 위 애플리케이션 프로토콜을 **스트림/프레임 기반**으로 재설계하여 `StreamOpen` / `StreamData` / `StreamClose` 를 사용합니다.
|
||||
The initial tunneling model used a **single JSON envelope + single DTLS write per HTTP message**, which could hit UDP MTU limits (`sendto: message too long`) for large bodies.
|
||||
The current implementation uses a **stream/frame-based** protocol over DTLS (`StreamOpen` / `StreamData` / `StreamClose`), and this section documents its constraints and further improvements (e.g. ARQ).
|
||||
|
||||
고려해야 할 제약 / Constraints:
|
||||
|
||||
- 전송 계층은 DTLS(pion/dtls)를 유지합니다.
|
||||
The transport layer must remain DTLS (pion/dtls).
|
||||
- JSON 기반 단일 Envelope 모델에서 벗어나, HTTP 바디를 안전한 크기의 chunk 로 나누어 전송해야 합니다.
|
||||
We must move away from the single-envelope JSON model and chunk HTTP bodies under a safe MTU.
|
||||
- UDP 특성상 일부 프레임 손실/오염에 대비해, **해당 chunk 만 재전송 요청할 수 있는 ARQ 메커니즘**이 필요합니다.
|
||||
Given UDP characteristics, we need an application-level ARQ so that **only lost/corrupted chunks are retransmitted**.
|
||||
|
||||
아래 단계들은 `feature/udp-stream` 브랜치에서 구현할 구체적인 작업 항목입니다.
|
||||
The following tasks describe concrete work items to be implemented on the `feature/udp-stream` branch.
|
||||
|
||||
---
|
||||
|
||||
##### 3.3A.1 스트림 프레이밍 프로토콜 설계 (JSON 1단계)
|
||||
##### 3.3A.1 Stream framing protocol (JSON, phase 1)
|
||||
|
||||
- [x] 스트림 프레임 타입 정리 및 확장: [`internal/protocol/protocol.go`](internal/protocol/protocol.go:35)
|
||||
- 이미 정의된 스트림 관련 타입을 1단계에서 적극 활용합니다.
|
||||
Reuse the already defined stream-related types in phase 1:
|
||||
- `MessageTypeStreamOpen`, `MessageTypeStreamData`, `MessageTypeStreamClose`
|
||||
- [`Envelope`](internal/protocol/protocol.go:52), [`StreamOpen`](internal/protocol/protocol.go:69), [`StreamData`](internal/protocol/protocol.go:80), [`StreamClose`](internal/protocol/protocol.go:86)
|
||||
- `StreamData` 에 per-stream 시퀀스 번호를 추가합니다.
|
||||
Add a per-stream sequence number to `StreamData`:
|
||||
- 예시 / Example:
|
||||
```go
|
||||
type StreamData struct {
|
||||
ID StreamID `json:"id"`
|
||||
Seq uint64 `json:"seq"` // 0부터 시작하는 per-stream sequence
|
||||
Data []byte `json:"data"`
|
||||
}
|
||||
```
|
||||
|
||||
- [x] 스트림 ACK / 재전송 제어 메시지 추가: [`internal/protocol/protocol.go`](internal/protocol/protocol.go:52)
|
||||
- 선택적 재전송(Selective Retransmission)을 위해 `StreamAck` 메시지와 `MessageTypeStreamAck` 를 추가합니다.
|
||||
Add `StreamAck` message and `MessageTypeStreamAck` for selective retransmission:
|
||||
```go
|
||||
const (
|
||||
MessageTypeStreamAck MessageType = "stream_ack"
|
||||
)
|
||||
|
||||
type StreamAck struct {
|
||||
ID StreamID `json:"id"` // 대상 스트림 / target stream
|
||||
AckSeq uint64 `json:"ack_seq"` // 연속으로 수신 완료한 마지막 Seq / last contiguous sequence
|
||||
LostSeqs []uint64 `json:"lost_seqs"` // 누락된 시퀀스 목록(선택) / optional list of missing seqs
|
||||
WindowSize uint32 `json:"window_size"` // 선택: 허용 in-flight 프레임 수 / optional receive window
|
||||
}
|
||||
```
|
||||
- [`Envelope`](internal/protocol/protocol.go:52)에 `StreamAck *StreamAck` 필드를 추가합니다.
|
||||
Extend `Envelope` with a `StreamAck *StreamAck` field.
|
||||
|
||||
- [x] MTU-safe chunk 크기 정의
|
||||
- DTLS/UDP 헤더 및 Protobuf/length-prefix 오버헤드를 고려해 안전한 payload 크기(4KiB)를 상수로 정의합니다.
|
||||
Define a safe payload size constant (4KiB) considering DTLS/UDP headers and Protobuf/length-prefix framing.
|
||||
- 이 값은 [`internal/protocol/protocol.go`](internal/protocol/protocol.go:32) 의 `StreamChunkSize` 로 정의되었습니다.
|
||||
Implemented as `StreamChunkSize` in [`internal/protocol/protocol.go`](internal/protocol/protocol.go:32).
|
||||
- 이후 HTTP 바디 스트림 터널링 구현 시, 모든 `StreamData.Data` 는 이 크기 이하 chunk 로 잘라 전송해야 합니다.
|
||||
In the stream tunneling implementation, every `StreamData.Data` must be sliced into chunks no larger than this size.
|
||||
|
||||
---
|
||||
|
||||
##### 3.3A.2 애플리케이션 레벨 ARQ 설계 (Selective Retransmission)
|
||||
##### 3.3A.2 Application-level ARQ (Selective Retransmission)
|
||||
|
||||
- [x] 수신 측 ARQ 상태 관리 구현
|
||||
- 스트림별로 `expectedSeq`, out-of-order chunk 버퍼(`received`), 누락 시퀀스 집합(`lost`)을 유지하면서,
|
||||
in-order / out-of-order 프레임을 구분해 HTTP 바디 버퍼에 순서대로 쌓습니다.
|
||||
- For each stream, maintain `expectedSeq`, an out-of-order buffer (`received`), and a lost-sequence set (`lost`),
|
||||
delivering in-order frames directly to the HTTP body buffer while buffering/reordering out-of-order ones.
|
||||
|
||||
- [x] 수신 측 StreamAck 전송 정책 구현
|
||||
- 각 `StreamData` 수신 시점에 `AckSeq = expectedSeq - 1` 과 현재 윈도우에서 누락된 시퀀스 일부(`LostSeqs`, 상한 개수 적용)를 포함한
|
||||
`StreamAck{AckSeq, LostSeqs}` 를 전송해 선택적 재전송을 유도합니다.
|
||||
- On every `StreamData` frame, send `StreamAck{AckSeq, LostSeqs}` where `AckSeq = expectedSeq - 1` and `LostSeqs`
|
||||
contains a bounded set (up to a fixed limit) of missing sequence numbers in the current receive window.
|
||||
|
||||
- [x] 송신 측 재전송 로직 구현 (StreamAck 기반)
|
||||
- 응답 스트림 송신 측에서 스트림별 `streamSender` 를 두고, `outstanding[seq] = payload` 로 아직 Ack 되지 않은 프레임을 추적합니다.
|
||||
- `StreamAck{AckSeq, LostSeqs}` 수신 시:
|
||||
- `seq <= AckSeq` 인 항목은 모두 제거하고,
|
||||
- `LostSeqs` 에 포함된 시퀀스에 대해서만 `StreamData{ID, Seq, Data}` 를 재전송합니다.
|
||||
- A per-stream `streamSender` tracks `outstanding[seq] = payload` for unacknowledged frames. Upon receiving
|
||||
`StreamAck{AckSeq, LostSeqs}`, it deletes all `seq <= AckSeq` and retransmits only frames whose sequence
|
||||
numbers appear in `LostSeqs`.
|
||||
|
||||
> Note: 현재 구현은 StreamAck 기반 **선택적 재전송(Selective Retransmission)** 까지 포함하며,
|
||||
> 별도의 RTO(재전송 타이머) 기반 백그라운드 재전송 루프는 향후 확장 여지로 남겨둔 상태입니다.
|
||||
> Note: The current implementation covers StreamAck-based **selective retransmission**; a separate RTO-based
|
||||
> background retransmission loop is left as a potential future enhancement.
|
||||
|
||||
---
|
||||
|
||||
##### 3.3A.3 HTTP ↔ 스트림 매핑 (서버/클라이언트)
|
||||
##### 3.3A.3 HTTP ↔ stream mapping (server/client)
|
||||
|
||||
- [x] 서버 → 클라이언트 요청 스트림: [`cmd/server/main.go`](cmd/server/main.go:200)
|
||||
- `ForwardHTTP` 는 스트림 기반 HTTP 요청/응답을 처리하도록 구현되어 있으며, 동작은 다음과 같습니다.
|
||||
`ForwardHTTP` is implemented in stream mode and behaves as follows:
|
||||
- HTTP 요청 수신 시:
|
||||
- 새로운 `StreamID` 를 발급합니다 (세션별 증가).
|
||||
Generate a new `StreamID` per incoming HTTP request on the DTLS session.
|
||||
- `StreamOpen` 전송:
|
||||
- 요청 메서드/URL/헤더를 [`StreamOpen`](internal/protocol/protocol.go:69) 의 `Header` 혹은 pseudo-header 로 encode.
|
||||
Encode method/URL/headers into `StreamOpen.Header` or a pseudo-header scheme.
|
||||
- 요청 바디를 읽으면서 `StreamData{ID, Seq, Data}` 를 지속적으로 전송합니다.
|
||||
Read the HTTP request body and send it as a sequence of `StreamData` frames.
|
||||
- 바디 종료 시 `StreamClose{ID, Error:""}` 를 전송합니다.
|
||||
When the body ends, send `StreamClose{ID, Error:""}`.
|
||||
- 응답 수신:
|
||||
- 클라이언트에서 오는 역방향 `StreamOpen` 으로 HTTP status/header 를 수신하고,
|
||||
이를 `http.ResponseWriter` 에 반영합니다.
|
||||
Receive response status/headers via reverse-direction `StreamOpen` and map them to `http.ResponseWriter`.
|
||||
- 연속되는 `StreamData` 를 수신할 때마다 `http.ResponseWriter.Write` 로 chunk 를 바로 전송합니다.
|
||||
For each `StreamData`, write the chunk directly to the HTTP response.
|
||||
- `StreamClose` 수신 시 응답 종료 및 스트림 자원 정리.
|
||||
On `StreamClose`, finish the response and clean up per-stream state.
|
||||
|
||||
- [x] 클라이언트에서의 요청 처리 스트림: [`internal/proxy/client.go`](internal/proxy/client.go:200)
|
||||
- 서버로부터 들어오는 `StreamOpen{ID, ...}` 을 수신하면,
|
||||
새로운 goroutine 을 띄워 해당 ID에 대한 로컬 HTTP 요청을 수행합니다.
|
||||
On receiving `StreamOpen{ID, ...}` from the server, spawn a goroutine to handle the local HTTP request for that stream ID.
|
||||
- 스트림별로 `io.Pipe` 또는 채널 기반 바디 리더를 준비하고,
|
||||
`StreamData` 프레임을 수신할 때마다 이 파이프에 쓰도록 합니다.
|
||||
Prepare an `io.Pipe` (or channel-backed reader) per stream and write incoming `StreamData` chunks into it.
|
||||
- 로컬 HTTP 클라이언트 응답은 반대로:
|
||||
For the local HTTP client response:
|
||||
- 응답 status/header → `StreamOpen` (client → server)
|
||||
- 응답 바디 → 여러 개의 `StreamData`
|
||||
- 종료 시점에 `StreamClose` 전송
|
||||
Send `StreamOpen` (status/headers), then a sequence of `StreamData`, followed by `StreamClose` when done.
|
||||
|
||||
---
|
||||
|
||||
##### 3.3A.4 JSON → 바이너리 직렬화로의 잠재적 전환 (2단계)
|
||||
##### 3.3A.4 JSON → binary serialization (potential phase 2)
|
||||
|
||||
- [x] JSON 기반 스트림 프로토콜의 1단계 구현/안정화 이후, 직렬화 포맷 재검토 및 Protobuf 전환
|
||||
- 현재는 JSON 대신 Protobuf length-prefix `Envelope` 포맷을 기본으로 사용합니다.
|
||||
The runtime now uses a Protobuf-based, length-prefixed `Envelope` format instead of JSON.
|
||||
- HTTP/스트림 payload 는 여전히 MTU-safe 크기(예: 4KiB, `StreamChunkSize`)로 제한되어 있어, 단일 프레임이 과도하게 커지지 않습니다.
|
||||
HTTP/stream payloads remain bounded to an MTU-safe size (e.g. 4KiB via `StreamChunkSize`), so individual frames stay small.
|
||||
- [x] length-prefix 이진 프레임(Protobuf)으로 전환
|
||||
- 동일한 logical model (`StreamOpen` / `StreamData(seq)` / `StreamClose` / `StreamAck`)을 유지한 채,
|
||||
wire-format 을 Protobuf length-prefix binary 프레이밍으로 교체했고, 이는 `protobufCodec` 으로 구현되었습니다.
|
||||
We now keep the same logical model while using Protobuf length-prefixed framing via `protobufCodec`.
|
||||
- [x] 이 전환은 `internal/protocol` 내 직렬화 레이어를 얇은 abstraction 으로 감싸 구현했습니다.
|
||||
- [`internal/protocol/codec.go`](internal/protocol/codec.go:130) 에 `WireCodec` 인터페이스와 Protobuf 기반 `DefaultCodec` 을 도입해,
|
||||
호출자는 `protocol.DefaultCodec` 만 사용하고, JSON codec 은 보조 용도로만 남아 있습니다.
|
||||
In [`internal/protocol/codec.go`](internal/protocol/codec.go:130), the `WireCodec` abstraction and Protobuf-based `DefaultCodec` allow callers to use only `protocol.DefaultCodec` while JSON remains as an auxiliary codec.
|
||||
|
||||
---
|
||||
|
||||
##### 3.3B DTLS Session Multiplexing / 세션 내 다중 HTTP 요청 처리
|
||||
|
||||
현재 구현은 클라이언트 측에서 단일 DTLS 세션 내에 **동시에 하나의 HTTP 요청 스트림만** 처리할 수 있습니다.
|
||||
`ClientProxy.handleStreamRequest` 가 DTLS 세션의 reader 를 직접 소비하기 때문에, 동일 세션에서 두 번째 `StreamOpen` 이 섞여 들어오면 프로토콜 위반으로 간주되고 세션이 끊어집니다.
|
||||
이 섹션은 **클라이언트 측 스트림 demux + per-stream goroutine 구조**를 도입해, 하나의 DTLS 세션 안에서 여러 HTTP 요청을 안전하게 병렬 처리하기 위한 단계입니다.
|
||||
|
||||
Currently, the client can effectively handle **only one HTTP request stream at a time per DTLS session**.
|
||||
Because `ClientProxy.handleStreamRequest` directly consumes the DTLS session reader, an additional `StreamOpen` for a different stream interleaving on the same session is treated as a protocol error and tears down the session.
|
||||
This section introduces a **client-side stream demultiplexer + per-stream goroutines** to safely support multiple concurrent HTTP requests within a single DTLS session.
|
||||
|
||||
---
|
||||
|
||||
##### 3.3B.1 클라이언트 측 중앙 readLoop → 스트림 demux 설계
|
||||
##### 3.3B.1 Design client-side central readLoop → per-stream demux
|
||||
|
||||
- [x] `ClientProxy.StartLoop` 의 역할을 명확히 분리
|
||||
- DTLS 세션에서 `Envelope` 를 연속해서 읽어들이는 **중앙 readLoop** 를 유지하되,
|
||||
- 개별 스트림의 HTTP 처리 로직(현재 `handleStreamRequest` 내부 로직)을 분리해 별도 타입/구조체로 옮길 계획을 문서화합니다.
|
||||
- [x] 스트림 demux 위한 자료구조 설계
|
||||
- `map[protocol.StreamID]*streamReceiver` 형태의 수신측 스트림 상태 테이블을 정의합니다.
|
||||
- 각 `streamReceiver` 는 자신만의 입력 채널(예: `inCh chan *protocol.Envelope`)을 가져, 중앙 readLoop 로부터 `StreamOpen/StreamData/StreamClose` 를 전달받도록 합니다.
|
||||
- [x] 중앙 readLoop 에서 스트림별 라우팅 규칙 정의
|
||||
- `Envelope.Type` 에 따라:
|
||||
- `StreamOpen` / `StreamData` / `StreamClose`:
|
||||
- `streamID` 를 추출하고, 해당 `streamReceiver` 의 `inCh` 로 전달.
|
||||
- `StreamOpen` 수신 시에는 아직 없는 경우 `streamReceiver` 를 생성 후 등록.
|
||||
- `StreamAck`:
|
||||
- 송신 측 ARQ(`streamSender`) 용 테이블(이미 구현된 구조)을 찾아 재전송 로직으로 전달.
|
||||
- 이 설계를 통해 중앙 readLoop 는 **DTLS 세션 → 스트림 단위 이벤트 분배**만 담당하도록 제한합니다.
|
||||
|
||||
---
|
||||
|
||||
##### 3.3B.2 streamReceiver 타입 설계 및 HTTP 매핑 리팩터링
|
||||
##### 3.3B.2 Design streamReceiver type and refactor HTTP mapping
|
||||
|
||||
- [x] `streamReceiver` 타입 정의
|
||||
- 필드 예시:
|
||||
- `id protocol.StreamID`
|
||||
- 수신 ARQ 상태: `expectedSeq`, `received map[uint64][]byte`, `lost map[uint64]struct{}`
|
||||
- 입력 채널: `inCh chan *protocol.Envelope`
|
||||
- DTLS 세션/codec/logging 핸들: `sess dtls.Session`, `codec protocol.WireCodec`, `logger logging.Logger`
|
||||
- 로컬 HTTP 호출 관련: `HTTPClient *http.Client`, `LocalTarget string`
|
||||
- 역할:
|
||||
- 서버에서 온 `StreamOpen`/`StreamData`/`StreamClose` 를 순서대로 처리해 로컬 HTTP 요청을 구성하고,
|
||||
- 로컬 HTTP 응답을 다시 `StreamOpen`/`StreamData`/`StreamClose` 로 역방향 전송합니다.
|
||||
- [x] 기존 `ClientProxy.handleStreamRequest` 의 로직을 `streamReceiver` 로 이전
|
||||
- 현재 `handleStreamRequest` 안에서 수행하던 작업을 단계적으로 옮깁니다:
|
||||
- `StreamOpen` 의 pseudo-header 에서 HTTP 메서드/URL/헤더를 복원.
|
||||
- 요청 바디 수신용 수신 측 ARQ(`expectedSeq`, `received`, `lost`) 처리.
|
||||
- 로컬 HTTP 요청 생성/실행 및 에러 처리.
|
||||
- 응답을 4KiB `StreamData` chunk 로 전송 + 송신 측 ARQ(`streamSender.register`) 기록.
|
||||
- 이때 **DTLS reader 를 직접 읽던 부분**은 제거하고, 대신 `inCh` 에서 전달된 `Envelope` 만 사용하도록 리팩터링합니다.
|
||||
- [x] streamReceiver 생명주기 관리
|
||||
- `StreamClose` 수신 시:
|
||||
- 로컬 HTTP 요청 바디 구성 종료.
|
||||
- 로컬 HTTP 요청 실행 및 응답 스트림 전송 완료 후,
|
||||
- `streamReceivers[streamID]` 에서 자신을 제거하고 goroutine 을 종료하는 정책을 명확히 정의합니다.
|
||||
|
||||
---
|
||||
|
||||
##### 3.3B.3 StartLoop 와 streamReceiver 통합
|
||||
##### 3.3B.3 Integrate StartLoop and streamReceiver
|
||||
|
||||
- [x] `ClientProxy.StartLoop` 을 “중앙 readLoop + demux” 로 단순화
|
||||
- `MessageTypeStreamOpen` 수신 시:
|
||||
- `streamID := env.StreamOpen.ID` 를 기준으로 기존 `streamReceiver` 존재 여부를 검사.
|
||||
- 없으면 새 `streamReceiver` 생성 후, goroutine 을 띄우고 `inCh <- env` 로 첫 메시지 전달.
|
||||
- `MessageTypeStreamData` / `MessageTypeStreamClose` 수신 시:
|
||||
- 해당 `streamReceiver` 의 `inCh` 로 그대로 전달.
|
||||
- `MessageTypeStreamAck` 는 기존처럼 송신 측 `streamSender` 로 라우팅.
|
||||
- [x] 에러/종료 처리 전략 정리
|
||||
- 개별 `streamReceiver` 에서 발생하는 에러는:
|
||||
- 로컬 HTTP 에러 → 스트림 응답에 5xx/에러 바디로 반영.
|
||||
- 프로토콜 위반(예: 잘못된 순서의 `StreamClose`) → 해당 스트림만 정리하고 세션은 유지하는지 여부를 정의.
|
||||
- DTLS 세션 레벨 에러(EOF, decode 실패 등)는:
|
||||
- 모든 `streamReceiver` 의 `inCh` 를 닫고,
|
||||
- 이후 클라이언트 전체 루프를 종료하는 방향으로 합의합니다.
|
||||
|
||||
---
|
||||
|
||||
##### 3.3B.4 세션 단위 직렬화 락 제거 및 멀티플렉싱 검증
|
||||
##### 3.3B.4 Remove session-level serialization lock and validate multiplexing
|
||||
|
||||
- [x] 서버 측 세션 직렬화 락 제거 계획 수립
|
||||
- 현재 서버는 [`dtlsSessionWrapper`](cmd/server/main.go:111)에 `requestMu` 를 두어,
|
||||
- 동일 DTLS 세션에서 동시에 하나의 `ForwardHTTP` 만 수행하도록 직렬화하고 있습니다.
|
||||
- 클라이언트 측 멀티플렉싱이 안정화되면, `requestMu` 를 제거하고
|
||||
- 하나의 세션 안에서 여러 HTTP 요청이 각기 다른 `StreamID` 로 병렬 진행되도록 허용합니다.
|
||||
- [ ] E2E 멀티플렉싱 테스트 시나리오 정의
|
||||
- 하나의 DTLS 세션 위에서:
|
||||
- 동시에 여러 정적 리소스(`/css`, `/js`, `/img`) 요청.
|
||||
- 큰 응답(수 MB 파일)과 작은 응답(API JSON)이 섞여 있는 시나리오.
|
||||
- [ ] gRPC 터널 기반 E2E 플로우 정의/테스트 계획
|
||||
- 하나의 gRPC 스트림 위에서:
|
||||
- 동시에 여러 정적 리소스(`/css`, `/js`, `/img`) 요청,
|
||||
- 큰 응답(수 MB 파일)과 작은 응답(API JSON)이 섞여 있는 시나리오,
|
||||
- 클라이언트 재시작/네트워크 단절 후 재연결 시나리오
|
||||
를 포함하는 테스트 플랜을 작성합니다.
|
||||
- 기대 동작:
|
||||
- 어떤 요청이 느리더라도, 다른 요청이 세션 내부 큐잉 때문에 과도하게 지연되지 않고 병렬로 완료되는지 확인.
|
||||
- 클라이언트/서버 로그에 프로토콜 위반(`unexpected envelope type ...`) 이 더 이상 발생하지 않는지 확인.
|
||||
- [ ] 관측성/메트릭에 멀티플렉싱 관련 라벨/필드 추가(선택)
|
||||
- 필요 시:
|
||||
- 세션당 동시 활성 스트림 수,
|
||||
- 스트림 수명(요청-응답 왕복 시간),
|
||||
- 세션 내 스트림 에러 수
|
||||
를 관찰할 수 있는 메트릭/로그 필드를 설계합니다.
|
||||
- 느린 요청이 있더라도 다른 요청이 **같은 TCP 연결/스트림 집합 내에서** 과도하게 지연되지 않을 것.
|
||||
- 서버/클라이언트 로그에 프로토콜 위반 경고(`unexpected frame ...`)가 발생하지 않을 것.
|
||||
|
||||
> Note: 기존 DTLS 기반 스트림/ARQ/멀티플렉싱(3.3A/3.3B)의 작업 내역은
|
||||
> 구현 경험/아이디어 참고용으로만 유지하며, 신규 기능/운영 계획은 gRPC 터널을 기준으로 진행합니다.
|
||||
|
||||
---
|
||||
|
||||
@@ -539,7 +299,7 @@ This section introduces a **client-side stream demultiplexer + per-stream gorout
|
||||
|
||||
### 3.6 Hardening / 안정성 & 구성
|
||||
|
||||
- [ ] 설정 유효성 검사 추가
|
||||
- [x] 설정 유효성 검사 추가
|
||||
- 필수 env 누락/오류에 대한 명확한 에러 메시지.
|
||||
|
||||
- [ ] 에러 처리/재시도 정책
|
||||
|
||||
Reference in New Issue
Block a user