mirror of
https://github.com/dalbodeule/hop-gate.git
synced 2025-12-07 20:35:44 +09:00
- Introduced a new `legoManager` for managing per-domain TLS certificates. - Implemented ACME HTTP-01 challenge handling with a configurable webroot directory. - Created `NewLegoManagerFromEnv` to initialize ACME settings from environment variables. - Added `verifyDomainsResolve` to validate domain DNS resolutions. - Updated `.gitignore` to include ACME cache and webroot directories. - Updated `go.mod` and `go.sum` with new dependencies, including `go-acme/lego`.
219 lines
6.3 KiB
Go
219 lines
6.3 KiB
Go
package dtls
|
|
|
|
import (
|
|
"context"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"net"
|
|
"time"
|
|
|
|
piondtls "github.com/pion/dtls/v3"
|
|
)
|
|
|
|
// pionSession 은 pion/dtls.Conn 을 감싸 Session 인터페이스를 구현합니다.
|
|
type pionSession struct {
|
|
conn *piondtls.Conn
|
|
id string
|
|
}
|
|
|
|
func (s *pionSession) Read(b []byte) (int, error) { return s.conn.Read(b) }
|
|
func (s *pionSession) Write(b []byte) (int, error) { return s.conn.Write(b) }
|
|
func (s *pionSession) Close() error { return s.conn.Close() }
|
|
func (s *pionSession) ID() string { return s.id }
|
|
|
|
// pionServer 는 pion/dtls 기반 Server 구현입니다.
|
|
type pionServer struct {
|
|
listener net.Listener
|
|
}
|
|
|
|
// PionServerConfig 는 DTLS 서버 리스너 구성을 정의합니다.
|
|
type PionServerConfig struct {
|
|
// Addr 는 "0.0.0.0:443" 와 같은 UDP 리스닝 주소입니다.
|
|
Addr string
|
|
|
|
// TLSConfig 는 ACME 등을 통해 준비된 tls.Config 입니다.
|
|
// Certificates, RootCAs, ClientAuth 등의 설정이 여기서 넘어옵니다.
|
|
// nil 인 경우 기본 빈 tls.Config 가 사용됩니다.
|
|
TLSConfig *tls.Config
|
|
}
|
|
|
|
// NewPionServer 는 pion/dtls 기반 DTLS 서버를 생성합니다.
|
|
// 내부적으로 udp 리스너를 열고, DTLS 핸드셰이크를 수행할 준비를 합니다.
|
|
func NewPionServer(cfg PionServerConfig) (Server, error) {
|
|
if cfg.Addr == "" {
|
|
return nil, fmt.Errorf("PionServerConfig.Addr is required")
|
|
}
|
|
if cfg.TLSConfig == nil {
|
|
cfg.TLSConfig = &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
}
|
|
|
|
udpAddr, err := net.ResolveUDPAddr("udp", cfg.Addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve udp addr: %w", err)
|
|
}
|
|
|
|
// tls.Config.GetCertificate (crypto/tls) → pion/dtls.GetCertificate 어댑터
|
|
var getCert func(*piondtls.ClientHelloInfo) (*tls.Certificate, error)
|
|
if cfg.TLSConfig.GetCertificate != nil {
|
|
tlsGetCert := cfg.TLSConfig.GetCertificate
|
|
getCert = func(chi *piondtls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
if chi == nil {
|
|
return tlsGetCert(&tls.ClientHelloInfo{})
|
|
}
|
|
// ACME 매니저는 주로 SNI(ServerName)에 기반해 인증서를 선택하므로,
|
|
// 필요한 최소 필드만 복사해서 전달한다.
|
|
return tlsGetCert(&tls.ClientHelloInfo{
|
|
ServerName: chi.ServerName,
|
|
})
|
|
}
|
|
}
|
|
|
|
dtlsCfg := &piondtls.Config{
|
|
// 서버가 사용할 인증서 설정: 정적 Certificates + GetCertificate 어댑터
|
|
Certificates: cfg.TLSConfig.Certificates,
|
|
GetCertificate: getCert,
|
|
InsecureSkipVerify: cfg.TLSConfig.InsecureSkipVerify,
|
|
ClientAuth: piondtls.ClientAuthType(cfg.TLSConfig.ClientAuth),
|
|
ClientCAs: cfg.TLSConfig.ClientCAs,
|
|
RootCAs: cfg.TLSConfig.RootCAs,
|
|
ServerName: cfg.TLSConfig.ServerName,
|
|
// 필요 시 ExtendedMasterSecret 등을 추가 설정
|
|
}
|
|
l, err := piondtls.Listen("udp", udpAddr, dtlsCfg)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("dtls listen: %w", err)
|
|
}
|
|
|
|
return &pionServer{
|
|
listener: l,
|
|
}, nil
|
|
}
|
|
|
|
// Accept 는 새로운 DTLS 연결을 수락하고, Session 으로 래핑합니다.
|
|
func (s *pionServer) Accept() (Session, error) {
|
|
conn, err := s.listener.Accept()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
dtlsConn, ok := conn.(*piondtls.Conn)
|
|
if !ok {
|
|
_ = conn.Close()
|
|
return nil, fmt.Errorf("accepted connection is not *dtls.Conn")
|
|
}
|
|
|
|
id := ""
|
|
if ra := dtlsConn.RemoteAddr(); ra != nil {
|
|
id = ra.String()
|
|
}
|
|
|
|
return &pionSession{
|
|
conn: dtlsConn,
|
|
id: id,
|
|
}, nil
|
|
}
|
|
|
|
// Close 는 DTLS 리스너를 종료합니다.
|
|
func (s *pionServer) Close() error {
|
|
return s.listener.Close()
|
|
}
|
|
|
|
// pionClient 는 pion/dtls 기반 Client 구현입니다.
|
|
type pionClient struct {
|
|
addr string
|
|
tlsConfig *tls.Config
|
|
timeout time.Duration
|
|
}
|
|
|
|
// PionClientConfig 는 DTLS 클라이언트 구성을 정의합니다.
|
|
type PionClientConfig struct {
|
|
// Addr 는 서버의 UDP 주소 (예: "example.com:443") 입니다.
|
|
Addr string
|
|
|
|
// TLSConfig 는 서버 인증에 사용할 tls.Config 입니다.
|
|
// InsecureSkipVerify=true 로 두면 서버 인증을 건너뛰므로 개발/테스트에만 사용해야 합니다.
|
|
TLSConfig *tls.Config
|
|
|
|
// Timeout 은 DTLS 핸드셰이크 타임아웃입니다.
|
|
// 0 이면 기본값 10초가 사용됩니다.
|
|
Timeout time.Duration
|
|
}
|
|
|
|
// NewPionClient 는 pion/dtls 기반 DTLS 클라이언트를 생성합니다.
|
|
func NewPionClient(cfg PionClientConfig) Client {
|
|
if cfg.Timeout == 0 {
|
|
cfg.Timeout = 10 * time.Second
|
|
}
|
|
if cfg.TLSConfig == nil {
|
|
// 기본값: 인증서 검증을 수행하는 안전한 설정(루트 CA 체인은 시스템 기본값 사용).
|
|
// 디버그 모드에서 인증서 검증을 스킵하고 싶다면, 호출 측에서
|
|
// TLSConfig: &tls.Config{InsecureSkipVerify: true} 를 명시적으로 전달해야 합니다.
|
|
cfg.TLSConfig = &tls.Config{
|
|
MinVersion: tls.VersionTLS12,
|
|
}
|
|
}
|
|
return &pionClient{
|
|
addr: cfg.Addr,
|
|
tlsConfig: cfg.TLSConfig,
|
|
timeout: cfg.Timeout,
|
|
}
|
|
}
|
|
|
|
// Connect 는 서버와 DTLS 핸드셰이크를 수행하고 Session 을 반환합니다.
|
|
func (c *pionClient) Connect() (Session, error) {
|
|
if c.addr == "" {
|
|
return nil, fmt.Errorf("PionClientConfig.Addr is required")
|
|
}
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), c.timeout)
|
|
defer cancel()
|
|
|
|
raddr, err := net.ResolveUDPAddr("udp", c.addr)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("resolve udp addr: %w", err)
|
|
}
|
|
|
|
dtlsCfg := &piondtls.Config{
|
|
// 클라이언트는 서버 인증을 위해 RootCAs/ServerName 만 사용.
|
|
// (현재는 클라이언트 인증서 사용 계획이 없으므로 GetCertificate 는 전달하지 않는다.)
|
|
Certificates: c.tlsConfig.Certificates,
|
|
InsecureSkipVerify: c.tlsConfig.InsecureSkipVerify,
|
|
RootCAs: c.tlsConfig.RootCAs,
|
|
ServerName: c.tlsConfig.ServerName,
|
|
}
|
|
|
|
type result struct {
|
|
conn *piondtls.Conn
|
|
err error
|
|
}
|
|
ch := make(chan result, 1)
|
|
|
|
go func() {
|
|
conn, err := piondtls.Dial("udp", raddr, dtlsCfg)
|
|
ch <- result{conn: conn, err: err}
|
|
}()
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
return nil, fmt.Errorf("dtls dial timeout: %w", ctx.Err())
|
|
case res := <-ch:
|
|
if res.err != nil {
|
|
return nil, fmt.Errorf("dtls dial: %w", res.err)
|
|
}
|
|
id := ""
|
|
if ra := res.conn.RemoteAddr(); ra != nil {
|
|
id = ra.String()
|
|
}
|
|
return &pionSession{
|
|
conn: res.conn,
|
|
id: id,
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// Close 는 클라이언트 단에서 유지하는 리소스가 없으므로 no-op 입니다.
|
|
func (c *pionClient) Close() error {
|
|
return nil
|
|
}
|