mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2025-12-08 04:45:43 +09:00
@@ -5,6 +5,7 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"flag"
|
||||
"net"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
@@ -118,7 +119,13 @@ func main() {
|
||||
}
|
||||
// DTLS 서버 측은 SNI(ServerName)가 HOP_SERVER_DOMAIN(cfg.Domain)과 일치하는지 검사하므로,
|
||||
// 클라이언트 TLS 설정에도 반드시 도메인을 설정해준다.
|
||||
tlsCfg.ServerName = finalCfg.Domain
|
||||
//
|
||||
// finalCfg.ServerAddr 가 "host:port" 형태이므로, SNI 에는 DNS(host) 부분만 넣어야 한다.
|
||||
host := finalCfg.ServerAddr
|
||||
if h, _, err := net.SplitHostPort(finalCfg.ServerAddr); err == nil && strings.TrimSpace(h) != "" {
|
||||
host = h
|
||||
}
|
||||
tlsCfg.ServerName = host
|
||||
|
||||
client := dtls.NewPionClient(dtls.PionClientConfig{
|
||||
Addr: finalCfg.ServerAddr,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
|
||||
103
internal/admin/domain_validator.go
Normal file
103
internal/admin/domain_validator.go
Normal file
@@ -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 {
|
||||
// [nitpick] If the input is a bracketed IPv6 address without a port (e.g., "[::1]"),
|
||||
// net.SplitHostPort fails, and the fallback above won't execute due to the ']' check.
|
||||
// For robustness, strip brackets if present. Note: normalizeDomain requires a dot,
|
||||
// so IP addresses (including IPv6) will be rejected downstream.
|
||||
if strings.HasPrefix(d, "[") && strings.HasSuffix(d, "]") {
|
||||
d = d[1 : len(d)-1]
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
21
progress.md
21
progress.md
@@ -95,8 +95,14 @@ This document tracks implementation progress against the HopGate architecture an
|
||||
- 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`).
|
||||
- [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 부분만 사용.
|
||||
|
||||
---
|
||||
|
||||
@@ -286,8 +293,8 @@ This document tracks implementation progress against the HopGate architecture an
|
||||
|
||||
- [x] DTLS transport & handshake skeleton 구현 (server/client).
|
||||
- [x] Domain ent schema + PostgreSQL 연결 & schema init.
|
||||
- [ ] DomainService 실제 구현 + DomainValidator 구현.
|
||||
- [ ] Admin API + ent + PostgreSQL 연결 (실제 도메인 등록/해제 동작).
|
||||
- [x] DomainService 실제 구현 + DomainValidator 구현.
|
||||
- [x] Admin API + ent + PostgreSQL 연결 (실제 도메인 등록/해제 동작).
|
||||
|
||||
### Milestone 2 — Full HTTP Tunneling (프락시 동작 완성)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user