mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2025-12-08 04:45:43 +09:00
feat(dtls): add dtls client-server handshake flow
Implement initial DTLS handshake flow for server and client using pion/dtls. Load server and client configuration from .env/environment, including new debug flags and logging config. On the server: - load ServerConfig from env, including DTLS listen addr and debug flag - create DTLS listener with optional self-signed localhost cert in debug - accept DTLS sessions and run PerformServerHandshake with a dummy domain validator On the client: - load ClientConfig from env, then override with CLI flags where given - validate required fields: server_addr, domain, api_key, local_target - create DTLS client and run PerformClientHandshake - support debug mode to skip server certificate verification Also: - update go.mod/go.sum with pion/dtls and related dependencies - extend .env.example with new ports, client config, and debug flags - ignore built binaries via bin/ in .gitignore BREAKING CHANGE: client environment variables have changed. The former HOP_CLIENT_ID, HOP_CLIENT_AUTH_TOKEN and HOP_CLIENT_SERVICE_PORTS are replaced by HOP_CLIENT_DOMAIN, HOP_CLIENT_API_KEY, HOP_CLIENT_LOCAL_TARGET and HOP_CLIENT_DEBUG. Client startup now requires server_addr, domain, api_key and local_target to be provided (via env or CLI).
This commit is contained in:
@@ -33,16 +33,26 @@ type ServerConfig struct {
|
||||
DTLSListen string // 예: ":443"
|
||||
Domain string // 메인 도메인
|
||||
ProxyDomains []string // 프록시 서브도메인 또는 별도 도메인
|
||||
Debug bool // true 이면 디버그 모드 (예: self-signed 인증서 신뢰, 검증 스킵 등)
|
||||
|
||||
Logging LoggingConfig // 서버용 로그 설정
|
||||
}
|
||||
|
||||
// ClientConfig 는 클라이언트 프로세스 설정을 담습니다.
|
||||
// 현재 클라이언트는 다음 4가지 설정만 사용합니다.
|
||||
// - ServerAddr : DTLS 서버 주소 (host:port)
|
||||
// - Domain : 서버에서 등록된 도메인 (예: api.example.com)
|
||||
// - ClientAPIKey : 도메인에 매핑된 64자 클라이언트 API Key
|
||||
// - LocalTarget : 로컬에서 요청할 서버 주소 (예: 127.0.0.1:8080)
|
||||
//
|
||||
// 값은 .env/환경변수와 CLI 인자를 조합해 구성하며,
|
||||
// CLI 인자가 우선, env 가 후순위로 적용됩니다.
|
||||
type ClientConfig struct {
|
||||
ServerAddr string // DTLS 서버 주소 (host:port)
|
||||
ClientID string // 클라이언트 식별자
|
||||
AuthToken string // 선택적 인증 토큰
|
||||
ServicePorts map[string]string // service name -> "127.0.0.1:PORT"
|
||||
ServerAddr string // DTLS 서버 주소 (host:port)
|
||||
Domain string // 서버에서 등록된 도메인 (예: api.example.com)
|
||||
ClientAPIKey string // 도메인에 매핑된 64자 클라이언트 API Key
|
||||
LocalTarget string // 로컬에서 요청할 서버 주소 (예: 127.0.0.1:8080)
|
||||
Debug bool // true 이면 디버그 모드 (예: 서버 인증서 검증 스킵 등)
|
||||
|
||||
Logging LoggingConfig // 클라이언트용 로그 설정
|
||||
}
|
||||
@@ -212,12 +222,14 @@ func LoadServerConfigFromEnv() (*ServerConfig, error) {
|
||||
DTLSListen: getEnvOrDefault("HOP_SERVER_DTLS_LISTEN", ":443"),
|
||||
Domain: os.Getenv("HOP_SERVER_DOMAIN"),
|
||||
ProxyDomains: parseCSVEnv("HOP_SERVER_PROXY_DOMAINS"),
|
||||
Debug: getEnvBool("HOP_SERVER_DEBUG", false),
|
||||
Logging: loadLoggingFromEnv(),
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
// LoadClientConfigFromEnv 는 .env 를 우선 읽고, 이후 환경 변수를 기반으로 클라이언트 설정을 구성합니다.
|
||||
// 실제 런타임에서 사용되는 필드는 ServerAddr, Domain, ClientAPIKey, LocalTarget 입니다.
|
||||
func LoadClientConfigFromEnv() (*ClientConfig, error) {
|
||||
loadDotEnvOnce()
|
||||
if dotenvErr != nil {
|
||||
@@ -226,9 +238,10 @@ func LoadClientConfigFromEnv() (*ClientConfig, error) {
|
||||
|
||||
cfg := &ClientConfig{
|
||||
ServerAddr: os.Getenv("HOP_CLIENT_SERVER_ADDR"),
|
||||
ClientID: os.Getenv("HOP_CLIENT_ID"),
|
||||
AuthToken: os.Getenv("HOP_CLIENT_AUTH_TOKEN"),
|
||||
ServicePorts: parseServicePortsEnv("HOP_CLIENT_SERVICE_PORTS"),
|
||||
Domain: os.Getenv("HOP_CLIENT_DOMAIN"),
|
||||
ClientAPIKey: os.Getenv("HOP_CLIENT_API_KEY"),
|
||||
LocalTarget: os.Getenv("HOP_CLIENT_LOCAL_TARGET"),
|
||||
Debug: getEnvBool("HOP_CLIENT_DEBUG", false),
|
||||
Logging: loadLoggingFromEnv(),
|
||||
}
|
||||
return cfg, nil
|
||||
|
||||
196
internal/dtls/handshake.go
Normal file
196
internal/dtls/handshake.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package dtls
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/dalbodeule/hop-gate/internal/logging"
|
||||
)
|
||||
|
||||
// DomainValidator 는 (domain, clientAPIKey) 조합이 유효한지 검증하는 인터페이스입니다.
|
||||
// 실제 구현에서는 ent + PostgreSQL 을 사용해 Domain 테이블을 조회하면 됩니다.
|
||||
type DomainValidator interface {
|
||||
ValidateDomainAPIKey(ctx context.Context, domain, clientAPIKey string) error
|
||||
}
|
||||
|
||||
// ServerHandshakeResult 는 서버 측에서 핸드셰이크가 완료된 후의 정보를 담습니다.
|
||||
type ServerHandshakeResult struct {
|
||||
Domain string
|
||||
}
|
||||
|
||||
// ClientHandshakeResult 는 클라이언트 측에서 핸드셰이크가 완료된 후의 정보를 담습니다.
|
||||
type ClientHandshakeResult struct {
|
||||
Domain string
|
||||
Message string
|
||||
}
|
||||
|
||||
// handshakeRequest 는 클라이언트가 최초 DTLS 연결 후 서버로 보내는 메시지입니다.
|
||||
// - Domain: 사용할 도메인 (예: api.example.com)
|
||||
// - ClientAPIKey: 관리 plane 을 통해 발급받은 64자 API Key
|
||||
type handshakeRequest struct {
|
||||
Domain string `json:"domain"`
|
||||
ClientAPIKey string `json:"client_api_key"`
|
||||
}
|
||||
|
||||
// handshakeResponse 는 서버가 핸드셰이크 결과를 클라이언트로 돌려줄 때 사용하는 메시지입니다.
|
||||
type handshakeResponse struct {
|
||||
OK bool `json:"ok"`
|
||||
Message string `json:"message"`
|
||||
Domain string `json:"domain"`
|
||||
}
|
||||
|
||||
// PerformServerHandshake 는 서버 측에서 DTLS 세션이 생성된 직후 호출되어
|
||||
// 클라이언트가 보낸 (domain, client_api_key)를 검증합니다.
|
||||
//
|
||||
// 성공 시:
|
||||
// - 서버 로그에 "어떤 도메인이 연결되었는지" 기록
|
||||
// - 클라이언트로 OK 응답을 전송
|
||||
// - ServerHandshakeResult 에 도메인 정보를 담아 반환
|
||||
func PerformServerHandshake(
|
||||
ctx context.Context,
|
||||
sess Session,
|
||||
validator DomainValidator,
|
||||
logger logging.Logger,
|
||||
) (*ServerHandshakeResult, error) {
|
||||
log := logger.With(logging.Fields{"phase": "dtls_handshake", "side": "server"})
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var req handshakeRequest
|
||||
if err := json.NewDecoder(sess).Decode(&req); err != nil {
|
||||
log.Error("failed to read handshake request", logging.Fields{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("read handshake request: %w", err)
|
||||
}
|
||||
|
||||
req.Domain = stringTrimSpace(req.Domain)
|
||||
req.ClientAPIKey = stringTrimSpace(req.ClientAPIKey)
|
||||
|
||||
if req.Domain == "" || req.ClientAPIKey == "" {
|
||||
_ = writeHandshakeResponse(sess, handshakeResponse{
|
||||
OK: false,
|
||||
Message: "domain and client_api_key are required",
|
||||
Domain: req.Domain,
|
||||
})
|
||||
return nil, fmt.Errorf("invalid handshake parameters")
|
||||
}
|
||||
|
||||
if err := validator.ValidateDomainAPIKey(ctx, req.Domain, req.ClientAPIKey); err != nil {
|
||||
log.Warn("domain/api_key validation failed", logging.Fields{
|
||||
"domain": req.Domain,
|
||||
"error": err.Error(),
|
||||
})
|
||||
_ = writeHandshakeResponse(sess, handshakeResponse{
|
||||
OK: false,
|
||||
Message: "invalid domain or api key",
|
||||
Domain: req.Domain,
|
||||
})
|
||||
return nil, fmt.Errorf("handshake validation failed: %w", err)
|
||||
}
|
||||
|
||||
// 검증 성공
|
||||
log.Info("dtls handshake success", logging.Fields{
|
||||
"domain": req.Domain,
|
||||
})
|
||||
|
||||
if err := writeHandshakeResponse(sess, handshakeResponse{
|
||||
OK: true,
|
||||
Message: "handshake ok",
|
||||
Domain: req.Domain,
|
||||
}); err != nil {
|
||||
log.Error("failed to write handshake response", logging.Fields{
|
||||
"domain": req.Domain,
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("write handshake response: %w", err)
|
||||
}
|
||||
|
||||
return &ServerHandshakeResult{
|
||||
Domain: req.Domain,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PerformClientHandshake 는 클라이언트 측에서 DTLS 세션이 생성된 직후 호출되어
|
||||
// 서버로 (domain, client_api_key)를 전송하고 결과를 검증합니다.
|
||||
//
|
||||
// localTarget 은 "로컬에서 요청할 서버 주소" (예: 127.0.0.1:8080) 로,
|
||||
// 핸드셰이크 성공 시 로그에 함께 출력됩니다.
|
||||
func PerformClientHandshake(
|
||||
ctx context.Context,
|
||||
sess Session,
|
||||
logger logging.Logger,
|
||||
domain string,
|
||||
clientAPIKey string,
|
||||
localTarget string,
|
||||
) (*ClientHandshakeResult, error) {
|
||||
log := logger.With(logging.Fields{"phase": "dtls_handshake", "side": "client"})
|
||||
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req := handshakeRequest{
|
||||
Domain: stringTrimSpace(domain),
|
||||
ClientAPIKey: stringTrimSpace(clientAPIKey),
|
||||
}
|
||||
|
||||
if req.Domain == "" || req.ClientAPIKey == "" {
|
||||
return nil, fmt.Errorf("domain and client_api_key are required")
|
||||
}
|
||||
|
||||
if err := writeHandshakeRequest(sess, req); err != nil {
|
||||
log.Error("failed to write handshake request", logging.Fields{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("write handshake request: %w", err)
|
||||
}
|
||||
|
||||
var resp handshakeResponse
|
||||
if err := json.NewDecoder(sess).Decode(&resp); err != nil {
|
||||
log.Error("failed to read handshake response", logging.Fields{
|
||||
"error": err.Error(),
|
||||
})
|
||||
return nil, fmt.Errorf("read handshake response: %w", err)
|
||||
}
|
||||
|
||||
if !resp.OK {
|
||||
log.Error("dtls handshake failed", logging.Fields{
|
||||
"domain": req.Domain,
|
||||
"message": resp.Message,
|
||||
})
|
||||
return nil, fmt.Errorf("handshake failed: %s", resp.Message)
|
||||
}
|
||||
|
||||
// 성공 로그: 연결 성공 메시지 + 도메인 + 로컬에서 요청할 서버 주소
|
||||
log.Info("dtls handshake success", logging.Fields{
|
||||
"domain": resp.Domain,
|
||||
"message": resp.Message,
|
||||
"local_target": localTarget,
|
||||
})
|
||||
|
||||
return &ClientHandshakeResult{
|
||||
Domain: resp.Domain,
|
||||
Message: resp.Message,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// writeHandshakeRequest 는 JSON 인코더를 사용해 handshakeRequest 를 세션으로 전송합니다.
|
||||
func writeHandshakeRequest(sess Session, req handshakeRequest) error {
|
||||
enc := json.NewEncoder(sess)
|
||||
return enc.Encode(&req)
|
||||
}
|
||||
|
||||
// writeHandshakeResponse 는 JSON 인코더를 사용해 handshakeResponse 를 세션으로 전송합니다.
|
||||
func writeHandshakeResponse(sess Session, resp handshakeResponse) error {
|
||||
enc := json.NewEncoder(sess)
|
||||
return enc.Encode(&resp)
|
||||
}
|
||||
|
||||
func stringTrimSpace(s string) string {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
69
internal/dtls/selfsigned.go
Normal file
69
internal/dtls/selfsigned.go
Normal file
@@ -0,0 +1,69 @@
|
||||
package dtls
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"math/big"
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NewSelfSignedLocalhostConfig 는 테스트용 self-signed TLS 설정을 생성합니다.
|
||||
//
|
||||
// - CN: "localhost"
|
||||
// - DNS SAN: ["localhost"]
|
||||
// - IP SAN: [127.0.0.1]
|
||||
// - 유효기간: 생성 시점 기준 1년
|
||||
//
|
||||
// DTLS, 일반 TLS 서버 모두에서 사용할 수 있으며,
|
||||
// 서버 측에서는 Certificates 에 이 인증서를 넣어주고,
|
||||
// 클라이언트 측에서는 debug 모드에서 InsecureSkipVerify 를 true 로 두어
|
||||
// 체인 검증을 스킵하는 방식으로 사용할 수 있습니다.
|
||||
func NewSelfSignedLocalhostConfig() (*tls.Config, error) {
|
||||
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
serial, err := rand.Int(rand.Reader, big.NewInt(1<<62))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
notBefore := time.Now().Add(-1 * time.Hour)
|
||||
notAfter := notBefore.Add(365 * 24 * time.Hour)
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: serial,
|
||||
Subject: pkix.Name{
|
||||
CommonName: "localhost",
|
||||
},
|
||||
NotBefore: notBefore,
|
||||
NotAfter: notAfter,
|
||||
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
BasicConstraintsValid: true,
|
||||
|
||||
DNSNames: []string{"localhost"},
|
||||
IPAddresses: []net.IP{net.ParseIP("127.0.0.1")},
|
||||
}
|
||||
|
||||
derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsCert := tls.Certificate{
|
||||
Certificate: [][]byte{derBytes},
|
||||
PrivateKey: priv,
|
||||
}
|
||||
|
||||
return &tls.Config{
|
||||
Certificates: []tls.Certificate{tlsCert},
|
||||
MinVersion: tls.VersionTLS12,
|
||||
}, nil
|
||||
}
|
||||
194
internal/dtls/transport_pion.go
Normal file
194
internal/dtls/transport_pion.go
Normal file
@@ -0,0 +1,194 @@
|
||||
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 서버 리스너 구성을 정의합니다.
|
||||
type PionServerConfig struct {
|
||||
// Addr 는 "0.0.0.0:443" 와 같은 UDP 리스닝 주소입니다.
|
||||
Addr string
|
||||
|
||||
// TLSConfig 는 ACME 등을 통해 준비된 tls.Config 입니다.
|
||||
// Certificates, RootCAs, ClientAuth 등의 설정이 여기서 넘어옵니다.
|
||||
// nil 인 경우 기본 빈 tls.Config 가 사용됩니다.
|
||||
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)
|
||||
}
|
||||
|
||||
dtlsCfg := &piondtls.Config{
|
||||
Certificates: cfg.TLSConfig.Certificates,
|
||||
InsecureSkipVerify: cfg.TLSConfig.InsecureSkipVerify,
|
||||
// 필요 시 RootCAs, ClientAuth, 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 클라이언트 구성을 정의합니다.
|
||||
type PionClientConfig struct {
|
||||
// Addr 는 서버의 UDP 주소 (예: "example.com:443") 입니다.
|
||||
Addr string
|
||||
|
||||
// TLSConfig 는 서버 인증에 사용할 tls.Config 입니다.
|
||||
// InsecureSkipVerify=true 로 두면 서버 인증을 건너뛰므로 개발/테스트에만 사용해야 합니다.
|
||||
TLSConfig *tls.Config
|
||||
|
||||
// Timeout 은 DTLS 핸드셰이크 타임아웃입니다.
|
||||
// 0 이면 기본값 10초가 사용됩니다.
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
// 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{
|
||||
Certificates: c.tlsConfig.Certificates,
|
||||
InsecureSkipVerify: c.tlsConfig.InsecureSkipVerify,
|
||||
// 필요 시 ServerName, RootCAs 등 추가 설정
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
33
internal/dtls/validator_dummy.go
Normal file
33
internal/dtls/validator_dummy.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package dtls
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/dalbodeule/hop-gate/internal/logging"
|
||||
)
|
||||
|
||||
// DomainValidator 는 handshake.go 에 정의된 인터페이스를 재노출합니다.
|
||||
// (동일 패키지이므로 별도 선언 없이 사용하지만, 여기에 더미 구현을 둡니다.)
|
||||
|
||||
// DummyDomainValidator 는 임시 개발용으로 모든 (domain, api_key) 조합을 허용하는 Validator 입니다.
|
||||
// 실제 운영 환경에서는 ent + PostgreSQL 기반의 구현으로 교체해야 합니다.
|
||||
type DummyDomainValidator struct {
|
||||
Logger logging.Logger
|
||||
}
|
||||
|
||||
func (d DummyDomainValidator) ValidateDomainAPIKey(ctx context.Context, domain, clientAPIKey string) error {
|
||||
if d.Logger != nil {
|
||||
d.Logger.Debug("dummy domain validator used (ALWAYS ALLOW)", logging.Fields{
|
||||
"domain": domain,
|
||||
"client_api_key_masked": maskKey(clientAPIKey),
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func maskKey(key string) string {
|
||||
if len(key) <= 8 {
|
||||
return "***"
|
||||
}
|
||||
return key[:4] + "..." + key[len(key)-4:]
|
||||
}
|
||||
Reference in New Issue
Block a user