mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2026-02-04 15:52:24 +09:00
[feat](protocol, client, server): replace DTLS with gRPC for tunnel implementation
- Introduced gRPC-based tunnel design for bi-directional communication, replacing legacy DTLS transport. - Added `HopGateTunnel` gRPC service with client and server logic for `OpenTunnel` stream handling. - Updated client to use gRPC tunnel exclusively, including experimental entry point for stream-based HTTP proxying. - Removed DTLS-specific client, server, and related dependencies (`pion/dtls`). - Adjusted `cmd/server` to route gRPC and HTTP/HTTPS traffic dynamically on shared ports.
This commit is contained in:
@@ -1,218 +1,58 @@
|
||||
package dtls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
piondtls "github.com/pion/dtls/v3"
|
||||
)
|
||||
|
||||
// pionSession 은 pion/dtls.Conn 을 감싸 Session 인터페이스를 구현합니다.
|
||||
type pionSession struct {
|
||||
conn *piondtls.Conn
|
||||
id string
|
||||
}
|
||||
|
||||
func (s *pionSession) Read(b []byte) (int, error) { return s.conn.Read(b) }
|
||||
func (s *pionSession) Write(b []byte) (int, error) { return s.conn.Write(b) }
|
||||
func (s *pionSession) Close() error { return s.conn.Close() }
|
||||
func (s *pionSession) ID() string { return s.id }
|
||||
|
||||
// pionServer 는 pion/dtls 기반 Server 구현입니다.
|
||||
type pionServer struct {
|
||||
listener net.Listener
|
||||
}
|
||||
|
||||
// PionServerConfig 는 DTLS 서버 리스너 구성을 정의합니다.
|
||||
// PionServerConfig 는 DTLS 서버 리스너 구성을 정의하는 기존 구조체를 그대로 유지합니다. (ko)
|
||||
// PionServerConfig keeps the old DTLS server listener configuration shape for compatibility. (en)
|
||||
type PionServerConfig struct {
|
||||
// Addr 는 "0.0.0.0:443" 와 같은 UDP 리스닝 주소입니다.
|
||||
Addr string
|
||||
|
||||
// TLSConfig 는 ACME 등을 통해 준비된 tls.Config 입니다.
|
||||
// Certificates, RootCAs, ClientAuth 등의 설정이 여기서 넘어옵니다.
|
||||
// nil 인 경우 기본 빈 tls.Config 가 사용됩니다.
|
||||
Addr string
|
||||
TLSConfig *tls.Config
|
||||
}
|
||||
|
||||
// NewPionServer 는 pion/dtls 기반 DTLS 서버를 생성합니다.
|
||||
// 내부적으로 udp 리스너를 열고, DTLS 핸드셰이크를 수행할 준비를 합니다.
|
||||
func NewPionServer(cfg PionServerConfig) (Server, error) {
|
||||
if cfg.Addr == "" {
|
||||
return nil, fmt.Errorf("PionServerConfig.Addr is required")
|
||||
}
|
||||
if cfg.TLSConfig == nil {
|
||||
cfg.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr("udp", cfg.Addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve udp addr: %w", err)
|
||||
}
|
||||
|
||||
// tls.Config.GetCertificate (crypto/tls) → pion/dtls.GetCertificate 어댑터
|
||||
var getCert func(*piondtls.ClientHelloInfo) (*tls.Certificate, error)
|
||||
if cfg.TLSConfig.GetCertificate != nil {
|
||||
tlsGetCert := cfg.TLSConfig.GetCertificate
|
||||
getCert = func(chi *piondtls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
if chi == nil {
|
||||
return tlsGetCert(&tls.ClientHelloInfo{})
|
||||
}
|
||||
// ACME 매니저는 주로 SNI(ServerName)에 기반해 인증서를 선택하므로,
|
||||
// 필요한 최소 필드만 복사해서 전달한다.
|
||||
return tlsGetCert(&tls.ClientHelloInfo{
|
||||
ServerName: chi.ServerName,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
dtlsCfg := &piondtls.Config{
|
||||
// 서버가 사용할 인증서 설정: 정적 Certificates + GetCertificate 어댑터
|
||||
Certificates: cfg.TLSConfig.Certificates,
|
||||
GetCertificate: getCert,
|
||||
InsecureSkipVerify: cfg.TLSConfig.InsecureSkipVerify,
|
||||
ClientAuth: piondtls.ClientAuthType(cfg.TLSConfig.ClientAuth),
|
||||
ClientCAs: cfg.TLSConfig.ClientCAs,
|
||||
RootCAs: cfg.TLSConfig.RootCAs,
|
||||
ServerName: cfg.TLSConfig.ServerName,
|
||||
// 필요 시 ExtendedMasterSecret 등을 추가 설정
|
||||
}
|
||||
l, err := piondtls.Listen("udp", udpAddr, dtlsCfg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dtls listen: %w", err)
|
||||
}
|
||||
|
||||
return &pionServer{
|
||||
listener: l,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Accept 는 새로운 DTLS 연결을 수락하고, Session 으로 래핑합니다.
|
||||
func (s *pionServer) Accept() (Session, error) {
|
||||
conn, err := s.listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dtlsConn, ok := conn.(*piondtls.Conn)
|
||||
if !ok {
|
||||
_ = conn.Close()
|
||||
return nil, fmt.Errorf("accepted connection is not *dtls.Conn")
|
||||
}
|
||||
|
||||
id := ""
|
||||
if ra := dtlsConn.RemoteAddr(); ra != nil {
|
||||
id = ra.String()
|
||||
}
|
||||
|
||||
return &pionSession{
|
||||
conn: dtlsConn,
|
||||
id: id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Close 는 DTLS 리스너를 종료합니다.
|
||||
func (s *pionServer) Close() error {
|
||||
return s.listener.Close()
|
||||
}
|
||||
|
||||
// pionClient 는 pion/dtls 기반 Client 구현입니다.
|
||||
type pionClient struct {
|
||||
addr string
|
||||
tlsConfig *tls.Config
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// PionClientConfig 는 DTLS 클라이언트 구성을 정의합니다.
|
||||
// PionClientConfig 는 DTLS 클라이언트 구성을 정의하는 기존 구조체를 그대로 유지합니다. (ko)
|
||||
// PionClientConfig keeps the old DTLS client configuration shape for compatibility. (en)
|
||||
type PionClientConfig struct {
|
||||
// Addr 는 서버의 UDP 주소 (예: "example.com:443") 입니다.
|
||||
Addr string
|
||||
|
||||
// TLSConfig 는 서버 인증에 사용할 tls.Config 입니다.
|
||||
// InsecureSkipVerify=true 로 두면 서버 인증을 건너뛰므로 개발/테스트에만 사용해야 합니다.
|
||||
Addr string
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// Timeout 은 DTLS 핸드셰이크 타임아웃입니다.
|
||||
// 0 이면 기본값 10초가 사용됩니다.
|
||||
Timeout time.Duration
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
// NewPionClient 는 pion/dtls 기반 DTLS 클라이언트를 생성합니다.
|
||||
func NewPionClient(cfg PionClientConfig) Client {
|
||||
if cfg.Timeout == 0 {
|
||||
cfg.Timeout = 10 * time.Second
|
||||
}
|
||||
if cfg.TLSConfig == nil {
|
||||
// 기본값: 인증서 검증을 수행하는 안전한 설정(루트 CA 체인은 시스템 기본값 사용).
|
||||
// 디버그 모드에서 인증서 검증을 스킵하고 싶다면, 호출 측에서
|
||||
// TLSConfig: &tls.Config{InsecureSkipVerify: true} 를 명시적으로 전달해야 합니다.
|
||||
cfg.TLSConfig = &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}
|
||||
}
|
||||
return &pionClient{
|
||||
addr: cfg.Addr,
|
||||
tlsConfig: cfg.TLSConfig,
|
||||
timeout: cfg.Timeout,
|
||||
}
|
||||
// disabledServer 는 DTLS 전송이 비활성화되었음을 나타내는 더미 구현입니다. (ko)
|
||||
// disabledServer is a dummy Server implementation indicating that DTLS transport is disabled. (en)
|
||||
type disabledServer struct{}
|
||||
|
||||
func (s *disabledServer) Accept() (Session, error) {
|
||||
return nil, fmt.Errorf("dtls transport is disabled; use gRPC tunnel instead")
|
||||
}
|
||||
|
||||
// Connect 는 서버와 DTLS 핸드셰이크를 수행하고 Session 을 반환합니다.
|
||||
func (c *pionClient) Connect() (Session, error) {
|
||||
if c.addr == "" {
|
||||
return nil, fmt.Errorf("PionClientConfig.Addr is required")
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
|
||||
defer cancel()
|
||||
|
||||
raddr, err := net.ResolveUDPAddr("udp", c.addr)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("resolve udp addr: %w", err)
|
||||
}
|
||||
|
||||
dtlsCfg := &piondtls.Config{
|
||||
// 클라이언트는 서버 인증을 위해 RootCAs/ServerName 만 사용.
|
||||
// (현재는 클라이언트 인증서 사용 계획이 없으므로 GetCertificate 는 전달하지 않는다.)
|
||||
Certificates: c.tlsConfig.Certificates,
|
||||
InsecureSkipVerify: c.tlsConfig.InsecureSkipVerify,
|
||||
RootCAs: c.tlsConfig.RootCAs,
|
||||
ServerName: c.tlsConfig.ServerName,
|
||||
}
|
||||
|
||||
type result struct {
|
||||
conn *piondtls.Conn
|
||||
err error
|
||||
}
|
||||
ch := make(chan result, 1)
|
||||
|
||||
go func() {
|
||||
conn, err := piondtls.Dial("udp", raddr, dtlsCfg)
|
||||
ch <- result{conn: conn, err: err}
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("dtls dial timeout: %w", ctx.Err())
|
||||
case res := <-ch:
|
||||
if res.err != nil {
|
||||
return nil, fmt.Errorf("dtls dial: %w", res.err)
|
||||
}
|
||||
id := ""
|
||||
if ra := res.conn.RemoteAddr(); ra != nil {
|
||||
id = ra.String()
|
||||
}
|
||||
return &pionSession{
|
||||
conn: res.conn,
|
||||
id: id,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Close 는 클라이언트 단에서 유지하는 리소스가 없으므로 no-op 입니다.
|
||||
func (c *pionClient) Close() error {
|
||||
func (s *disabledServer) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// disabledClient 는 DTLS 전송이 비활성화되었음을 나타내는 더미 구현입니다. (ko)
|
||||
// disabledClient is a dummy Client implementation indicating that DTLS transport is disabled. (en)
|
||||
type disabledClient struct{}
|
||||
|
||||
func (c *disabledClient) Connect() (Session, error) {
|
||||
return nil, fmt.Errorf("dtls transport is disabled; use gRPC tunnel instead")
|
||||
}
|
||||
|
||||
func (c *disabledClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// NewPionServer 는 더 이상 실제 DTLS 서버를 생성하지 않고, 항상 에러를 반환합니다. (ko)
|
||||
// NewPionServer no longer creates a real DTLS server and always returns an error. (en)
|
||||
func NewPionServer(cfg PionServerConfig) (Server, error) {
|
||||
return nil, fmt.Errorf("dtls transport is disabled; NewPionServer is no longer supported")
|
||||
}
|
||||
|
||||
// NewPionClient 는 더 이상 실제 DTLS 클라이언트를 생성하지 않고, disabledClient 를 반환합니다. (ko)
|
||||
// NewPionClient no longer creates a real DTLS client and instead returns a disabledClient. (en)
|
||||
func NewPionClient(cfg PionClientConfig) Client {
|
||||
return &disabledClient{}
|
||||
}
|
||||
|
||||
119
internal/protocol/pb/hopgate_stream_grpc.go
Normal file
119
internal/protocol/pb/hopgate_stream_grpc.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package pb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
// HopGateTunnelClient is the client API for the HopGateTunnel service.
|
||||
type HopGateTunnelClient interface {
|
||||
// OpenTunnel establishes a long-lived bi-directional stream between
|
||||
// a HopGate client and the server. Both HTTP requests and responses
|
||||
// are multiplexed as Envelope messages on this stream.
|
||||
OpenTunnel(ctx context.Context, opts ...grpc.CallOption) (HopGateTunnel_OpenTunnelClient, error)
|
||||
}
|
||||
|
||||
type hopGateTunnelClient struct {
|
||||
cc grpc.ClientConnInterface
|
||||
}
|
||||
|
||||
// NewHopGateTunnelClient creates a new HopGateTunnelClient.
|
||||
func NewHopGateTunnelClient(cc grpc.ClientConnInterface) HopGateTunnelClient {
|
||||
return &hopGateTunnelClient{cc: cc}
|
||||
}
|
||||
|
||||
func (c *hopGateTunnelClient) OpenTunnel(ctx context.Context, opts ...grpc.CallOption) (HopGateTunnel_OpenTunnelClient, error) {
|
||||
stream, err := c.cc.NewStream(ctx, &_HopGateTunnel_serviceDesc.Streams[0], "/hopgate.protocol.v1.HopGateTunnel/OpenTunnel", opts...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &hopGateTunnelOpenTunnelClient{ClientStream: stream}, nil
|
||||
}
|
||||
|
||||
// HopGateTunnel_OpenTunnelClient is the client-side stream for OpenTunnel.
|
||||
type HopGateTunnel_OpenTunnelClient interface {
|
||||
Send(*Envelope) error
|
||||
Recv() (*Envelope, error)
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
type hopGateTunnelOpenTunnelClient struct {
|
||||
grpc.ClientStream
|
||||
}
|
||||
|
||||
func (x *hopGateTunnelOpenTunnelClient) Send(m *Envelope) error {
|
||||
return x.ClientStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (x *hopGateTunnelOpenTunnelClient) Recv() (*Envelope, error) {
|
||||
m := new(Envelope)
|
||||
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
// HopGateTunnelServer is the server API for the HopGateTunnel service.
|
||||
type HopGateTunnelServer interface {
|
||||
// OpenTunnel handles a long-lived bi-directional stream between the server
|
||||
// and a HopGate client. Implementations are responsible for reading and
|
||||
// writing Envelope messages on the stream.
|
||||
OpenTunnel(HopGateTunnel_OpenTunnelServer) error
|
||||
}
|
||||
|
||||
// UnimplementedHopGateTunnelServer can be embedded to have forward compatible implementations.
|
||||
type UnimplementedHopGateTunnelServer struct{}
|
||||
|
||||
// OpenTunnel returns an Unimplemented error by default.
|
||||
func (UnimplementedHopGateTunnelServer) OpenTunnel(HopGateTunnel_OpenTunnelServer) error {
|
||||
return status.Errorf(codes.Unimplemented, "method OpenTunnel not implemented")
|
||||
}
|
||||
|
||||
// RegisterHopGateTunnelServer registers the HopGateTunnel service with the given gRPC server.
|
||||
func RegisterHopGateTunnelServer(s grpc.ServiceRegistrar, srv HopGateTunnelServer) {
|
||||
s.RegisterService(&_HopGateTunnel_serviceDesc, srv)
|
||||
}
|
||||
|
||||
// HopGateTunnel_OpenTunnelServer is the server-side stream for OpenTunnel.
|
||||
type HopGateTunnel_OpenTunnelServer interface {
|
||||
Send(*Envelope) error
|
||||
Recv() (*Envelope, error)
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
func _HopGateTunnel_OpenTunnel_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||
return srv.(HopGateTunnelServer).OpenTunnel(&hopGateTunnelOpenTunnelServer{ServerStream: stream})
|
||||
}
|
||||
|
||||
type hopGateTunnelOpenTunnelServer struct {
|
||||
grpc.ServerStream
|
||||
}
|
||||
|
||||
func (x *hopGateTunnelOpenTunnelServer) Send(m *Envelope) error {
|
||||
return x.ServerStream.SendMsg(m)
|
||||
}
|
||||
|
||||
func (x *hopGateTunnelOpenTunnelServer) Recv() (*Envelope, error) {
|
||||
m := new(Envelope)
|
||||
if err := x.ServerStream.RecvMsg(m); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
var _HopGateTunnel_serviceDesc = grpc.ServiceDesc{
|
||||
ServiceName: "hopgate.protocol.v1.HopGateTunnel",
|
||||
HandlerType: (*HopGateTunnelServer)(nil),
|
||||
Streams: []grpc.StreamDesc{
|
||||
{
|
||||
StreamName: "OpenTunnel",
|
||||
Handler: _HopGateTunnel_OpenTunnel_Handler,
|
||||
ServerStreams: true,
|
||||
ClientStreams: true,
|
||||
},
|
||||
},
|
||||
Metadata: "internal/protocol/hopgate_stream.proto",
|
||||
}
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/dalbodeule/hop-gate/internal/dtls"
|
||||
"github.com/dalbodeule/hop-gate/internal/logging"
|
||||
"github.com/dalbodeule/hop-gate/internal/protocol"
|
||||
)
|
||||
@@ -144,9 +143,9 @@ type streamReceiver struct {
|
||||
// Input channel for envelopes dispatched from the central readLoop. (en)
|
||||
inCh chan *protocol.Envelope
|
||||
|
||||
// DTLS 세션 및 직렬화 codec / 로깅 핸들. (ko)
|
||||
// DTLS session, wire codec and logging handles. (en)
|
||||
sess dtls.Session
|
||||
// 세션(write 방향) 및 직렬화 codec / 로깅 핸들. (ko)
|
||||
// Session (write side only), wire codec and logging handles. (en)
|
||||
sess io.ReadWriter
|
||||
codec protocol.WireCodec
|
||||
logger logging.Logger
|
||||
|
||||
@@ -161,7 +160,7 @@ type streamReceiver struct {
|
||||
// newStreamReceiver initializes a streamReceiver for a single stream ID. (en)
|
||||
func newStreamReceiver(
|
||||
id protocol.StreamID,
|
||||
sess dtls.Session,
|
||||
sess io.ReadWriter,
|
||||
codec protocol.WireCodec,
|
||||
logger logging.Logger,
|
||||
httpClient *http.Client,
|
||||
@@ -604,7 +603,7 @@ func (p *ClientProxy) getStreamSender(id protocol.StreamID) *streamSender {
|
||||
// - `handleStreamRequest` 내부 HTTP 매핑 로직을 `streamReceiver` 로 옮기고,
|
||||
// - StartLoop 가 DTLS 세션 → per-stream goroutine 으로 이벤트를 분배하는 역할만 수행하도록
|
||||
// 점진적으로 리팩터링할 예정입니다.
|
||||
func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
|
||||
func (p *ClientProxy) StartLoop(ctx context.Context, sess io.ReadWriter) error {
|
||||
if ctx == nil {
|
||||
ctx = context.Background()
|
||||
}
|
||||
@@ -839,7 +838,7 @@ func (p *ClientProxy) StartLoop(ctx context.Context, sess dtls.Session) error {
|
||||
|
||||
// handleHTTPEnvelope 는 기존 단일 HTTP 요청/응답 Envelope 경로를 처리합니다. (ko)
|
||||
// handleHTTPEnvelope handles the legacy single HTTP request/response envelope path. (en)
|
||||
func (p *ClientProxy) handleHTTPEnvelope(ctx context.Context, sess dtls.Session, env *protocol.Envelope) error {
|
||||
func (p *ClientProxy) handleHTTPEnvelope(ctx context.Context, sess io.ReadWriter, env *protocol.Envelope) error {
|
||||
if env.HTTPRequest == nil {
|
||||
return fmt.Errorf("http envelope missing http_request payload")
|
||||
}
|
||||
@@ -896,7 +895,7 @@ func (p *ClientProxy) handleHTTPEnvelope(ctx context.Context, sess dtls.Session,
|
||||
|
||||
// handleStreamRequest 는 StreamOpen/StreamData/StreamClose 기반 HTTP 요청/응답 스트림을 처리합니다. (ko)
|
||||
// handleStreamRequest handles an HTTP request/response exchange using StreamOpen/StreamData/StreamClose frames. (en)
|
||||
func (p *ClientProxy) handleStreamRequest(ctx context.Context, sess dtls.Session, reader io.Reader, openEnv *protocol.Envelope) error {
|
||||
func (p *ClientProxy) handleStreamRequest(ctx context.Context, sess io.ReadWriter, reader io.Reader, openEnv *protocol.Envelope) error {
|
||||
codec := protocol.DefaultCodec
|
||||
log := p.Logger
|
||||
|
||||
|
||||
Reference in New Issue
Block a user