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:
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)
|
||||
}
|
||||
Reference in New Issue
Block a user