mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2025-12-08 04:45:43 +09:00
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).
197 lines
5.9 KiB
Go
197 lines
5.9 KiB
Go
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)
|
|
}
|