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 }