From 763daf5a566aa33cd11b226ce1e9560061bd40d1 Mon Sep 17 00:00:00 2001 From: dalbodeule <11470513+dalbodeule@users.noreply.github.com> Date: Tue, 2 Dec 2025 23:40:07 +0900 Subject: [PATCH] [feat](server): implement ent-based domain validation for handshake - Added `entDomainValidator` implementation to validate `(domain, client_api_key)` combinations from the `Domain` table using `ent.Client`. - Replaced dummy validator with the new ent-based validator in server initialization. - Updated documentation and progress tracking for domain validation implementation. - Ensured compatibility with `host` and `host:port` formats by normalizing domain strings during validation. --- cmd/server/main.go | 9 ++- internal/admin/domain_validator.go | 103 +++++++++++++++++++++++++++++ progress.md | 35 ++++++---- 3 files changed, 128 insertions(+), 19 deletions(-) create mode 100644 internal/admin/domain_validator.go diff --git a/cmd/server/main.go b/cmd/server/main.go index 8b73011..0358446 100644 --- a/cmd/server/main.go +++ b/cmd/server/main.go @@ -721,16 +721,15 @@ func main() { } }() - // 6. 도메인 검증기 준비 (현재는 Dummy 구현, 추후 ent + PostgreSQL 기반으로 교체 예정) - baseValidator := dtls.DummyDomainValidator{ - Logger: logger, - } + // 6. 도메인 검증기 준비 (ent + PostgreSQL 기반 실제 구현) + // Admin Plane 에서 관리하는 Domain 테이블을 사용해 (domain, client_api_key) 조합을 검증합니다. + domainValidator := admin.NewEntDomainValidator(logger, dbClient) // DTLS 핸드셰이크 단계에서 HOP_SERVER_DOMAIN 으로 설정된 도메인만 허용하도록 래핑합니다. allowedDomain = strings.ToLower(strings.TrimSpace(cfg.Domain)) var validator dtls.DomainValidator = &domainGateValidator{ allowed: allowedDomain, - inner: baseValidator, + inner: domainValidator, logger: logger, } diff --git a/internal/admin/domain_validator.go b/internal/admin/domain_validator.go new file mode 100644 index 0000000..3de4b6a --- /dev/null +++ b/internal/admin/domain_validator.go @@ -0,0 +1,103 @@ +package admin + +import ( + "context" + "fmt" + "net" + "strings" + + "github.com/dalbodeule/hop-gate/ent" + entdomain "github.com/dalbodeule/hop-gate/ent/domain" + "github.com/dalbodeule/hop-gate/internal/dtls" + "github.com/dalbodeule/hop-gate/internal/logging" +) + +// entDomainValidator 는 ent.Client 를 사용해 Domain 테이블에서 +// (domain, client_api_key) 조합을 검증하는 DomainValidator 구현체입니다. +type entDomainValidator struct { + logger logging.Logger + client *ent.Client +} + +// NewEntDomainValidator 는 ent 기반 DomainValidator 를 생성합니다. +// - domain 파라미터는 "host" 또는 "host:port" 형태 모두 허용하며, +// DB 조회 시에는 host 부분만 사용합니다. +func NewEntDomainValidator(logger logging.Logger, client *ent.Client) dtls.DomainValidator { + return &entDomainValidator{ + logger: logger.With(logging.Fields{"component": "domain_validator"}), + client: client, + } +} + +// canonicalDomainForLookup 는 handshake 에서 전달된 domain 문자열을 +// DB 조회용 정규 도메인으로 변환합니다. +// - "host:port" 형태인 경우 port 를 제거하고 host 만 사용합니다. +// - 공백 제거 및 소문자 변환 후, normalizeDomain 을 통해 기본 형식을 검증합니다. +func canonicalDomainForLookup(raw string) string { + d := strings.TrimSpace(raw) + if d == "" { + return "" + } + + // host:port 형태를 우선적으로 처리합니다. + if h, _, err := net.SplitHostPort(d); err == nil && strings.TrimSpace(h) != "" { + d = h + } else { + // net.SplitHostPort 가 실패했지만 콜론이 포함되어 있는 경우 (예: 잘못된 포맷), + // IPv6 를 고려하지 않는 단순 환경에서는 마지막 콜론 기준으로 host 부분만 시도해볼 수 있습니다. + if idx := strings.LastIndex(d, ":"); idx > 0 && !strings.Contains(d, "]") { + if h := strings.TrimSpace(d[:idx]); h != "" { + d = h + } + } + } + + // admin/service.go 에 정의된 normalizeDomain 과 동일한 규칙을 적용합니다. + return normalizeDomain(d) +} + +// ValidateDomainAPIKey 는 (domain, client_api_key) 조합을 DB 에서 검증합니다. +func (v *entDomainValidator) ValidateDomainAPIKey(ctx context.Context, domain, clientAPIKey string) error { + if v.client == nil { + return fmt.Errorf("domain validator: ent client is nil") + } + + d := canonicalDomainForLookup(domain) + key := strings.TrimSpace(clientAPIKey) + + if d == "" || key == "" { + return fmt.Errorf("domain validator: invalid domain or client_api_key") + } + + if ctx == nil { + ctx = context.Background() + } + + log := v.logger.With(logging.Fields{ + "domain": d, + "client_api_key_masked": maskKey(key), + }) + + // Domain 테이블에서 정확히 일치하는 (domain, client_api_key) 를 조회합니다. + exists, err := v.client.Domain. + Query(). + Where( + entdomain.DomainEQ(d), + entdomain.ClientAPIKeyEQ(key), + ). + Exist(ctx) + if err != nil { + log.Error("failed to query domain/client_api_key from db", logging.Fields{ + "error": err.Error(), + }) + return fmt.Errorf("domain validator: db query failed: %w", err) + } + + if !exists { + log.Warn("no matching domain/client_api_key found", nil) + return fmt.Errorf("domain validator: domain/api_key not found") + } + + log.Debug("domain/api_key validation succeeded", nil) + return nil +} diff --git a/progress.md b/progress.md index 6f35bf6..8f18c94 100644 --- a/progress.md +++ b/progress.md @@ -92,11 +92,17 @@ This document tracks implementation progress against the HopGate architecture an - `DomainValidator` 인터페이스. - `PerformServerHandshake` / `PerformClientHandshake` 구현 완료. -- self-signed TLS: [`internal/dtls/selfsigned.go`](internal/dtls/selfsigned.go) - - localhost CN, SAN(DNS/IP) 포함 self-signed cert 생성. +- self-signed TLS: [`internal/dtls/selfsigned.go`](internal/dtls/selfsigned.go) + - localhost CN, SAN(DNS/IP) 포함 self-signed cert 생성. -- Dummy Validator: [`internal/dtls/validator_dummy.go`](internal/dtls/validator_dummy.go) - - 현재 모든 도메인/API Key 조합을 허용하며, 마스킹된 키와 함께 디버그 로그 출력. +- Domain Validator: + - 인터페이스 정의: [`internal/dtls/handshake.go`](internal/dtls/handshake.go) + - `ValidateDomainAPIKey(ctx, domain, clientAPIKey string) error`. + - 실제 구현: [`internal/admin/domain_validator.go`](internal/admin/domain_validator.go) + - ent.Client + PostgreSQL 기반으로 `Domain` 테이블 조회. + - 도메인 문자열은 `"host"` 또는 `"host:port"` 모두 허용하되, DB 조회 시에는 host 부분만 사용. + - `(domain, client_api_key)` 조합이 정확히 일치하는지 검증. + - 기존 Dummy 구현: [`internal/dtls/validator_dummy.go`](internal/dtls/validator_dummy.go) 는 이제 개발/테스트용 참고 구현으로만 유지. --- @@ -202,13 +208,14 @@ This document tracks implementation progress against the HopGate architecture an ### 3.2 DomainValidator Implementation / DomainValidator 구현 -- [ ] `DomainValidator` 의 실제 구현 추가 (예: `internal/admin/domain_validator.go`). - - ent.Client 를 사용해 `Domain` 테이블 조회. - - `(domain, client_api_key)` 조합 검증. - - DummyDomainValidator 를 실제 구현으로 교체. +- [x] `DomainValidator` 의 실제 구현 추가 (예: `internal/admin/domain_validator.go`). + - ent.Client 를 사용해 `Domain` 테이블 조회. + - `(domain, client_api_key)` 조합 검증. + - DummyDomainValidator 를 실제 구현으로 교체. -- [ ] DTLS Handshake 와 Admin Plane 통합 - - Domain 등록/해제가 handshake 검증 로직에 반영되도록 설계. +- [x] DTLS Handshake 와 Admin Plane 통합 + - Admin Plane 에서 관리하는 Domain 테이블을 사용해, 핸드셰이크 시 `(domain, client_api_key)` 조합을 DB 기준으로 검증. + - 도메인 문자열은 `"host"` 또는 `"host:port"` 형태 모두 허용하되, DB 조회용 canonical 도메인에서는 host 부분만 사용. --- @@ -284,10 +291,10 @@ This document tracks implementation progress against the HopGate architecture an ### Milestone 1 — DTLS Handshake + Admin + DB (기본 인증 토대) -- [x] DTLS transport & handshake skeleton 구현 (server/client). -- [x] Domain ent schema + PostgreSQL 연결 & schema init. -- [ ] DomainService 실제 구현 + DomainValidator 구현. -- [ ] Admin API + ent + PostgreSQL 연결 (실제 도메인 등록/해제 동작). +- [x] DTLS transport & handshake skeleton 구현 (server/client). +- [x] Domain ent schema + PostgreSQL 연결 & schema init. +- [x] DomainService 실제 구현 + DomainValidator 구현. +- [ ] Admin API + ent + PostgreSQL 연결 (실제 도메인 등록/해제 동작). ### Milestone 2 — Full HTTP Tunneling (프락시 동작 완성)