Files
hop-gate/internal/config/config.go
dalbodeule 2121b56511 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).
2025-11-26 17:04:45 +09:00

266 lines
7.6 KiB
Go

package config
import (
"bufio"
"errors"
"os"
"strconv"
"strings"
"sync"
)
// LoggingConfig 는 공통 로그 설정을 담습니다.
// Loki push 에 필요한 엔드포인트/인증/정적 라벨 등을 포함합니다.
type LoggingConfig struct {
Level string // 예: "debug", "info", "warn", "error"
Loki LokiConfig // Loki 관련 설정
}
// LokiConfig 는 Loki HTTP push 설정을 담습니다.
type LokiConfig struct {
Enable bool // true 인 경우 Loki 로도 push
Endpoint string // 예: "http://loki:3100/loki/api/v1/push"
TenantID string // multi-tenant Loki 사용 시 X-Scope-OrgID 등에 사용
Username string // basic auth 사용자명(선택)
Password string // basic auth 비밀번호(선택)
StaticLabels map[string]string // 모든 로그에 공통으로 붙일 라벨 (app=hop-gate,env=dev 등)
}
// ServerConfig 는 서버 프로세스 설정을 담습니다.
type ServerConfig struct {
HTTPListen string // 예: ":80"
HTTPSListen string // 예: ":443"
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)
Domain string // 서버에서 등록된 도메인 (예: api.example.com)
ClientAPIKey string // 도메인에 매핑된 64자 클라이언트 API Key
LocalTarget string // 로컬에서 요청할 서버 주소 (예: 127.0.0.1:8080)
Debug bool // true 이면 디버그 모드 (예: 서버 인증서 검증 스킵 등)
Logging LoggingConfig // 클라이언트용 로그 설정
}
var (
dotenvOnce sync.Once
dotenvErr error
)
// loadDotEnvOnce 는 현재 작업 디렉터리의 .env 파일을 한 번만 읽어서 os.Environ 에 주입합니다.
// - KEY=VALUE, export KEY=VALUE 형식을 지원
// - # 으로 시작하는 줄은 주석으로 간주합니다.
func loadDotEnvOnce() {
dotenvOnce.Do(func() {
fi, err := os.Stat(".env")
if err != nil {
if errors.Is(err, os.ErrNotExist) {
// .env 가 없으면 조용히 무시
return
}
dotenvErr = err
return
}
if fi.IsDir() {
// 디렉터리이면 무시
return
}
f, err := os.Open(".env")
if err != nil {
dotenvErr = err
return
}
defer f.Close()
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if line == "" || strings.HasPrefix(line, "#") {
continue
}
if strings.HasPrefix(line, "export ") {
line = strings.TrimSpace(strings.TrimPrefix(line, "export "))
}
parts := strings.SplitN(line, "=", 2)
if len(parts) != 2 {
continue
}
key := strings.TrimSpace(parts[0])
val := strings.TrimSpace(parts[1])
// 양 끝의 작은/큰따옴표 제거
val = strings.Trim(val, `"'`)
if key != "" {
_ = os.Setenv(key, val)
}
}
if err := scanner.Err(); err != nil {
dotenvErr = err
return
}
})
}
func getEnvOrDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func getEnvBool(key string, def bool) bool {
v := strings.ToLower(strings.TrimSpace(os.Getenv(key)))
if v == "" {
return def
}
switch v {
case "1", "true", "yes", "y", "on":
return true
case "0", "false", "no", "n", "off":
return false
default:
return def
}
}
func parseCSVEnv(key string) []string {
raw := os.Getenv(key)
if raw == "" {
return nil
}
parts := strings.Split(raw, ",")
out := make([]string, 0, len(parts))
for _, p := range parts {
p = strings.TrimSpace(p)
if p != "" {
out = append(out, p)
}
}
return out
}
// parseKeyValueCSV 는 "k1=v1,k2=v2" 형태의 문자열을 map 으로 변환합니다.
func parseKeyValueCSV(raw string) map[string]string {
if raw == "" {
return nil
}
m := make(map[string]string)
for _, part := range strings.Split(raw, ",") {
part = strings.TrimSpace(part)
if part == "" {
continue
}
kv := strings.SplitN(part, "=", 2)
if len(kv) != 2 {
continue
}
k := strings.TrimSpace(kv[0])
v := strings.TrimSpace(kv[1])
if k != "" {
m[k] = v
}
}
return m
}
// parseServicePortsEnv 는 "name1=127.0.0.1:8080,name2=127.0.0.1:9000" 형식을 파싱합니다.
func parseServicePortsEnv(key string) map[string]string {
raw := os.Getenv(key)
return parseKeyValueCSV(raw)
}
// loadLoggingFromEnv 는 공통 로그 설정을 .env/환경변수에서 읽어옵니다.
func loadLoggingFromEnv() LoggingConfig {
level := getEnvOrDefault("HOP_LOG_LEVEL", "info")
lokiEnable := getEnvBool("HOP_LOKI_ENABLE", false)
lokiEndpoint := os.Getenv("HOP_LOKI_ENDPOINT")
lokiTenantID := os.Getenv("HOP_LOKI_TENANT_ID")
lokiUsername := os.Getenv("HOP_LOKI_USERNAME")
lokiPassword := os.Getenv("HOP_LOKI_PASSWORD")
lokiStaticLabels := parseKeyValueCSV(os.Getenv("HOP_LOKI_STATIC_LABELS"))
return LoggingConfig{
Level: level,
Loki: LokiConfig{
Enable: lokiEnable,
Endpoint: lokiEndpoint,
TenantID: lokiTenantID,
Username: lokiUsername,
Password: lokiPassword,
StaticLabels: lokiStaticLabels,
},
}
}
// LoadServerConfigFromEnv 는 .env 를 우선 읽고, 이후 환경 변수를 기반으로 서버 설정을 구성합니다.
func LoadServerConfigFromEnv() (*ServerConfig, error) {
loadDotEnvOnce()
if dotenvErr != nil {
return nil, dotenvErr
}
cfg := &ServerConfig{
HTTPListen: getEnvOrDefault("HOP_SERVER_HTTP_LISTEN", ":80"),
HTTPSListen: getEnvOrDefault("HOP_SERVER_HTTPS_LISTEN", ":443"),
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 {
return nil, dotenvErr
}
cfg := &ClientConfig{
ServerAddr: os.Getenv("HOP_CLIENT_SERVER_ADDR"),
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
}
// Optional: 숫자 포트만 지정하고 싶을 경우를 위한 헬퍼 (예: "80" -> ":80").
// 현재는 사용하지 않지만, 향후 유효성 검사/정규화에 사용할 수 있습니다.
func normalizePort(p string, def string) string {
p = strings.TrimSpace(p)
if p == "" {
return def
}
if strings.HasPrefix(p, ":") {
return p
}
// 숫자로만 구성된 경우 ":" prefix 를 붙입니다.
if _, err := strconv.Atoi(p); err == nil {
return ":" + p
}
return p
}