- Changed `go_package` option in `hopgate_stream.proto` to `internal/protocol/pb;pb`. - Regenerated `hopgate_stream.pb.go` with updated package path to align with new structure. - Added `protocol.md` documenting the gRPC-based HTTP tunneling protocol.
9.1 KiB
HopGate gRPC Tunnel Protocol
이 문서는 HopGate 서버–클라이언트 사이의 gRPC 기반 HTTP 터널링 규약을 정리합니다. (ko) This document describes the gRPC-based HTTP tunneling protocol between HopGate server and clients. (en)
1. Transport Overview / 전송 개요
- Transport: TCP + TLS(HTTPS) + HTTP/2 + gRPC
- Single long-lived bi-directional gRPC stream per client:
OpenTunnel - Application payload type:
Envelope(frominternal/protocol/hopgate_stream.proto) - HTTP requests/responses are multiplexed as logical streams identified by
StreamID.
gRPC service (conceptual):
service HopGateTunnel {
rpc OpenTunnel (stream Envelope) returns (stream Envelope);
}
2. Message Types / 메시지 타입
Defined in internal/protocol/hopgate_stream.proto:
HeaderValues- Wraps repeated header values:
map<string, HeaderValues>
- Wraps repeated header values:
Request/Response- Simple single-message HTTP representation (not used in the streaming tunnel path initially).
StreamOpen- Opens a new logical stream for HTTP request/response (or other protocols in the future).
StreamData- Carries body chunks for a stream (
id,seq,data).
- Carries body chunks for a stream (
StreamClose- Marks the end of a stream (
id,error).
- Marks the end of a stream (
StreamAck- Legacy ARQ/flow-control hint for UDP/DTLS; in gRPC tunnel it is reserved/optional.
Envelope- Top-level container with
oneof payloadof the above types.
- Top-level container with
In the gRPC tunnel, Envelope is the only gRPC message type used on the OpenTunnel stream.
3. Logical Streams and StreamID / 논리 스트림과 StreamID
- A single
OpenTunnelgRPC stream multiplexes many logical streams. - Each logical stream corresponds to one HTTP request/response pair.
- Logical streams are identified by
StreamOpen.id(text StreamID). - The server generates unique IDs per gRPC connection:
- HTTP streams:
"http-{n}"wherenis a monotonically increasing counter. - Control stream:
"control-0"(special handshake/metadata stream).
- HTTP streams:
Within a gRPC connection:
- Multiple
StreamIDs may be active concurrently. - Frames with different StreamIDs may be arbitrarily interleaved.
- Order within a stream is tracked by
StreamData.seq(starting at 0).
4. HTTP Request Mapping (Server → Client) / HTTP 요청 매핑
When the public HTTPS reverse-proxy (cmd/server/main.go) receives an HTTP request for a domain that is bound
to a client tunnel, it serializes the request into a logical stream as follows.
4.1 StreamOpen (request metadata and headers)
StreamOpen.id- New unique StreamID:
"http-{n}".
- New unique StreamID:
StreamOpen.service_name- Logical service selection on the client (e.g.,
"web").
- Logical service selection on the client (e.g.,
StreamOpen.target_addr- Optional explicit local target address on the client (e.g.,
"127.0.0.1:8080").
- Optional explicit local target address on the client (e.g.,
StreamOpen.header- Contains HTTP request headers and pseudo-headers:
- Pseudo-headers:
X-HopGate-Method: HTTP method (e.g.,"GET","POST").X-HopGate-URL: original URL path + query (e.g.,"/api/v1/foo?bar=1").X-HopGate-Host: Host header value.
- Other keys:
- All remaining HTTP headers from the incoming request, copied as-is into the map.
4.2 StreamData* (request body chunks)
- If the request has a body, the server chunks it into fixed-size pieces.
- Chunk size:
protocol.StreamChunkSize(currently 4 KiB). - For each chunk:
StreamData.id = StreamOpen.idStreamData.seqincrements from 0, 1, 2, …StreamData.datacontains the raw bytes.
4.3 StreamClose (end of request body)
- After sending all body chunks, the server sends a
StreamClose:StreamClose.id = StreamOpen.idStreamClose.erroris empty on success.- If there was an application-level error while reading the body,
errorcontains a short description.
The client reconstructs the HTTP request by:
- Reassembling the URL and headers from the
StreamOpenpseudo-headers and header map. - Concatenating
StreamData.datainseqorder into the request body. - Treating
StreamCloseas the end-of-stream marker.
5. HTTP Response Mapping (Client → Server) / HTTP 응답 매핑
The client receives StreamOpen + StreamData* + StreamClose, performs a local HTTP request to its
configured target (e.g., http://127.0.0.1:8080), then returns an HTTP response using the same StreamID.
5.1 StreamOpen (response headers and status)
StreamOpen.id- Same as the request StreamID.
StreamOpen.header- Contains response headers and a pseudo-header for status:
- Pseudo-header:
X-HopGate-Status: HTTP status code as a string (e.g.,"200","502").
- Other keys:
- All HTTP response headers from the local backend, copied as-is.
5.2 StreamData* (response body chunks)
- The client reads the local HTTP response body and chunks it into 4 KiB pieces (same
StreamChunkSize). - For each chunk:
StreamData.id = StreamOpen.idStreamData.seqincrements from 0.StreamData.datacontains the raw bytes.
5.3 StreamClose (end of response body)
- When the local backend response is fully read, the client sends a
StreamClose:StreamClose.idis the same StreamID.StreamClose.error:- Empty string on success.
- Short error description if the local HTTP request/response failed (e.g., connect timeout).
The server reconstructs the HTTP response by:
- Parsing
X-HopGate-Statusinto an integer HTTP status code. - Copying other headers into the outgoing response writer (with some security headers overridden by the server).
- Concatenating
StreamData.datainseqorder into the HTTP response body. - Considering
StreamClose.errorfor logging/metrics and possibly mapping to error pages if needed.
6. Control / Handshake Stream / 컨트롤 스트림
Before any HTTP request streams are opened, the client sends a single control stream to authenticate and describe itself.
StreamOpen(control):id = "control-0"service_name = "control"headercontains:X-HopGate-Domain: domain this client is responsible for.X-HopGate-API-Key: client API key for the domain.X-HopGate-Local-Target: default local target such as127.0.0.1:8080.
- No
StreamDatais required for the control stream in the initial design. - The server can optionally reply with its own control
StreamOpen/Closeto signal acceptance/rejection.
On the server side:
grpcTunnelServer.OpenTunnelshould:- Wait for the first
EnvelopewithStreamOpen.id == "control-0". - Extract domain, api key, and local target from the headers.
- Call the ent-based
DomainValidatorto validate(domain, api_key). - If validation succeeds, register this gRPC stream as the active tunnel for that domain.
- If validation fails, log and close the gRPC stream.
- Wait for the first
Once the control stream handshake completes successfully, the server may start multiplexing multiple
HTTP request streams (http-0, http-1, …) over the same OpenTunnel connection.
7. Multiplexing Semantics / 멀티플렉싱 의미
- A single TCP + TLS + HTTP/2 + gRPC connection carries:
- One long-lived
OpenTunnelgRPC bi-di stream. - Within it, many logical streams identified by
StreamID.
- One long-lived
- The server can open multiple HTTP streams concurrently for a given client:
- Example:
http-0for/css/app.css,http-1for/api/users,http-2for/img/logo.png. - Frames for these IDs can interleave arbitrarily on the wire.
- Example:
- Per-stream ordering is preserved by combining
seqordering and the reliability of TCP/gRPC. - Slow or large responses on one stream should not prevent other streams from making progress, because gRPC/HTTP2 handles stream-level flow control and scheduling.
8. Flow Control and StreamAck / 플로우 컨트롤 및 StreamAck
- The gRPC tunnel runs over TCP/HTTP2, which already provides:
- Reliable, in-order delivery.
- Connection-level and stream-level flow control.
- Therefore, application-level selective retransmission is not required for the gRPC tunnel.
StreamAckremains defined in the proto for backward compatibility with the DTLS design and as a potential future hint channel (e.g., window size hints), but is not used in the initial gRPC tunnel.
9. Security Considerations / 보안 고려사항
- TLS:
- In production, the server uses ACME-issued certificates, and clients validate the server certificate
using system Root CAs and SNI (
ServerName). - In debug mode, clients may use
InsecureSkipVerify: trueto allow local/self-signed certs.
- In production, the server uses ACME-issued certificates, and clients validate the server certificate
using system Root CAs and SNI (
- Authentication:
- Application-level authentication relies on
(domain, api_key)pairs sent via the control stream headers. - The server must validate these pairs against the
Domaintable usingDomainValidator.
- Application-level authentication relies on
- Authorization and isolation:
- Each gRPC tunnel is bound to a single domain (or a defined set of domains) after successful control handshake.
- HTTP requests for other domains must not be forwarded over this tunnel.
이 규약을 기준으로 서버/클라이언트 구현을 정렬하면, 하나의 gRPC OpenTunnel 스트림 위에서
여러 HTTP 요청을 안정적으로 멀티플렉싱하면서도, 도메인/API 키 기반 인증과 TLS 보안을 함께 유지할 수 있습니다.