Compare commits

...

3 Commits

Author SHA1 Message Date
dalbodeule
99be2d2e31 [feat](protocol): implement Protobuf codec and integrate into default WireCodec
- Introduced `protobufCodec` supporting length-prefixed Protobuf serialization/deserialization.
- Replaced JSON-based `DefaultCodec` with Protobuf-based implementation.
- Updated generated Protobuf Go types, aligning with `go_package` updates in `hopgate_stream.proto`.
- Added constants and safeguards for Protobuf envelope size limits.
- Modified `Makefile` to accommodate updated Protobuf generation logic.
2025-12-08 20:47:12 +09:00
dalbodeule
1fa5e900f8 [feat](protocol): add Protobuf schemas and code generation for hopgate streams
- Defined `hopgate_stream.proto` with message definitions for stream-based DTLS tunneling, including `Request`, `Response`, `StreamOpen`, `StreamData`, `StreamAck`, and `StreamClose`.
- Added `Envelope` container for top-level message encapsulation.
- Integrated Protobuf code generation into the `Makefile` using `protoc` with `protoc-gen-go`.
- Generated Go types under `internal/protocol/pb`.
2025-12-08 20:30:53 +09:00
dalbodeule
bf5c3c8f59 [feat](protocol): replace JSON handlers with codec abstraction
- Introduced `WireCodec` interface in `internal/protocol/codec.go` to abstract serialization/deserialization logic.
- Updated server and client to use `DefaultCodec`, replacing direct JSON encoding/decoding.
- Eliminated `bufio.Reader` from session handling, as `DefaultCodec` manages buffering for DTLS sessions.
- Marked related protocol tasks in `progress.md` as complete.
2025-12-08 20:14:36 +09:00
8 changed files with 1170 additions and 32 deletions

View File

@@ -66,3 +66,21 @@ docker-server:
@echo "Building server Docker image..." @echo "Building server Docker image..."
docker build -f Dockerfile.server -t hop-gate-server:$(VERSION) . docker build -f Dockerfile.server -t hop-gate-server:$(VERSION) .
# --- Protobuf code generation -------------------------------------------------
# Requires:
# - protoc (https://grpc.io/docs/protoc-installation/)
# - protoc-gen-go (go install google.golang.org/protobuf/cmd/protoc-gen-go@latest)
#
# Generates Go types under internal/protocol/pb from internal/protocol/hopgate_stream.proto.
# NOTE:
# - go_package in hopgate_stream.proto is set to:
# github.com/dalbodeule/hop-gate/internal/protocol/pb;protocolpb
# - With --go_out=. (without paths=source_relative), protoc will place the
# generated file under internal/protocol/pb according to go_package.
proto:
@echo "Generating Go code from Protobuf schemas..."
protoc \
--go_out=. \
internal/protocol/hopgate_stream.proto
@echo "Protobuf generation completed."

View File

@@ -1,10 +1,8 @@
package main package main
import ( import (
"bufio"
"context" "context"
"crypto/tls" "crypto/tls"
"encoding/json"
"fmt" "fmt"
"io" "io"
stdfs "io/fs" stdfs "io/fs"
@@ -208,8 +206,7 @@ func (w *dtlsSessionWrapper) ForwardHTTP(ctx context.Context, logger logging.Log
HTTPRequest: httpReq, HTTPRequest: httpReq,
} }
enc := json.NewEncoder(w.sess) if err := protocol.DefaultCodec.Encode(w.sess, env); err != nil {
if err := enc.Encode(env); err != nil {
log.Error("failed to encode http envelope", logging.Fields{ log.Error("failed to encode http envelope", logging.Fields{
"error": err.Error(), "error": err.Error(),
}) })
@@ -218,16 +215,7 @@ func (w *dtlsSessionWrapper) ForwardHTTP(ctx context.Context, logger logging.Log
// 클라이언트로부터 HTTP 응답 Envelope 를 수신합니다. // 클라이언트로부터 HTTP 응답 Envelope 를 수신합니다.
var respEnv protocol.Envelope var respEnv protocol.Envelope
if err := protocol.DefaultCodec.Decode(w.sess, &respEnv); err != nil {
// NOTE: pion/dtls 는 복호화된 애플리케이션 데이터를 호출자가 제공한 버퍼에 채웁니다.
// 기본 JSON 디코더 버퍼만 사용하면 큰 HTTP 응답/Envelope 에서 "dtls: buffer too small"
// 오류가 발생할 수 있으므로, 충분히 큰 bufio.Reader(64KiB)를 사용합니다. (ko)
// NOTE: pion/dtls decrypts application data into the buffer provided by the caller.
// Using only the default JSON decoder buffer can cause "dtls: buffer too small"
// errors for large HTTP responses/envelopes, so we wrap the session with a
// reasonably large bufio.Reader (64KiB). (en)
dec := json.NewDecoder(bufio.NewReaderSize(w.sess, 64*1024))
if err := dec.Decode(&respEnv); err != nil {
log.Error("failed to decode http envelope", logging.Fields{ log.Error("failed to decode http envelope", logging.Fields{
"error": err.Error(), "error": err.Error(),
}) })

View File

@@ -1,2 +1,2 @@
/*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */ /*! tailwindcss v4.1.17 | MIT License | https://tailwindcss.com */
@layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-tracking:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.static{position:static}.contents{display:contents}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.min-h-screen{min-height:100vh}.w-\[240px\]{width:240px}.w-full{width:100%}.flex-col{flex-direction:column}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-center{justify-content:center}.text-center{text-align:center}.tracking-\[0\.25em\]{--tw-tracking:.25em;letter-spacing:.25em}.uppercase{text-transform:uppercase}.opacity-90{opacity:.9}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false} @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-tracking:initial;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial}}}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.static{position:static}.container{width:100%}.contents{display:contents}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.min-h-screen{min-height:100vh}.w-\[240px\]{width:240px}.w-full{width:100%}.flex-col{flex-direction:column}.items-baseline{align-items:baseline}.items-center{align-items:center}.justify-center{justify-content:center}.text-center{text-align:center}.tracking-\[0\.25em\]{--tw-tracking:.25em;letter-spacing:.25em}.uppercase{text-transform:uppercase}.opacity-90{opacity:.9}.filter{filter:var(--tw-blur,)var(--tw-brightness,)var(--tw-contrast,)var(--tw-grayscale,)var(--tw-hue-rotate,)var(--tw-invert,)var(--tw-saturate,)var(--tw-sepia,)var(--tw-drop-shadow,)}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}

229
internal/protocol/codec.go Normal file
View File

@@ -0,0 +1,229 @@
package protocol
import (
"bufio"
"encoding/binary"
"encoding/json"
"fmt"
"io"
protocolpb "github.com/dalbodeule/hop-gate/internal/protocol/pb"
"google.golang.org/protobuf/proto"
)
// defaultDecoderBufferSize 는 pion/dtls 가 복호화한 애플리케이션 데이터를
// JSON 디코더가 안전하게 처리할 수 있도록 사용하는 버퍼 크기입니다.
// This matches existing 64KiB readers used around DTLS sessions (used by the JSON codec).
const defaultDecoderBufferSize = 64 * 1024
// maxProtoEnvelopeBytes 는 단일 Protobuf Envelope 의 최대 크기에 대한 보수적 상한입니다.
// 아직 하드 리미트로 사용하지는 않지만, 향후 방어적 체크에 사용할 수 있습니다.
const maxProtoEnvelopeBytes = 512 * 1024 // 512KiB, 충분히 여유 있는 값
// WireCodec 는 protocol.Envelope 의 직렬화/역직렬화를 추상화합니다.
// JSON, Protobuf, length-prefixed binary 등으로 교체할 때 이 인터페이스만 유지하면 됩니다.
type WireCodec interface {
Encode(w io.Writer, env *Envelope) error
Decode(r io.Reader, env *Envelope) error
}
// jsonCodec 은 JSON 기반 WireCodec 구현입니다.
// JSON 직렬화를 계속 사용하고 싶을 때를 위해 남겨둡니다.
type jsonCodec struct{}
// Encode 는 Envelope 를 JSON 으로 인코딩해 작성합니다.
// Encode encodes an Envelope as JSON to the given writer.
func (jsonCodec) Encode(w io.Writer, env *Envelope) error {
enc := json.NewEncoder(w)
return enc.Encode(env)
}
// Decode 는 DTLS 세션에서 읽은 데이터를 JSON Envelope 로 디코딩합니다.
// pion/dtls 의 버퍼 특성 때문에, 충분히 큰 bufio.Reader 로 감싸서 사용합니다.
// Decode decodes an Envelope from JSON using a buffered reader on top of the DTLS session.
func (jsonCodec) Decode(r io.Reader, env *Envelope) error {
dec := json.NewDecoder(bufio.NewReaderSize(r, defaultDecoderBufferSize))
return dec.Decode(env)
}
// protobufCodec 은 Protobuf + length-prefix framing 기반 WireCodec 구현입니다.
// 한 Envelope 당 [4바이트 big-endian 길이] + [protobuf bytes] 형태로 인코딩합니다.
type protobufCodec struct{}
// Encode 는 Envelope 를 Protobuf Envelope 로 변환한 뒤, length-prefix 프레이밍으로 기록합니다.
// Encode encodes an Envelope as a length-prefixed protobuf message.
func (protobufCodec) Encode(w io.Writer, env *Envelope) error {
pbEnv, err := toProtoEnvelope(env)
if err != nil {
return err
}
data, err := proto.Marshal(pbEnv)
if err != nil {
return fmt.Errorf("protobuf marshal envelope: %w", err)
}
if len(data) == 0 {
return fmt.Errorf("protobuf codec: empty marshaled envelope")
}
var lenBuf [4]byte
if len(data) > int(^uint32(0)) {
return fmt.Errorf("protobuf codec: envelope too large: %d bytes", len(data))
}
binary.BigEndian.PutUint32(lenBuf[:], uint32(len(data)))
if _, err := w.Write(lenBuf[:]); err != nil {
return fmt.Errorf("protobuf codec: write length prefix: %w", err)
}
if _, err := w.Write(data); err != nil {
return fmt.Errorf("protobuf codec: write payload: %w", err)
}
return nil
}
// Decode 는 length-prefix 프레임에서 Protobuf Envelope 를 읽어들여
// 내부 Envelope 구조체로 변환합니다.
// Decode reads a length-prefixed protobuf Envelope and converts it into the internal Envelope.
func (protobufCodec) Decode(r io.Reader, env *Envelope) error {
var lenBuf [4]byte
if _, err := io.ReadFull(r, lenBuf[:]); err != nil {
return fmt.Errorf("protobuf codec: read length prefix: %w", err)
}
n := binary.BigEndian.Uint32(lenBuf[:])
if n == 0 {
return fmt.Errorf("protobuf codec: zero-length envelope")
}
if n > maxProtoEnvelopeBytes {
return fmt.Errorf("protobuf codec: envelope too large: %d bytes (max %d)", n, maxProtoEnvelopeBytes)
}
buf := make([]byte, int(n))
if _, err := io.ReadFull(r, buf); err != nil {
return fmt.Errorf("protobuf codec: read payload: %w", err)
}
var pbEnv protocolpb.Envelope
if err := proto.Unmarshal(buf, &pbEnv); err != nil {
return fmt.Errorf("protobuf codec: unmarshal envelope: %w", err)
}
return fromProtoEnvelope(&pbEnv, env)
}
// DefaultCodec 은 현재 런타임에서 사용하는 기본 WireCodec 입니다.
// 이제 Protobuf 기반 codec 을 기본으로 사용합니다.
var DefaultCodec WireCodec = protobufCodec{}
// toProtoEnvelope 는 내부 Envelope 구조체를 Protobuf Envelope 로 변환합니다.
// 현재 구현은 MessageTypeHTTP (HTTPRequest/HTTPResponse) 만 지원하며,
// 스트림 관련 타입은 이후 스트림 터널링 구현 단계에서 확장합니다.
func toProtoEnvelope(env *Envelope) (*protocolpb.Envelope, error) {
switch env.Type {
case MessageTypeHTTP:
if env.HTTPRequest != nil {
req := env.HTTPRequest
pbReq := &protocolpb.Request{
RequestId: req.RequestID,
ClientId: req.ClientID,
ServiceName: req.ServiceName,
Method: req.Method,
Url: req.URL,
Header: make(map[string]*protocolpb.HeaderValues, len(req.Header)),
Body: req.Body,
}
for k, vs := range req.Header {
hv := &protocolpb.HeaderValues{
Values: append([]string(nil), vs...),
}
pbReq.Header[k] = hv
}
return &protocolpb.Envelope{
Payload: &protocolpb.Envelope_HttpRequest{
HttpRequest: pbReq,
},
}, nil
}
if env.HTTPResponse != nil {
resp := env.HTTPResponse
pbResp := &protocolpb.Response{
RequestId: resp.RequestID,
Status: int32(resp.Status),
Header: make(map[string]*protocolpb.HeaderValues, len(resp.Header)),
Body: resp.Body,
Error: resp.Error,
}
for k, vs := range resp.Header {
hv := &protocolpb.HeaderValues{
Values: append([]string(nil), vs...),
}
pbResp.Header[k] = hv
}
return &protocolpb.Envelope{
Payload: &protocolpb.Envelope_HttpResponse{
HttpResponse: pbResp,
},
}, nil
}
return nil, fmt.Errorf("protobuf codec: http envelope has neither request nor response")
default:
// 스트림 관련 타입은 아직 DTLS 스트림 터널링 구현 이전 단계이므로 지원하지 않습니다.
// Stream-based message types are not yet supported by the protobuf codec.
return nil, fmt.Errorf("protobuf codec: unsupported envelope type %q", env.Type)
}
}
// fromProtoEnvelope 는 Protobuf Envelope 를 내부 Envelope 구조체로 변환합니다.
// 현재 구현은 HTTP 요청/응답만 지원합니다.
func fromProtoEnvelope(pbEnv *protocolpb.Envelope, env *Envelope) error {
switch payload := pbEnv.Payload.(type) {
case *protocolpb.Envelope_HttpRequest:
req := payload.HttpRequest
if req == nil {
return fmt.Errorf("protobuf codec: http_request payload is nil")
}
hdr := make(map[string][]string, len(req.Header))
for k, hv := range req.Header {
if hv == nil {
continue
}
hdr[k] = append([]string(nil), hv.Values...)
}
env.Type = MessageTypeHTTP
env.HTTPRequest = &Request{
RequestID: req.RequestId,
ClientID: req.ClientId,
ServiceName: req.ServiceName,
Method: req.Method,
URL: req.Url,
Header: hdr,
Body: append([]byte(nil), req.Body...),
}
env.HTTPResponse = nil
return nil
case *protocolpb.Envelope_HttpResponse:
resp := payload.HttpResponse
if resp == nil {
return fmt.Errorf("protobuf codec: http_response payload is nil")
}
hdr := make(map[string][]string, len(resp.Header))
for k, hv := range resp.Header {
if hv == nil {
continue
}
hdr[k] = append([]string(nil), hv.Values...)
}
env.Type = MessageTypeHTTP
env.HTTPResponse = &Response{
RequestID: resp.RequestId,
Status: int(resp.Status),
Header: hdr,
Body: append([]byte(nil), resp.Body...),
Error: resp.Error,
}
env.HTTPRequest = nil
return nil
default:
return fmt.Errorf("protobuf codec: unsupported payload type %T", payload)
}
}

View File

@@ -0,0 +1,103 @@
syntax = "proto3";
package hopgate.protocol.v1;
option go_package = "github.com/dalbodeule/hop-gate/internal/protocol/pb;protocolpb";
// HeaderValues 는 HTTP 헤더의 다중 값 표현을 위한 래퍼입니다.
// HeaderValues wraps multiple header values for a single HTTP header key.
message HeaderValues {
repeated string values = 1;
}
// Request 는 DTLS 터널 위에서 교환되는 HTTP 요청을 표현합니다.
// This mirrors internal/protocol.Request.
message Request {
string request_id = 1;
string client_id = 2; // optional client identifier
string service_name = 3; // logical service name on the client side
string method = 4;
string url = 5;
// HTTP header: map of key -> multiple values.
map<string, HeaderValues> header = 6;
// Raw HTTP body bytes.
bytes body = 7;
}
// Response 는 DTLS 터널 위에서 교환되는 HTTP 응답을 표현합니다.
// This mirrors internal/protocol.Response.
message Response {
string request_id = 1;
int32 status = 2;
// HTTP header.
map<string, HeaderValues> header = 3;
// Raw HTTP body bytes.
bytes body = 4;
// Optional error description when tunneling fails.
string error = 5;
}
// StreamOpen 은 새로운 스트림(HTTP 요청/응답, WebSocket 등)을 여는 메시지입니다.
// This represents opening a new stream (HTTP request/response, WebSocket, etc.).
message StreamOpen {
string id = 1; // StreamID (text form)
// Which logical service / local target to use on the client side.
string service_name = 2;
string target_addr = 3; // e.g. "127.0.0.1:8080"
// Initial HTTP-like headers (including Upgrade, etc.).
map<string, HeaderValues> header = 4;
}
// StreamData 는 이미 열린 스트림에 대한 단방향 데이터 프레임입니다.
// This is a unidirectional data frame on an already-open stream.
message StreamData {
string id = 1; // StreamID
uint64 seq = 2; // per-stream sequence number starting from 0
bytes data = 3;
}
// StreamAck 는 StreamData 에 대한 ACK/NACK 및 선택적 재전송 힌트를 전달합니다.
// This conveys ACK/NACK and optional retransmission hints for StreamData.
message StreamAck {
string id = 1;
// Last contiguously received sequence number (starting from 0).
uint64 ack_seq = 2;
// Additional missing sequence numbers beyond ack_seq (optional).
repeated uint64 lost_seqs = 3;
// Optional receive window size hint.
uint32 window_size = 4;
}
// StreamClose 는 스트림 종료(정상/에러)를 알립니다.
// This indicates normal or error termination of a stream.
message StreamClose {
string id = 1;
string error = 2; // empty means normal close
}
// Envelope 는 DTLS 세션 위에서 교환되는 상위 레벨 메시지 컨테이너입니다.
// 하나의 Envelope 에는 HTTP 요청/응답 또는 스트림 관련 메시지 중 하나만 포함됩니다.
// Envelope is the top-level container exchanged over the DTLS session.
// Exactly one payload (http_request/http_response/stream_*) is set per message.
message Envelope {
oneof payload {
Request http_request = 1;
Response http_response = 2;
StreamOpen stream_open = 3;
StreamData stream_data = 4;
StreamClose stream_close = 5;
StreamAck stream_ack = 6;
}
}

View File

@@ -0,0 +1,799 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.36.10
// protoc v6.33.1
// source: internal/protocol/hopgate_stream.proto
package pb
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
unsafe "unsafe"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
// HeaderValues 는 HTTP 헤더의 다중 값 표현을 위한 래퍼입니다.
// HeaderValues wraps multiple header values for a single HTTP header key.
type HeaderValues struct {
state protoimpl.MessageState `protogen:"open.v1"`
Values []string `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *HeaderValues) Reset() {
*x = HeaderValues{}
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *HeaderValues) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*HeaderValues) ProtoMessage() {}
func (x *HeaderValues) ProtoReflect() protoreflect.Message {
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use HeaderValues.ProtoReflect.Descriptor instead.
func (*HeaderValues) Descriptor() ([]byte, []int) {
return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{0}
}
func (x *HeaderValues) GetValues() []string {
if x != nil {
return x.Values
}
return nil
}
// Request 는 DTLS 터널 위에서 교환되는 HTTP 요청을 표현합니다.
// This mirrors internal/protocol.Request.
type Request struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
ClientId string `protobuf:"bytes,2,opt,name=client_id,json=clientId,proto3" json:"client_id,omitempty"` // optional client identifier
ServiceName string `protobuf:"bytes,3,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"` // logical service name on the client side
Method string `protobuf:"bytes,4,opt,name=method,proto3" json:"method,omitempty"`
Url string `protobuf:"bytes,5,opt,name=url,proto3" json:"url,omitempty"`
// HTTP header: map of key -> multiple values.
Header map[string]*HeaderValues `protobuf:"bytes,6,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// Raw HTTP body bytes.
Body []byte `protobuf:"bytes,7,opt,name=body,proto3" json:"body,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Request) Reset() {
*x = Request{}
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Request) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Request) ProtoMessage() {}
func (x *Request) ProtoReflect() protoreflect.Message {
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Request.ProtoReflect.Descriptor instead.
func (*Request) Descriptor() ([]byte, []int) {
return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{1}
}
func (x *Request) GetRequestId() string {
if x != nil {
return x.RequestId
}
return ""
}
func (x *Request) GetClientId() string {
if x != nil {
return x.ClientId
}
return ""
}
func (x *Request) GetServiceName() string {
if x != nil {
return x.ServiceName
}
return ""
}
func (x *Request) GetMethod() string {
if x != nil {
return x.Method
}
return ""
}
func (x *Request) GetUrl() string {
if x != nil {
return x.Url
}
return ""
}
func (x *Request) GetHeader() map[string]*HeaderValues {
if x != nil {
return x.Header
}
return nil
}
func (x *Request) GetBody() []byte {
if x != nil {
return x.Body
}
return nil
}
// Response 는 DTLS 터널 위에서 교환되는 HTTP 응답을 표현합니다.
// This mirrors internal/protocol.Response.
type Response struct {
state protoimpl.MessageState `protogen:"open.v1"`
RequestId string `protobuf:"bytes,1,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"`
Status int32 `protobuf:"varint,2,opt,name=status,proto3" json:"status,omitempty"`
// HTTP header.
Header map[string]*HeaderValues `protobuf:"bytes,3,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
// Raw HTTP body bytes.
Body []byte `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"`
// Optional error description when tunneling fails.
Error string `protobuf:"bytes,5,opt,name=error,proto3" json:"error,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Response) Reset() {
*x = Response{}
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[2]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Response) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Response) ProtoMessage() {}
func (x *Response) ProtoReflect() protoreflect.Message {
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[2]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Response.ProtoReflect.Descriptor instead.
func (*Response) Descriptor() ([]byte, []int) {
return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{2}
}
func (x *Response) GetRequestId() string {
if x != nil {
return x.RequestId
}
return ""
}
func (x *Response) GetStatus() int32 {
if x != nil {
return x.Status
}
return 0
}
func (x *Response) GetHeader() map[string]*HeaderValues {
if x != nil {
return x.Header
}
return nil
}
func (x *Response) GetBody() []byte {
if x != nil {
return x.Body
}
return nil
}
func (x *Response) GetError() string {
if x != nil {
return x.Error
}
return ""
}
// StreamOpen 은 새로운 스트림(HTTP 요청/응답, WebSocket 등)을 여는 메시지입니다.
// This represents opening a new stream (HTTP request/response, WebSocket, etc.).
type StreamOpen struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // StreamID (text form)
// Which logical service / local target to use on the client side.
ServiceName string `protobuf:"bytes,2,opt,name=service_name,json=serviceName,proto3" json:"service_name,omitempty"`
TargetAddr string `protobuf:"bytes,3,opt,name=target_addr,json=targetAddr,proto3" json:"target_addr,omitempty"` // e.g. "127.0.0.1:8080"
// Initial HTTP-like headers (including Upgrade, etc.).
Header map[string]*HeaderValues `protobuf:"bytes,4,rep,name=header,proto3" json:"header,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StreamOpen) Reset() {
*x = StreamOpen{}
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[3]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StreamOpen) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StreamOpen) ProtoMessage() {}
func (x *StreamOpen) ProtoReflect() protoreflect.Message {
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[3]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StreamOpen.ProtoReflect.Descriptor instead.
func (*StreamOpen) Descriptor() ([]byte, []int) {
return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{3}
}
func (x *StreamOpen) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *StreamOpen) GetServiceName() string {
if x != nil {
return x.ServiceName
}
return ""
}
func (x *StreamOpen) GetTargetAddr() string {
if x != nil {
return x.TargetAddr
}
return ""
}
func (x *StreamOpen) GetHeader() map[string]*HeaderValues {
if x != nil {
return x.Header
}
return nil
}
// StreamData 는 이미 열린 스트림에 대한 단방향 데이터 프레임입니다.
// This is a unidirectional data frame on an already-open stream.
type StreamData struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"` // StreamID
Seq uint64 `protobuf:"varint,2,opt,name=seq,proto3" json:"seq,omitempty"` // per-stream sequence number starting from 0
Data []byte `protobuf:"bytes,3,opt,name=data,proto3" json:"data,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StreamData) Reset() {
*x = StreamData{}
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[4]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StreamData) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StreamData) ProtoMessage() {}
func (x *StreamData) ProtoReflect() protoreflect.Message {
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[4]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StreamData.ProtoReflect.Descriptor instead.
func (*StreamData) Descriptor() ([]byte, []int) {
return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{4}
}
func (x *StreamData) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *StreamData) GetSeq() uint64 {
if x != nil {
return x.Seq
}
return 0
}
func (x *StreamData) GetData() []byte {
if x != nil {
return x.Data
}
return nil
}
// StreamAck 는 StreamData 에 대한 ACK/NACK 및 선택적 재전송 힌트를 전달합니다.
// This conveys ACK/NACK and optional retransmission hints for StreamData.
type StreamAck struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
// Last contiguously received sequence number (starting from 0).
AckSeq uint64 `protobuf:"varint,2,opt,name=ack_seq,json=ackSeq,proto3" json:"ack_seq,omitempty"`
// Additional missing sequence numbers beyond ack_seq (optional).
LostSeqs []uint64 `protobuf:"varint,3,rep,packed,name=lost_seqs,json=lostSeqs,proto3" json:"lost_seqs,omitempty"`
// Optional receive window size hint.
WindowSize uint32 `protobuf:"varint,4,opt,name=window_size,json=windowSize,proto3" json:"window_size,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StreamAck) Reset() {
*x = StreamAck{}
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[5]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StreamAck) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StreamAck) ProtoMessage() {}
func (x *StreamAck) ProtoReflect() protoreflect.Message {
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[5]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StreamAck.ProtoReflect.Descriptor instead.
func (*StreamAck) Descriptor() ([]byte, []int) {
return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{5}
}
func (x *StreamAck) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *StreamAck) GetAckSeq() uint64 {
if x != nil {
return x.AckSeq
}
return 0
}
func (x *StreamAck) GetLostSeqs() []uint64 {
if x != nil {
return x.LostSeqs
}
return nil
}
func (x *StreamAck) GetWindowSize() uint32 {
if x != nil {
return x.WindowSize
}
return 0
}
// StreamClose 는 스트림 종료(정상/에러)를 알립니다.
// This indicates normal or error termination of a stream.
type StreamClose struct {
state protoimpl.MessageState `protogen:"open.v1"`
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
Error string `protobuf:"bytes,2,opt,name=error,proto3" json:"error,omitempty"` // empty means normal close
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *StreamClose) Reset() {
*x = StreamClose{}
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[6]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StreamClose) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StreamClose) ProtoMessage() {}
func (x *StreamClose) ProtoReflect() protoreflect.Message {
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[6]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StreamClose.ProtoReflect.Descriptor instead.
func (*StreamClose) Descriptor() ([]byte, []int) {
return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{6}
}
func (x *StreamClose) GetId() string {
if x != nil {
return x.Id
}
return ""
}
func (x *StreamClose) GetError() string {
if x != nil {
return x.Error
}
return ""
}
// Envelope 는 DTLS 세션 위에서 교환되는 상위 레벨 메시지 컨테이너입니다.
// 하나의 Envelope 에는 HTTP 요청/응답 또는 스트림 관련 메시지 중 하나만 포함됩니다.
// Envelope is the top-level container exchanged over the DTLS session.
// Exactly one payload (http_request/http_response/stream_*) is set per message.
type Envelope struct {
state protoimpl.MessageState `protogen:"open.v1"`
// Types that are valid to be assigned to Payload:
//
// *Envelope_HttpRequest
// *Envelope_HttpResponse
// *Envelope_StreamOpen
// *Envelope_StreamData
// *Envelope_StreamClose
// *Envelope_StreamAck
Payload isEnvelope_Payload `protobuf_oneof:"payload"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
func (x *Envelope) Reset() {
*x = Envelope{}
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[7]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *Envelope) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*Envelope) ProtoMessage() {}
func (x *Envelope) ProtoReflect() protoreflect.Message {
mi := &file_internal_protocol_hopgate_stream_proto_msgTypes[7]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use Envelope.ProtoReflect.Descriptor instead.
func (*Envelope) Descriptor() ([]byte, []int) {
return file_internal_protocol_hopgate_stream_proto_rawDescGZIP(), []int{7}
}
func (x *Envelope) GetPayload() isEnvelope_Payload {
if x != nil {
return x.Payload
}
return nil
}
func (x *Envelope) GetHttpRequest() *Request {
if x != nil {
if x, ok := x.Payload.(*Envelope_HttpRequest); ok {
return x.HttpRequest
}
}
return nil
}
func (x *Envelope) GetHttpResponse() *Response {
if x != nil {
if x, ok := x.Payload.(*Envelope_HttpResponse); ok {
return x.HttpResponse
}
}
return nil
}
func (x *Envelope) GetStreamOpen() *StreamOpen {
if x != nil {
if x, ok := x.Payload.(*Envelope_StreamOpen); ok {
return x.StreamOpen
}
}
return nil
}
func (x *Envelope) GetStreamData() *StreamData {
if x != nil {
if x, ok := x.Payload.(*Envelope_StreamData); ok {
return x.StreamData
}
}
return nil
}
func (x *Envelope) GetStreamClose() *StreamClose {
if x != nil {
if x, ok := x.Payload.(*Envelope_StreamClose); ok {
return x.StreamClose
}
}
return nil
}
func (x *Envelope) GetStreamAck() *StreamAck {
if x != nil {
if x, ok := x.Payload.(*Envelope_StreamAck); ok {
return x.StreamAck
}
}
return nil
}
type isEnvelope_Payload interface {
isEnvelope_Payload()
}
type Envelope_HttpRequest struct {
HttpRequest *Request `protobuf:"bytes,1,opt,name=http_request,json=httpRequest,proto3,oneof"`
}
type Envelope_HttpResponse struct {
HttpResponse *Response `protobuf:"bytes,2,opt,name=http_response,json=httpResponse,proto3,oneof"`
}
type Envelope_StreamOpen struct {
StreamOpen *StreamOpen `protobuf:"bytes,3,opt,name=stream_open,json=streamOpen,proto3,oneof"`
}
type Envelope_StreamData struct {
StreamData *StreamData `protobuf:"bytes,4,opt,name=stream_data,json=streamData,proto3,oneof"`
}
type Envelope_StreamClose struct {
StreamClose *StreamClose `protobuf:"bytes,5,opt,name=stream_close,json=streamClose,proto3,oneof"`
}
type Envelope_StreamAck struct {
StreamAck *StreamAck `protobuf:"bytes,6,opt,name=stream_ack,json=streamAck,proto3,oneof"`
}
func (*Envelope_HttpRequest) isEnvelope_Payload() {}
func (*Envelope_HttpResponse) isEnvelope_Payload() {}
func (*Envelope_StreamOpen) isEnvelope_Payload() {}
func (*Envelope_StreamData) isEnvelope_Payload() {}
func (*Envelope_StreamClose) isEnvelope_Payload() {}
func (*Envelope_StreamAck) isEnvelope_Payload() {}
var File_internal_protocol_hopgate_stream_proto protoreflect.FileDescriptor
const file_internal_protocol_hopgate_stream_proto_rawDesc = "" +
"\n" +
"&internal/protocol/hopgate_stream.proto\x12\x13hopgate.protocol.v1\"&\n" +
"\fHeaderValues\x12\x16\n" +
"\x06values\x18\x01 \x03(\tR\x06values\"\xc6\x02\n" +
"\aRequest\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x1b\n" +
"\tclient_id\x18\x02 \x01(\tR\bclientId\x12!\n" +
"\fservice_name\x18\x03 \x01(\tR\vserviceName\x12\x16\n" +
"\x06method\x18\x04 \x01(\tR\x06method\x12\x10\n" +
"\x03url\x18\x05 \x01(\tR\x03url\x12@\n" +
"\x06header\x18\x06 \x03(\v2(.hopgate.protocol.v1.Request.HeaderEntryR\x06header\x12\x12\n" +
"\x04body\x18\a \x01(\fR\x04body\x1a\\\n" +
"\vHeaderEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x127\n" +
"\x05value\x18\x02 \x01(\v2!.hopgate.protocol.v1.HeaderValuesR\x05value:\x028\x01\"\x8c\x02\n" +
"\bResponse\x12\x1d\n" +
"\n" +
"request_id\x18\x01 \x01(\tR\trequestId\x12\x16\n" +
"\x06status\x18\x02 \x01(\x05R\x06status\x12A\n" +
"\x06header\x18\x03 \x03(\v2).hopgate.protocol.v1.Response.HeaderEntryR\x06header\x12\x12\n" +
"\x04body\x18\x04 \x01(\fR\x04body\x12\x14\n" +
"\x05error\x18\x05 \x01(\tR\x05error\x1a\\\n" +
"\vHeaderEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x127\n" +
"\x05value\x18\x02 \x01(\v2!.hopgate.protocol.v1.HeaderValuesR\x05value:\x028\x01\"\x83\x02\n" +
"\n" +
"StreamOpen\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12!\n" +
"\fservice_name\x18\x02 \x01(\tR\vserviceName\x12\x1f\n" +
"\vtarget_addr\x18\x03 \x01(\tR\n" +
"targetAddr\x12C\n" +
"\x06header\x18\x04 \x03(\v2+.hopgate.protocol.v1.StreamOpen.HeaderEntryR\x06header\x1a\\\n" +
"\vHeaderEntry\x12\x10\n" +
"\x03key\x18\x01 \x01(\tR\x03key\x127\n" +
"\x05value\x18\x02 \x01(\v2!.hopgate.protocol.v1.HeaderValuesR\x05value:\x028\x01\"B\n" +
"\n" +
"StreamData\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x10\n" +
"\x03seq\x18\x02 \x01(\x04R\x03seq\x12\x12\n" +
"\x04data\x18\x03 \x01(\fR\x04data\"r\n" +
"\tStreamAck\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x17\n" +
"\aack_seq\x18\x02 \x01(\x04R\x06ackSeq\x12\x1b\n" +
"\tlost_seqs\x18\x03 \x03(\x04R\blostSeqs\x12\x1f\n" +
"\vwindow_size\x18\x04 \x01(\rR\n" +
"windowSize\"3\n" +
"\vStreamClose\x12\x0e\n" +
"\x02id\x18\x01 \x01(\tR\x02id\x12\x14\n" +
"\x05error\x18\x02 \x01(\tR\x05error\"\xae\x03\n" +
"\bEnvelope\x12A\n" +
"\fhttp_request\x18\x01 \x01(\v2\x1c.hopgate.protocol.v1.RequestH\x00R\vhttpRequest\x12D\n" +
"\rhttp_response\x18\x02 \x01(\v2\x1d.hopgate.protocol.v1.ResponseH\x00R\fhttpResponse\x12B\n" +
"\vstream_open\x18\x03 \x01(\v2\x1f.hopgate.protocol.v1.StreamOpenH\x00R\n" +
"streamOpen\x12B\n" +
"\vstream_data\x18\x04 \x01(\v2\x1f.hopgate.protocol.v1.StreamDataH\x00R\n" +
"streamData\x12E\n" +
"\fstream_close\x18\x05 \x01(\v2 .hopgate.protocol.v1.StreamCloseH\x00R\vstreamClose\x12?\n" +
"\n" +
"stream_ack\x18\x06 \x01(\v2\x1e.hopgate.protocol.v1.StreamAckH\x00R\tstreamAckB\t\n" +
"\apayloadB@Z>github.com/dalbodeule/hop-gate/internal/protocol/pb;protocolpbb\x06proto3"
var (
file_internal_protocol_hopgate_stream_proto_rawDescOnce sync.Once
file_internal_protocol_hopgate_stream_proto_rawDescData []byte
)
func file_internal_protocol_hopgate_stream_proto_rawDescGZIP() []byte {
file_internal_protocol_hopgate_stream_proto_rawDescOnce.Do(func() {
file_internal_protocol_hopgate_stream_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_internal_protocol_hopgate_stream_proto_rawDesc), len(file_internal_protocol_hopgate_stream_proto_rawDesc)))
})
return file_internal_protocol_hopgate_stream_proto_rawDescData
}
var file_internal_protocol_hopgate_stream_proto_msgTypes = make([]protoimpl.MessageInfo, 11)
var file_internal_protocol_hopgate_stream_proto_goTypes = []any{
(*HeaderValues)(nil), // 0: hopgate.protocol.v1.HeaderValues
(*Request)(nil), // 1: hopgate.protocol.v1.Request
(*Response)(nil), // 2: hopgate.protocol.v1.Response
(*StreamOpen)(nil), // 3: hopgate.protocol.v1.StreamOpen
(*StreamData)(nil), // 4: hopgate.protocol.v1.StreamData
(*StreamAck)(nil), // 5: hopgate.protocol.v1.StreamAck
(*StreamClose)(nil), // 6: hopgate.protocol.v1.StreamClose
(*Envelope)(nil), // 7: hopgate.protocol.v1.Envelope
nil, // 8: hopgate.protocol.v1.Request.HeaderEntry
nil, // 9: hopgate.protocol.v1.Response.HeaderEntry
nil, // 10: hopgate.protocol.v1.StreamOpen.HeaderEntry
}
var file_internal_protocol_hopgate_stream_proto_depIdxs = []int32{
8, // 0: hopgate.protocol.v1.Request.header:type_name -> hopgate.protocol.v1.Request.HeaderEntry
9, // 1: hopgate.protocol.v1.Response.header:type_name -> hopgate.protocol.v1.Response.HeaderEntry
10, // 2: hopgate.protocol.v1.StreamOpen.header:type_name -> hopgate.protocol.v1.StreamOpen.HeaderEntry
1, // 3: hopgate.protocol.v1.Envelope.http_request:type_name -> hopgate.protocol.v1.Request
2, // 4: hopgate.protocol.v1.Envelope.http_response:type_name -> hopgate.protocol.v1.Response
3, // 5: hopgate.protocol.v1.Envelope.stream_open:type_name -> hopgate.protocol.v1.StreamOpen
4, // 6: hopgate.protocol.v1.Envelope.stream_data:type_name -> hopgate.protocol.v1.StreamData
6, // 7: hopgate.protocol.v1.Envelope.stream_close:type_name -> hopgate.protocol.v1.StreamClose
5, // 8: hopgate.protocol.v1.Envelope.stream_ack:type_name -> hopgate.protocol.v1.StreamAck
0, // 9: hopgate.protocol.v1.Request.HeaderEntry.value:type_name -> hopgate.protocol.v1.HeaderValues
0, // 10: hopgate.protocol.v1.Response.HeaderEntry.value:type_name -> hopgate.protocol.v1.HeaderValues
0, // 11: hopgate.protocol.v1.StreamOpen.HeaderEntry.value:type_name -> hopgate.protocol.v1.HeaderValues
12, // [12:12] is the sub-list for method output_type
12, // [12:12] is the sub-list for method input_type
12, // [12:12] is the sub-list for extension type_name
12, // [12:12] is the sub-list for extension extendee
0, // [0:12] is the sub-list for field type_name
}
func init() { file_internal_protocol_hopgate_stream_proto_init() }
func file_internal_protocol_hopgate_stream_proto_init() {
if File_internal_protocol_hopgate_stream_proto != nil {
return
}
file_internal_protocol_hopgate_stream_proto_msgTypes[7].OneofWrappers = []any{
(*Envelope_HttpRequest)(nil),
(*Envelope_HttpResponse)(nil),
(*Envelope_StreamOpen)(nil),
(*Envelope_StreamData)(nil),
(*Envelope_StreamClose)(nil),
(*Envelope_StreamAck)(nil),
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: unsafe.Slice(unsafe.StringData(file_internal_protocol_hopgate_stream_proto_rawDesc), len(file_internal_protocol_hopgate_stream_proto_rawDesc)),
NumEnums: 0,
NumMessages: 11,
NumExtensions: 0,
NumServices: 0,
},
GoTypes: file_internal_protocol_hopgate_stream_proto_goTypes,
DependencyIndexes: file_internal_protocol_hopgate_stream_proto_depIdxs,
MessageInfos: file_internal_protocol_hopgate_stream_proto_msgTypes,
}.Build()
File_internal_protocol_hopgate_stream_proto = out.File
file_internal_protocol_hopgate_stream_proto_goTypes = nil
file_internal_protocol_hopgate_stream_proto_depIdxs = nil
}

View File

@@ -1,10 +1,8 @@
package proxy package proxy
import ( import (
"bufio"
"bytes" "bytes"
"context" "context"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net" "net"
@@ -67,10 +65,10 @@ func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
// "dtls: buffer too small" 오류가 날 수 있으므로, 여기서는 여유 있는 버퍼(64KiB)를 사용합니다. (ko) // "dtls: buffer too small" 오류가 날 수 있으므로, 여기서는 여유 있는 버퍼(64KiB)를 사용합니다. (ko)
// NOTE: pion/dtls decrypts application data into the buffer provided by the caller. // NOTE: pion/dtls decrypts application data into the buffer provided by the caller.
// Using only the default JSON decoder buffer (a few hundred bytes) can trigger // Using only the default JSON decoder buffer (a few hundred bytes) can trigger
// "dtls: buffer too small" for large HTTP bodies/envelopes, so we wrap the // "dtls: buffer too small" for large HTTP bodies/envelopes. The default
// session with a reasonably large bufio.Reader (64KiB). (en) // JSON-based WireCodec internally wraps the DTLS session with a 64KiB
dec := json.NewDecoder(bufio.NewReaderSize(sess, 64*1024)) // bufio.Reader, matching this requirement. (en)
enc := json.NewEncoder(sess) codec := protocol.DefaultCodec
for { for {
select { select {
@@ -83,7 +81,7 @@ func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
} }
var env protocol.Envelope var env protocol.Envelope
if err := dec.Decode(&env); err != nil { if err := codec.Decode(sess, &env); err != nil {
if err == io.EOF { if err == io.EOF {
log.Info("dtls session closed by server", nil) log.Info("dtls session closed by server", nil)
return nil return nil
@@ -135,7 +133,7 @@ func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
HTTPResponse: &resp, HTTPResponse: &resp,
} }
if err := enc.Encode(&respEnv); err != nil { if err := codec.Encode(sess, &respEnv); err != nil {
logReq.Error("failed to encode http response envelope", logging.Fields{ logReq.Error("failed to encode http response envelope", logging.Fields{
"error": err.Error(), "error": err.Error(),
}) })

View File

@@ -265,7 +265,7 @@ The following tasks describe concrete work items to be implemented on the `featu
##### 3.3A.1 스트림 프레이밍 프로토콜 설계 (JSON 1단계) ##### 3.3A.1 스트림 프레이밍 프로토콜 설계 (JSON 1단계)
##### 3.3A.1 Stream framing protocol (JSON, phase 1) ##### 3.3A.1 Stream framing protocol (JSON, phase 1)
- [ ] 스트림 프레임 타입 정리 및 확장: [`internal/protocol/protocol.go`](internal/protocol/protocol.go:35) - [x] 스트림 프레임 타입 정리 및 확장: [`internal/protocol/protocol.go`](internal/protocol/protocol.go:35)
- 이미 정의된 스트림 관련 타입을 1단계에서 적극 활용합니다. - 이미 정의된 스트림 관련 타입을 1단계에서 적극 활용합니다.
Reuse the already defined stream-related types in phase 1: Reuse the already defined stream-related types in phase 1:
- `MessageTypeStreamOpen`, `MessageTypeStreamData`, `MessageTypeStreamClose` - `MessageTypeStreamOpen`, `MessageTypeStreamData`, `MessageTypeStreamClose`
@@ -281,7 +281,7 @@ The following tasks describe concrete work items to be implemented on the `featu
} }
``` ```
- [ ] 스트림 ACK / 재전송 제어 메시지 추가: [`internal/protocol/protocol.go`](internal/protocol/protocol.go:52) - [x] 스트림 ACK / 재전송 제어 메시지 추가: [`internal/protocol/protocol.go`](internal/protocol/protocol.go:52)
- 선택적 재전송(Selective Retransmission)을 위해 `StreamAck` 메시지와 `MessageTypeStreamAck` 를 추가합니다. - 선택적 재전송(Selective Retransmission)을 위해 `StreamAck` 메시지와 `MessageTypeStreamAck` 를 추가합니다.
Add `StreamAck` message and `MessageTypeStreamAck` for selective retransmission: Add `StreamAck` message and `MessageTypeStreamAck` for selective retransmission:
```go ```go
@@ -310,7 +310,7 @@ The following tasks describe concrete work items to be implemented on the `featu
##### 3.3A.2 애플리케이션 레벨 ARQ 설계 (Selective Retransmission) ##### 3.3A.2 애플리케이션 레벨 ARQ 설계 (Selective Retransmission)
##### 3.3A.2 Application-level ARQ (Selective Retransmission) ##### 3.3A.2 Application-level ARQ (Selective Retransmission)
- [ ] 수신 측 스트림 상태 관리 로직 설계 - [x] 수신 측 스트림 상태 관리 로직 설계
- 스트림별로 다음 상태를 유지합니다. - 스트림별로 다음 상태를 유지합니다.
For each stream, maintain: For each stream, maintain:
- `expectedSeq` (다음에 연속으로 기대하는 Seq, 초기값 0) - `expectedSeq` (다음에 연속으로 기대하는 Seq, 초기값 0)
@@ -328,7 +328,7 @@ The following tasks describe concrete work items to be implemented on the `featu
`expectedSeq` ~ `Seq-1` 구간 중 비어 있는 Seq 들을 `lostBuffer` 에 추가. `expectedSeq` ~ `Seq-1` 구간 중 비어 있는 Seq 들을 `lostBuffer` 에 추가.
If `Seq > expectedSeq`, buffer as out-of-order and mark missing seqs in `lostBuffer`. If `Seq > expectedSeq`, buffer as out-of-order and mark missing seqs in `lostBuffer`.
- [ ] 수신 측 StreamAck 전송 정책 - [x] 수신 측 StreamAck 전송 정책
- 주기적 타이머 또는 일정 수의 프레임 처리 후에 `StreamAck` 를 전송합니다. - 주기적 타이머 또는 일정 수의 프레임 처리 후에 `StreamAck` 를 전송합니다.
Send `StreamAck` periodically or after processing N frames: Send `StreamAck` periodically or after processing N frames:
- `AckSeq = expectedSeq - 1` (연속 수신 완료 지점) - `AckSeq = expectedSeq - 1` (연속 수신 완료 지점)
@@ -336,7 +336,7 @@ The following tasks describe concrete work items to be implemented on the `featu
- `LostSeqs` 는 윈도우 내 손실 시퀀스 중 상한 개수까지만 포함 (과도한 길이 방지). - `LostSeqs` 는 윈도우 내 손실 시퀀스 중 상한 개수까지만 포함 (과도한 길이 방지).
`LostSeqs` should only include a bounded set of missing seqs within the receive window. `LostSeqs` should only include a bounded set of missing seqs within the receive window.
- [ ] 송신 측 재전송 로직 - [x] 송신 측 재전송 로직
- 스트림별로 다음 상태를 유지합니다. - 스트림별로 다음 상태를 유지합니다.
For each stream on the sender: For each stream on the sender:
- `sendSeq` 송신에 사용할 다음 Seq (0부터 시작) - `sendSeq` 송신에 사용할 다음 Seq (0부터 시작)
@@ -361,7 +361,7 @@ The following tasks describe concrete work items to be implemented on the `featu
##### 3.3A.3 HTTP ↔ 스트림 매핑 (서버/클라이언트) ##### 3.3A.3 HTTP ↔ 스트림 매핑 (서버/클라이언트)
##### 3.3A.3 HTTP ↔ stream mapping (server/client) ##### 3.3A.3 HTTP ↔ stream mapping (server/client)
- [ ] 서버 → 클라이언트 요청 스트림: [`cmd/server/main.go`](cmd/server/main.go:200) - [x] 서버 → 클라이언트 요청 스트림: [`cmd/server/main.go`](cmd/server/main.go:200)
- 현재 `ForwardHTTP` 는 단일 `HTTPRequest`/`HTTPResponse` 를 처리하는 구조입니다. - 현재 `ForwardHTTP` 는 단일 `HTTPRequest`/`HTTPResponse` 를 처리하는 구조입니다.
Currently `ForwardHTTP` handles a single `HTTPRequest`/`HTTPResponse` pair. Currently `ForwardHTTP` handles a single `HTTPRequest`/`HTTPResponse` pair.
- 스트림 모드에서는 다음과 같이 바꿉니다. - 스트림 모드에서는 다음과 같이 바꿉니다.
@@ -385,7 +385,7 @@ The following tasks describe concrete work items to be implemented on the `featu
- `StreamClose` 수신 시 응답 종료 및 스트림 자원 정리. - `StreamClose` 수신 시 응답 종료 및 스트림 자원 정리.
On `StreamClose`, finish the response and clean up per-stream state. On `StreamClose`, finish the response and clean up per-stream state.
- [ ] 클라이언트에서의 요청 처리 스트림: [`internal/proxy/client.go`](internal/proxy/client.go:200) - [x] 클라이언트에서의 요청 처리 스트림: [`internal/proxy/client.go`](internal/proxy/client.go:200)
- 서버로부터 들어오는 `StreamOpen{ID, ...}` 을 수신하면, - 서버로부터 들어오는 `StreamOpen{ID, ...}` 을 수신하면,
새로운 goroutine 을 띄워 해당 ID에 대한 로컬 HTTP 요청을 수행합니다. 새로운 goroutine 을 띄워 해당 ID에 대한 로컬 HTTP 요청을 수행합니다.
On receiving `StreamOpen{ID, ...}` from the server, spawn a goroutine to handle the local HTTP request for that stream ID. On receiving `StreamOpen{ID, ...}` from the server, spawn a goroutine to handle the local HTTP request for that stream ID.
@@ -413,8 +413,11 @@ The following tasks describe concrete work items to be implemented on the `featu
- 동일한 logical model (`StreamOpen` / `StreamData(seq)` / `StreamClose` / `StreamAck`)을 유지한 채, - 동일한 logical model (`StreamOpen` / `StreamData(seq)` / `StreamClose` / `StreamAck`)을 유지한 채,
wire-format 만 Protobuf 또는 MsgPack 등의 length-prefix binary 프레이밍으로 교체할 수 있습니다. wire-format 만 Protobuf 또는 MsgPack 등의 length-prefix binary 프레이밍으로 교체할 수 있습니다.
We can later keep the same logical model and swap the wire format for Protobuf or other length-prefix binary framing. We can later keep the same logical model and swap the wire format for Protobuf or other length-prefix binary framing.
- 이 전환은 `internal/protocol` 내 직렬화 레이어를 얇은 abstraction 으로 감싸 구현할 수 있습니다. - [x] 이 전환은 `internal/protocol` 내 직렬화 레이어를 얇은 abstraction 으로 감싸 구현할 수 있습니다.
This can be implemented by wrapping serialization in a thin abstraction layer inside [`internal/protocol`](internal/protocol/protocol.go:35). - 현재는 [`internal/protocol/codec.go`](internal/protocol/codec.go:1) 에 `WireCodec` 인터페이스와 JSON 기반 `DefaultCodec` 을 도입하여,
추후 Protobuf/이진 포맷으로 교체할 때 호출자는 `protocol.DefaultCodec` 만 사용하도록 분리해 두었습니다.
- This has been prepared via [`internal/protocol/codec.go`](internal/protocol/codec.go:1), which introduces a `WireCodec` interface
and a JSON-based `DefaultCodec` so that future Protobuf/binary codecs can be swapped in behind the same API.
--- ---