Compare commits

...

10 commits

Author SHA1 Message Date
413d0f24b6 test(webrtc): add Pion WHEP subscriber client + e2e test
Some checks failed
tests / build (push) Failing after 13s
CodeQL / Analyze (pull_request) Failing after 2s
tests / build (pull_request) Failing after 2s
whep-client/main.go: minimal Pion subscriber that POSTs a recvonly
offer, applies the answer, and waits for one RTP packet on each of
the video and audio tracks. Used as M1's end-to-end verifier.

whep-client/main_test.go: in-process e2e wiring — stands up Source,
Registry, PeerFactory and WHEPHandler behind an httptest server,
injects synthetic PT=102/111 RTP on the Source's UDP port and calls
Subscribe. Validates the full egress pipeline without requiring
FFmpeg or external network. Skipped under -short.
2026-04-17 08:52:40 -04:00
e471bd02b2 test(webrtc): add FFmpeg publisher script for M1 PoC
Generates a synthetic testsrc2 video + sine audio and pushes H.264/Opus
RTP to the webrtc-poc's UDP port, using the hard-coded payload types
(102 video, 111 audio) the M1 forwarder dispatches on. Intended to be
run alongside test/whep-client (M1 Task 11) for end-to-end verification.
2026-04-17 08:51:22 -04:00
c24c96d022 feat(webrtc): add standalone webrtc-poc binary for M1 testing
Minimal egress-only server that wires Source, Registry, PeerFactory and
WHEPHandler together on a single stream id. Listens for RTP on a local
UDP port (default 127.0.0.1:10000) and serves WHEP on :8787.

Not part of the Core binary — will be demoted to an internal test helper
once M2 integrates WebRTC output into the process-graph.
2026-04-17 08:50:31 -04:00
f6ddae23c9 feat(webrtc): add WHEP POST handler (happy path) 2026-04-17 08:48:06 -04:00
b2a691186c feat(webrtc): add PeerFactory, Peer, and RTP forwarder 2026-04-17 08:47:27 -04:00
917c353e03 feat(webrtc): add ICE config helper (Configuration + SettingEngine)
Vendors github.com/pion/webrtc/v4 v4.2.11 and its transitive
dependencies (datachannel, dtls/v3, ice/v4, interceptor, logging,
mdns/v2, sctp, sdp/v3, srtp/v3, stun/v3, transport/v4, turn/v4).
2026-04-17 08:46:27 -04:00
1fdc29ace1 feat(webrtc): add Source with UDP RTP reader and subscriber fan-out
Adds github.com/pion/rtp v1.10.1 as a direct dependency (vendored).
2026-04-17 08:45:48 -04:00
3a17e543c5 feat(webrtc): add thread-safe Registry for stream_id -> SourceHandle 2026-04-17 08:44:59 -04:00
2250cb0a8f feat(webrtc): add Config with defaults and validation 2026-04-17 08:44:30 -04:00
7ea1844869 feat(webrtc): add package skeleton and typed errors 2026-04-17 08:43:57 -04:00
749 changed files with 113887 additions and 0 deletions

9
.gitignore vendored
View file

@ -7,6 +7,15 @@
/test/**
.vscode
# Dragon Fork additions: source trees under /core/ and /test/ must be tracked.
!/core/
!/core/webrtc/
!/core/webrtc/**
!/test/
!/test/publish.sh
!/test/whep-client/
!/test/whep-client/**
*.ts
*.ts.tmp
*.m3u8

55
cmd/webrtc-poc/main.go Normal file
View file

@ -0,0 +1,55 @@
// Command webrtc-poc runs a minimal Dragon Fork WebRTC egress server for
// manual end-to-end testing. It listens for RTP on 127.0.0.1:10000 as
// stream "test" and serves WHEP at :8787.
//
// This is NOT part of the datarhei Core binary. It will be removed or
// demoted to an internal test helper once milestone M2 lands.
package main
import (
"flag"
"log"
"net/http"
"github.com/datarhei/core/v16/core/webrtc"
)
func main() {
var (
streamID = flag.String("stream", "test", "stream id to serve")
rtpPort = flag.Int("rtp-port", 10000, "UDP port to receive RTP on")
listen = flag.String("listen", ":8787", "WHEP HTTP listen address")
publicIP = flag.String("public-ip", "", "server public IP for NAT1To1 (optional)")
)
flag.Parse()
cfg := webrtc.DefaultConfig()
cfg.WHEPListen = *listen
cfg.PublicIP = *publicIP
src, err := webrtc.NewSource(*streamID, *rtpPort)
if err != nil {
log.Fatalf("NewSource: %v", err)
}
src.Start()
defer src.Close()
log.Printf("listening for RTP on %s", src.LocalAddr())
reg := webrtc.NewRegistry()
if err := reg.Register(*streamID, src); err != nil {
log.Fatalf("Register: %v", err)
}
factory, err := webrtc.NewPeerFactory(cfg)
if err != nil {
log.Fatalf("NewPeerFactory: %v", err)
}
handler := webrtc.NewWHEPHandler(reg, factory, cfg)
mux := http.NewServeMux()
mux.Handle("/whep/", handler)
log.Printf("WHEP listening on %s — POST /whep/%s to subscribe", *listen, *streamID)
log.Fatal(http.ListenAndServe(*listen, mux))
}

59
core/webrtc/config.go Normal file
View file

@ -0,0 +1,59 @@
package webrtc
import "fmt"
// PortRange represents an inclusive UDP port range.
type PortRange struct {
Low, High int
}
// Config controls the WebRTC egress module.
type Config struct {
// Enabled toggles the entire module. When false, no endpoints are served.
Enabled bool
// WHEPListen is the address the WHEP HTTP endpoint binds to (e.g. ":8787").
WHEPListen string
// PublicIP is the server's externally-reachable IP, advertised in ICE
// candidates via NAT1To1. Empty means rely on STUN discovery.
PublicIP string
// UDPPortRange bounds the local UDP ports allocated for FFmpeg→Pion RTP.
UDPPortRange PortRange
// ICEServers is the list of STUN/TURN URIs given to each PeerConnection.
ICEServers []string
// MaxPeersTotal is a hard safety cap on concurrent subscribers.
MaxPeersTotal int
}
// DefaultConfig returns production-reasonable defaults.
func DefaultConfig() Config {
return Config{
Enabled: true,
WHEPListen: ":8787",
PublicIP: "",
UDPPortRange: PortRange{Low: 10000, High: 10100},
ICEServers: []string{"stun:stun.cloudflare.com:3478", "stun:stun.l.google.com:19302"},
MaxPeersTotal: 32,
}
}
// Validate returns an error if the config is internally inconsistent.
func (c Config) Validate() error {
if c.WHEPListen == "" {
return fmt.Errorf("webrtc: WHEPListen must not be empty")
}
if c.UDPPortRange.Low <= 0 || c.UDPPortRange.High <= 0 {
return fmt.Errorf("webrtc: UDPPortRange must have positive bounds, got %v", c.UDPPortRange)
}
if c.UDPPortRange.Low > c.UDPPortRange.High {
return fmt.Errorf("webrtc: UDPPortRange.Low > High (%d > %d)", c.UDPPortRange.Low, c.UDPPortRange.High)
}
if c.MaxPeersTotal <= 0 {
return fmt.Errorf("webrtc: MaxPeersTotal must be positive, got %d", c.MaxPeersTotal)
}
return nil
}

View file

@ -0,0 +1,48 @@
package webrtc
import (
"testing"
)
func TestConfig_Defaults(t *testing.T) {
c := DefaultConfig()
if !c.Enabled {
t.Error("default Enabled should be true")
}
if c.WHEPListen != ":8787" {
t.Errorf("default WHEPListen = %q, want :8787", c.WHEPListen)
}
if c.UDPPortRange.Low != 10000 || c.UDPPortRange.High != 10100 {
t.Errorf("default UDPPortRange = %v, want 10000-10100", c.UDPPortRange)
}
if c.MaxPeersTotal != 32 {
t.Errorf("default MaxPeersTotal = %d, want 32", c.MaxPeersTotal)
}
if len(c.ICEServers) == 0 {
t.Error("default ICEServers should have at least one STUN entry")
}
}
func TestConfig_Validate(t *testing.T) {
tests := []struct {
name string
mutate func(*Config)
wantErr bool
}{
{"defaults are valid", func(c *Config) {}, false},
{"empty listen", func(c *Config) { c.WHEPListen = "" }, true},
{"inverted port range", func(c *Config) { c.UDPPortRange.Low = 20000; c.UDPPortRange.High = 10000 }, true},
{"zero max peers", func(c *Config) { c.MaxPeersTotal = 0 }, true},
{"negative max peers", func(c *Config) { c.MaxPeersTotal = -1 }, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := DefaultConfig()
tt.mutate(&c)
err := c.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() err = %v, wantErr %v", err, tt.wantErr)
}
})
}
}

11
core/webrtc/doc.go Normal file
View file

@ -0,0 +1,11 @@
// Package webrtc implements the Dragon Fork WebRTC egress module.
//
// It exposes a WHEP (WebRTC-HTTP Egress Protocol) HTTP endpoint and serves
// live RTP produced by an FFmpeg process on a local UDP socket to one or
// more WebRTC peer connections built with Pion.
//
// This package is additive: it does not modify existing datarhei ingest,
// transcode, or non-WebRTC output code paths. The only contact with
// existing code is a new URL scheme ("webrtc://") registered with the
// output resolver (done in milestone M2, not here).
package webrtc

26
core/webrtc/errors.go Normal file
View file

@ -0,0 +1,26 @@
package webrtc
import "errors"
// Sentinel errors returned by package functions.
var (
// ErrStreamNotFound indicates a WHEP subscribe referenced a stream_id
// that has no registered source. Maps to HTTP 404.
ErrStreamNotFound = errors.New("webrtc: stream not found")
// ErrPeerCapReached indicates max_peers_total has been exceeded.
// Maps to HTTP 503.
ErrPeerCapReached = errors.New("webrtc: peer capacity reached")
// ErrCodecMismatch indicates the viewer's SDP offer does not include
// a codec the source can serve (expected H.264 + Opus). Maps to HTTP 406.
ErrCodecMismatch = errors.New("webrtc: codec mismatch")
// ErrInvalidSDP indicates the request body was not a parseable SDP offer.
// Maps to HTTP 400.
ErrInvalidSDP = errors.New("webrtc: invalid SDP")
// ErrICETimeout indicates ICE gathering did not complete within the
// configured timeout. Maps to HTTP 500.
ErrICETimeout = errors.New("webrtc: ICE gathering timeout")
)

37
core/webrtc/forward.go Normal file
View file

@ -0,0 +1,37 @@
package webrtc
import (
"github.com/pion/rtp"
"github.com/pion/webrtc/v4"
)
// forwardRTP reads packets from sub and writes them to the correct track
// based on payload type (H.264 → video, Opus → audio). Payload-type
// inspection is the simplest M1 approach; M2 will switch to per-track
// source channels once the process resolver manages separate video/audio
// UDP ports.
func forwardRTP(done <-chan struct{}, sub <-chan *rtp.Packet,
video, audio *webrtc.TrackLocalStaticRTP) {
for {
select {
case <-done:
return
case pkt, ok := <-sub:
if !ok {
return
}
// Pion default H.264 PT = 102, Opus PT = 111. If the publisher
// uses different PTs we'll revisit in M2 — for M1 PoC we
// configure FFmpeg to these values explicitly in the publisher
// script.
switch pkt.PayloadType {
case 102:
_ = video.WriteRTP(pkt)
case 111:
_ = audio.WriteRTP(pkt)
default:
// Unknown PT — drop. Log in M3.
}
}
}
}

47
core/webrtc/ice.go Normal file
View file

@ -0,0 +1,47 @@
package webrtc
import (
"github.com/pion/webrtc/v4"
)
// BuildICEConfig translates a Config into the two Pion config pieces every
// PeerConnection needs: a webrtc.Configuration (with ICE servers) and a
// SettingEngine (with NAT1To1 and port range tuning).
//
// The returned *SettingEngine may be nil if no engine-level tuning is
// required (i.e. PublicIP unset and UDPPortRange at defaults). Callers
// should only pass it to webrtc.NewAPI when non-nil.
func BuildICEConfig(c Config) (webrtc.Configuration, *webrtc.SettingEngine, error) {
if err := c.Validate(); err != nil {
return webrtc.Configuration{}, nil, err
}
rtcConfig := webrtc.Configuration{
ICEServers: make([]webrtc.ICEServer, 0, len(c.ICEServers)),
}
for _, uri := range c.ICEServers {
rtcConfig.ICEServers = append(rtcConfig.ICEServers, webrtc.ICEServer{
URLs: []string{uri},
})
}
var se *webrtc.SettingEngine
if c.PublicIP != "" || c.UDPPortRange.Low > 0 {
engine := webrtc.SettingEngine{}
if c.PublicIP != "" {
engine.SetNAT1To1IPs([]string{c.PublicIP}, webrtc.ICECandidateTypeHost)
}
// Constrain the ephemeral UDP range Pion allocates for ICE candidates.
// Note: this is a separate concern from our FFmpeg→Source UDP ports;
// Pion uses its own port pool for the WebRTC media path.
if c.UDPPortRange.Low > 0 && c.UDPPortRange.High >= c.UDPPortRange.Low {
if err := engine.SetEphemeralUDPPortRange(
uint16(c.UDPPortRange.Low), uint16(c.UDPPortRange.High)); err != nil {
return webrtc.Configuration{}, nil, err
}
}
se = &engine
}
return rtcConfig, se, nil
}

50
core/webrtc/ice_test.go Normal file
View file

@ -0,0 +1,50 @@
package webrtc
import (
"testing"
"github.com/pion/webrtc/v4"
)
func TestBuildICEConfig_Defaults(t *testing.T) {
c := DefaultConfig()
rtcConfig, _, err := BuildICEConfig(c)
if err != nil {
t.Fatalf("BuildICEConfig: %v", err)
}
if len(rtcConfig.ICEServers) == 0 {
t.Error("ICEServers should not be empty")
}
// First default is Cloudflare STUN.
if rtcConfig.ICEServers[0].URLs[0] != "stun:stun.cloudflare.com:3478" {
t.Errorf("first ICE server = %q, want stun:stun.cloudflare.com:3478",
rtcConfig.ICEServers[0].URLs[0])
}
}
func TestBuildICEConfig_PublicIP(t *testing.T) {
c := DefaultConfig()
c.PublicIP = "203.0.113.10"
_, se, err := BuildICEConfig(c)
if err != nil {
t.Fatalf("BuildICEConfig: %v", err)
}
if se == nil {
t.Fatal("SettingEngine should not be nil when PublicIP is set")
}
// We can't introspect NAT1To1IPs directly from Pion's public API; the
// smoke test is that building an API from this engine works.
api := webrtc.NewAPI(webrtc.WithSettingEngine(*se))
if api == nil {
t.Fatal("NewAPI returned nil")
}
}
func TestBuildICEConfig_InvalidConfig(t *testing.T) {
c := DefaultConfig()
c.WHEPListen = ""
_, _, err := BuildICEConfig(c)
if err == nil {
t.Error("BuildICEConfig should reject invalid config")
}
}

159
core/webrtc/peer.go Normal file
View file

@ -0,0 +1,159 @@
package webrtc
import (
"context"
"crypto/rand"
"encoding/hex"
"fmt"
"sync"
"github.com/pion/rtp"
"github.com/pion/webrtc/v4"
)
// PeerFactory builds PeerConnections from a shared Pion API instance
// configured from Config.
type PeerFactory struct {
api *webrtc.API
rtcConfig webrtc.Configuration
}
// NewPeerFactory initializes a Pion API with the codec set we support
// (H.264 + Opus) and applies the provided Config.
func NewPeerFactory(c Config) (*PeerFactory, error) {
if err := c.Validate(); err != nil {
return nil, err
}
me := &webrtc.MediaEngine{}
if err := me.RegisterDefaultCodecs(); err != nil {
return nil, fmt.Errorf("webrtc: register default codecs: %w", err)
}
rtcConfig, se, err := BuildICEConfig(c)
if err != nil {
return nil, err
}
opts := []func(*webrtc.API){webrtc.WithMediaEngine(me)}
if se != nil {
opts = append(opts, webrtc.WithSettingEngine(*se))
}
api := webrtc.NewAPI(opts...)
return &PeerFactory{api: api, rtcConfig: rtcConfig}, nil
}
// Peer wraps a Pion PeerConnection bound to a Source's subscription.
type Peer struct {
resourceID string
pc *webrtc.PeerConnection
answer webrtc.SessionDescription
source *Source
sub chan *rtp.Packet
done chan struct{}
once sync.Once
}
// CreatePeer builds a PeerConnection, sets the remote offer, generates an
// answer, attaches video+audio tracks fed from src, and blocks until ICE
// gathering completes or ctx expires.
func (f *PeerFactory) CreatePeer(ctx context.Context, src *Source, offer webrtc.SessionDescription) (*Peer, error) {
pc, err := f.api.NewPeerConnection(f.rtcConfig)
if err != nil {
return nil, fmt.Errorf("webrtc: new peer connection: %w", err)
}
videoTrack, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264},
"video", "dragonfork")
if err != nil {
_ = pc.Close()
return nil, fmt.Errorf("webrtc: new video track: %w", err)
}
audioTrack, err := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus},
"audio", "dragonfork")
if err != nil {
_ = pc.Close()
return nil, fmt.Errorf("webrtc: new audio track: %w", err)
}
if _, err := pc.AddTrack(videoTrack); err != nil {
_ = pc.Close()
return nil, fmt.Errorf("webrtc: add video track: %w", err)
}
if _, err := pc.AddTrack(audioTrack); err != nil {
_ = pc.Close()
return nil, fmt.Errorf("webrtc: add audio track: %w", err)
}
if err := pc.SetRemoteDescription(offer); err != nil {
_ = pc.Close()
return nil, fmt.Errorf("webrtc: set remote: %w", err)
}
answer, err := pc.CreateAnswer(nil)
if err != nil {
_ = pc.Close()
return nil, fmt.Errorf("webrtc: create answer: %w", err)
}
gatherComplete := webrtc.GatheringCompletePromise(pc)
if err := pc.SetLocalDescription(answer); err != nil {
_ = pc.Close()
return nil, fmt.Errorf("webrtc: set local: %w", err)
}
select {
case <-gatherComplete:
case <-ctx.Done():
_ = pc.Close()
return nil, ErrICETimeout
}
sub := src.Subscribe(64)
p := &Peer{
resourceID: newResourceID(),
pc: pc,
answer: *pc.LocalDescription(),
source: src,
sub: sub,
done: make(chan struct{}),
}
pc.OnConnectionStateChange(func(st webrtc.PeerConnectionState) {
if st == webrtc.PeerConnectionStateFailed ||
st == webrtc.PeerConnectionStateDisconnected ||
st == webrtc.PeerConnectionStateClosed {
_ = p.Close()
}
})
go forwardRTP(p.done, sub, videoTrack, audioTrack)
return p, nil
}
// Answer returns the locally-created SDP answer. Valid after CreatePeer.
func (p *Peer) Answer() webrtc.SessionDescription { return p.answer }
// ResourceID returns the stable resource id used in the WHEP Location header.
func (p *Peer) ResourceID() string { return p.resourceID }
// Close tears down the peer connection and unsubscribes from the source.
// Safe to call multiple times.
func (p *Peer) Close() error {
var err error
p.once.Do(func() {
close(p.done)
p.source.Unsubscribe(p.sub)
err = p.pc.Close()
})
return err
}
func newResourceID() string {
b := make([]byte, 8)
_, _ = rand.Read(b)
return hex.EncodeToString(b)
}

96
core/webrtc/peer_test.go Normal file
View file

@ -0,0 +1,96 @@
package webrtc
import (
"context"
"testing"
"time"
"github.com/pion/webrtc/v4"
)
// minimalOfferSDP returns an SDP offer that advertises H.264 (video) and
// Opus (audio) as recvonly — the minimum a WHEP client sends.
func minimalOfferSDP(t *testing.T) webrtc.SessionDescription {
t.Helper()
// Create a throwaway PC to generate a valid offer.
me := &webrtc.MediaEngine{}
if err := me.RegisterDefaultCodecs(); err != nil {
t.Fatalf("RegisterDefaultCodecs: %v", err)
}
api := webrtc.NewAPI(webrtc.WithMediaEngine(me))
pc, err := api.NewPeerConnection(webrtc.Configuration{})
if err != nil {
t.Fatalf("NewPeerConnection: %v", err)
}
defer pc.Close()
if _, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo,
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly}); err != nil {
t.Fatalf("AddTransceiver video: %v", err)
}
if _, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio,
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly}); err != nil {
t.Fatalf("AddTransceiver audio: %v", err)
}
offer, err := pc.CreateOffer(nil)
if err != nil {
t.Fatalf("CreateOffer: %v", err)
}
return offer
}
func TestPeerFactory_CreateAnswer(t *testing.T) {
src, err := NewSource("streamA", 0)
if err != nil {
t.Fatalf("NewSource: %v", err)
}
defer src.Close()
src.Start()
cfg := DefaultConfig()
factory, err := NewPeerFactory(cfg)
if err != nil {
t.Fatalf("NewPeerFactory: %v", err)
}
offer := minimalOfferSDP(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
peer, err := factory.CreatePeer(ctx, src, offer)
if err != nil {
t.Fatalf("CreatePeer: %v", err)
}
defer peer.Close()
if peer.Answer().Type != webrtc.SDPTypeAnswer {
t.Errorf("Answer().Type = %v, want answer", peer.Answer().Type)
}
if peer.ResourceID() == "" {
t.Error("ResourceID should be non-empty")
}
}
func TestPeerFactory_ClosesCleanly(t *testing.T) {
src, _ := NewSource("streamA", 0)
defer src.Close()
src.Start()
factory, _ := NewPeerFactory(DefaultConfig())
offer := minimalOfferSDP(t)
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
peer, err := factory.CreatePeer(ctx, src, offer)
if err != nil {
t.Fatalf("CreatePeer: %v", err)
}
if err := peer.Close(); err != nil {
t.Errorf("Close: %v", err)
}
// Second close should be a no-op, not panic.
if err := peer.Close(); err != nil {
t.Errorf("second Close: %v", err)
}
}

51
core/webrtc/registry.go Normal file
View file

@ -0,0 +1,51 @@
package webrtc
import (
"fmt"
"sync"
)
// SourceHandle is the minimal interface the Registry stores per stream_id.
// The concrete type is *Source, defined in source.go.
type SourceHandle interface {
ID() string
}
// Registry is a thread-safe map from stream_id to active SourceHandle.
type Registry struct {
mu sync.RWMutex
streams map[string]SourceHandle
}
// NewRegistry returns an empty Registry.
func NewRegistry() *Registry {
return &Registry{streams: make(map[string]SourceHandle)}
}
// Register associates src with streamID. Returns an error if streamID is
// already registered.
func (r *Registry) Register(streamID string, src SourceHandle) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.streams[streamID]; exists {
return fmt.Errorf("webrtc: stream %q already registered", streamID)
}
r.streams[streamID] = src
return nil
}
// Lookup returns the handle for streamID. The second return value is false
// if no source is registered.
func (r *Registry) Lookup(streamID string) (SourceHandle, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
src, ok := r.streams[streamID]
return src, ok
}
// Deregister removes streamID. No-op if not present.
func (r *Registry) Deregister(streamID string) {
r.mu.Lock()
defer r.mu.Unlock()
delete(r.streams, streamID)
}

View file

@ -0,0 +1,74 @@
package webrtc
import (
"sync"
"testing"
)
// mockSource implements the minimum Source-like shape needed by the registry.
// The real Source type is defined in Task 5; the registry only needs a
// stable type to store and retrieve.
type mockSource struct {
id string
}
func (m *mockSource) ID() string { return m.id }
func TestRegistry_RegisterAndLookup(t *testing.T) {
r := NewRegistry()
src := &mockSource{id: "streamA"}
if err := r.Register("streamA", src); err != nil {
t.Fatalf("Register returned error: %v", err)
}
got, ok := r.Lookup("streamA")
if !ok {
t.Fatal("Lookup(streamA) returned ok=false, want true")
}
if got != src {
t.Errorf("Lookup returned %v, want %v", got, src)
}
}
func TestRegistry_LookupMissing(t *testing.T) {
r := NewRegistry()
_, ok := r.Lookup("nope")
if ok {
t.Error("Lookup on empty registry returned ok=true, want false")
}
}
func TestRegistry_DuplicateRegister(t *testing.T) {
r := NewRegistry()
_ = r.Register("streamA", &mockSource{id: "streamA"})
if err := r.Register("streamA", &mockSource{id: "streamA"}); err == nil {
t.Error("duplicate Register should return error, got nil")
}
}
func TestRegistry_Deregister(t *testing.T) {
r := NewRegistry()
_ = r.Register("streamA", &mockSource{id: "streamA"})
r.Deregister("streamA")
if _, ok := r.Lookup("streamA"); ok {
t.Error("after Deregister, Lookup should return ok=false")
}
}
func TestRegistry_ConcurrentAccess(t *testing.T) {
r := NewRegistry()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(3)
id := string(rune('a' + (i % 26)))
go func() { defer wg.Done(); _ = r.Register(id, &mockSource{id: id}) }()
go func() { defer wg.Done(); _, _ = r.Lookup(id) }()
go func() { defer wg.Done(); r.Deregister(id) }()
}
wg.Wait()
// No assertion — test passes if -race doesn't flag anything.
}

133
core/webrtc/source.go Normal file
View file

@ -0,0 +1,133 @@
package webrtc
import (
"fmt"
"net"
"sync"
"github.com/pion/rtp"
)
// Source reads RTP packets from a local UDP socket and fans them out to
// subscribed peers via per-subscriber buffered channels.
type Source struct {
id string
conn *net.UDPConn
mu sync.Mutex
subscribers map[chan *rtp.Packet]struct{}
started bool
closed bool
done chan struct{}
}
// NewSource binds a UDP socket on 127.0.0.1:port. Pass port=0 to let the OS
// assign an ephemeral port (useful for tests).
func NewSource(streamID string, port int) (*Source, error) {
addr := &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: port}
conn, err := net.ListenUDP("udp4", addr)
if err != nil {
return nil, fmt.Errorf("webrtc: listen udp: %w", err)
}
return &Source{
id: streamID,
conn: conn,
subscribers: make(map[chan *rtp.Packet]struct{}),
done: make(chan struct{}),
}, nil
}
// ID returns the registered stream identifier.
func (s *Source) ID() string { return s.id }
// LocalAddr returns the UDP address the source is listening on.
func (s *Source) LocalAddr() *net.UDPAddr {
return s.conn.LocalAddr().(*net.UDPAddr)
}
// Subscribe returns a new buffered channel that receives every RTP packet
// read from the UDP socket. bufDepth is the channel buffer size; when full,
// packets are dropped (preventing a slow subscriber from back-pressuring
// the reader).
func (s *Source) Subscribe(bufDepth int) chan *rtp.Packet {
ch := make(chan *rtp.Packet, bufDepth)
s.mu.Lock()
s.subscribers[ch] = struct{}{}
s.mu.Unlock()
return ch
}
// Unsubscribe removes ch from the subscriber set and closes it.
func (s *Source) Unsubscribe(ch chan *rtp.Packet) {
s.mu.Lock()
defer s.mu.Unlock()
if _, ok := s.subscribers[ch]; ok {
delete(s.subscribers, ch)
close(ch)
}
}
// Start begins the RTP reader goroutine. Safe to call once; subsequent calls
// are no-ops.
func (s *Source) Start() {
s.mu.Lock()
if s.started || s.closed {
s.mu.Unlock()
return
}
s.started = true
s.mu.Unlock()
go s.readLoop()
}
func (s *Source) readLoop() {
buf := make([]byte, 1500) // MTU-sized; RTP over UDP should fit
for {
select {
case <-s.done:
return
default:
}
n, _, err := s.conn.ReadFromUDP(buf)
if err != nil {
// Socket closed or error — exit the loop.
return
}
pkt := &rtp.Packet{}
if err := pkt.Unmarshal(buf[:n]); err != nil {
// Malformed packet; skip without crashing.
continue
}
s.mu.Lock()
for ch := range s.subscribers {
select {
case ch <- pkt:
default:
// Subscriber full — drop to protect the reader.
}
}
s.mu.Unlock()
}
}
// Close stops the reader goroutine, closes the UDP socket, and closes every
// subscriber channel.
func (s *Source) Close() error {
s.mu.Lock()
if s.closed {
s.mu.Unlock()
return nil
}
s.closed = true
close(s.done)
for ch := range s.subscribers {
delete(s.subscribers, ch)
close(ch)
}
s.mu.Unlock()
return s.conn.Close()
}

129
core/webrtc/source_test.go Normal file
View file

@ -0,0 +1,129 @@
package webrtc
import (
"net"
"testing"
"time"
"github.com/pion/rtp"
)
func TestSource_ID(t *testing.T) {
s, err := NewSource("streamA", 0) // 0 = ephemeral port
if err != nil {
t.Fatalf("NewSource: %v", err)
}
defer s.Close()
if s.ID() != "streamA" {
t.Errorf("ID() = %q, want streamA", s.ID())
}
}
func TestSource_ReceiveAndFanout(t *testing.T) {
s, err := NewSource("streamA", 0)
if err != nil {
t.Fatalf("NewSource: %v", err)
}
defer s.Close()
// Subscribe before sending.
sub := s.Subscribe(16) // buffer depth 16
defer s.Unsubscribe(sub)
s.Start()
// Build and send a minimal RTP packet to the source's UDP port.
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
PayloadType: 96,
SequenceNumber: 1,
Timestamp: 1000,
SSRC: 0xDEADBEEF,
},
Payload: []byte{0x01, 0x02, 0x03, 0x04},
}
raw, err := pkt.Marshal()
if err != nil {
t.Fatalf("pkt.Marshal: %v", err)
}
conn, err := net.Dial("udp", s.LocalAddr().String())
if err != nil {
t.Fatalf("net.Dial: %v", err)
}
defer conn.Close()
if _, err := conn.Write(raw); err != nil {
t.Fatalf("conn.Write: %v", err)
}
select {
case got := <-sub:
if got.SSRC != 0xDEADBEEF {
t.Errorf("received SSRC = %x, want DEADBEEF", got.SSRC)
}
if got.SequenceNumber != 1 {
t.Errorf("received SeqNum = %d, want 1", got.SequenceNumber)
}
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for RTP packet on subscriber channel")
}
}
func TestSource_MultipleSubscribers(t *testing.T) {
s, err := NewSource("streamA", 0)
if err != nil {
t.Fatalf("NewSource: %v", err)
}
defer s.Close()
subs := []chan *rtp.Packet{
s.Subscribe(8),
s.Subscribe(8),
s.Subscribe(8),
}
for _, sub := range subs {
defer s.Unsubscribe(sub)
}
s.Start()
raw, _ := (&rtp.Packet{
Header: rtp.Header{Version: 2, PayloadType: 96, SequenceNumber: 42, SSRC: 1},
Payload: []byte{0xAA},
}).Marshal()
conn, _ := net.Dial("udp", s.LocalAddr().String())
defer conn.Close()
_, _ = conn.Write(raw)
for i, sub := range subs {
select {
case got := <-sub:
if got.SequenceNumber != 42 {
t.Errorf("sub %d got seq %d, want 42", i, got.SequenceNumber)
}
case <-time.After(2 * time.Second):
t.Errorf("sub %d timed out", i)
}
}
}
func TestSource_UnsubscribeStopsDelivery(t *testing.T) {
s, _ := NewSource("streamA", 0)
defer s.Close()
sub := s.Subscribe(8)
s.Start()
s.Unsubscribe(sub)
// After Unsubscribe, the channel should be closed.
select {
case _, ok := <-sub:
if ok {
t.Error("expected channel closed after Unsubscribe, got value")
}
case <-time.After(500 * time.Millisecond):
t.Error("timed out waiting for channel close")
}
}

93
core/webrtc/whep.go Normal file
View file

@ -0,0 +1,93 @@
package webrtc
import (
"io"
"net/http"
"strings"
"sync"
"sync/atomic"
"github.com/pion/webrtc/v4"
)
// WHEPHandler serves the WebRTC-HTTP Egress Protocol POST.
type WHEPHandler struct {
registry *Registry
factory *PeerFactory
config Config
mu sync.Mutex
peers map[string]*Peer // resourceID → Peer
peersCount int64 // atomic, for cap check without lock
}
// NewWHEPHandler constructs a handler with the given dependencies.
func NewWHEPHandler(r *Registry, f *PeerFactory, c Config) *WHEPHandler {
return &WHEPHandler{
registry: r,
factory: f,
config: c,
peers: make(map[string]*Peer),
}
}
// ServeHTTP handles POST /whep/{stream_id}. Other methods and paths return 405.
func (h *WHEPHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.Header().Set("Allow", "POST")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
// Extract stream_id from path: /whep/{stream_id}
streamID := strings.TrimPrefix(r.URL.Path, "/whep/")
if streamID == "" || strings.Contains(streamID, "/") {
http.Error(w, "invalid stream id", http.StatusBadRequest)
return
}
// Peer cap enforcement (happy path still respects the cap).
if atomic.LoadInt64(&h.peersCount) >= int64(h.config.MaxPeersTotal) {
http.Error(w, ErrPeerCapReached.Error(), http.StatusServiceUnavailable)
return
}
handle, ok := h.registry.Lookup(streamID)
if !ok {
http.Error(w, ErrStreamNotFound.Error(), http.StatusNotFound)
return
}
src, ok := handle.(*Source)
if !ok {
http.Error(w, "registered source is not a *Source", http.StatusInternalServerError)
return
}
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "read body: "+err.Error(), http.StatusBadRequest)
return
}
if len(body) == 0 {
http.Error(w, ErrInvalidSDP.Error(), http.StatusBadRequest)
return
}
offer := webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: string(body)}
peer, err := h.factory.CreatePeer(r.Context(), src, offer)
if err != nil {
http.Error(w, "create peer: "+err.Error(), http.StatusInternalServerError)
return
}
h.mu.Lock()
h.peers[peer.ResourceID()] = peer
h.mu.Unlock()
atomic.AddInt64(&h.peersCount, 1)
w.Header().Set("Content-Type", "application/sdp")
w.Header().Set("Location", "/whep/"+streamID+"/"+peer.ResourceID())
w.WriteHeader(http.StatusCreated)
_, _ = io.WriteString(w, peer.Answer().SDP)
}

64
core/webrtc/whep_test.go Normal file
View file

@ -0,0 +1,64 @@
package webrtc
import (
"context"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
"github.com/pion/webrtc/v4"
)
func TestWHEP_POSTReturns201WithSDP(t *testing.T) {
// Set up a Source and register it.
src, _ := NewSource("streamA", 0)
defer src.Close()
src.Start()
reg := NewRegistry()
_ = reg.Register("streamA", src)
factory, _ := NewPeerFactory(DefaultConfig())
handler := NewWHEPHandler(reg, factory, DefaultConfig())
// Build an offer using a throwaway PC.
me := &webrtc.MediaEngine{}
_ = me.RegisterDefaultCodecs()
api := webrtc.NewAPI(webrtc.WithMediaEngine(me))
pc, _ := api.NewPeerConnection(webrtc.Configuration{})
defer pc.Close()
_, _ = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo,
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly})
_, _ = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio,
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly})
offer, _ := pc.CreateOffer(nil)
req := httptest.NewRequest(http.MethodPost, "/whep/streamA",
strings.NewReader(offer.SDP))
req.Header.Set("Content-Type", "application/sdp")
// Give the handler generous ICE gathering time in tests.
ctx, cancel := context.WithTimeout(req.Context(), 10*time.Second)
defer cancel()
req = req.WithContext(ctx)
rr := httptest.NewRecorder()
handler.ServeHTTP(rr, req)
if rr.Code != http.StatusCreated {
body, _ := io.ReadAll(rr.Result().Body)
t.Fatalf("status = %d, want 201. body=%s", rr.Code, string(body))
}
if ct := rr.Header().Get("Content-Type"); ct != "application/sdp" {
t.Errorf("Content-Type = %q, want application/sdp", ct)
}
if loc := rr.Header().Get("Location"); !strings.HasPrefix(loc, "/whep/streamA/") {
t.Errorf("Location = %q, want /whep/streamA/<id>", loc)
}
if !strings.Contains(rr.Body.String(), "v=0") {
t.Errorf("body does not look like SDP: %s", rr.Body.String())
}
}

17
go.mod
View file

@ -20,6 +20,8 @@ require (
github.com/lithammer/shortuuid/v4 v4.0.0
github.com/mattn/go-isatty v0.0.20
github.com/minio/minio-go/v7 v7.0.70
github.com/pion/rtp v1.10.1
github.com/pion/webrtc/v4 v4.2.11
github.com/prep/average v0.0.0-20200506183628-d26c465f48c3
github.com/prometheus/client_golang v1.19.1
github.com/puzpuzpuz/xsync/v3 v3.1.0
@ -71,6 +73,20 @@ require (
github.com/miekg/dns v1.1.59 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/pion/datachannel v1.6.0 // indirect
github.com/pion/dtls/v3 v3.1.2 // indirect
github.com/pion/ice/v4 v4.2.2 // indirect
github.com/pion/interceptor v0.1.44 // indirect
github.com/pion/logging v0.2.4 // indirect
github.com/pion/mdns/v2 v2.1.0 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.16 // indirect
github.com/pion/sctp v1.9.4 // indirect
github.com/pion/sdp/v3 v3.0.18 // indirect
github.com/pion/srtp/v3 v3.0.10 // indirect
github.com/pion/stun/v3 v3.1.1 // indirect
github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pion/turn/v4 v4.1.4 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
@ -86,6 +102,7 @@ require (
github.com/urfave/cli/v2 v2.27.2 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasttemplate v1.2.2 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect

36
go.sum
View file

@ -134,6 +134,40 @@ github.com/minio/minio-go/v7 v7.0.70 h1:1u9NtMgfK1U42kUxcsl5v0yj6TEOPR497OAQxpJn
github.com/minio/minio-go/v7 v7.0.70/go.mod h1:4yBA8v80xGA30cfM3fz0DKYMXunWl/AV/6tWEs9ryzo=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=
github.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=
github.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=
github.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=
github.com/pion/ice/v4 v4.2.2 h1:dQJzzcgTFHDYyV3BoCfjPeX+JEtr58BWPi4PGyo6Vjg=
github.com/pion/ice/v4 v4.2.2/go.mod h1:2quLV1S5v1tAx3VvAJaH//KGitRXvo4RKlX6D3tnN+c=
github.com/pion/interceptor v0.1.44 h1:sNlZwM8dWXU9JQAkJh8xrarC0Etn8Oolcniukmuy0/I=
github.com/pion/interceptor v0.1.44/go.mod h1:4atVlBkcgXuUP+ykQF0qOCGU2j7pQzX2ofvPRFsY5RY=
github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=
github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=
github.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=
github.com/pion/rtp v1.10.1 h1:xP1prZcCTUuhO2c83XtxyOHJteISg6o8iPsE2acaMtA=
github.com/pion/rtp v1.10.1/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
github.com/pion/sctp v1.9.4 h1:cMxEu0F5tbP4qH07bKf1Zjf4rUih9LIo0qQt424e258=
github.com/pion/sctp v1.9.4/go.mod h1:N20Dq6LY+JvJDAh9VVh1JELngb2rQ8dPgds5yBWiPgw=
github.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI=
github.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8=
github.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=
github.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=
github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=
github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=
github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=
github.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=
github.com/pion/webrtc/v4 v4.2.11 h1:QUX1QZKlNIn4O7U5JxLPGP0sV5RTncZkzu9SPR3jVNU=
github.com/pion/webrtc/v4 v4.2.11/go.mod h1:s/rAiyy77GyRFrZMx+Ls6aua26dIBPudH8/ZHYbIRWY=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -200,6 +234,8 @@ github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQ
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
github.com/vektah/gqlparser/v2 v2.5.12 h1:COMhVVnql6RoaF7+aTBWiTADdpLGyZWU3K/NwW0ph98=
github.com/vektah/gqlparser/v2 v2.5.12/go.mod h1:WQQjFc+I1YIzoPvZBhUQX7waZgg3pMLi0r8KymvAE2w=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo=
github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU=

46
test/publish.sh Executable file
View file

@ -0,0 +1,46 @@
#!/usr/bin/env bash
# publish.sh — M1 PoC test publisher.
#
# Generates a synthetic test pattern + sine tone, encodes to H.264
# baseline (PT=102) and Opus (PT=111), then sends both RTP streams
# muxed on a single UDP port to match the M1 webrtc-poc server, which
# reads one UDP port and dispatches by payload type.
#
# Usage:
# ./test/publish.sh [host] [port]
#
# Defaults: host=127.0.0.1 port=10000
set -euo pipefail
HOST="${1:-127.0.0.1}"
PORT="${2:-10000}"
echo "publishing test pattern + tone to rtp://${HOST}:${PORT}"
echo "video PT=102 (H.264 baseline), audio PT=111 (Opus)"
echo "press Ctrl-C to stop."
# -re real-time pace (wall-clock)
# testsrc / sine synthetic A/V so no devices needed
# libx264 baseline widely compatible profile for WebRTC
# -tune zerolatency minimize encoder buffering
# -bsf:v h264_mp4toannexb ensure Annex-B for RTP packetization
# -payload_type 102/111 match the hard-coded PTs in forward.go
# -f rtp_mpegts fails (we need plain rtp, not mpegts-in-rtp)
# Using two separate -f rtp outputs, both to the same UDP port.
# FFmpeg 4.x requires an SDP file per output; we write them to /tmp
# but the server doesn't use them — it only cares about PT.
exec ffmpeg -hide_banner -loglevel warning \
-re \
-f lavfi -i "testsrc2=size=640x360:rate=30" \
-f lavfi -i "sine=frequency=440:sample_rate=48000" \
-map 0:v:0 \
-c:v libx264 -profile:v baseline -preset veryfast -tune zerolatency \
-g 30 -keyint_min 30 -x264-params "repeat-headers=1" \
-pix_fmt yuv420p \
-bsf:v h264_mp4toannexb \
-payload_type 102 \
-f rtp "rtp://${HOST}:${PORT}?pkt_size=1200" \
-map 1:a:0 \
-c:a libopus -b:a 64k -ar 48000 -ac 2 \
-payload_type 111 \
-f rtp "rtp://${HOST}:${PORT}?pkt_size=1200"

152
test/whep-client/main.go Normal file
View file

@ -0,0 +1,152 @@
// Command whep-client is a minimal Pion-based WHEP subscriber used for
// M1 end-to-end verification. It POSTs a recvonly SDP offer to a WHEP
// endpoint, applies the answer, then reports whether the video and
// audio tracks receive at least one RTP packet before a timeout.
//
// This is a test helper; it is NOT part of the Core binary.
package main
import (
"bytes"
"context"
"flag"
"fmt"
"io"
"log"
"net/http"
"os"
"time"
"github.com/pion/webrtc/v4"
)
func main() {
var (
whepURL = flag.String("url", "http://127.0.0.1:8787/whep/test", "WHEP endpoint URL")
timeout = flag.Duration("timeout", 10*time.Second, "overall subscribe+receive timeout")
)
flag.Parse()
ctx, cancel := context.WithTimeout(context.Background(), *timeout)
defer cancel()
if err := Subscribe(ctx, *whepURL); err != nil {
log.Fatalf("subscribe failed: %v", err)
}
fmt.Println("OK: received video and audio RTP")
os.Exit(0)
}
// Subscribe performs a full WHEP subscribe against whepURL and returns
// nil when both a video and an audio RTP packet have been observed
// before ctx expires. It is exported so tests can exercise it.
func Subscribe(ctx context.Context, whepURL string) error {
me := &webrtc.MediaEngine{}
if err := me.RegisterDefaultCodecs(); err != nil {
return fmt.Errorf("register codecs: %w", err)
}
api := webrtc.NewAPI(webrtc.WithMediaEngine(me))
pc, err := api.NewPeerConnection(webrtc.Configuration{})
if err != nil {
return fmt.Errorf("new peer connection: %w", err)
}
defer pc.Close()
if _, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo,
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly}); err != nil {
return fmt.Errorf("add video transceiver: %w", err)
}
if _, err := pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio,
webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly}); err != nil {
return fmt.Errorf("add audio transceiver: %w", err)
}
videoDone := make(chan struct{})
audioDone := make(chan struct{})
pc.OnTrack(func(t *webrtc.TrackRemote, _ *webrtc.RTPReceiver) {
kind := t.Kind()
log.Printf("OnTrack: kind=%s codec=%s pt=%d", kind, t.Codec().MimeType, t.PayloadType())
go func() {
buf := make([]byte, 1500)
// One successful ReadRTP is enough to prove egress.
if _, _, err := t.Read(buf); err != nil {
log.Printf("read %s: %v", kind, err)
return
}
switch kind {
case webrtc.RTPCodecTypeVideo:
select {
case <-videoDone:
default:
close(videoDone)
}
case webrtc.RTPCodecTypeAudio:
select {
case <-audioDone:
default:
close(audioDone)
}
}
}()
})
offer, err := pc.CreateOffer(nil)
if err != nil {
return fmt.Errorf("create offer: %w", err)
}
gather := webrtc.GatheringCompletePromise(pc)
if err := pc.SetLocalDescription(offer); err != nil {
return fmt.Errorf("set local: %w", err)
}
select {
case <-gather:
case <-ctx.Done():
return fmt.Errorf("ice gather: %w", ctx.Err())
}
answerSDP, err := postOffer(ctx, whepURL, pc.LocalDescription().SDP)
if err != nil {
return err
}
if err := pc.SetRemoteDescription(webrtc.SessionDescription{
Type: webrtc.SDPTypeAnswer,
SDP: answerSDP,
}); err != nil {
return fmt.Errorf("set remote: %w", err)
}
// Wait for one RTP packet on each track or ctx timeout.
select {
case <-videoDone:
case <-ctx.Done():
return fmt.Errorf("waiting for video: %w", ctx.Err())
}
select {
case <-audioDone:
case <-ctx.Done():
return fmt.Errorf("waiting for audio: %w", ctx.Err())
}
return nil
}
func postOffer(ctx context.Context, url, sdp string) (string, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url,
bytes.NewReader([]byte(sdp)))
if err != nil {
return "", fmt.Errorf("new request: %w", err)
}
req.Header.Set("Content-Type", "application/sdp")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", fmt.Errorf("POST %s: %w", url, err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
if resp.StatusCode != http.StatusCreated {
return "", fmt.Errorf("WHEP %s: %d %s", url, resp.StatusCode, string(body))
}
return string(body), nil
}

View file

@ -0,0 +1,110 @@
package main
import (
"context"
"net"
"net/http"
"net/http/httptest"
"strings"
"sync"
"testing"
"time"
coreweb "github.com/datarhei/core/v16/core/webrtc"
"github.com/pion/rtp"
)
// TestSubscribe_EndToEnd stands up an in-process webrtc-poc stack,
// injects synthetic H.264(PT=102) + Opus(PT=111) RTP into the Source's
// UDP port, and asserts Subscribe returns nil within the timeout.
//
// Network-heavy; skipped under -short.
func TestSubscribe_EndToEnd(t *testing.T) {
if testing.Short() {
t.Skip("skipping end-to-end subscribe test in short mode")
}
src, err := coreweb.NewSource("stream-e2e", 0)
if err != nil {
t.Fatalf("NewSource: %v", err)
}
src.Start()
defer src.Close()
reg := coreweb.NewRegistry()
if err := reg.Register("stream-e2e", src); err != nil {
t.Fatalf("Register: %v", err)
}
factory, err := coreweb.NewPeerFactory(coreweb.DefaultConfig())
if err != nil {
t.Fatalf("NewPeerFactory: %v", err)
}
handler := coreweb.NewWHEPHandler(reg, factory, coreweb.DefaultConfig())
ts := httptest.NewServer(http.StripPrefix("", handler))
defer ts.Close()
// Begin injecting RTP into the source.
rtpAddr := src.LocalAddr()
conn, err := net.DialUDP("udp", nil, rtpAddr)
if err != nil {
t.Fatalf("dial udp: %v", err)
}
defer conn.Close()
stop := make(chan struct{})
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
tick := time.NewTicker(20 * time.Millisecond)
defer tick.Stop()
var seq uint16
var ts uint32
for {
select {
case <-stop:
return
case <-tick.C:
// Video packet (PT=102).
pkt := &rtp.Packet{
Header: rtp.Header{
Version: 2,
PayloadType: 102,
SequenceNumber: seq,
Timestamp: ts,
SSRC: 0x1234,
},
Payload: []byte{0x00, 0x00, 0x00, 0x01, 0x09, 0x10},
}
if b, err := pkt.Marshal(); err == nil {
_, _ = conn.Write(b)
}
// Audio packet (PT=111).
pkt.PayloadType = 111
pkt.SSRC = 0x5678
pkt.SequenceNumber = seq
if b, err := pkt.Marshal(); err == nil {
_, _ = conn.Write(b)
}
seq++
ts += 3000
}
}
}()
defer func() {
close(stop)
wg.Wait()
}()
// We don't care whether the test client's Subscribe can actually
// decode H.264 — just that it observed *some* RTP on both tracks.
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
whepURL := strings.TrimRight(ts.URL, "/") + "/whep/stream-e2e"
if err := Subscribe(ctx, whepURL); err != nil {
t.Fatalf("Subscribe: %v", err)
}
}

28
vendor/github.com/pion/datachannel/.gitignore generated vendored Normal file
View file

@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
# SPDX-License-Identifier: MIT
### JetBrains IDE ###
#####################
.idea/
### Emacs Temporary Files ###
#############################
*~
### Folders ###
###############
bin/
vendor/
node_modules/
### Files ###
#############
*.ivf
*.ogg
tags
cover.out
*.sw[poe]
*.wasm
examples/sfu-ws/cert.pem
examples/sfu-ws/key.pem
wasm_exec.js

147
vendor/github.com/pion/datachannel/.golangci.yml generated vendored Normal file
View file

@ -0,0 +1,147 @@
# SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
# SPDX-License-Identifier: MIT
version: "2"
linters:
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bidichk # Checks for dangerous unicode character sequences
- bodyclose # checks whether HTTP response body is closed successfully
- containedctx # containedctx is a linter that detects struct contained context.Context field
- contextcheck # check the function whether use a non-inherited context
- cyclop # checks function and package cyclomatic complexity
- decorder # check declaration order and count of types, constants, variables and functions
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
- dupl # Tool for code clone detection
- durationcheck # check for two durations multiplied together
- err113 # Golang linter to check the errors handling expressions
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted.
- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`.
- errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13.
- exhaustive # check exhaustiveness of enum switch statements
- forbidigo # Forbids identifiers
- forcetypeassert # finds forced type assertions
- gochecknoglobals # Checks that no globals are present in Go code
- gocognit # Computes and checks the cognitive complexity of functions
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # The most opinionated Go source code linter
- gocyclo # Computes and checks the cyclomatic complexity of functions
- godot # Check if comments end in a period
- godox # Tool for detection of FIXME, TODO and other comment keywords
- goheader # Checks is file header matches to pattern
- gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod.
- goprintffuncname # Checks that printf-like functions are named with `f` at the end
- gosec # Inspects source code for security problems
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- grouper # An analyzer to analyze expression groups.
- importas # Enforces consistent import aliases
- ineffassign # Detects when assignments to existing variables are not used
- lll # Reports long lines
- maintidx # maintidx measures the maintainability index of each function.
- makezero # Finds slice declarations with non-zero initial length
- misspell # Finds commonly misspelled English words in comments
- nakedret # Finds naked returns in functions greater than a specified function length
- nestif # Reports deeply nested if statements
- nilerr # Finds the code that returns nil even if it checks that the error is not nil.
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value.
- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity
- noctx # noctx finds sending http request without context.Context
- predeclared # find code that shadows one of Go's predeclared identifiers
- revive # golint replacement, finds style mistakes
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
- tagliatelle # Checks the struct tags.
- thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers
- unconvert # Remove unnecessary type conversions
- unparam # Reports unused function parameters
- unused # Checks Go code for unused constants, variables, functions and types
- varnamelen # checks that the length of a variable's name matches its scope
- wastedassign # wastedassign finds wasted assignment statements
- whitespace # Tool for detection of leading and trailing whitespace
disable:
- depguard # Go linter that checks if package imports are in a list of acceptable packages
- funlen # Tool for detection of long functions
- gochecknoinits # Checks that no init functions are present in Go code
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations.
- interfacebloat # A linter that checks length of interface.
- ireturn # Accept Interfaces, Return Concrete Types
- mnd # An analyzer to detect magic numbers
- nolintlint # Reports ill-formed or insufficient nolint directives
- paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test
- prealloc # Finds slice declarations that could potentially be preallocated
- promlinter # Check Prometheus metrics naming via promlint
- rowserrcheck # checks whether Err of rows is checked successfully
- sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed.
- testpackage # linter that makes you use a separate _test package
- tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes
- wrapcheck # Checks that errors returned from external packages are wrapped
- wsl # Whitespace Linter - Forces you to use empty lines!
settings:
staticcheck:
checks:
- all
- -QF1008 # "could remove embedded field", to keep it explicit!
- -QF1003 # "could use tagged switch on enum", Cases conflicts with exhaustive!
exhaustive:
default-signifies-exhaustive: true
forbidigo:
forbid:
- pattern: ^fmt.Print(f|ln)?$
- pattern: ^log.(Panic|Fatal|Print)(f|ln)?$
- pattern: ^os.Exit$
- pattern: ^panic$
- pattern: ^print(ln)?$
- pattern: ^testing.T.(Error|Errorf|Fatal|Fatalf|Fail|FailNow)$
pkg: ^testing$
msg: use testify/assert instead
analyze-types: true
gomodguard:
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- errors
govet:
enable:
- shadow
revive:
rules:
# Prefer 'any' type alias over 'interface{}' for Go 1.18+ compatibility
- name: use-any
severity: warning
disabled: false
misspell:
locale: US
varnamelen:
max-distance: 12
min-name-length: 2
ignore-type-assert-ok: true
ignore-map-index-ok: true
ignore-chan-recv-ok: true
ignore-decls:
- i int
- n int
- w io.Writer
- r io.Reader
- b []byte
exclusions:
generated: lax
rules:
- linters:
- forbidigo
- gocognit
path: (examples|main\.go)
- linters:
- gocognit
path: _test\.go
- linters:
- forbidigo
path: cmd
formatters:
enable:
- gci # Gci control golang package import order and make it always deterministic.
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
- gofumpt # Gofumpt checks whether code was gofumpt-ed.
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports
exclusions:
generated: lax

5
vendor/github.com/pion/datachannel/.goreleaser.yml generated vendored Normal file
View file

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
# SPDX-License-Identifier: MIT
builds:
- skip: true

9
vendor/github.com/pion/datachannel/LICENSE generated vendored Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2023 The Pion community <https://pion.ly>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

34
vendor/github.com/pion/datachannel/README.md generated vendored Normal file
View file

@ -0,0 +1,34 @@
<h1 align="center">
<br>
Pion Data Channels
<br>
</h1>
<h4 align="center">A Go implementation of WebRTC Data Channels</h4>
<p align="center">
<a href="https://pion.ly"><img src="https://img.shields.io/badge/pion-datachannel-gray.svg?longCache=true&colorB=brightgreen" alt="Pion Data Channels"></a>
<a href="https://discord.gg/PngbdqpFbt"><img src="https://img.shields.io/badge/join-us%20on%20discord-gray.svg?longCache=true&logo=discord&colorB=brightblue" alt="join us on Discord"></a> <a href="https://bsky.app/profile/pion.ly"><img src="https://img.shields.io/badge/follow-us%20on%20bluesky-gray.svg?longCache=true&logo=bluesky&colorB=brightblue" alt="Follow us on Bluesky"></a>
<br>
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/pion/datachannel/test.yaml">
<a href="https://pkg.go.dev/github.com/pion/datachannel"><img src="https://pkg.go.dev/badge/github.com/pion/datachannel.svg" alt="Go Reference"></a>
<a href="https://codecov.io/gh/pion/datachannel"><img src="https://codecov.io/gh/pion/datachannel/branch/master/graph/badge.svg" alt="Coverage Status"></a>
<a href="https://goreportcard.com/report/github.com/pion/datachannel"><img src="https://goreportcard.com/badge/github.com/pion/datachannel" alt="Go Report Card"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
</p>
<br>
### Roadmap
The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones.
### Community
Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt).
Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news.
We are always looking to support **your projects**. Please reach out if you have something to build!
If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly)
### Contributing
Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible
### License
MIT License - see [LICENSE](LICENSE) for full text

22
vendor/github.com/pion/datachannel/codecov.yml generated vendored Normal file
View file

@ -0,0 +1,22 @@
#
# DO NOT EDIT THIS FILE
#
# It is automatically copied from https://github.com/pion/.goassets repository.
#
# SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
# SPDX-License-Identifier: MIT
coverage:
status:
project:
default:
# Allow decreasing 2% of total coverage to avoid noise.
threshold: 2%
patch:
default:
target: 70%
only_pulls: true
ignore:
- "examples/*"
- "examples/**/*"

445
vendor/github.com/pion/datachannel/datachannel.go generated vendored Normal file
View file

@ -0,0 +1,445 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package datachannel implements WebRTC Data Channels
package datachannel
import (
"errors"
"fmt"
"io"
"sync"
"sync/atomic"
"time"
"github.com/pion/logging"
"github.com/pion/sctp"
)
const receiveMTU = 8192
// Reader is an extended io.Reader
// that also returns if the message is text.
type Reader interface {
ReadDataChannel([]byte) (int, bool, error)
}
// ReadDeadliner extends an io.Reader to expose setting a read deadline.
type ReadDeadliner interface {
SetReadDeadline(time.Time) error
}
// Writer is an extended io.Writer
// that also allows indicating if a message is text.
type Writer interface {
WriteDataChannel([]byte, bool) (int, error)
}
// WriteDeadliner extends an io.Writer to expose setting a write deadline.
type WriteDeadliner interface {
SetWriteDeadline(time.Time) error
}
// ReadWriteCloser is an extended io.ReadWriteCloser
// that also implements our Reader and Writer.
type ReadWriteCloser interface {
io.Reader
io.Writer
Reader
Writer
io.Closer
}
// ReadWriteCloserDeadliner is an extended ReadWriteCloser
// that also implements r/w deadline.
type ReadWriteCloserDeadliner interface {
ReadWriteCloser
ReadDeadliner
WriteDeadliner
}
// DataChannel represents a data channel.
type DataChannel struct {
Config
// stats
messagesSent uint32
messagesReceived uint32
bytesSent uint64
bytesReceived uint64
mu sync.Mutex
onOpenCompleteHandler func()
openCompleteHandlerOnce sync.Once
stream *sctp.Stream
log logging.LeveledLogger
}
// Config is used to configure the data channel.
type Config struct {
ChannelType ChannelType
Negotiated bool
Priority uint16
ReliabilityParameter uint32
Label string
Protocol string
LoggerFactory logging.LoggerFactory
}
func newDataChannel(stream *sctp.Stream, config *Config) *DataChannel {
return &DataChannel{
Config: *config,
stream: stream,
log: config.LoggerFactory.NewLogger("datachannel"),
}
}
// Dial opens a data channels over SCTP.
func Dial(a *sctp.Association, id uint16, config *Config) (*DataChannel, error) {
stream, err := a.OpenStream(id, sctp.PayloadTypeWebRTCBinary)
if err != nil {
return nil, err
}
dc, err := Client(stream, config)
if err != nil {
return nil, err
}
isReliable := dc.ChannelType == ChannelTypeReliable || dc.ChannelType == ChannelTypeReliableUnordered
if isReliable && dc.ReliabilityParameter != 0 {
dc.log.Warnf("DataChannel opened with channel type %s, but has a non-zero reliability parameter: %d (expected 0)",
dc.ChannelType,
dc.ReliabilityParameter)
}
return dc, nil
}
// Client opens a data channel over an SCTP stream.
func Client(stream *sctp.Stream, config *Config) (*DataChannel, error) {
msg := &channelOpen{
ChannelType: config.ChannelType,
Priority: config.Priority,
ReliabilityParameter: config.ReliabilityParameter,
Label: []byte(config.Label),
Protocol: []byte(config.Protocol),
}
if !config.Negotiated {
rawMsg, err := msg.Marshal()
if err != nil {
return nil, fmt.Errorf("failed to marshal ChannelOpen %w", err)
}
if _, err = stream.WriteSCTP(rawMsg, sctp.PayloadTypeWebRTCDCEP); err != nil {
return nil, fmt.Errorf("failed to send ChannelOpen %w", err)
}
}
return newDataChannel(stream, config), nil
}
// Accept is used to accept incoming data channels over SCTP.
func Accept(a *sctp.Association, config *Config, existingChannels ...*DataChannel) (*DataChannel, error) {
stream, err := a.AcceptStream()
if err != nil {
return nil, err
}
for _, ch := range existingChannels {
if ch.StreamIdentifier() == stream.StreamIdentifier() {
ch.stream.SetDefaultPayloadType(sctp.PayloadTypeWebRTCBinary)
return ch, nil
}
}
stream.SetDefaultPayloadType(sctp.PayloadTypeWebRTCBinary)
dc, err := Server(stream, config)
if err != nil {
return nil, err
}
return dc, nil
}
// Server accepts a data channel over an SCTP stream.
func Server(stream *sctp.Stream, config *Config) (*DataChannel, error) {
buffer := make([]byte, receiveMTU)
n, ppi, err := stream.ReadSCTP(buffer)
if err != nil {
return nil, err
}
if ppi != sctp.PayloadTypeWebRTCDCEP {
return nil, fmt.Errorf("%w %s", ErrInvalidPayloadProtocolIdentifier, ppi)
}
openMsg, err := parseExpectDataChannelOpen(buffer[:n])
if err != nil {
return nil, fmt.Errorf("failed to parse DataChannelOpen packet %w", err)
}
config.ChannelType = openMsg.ChannelType
config.Priority = openMsg.Priority
config.ReliabilityParameter = openMsg.ReliabilityParameter
config.Label = string(openMsg.Label)
config.Protocol = string(openMsg.Protocol)
dataChannel := newDataChannel(stream, config)
err = dataChannel.writeDataChannelAck()
if err != nil {
return nil, err
}
err = dataChannel.commitReliabilityParams()
if err != nil {
return nil, err
}
return dataChannel, nil
}
// Read reads a packet of len(pkt) bytes as binary data.
func (c *DataChannel) Read(pkt []byte) (int, error) {
n, _, err := c.ReadDataChannel(pkt)
return n, err
}
// ReadDataChannel reads a packet of len(pkt) bytes.
func (c *DataChannel) ReadDataChannel(pkt []byte) (int, bool, error) {
for {
n, ppi, err := c.stream.ReadSCTP(pkt)
if errors.Is(err, io.EOF) {
// When the peer sees that an incoming stream was
// reset, it also resets its corresponding outgoing stream.
if closeErr := c.stream.Close(); closeErr != nil {
return 0, false, closeErr
}
}
if err != nil {
return 0, false, err
}
if ppi == sctp.PayloadTypeWebRTCDCEP {
if err = c.handleDCEP(pkt[:n]); err != nil {
c.log.Errorf("Failed to handle DCEP: %s", err.Error())
}
continue
} else if ppi == sctp.PayloadTypeWebRTCBinaryEmpty || ppi == sctp.PayloadTypeWebRTCStringEmpty {
n = 0
}
atomic.AddUint32(&c.messagesReceived, 1)
atomic.AddUint64(&c.bytesReceived, uint64(n)) //nolint:gosec //G115
isString := ppi == sctp.PayloadTypeWebRTCString || ppi == sctp.PayloadTypeWebRTCStringEmpty
return n, isString, err
}
}
// SetReadDeadline sets a deadline for reads to return.
func (c *DataChannel) SetReadDeadline(t time.Time) error {
return c.stream.SetReadDeadline(t)
}
// SetWriteDeadline sets a deadline for writes to return,
// only available if the BlockWrite is enabled for sctp.
func (c *DataChannel) SetWriteDeadline(t time.Time) error {
return c.stream.SetWriteDeadline(t)
}
// MessagesSent returns the number of messages sent.
func (c *DataChannel) MessagesSent() uint32 {
return atomic.LoadUint32(&c.messagesSent)
}
// MessagesReceived returns the number of messages received.
func (c *DataChannel) MessagesReceived() uint32 {
return atomic.LoadUint32(&c.messagesReceived)
}
// OnOpen sets an event handler which is invoked when
// a DATA_CHANNEL_ACK message is received.
// The handler is called only on thefor the channel opened
// https://datatracker.ietf.org/doc/html/draft-ietf-rtcweb-data-protocol-09#section-5.2
func (c *DataChannel) OnOpen(f func()) {
c.mu.Lock()
c.openCompleteHandlerOnce = sync.Once{}
c.onOpenCompleteHandler = f
c.mu.Unlock()
}
func (c *DataChannel) onOpenComplete() {
c.mu.Lock()
hdlr := c.onOpenCompleteHandler
c.mu.Unlock()
if hdlr != nil {
go c.openCompleteHandlerOnce.Do(func() {
hdlr()
})
}
}
// BytesSent returns the number of bytes sent.
func (c *DataChannel) BytesSent() uint64 {
return atomic.LoadUint64(&c.bytesSent)
}
// BytesReceived returns the number of bytes received.
func (c *DataChannel) BytesReceived() uint64 {
return atomic.LoadUint64(&c.bytesReceived)
}
// StreamIdentifier returns the Stream identifier associated to the stream.
func (c *DataChannel) StreamIdentifier() uint16 {
return c.stream.StreamIdentifier()
}
func (c *DataChannel) handleDCEP(data []byte) error {
msg, err := parse(data)
if err != nil {
return fmt.Errorf("failed to parse DataChannel packet %w", err)
}
switch msg := msg.(type) {
case *channelAck:
if err := c.commitReliabilityParams(); err != nil {
return err
}
c.onOpenComplete()
default:
return fmt.Errorf("%w, wanted ACK got %v", ErrUnexpectedDataChannelType, msg)
}
return nil
}
// Write writes len(pkt) bytes from pkt as binary data.
func (c *DataChannel) Write(pkt []byte) (n int, err error) {
return c.WriteDataChannel(pkt, false)
}
// WriteDataChannel writes len(pkt) bytes from pkt.
func (c *DataChannel) WriteDataChannel(pkt []byte, isString bool) (n int, err error) {
// https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-12#section-6.6
// SCTP does not support the sending of empty user messages. Therefore,
// if an empty message has to be sent, the appropriate PPID (WebRTC
// String Empty or WebRTC Binary Empty) is used and the SCTP user
// message of one zero byte is sent. When receiving an SCTP user
// message with one of these PPIDs, the receiver MUST ignore the SCTP
// user message and process it as an empty message.
var ppi sctp.PayloadProtocolIdentifier
switch {
case !isString && len(pkt) > 0:
ppi = sctp.PayloadTypeWebRTCBinary
case !isString && len(pkt) == 0:
ppi = sctp.PayloadTypeWebRTCBinaryEmpty
case isString && len(pkt) > 0:
ppi = sctp.PayloadTypeWebRTCString
case isString && len(pkt) == 0:
ppi = sctp.PayloadTypeWebRTCStringEmpty
}
atomic.AddUint32(&c.messagesSent, 1)
atomic.AddUint64(&c.bytesSent, uint64(len(pkt)))
if len(pkt) == 0 {
_, err := c.stream.WriteSCTP([]byte{0}, ppi)
return 0, err
}
return c.stream.WriteSCTP(pkt, ppi)
}
func (c *DataChannel) writeDataChannelAck() error {
ack := channelAck{}
ackMsg, err := ack.Marshal()
if err != nil {
return fmt.Errorf("failed to marshal ChannelOpen ACK: %w", err)
}
if _, err = c.stream.WriteSCTP(ackMsg, sctp.PayloadTypeWebRTCDCEP); err != nil {
return fmt.Errorf("failed to send ChannelOpen ACK: %w", err)
}
return err
}
// Close closes the DataChannel and the underlying SCTP stream.
func (c *DataChannel) Close() error {
// https://tools.ietf.org/html/draft-ietf-rtcweb-data-channel-13#section-6.7
// Closing of a data channel MUST be signaled by resetting the
// corresponding outgoing streams [RFC6525]. This means that if one
// side decides to close the data channel, it resets the corresponding
// outgoing stream. When the peer sees that an incoming stream was
// reset, it also resets its corresponding outgoing stream. Once this
// is completed, the data channel is closed. Resetting a stream sets
// the Stream Sequence Numbers (SSNs) of the stream back to 'zero' with
// a corresponding notification to the application layer that the reset
// has been performed. Streams are available for reuse after a reset
// has been performed.
return c.stream.Close()
}
// BufferedAmount returns the number of bytes of data currently queued to be
// sent over this stream.
func (c *DataChannel) BufferedAmount() uint64 {
return c.stream.BufferedAmount()
}
// BufferedAmountLowThreshold returns the number of bytes of buffered outgoing
// data that is considered "low." Defaults to 0.
func (c *DataChannel) BufferedAmountLowThreshold() uint64 {
return c.stream.BufferedAmountLowThreshold()
}
// SetBufferedAmountLowThreshold is used to update the threshold.
// See BufferedAmountLowThreshold().
func (c *DataChannel) SetBufferedAmountLowThreshold(th uint64) {
c.stream.SetBufferedAmountLowThreshold(th)
}
// OnBufferedAmountLow sets the callback handler which would be called when the
// number of bytes of outgoing data buffered is lower than the threshold.
func (c *DataChannel) OnBufferedAmountLow(f func()) {
c.stream.OnBufferedAmountLow(f)
}
func (c *DataChannel) commitReliabilityParams() error {
switch c.Config.ChannelType {
case ChannelTypeReliable:
c.stream.SetReliabilityParams(false, sctp.ReliabilityTypeReliable, c.Config.ReliabilityParameter) // RFC 8832 sec 5.1
if c.Config.ReliabilityParameter != 0 {
c.log.Warnf("Channel type is Reliable but has a non-zero reliability parameter: %d (expected 0)",
c.Config.ReliabilityParameter)
}
case ChannelTypeReliableUnordered:
c.stream.SetReliabilityParams(true, sctp.ReliabilityTypeReliable, c.Config.ReliabilityParameter) // RFC 8832 sec 5.1
if c.Config.ReliabilityParameter != 0 {
c.log.Warnf("Channel type is ReliableUnordered but has a non-zero reliability parameter: %d (expected 0)",
c.Config.ReliabilityParameter)
}
case ChannelTypePartialReliableRexmit:
c.stream.SetReliabilityParams(false, sctp.ReliabilityTypeRexmit, c.Config.ReliabilityParameter)
case ChannelTypePartialReliableRexmitUnordered:
c.stream.SetReliabilityParams(true, sctp.ReliabilityTypeRexmit, c.Config.ReliabilityParameter)
case ChannelTypePartialReliableTimed:
c.stream.SetReliabilityParams(false, sctp.ReliabilityTypeTimed, c.Config.ReliabilityParameter)
case ChannelTypePartialReliableTimedUnordered:
c.stream.SetReliabilityParams(true, sctp.ReliabilityTypeTimed, c.Config.ReliabilityParameter)
default:
return fmt.Errorf("%w %v", ErrInvalidChannelType, c.Config.ChannelType)
}
return nil
}

29
vendor/github.com/pion/datachannel/errors.go generated vendored Normal file
View file

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package datachannel
import "errors"
var (
// ErrDataChannelMessageTooShort means that the data isn't long enough to be a valid DataChannel message.
ErrDataChannelMessageTooShort = errors.New("DataChannel message is not long enough to determine type")
// ErrInvalidPayloadProtocolIdentifier means that we got a DataChannel messages with a Payload Protocol Identifier
// we don't know how to handle.
ErrInvalidPayloadProtocolIdentifier = errors.New(
"DataChannel message Payload Protocol Identifier is value we can't handle",
)
// ErrInvalidChannelType means that the remote requested a channel type that we don't support.
ErrInvalidChannelType = errors.New("invalid Channel Type")
// ErrInvalidMessageType is returned when a DataChannel Message has a type we don't support.
ErrInvalidMessageType = errors.New("invalid Message Type")
// ErrExpectedAndActualLengthMismatch is when the declared length and actual length don't match.
ErrExpectedAndActualLengthMismatch = errors.New("expected and actual length do not match")
// ErrUnexpectedDataChannelType is when a message type does not match the expected type.
ErrUnexpectedDataChannelType = errors.New("expected and actual message type does not match")
)

92
vendor/github.com/pion/datachannel/message.go generated vendored Normal file
View file

@ -0,0 +1,92 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package datachannel
import (
"fmt"
)
// message is a parsed DataChannel message.
type message interface {
Marshal() ([]byte, error)
Unmarshal([]byte) error
String() string
}
// messageType is the first byte in a DataChannel message that specifies type.
type messageType byte
// DataChannel Message Types.
const (
dataChannelAck messageType = 0x02
dataChannelOpen messageType = 0x03
)
func (t messageType) String() string {
switch t {
case dataChannelAck:
return "DataChannelAck"
case dataChannelOpen:
return "DataChannelOpen"
default:
return fmt.Sprintf("Unknown MessageType: %d", t)
}
}
// parse accepts raw input and returns a DataChannel message.
func parse(raw []byte) (message, error) {
if len(raw) == 0 {
return nil, ErrDataChannelMessageTooShort
}
var msg message
switch messageType(raw[0]) {
case dataChannelOpen:
msg = &channelOpen{}
case dataChannelAck:
msg = &channelAck{}
default:
return nil, fmt.Errorf("%w %v", ErrInvalidMessageType, messageType(raw[0]))
}
if err := msg.Unmarshal(raw); err != nil {
return nil, err
}
return msg, nil
}
// parseExpectDataChannelOpen parses a DataChannelOpen message
// or throws an error.
func parseExpectDataChannelOpen(raw []byte) (*channelOpen, error) {
if len(raw) == 0 {
return nil, ErrDataChannelMessageTooShort
}
if actualTyp := messageType(raw[0]); actualTyp != dataChannelOpen {
return nil, fmt.Errorf("%w expected(%s) actual(%s)", ErrUnexpectedDataChannelType, actualTyp, dataChannelOpen)
}
msg := &channelOpen{}
if err := msg.Unmarshal(raw); err != nil {
return nil, err
}
return msg, nil
}
// TryMarshalUnmarshal attempts to marshal and unmarshal a message. Added for fuzzing.
func TryMarshalUnmarshal(msg []byte) int {
message, err := parse(msg)
if err != nil {
return 0
}
_, err = message.Marshal()
if err != nil {
return 0
}
return 1
}

View file

@ -0,0 +1,29 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package datachannel
// channelAck is used to ACK a DataChannel open.
type channelAck struct{}
const (
channelOpenAckLength = 4
)
// Marshal returns raw bytes for the given message.
func (c *channelAck) Marshal() ([]byte, error) {
raw := make([]byte, channelOpenAckLength)
raw[0] = uint8(dataChannelAck)
return raw, nil
}
// Unmarshal populates the struct with the given raw data.
func (c *channelAck) Unmarshal(_ []byte) error {
// Message type already checked in Parse and there is no further data
return nil
}
func (c channelAck) String() string {
return "ACK"
}

View file

@ -0,0 +1,155 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package datachannel
import (
"encoding/binary"
"fmt"
)
/*
channelOpen represents a DATA_CHANNEL_OPEN Message
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Message Type | Channel Type | Priority |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reliability Parameter |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Label Length | Protocol Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Label |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| |
| Protocol |
| |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
.
*/
type channelOpen struct {
ChannelType ChannelType
Priority uint16
ReliabilityParameter uint32
Label []byte
Protocol []byte
}
const (
channelOpenHeaderLength = 12
)
// ChannelType determines the reliability of the WebRTC DataChannel.
type ChannelType byte
// ChannelType enums.
const (
// ChannelTypeReliable determines the Data Channel provides a
// reliable in-order bi-directional communication.
ChannelTypeReliable ChannelType = 0x00
// ChannelTypeReliableUnordered determines the Data Channel
// provides a reliable unordered bi-directional communication.
ChannelTypeReliableUnordered ChannelType = 0x80
// ChannelTypePartialReliableRexmit determines the Data Channel
// provides a partially-reliable in-order bi-directional communication.
// User messages will not be retransmitted more times than specified in the Reliability Parameter.
ChannelTypePartialReliableRexmit ChannelType = 0x01
// ChannelTypePartialReliableRexmitUnordered determines
// the Data Channel provides a partial reliable unordered bi-directional communication.
// User messages will not be retransmitted more times than specified in the Reliability Parameter.
ChannelTypePartialReliableRexmitUnordered ChannelType = 0x81
// ChannelTypePartialReliableTimed determines the Data Channel
// provides a partial reliable in-order bi-directional communication.
// User messages might not be transmitted or retransmitted after
// a specified life-time given in milli- seconds in the Reliability Parameter.
// This life-time starts when providing the user message to the protocol stack.
ChannelTypePartialReliableTimed ChannelType = 0x02
// The Data Channel provides a partial reliable unordered bi-directional
// communication. User messages might not be transmitted or retransmitted
// after a specified life-time given in milli- seconds in the Reliability Parameter.
// This life-time starts when providing the user message to the protocol stack.
ChannelTypePartialReliableTimedUnordered ChannelType = 0x82
)
func (c ChannelType) String() string {
switch c {
case ChannelTypeReliable:
return "ReliableOrdered"
case ChannelTypeReliableUnordered:
return "ReliableUnordered"
case ChannelTypePartialReliableRexmit:
return "PartialReliableRexmit"
case ChannelTypePartialReliableRexmitUnordered:
return "PartialReliableRexmitUnordered"
case ChannelTypePartialReliableTimed:
return "PartialReliableTimed"
case ChannelTypePartialReliableTimedUnordered:
return "PartialReliableTimedUnordered"
}
return "Unknown"
}
// ChannelPriority enums.
const (
ChannelPriorityBelowNormal uint16 = 128
ChannelPriorityNormal uint16 = 256
ChannelPriorityHigh uint16 = 512
ChannelPriorityExtraHigh uint16 = 1024
)
// Marshal returns raw bytes for the given message.
func (c *channelOpen) Marshal() ([]byte, error) {
labelLength := len(c.Label)
protocolLength := len(c.Protocol)
totalLen := channelOpenHeaderLength + labelLength + protocolLength
raw := make([]byte, totalLen)
raw[0] = uint8(dataChannelOpen)
raw[1] = byte(c.ChannelType)
binary.BigEndian.PutUint16(raw[2:], c.Priority)
binary.BigEndian.PutUint32(raw[4:], c.ReliabilityParameter)
binary.BigEndian.PutUint16(raw[8:], uint16(labelLength)) //nolint:gosec //G115
binary.BigEndian.PutUint16(raw[10:], uint16(protocolLength)) //nolint:gosec //G115
endLabel := channelOpenHeaderLength + labelLength
copy(raw[channelOpenHeaderLength:endLabel], c.Label)
copy(raw[endLabel:endLabel+protocolLength], c.Protocol)
return raw, nil
}
// Unmarshal populates the struct with the given raw data.
func (c *channelOpen) Unmarshal(raw []byte) error {
if len(raw) < channelOpenHeaderLength {
return fmt.Errorf("%w expected(%d) actual(%d)", ErrExpectedAndActualLengthMismatch, channelOpenHeaderLength, len(raw))
}
c.ChannelType = ChannelType(raw[1])
c.Priority = binary.BigEndian.Uint16(raw[2:])
c.ReliabilityParameter = binary.BigEndian.Uint32(raw[4:])
labelLength := binary.BigEndian.Uint16(raw[8:])
protocolLength := binary.BigEndian.Uint16(raw[10:])
if expectedLen := channelOpenHeaderLength + int(labelLength) + int(protocolLength); len(raw) != expectedLen {
return fmt.Errorf("%w expected(%d) actual(%d)", ErrExpectedAndActualLengthMismatch, expectedLen, len(raw))
}
c.Label = raw[channelOpenHeaderLength : channelOpenHeaderLength+labelLength]
c.Protocol = raw[channelOpenHeaderLength+labelLength : channelOpenHeaderLength+labelLength+protocolLength]
return nil
}
func (c channelOpen) String() string {
return fmt.Sprintf(
"Open ChannelType(%s) Priority(%v) ReliabilityParameter(%d) Label(%s) Protocol(%s)",
c.ChannelType, c.Priority, c.ReliabilityParameter, string(c.Label), string(c.Protocol),
)
}

6
vendor/github.com/pion/datachannel/renovate.json generated vendored Normal file
View file

@ -0,0 +1,6 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": [
"github>pion/renovate-config"
]
}

23
vendor/github.com/pion/dtls/v3/.editorconfig generated vendored Normal file
View file

@ -0,0 +1,23 @@
# http://editorconfig.org/
# SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
# SPDX-License-Identifier: MIT
root = true
[*]
charset = utf-8
insert_final_newline = true
trim_trailing_whitespace = true
end_of_line = lf
[*.go]
indent_style = tab
indent_size = 4
[{*.yml,*.yaml}]
indent_style = space
indent_size = 2
# Makefiles always use tabs for indentation
[Makefile]
indent_style = tab

28
vendor/github.com/pion/dtls/v3/.gitignore generated vendored Normal file
View file

@ -0,0 +1,28 @@
# SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
# SPDX-License-Identifier: MIT
### JetBrains IDE ###
#####################
.idea/
### Emacs Temporary Files ###
#############################
*~
### Folders ###
###############
bin/
vendor/
node_modules/
### Files ###
#############
*.ivf
*.ogg
tags
cover.out
*.sw[poe]
*.wasm
examples/sfu-ws/cert.pem
examples/sfu-ws/key.pem
wasm_exec.js

147
vendor/github.com/pion/dtls/v3/.golangci.yml generated vendored Normal file
View file

@ -0,0 +1,147 @@
# SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
# SPDX-License-Identifier: MIT
version: "2"
linters:
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bidichk # Checks for dangerous unicode character sequences
- bodyclose # checks whether HTTP response body is closed successfully
- containedctx # containedctx is a linter that detects struct contained context.Context field
- contextcheck # check the function whether use a non-inherited context
- cyclop # checks function and package cyclomatic complexity
- decorder # check declaration order and count of types, constants, variables and functions
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
- dupl # Tool for code clone detection
- durationcheck # check for two durations multiplied together
- err113 # Golang linter to check the errors handling expressions
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
- errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted.
- errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`.
- errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13.
- exhaustive # check exhaustiveness of enum switch statements
- forbidigo # Forbids identifiers
- forcetypeassert # finds forced type assertions
- gochecknoglobals # Checks that no globals are present in Go code
- gocognit # Computes and checks the cognitive complexity of functions
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # The most opinionated Go source code linter
- gocyclo # Computes and checks the cyclomatic complexity of functions
- godot # Check if comments end in a period
- godox # Tool for detection of FIXME, TODO and other comment keywords
- goheader # Checks is file header matches to pattern
- gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod.
- goprintffuncname # Checks that printf-like functions are named with `f` at the end
- gosec # Inspects source code for security problems
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- grouper # An analyzer to analyze expression groups.
- importas # Enforces consistent import aliases
- ineffassign # Detects when assignments to existing variables are not used
- lll # Reports long lines
- maintidx # maintidx measures the maintainability index of each function.
- makezero # Finds slice declarations with non-zero initial length
- misspell # Finds commonly misspelled English words in comments
- nakedret # Finds naked returns in functions greater than a specified function length
- nestif # Reports deeply nested if statements
- nilerr # Finds the code that returns nil even if it checks that the error is not nil.
- nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value.
- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity
- noctx # noctx finds sending http request without context.Context
- predeclared # find code that shadows one of Go's predeclared identifiers
- revive # golint replacement, finds style mistakes
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
- tagliatelle # Checks the struct tags.
- thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers
- unconvert # Remove unnecessary type conversions
- unparam # Reports unused function parameters
- unused # Checks Go code for unused constants, variables, functions and types
- varnamelen # checks that the length of a variable's name matches its scope
- wastedassign # wastedassign finds wasted assignment statements
- whitespace # Tool for detection of leading and trailing whitespace
disable:
- depguard # Go linter that checks if package imports are in a list of acceptable packages
- funlen # Tool for detection of long functions
- gochecknoinits # Checks that no init functions are present in Go code
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations.
- interfacebloat # A linter that checks length of interface.
- ireturn # Accept Interfaces, Return Concrete Types
- mnd # An analyzer to detect magic numbers
- nolintlint # Reports ill-formed or insufficient nolint directives
- paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test
- prealloc # Finds slice declarations that could potentially be preallocated
- promlinter # Check Prometheus metrics naming via promlint
- rowserrcheck # checks whether Err of rows is checked successfully
- sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed.
- testpackage # linter that makes you use a separate _test package
- tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes
- wrapcheck # Checks that errors returned from external packages are wrapped
- wsl # Whitespace Linter - Forces you to use empty lines!
settings:
staticcheck:
checks:
- all
- -QF1008 # "could remove embedded field", to keep it explicit!
- -QF1003 # "could use tagged switch on enum", Cases conflicts with exhaustive!
exhaustive:
default-signifies-exhaustive: true
forbidigo:
forbid:
- pattern: ^fmt.Print(f|ln)?$
- pattern: ^log.(Panic|Fatal|Print)(f|ln)?$
- pattern: ^os.Exit$
- pattern: ^panic$
- pattern: ^print(ln)?$
- pattern: ^testing.T.(Error|Errorf|Fatal|Fatalf|Fail|FailNow)$
pkg: ^testing$
msg: use testify/assert instead
analyze-types: true
gomodguard:
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- errors
govet:
enable:
- shadow
revive:
rules:
# Prefer 'any' type alias over 'interface{}' for Go 1.18+ compatibility
- name: use-any
severity: warning
disabled: false
misspell:
locale: US
varnamelen:
max-distance: 12
min-name-length: 2
ignore-type-assert-ok: true
ignore-map-index-ok: true
ignore-chan-recv-ok: true
ignore-decls:
- i int
- n int
- w io.Writer
- r io.Reader
- b []byte
exclusions:
generated: lax
rules:
- linters:
- forbidigo
- gocognit
path: (examples|main\.go)
- linters:
- gocognit
path: _test\.go
- linters:
- forbidigo
path: cmd
formatters:
enable:
- gci # Gci control golang package import order and make it always deterministic.
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
- gofumpt # Gofumpt checks whether code was gofumpt-ed.
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports
exclusions:
generated: lax

5
vendor/github.com/pion/dtls/v3/.goreleaser.yml generated vendored Normal file
View file

@ -0,0 +1,5 @@
# SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
# SPDX-License-Identifier: MIT
builds:
- skip: true

9
vendor/github.com/pion/dtls/v3/LICENSE generated vendored Normal file
View file

@ -0,0 +1,9 @@
MIT License
Copyright (c) 2026 The Pion community <https://pion.ly>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

159
vendor/github.com/pion/dtls/v3/README.md generated vendored Normal file
View file

@ -0,0 +1,159 @@
<h1 align="center">
<br>
Pion DTLS
<br>
</h1>
<h4 align="center">A Go implementation of DTLS</h4>
<p align="center">
<a href="https://pion.ly"><img src="https://img.shields.io/badge/pion-dtls-gray.svg?longCache=true&colorB=brightgreen" alt="Pion DTLS"></a>
<a href="https://sourcegraph.com/github.com/pion/dtls"><img src="https://sourcegraph.com/github.com/pion/dtls/-/badge.svg" alt="Sourcegraph Widget"></a>
<a href="https://discord.gg/PngbdqpFbt"><img src="https://img.shields.io/badge/join-us%20on%20discord-gray.svg?longCache=true&logo=discord&colorB=brightblue" alt="join us on Discord"></a> <a href="https://bsky.app/profile/pion.ly"><img src="https://img.shields.io/badge/follow-us%20on%20bluesky-gray.svg?longCache=true&logo=bluesky&colorB=brightblue" alt="Follow us on Bluesky"></a>
<br>
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/pion/dtls/test.yaml">
<a href="https://pkg.go.dev/github.com/pion/dtls/v3"><img src="https://pkg.go.dev/badge/github.com/pion/dtls/v3.svg" alt="Go Reference"></a>
<a href="https://codecov.io/gh/pion/dtls"><img src="https://codecov.io/gh/pion/dtls/branch/master/graph/badge.svg" alt="Coverage Status"></a>
<a href="https://goreportcard.com/report/github.com/pion/dtls/v3"><img src="https://goreportcard.com/badge/github.com/pion/dtls/v3" alt="Go Report Card"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
</p>
<br>
Native [DTLS 1.2][rfc6347] implementation in the Go programming language.
A long term goal is a professional security review, and maybe an inclusion in stdlib.
### RFCs
#### Implemented
- **RFC 6347**: [Datagram Transport Layer Security Version 1.2][rfc6347]
- **RFC 5705**: [Keying Material Exporters for Transport Layer Security (TLS)][rfc5705]
- **RFC 7627**: [Transport Layer Security (TLS) - Session Hash and Extended Master Secret Extension][rfc7627]
- **RFC 7301**: [Transport Layer Security (TLS) - Application-Layer Protocol Negotiation Extension][rfc7301]
[rfc5289]: https://tools.ietf.org/html/rfc5289
[rfc5487]: https://tools.ietf.org/html/rfc5487
[rfc5489]: https://tools.ietf.org/html/rfc5489
[rfc5705]: https://tools.ietf.org/html/rfc5705
[rfc6347]: https://tools.ietf.org/html/rfc6347
[rfc6655]: https://tools.ietf.org/html/rfc6655
[rfc7301]: https://tools.ietf.org/html/rfc7301
[rfc7627]: https://tools.ietf.org/html/rfc7627
[rfc8422]: https://tools.ietf.org/html/rfc8422
[rfc9147]: https://tools.ietf.org/html/rfc9147
### Goals/Progress
This will only be targeting DTLS 1.2, and the most modern/common cipher suites.
We would love contributions that fall under the 'Planned Features' and any bug fixes!
#### Current features
* DTLS 1.2 Client/Server
* Key Exchange via ECDHE(curve25519, nistp256, nistp384) and PSK
* Packet loss and re-ordering is handled during handshaking
* Key export ([RFC 5705][rfc5705])
* Serialization and Resumption of sessions
* Extended Master Secret extension ([RFC 7627][rfc7627])
* ALPN extension ([RFC 7301][rfc7301])
#### Supported ciphers
##### ECDHE
* TLS_ECDHE_ECDSA_WITH_AES_128_CCM ([RFC 6655][rfc6655])
* TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 ([RFC 6655][rfc6655])
* TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ([RFC 5289][rfc5289])
* TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ([RFC 5289][rfc5289])
* TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 ([RFC 5289][rfc5289])
* TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ([RFC 5289][rfc5289])
* TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA ([RFC 8422][rfc8422])
* TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA ([RFC 8422][rfc8422])
##### PSK
* TLS_PSK_WITH_AES_128_CCM ([RFC 6655][rfc6655])
* TLS_PSK_WITH_AES_128_CCM_8 ([RFC 6655][rfc6655])
* TLS_PSK_WITH_AES_256_CCM_8 ([RFC 6655][rfc6655])
* TLS_PSK_WITH_AES_128_GCM_SHA256 ([RFC 5487][rfc5487])
* TLS_PSK_WITH_AES_128_CBC_SHA256 ([RFC 5487][rfc5487])
##### ECDHE & PSK
* TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 ([RFC 5489][rfc5489])
#### Planned Features
* DTLS 1.3 ([RFC 9147][rfc9147])
* Chacha20Poly1305
#### Excluded Features
* DTLS 1.0
* Renegotiation
* Compression
### Using
This library needs at least Go 1.21, and you should have [Go modules
enabled](https://github.com/golang/go/wiki/Modules).
#### Pion DTLS
For a DTLS 1.2 Server that listens on 127.0.0.1:4444
```sh
go run examples/listen/selfsign/main.go
```
For a DTLS 1.2 Client that connects to 127.0.0.1:4444
```sh
go run examples/dial/selfsign/main.go
```
#### OpenSSL
Pion DTLS can connect to itself and OpenSSL.
```
// Generate a certificate
openssl ecparam -out key.pem -name prime256v1 -genkey
openssl req -new -sha256 -key key.pem -out server.csr
openssl x509 -req -sha256 -days 365 -in server.csr -signkey key.pem -out cert.pem
// Use with examples/dial/selfsign/main.go
openssl s_server -dtls1_2 -cert cert.pem -key key.pem -accept 4444
// Use with examples/listen/selfsign/main.go
openssl s_client -dtls1_2 -connect 127.0.0.1:4444 -debug -cert cert.pem -key key.pem
```
### Using with PSK
Pion DTLS also comes with examples that do key exchange via PSK
#### Pion DTLS
```sh
go run examples/listen/psk/main.go
```
```sh
go run examples/dial/psk/main.go
```
#### OpenSSL
```
// Use with examples/dial/psk/main.go
openssl s_server -dtls1_2 -accept 4444 -nocert -psk abc123 -cipher PSK-AES128-CCM8
// Use with examples/listen/psk/main.go
openssl s_client -dtls1_2 -connect 127.0.0.1:4444 -psk abc123 -cipher PSK-AES128-CCM8
```
### Community
Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt).
Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news.
We are always looking to support **your projects**. Please reach out if you have something to build!
If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly)
### Contributing
Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible
### Funding
<a href="https://nlnet.nl/"><img src="https://nlnet.nl/logo/banner.svg" alt="NLnet foundation logo" width="200"></a>
<a href="https://nlnet.nl/commonsfund/"><img src="https://nlnet.nl/image/logos/NGI0Core_tag.svg" alt="NLnet foundation logo" width="200"></a>
The DTLS 1.3 implementation in this project is funded through the [NGI0 Commons Fund](https://nlnet.nl/commonsfund), a fund established by [NLnet](https://nlnet.nl/) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu/) programme, under the aegis of [DG Communications Networks, Content and Technology](https://commission.europa.eu/about-european-commission/departments-and-executive-agencies/communications-networks-content-and-technology_en) under grant agreement No [101135429](https://cordis.europa.eu/project/id/101135429). Additional funding is made available by the [Swiss State Secretariat for Education, Research and Innovation](https://www.sbfi.admin.ch/sbfi/en/home.html) (SERI). Learn more on the [NLnet project page](https://nlnet.nl/project/PION-DTLS1.3/).
### License
MIT License - see [LICENSE](LICENSE) for full text

167
vendor/github.com/pion/dtls/v3/certificate.go generated vendored Normal file
View file

@ -0,0 +1,167 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"bytes"
"crypto/tls"
"crypto/x509"
"fmt"
"strings"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
)
// ClientHelloInfo contains information from a ClientHello message in order to
// guide application logic in the GetCertificate.
type ClientHelloInfo struct {
// ServerName indicates the name of the server requested by the client
// in order to support virtual hosting. ServerName is only set if the
// client is using SNI (see RFC 4366, Section 3.1).
ServerName string
// CipherSuites lists the CipherSuites supported by the client (e.g.
// TLS_AES_128_GCM_SHA256, TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256).
CipherSuites []CipherSuiteID
// RandomBytes stores the client hello random bytes
RandomBytes [handshake.RandomBytesLength]byte
}
// CertificateRequestInfo contains information from a server's
// CertificateRequest message, which is used to demand a certificate and proof
// of control from a client.
type CertificateRequestInfo struct {
// AcceptableCAs contains zero or more, DER-encoded, X.501
// Distinguished Names. These are the names of root or intermediate CAs
// that the server wishes the returned certificate to be signed by. An
// empty slice indicates that the server has no preference.
AcceptableCAs [][]byte
}
// SupportsCertificate returns nil if the provided certificate is supported by
// the server that sent the CertificateRequest. Otherwise, it returns an error
// describing the reason for the incompatibility.
// NOTE: original src:
// https://github.com/golang/go/blob/29b9a328d268d53833d2cc063d1d8b4bf6852675/src/crypto/tls/common.go#L1273
func (cri *CertificateRequestInfo) SupportsCertificate(c *tls.Certificate) error {
if len(cri.AcceptableCAs) == 0 {
return nil
}
for j, cert := range c.Certificate {
x509Cert := c.Leaf
// Parse the certificate if this isn't the leaf node, or if
// chain.Leaf was nil.
if j != 0 || x509Cert == nil {
var err error
if x509Cert, err = x509.ParseCertificate(cert); err != nil {
return fmt.Errorf("failed to parse certificate #%d in the chain: %w", j, err)
}
}
for _, ca := range cri.AcceptableCAs {
if bytes.Equal(x509Cert.RawIssuer, ca) {
return nil
}
}
}
return errNotAcceptableCertificateChain
}
func (c *handshakeConfig) setNameToCertificateLocked() {
nameToCertificate := make(map[string]*tls.Certificate)
for i := range c.localCertificates {
cert := &c.localCertificates[i]
x509Cert := cert.Leaf
if x509Cert == nil {
var parseErr error
x509Cert, parseErr = x509.ParseCertificate(cert.Certificate[0])
if parseErr != nil {
continue
}
}
if len(x509Cert.Subject.CommonName) > 0 {
nameToCertificate[strings.ToLower(x509Cert.Subject.CommonName)] = cert
}
for _, san := range x509Cert.DNSNames {
nameToCertificate[strings.ToLower(san)] = cert
}
}
c.nameToCertificate = nameToCertificate
}
//nolint:cyclop
func (c *handshakeConfig) getCertificate(clientHelloInfo *ClientHelloInfo) (*tls.Certificate, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.localGetCertificate != nil &&
(len(c.localCertificates) == 0 || len(clientHelloInfo.ServerName) > 0) {
cert, err := c.localGetCertificate(clientHelloInfo)
if cert != nil || err != nil {
return cert, err
}
}
if c.nameToCertificate == nil {
c.setNameToCertificateLocked()
}
if len(c.localCertificates) == 0 {
return nil, errNoCertificates
}
if len(c.localCertificates) == 1 {
// There's only one choice, so no point doing any work.
return &c.localCertificates[0], nil
}
if len(clientHelloInfo.ServerName) == 0 {
return &c.localCertificates[0], nil
}
name := strings.TrimRight(strings.ToLower(clientHelloInfo.ServerName), ".")
if cert, ok := c.nameToCertificate[name]; ok {
return cert, nil
}
// try replacing labels in the name with wildcards until we get a
// match.
labels := strings.Split(name, ".")
for i := range labels {
labels[i] = "*"
candidate := strings.Join(labels, ".")
if cert, ok := c.nameToCertificate[candidate]; ok {
return cert, nil
}
}
// If nothing matches, return the first certificate.
return &c.localCertificates[0], nil
}
// NOTE: original src:
// https://github.com/golang/go/blob/29b9a328d268d53833d2cc063d1d8b4bf6852675/src/crypto/tls/handshake_client.go#L974
func (c *handshakeConfig) getClientCertificate(cri *CertificateRequestInfo) (*tls.Certificate, error) {
c.mu.Lock()
defer c.mu.Unlock()
if c.localGetClientCertificate != nil {
return c.localGetClientCertificate(cri)
}
for i := range c.localCertificates {
chain := c.localCertificates[i]
if err := cri.SupportsCertificate(&chain); err != nil {
continue
}
return &chain, nil
}
// No acceptable certificate found. Don't send a certificate.
return new(tls.Certificate), nil
}

295
vendor/github.com/pion/dtls/v3/cipher_suite.go generated vendored Normal file
View file

@ -0,0 +1,295 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/tls"
"fmt"
"hash"
"github.com/pion/dtls/v3/internal/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
// CipherSuiteID is an ID for our supported CipherSuites.
type CipherSuiteID = ciphersuite.ID
// Supported Cipher Suites.
const (
// nolint: godot
// AES-128-CCM
TLS_ECDHE_ECDSA_WITH_AES_128_CCM CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM // nolint: revive,staticcheck,lll
TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 // nolint: revive,staticcheck,lll
// nolint: godot
// AES-128-GCM-SHA256
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 // nolint: revive,staticcheck,lll
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 CipherSuiteID = ciphersuite.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 // nolint: revive,staticcheck,lll
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 // nolint: revive,staticcheck,lll
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 CipherSuiteID = ciphersuite.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 // nolint: revive,staticcheck,lll
// nolint: godot
// AES-256-CBC-SHA
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA CipherSuiteID = ciphersuite.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA // nolint: revive,staticcheck,lll
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA CipherSuiteID = ciphersuite.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA // nolint: revive,staticcheck,lll
TLS_PSK_WITH_AES_128_CCM CipherSuiteID = ciphersuite.TLS_PSK_WITH_AES_128_CCM // nolint: revive,staticcheck,lll
TLS_PSK_WITH_AES_128_CCM_8 CipherSuiteID = ciphersuite.TLS_PSK_WITH_AES_128_CCM_8 // nolint: revive,staticcheck,lll
TLS_PSK_WITH_AES_256_CCM_8 CipherSuiteID = ciphersuite.TLS_PSK_WITH_AES_256_CCM_8 // nolint: revive,staticcheck,lll
TLS_PSK_WITH_AES_128_GCM_SHA256 CipherSuiteID = ciphersuite.TLS_PSK_WITH_AES_128_GCM_SHA256 // nolint: revive,staticcheck,lll
TLS_PSK_WITH_AES_128_CBC_SHA256 CipherSuiteID = ciphersuite.TLS_PSK_WITH_AES_128_CBC_SHA256 // nolint: revive,staticcheck,lll
TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 CipherSuiteID = ciphersuite.TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 // nolint: revive,staticcheck,lll
)
// CipherSuiteAuthenticationType controls what authentication method is using during the handshake for a CipherSuite.
type CipherSuiteAuthenticationType = ciphersuite.AuthenticationType
// AuthenticationType Enums.
const (
CipherSuiteAuthenticationTypeCertificate CipherSuiteAuthenticationType = ciphersuite.AuthenticationTypeCertificate
CipherSuiteAuthenticationTypePreSharedKey CipherSuiteAuthenticationType = ciphersuite.AuthenticationTypePreSharedKey
CipherSuiteAuthenticationTypeAnonymous CipherSuiteAuthenticationType = ciphersuite.AuthenticationTypeAnonymous
)
// CipherSuiteKeyExchangeAlgorithm controls what exchange algorithm is using during the handshake for a CipherSuite.
type CipherSuiteKeyExchangeAlgorithm = ciphersuite.KeyExchangeAlgorithm
// CipherSuiteKeyExchangeAlgorithm Bitmask.
const (
CipherSuiteKeyExchangeAlgorithmNone CipherSuiteKeyExchangeAlgorithm = ciphersuite.KeyExchangeAlgorithmNone
CipherSuiteKeyExchangeAlgorithmPsk CipherSuiteKeyExchangeAlgorithm = ciphersuite.KeyExchangeAlgorithmPsk
CipherSuiteKeyExchangeAlgorithmEcdhe CipherSuiteKeyExchangeAlgorithm = ciphersuite.KeyExchangeAlgorithmEcdhe
)
var _ = allCipherSuites() // Necessary until this function isn't only used by Go 1.14
// CipherSuite is an interface that all DTLS CipherSuites must satisfy.
type CipherSuite interface {
// String of CipherSuite, only used for logging
String() string
// ID of CipherSuite.
ID() CipherSuiteID
// What type of Certificate does this CipherSuite use
CertificateType() clientcertificate.Type
// What Hash function is used during verification
HashFunc() func() hash.Hash
// AuthenticationType controls what authentication method is using during the handshake
AuthenticationType() CipherSuiteAuthenticationType
// KeyExchangeAlgorithm controls what exchange algorithm is using during the handshake
KeyExchangeAlgorithm() CipherSuiteKeyExchangeAlgorithm
// ECC (Elliptic Curve Cryptography) determines whether ECC extesions will be send during handshake.
// https://datatracker.ietf.org/doc/html/rfc4492#page-10
ECC() bool
// Called when keying material has been generated, should initialize the internal cipher
Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error
IsInitialized() bool
Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error)
Decrypt(h recordlayer.Header, in []byte) ([]byte, error)
}
// CipherSuiteName provides the same functionality as tls.CipherSuiteName
// that appeared first in Go 1.14.
//
// Our implementation differs slightly in that it takes in a CiperSuiteID,
// like the rest of our library, instead of a uint16 like crypto/tls.
func CipherSuiteName(id CipherSuiteID) string {
suite := cipherSuiteForID(id, nil)
if suite != nil {
return suite.String()
}
return fmt.Sprintf("0x%04X", uint16(id))
}
// Taken from https://www.iana.org/assignments/tls-parameters/tls-parameters.xml
// A cipherSuite is a specific combination of key agreement, cipher and MAC
// function.
func cipherSuiteForID(id CipherSuiteID, customCiphers func() []CipherSuite) CipherSuite { //nolint:cyclop
switch id { //nolint:exhaustive
case TLS_ECDHE_ECDSA_WITH_AES_128_CCM:
return ciphersuite.NewTLSEcdheEcdsaWithAes128Ccm()
case TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8:
return ciphersuite.NewTLSEcdheEcdsaWithAes128Ccm8()
case TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
return &ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{}
case TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
return &ciphersuite.TLSEcdheRsaWithAes128GcmSha256{}
case TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:
return &ciphersuite.TLSEcdheEcdsaWithAes256CbcSha{}
case TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:
return &ciphersuite.TLSEcdheRsaWithAes256CbcSha{}
case TLS_PSK_WITH_AES_128_CCM:
return ciphersuite.NewTLSPskWithAes128Ccm()
case TLS_PSK_WITH_AES_128_CCM_8:
return ciphersuite.NewTLSPskWithAes128Ccm8()
case TLS_PSK_WITH_AES_256_CCM_8:
return ciphersuite.NewTLSPskWithAes256Ccm8()
case TLS_PSK_WITH_AES_128_GCM_SHA256:
return &ciphersuite.TLSPskWithAes128GcmSha256{}
case TLS_PSK_WITH_AES_128_CBC_SHA256:
return &ciphersuite.TLSPskWithAes128CbcSha256{}
case TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
return &ciphersuite.TLSEcdheEcdsaWithAes256GcmSha384{}
case TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:
return &ciphersuite.TLSEcdheRsaWithAes256GcmSha384{}
case TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256:
return ciphersuite.NewTLSEcdhePskWithAes128CbcSha256()
}
if customCiphers != nil {
for _, c := range customCiphers() {
if c.ID() == id {
return c
}
}
}
return nil
}
// CipherSuites we support in order of preference.
func defaultCipherSuites() []CipherSuite {
return []CipherSuite{
&ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{},
&ciphersuite.TLSEcdheRsaWithAes128GcmSha256{},
&ciphersuite.TLSEcdheEcdsaWithAes256CbcSha{},
&ciphersuite.TLSEcdheRsaWithAes256CbcSha{},
&ciphersuite.TLSEcdheEcdsaWithAes256GcmSha384{},
&ciphersuite.TLSEcdheRsaWithAes256GcmSha384{},
}
}
func allCipherSuites() []CipherSuite {
return []CipherSuite{
ciphersuite.NewTLSEcdheEcdsaWithAes128Ccm(),
ciphersuite.NewTLSEcdheEcdsaWithAes128Ccm8(),
&ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256{},
&ciphersuite.TLSEcdheRsaWithAes128GcmSha256{},
&ciphersuite.TLSEcdheEcdsaWithAes256CbcSha{},
&ciphersuite.TLSEcdheRsaWithAes256CbcSha{},
ciphersuite.NewTLSPskWithAes128Ccm(),
ciphersuite.NewTLSPskWithAes128Ccm8(),
ciphersuite.NewTLSPskWithAes256Ccm8(),
&ciphersuite.TLSPskWithAes128GcmSha256{},
&ciphersuite.TLSEcdheEcdsaWithAes256GcmSha384{},
&ciphersuite.TLSEcdheRsaWithAes256GcmSha384{},
}
}
func cipherSuiteIDs(cipherSuites []CipherSuite) []uint16 {
rtrn := []uint16{}
for _, c := range cipherSuites {
rtrn = append(rtrn, uint16(c.ID()))
}
return rtrn
}
//nolint:cyclop
func parseCipherSuites(
userSelectedSuites []CipherSuiteID,
customCipherSuites func() []CipherSuite,
includeCertificateSuites, includePSKSuites bool,
) ([]CipherSuite, error) {
cipherSuitesForIDs := func(ids []CipherSuiteID) ([]CipherSuite, error) {
cipherSuites := []CipherSuite{}
for _, id := range ids {
c := cipherSuiteForID(id, nil)
if c == nil {
return nil, &invalidCipherSuiteError{id}
}
cipherSuites = append(cipherSuites, c)
}
return cipherSuites, nil
}
var (
cipherSuites []CipherSuite
err error
i int
)
if userSelectedSuites != nil {
cipherSuites, err = cipherSuitesForIDs(userSelectedSuites)
if err != nil {
return nil, err
}
} else {
cipherSuites = defaultCipherSuites()
}
// Put CustomCipherSuites before ID selected suites
if customCipherSuites != nil {
cipherSuites = append(customCipherSuites(), cipherSuites...)
}
var foundCertificateSuite, foundPSKSuite, foundAnonymousSuite bool
for _, c := range cipherSuites {
switch {
case includeCertificateSuites && c.AuthenticationType() == CipherSuiteAuthenticationTypeCertificate:
foundCertificateSuite = true
case includePSKSuites && c.AuthenticationType() == CipherSuiteAuthenticationTypePreSharedKey:
foundPSKSuite = true
case c.AuthenticationType() == CipherSuiteAuthenticationTypeAnonymous:
foundAnonymousSuite = true
default:
continue
}
cipherSuites[i] = c
i++
}
switch {
case includeCertificateSuites && !foundCertificateSuite && !foundAnonymousSuite:
return nil, errNoAvailableCertificateCipherSuite
case includePSKSuites && !foundPSKSuite:
return nil, errNoAvailablePSKCipherSuite
case i == 0:
return nil, errNoAvailableCipherSuites
}
return cipherSuites[:i], nil
}
func filterCipherSuitesForCertificate(cert *tls.Certificate, cipherSuites []CipherSuite) []CipherSuite {
if cert == nil || cert.PrivateKey == nil {
return cipherSuites
}
signer, ok := cert.PrivateKey.(crypto.Signer)
if !ok {
return cipherSuites
}
var certType clientcertificate.Type
switch signer.Public().(type) {
case ed25519.PublicKey, *ecdsa.PublicKey:
certType = clientcertificate.ECDSASign
case *rsa.PublicKey:
certType = clientcertificate.RSASign
}
filtered := []CipherSuite{}
for _, c := range cipherSuites {
if c.AuthenticationType() != CipherSuiteAuthenticationTypeCertificate || certType == c.CertificateType() {
filtered = append(filtered, c)
}
}
return filtered
}

46
vendor/github.com/pion/dtls/v3/cipher_suite_go114.go generated vendored Normal file
View file

@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
//go:build go1.14
// +build go1.14
package dtls
import (
"crypto/tls"
)
// VersionDTLS12 is the DTLS version in the same style as
// VersionTLSXX from crypto/tls.
const VersionDTLS12 = 0xfefd
// Convert from our cipherSuite interface to a tls.CipherSuite struct.
func toTLSCipherSuite(c CipherSuite) *tls.CipherSuite {
return &tls.CipherSuite{
ID: uint16(c.ID()),
Name: c.String(),
SupportedVersions: []uint16{VersionDTLS12},
Insecure: false,
}
}
// CipherSuites returns a list of cipher suites currently implemented by this
// package, excluding those with security issues, which are returned by
// InsecureCipherSuites.
func CipherSuites() []*tls.CipherSuite {
suites := allCipherSuites()
res := make([]*tls.CipherSuite, len(suites))
for i, c := range suites {
res[i] = toTLSCipherSuite(c)
}
return res
}
// InsecureCipherSuites returns a list of cipher suites currently implemented by
// this package and which have security issues.
func InsecureCipherSuites() []*tls.CipherSuite {
var res []*tls.CipherSuite
return res
}

22
vendor/github.com/pion/dtls/v3/codecov.yml generated vendored Normal file
View file

@ -0,0 +1,22 @@
#
# DO NOT EDIT THIS FILE
#
# It is automatically copied from https://github.com/pion/.goassets repository.
#
# SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
# SPDX-License-Identifier: MIT
coverage:
status:
project:
default:
# Allow decreasing 2% of total coverage to avoid noise.
threshold: 2%
patch:
default:
target: 70%
only_pulls: true
ignore:
- "examples/*"
- "examples/**/*"

12
vendor/github.com/pion/dtls/v3/compression_method.go generated vendored Normal file
View file

@ -0,0 +1,12 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import "github.com/pion/dtls/v3/pkg/protocol"
func defaultCompressionMethods() []*protocol.CompressionMethod {
return []*protocol.CompressionMethod{
{},
}
}

306
vendor/github.com/pion/dtls/v3/config.go generated vendored Normal file
View file

@ -0,0 +1,306 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rsa"
"crypto/tls"
"crypto/x509"
"io"
"net"
"time"
"github.com/pion/dtls/v3/pkg/crypto/elliptic"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/logging"
)
const keyLogLabelTLS12 = "CLIENT_RANDOM"
// Config is used to configure a DTLS client or server.
// After a Config is passed to a DTLS function it must not be modified.
//
// Deprecated: prefer the options-based APIs (`*WithOptions`) to construct immutable configs,
// This will be removed in the next major version.
type Config struct { //nolint:dupl
// Certificates contains certificate chain to present to the other side of the connection.
// Server MUST set this if PSK is non-nil
// client SHOULD sets this so CertificateRequests can be handled if PSK is non-nil
Certificates []tls.Certificate
// CipherSuites is a list of supported cipher suites.
// If CipherSuites is nil, a default list is used
CipherSuites []CipherSuiteID
// CustomCipherSuites is a list of CipherSuites that can be
// provided by the user. This allow users to user Ciphers that are reserved
// for private usage.
CustomCipherSuites func() []CipherSuite
// SignatureSchemes contains the signature and hash schemes that the peer requests to verify.
SignatureSchemes []tls.SignatureScheme
// CertificateSignatureSchemes contains the signature and hash schemes that may be used
// in digital signatures for X.509 certificates. If not set, the signature_algorithms_cert
// extension is not sent, and SignatureSchemes is used for both handshake signatures and
// certificate chain validation, as specified in RFC 8446 Section 4.2.3.
CertificateSignatureSchemes []tls.SignatureScheme
// SRTPProtectionProfiles are the supported protection profiles
// Clients will send this via use_srtp and assert that the server properly responds
// Servers will assert that clients send one of these profiles and will respond as needed
SRTPProtectionProfiles []SRTPProtectionProfile
// SRTPMasterKeyIdentifier value (if any) is sent via the use_srtp
// extension for Clients and Servers
SRTPMasterKeyIdentifier []byte
// ClientAuth determines the server's policy for
// TLS Client Authentication. The default is NoClientCert.
ClientAuth ClientAuthType
// RequireExtendedMasterSecret determines if the "Extended Master Secret" extension
// should be disabled, requested, or required (default requested).
ExtendedMasterSecret ExtendedMasterSecretType
// FlightInterval controls how often we send outbound handshake messages
// defaults to time.Second
FlightInterval time.Duration
// DisableRetransmitBackoff can be used to the disable the backoff feature
// when sending outbound messages as specified in RFC 4347 4.2.4.1
DisableRetransmitBackoff bool
// PSK sets the pre-shared key used by this DTLS connection
// If PSK is non-nil only PSK CipherSuites will be used
PSK PSKCallback
PSKIdentityHint []byte
// InsecureSkipVerify controls whether a client verifies the
// server's certificate chain and host name.
// If InsecureSkipVerify is true, TLS accepts any certificate
// presented by the server and any host name in that certificate.
// In this mode, TLS is susceptible to man-in-the-middle attacks.
// This should be used only for testing.
InsecureSkipVerify bool
// InsecureHashes allows the use of hashing algorithms that are known
// to be vulnerable.
InsecureHashes bool
// VerifyPeerCertificate, if not nil, is called after normal
// certificate verification by either a client or server. It
// receives the certificate provided by the peer and also a flag
// that tells if normal verification has succeedded. If it returns a
// non-nil error, the handshake is aborted and that error results.
//
// If normal verification fails then the handshake will abort before
// considering this callback. If normal verification is disabled by
// setting InsecureSkipVerify, or (for a server) when ClientAuth is
// RequestClientCert or RequireAnyClientCert, then this callback will
// be considered but the verifiedChains will always be nil.
VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
// VerifyConnection, if not nil, is called after normal certificate
// verification/PSK and after VerifyPeerCertificate by either a TLS client
// or server. If it returns a non-nil error, the handshake is aborted
// and that error results.
//
// If normal verification fails then the handshake will abort before
// considering this callback. This callback will run for all connections
// regardless of InsecureSkipVerify or ClientAuth settings.
VerifyConnection func(*State) error
// RootCAs defines the set of root certificate authorities
// that one peer uses when verifying the other peer's certificates.
// If RootCAs is nil, TLS uses the host's root CA set.
RootCAs *x509.CertPool
// ClientCAs defines the set of root certificate authorities
// that servers use if required to verify a client certificate
// by the policy in ClientAuth.
ClientCAs *x509.CertPool
// ServerName is used to verify the hostname on the returned
// certificates unless InsecureSkipVerify is given.
ServerName string
LoggerFactory logging.LoggerFactory
// MTU is the length at which handshake messages will be fragmented to
// fit within the maximum transmission unit (default is 1200 bytes)
MTU int
// ReplayProtectionWindow is the size of the replay attack protection window.
// Duplication of the sequence number is checked in this window size.
// Packet with sequence number older than this value compared to the latest
// accepted packet will be discarded. (default is 64)
ReplayProtectionWindow int
// KeyLogWriter optionally specifies a destination for TLS master secrets
// in NSS key log format that can be used to allow external programs
// such as Wireshark to decrypt TLS connections.
// See https://developer.mozilla.org/en-US/docs/Mozilla/Projects/NSS/Key_Log_Format.
// Use of KeyLogWriter compromises security and should only be
// used for debugging.
KeyLogWriter io.Writer
// SessionStore is the container to store session for resumption.
SessionStore SessionStore
// List of application protocols the peer supports, for ALPN
SupportedProtocols []string
// List of Elliptic Curves to use
//
// If an ECC ciphersuite is configured and EllipticCurves is empty
// it will default to X25519, P-256, P-384 in this specific order.
EllipticCurves []elliptic.Curve
// GetCertificate returns a Certificate based on the given
// ClientHelloInfo. It will only be called if the client supplies SNI
// information or if Certificates is empty.
//
// If GetCertificate is nil or returns nil, then the certificate is
// retrieved from NameToCertificate. If NameToCertificate is nil, the
// best element of Certificates will be used.
GetCertificate func(*ClientHelloInfo) (*tls.Certificate, error)
// GetClientCertificate, if not nil, is called when a server requests a
// certificate from a client. If set, the contents of Certificates will
// be ignored.
//
// If GetClientCertificate returns an error, the handshake will be
// aborted and that error will be returned. Otherwise
// GetClientCertificate must return a non-nil Certificate. If
// Certificate.Certificate is empty then no certificate will be sent to
// the server. If this is unacceptable to the server then it may abort
// the handshake.
GetClientCertificate func(*CertificateRequestInfo) (*tls.Certificate, error)
// InsecureSkipVerifyHello, if true and when acting as server, allow client to
// skip hello verify phase and receive ServerHello after initial ClientHello.
// This have implication on DoS attack resistance.
InsecureSkipVerifyHello bool
// ConnectionIDGenerator generates connection identifiers that should be
// sent by the remote party if it supports the DTLS Connection Identifier
// extension, as determined during the handshake. Generated connection
// identifiers must always have the same length. Returning a zero-length
// connection identifier indicates that the local party supports sending
// connection identifiers but does not require the remote party to send
// them. A nil ConnectionIDGenerator indicates that connection identifiers
// are not supported.
// https://datatracker.ietf.org/doc/html/rfc9146
ConnectionIDGenerator func() []byte
// PaddingLengthGenerator generates the number of padding bytes used to
// inflate ciphertext size in order to obscure content size from observers.
// The length of the content is passed to the generator such that both
// deterministic and random padding schemes can be applied while not
// exceeding maximum record size.
// If no PaddingLengthGenerator is specified, padding will not be applied.
// https://datatracker.ietf.org/doc/html/rfc9146#section-4
PaddingLengthGenerator func(uint) uint
// HelloRandomBytesGenerator generates custom client hello random bytes.
HelloRandomBytesGenerator func() [handshake.RandomBytesLength]byte
// Handshake hooks: hooks can be used for testing invalid messages,
// mimicking other implementations or randomizing fields, which is valuable
// for applications that need censorship-resistance by making
// fingerprinting more difficult.
// ClientHelloMessageHook, if not nil, is called when a Client Hello message is sent
// from a client. The returned handshake message replaces the original message.
ClientHelloMessageHook func(handshake.MessageClientHello) handshake.Message
// ServerHelloMessageHook, if not nil, is called when a Server Hello message is sent
// from a server. The returned handshake message replaces the original message.
ServerHelloMessageHook func(handshake.MessageServerHello) handshake.Message
// CertificateRequestMessageHook, if not nil, is called when a Certificate Request
// message is sent from a server. The returned handshake message replaces the original message.
CertificateRequestMessageHook func(handshake.MessageCertificateRequest) handshake.Message
// OnConnectionAttempt is fired Whenever a connection attempt is made,
// the server or application can call this callback function.
// The callback function can then implement logic to handle the connection attempt, such as logging the attempt,
// checking against a list of blocked IPs, or counting the attempts to prevent brute force attacks.
// If the callback function returns an error, the connection attempt will be aborted.
OnConnectionAttempt func(net.Addr) error
}
func (c *Config) includeCertificateSuites() bool {
return c.PSK == nil || len(c.Certificates) > 0 || c.GetCertificate != nil || c.GetClientCertificate != nil
}
const defaultMTU = 1200 // bytes
var defaultCurves = []elliptic.Curve{elliptic.X25519, elliptic.P256, elliptic.P384} //nolint:gochecknoglobals
// PSKCallback is called once we have the remote's PSKIdentityHint.
// If the remote provided none it will be nil.
type PSKCallback func([]byte) ([]byte, error)
// ClientAuthType declares the policy the server will follow for
// TLS Client Authentication.
type ClientAuthType int
// ClientAuthType enums.
const (
NoClientCert ClientAuthType = iota
RequestClientCert
RequireAnyClientCert
VerifyClientCertIfGiven
RequireAndVerifyClientCert
)
// ExtendedMasterSecretType declares the policy the client and server
// will follow for the Extended Master Secret extension.
type ExtendedMasterSecretType int
// ExtendedMasterSecretType enums.
const (
RequestExtendedMasterSecret ExtendedMasterSecretType = iota
RequireExtendedMasterSecret
DisableExtendedMasterSecret
)
func validateConfig(config *Config) error { //nolint:cyclop
switch {
case config == nil:
return errNoConfigProvided
case config.PSKIdentityHint != nil && config.PSK == nil:
return errIdentityNoPSK
}
for _, cert := range config.Certificates {
if cert.Certificate == nil {
return errInvalidCertificate
}
if cert.PrivateKey != nil {
signer, ok := cert.PrivateKey.(crypto.Signer)
if !ok {
return errInvalidPrivateKey
}
switch signer.Public().(type) {
case ed25519.PublicKey:
case *ecdsa.PublicKey:
case *rsa.PublicKey:
default:
return errInvalidPrivateKey
}
}
}
_, err := parseCipherSuites(
config.CipherSuites, config.CustomCipherSuites, config.includeCertificateSuites(), config.PSK != nil,
)
return err
}

1397
vendor/github.com/pion/dtls/v3/conn.go generated vendored Normal file

File diff suppressed because it is too large Load diff

105
vendor/github.com/pion/dtls/v3/connection_id.go generated vendored Normal file
View file

@ -0,0 +1,105 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"crypto/rand"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/extension"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
// RandomCIDGenerator is a random Connection ID generator where CID is the
// specified size. Specifying a size of 0 will indicate to peers that sending a
// Connection ID is not necessary.
func RandomCIDGenerator(size int) func() []byte {
return func() []byte {
cid := make([]byte, size)
if _, err := rand.Read(cid); err != nil {
panic(err) //nolint -- nonrecoverable
}
return cid
}
}
// OnlySendCIDGenerator enables sending Connection IDs negotiated with a peer,
// but indicates to the peer that sending Connection IDs in return is not
// necessary.
func OnlySendCIDGenerator() func() []byte {
return func() []byte {
return nil
}
}
// cidDatagramRouter extracts connection IDs from incoming datagram payloads and
// uses them to route to the proper connection.
// NOTE: properly routing datagrams based on connection IDs requires using
// constant size connection IDs.
func cidDatagramRouter(size int) func([]byte) (string, bool) {
return func(packet []byte) (string, bool) {
pkts, err := recordlayer.ContentAwareUnpackDatagram(packet, size)
if err != nil || len(pkts) < 1 {
return "", false
}
for _, pkt := range pkts {
h := &recordlayer.Header{
ConnectionID: make([]byte, size),
}
if err := h.Unmarshal(pkt); err != nil {
continue
}
if h.ContentType != protocol.ContentTypeConnectionID {
continue
}
return string(h.ConnectionID), true
}
return "", false
}
}
// cidConnIdentifier extracts connection IDs from outgoing ServerHello records
// and associates them with the associated connection.
// NOTE: a ServerHello should always be the first record in a datagram if
// multiple are present, so we avoid iterating through all packets if the first
// is not a ServerHello.
func cidConnIdentifier() func([]byte) (string, bool) { //nolint:cyclop
return func(packet []byte) (string, bool) {
pkts, err := recordlayer.UnpackDatagram(packet)
if err != nil || len(pkts) < 1 {
return "", false
}
var h recordlayer.Header
if hErr := h.Unmarshal(pkts[0]); hErr != nil {
return "", false
}
if h.ContentType != protocol.ContentTypeHandshake {
return "", false
}
var hh handshake.Header
var sh handshake.MessageServerHello
for _, pkt := range pkts {
if hhErr := hh.Unmarshal(pkt[recordlayer.FixedHeaderSize:]); hhErr != nil {
continue
}
if err = sh.Unmarshal(pkt[recordlayer.FixedHeaderSize+handshake.HeaderLength:]); err == nil {
break
}
}
if err != nil {
return "", false
}
for _, ext := range sh.Extensions {
if e, ok := ext.(*extension.ConnectionID); ok {
return string(e.CID), true
}
}
return "", false
}
}

457
vendor/github.com/pion/dtls/v3/crypto.go generated vendored Normal file
View file

@ -0,0 +1,457 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"crypto"
"crypto/ecdsa"
"crypto/ed25519"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/asn1"
"encoding/binary"
"math/big"
"time"
"github.com/pion/dtls/v3/pkg/crypto/elliptic"
"github.com/pion/dtls/v3/pkg/crypto/hash"
"github.com/pion/dtls/v3/pkg/crypto/signature"
"github.com/pion/dtls/v3/pkg/crypto/signaturehash"
)
type ecdsaSignature struct {
R, S *big.Int
}
func valueKeyMessage(clientRandom, serverRandom, publicKey []byte, namedCurve elliptic.Curve) []byte {
serverECDHParams := make([]byte, 4)
serverECDHParams[0] = 3 // named curve
binary.BigEndian.PutUint16(serverECDHParams[1:], uint16(namedCurve))
serverECDHParams[3] = byte(len(publicKey))
plaintext := []byte{}
plaintext = append(plaintext, clientRandom...)
plaintext = append(plaintext, serverRandom...)
plaintext = append(plaintext, serverECDHParams...)
plaintext = append(plaintext, publicKey...)
return plaintext
}
// validateSignatureAlgOID validates that the signature scheme matches the
// certificate's public key algorithm OID. This is required by RFC 8446 Section 4.2.3:
// - RSA_PSS_RSAE requires rsaEncryption OID
// - RSA_PSS_PSS requires id-RSASSA-PSS OID
//
// Note: returns nil if the given signature.Algorithm is not PSS based.
//
// https://www.rfc-editor.org/rfc/rfc8446#section-4.2.3
func validateSignatureAlgOID(cert *x509.Certificate, sigAlg signature.Algorithm) error {
if !sigAlg.IsPSS() {
return nil
}
// Get the certificate's public key algorithm OID from the raw certificate
// We need to parse the SubjectPublicKeyInfo to get the algorithm OID
var spki struct {
Algorithm pkix.AlgorithmIdentifier
PublicKey asn1.BitString
}
if _, err := asn1.Unmarshal(cert.RawSubjectPublicKeyInfo, &spki); err != nil {
return err
}
certOID := spki.Algorithm.Algorithm
switch sigAlg {
// Check RSAE variants (0x0804-0x0806) require rsaEncryption OID
case signature.RSA_PSS_RSAE_SHA256, signature.RSA_PSS_RSAE_SHA384, signature.RSA_PSS_RSAE_SHA512:
oidPublicKeyRSA := asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1} // OID: rsaEncryption
if !certOID.Equal(oidPublicKeyRSA) {
return errInvalidCertificateOID
}
return nil
// Check PSS variants (0x0809-0x080b) require id-RSASSA-PSS OID
case signature.RSA_PSS_PSS_SHA256, signature.RSA_PSS_PSS_SHA384, signature.RSA_PSS_PSS_SHA512:
oidPublicKeyRSAPSS := asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 10} // OID: id-RSASSA-PSS
if !certOID.Equal(oidPublicKeyRSAPSS) {
return errInvalidCertificateOID
}
return nil
default:
return nil
}
}
// If the client provided a "signature_algorithms" extension, then all
// certificates provided by the server MUST be signed by a
// hash/signature algorithm pair that appears in that extension
//
// https://tools.ietf.org/html/rfc5246#section-7.4.2
func generateKeySignature(
clientRandom, serverRandom, publicKey []byte,
namedCurve elliptic.Curve,
signer crypto.Signer,
hashAlgorithm hash.Algorithm,
signatureAlgorithm signature.Algorithm,
) ([]byte, error) {
msg := valueKeyMessage(clientRandom, serverRandom, publicKey, namedCurve)
switch signer.Public().(type) {
case ed25519.PublicKey:
// https://crypto.stackexchange.com/a/55483
return signer.Sign(rand.Reader, msg, crypto.Hash(0))
case *ecdsa.PublicKey:
hashed := hashAlgorithm.Digest(msg)
return signer.Sign(rand.Reader, hashed, hashAlgorithm.CryptoHash())
case *rsa.PublicKey:
hashed := hashAlgorithm.Digest(msg)
// Use RSA-PSS if the signature algorithm is PSS
if signatureAlgorithm.IsPSS() {
pssOpts := &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: hashAlgorithm.CryptoHash(),
}
return signer.Sign(rand.Reader, hashed, pssOpts)
}
// Otherwise use PKCS#1 v1.5
return signer.Sign(rand.Reader, hashed, hashAlgorithm.CryptoHash())
}
return nil, errKeySignatureGenerateUnimplemented
}
//nolint:dupl,cyclop
func verifyKeySignature(
message, remoteKeySignature []byte,
hashAlgorithm hash.Algorithm,
signatureAlgorithm signature.Algorithm,
rawCertificates [][]byte,
) error {
if len(rawCertificates) == 0 {
return errLengthMismatch
}
certificate, err := x509.ParseCertificate(rawCertificates[0])
if err != nil {
return err
}
// Validate that the signature algorithm matches the certificate's OID
if err := validateSignatureAlgOID(certificate, signatureAlgorithm); err != nil {
return err
}
switch pubKey := certificate.PublicKey.(type) {
case ed25519.PublicKey:
if ok := ed25519.Verify(pubKey, message, remoteKeySignature); !ok {
return errKeySignatureMismatch
}
return nil
case *ecdsa.PublicKey:
ecdsaSig := &ecdsaSignature{}
if _, err := asn1.Unmarshal(remoteKeySignature, ecdsaSig); err != nil {
return err
}
if ecdsaSig.R.Sign() <= 0 || ecdsaSig.S.Sign() <= 0 {
return errInvalidECDSASignature
}
hashed := hashAlgorithm.Digest(message)
if !ecdsa.Verify(pubKey, hashed, ecdsaSig.R, ecdsaSig.S) {
return errKeySignatureMismatch
}
return nil
case *rsa.PublicKey:
hashed := hashAlgorithm.Digest(message)
// Use RSA-PSS verification if the signature algorithm is PSS
if signatureAlgorithm.IsPSS() {
pssOpts := &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: hashAlgorithm.CryptoHash(),
}
if err := rsa.VerifyPSS(pubKey, hashAlgorithm.CryptoHash(), hashed, remoteKeySignature, pssOpts); err != nil {
return errKeySignatureMismatch
}
return nil
}
// Otherwise use PKCS#1 v1.5
if rsa.VerifyPKCS1v15(pubKey, hashAlgorithm.CryptoHash(), hashed, remoteKeySignature) != nil {
return errKeySignatureMismatch
}
return nil
}
return errKeySignatureVerifyUnimplemented
}
// If the server has sent a CertificateRequest message, the client MUST send the Certificate
// message. The ClientKeyExchange message is now sent, and the content
// of that message will depend on the public key algorithm selected
// between the ClientHello and the ServerHello. If the client has sent
// a certificate with signing ability, a digitally-signed
// CertificateVerify message is sent to explicitly verify possession of
// the private key in the certificate.
// https://tools.ietf.org/html/rfc5246#section-7.3
func generateCertificateVerify(
handshakeBodies []byte,
signer crypto.Signer,
hashAlgorithm hash.Algorithm,
signatureAlgorithm signature.Algorithm,
) ([]byte, error) {
if _, ok := signer.Public().(ed25519.PublicKey); ok {
// https://pkg.go.dev/crypto/ed25519#PrivateKey.Sign
// Sign signs the given message with priv. Ed25519 performs two passes over
// messages to be signed and therefore cannot handle pre-hashed messages.
return signer.Sign(rand.Reader, handshakeBodies, crypto.Hash(0))
}
hashed := hashAlgorithm.Digest(handshakeBodies)
switch signer.Public().(type) {
case *ecdsa.PublicKey:
return signer.Sign(rand.Reader, hashed, hashAlgorithm.CryptoHash())
case *rsa.PublicKey:
// Use RSA-PSS if the signature algorithm is PSS
if signatureAlgorithm.IsPSS() {
pssOpts := &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: hashAlgorithm.CryptoHash(),
}
return signer.Sign(rand.Reader, hashed, pssOpts)
}
// Otherwise use PKCS#1 v1.5
return signer.Sign(rand.Reader, hashed, hashAlgorithm.CryptoHash())
}
return nil, errInvalidSignatureAlgorithm
}
//nolint:dupl,cyclop
func verifyCertificateVerify(
handshakeBodies []byte,
hashAlgorithm hash.Algorithm,
signatureAlgorithm signature.Algorithm,
remoteKeySignature []byte,
rawCertificates [][]byte,
) error {
if len(rawCertificates) == 0 {
return errLengthMismatch
}
certificate, err := x509.ParseCertificate(rawCertificates[0])
if err != nil {
return err
}
// Validate that the signature algorithm matches the certificate's OID
if err := validateSignatureAlgOID(certificate, signatureAlgorithm); err != nil {
return err
}
switch pubKey := certificate.PublicKey.(type) {
case ed25519.PublicKey:
if ok := ed25519.Verify(pubKey, handshakeBodies, remoteKeySignature); !ok {
return errKeySignatureMismatch
}
return nil
case *ecdsa.PublicKey:
ecdsaSig := &ecdsaSignature{}
if _, err := asn1.Unmarshal(remoteKeySignature, ecdsaSig); err != nil {
return err
}
if ecdsaSig.R.Sign() <= 0 || ecdsaSig.S.Sign() <= 0 {
return errInvalidECDSASignature
}
hash := hashAlgorithm.Digest(handshakeBodies)
if !ecdsa.Verify(pubKey, hash, ecdsaSig.R, ecdsaSig.S) {
return errKeySignatureMismatch
}
return nil
case *rsa.PublicKey:
hash := hashAlgorithm.Digest(handshakeBodies)
// Use RSA-PSS verification if the signature algorithm is PSS
if signatureAlgorithm.IsPSS() {
pssOpts := &rsa.PSSOptions{
SaltLength: rsa.PSSSaltLengthEqualsHash,
Hash: hashAlgorithm.CryptoHash(),
}
if err := rsa.VerifyPSS(pubKey, hashAlgorithm.CryptoHash(), hash, remoteKeySignature, pssOpts); err != nil {
return errKeySignatureMismatch
}
return nil
}
// Otherwise use PKCS#1 v1.5
if rsa.VerifyPKCS1v15(pubKey, hashAlgorithm.CryptoHash(), hash, remoteKeySignature) != nil {
return errKeySignatureMismatch
}
return nil
}
return errKeySignatureVerifyUnimplemented
}
func loadCerts(rawCertificates [][]byte) ([]*x509.Certificate, error) {
if len(rawCertificates) == 0 {
return nil, errLengthMismatch
}
certs := make([]*x509.Certificate, 0, len(rawCertificates))
for _, rawCert := range rawCertificates {
cert, err := x509.ParseCertificate(rawCert)
if err != nil {
return nil, err
}
certs = append(certs, cert)
}
return certs, nil
}
func verifyClientCert(
rawCertificates [][]byte,
roots *x509.CertPool,
certSignatureSchemes []signaturehash.Algorithm,
) (chains [][]*x509.Certificate, err error) {
certificate, err := loadCerts(rawCertificates)
if err != nil {
return nil, err
}
intermediateCAPool := x509.NewCertPool()
for _, cert := range certificate[1:] {
intermediateCAPool.AddCert(cert)
}
opts := x509.VerifyOptions{
Roots: roots,
CurrentTime: time.Now(),
Intermediates: intermediateCAPool,
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
}
chains, err = certificate[0].Verify(opts)
if err != nil {
return nil, err
}
// Validate certificate signature algorithms if specified.
// At least one chain must use only allowed signature algorithms.
if len(certSignatureSchemes) > 0 && len(chains) > 0 {
var validChainFound bool
for _, chain := range chains {
if err := validateCertificateSignatureAlgorithms(chain, certSignatureSchemes); err == nil {
validChainFound = true
break
}
}
if !validChainFound {
return nil, errInvalidCertificateSignatureAlgorithm
}
}
return chains, nil
}
func verifyServerCert(
rawCertificates [][]byte,
roots *x509.CertPool,
serverName string,
certSignatureSchemes []signaturehash.Algorithm,
) (chains [][]*x509.Certificate, err error) {
certificate, err := loadCerts(rawCertificates)
if err != nil {
return nil, err
}
intermediateCAPool := x509.NewCertPool()
for _, cert := range certificate[1:] {
intermediateCAPool.AddCert(cert)
}
opts := x509.VerifyOptions{
Roots: roots,
CurrentTime: time.Now(),
DNSName: serverName,
Intermediates: intermediateCAPool,
}
chains, err = certificate[0].Verify(opts)
if err != nil {
return nil, err
}
// Validate certificate signature algorithms if specified.
// At least one chain must use only allowed signature algorithms.
if len(certSignatureSchemes) > 0 && len(chains) > 0 {
var validChainFound bool
for _, chain := range chains {
if err := validateCertificateSignatureAlgorithms(chain, certSignatureSchemes); err == nil {
validChainFound = true
break
}
}
if !validChainFound {
return nil, errInvalidCertificateSignatureAlgorithm
}
}
return chains, nil
}
// validateCertificateSignatureAlgorithms validates that all certificates in the chain
// use signature algorithms that are in the allowed list. This implements the
// signature_algorithms_cert extension validation per RFC 8446 Section 4.2.3.
func validateCertificateSignatureAlgorithms(
certs []*x509.Certificate,
allowedAlgorithms []signaturehash.Algorithm,
) error {
if len(allowedAlgorithms) == 0 {
// No restrictions specified
return nil
}
// Validate each certificate's signature algorithm (except the root, which we trust)
for i := 0; i < len(certs)-1; i++ {
cert := certs[i]
certAlg, err := signaturehash.FromCertificate(cert)
if err != nil {
return err
}
// Check if this algorithm is in the allowed list
found := false
for _, allowed := range allowedAlgorithms {
if certAlg.Hash == allowed.Hash && certAlg.Signature == allowed.Signature {
found = true
break
}
}
if !found {
return errInvalidCertificateSignatureAlgorithm
}
}
return nil
}

5
vendor/github.com/pion/dtls/v3/dtls.go generated vendored Normal file
View file

@ -0,0 +1,5 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package dtls implements Datagram Transport Layer Security (DTLS) 1.2
package dtls

308
vendor/github.com/pion/dtls/v3/errors.go generated vendored Normal file
View file

@ -0,0 +1,308 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"context"
"errors"
"fmt"
"io"
"net"
"os"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/alert"
)
// Typed errors.
var (
ErrConnClosed = &FatalError{Err: errors.New("conn is closed")} //nolint:err113
errDeadlineExceeded = &TimeoutError{Err: fmt.Errorf("read/write timeout: %w", context.DeadlineExceeded)}
errInvalidContentType = &TemporaryError{Err: errors.New("invalid content type")} //nolint:err113
//nolint:err113
errBufferTooSmall = &TemporaryError{Err: errors.New("buffer is too small")}
//nolint:err113
errContextUnsupported = &TemporaryError{Err: errors.New("context is not supported for ExportKeyingMaterial")}
//nolint:err113
errHandshakeInProgress = &TemporaryError{Err: errors.New("handshake is in progress")}
//nolint:err113
errReservedExportKeyingMaterial = &TemporaryError{
Err: errors.New("ExportKeyingMaterial can not be used with a reserved label"),
}
//nolint:err113
errApplicationDataEpochZero = &TemporaryError{Err: errors.New("ApplicationData with epoch of 0")}
//nolint:err113
errUnhandledContextType = &TemporaryError{Err: errors.New("unhandled contentType")}
//nolint:err113
errCertificateVerifyNoCertificate = &FatalError{
Err: errors.New("client sent certificate verify but we have no certificate to verify"),
}
//nolint:err113
errCipherSuiteNoIntersection = &FatalError{Err: errors.New("client+server do not support any shared cipher suites")}
//nolint:err113
errClientCertificateNotVerified = &FatalError{Err: errors.New("client sent certificate but did not verify it")}
//nolint:err113
errClientCertificateRequired = &FatalError{Err: errors.New("server required client verification, but got none")}
//nolint:err113
errClientNoMatchingSRTPProfile = &FatalError{Err: errors.New("server responded with SRTP Profile we do not support")}
//nolint:err113
errClientRequiredButNoServerEMS = &FatalError{
Err: errors.New("client required Extended Master Secret extension, but server does not support it"),
}
//nolint:err113
errCookieMismatch = &FatalError{Err: errors.New("client+server cookie does not match")}
//nolint:err113
errIdentityNoPSK = &FatalError{Err: errors.New("PSK Identity Hint provided but PSK is nil")}
//nolint:err113
errInvalidCertificate = &FatalError{Err: errors.New("no certificate provided")}
//nolint:err113
errInvalidCipherSuite = &FatalError{Err: errors.New("invalid or unknown cipher suite")}
//nolint:err113
errInvalidClientAuthType = &FatalError{Err: errors.New("invalid client auth type")}
//nolint:err113
errInvalidECDSASignature = &FatalError{Err: errors.New("ECDSA signature contained zero or negative values")}
//nolint:err113
errInvalidPrivateKey = &FatalError{Err: errors.New("invalid private key type")}
//nolint:err113
errInvalidSignatureAlgorithm = &FatalError{Err: errors.New("invalid signature algorithm")}
//nolint:err113
errInvalidExtendedMasterSecretType = &FatalError{Err: errors.New("invalid extended master secret type")}
//nolint:err113
errInvalidCertificateSignatureAlgorithm = &FatalError{
Err: errors.New("certificate uses a signature algorithm that is not allowed"),
}
//nolint:err113
errKeySignatureMismatch = &FatalError{Err: errors.New("expected and actual key signature do not match")}
//nolint:err113
errInvalidCertificateOID = &FatalError{Err: errors.New("certificate OID does not match signature algorithm")}
//nolint:err113
errNilNextConn = &FatalError{Err: errors.New("Conn can not be created with a nil nextConn")}
//nolint:err113
errNoAvailableCipherSuites = &FatalError{
Err: errors.New("connection can not be created, no CipherSuites satisfy this Config"),
}
//nolint:err113
errNoAvailablePSKCipherSuite = &FatalError{
Err: errors.New("connection can not be created, pre-shared key present but no compatible CipherSuite"),
}
//nolint:err113
errNoAvailableCertificateCipherSuite = &FatalError{
Err: errors.New("connection can not be created, certificate present but no compatible CipherSuite"),
}
//nolint:err113
errNoAvailableSignatureSchemes = &FatalError{
Err: errors.New("connection can not be created, no SignatureScheme satisfy this Config"),
}
//nolint:err113
errNoCertificates = &FatalError{Err: errors.New("no certificates configured")}
//nolint:err113
errNoConfigProvided = &FatalError{Err: errors.New("no config provided")}
//nolint:err113
errNoSupportedEllipticCurves = &FatalError{
Err: errors.New("client requested zero or more elliptic curves that are not supported by the server"),
}
//nolint:err113
errUnsupportedProtocolVersion = &FatalError{Err: errors.New("unsupported protocol version")}
//nolint:err113
errPSKAndIdentityMustBeSetForClient = &FatalError{
Err: errors.New("PSK and PSK Identity Hint must both be set for client"),
}
//nolint:err113
errRequestedButNoSRTPExtension = &FatalError{
Err: errors.New("SRTP support was requested but server did not respond with use_srtp extension"),
}
//nolint:err113
errServerNoMatchingSRTPProfile = &FatalError{Err: errors.New("client requested SRTP but we have no matching profiles")}
//nolint:err113
errServerRequiredButNoClientEMS = &FatalError{
Err: errors.New("server requires the Extended Master Secret extension, but the client does not support it"),
}
//nolint:err113
errVerifyDataMismatch = &FatalError{Err: errors.New("expected and actual verify data does not match")}
//nolint:err113
errNotAcceptableCertificateChain = &FatalError{Err: errors.New("certificate chain is not signed by an acceptable CA")}
//nolint:err113
errInvalidFlight = &InternalError{Err: errors.New("invalid flight number")}
//nolint:err113
errKeySignatureGenerateUnimplemented = &InternalError{
Err: errors.New("unable to generate key signature, unimplemented"),
}
//nolint:err113
errKeySignatureVerifyUnimplemented = &InternalError{Err: errors.New("unable to verify key signature, unimplemented")}
//nolint:err113
errLengthMismatch = &InternalError{Err: errors.New("data length and declared length do not match")}
//nolint:err113
errSequenceNumberOverflow = &InternalError{Err: errors.New("sequence number overflow")}
//nolint:err113
errInvalidFSMTransition = &InternalError{Err: errors.New("invalid state machine transition")}
//nolint:err113
errFailedToAccessPoolReadBuffer = &InternalError{Err: errors.New("failed to access pool read buffer")}
//nolint:err113
errFragmentBufferOverflow = &InternalError{Err: errors.New("fragment buffer overflow")}
//nolint:err113
errEmptyCertificates = &FatalError{Err: errors.New("certificates option requires at least one certificate")}
//nolint:err113
errEmptyCipherSuites = &FatalError{Err: errors.New("cipher suites option requires at least one cipher suite")}
//nolint:err113
errNilCustomCipherSuites = &FatalError{Err: errors.New("custom cipher suites option requires a non-nil function")}
//nolint:err113
errEmptySignatureSchemes = &FatalError{Err: errors.New("signature schemes option requires at least one scheme")}
//nolint:err113
errEmptyCertificateSignatureSchemes = &FatalError{
Err: errors.New("certificate signature schemes option requires at least one scheme"),
}
//nolint:err113
errEmptySRTPProtectionProfiles = &FatalError{
Err: errors.New("SRTP protection profiles option requires at least one profile"),
}
//nolint:err113
errInvalidFlightInterval = &FatalError{Err: errors.New("flight interval must be positive")}
//nolint:err113
errNilPSKCallback = &FatalError{Err: errors.New("PSK option requires a non-nil callback")}
//nolint:err113
errNilVerifyPeerCertificate = &FatalError{
Err: errors.New("verify peer certificate option requires a non-nil callback"),
}
//nolint:err113
errNilVerifyConnection = &FatalError{Err: errors.New("verify connection option requires a non-nil callback")}
//nolint:err113
errInvalidMTU = &FatalError{Err: errors.New("MTU must be positive")}
//nolint:err113
errInvalidReplayProtectionWindow = &FatalError{Err: errors.New("replay protection window must be non-negative")}
//nolint:err113
errEmptySupportedProtocols = &FatalError{
Err: errors.New("supported protocols option requires at least one protocol"),
}
//nolint:err113
errEmptyEllipticCurves = &FatalError{Err: errors.New("elliptic curves option requires at least one curve")}
//nolint:err113
errNilGetClientCertificate = &FatalError{
Err: errors.New("get client certificate option requires a non-nil callback"),
}
//nolint:err113
errNilConnectionIDGenerator = &FatalError{
Err: errors.New("connection ID generator option requires a non-nil function"),
}
//nolint:err113
errNilPaddingLengthGenerator = &FatalError{
Err: errors.New("padding length generator option requires a non-nil function"),
}
//nolint:err113
errNilHelloRandomBytesGenerator = &FatalError{
Err: errors.New("hello random bytes generator option requires a non-nil function"),
}
//nolint:err113
errNilClientHelloMessageHook = &FatalError{
Err: errors.New("client hello message hook option requires a non-nil function"),
}
//nolint:err113
errNilGetCertificate = &FatalError{Err: errors.New("get certificate option requires a non-nil callback")}
//nolint:err113
errNilServerHelloMessageHook = &FatalError{
Err: errors.New("server hello message hook option requires a non-nil function"),
}
//nolint:err113
errNilCertificateRequestMessageHook = &FatalError{
Err: errors.New("certificate request message hook option requires a non-nil function"),
}
//nolint:err113
errNilOnConnectionAttempt = &FatalError{
Err: errors.New("on connection attempt option requires a non-nil callback"),
}
)
// FatalError indicates that the DTLS connection is no longer available.
// It is mainly caused by wrong configuration of server or client.
type FatalError = protocol.FatalError
// InternalError indicates and internal error caused by the implementation,
// and the DTLS connection is no longer available.
// It is mainly caused by bugs or tried to use unimplemented features.
type InternalError = protocol.InternalError
// TemporaryError indicates that the DTLS connection is still available, but the request was failed temporary.
type TemporaryError = protocol.TemporaryError
// TimeoutError indicates that the request was timed out.
type TimeoutError = protocol.TimeoutError
// HandshakeError indicates that the handshake failed.
type HandshakeError = protocol.HandshakeError
// errInvalidCipherSuite indicates an attempt at using an unsupported cipher suite.
type invalidCipherSuiteError struct {
id CipherSuiteID
}
func (e *invalidCipherSuiteError) Error() string {
return fmt.Sprintf("CipherSuite with id(%d) is not valid", e.id)
}
func (e *invalidCipherSuiteError) Is(err error) bool {
var other *invalidCipherSuiteError
if errors.As(err, &other) {
return e.id == other.id
}
return false
}
// errAlert wraps DTLS alert notification as an error.
type alertError struct {
*alert.Alert
}
func (e *alertError) Error() string {
return fmt.Sprintf("alert: %s", e.Alert.String())
}
func (e *alertError) IsFatalOrCloseNotify() bool {
return e.Level == alert.Fatal || e.Description == alert.CloseNotify
}
func (e *alertError) Is(err error) bool {
var other *alertError
if errors.As(err, &other) {
return e.Level == other.Level && e.Description == other.Description
}
return false
}
// netError translates an error from underlying Conn to corresponding net.Error.
func netError(err error) error {
switch {
case errors.Is(err, io.EOF), errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded):
// Return io.EOF and context errors as is.
return err
}
var (
ne net.Error
opError *net.OpError
se *os.SyscallError
)
if errors.As(err, &opError) { //nolint:nestif
if errors.As(opError, &se) {
if se.Timeout() {
return &TimeoutError{Err: err}
}
if isOpErrorTemporary(se) {
return &TemporaryError{Err: err}
}
}
}
if errors.As(err, &ne) {
return err
}
return &FatalError{Err: err}
}

22
vendor/github.com/pion/dtls/v3/errors_errno.go generated vendored Normal file
View file

@ -0,0 +1,22 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
//go:build aix || darwin || dragonfly || freebsd || linux || nacl || nacljs || netbsd || openbsd || solaris || windows
// +build aix darwin dragonfly freebsd linux nacl nacljs netbsd openbsd solaris windows
// For systems having syscall.Errno.
// Update build targets by following command:
// $ grep -R ECONN $(go env GOROOT)/src/syscall/zerrors_*.go \
// | tr "." "_" | cut -d"_" -f"2" | sort | uniq
package dtls
import (
"errors"
"os"
"syscall"
)
func isOpErrorTemporary(err *os.SyscallError) bool {
return errors.Is(err.Err, syscall.ECONNREFUSED)
}

18
vendor/github.com/pion/dtls/v3/errors_noerrno.go generated vendored Normal file
View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
//go:build !aix && !darwin && !dragonfly && !freebsd && !linux && !nacl && !nacljs && !netbsd && !openbsd && !solaris && !windows
// +build !aix,!darwin,!dragonfly,!freebsd,!linux,!nacl,!nacljs,!netbsd,!openbsd,!solaris,!windows
// For systems without syscall.Errno.
// Build targets must be inverse of errors_errno.go
package dtls
import (
"os"
)
func isOpErrorTemporary(err *os.SyscallError) bool {
return false
}

104
vendor/github.com/pion/dtls/v3/flight.go generated vendored Normal file
View file

@ -0,0 +1,104 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
/*
DTLS messages are grouped into a series of message flights, according
to the diagrams below. Although each flight of messages may consist
of a number of messages, they should be viewed as monolithic for the
purpose of timeout and retransmission.
https://tools.ietf.org/html/rfc4347#section-4.2.4
Message flights for full handshake:
Client Server
------ ------
Waiting Flight 0
ClientHello --------> Flight 1
<------- HelloVerifyRequest Flight 2
ClientHello --------> Flight 3
ServerHello \
Certificate* \
ServerKeyExchange* Flight 4
CertificateRequest* /
<-------- ServerHelloDone /
Certificate* \
ClientKeyExchange \
CertificateVerify* Flight 5
[ChangeCipherSpec] /
Finished --------> /
[ChangeCipherSpec] \ Flight 6
<-------- Finished /
Message flights for session-resuming handshake (no cookie exchange):
Client Server
------ ------
Waiting Flight 0
ClientHello --------> Flight 1
ServerHello \
[ChangeCipherSpec] Flight 4b
<-------- Finished /
[ChangeCipherSpec] \ Flight 5b
Finished --------> /
[ChangeCipherSpec] \ Flight 6
<-------- Finished /
*/
type flightVal uint8
const (
flight0 flightVal = iota + 1
flight1
flight2
flight3
flight4
flight4b
flight5
flight5b
flight6
)
func (f flightVal) String() string { //nolint:cyclop
switch f {
case flight0:
return "Flight 0"
case flight1:
return "Flight 1"
case flight2:
return "Flight 2"
case flight3:
return "Flight 3"
case flight4:
return "Flight 4"
case flight4b:
return "Flight 4b"
case flight5:
return "Flight 5"
case flight5b:
return "Flight 5b"
case flight6:
return "Flight 6"
default:
return "Invalid Flight"
}
}
func (f flightVal) isLastSendFlight() bool {
return f == flight6 || f == flight5b
}
func (f flightVal) isLastRecvFlight() bool {
return f == flight5 || f == flight4b
}

189
vendor/github.com/pion/dtls/v3/flight0handler.go generated vendored Normal file
View file

@ -0,0 +1,189 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"context"
"crypto/rand"
"github.com/pion/dtls/v3/pkg/crypto/elliptic"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/alert"
"github.com/pion/dtls/v3/pkg/protocol/extension"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
)
// renegotiationInfoSCSV is TLS_EMPTY_RENEGOTIATION_INFO_SCSV defined in RFC 5746.
// https://datatracker.ietf.org/doc/html/rfc5746#section-3.3.
const renegotiationInfoSCSV uint16 = 0x00ff
//nolint:cyclop,gocognit
func flight0Parse(
_ context.Context,
_ flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) (flightVal, *alert.Alert, error) {
seq, msgs, ok := cache.fullPullMap(0, state.cipherSuite,
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
)
if !ok {
// No valid message received. Keep reading
return 0, nil, nil
}
// Connection Identifiers must be negotiated afresh on session resumption.
// https://datatracker.ietf.org/doc/html/rfc9146#name-the-connection_id-extension
state.setLocalConnectionID(nil)
state.remoteConnectionID = nil
state.handshakeRecvSequence = seq
var clientHello *handshake.MessageClientHello
// Validate type
if clientHello, ok = msgs[handshake.TypeClientHello].(*handshake.MessageClientHello); !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
}
if !clientHello.Version.Equal(protocol.Version1_2) {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion}, errUnsupportedProtocolVersion
}
state.remoteRandom = clientHello.Random
cipherSuites := []CipherSuite{}
for _, id := range clientHello.CipherSuiteIDs {
if id == renegotiationInfoSCSV {
state.remoteSupportsRenegotiation = true
continue
}
if c := cipherSuiteForID(CipherSuiteID(id), cfg.customCipherSuites); c != nil {
cipherSuites = append(cipherSuites, c)
}
}
if state.cipherSuite, ok = findMatchingCipherSuite(cipherSuites, cfg.localCipherSuites); !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errCipherSuiteNoIntersection
}
for _, val := range clientHello.Extensions {
switch ext := val.(type) {
case *extension.SupportedEllipticCurves:
if len(ext.EllipticCurves) == 0 {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errNoSupportedEllipticCurves
}
state.namedCurve = ext.EllipticCurves[0]
case *extension.UseSRTP:
profile, ok := findMatchingSRTPProfile(cfg.localSRTPProtectionProfiles, ext.ProtectionProfiles)
if !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errServerNoMatchingSRTPProfile
}
state.setSRTPProtectionProfile(profile)
state.remoteSRTPMasterKeyIdentifier = ext.MasterKeyIdentifier
case *extension.UseExtendedMasterSecret:
if cfg.extendedMasterSecret != DisableExtendedMasterSecret {
state.extendedMasterSecret = true
}
case *extension.ServerName:
state.serverName = ext.ServerName // remote server name
case *extension.RenegotiationInfo:
state.remoteSupportsRenegotiation = true
case *extension.ALPN:
state.peerSupportedProtocols = ext.ProtocolNameList
case *extension.ConnectionID:
// Only set connection ID to be sent if server supports connection
// IDs.
if cfg.connectionIDGenerator != nil {
state.remoteConnectionID = ext.CID
}
case *extension.SignatureAlgorithmsCert:
// Store the client's certificate signature schemes for later validation
state.remoteCertSignatureSchemes = ext.SignatureHashAlgorithms
}
}
// If the client doesn't support connection IDs, the server should not
// expect one to be sent.
if state.remoteConnectionID == nil {
state.setLocalConnectionID(nil)
}
if cfg.extendedMasterSecret == RequireExtendedMasterSecret && !state.extendedMasterSecret {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errServerRequiredButNoClientEMS
}
if state.localKeypair == nil {
var err error
state.localKeypair, err = elliptic.GenerateKeypair(state.namedCurve)
if err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, err
}
}
nextFlight := flight2
if cfg.insecureSkipHelloVerify {
nextFlight = flight4
}
return handleHelloResume(clientHello.SessionID, state, cfg, nextFlight)
}
func handleHelloResume(
sessionID []byte,
state *State,
cfg *handshakeConfig,
next flightVal,
) (flightVal, *alert.Alert, error) {
if len(sessionID) > 0 && cfg.sessionStore != nil {
if s, err := cfg.sessionStore.Get(sessionID); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
} else if s.ID != nil {
cfg.log.Tracef("[handshake] resume session: %x", sessionID)
state.SessionID = sessionID
state.masterSecret = s.Secret
if err := state.initCipherSuite(); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
clientRandom := state.localRandom.MarshalFixed()
cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.masterSecret)
return flight4b, nil, nil
}
}
return next, nil, nil
}
func flight0Generate(
_ flightConn,
state *State,
_ *handshakeCache,
cfg *handshakeConfig,
) ([]*packet, *alert.Alert, error) {
// Initialize
if !cfg.insecureSkipHelloVerify {
state.cookie = make([]byte, cookieLength)
if _, err := rand.Read(state.cookie); err != nil {
return nil, nil, err
}
}
var zeroEpoch uint16
state.localEpoch.Store(zeroEpoch)
state.remoteEpoch.Store(zeroEpoch)
state.namedCurve = defaultNamedCurve
if err := state.localRandom.Populate(); err != nil {
return nil, nil, err
}
return nil, nil, nil
}

189
vendor/github.com/pion/dtls/v3/flight1handler.go generated vendored Normal file
View file

@ -0,0 +1,189 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"context"
"github.com/pion/dtls/v3/pkg/crypto/elliptic"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/alert"
"github.com/pion/dtls/v3/pkg/protocol/extension"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
func flight1Parse(
ctx context.Context,
conn flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) (flightVal, *alert.Alert, error) {
// HelloVerifyRequest can be skipped by the server,
// so allow ServerHello during flight1 also
seq, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence, state.cipherSuite,
handshakeCachePullRule{handshake.TypeHelloVerifyRequest, cfg.initialEpoch, false, true},
handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, true},
)
if !ok {
// No valid message received. Keep reading
return 0, nil, nil
}
if _, ok := msgs[handshake.TypeServerHello]; ok {
// Flight1 and flight2 were skipped.
// Parse as flight3.
return flight3Parse(ctx, conn, state, cache, cfg)
}
if h, ok := msgs[handshake.TypeHelloVerifyRequest].(*handshake.MessageHelloVerifyRequest); ok {
// DTLS 1.2 clients must not assume that the server will use the protocol version
// specified in HelloVerifyRequest message. RFC 6347 Section 4.2.1
if !h.Version.Equal(protocol.Version1_0) && !h.Version.Equal(protocol.Version1_2) {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion}, errUnsupportedProtocolVersion
}
state.cookie = append([]byte{}, h.Cookie...)
state.handshakeRecvSequence = seq
return flight3, nil, nil
}
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
}
//nolint:cyclop
func flight1Generate(
conn flightConn,
state *State,
_ *handshakeCache,
cfg *handshakeConfig,
) ([]*packet, *alert.Alert, error) {
var zeroEpoch uint16
state.localEpoch.Store(zeroEpoch)
state.remoteEpoch.Store(zeroEpoch)
state.namedCurve = defaultNamedCurve
state.cookie = nil
if err := state.localRandom.Populate(); err != nil {
return nil, nil, err
}
if cfg.helloRandomBytesGenerator != nil {
state.localRandom.RandomBytes = cfg.helloRandomBytesGenerator()
}
extensions := []extension.Extension{
&extension.SupportedSignatureAlgorithms{
SignatureHashAlgorithms: cfg.localSignatureSchemes,
},
&extension.RenegotiationInfo{
RenegotiatedConnection: 0,
},
}
if len(cfg.localCertSignatureSchemes) > 0 {
extensions = append(extensions, &extension.SignatureAlgorithmsCert{
SignatureHashAlgorithms: cfg.localCertSignatureSchemes,
})
}
var setEllipticCurveCryptographyClientHelloExtensions bool
for _, c := range cfg.localCipherSuites {
if c.ECC() {
setEllipticCurveCryptographyClientHelloExtensions = true
break
}
}
if setEllipticCurveCryptographyClientHelloExtensions {
extensions = append(extensions, []extension.Extension{
&extension.SupportedEllipticCurves{
EllipticCurves: cfg.ellipticCurves,
},
&extension.SupportedPointFormats{
PointFormats: []elliptic.CurvePointFormat{elliptic.CurvePointFormatUncompressed},
},
}...)
}
if len(cfg.localSRTPProtectionProfiles) > 0 {
extensions = append(extensions, &extension.UseSRTP{
ProtectionProfiles: cfg.localSRTPProtectionProfiles,
MasterKeyIdentifier: cfg.localSRTPMasterKeyIdentifier,
})
}
if cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
cfg.extendedMasterSecret == RequireExtendedMasterSecret {
extensions = append(extensions, &extension.UseExtendedMasterSecret{
Supported: true,
})
}
if len(cfg.serverName) > 0 {
extensions = append(extensions, &extension.ServerName{ServerName: cfg.serverName})
}
if len(cfg.supportedProtocols) > 0 {
extensions = append(extensions, &extension.ALPN{ProtocolNameList: cfg.supportedProtocols})
}
if cfg.sessionStore != nil {
cfg.log.Tracef("[handshake] try to resume session")
if s, err := cfg.sessionStore.Get(conn.sessionKey()); err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
} else if s.ID != nil {
cfg.log.Tracef("[handshake] get saved session: %x", s.ID)
state.SessionID = s.ID
state.masterSecret = s.Secret
}
}
// If we have a connection ID generator, use it. The CID may be zero length,
// in which case we are just requesting that the server send us a CID to
// use.
if cfg.connectionIDGenerator != nil {
state.setLocalConnectionID(cfg.connectionIDGenerator())
// The presence of a generator indicates support for connection IDs. We
// use the presence of a non-nil local CID in flight 3 to determine
// whether we send a CID in the second ClientHello, so we convert any
// nil CID returned by a generator to []byte{}.
if state.getLocalConnectionID() == nil {
state.setLocalConnectionID([]byte{})
}
extensions = append(extensions, &extension.ConnectionID{CID: state.getLocalConnectionID()})
}
clientHello := &handshake.MessageClientHello{
Version: protocol.Version1_2,
SessionID: state.SessionID,
Cookie: state.cookie,
Random: state.localRandom,
CipherSuiteIDs: cipherSuiteIDs(cfg.localCipherSuites),
CompressionMethods: defaultCompressionMethods(),
Extensions: extensions,
}
var content handshake.Handshake
if cfg.clientHelloMessageHook != nil {
content = handshake.Handshake{Message: cfg.clientHelloMessageHook(*clientHello)}
} else {
content = handshake.Handshake{Message: clientHello}
}
return []*packet{
{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &content,
},
},
}, nil, nil
}

77
vendor/github.com/pion/dtls/v3/flight2handler.go generated vendored Normal file
View file

@ -0,0 +1,77 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"bytes"
"context"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/alert"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
func flight2Parse(
ctx context.Context,
c flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) (flightVal, *alert.Alert, error) {
seq, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence, state.cipherSuite,
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
)
if !ok {
// Client may retransmit the first ClientHello when HelloVerifyRequest is dropped.
// Parse as flight 0 in this case.
return flight0Parse(ctx, c, state, cache, cfg)
}
state.handshakeRecvSequence = seq
var clientHello *handshake.MessageClientHello
// Validate type
if clientHello, ok = msgs[handshake.TypeClientHello].(*handshake.MessageClientHello); !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
}
if !clientHello.Version.Equal(protocol.Version1_2) {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion}, errUnsupportedProtocolVersion
}
if len(clientHello.Cookie) == 0 {
return 0, nil, nil
}
if !bytes.Equal(state.cookie, clientHello.Cookie) {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.AccessDenied}, errCookieMismatch
}
return flight4, nil, nil
}
func flight2Generate(
_ flightConn,
state *State,
_ *handshakeCache,
_ *handshakeConfig,
) ([]*packet, *alert.Alert, error) {
state.handshakeSendSequence = 0
return []*packet{
{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &handshake.Handshake{
Message: &handshake.MessageHelloVerifyRequest{
Version: protocol.Version1_2,
Cookie: state.cookie,
},
},
},
},
}, nil, nil
}

363
vendor/github.com/pion/dtls/v3/flight3handler.go generated vendored Normal file
View file

@ -0,0 +1,363 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"bytes"
"context"
"github.com/pion/dtls/v3/internal/ciphersuite/types"
"github.com/pion/dtls/v3/pkg/crypto/elliptic"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/alert"
"github.com/pion/dtls/v3/pkg/protocol/extension"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
//nolint:gocognit,gocyclo,maintidx,cyclop
func flight3Parse(
ctx context.Context,
conn flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) (flightVal, *alert.Alert, error) {
// Clients may receive multiple HelloVerifyRequest messages with different cookies.
// Clients SHOULD handle this by sending a new ClientHello with a cookie in response
// to the new HelloVerifyRequest. RFC 6347 Section 4.2.1
seq, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence, state.cipherSuite,
handshakeCachePullRule{handshake.TypeHelloVerifyRequest, cfg.initialEpoch, false, true},
)
if ok {
if h, msgOk := msgs[handshake.TypeHelloVerifyRequest].(*handshake.MessageHelloVerifyRequest); msgOk {
// DTLS 1.2 clients must not assume that the server will use the protocol version
// specified in HelloVerifyRequest message. RFC 6347 Section 4.2.1
if !h.Version.Equal(protocol.Version1_0) && !h.Version.Equal(protocol.Version1_2) {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion}, errUnsupportedProtocolVersion
}
state.cookie = append([]byte{}, h.Cookie...)
state.handshakeRecvSequence = seq
return flight3, nil, nil
}
}
_, msgs, ok = cache.fullPullMap(state.handshakeRecvSequence, state.cipherSuite,
handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
)
if !ok {
// Don't have enough messages. Keep reading
return 0, nil, nil
}
if serverHelloMsg, msgOk := msgs[handshake.TypeServerHello].(*handshake.MessageServerHello); msgOk { //nolint:nestif
if !serverHelloMsg.Version.Equal(protocol.Version1_2) {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.ProtocolVersion}, errUnsupportedProtocolVersion
}
for _, v := range serverHelloMsg.Extensions {
switch ext := v.(type) {
case *extension.UseSRTP:
profile, found := findMatchingSRTPProfile(ext.ProtectionProfiles, cfg.localSRTPProtectionProfiles)
if !found {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, errClientNoMatchingSRTPProfile
}
state.setSRTPProtectionProfile(profile)
state.remoteSRTPMasterKeyIdentifier = ext.MasterKeyIdentifier
case *extension.UseExtendedMasterSecret:
if cfg.extendedMasterSecret != DisableExtendedMasterSecret {
state.extendedMasterSecret = true
}
case *extension.ALPN:
if len(ext.ProtocolNameList) > 1 { // This should be exactly 1, the zero case is handle when unmarshalling
return 0, &alert.Alert{
Level: alert.Fatal,
Description: alert.InternalError,
}, extension.ErrALPNInvalidFormat // Meh, internal error?
}
state.NegotiatedProtocol = ext.ProtocolNameList[0]
case *extension.ConnectionID:
// Only set connection ID to be sent if client supports connection
// IDs.
if cfg.connectionIDGenerator != nil {
state.remoteConnectionID = ext.CID
}
}
}
// If the server doesn't support connection IDs, the client should not
// expect one to be sent.
if state.remoteConnectionID == nil {
state.setLocalConnectionID(nil)
}
if cfg.extendedMasterSecret == RequireExtendedMasterSecret && !state.extendedMasterSecret {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errClientRequiredButNoServerEMS
}
if len(cfg.localSRTPProtectionProfiles) > 0 && state.getSRTPProtectionProfile() == 0 {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errRequestedButNoSRTPExtension
}
remoteCipherSuite := cipherSuiteForID(CipherSuiteID(*serverHelloMsg.CipherSuiteID), cfg.customCipherSuites)
if remoteCipherSuite == nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errCipherSuiteNoIntersection
}
selectedCipherSuite, found := findMatchingCipherSuite([]CipherSuite{remoteCipherSuite}, cfg.localCipherSuites)
if !found {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errInvalidCipherSuite
}
state.cipherSuite = selectedCipherSuite
state.remoteRandom = serverHelloMsg.Random
cfg.log.Tracef("[handshake] use cipher suite: %s", selectedCipherSuite.String())
if len(serverHelloMsg.SessionID) > 0 && bytes.Equal(state.SessionID, serverHelloMsg.SessionID) {
return handleResumption(ctx, conn, state, cache, cfg)
}
if len(state.SessionID) > 0 {
cfg.log.Tracef("[handshake] clean old session : %s", state.SessionID)
if err := cfg.sessionStore.Del(state.SessionID); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
if cfg.sessionStore == nil {
state.SessionID = []byte{}
} else {
state.SessionID = serverHelloMsg.SessionID
}
state.masterSecret = []byte{}
}
if cfg.localPSKCallback != nil {
seq, msgs, ok = cache.fullPullMap(state.handshakeRecvSequence+1, state.cipherSuite,
handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, true},
handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
)
} else {
seq, msgs, ok = cache.fullPullMap(state.handshakeRecvSequence+1, state.cipherSuite,
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, true},
handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, true},
handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
)
}
if !ok {
// Don't have enough messages. Keep reading
return 0, nil, nil
}
state.handshakeRecvSequence = seq
if h, ok := msgs[handshake.TypeCertificate].(*handshake.MessageCertificate); ok {
state.PeerCertificates = h.Certificate
} else if state.cipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypeCertificate {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.NoCertificate}, errInvalidCertificate
}
if h, ok := msgs[handshake.TypeServerKeyExchange].(*handshake.MessageServerKeyExchange); ok {
alertPtr, err := handleServerKeyExchange(conn, state, cfg, h)
if err != nil {
return 0, alertPtr, err
}
}
if creq, ok := msgs[handshake.TypeCertificateRequest].(*handshake.MessageCertificateRequest); ok {
state.remoteCertRequestAlgs = creq.SignatureHashAlgorithms
state.remoteRequestedCertificate = true
}
return flight5, nil, nil
}
func handleResumption(
ctx context.Context,
c flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) (flightVal, *alert.Alert, error) {
if err := state.initCipherSuite(); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
// Now, encrypted packets can be handled
if err := c.handleQueuedPackets(ctx); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
_, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence+1, state.cipherSuite,
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
)
if !ok {
// No valid message received. Keep reading
return 0, nil, nil
}
var finished *handshake.MessageFinished
if finished, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
}
plainText := cache.pullAndMerge(
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
)
expectedVerifyData, err := prf.VerifyDataServer(state.masterSecret, plainText, state.cipherSuite.HashFunc())
if err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
if !bytes.Equal(expectedVerifyData, finished.VerifyData) {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, errVerifyDataMismatch
}
clientRandom := state.localRandom.MarshalFixed()
cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.masterSecret)
return flight5b, nil, nil
}
//nolint:cyclop
func handleServerKeyExchange(
_ flightConn,
state *State,
cfg *handshakeConfig,
keyExchangeMessage *handshake.MessageServerKeyExchange,
) (*alert.Alert, error) {
var err error
if state.cipherSuite == nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errInvalidCipherSuite
}
if cfg.localPSKCallback != nil { //nolint:nestif
var psk []byte
if psk, err = cfg.localPSKCallback(keyExchangeMessage.IdentityHint); err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
state.IdentityHint = keyExchangeMessage.IdentityHint
switch state.cipherSuite.KeyExchangeAlgorithm() {
case types.KeyExchangeAlgorithmPsk:
state.preMasterSecret = prf.PSKPreMasterSecret(psk)
case (types.KeyExchangeAlgorithmEcdhe | types.KeyExchangeAlgorithmPsk):
if state.localKeypair, err = elliptic.GenerateKeypair(keyExchangeMessage.NamedCurve); err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
state.preMasterSecret, err = prf.EcdhePSKPreMasterSecret(
psk,
keyExchangeMessage.PublicKey,
state.localKeypair.PrivateKey,
state.localKeypair.Curve,
)
if err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
default:
return &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errInvalidCipherSuite
}
} else {
if state.localKeypair, err = elliptic.GenerateKeypair(keyExchangeMessage.NamedCurve); err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
if state.preMasterSecret, err = prf.PreMasterSecret(
keyExchangeMessage.PublicKey,
state.localKeypair.PrivateKey,
state.localKeypair.Curve,
); err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
return nil, nil //nolint:nilnil
}
func flight3Generate(
_ flightConn,
state *State,
_ *handshakeCache,
cfg *handshakeConfig,
) ([]*packet, *alert.Alert, error) {
extensions := []extension.Extension{
&extension.SupportedSignatureAlgorithms{
SignatureHashAlgorithms: cfg.localSignatureSchemes,
},
&extension.RenegotiationInfo{
RenegotiatedConnection: 0,
},
}
if len(cfg.localCertSignatureSchemes) > 0 {
extensions = append(extensions, &extension.SignatureAlgorithmsCert{
SignatureHashAlgorithms: cfg.localCertSignatureSchemes,
})
}
if state.namedCurve != 0 {
extensions = append(extensions, []extension.Extension{
&extension.SupportedEllipticCurves{
EllipticCurves: cfg.ellipticCurves,
},
&extension.SupportedPointFormats{
PointFormats: []elliptic.CurvePointFormat{elliptic.CurvePointFormatUncompressed},
},
}...)
}
if len(cfg.localSRTPProtectionProfiles) > 0 {
extensions = append(extensions, &extension.UseSRTP{
ProtectionProfiles: cfg.localSRTPProtectionProfiles,
})
}
if cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
cfg.extendedMasterSecret == RequireExtendedMasterSecret {
extensions = append(extensions, &extension.UseExtendedMasterSecret{
Supported: true,
})
}
if len(cfg.serverName) > 0 {
extensions = append(extensions, &extension.ServerName{ServerName: cfg.serverName})
}
if len(cfg.supportedProtocols) > 0 {
extensions = append(extensions, &extension.ALPN{ProtocolNameList: cfg.supportedProtocols})
}
// If we sent a connection ID on the first ClientHello, send it on the
// second.
if state.getLocalConnectionID() != nil {
extensions = append(extensions, &extension.ConnectionID{CID: state.getLocalConnectionID()})
}
clientHello := &handshake.MessageClientHello{
Version: protocol.Version1_2,
SessionID: state.SessionID,
Cookie: state.cookie,
Random: state.localRandom,
CipherSuiteIDs: cipherSuiteIDs(cfg.localCipherSuites),
CompressionMethods: defaultCompressionMethods(),
Extensions: extensions,
}
var content handshake.Handshake
if cfg.clientHelloMessageHook != nil {
content = handshake.Handshake{Message: cfg.clientHelloMessageHook(*clientHello)}
} else {
content = handshake.Handshake{Message: clientHello}
}
return []*packet{
{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &content,
},
},
}, nil, nil
}

163
vendor/github.com/pion/dtls/v3/flight4bhandler.go generated vendored Normal file
View file

@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"bytes"
"context"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/alert"
"github.com/pion/dtls/v3/pkg/protocol/extension"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
func flight4bParse(
_ context.Context,
_ flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) (flightVal, *alert.Alert, error) {
_, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence, state.cipherSuite,
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
)
if !ok {
// No valid message received. Keep reading
return 0, nil, nil
}
var finished *handshake.MessageFinished
if finished, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
}
plainText := cache.pullAndMerge(
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
)
expectedVerifyData, err := prf.VerifyDataClient(state.masterSecret, plainText, state.cipherSuite.HashFunc())
if err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
if !bytes.Equal(expectedVerifyData, finished.VerifyData) {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, errVerifyDataMismatch
}
// Other party may re-transmit the last flight. Keep state to be flight4b.
return flight4b, nil, nil
}
//nolint:cyclop
func flight4bGenerate(
_ flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) ([]*packet, *alert.Alert, error) {
var pkts []*packet
extensions := []extension.Extension{&extension.RenegotiationInfo{
RenegotiatedConnection: 0,
}}
if (cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
cfg.extendedMasterSecret == RequireExtendedMasterSecret) && state.extendedMasterSecret {
extensions = append(extensions, &extension.UseExtendedMasterSecret{
Supported: true,
})
}
if state.getSRTPProtectionProfile() != 0 {
extensions = append(extensions, &extension.UseSRTP{
ProtectionProfiles: []SRTPProtectionProfile{state.getSRTPProtectionProfile()},
MasterKeyIdentifier: cfg.localSRTPMasterKeyIdentifier,
})
}
selectedProto, err := extension.ALPNProtocolSelection(cfg.supportedProtocols, state.peerSupportedProtocols)
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.NoApplicationProtocol}, err
}
if selectedProto != "" {
extensions = append(extensions, &extension.ALPN{
ProtocolNameList: []string{selectedProto},
})
state.NegotiatedProtocol = selectedProto
}
cipherSuiteID := uint16(state.cipherSuite.ID())
var serverHello handshake.Handshake
serverHelloMessage := &handshake.MessageServerHello{
Version: protocol.Version1_2,
Random: state.localRandom,
SessionID: state.SessionID,
CipherSuiteID: &cipherSuiteID,
CompressionMethod: defaultCompressionMethods()[0],
Extensions: extensions,
}
if cfg.serverHelloMessageHook != nil {
serverHello = handshake.Handshake{Message: cfg.serverHelloMessageHook(*serverHelloMessage)}
} else {
serverHello = handshake.Handshake{Message: serverHelloMessage}
}
serverHello.Header.MessageSequence = uint16(state.handshakeSendSequence) //nolint:gosec // G115
if len(state.localVerifyData) == 0 {
plainText := cache.pullAndMerge(
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
)
raw, err := serverHello.Marshal()
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
plainText = append(plainText, raw...)
state.localVerifyData, err = prf.VerifyDataServer(state.masterSecret, plainText, state.cipherSuite.HashFunc())
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
pkts = append(pkts,
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &serverHello,
},
},
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &protocol.ChangeCipherSpec{},
},
},
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
Epoch: 1,
},
Content: &handshake.Handshake{
Message: &handshake.MessageFinished{
VerifyData: state.localVerifyData,
},
},
},
shouldEncrypt: true,
resetLocalSequenceNumber: true,
},
)
return pkts, nil, nil
}

500
vendor/github.com/pion/dtls/v3/flight4handler.go generated vendored Normal file
View file

@ -0,0 +1,500 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"context"
"crypto"
"crypto/rand"
"crypto/x509"
"github.com/pion/dtls/v3/internal/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
"github.com/pion/dtls/v3/pkg/crypto/elliptic"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/crypto/signaturehash"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/alert"
"github.com/pion/dtls/v3/pkg/protocol/extension"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
//nolint:gocognit,gocyclo,lll,cyclop,maintidx
func flight4Parse(
ctx context.Context,
conn flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) (flightVal, *alert.Alert, error) {
seq, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence, state.cipherSuite,
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, true},
handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeCertificateVerify, cfg.initialEpoch, true, true},
)
if !ok {
// No valid message received. Keep reading
return 0, nil, nil
}
// Validate type
var clientKeyExchange *handshake.MessageClientKeyExchange
if clientKeyExchange, ok = msgs[handshake.TypeClientKeyExchange].(*handshake.MessageClientKeyExchange); !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
}
if h, hasCert := msgs[handshake.TypeCertificate].(*handshake.MessageCertificate); hasCert {
state.PeerCertificates = h.Certificate
// If the client offer its certificate, just disable session resumption.
// Otherwise, we have to store the certificate identitfication and expire time.
// And we have to check whether this certificate expired, revoked or changed.
//
// https://curl.se/docs/CVE-2016-5419.html
state.SessionID = nil
}
//nolint:nestif
if verify, hasVerify := msgs[handshake.TypeCertificateVerify].(*handshake.MessageCertificateVerify); hasVerify {
if state.PeerCertificates == nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.NoCertificate}, errCertificateVerifyNoCertificate
}
plainText := cache.pullAndMerge(
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
)
// Verify that the pair of hash algorithm and signiture is listed.
var validSignatureScheme bool
for _, ss := range cfg.localSignatureSchemes {
if ss.Hash == verify.HashAlgorithm && ss.Signature == verify.SignatureAlgorithm {
validSignatureScheme = true
break
}
}
if !validSignatureScheme {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errNoAvailableSignatureSchemes
}
if err := verifyCertificateVerify(
plainText,
verify.HashAlgorithm,
verify.SignatureAlgorithm,
verify.Signature,
state.PeerCertificates,
); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
}
var chains [][]*x509.Certificate
var err error
var verified bool
if cfg.clientAuth >= VerifyClientCertIfGiven {
// Use cert-specific algorithms if present, otherwise fall back to signature_algorithms per RFC 8446
certAlgs := cfg.localCertSignatureSchemes
if len(certAlgs) == 0 {
certAlgs = cfg.localSignatureSchemes
}
if chains, err = verifyClientCert(state.PeerCertificates, cfg.clientCAs, certAlgs); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
}
verified = true
}
if cfg.verifyPeerCertificate != nil {
if err := cfg.verifyPeerCertificate(state.PeerCertificates, chains); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
}
}
state.peerCertificatesVerified = verified
} else if state.PeerCertificates != nil {
// A certificate was received, but we haven't seen a CertificateVerify
// keep reading until we receive one
return 0, nil, nil
}
if !state.cipherSuite.IsInitialized() { //nolint:nestif
serverRandom := state.localRandom.MarshalFixed()
clientRandom := state.remoteRandom.MarshalFixed()
var err error
var preMasterSecret []byte
if state.cipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypePreSharedKey {
var psk []byte
if psk, err = cfg.localPSKCallback(clientKeyExchange.IdentityHint); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
state.IdentityHint = clientKeyExchange.IdentityHint
switch state.cipherSuite.KeyExchangeAlgorithm() {
case CipherSuiteKeyExchangeAlgorithmPsk:
preMasterSecret = prf.PSKPreMasterSecret(psk)
case (CipherSuiteKeyExchangeAlgorithmPsk | CipherSuiteKeyExchangeAlgorithmEcdhe):
if preMasterSecret, err = prf.EcdhePSKPreMasterSecret(
psk,
clientKeyExchange.PublicKey,
state.localKeypair.PrivateKey,
state.localKeypair.Curve,
); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
default:
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, errInvalidCipherSuite
}
} else {
preMasterSecret, err = prf.PreMasterSecret(
clientKeyExchange.PublicKey,
state.localKeypair.PrivateKey,
state.localKeypair.Curve,
)
if err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, err
}
}
if state.extendedMasterSecret {
var sessionHash []byte
sessionHash, err = cache.sessionHash(state.cipherSuite.HashFunc(), cfg.initialEpoch)
if err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
state.masterSecret, err = prf.ExtendedMasterSecret(preMasterSecret, sessionHash, state.cipherSuite.HashFunc())
if err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
} else {
state.masterSecret, err = prf.MasterSecret(
preMasterSecret,
clientRandom[:],
serverRandom[:],
state.cipherSuite.HashFunc(),
)
if err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
if err := state.cipherSuite.Init(state.masterSecret, clientRandom[:], serverRandom[:], false); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.masterSecret)
}
if len(state.SessionID) > 0 {
s := Session{
ID: state.SessionID,
Secret: state.masterSecret,
}
cfg.log.Tracef("[handshake] save new session: %x", s.ID)
if err := cfg.sessionStore.Set(state.SessionID, s); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
// Now, encrypted packets can be handled
if err := conn.handleQueuedPackets(ctx); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
seq, msgs, ok = cache.fullPullMap(seq, state.cipherSuite,
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
)
if !ok {
// No valid message received. Keep reading
return 0, nil, nil
}
state.handshakeRecvSequence = seq
if _, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
}
if state.cipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypeAnonymous { //nolint:nestif
if cfg.verifyConnection != nil {
stateClone, err := state.clone()
if err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
if err := cfg.verifyConnection(stateClone); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
}
}
return flight6, nil, nil
}
switch cfg.clientAuth {
case RequireAnyClientCert:
if state.PeerCertificates == nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.NoCertificate}, errClientCertificateRequired
}
case VerifyClientCertIfGiven:
if state.PeerCertificates != nil && !state.peerCertificatesVerified {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, errClientCertificateNotVerified
}
case RequireAndVerifyClientCert:
if state.PeerCertificates == nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.NoCertificate}, errClientCertificateRequired
}
if !state.peerCertificatesVerified {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, errClientCertificateNotVerified
}
case NoClientCert, RequestClientCert:
// go to flight6
}
if cfg.verifyConnection != nil {
stateClone, err := state.clone()
if err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
if err := cfg.verifyConnection(stateClone); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
}
}
return flight6, nil, nil
}
//nolint:gocognit,cyclop,maintidx
func flight4Generate(
_ flightConn,
state *State,
_ *handshakeCache,
cfg *handshakeConfig,
) ([]*packet, *alert.Alert, error) {
extensions := []extension.Extension{}
if (cfg.extendedMasterSecret == RequestExtendedMasterSecret ||
cfg.extendedMasterSecret == RequireExtendedMasterSecret) && state.extendedMasterSecret {
extensions = append(extensions, &extension.UseExtendedMasterSecret{
Supported: true,
})
}
if state.getSRTPProtectionProfile() != 0 {
extensions = append(extensions, &extension.UseSRTP{
ProtectionProfiles: []SRTPProtectionProfile{state.getSRTPProtectionProfile()},
MasterKeyIdentifier: cfg.localSRTPMasterKeyIdentifier,
})
}
if state.remoteSupportsRenegotiation {
extensions = append(extensions, &extension.RenegotiationInfo{
RenegotiatedConnection: 0,
})
}
if state.cipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypeCertificate {
extensions = append(extensions, &extension.SupportedPointFormats{
PointFormats: []elliptic.CurvePointFormat{elliptic.CurvePointFormatUncompressed},
})
}
selectedProto, err := extension.ALPNProtocolSelection(cfg.supportedProtocols, state.peerSupportedProtocols)
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.NoApplicationProtocol}, err
}
if selectedProto != "" {
extensions = append(extensions, &extension.ALPN{
ProtocolNameList: []string{selectedProto},
})
state.NegotiatedProtocol = selectedProto
}
// If we have a connection ID generator, we are willing to use connection
// IDs. We already know whether the client supports connection IDs from
// parsing the ClientHello, so avoid setting local connection ID if the
// client won't send it.
if cfg.connectionIDGenerator != nil && state.remoteConnectionID != nil {
state.setLocalConnectionID(cfg.connectionIDGenerator())
extensions = append(extensions, &extension.ConnectionID{CID: state.getLocalConnectionID()})
}
var pkts []*packet
cipherSuiteID := uint16(state.cipherSuite.ID())
if cfg.sessionStore != nil {
state.SessionID = make([]byte, sessionLength)
if _, err := rand.Read(state.SessionID); err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
serverHello := &handshake.MessageServerHello{
Version: protocol.Version1_2,
Random: state.localRandom,
SessionID: state.SessionID,
CipherSuiteID: &cipherSuiteID,
CompressionMethod: defaultCompressionMethods()[0],
Extensions: extensions,
}
var content handshake.Handshake
if cfg.serverHelloMessageHook != nil {
content = handshake.Handshake{Message: cfg.serverHelloMessageHook(*serverHello)}
} else {
content = handshake.Handshake{Message: serverHello}
}
pkts = append(pkts, &packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &content,
},
})
switch {
case state.cipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypeCertificate:
certificate, err := cfg.getCertificate(&ClientHelloInfo{
ServerName: state.serverName,
CipherSuites: []ciphersuite.ID{state.cipherSuite.ID()},
RandomBytes: state.remoteRandom.RandomBytes,
})
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, err
}
pkts = append(pkts, &packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &handshake.Handshake{
Message: &handshake.MessageCertificate{
Certificate: certificate.Certificate,
},
},
},
})
serverRandom := state.localRandom.MarshalFixed()
clientRandom := state.remoteRandom.MarshalFixed()
signer, ok := certificate.PrivateKey.(crypto.Signer)
if !ok {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, errInvalidPrivateKey
}
// Find compatible signature scheme
signatureHashAlgo, err := signaturehash.SelectSignatureScheme(cfg.localSignatureSchemes, signer)
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, err
}
signature, err := generateKeySignature(
clientRandom[:],
serverRandom[:],
state.localKeypair.PublicKey,
state.namedCurve,
signer,
signatureHashAlgo.Hash,
signatureHashAlgo.Signature,
)
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
state.localKeySignature = signature
pkts = append(pkts, &packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &handshake.Handshake{
Message: &handshake.MessageServerKeyExchange{
EllipticCurveType: elliptic.CurveTypeNamedCurve,
NamedCurve: state.namedCurve,
PublicKey: state.localKeypair.PublicKey,
HashAlgorithm: signatureHashAlgo.Hash,
SignatureAlgorithm: signatureHashAlgo.Signature,
Signature: state.localKeySignature,
},
},
},
})
if cfg.clientAuth > NoClientCert {
// An empty list of certificateAuthorities signals to
// the client that it may send any certificate in response
// to our request. When we know the CAs we trust, then
// we can send them down, so that the client can choose
// an appropriate certificate to give to us.
var certificateAuthorities [][]byte
if cfg.clientCAs != nil {
// nolint:staticcheck // ignoring tlsCert.RootCAs.Subjects is deprecated ERR
// because cert does not come from SystemCertPool and it's ok if certificate
// authorities is empty.
certificateAuthorities = cfg.clientCAs.Subjects()
}
certReq := &handshake.MessageCertificateRequest{
CertificateTypes: []clientcertificate.Type{clientcertificate.RSASign, clientcertificate.ECDSASign},
SignatureHashAlgorithms: cfg.localSignatureSchemes,
CertificateAuthoritiesNames: certificateAuthorities,
}
var content handshake.Handshake
if cfg.certificateRequestMessageHook != nil {
content = handshake.Handshake{Message: cfg.certificateRequestMessageHook(*certReq)}
} else {
content = handshake.Handshake{Message: certReq}
}
pkts = append(pkts, &packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &content,
},
})
}
case cfg.localPSKIdentityHint != nil ||
state.cipherSuite.KeyExchangeAlgorithm().Has(CipherSuiteKeyExchangeAlgorithmEcdhe):
// To help the client in selecting which identity to use, the server
// can provide a "PSK identity hint" in the ServerKeyExchange message.
// If no hint is provided and cipher suite doesn't use elliptic curve,
// the ServerKeyExchange message is omitted.
//
// https://tools.ietf.org/html/rfc4279#section-2
srvExchange := &handshake.MessageServerKeyExchange{
IdentityHint: cfg.localPSKIdentityHint,
}
if state.cipherSuite.KeyExchangeAlgorithm().Has(CipherSuiteKeyExchangeAlgorithmEcdhe) {
srvExchange.EllipticCurveType = elliptic.CurveTypeNamedCurve
srvExchange.NamedCurve = state.namedCurve
srvExchange.PublicKey = state.localKeypair.PublicKey
}
pkts = append(pkts, &packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &handshake.Handshake{
Message: srvExchange,
},
},
})
}
pkts = append(pkts, &packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &handshake.Handshake{
Message: &handshake.MessageServerHelloDone{},
},
},
})
return pkts, nil, nil
}

89
vendor/github.com/pion/dtls/v3/flight5bhandler.go generated vendored Normal file
View file

@ -0,0 +1,89 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"context"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/alert"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
func flight5bParse(
_ context.Context,
_ flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) (flightVal, *alert.Alert, error) {
_, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence-1, state.cipherSuite,
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
)
if !ok {
// No valid message received. Keep reading
return 0, nil, nil
}
if _, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
}
// Other party may re-transmit the last flight. Keep state to be flight5b.
return flight5b, nil, nil
}
func flight5bGenerate(
_ flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) ([]*packet, *alert.Alert, error) { //nolint:gocognit
var pkts []*packet
pkts = append(pkts,
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &protocol.ChangeCipherSpec{},
},
})
if len(state.localVerifyData) == 0 {
plainText := cache.pullAndMerge(
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
)
var err error
state.localVerifyData, err = prf.VerifyDataClient(state.masterSecret, plainText, state.cipherSuite.HashFunc())
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
pkts = append(pkts,
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
Epoch: 1,
},
Content: &handshake.Handshake{
Message: &handshake.MessageFinished{
VerifyData: state.localVerifyData,
},
},
},
shouldEncrypt: true,
resetLocalSequenceNumber: true,
})
return pkts, nil, nil
}

410
vendor/github.com/pion/dtls/v3/flight5handler.go generated vendored Normal file
View file

@ -0,0 +1,410 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"bytes"
"context"
"crypto"
"crypto/x509"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/crypto/signaturehash"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/alert"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
func flight5Parse(
_ context.Context,
conn flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) (flightVal, *alert.Alert, error) {
_, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence, state.cipherSuite,
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, false, false},
)
if !ok {
// No valid message received. Keep reading
return 0, nil, nil
}
var finished *handshake.MessageFinished
if finished, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
}
plainText := cache.pullAndMerge(
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeCertificateVerify, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
)
expectedVerifyData, err := prf.VerifyDataServer(state.masterSecret, plainText, state.cipherSuite.HashFunc())
if err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
if !bytes.Equal(expectedVerifyData, finished.VerifyData) {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, errVerifyDataMismatch
}
if len(state.SessionID) > 0 {
s := Session{
ID: state.SessionID,
Secret: state.masterSecret,
}
cfg.log.Tracef("[handshake] save new session: %x", s.ID)
if err := cfg.sessionStore.Set(conn.sessionKey(), s); err != nil {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
return flight5, nil, nil
}
//nolint:gocognit,cyclop,maintidx
func flight5Generate(
conn flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) ([]*packet, *alert.Alert, error) {
var signer crypto.Signer
var pkts []*packet
if state.remoteRequestedCertificate { //nolint:nestif
_, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence-2, state.cipherSuite,
handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false})
if !ok {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, errClientCertificateRequired
}
reqInfo := CertificateRequestInfo{}
if r, ok2 := msgs[handshake.TypeCertificateRequest].(*handshake.MessageCertificateRequest); ok2 {
reqInfo.AcceptableCAs = r.CertificateAuthoritiesNames
} else {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, errClientCertificateRequired
}
certificate, err := cfg.getClientCertificate(&reqInfo)
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, err
}
if certificate == nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, errNotAcceptableCertificateChain
}
if certificate.Certificate != nil {
signer, ok = certificate.PrivateKey.(crypto.Signer)
if !ok {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.HandshakeFailure}, errInvalidPrivateKey
}
}
pkts = append(pkts,
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &handshake.Handshake{
Message: &handshake.MessageCertificate{
Certificate: certificate.Certificate,
},
},
},
})
}
clientKeyExchange := &handshake.MessageClientKeyExchange{}
if cfg.localPSKCallback == nil {
clientKeyExchange.PublicKey = state.localKeypair.PublicKey
} else {
clientKeyExchange.IdentityHint = cfg.localPSKIdentityHint
}
if state != nil && state.localKeypair != nil && len(state.localKeypair.PublicKey) > 0 {
clientKeyExchange.PublicKey = state.localKeypair.PublicKey
}
pkts = append(pkts,
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &handshake.Handshake{
Message: clientKeyExchange,
},
},
})
serverKeyExchangeData := cache.pullAndMerge(
handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
)
serverKeyExchange := &handshake.MessageServerKeyExchange{}
// handshakeMessageServerKeyExchange is optional for PSK
if len(serverKeyExchangeData) == 0 {
alertPtr, err := handleServerKeyExchange(conn, state, cfg, &handshake.MessageServerKeyExchange{})
if err != nil {
return nil, alertPtr, err
}
} else {
rawHandshake := &handshake.Handshake{
KeyExchangeAlgorithm: state.cipherSuite.KeyExchangeAlgorithm(),
}
err := rawHandshake.Unmarshal(serverKeyExchangeData)
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage}, err
}
switch h := rawHandshake.Message.(type) {
case *handshake.MessageServerKeyExchange:
serverKeyExchange = h
default:
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.UnexpectedMessage}, errInvalidContentType
}
}
// Append not-yet-sent packets
merged := []byte{}
seqPred := uint16(state.handshakeSendSequence) //nolint:gosec // G115
for _, p := range pkts {
h, ok := p.record.Content.(*handshake.Handshake)
if !ok {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, errInvalidContentType
}
h.Header.MessageSequence = seqPred
seqPred++
raw, err := h.Marshal()
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
merged = append(merged, raw...)
}
if alertPtr, err := initializeCipherSuite(state, cache, cfg, serverKeyExchange, merged); err != nil {
return nil, alertPtr, err
}
// If the client has sent a certificate with signing ability, a digitally-signed
// CertificateVerify message is sent to explicitly verify possession of the
// private key in the certificate.
if state.remoteRequestedCertificate && signer != nil {
plainText := append(cache.pullAndMerge(
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
), merged...)
// Find compatible signature scheme
signatureHashAlgo, err := signaturehash.SelectSignatureScheme(state.remoteCertRequestAlgs, signer)
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, err
}
certVerify, err := generateCertificateVerify(plainText, signer, signatureHashAlgo.Hash, signatureHashAlgo.Signature)
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
state.localCertificatesVerify = certVerify
pkt := &packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &handshake.Handshake{
Message: &handshake.MessageCertificateVerify{
HashAlgorithm: signatureHashAlgo.Hash,
SignatureAlgorithm: signatureHashAlgo.Signature,
Signature: state.localCertificatesVerify,
},
},
},
}
pkts = append(pkts, pkt)
h, ok := pkt.record.Content.(*handshake.Handshake)
if !ok {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, errInvalidContentType
}
h.Header.MessageSequence = seqPred
// seqPred++ // this is the last use of seqPred
raw, err := h.Marshal()
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
merged = append(merged, raw...)
}
pkts = append(pkts,
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &protocol.ChangeCipherSpec{},
},
})
if len(state.localVerifyData) == 0 {
plainText := cache.pullAndMerge(
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeCertificateVerify, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
)
var err error
state.localVerifyData, err = prf.VerifyDataClient(
state.masterSecret,
append(plainText, merged...),
state.cipherSuite.HashFunc(),
)
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
pkts = append(pkts,
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
Epoch: 1,
},
Content: &handshake.Handshake{
Message: &handshake.MessageFinished{
VerifyData: state.localVerifyData,
},
},
},
shouldWrapCID: len(state.remoteConnectionID) > 0,
shouldEncrypt: true,
resetLocalSequenceNumber: true,
})
return pkts, nil, nil
}
//nolint:gocognit,cyclop
func initializeCipherSuite(
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
handshakeKeyExchange *handshake.MessageServerKeyExchange,
sendingPlainText []byte,
) (*alert.Alert, error) {
if state.cipherSuite.IsInitialized() {
return nil, nil //nolint
}
clientRandom := state.localRandom.MarshalFixed()
serverRandom := state.remoteRandom.MarshalFixed()
var err error
if state.extendedMasterSecret {
var sessionHash []byte
sessionHash, err = cache.sessionHash(state.cipherSuite.HashFunc(), cfg.initialEpoch, sendingPlainText)
if err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
state.masterSecret, err = prf.ExtendedMasterSecret(state.preMasterSecret, sessionHash, state.cipherSuite.HashFunc())
if err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.IllegalParameter}, err
}
} else {
state.masterSecret, err = prf.MasterSecret(
state.preMasterSecret,
clientRandom[:],
serverRandom[:],
state.cipherSuite.HashFunc(),
)
if err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
if state.cipherSuite.AuthenticationType() == CipherSuiteAuthenticationTypeCertificate { //nolint:nestif
// Verify that the pair of hash algorithm and signiture is listed.
var validSignatureScheme bool
for _, ss := range cfg.localSignatureSchemes {
if ss.Hash == handshakeKeyExchange.HashAlgorithm && ss.Signature == handshakeKeyExchange.SignatureAlgorithm {
validSignatureScheme = true
break
}
}
if !validSignatureScheme {
return &alert.Alert{Level: alert.Fatal, Description: alert.InsufficientSecurity}, errNoAvailableSignatureSchemes
}
expectedMsg := valueKeyMessage(
clientRandom[:],
serverRandom[:],
handshakeKeyExchange.PublicKey,
handshakeKeyExchange.NamedCurve,
)
if err = verifyKeySignature(
expectedMsg,
handshakeKeyExchange.Signature,
handshakeKeyExchange.HashAlgorithm,
handshakeKeyExchange.SignatureAlgorithm,
state.PeerCertificates,
); err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
}
var chains [][]*x509.Certificate
if !cfg.insecureSkipVerify {
certAlgs := cfg.localCertSignatureSchemes
if len(certAlgs) == 0 {
certAlgs = cfg.localSignatureSchemes
}
if chains, err = verifyServerCert(state.PeerCertificates, cfg.rootCAs, cfg.serverName, certAlgs); err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
}
}
if cfg.verifyPeerCertificate != nil {
if err = cfg.verifyPeerCertificate(state.PeerCertificates, chains); err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, err
}
}
}
if cfg.verifyConnection != nil {
stateClone, errC := state.clone()
if errC != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, errC
}
if errC = cfg.verifyConnection(stateClone); errC != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.BadCertificate}, errC
}
}
if err = state.cipherSuite.Init(state.masterSecret, clientRandom[:], serverRandom[:], true); err != nil {
return &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
cfg.writeKeyLog(keyLogLabelTLS12, clientRandom[:], state.masterSecret)
return nil, nil //nolint
}

98
vendor/github.com/pion/dtls/v3/flight6handler.go generated vendored Normal file
View file

@ -0,0 +1,98 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"context"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/alert"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
func flight6Parse(
_ context.Context,
_ flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) (flightVal, *alert.Alert, error) {
_, msgs, ok := cache.fullPullMap(state.handshakeRecvSequence-1, state.cipherSuite,
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
)
if !ok {
// No valid message received. Keep reading
return 0, nil, nil
}
if _, ok = msgs[handshake.TypeFinished].(*handshake.MessageFinished); !ok {
return 0, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, nil
}
// Other party may re-transmit the last flight. Keep state to be flight6.
return flight6, nil, nil
}
func flight6Generate(
_ flightConn,
state *State,
cache *handshakeCache,
cfg *handshakeConfig,
) ([]*packet, *alert.Alert, error) {
var pkts []*packet
pkts = append(pkts,
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
},
Content: &protocol.ChangeCipherSpec{},
},
})
if len(state.localVerifyData) == 0 {
plainText := cache.pullAndMerge(
handshakeCachePullRule{handshake.TypeClientHello, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeServerHello, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeServerKeyExchange, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificateRequest, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeServerHelloDone, cfg.initialEpoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeClientKeyExchange, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeCertificateVerify, cfg.initialEpoch, true, false},
handshakeCachePullRule{handshake.TypeFinished, cfg.initialEpoch + 1, true, false},
)
var err error
state.localVerifyData, err = prf.VerifyDataServer(state.masterSecret, plainText, state.cipherSuite.HashFunc())
if err != nil {
return nil, &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}, err
}
}
pkts = append(pkts,
&packet{
record: &recordlayer.RecordLayer{
Header: recordlayer.Header{
Version: protocol.Version1_2,
Epoch: 1,
},
Content: &handshake.Handshake{
Message: &handshake.MessageFinished{
VerifyData: state.localVerifyData,
},
},
},
shouldWrapCID: len(state.remoteConnectionID) > 0,
shouldEncrypt: true,
resetLocalSequenceNumber: true,
},
)
return pkts, nil, nil
}

74
vendor/github.com/pion/dtls/v3/flighthandler.go generated vendored Normal file
View file

@ -0,0 +1,74 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"context"
"github.com/pion/dtls/v3/pkg/protocol/alert"
)
// Parse received handshakes and return next flightVal.
type flightParser func(
context.Context,
flightConn,
*State,
*handshakeCache,
*handshakeConfig,
) (flightVal, *alert.Alert, error)
// Generate flights.
type flightGenerator func(flightConn, *State, *handshakeCache, *handshakeConfig) ([]*packet, *alert.Alert, error)
func (f flightVal) getFlightParser() (flightParser, error) { //nolint:cyclop
switch f {
case flight0:
return flight0Parse, nil
case flight1:
return flight1Parse, nil
case flight2:
return flight2Parse, nil
case flight3:
return flight3Parse, nil
case flight4:
return flight4Parse, nil
case flight4b:
return flight4bParse, nil
case flight5:
return flight5Parse, nil
case flight5b:
return flight5bParse, nil
case flight6:
return flight6Parse, nil
default:
return nil, errInvalidFlight
}
}
func (f flightVal) getFlightGenerator() (gen flightGenerator, retransmit bool, err error) { //nolint:cyclop
switch f {
case flight0:
return flight0Generate, true, nil
case flight1:
return flight1Generate, true, nil
case flight2:
// https://tools.ietf.org/html/rfc6347#section-3.2.1
// HelloVerifyRequests must not be retransmitted.
return flight2Generate, false, nil
case flight3:
return flight3Generate, true, nil
case flight4:
return flight4Generate, true, nil
case flight4b:
return flight4bGenerate, true, nil
case flight5:
return flight5Generate, true, nil
case flight5b:
return flight5bGenerate, true, nil
case flight6:
return flight6Generate, true, nil
default:
return nil, false, errInvalidFlight
}
}

151
vendor/github.com/pion/dtls/v3/fragment_buffer.go generated vendored Normal file
View file

@ -0,0 +1,151 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
const (
// 2 megabytes.
fragmentBufferMaxSize = 2000000
fragmentBufferMaxCount = 1000
)
type fragment struct {
recordLayerHeader recordlayer.Header
handshakeHeader handshake.Header
data []byte
}
type fragments struct {
fragmentByOffset map[uint32]*fragment
fragmentsLength uint32
handshakeLength uint32
}
type fragmentBuffer struct {
// map of MessageSequenceNumbers that hold slices of fragments
cache map[uint16]*fragments
currentMessageSequenceNumber uint16
totalBufferSize int
totalFragmentCount int
}
func newFragmentBuffer() *fragmentBuffer {
return &fragmentBuffer{cache: map[uint16]*fragments{}}
}
// current total size of buffer.
func (f *fragmentBuffer) size() int {
return f.totalBufferSize
}
// Attempts to push a DTLS packet to the fragmentBuffer
// when it returns true it means the fragmentBuffer has inserted and the buffer shouldn't be handled
// when an error returns it is fatal, and the DTLS connection should be stopped.
func (f *fragmentBuffer) push(buf []byte) (isHandshake, isRetransmit bool, err error) { //nolint:cyclop
if f.size()+len(buf) >= fragmentBufferMaxSize || f.totalFragmentCount >= fragmentBufferMaxCount {
return false, false, errFragmentBufferOverflow
}
recordLayerHeader := recordlayer.Header{}
if err := recordLayerHeader.Unmarshal(buf); err != nil {
return false, false, err
}
// fragment isn't a handshake, we don't need to handle it
if recordLayerHeader.ContentType != protocol.ContentTypeHandshake {
return false, false, nil
}
frag := new(fragment)
for buf = buf[recordlayer.FixedHeaderSize:]; len(buf) != 0; frag = new(fragment) { //nolint:gosec // G602
if err := frag.handshakeHeader.Unmarshal(buf); err != nil {
return false, false, err
}
// Fragment is a retransmission. We have already assembled it before successfully
isRetransmit = frag.handshakeHeader.FragmentOffset == 0 &&
frag.handshakeHeader.MessageSequence < f.currentMessageSequenceNumber
end := int(handshake.HeaderLength + frag.handshakeHeader.FragmentLength)
if end > len(buf) {
return false, false, errBufferTooSmall
}
if frag.handshakeHeader.MessageSequence < f.currentMessageSequenceNumber {
buf = buf[end:]
continue
}
messageFragments, ok := f.cache[frag.handshakeHeader.MessageSequence]
if !ok {
messageFragments = &fragments{
fragmentByOffset: map[uint32]*fragment{}, handshakeLength: frag.handshakeHeader.Length,
}
f.cache[frag.handshakeHeader.MessageSequence] = messageFragments
}
// Discard all headers, when rebuilding the packet we will re-build
frag.data = append([]byte{}, buf[handshake.HeaderLength:end]...)
frag.recordLayerHeader = recordLayerHeader
if _, ok = messageFragments.fragmentByOffset[frag.handshakeHeader.FragmentOffset]; !ok {
messageFragments.fragmentByOffset[frag.handshakeHeader.FragmentOffset] = frag
messageFragments.fragmentsLength += frag.handshakeHeader.FragmentLength
f.totalBufferSize += int(frag.handshakeHeader.FragmentLength)
f.totalFragmentCount++
}
buf = buf[end:]
}
return true, isRetransmit, nil
}
func (f *fragmentBuffer) pop() (content []byte, epoch uint16) {
frags, ok := f.cache[f.currentMessageSequenceNumber]
if !ok {
return nil, 0
}
if frags.fragmentsLength != frags.handshakeLength {
return nil, 0
}
var rawMessage []byte
targetOffset := uint32(0)
for i := 0; i < len(frags.fragmentByOffset) && targetOffset < frags.handshakeLength; i++ {
if frag, ok := frags.fragmentByOffset[targetOffset]; ok {
rawMessage = append(rawMessage, frag.data...)
targetOffset = frag.handshakeHeader.FragmentOffset + frag.handshakeHeader.FragmentLength
} else {
return nil, 0
}
}
if int(frags.handshakeLength) != len(rawMessage) {
return nil, 0
}
firstHeader := frags.fragmentByOffset[0].handshakeHeader
firstHeader.FragmentOffset = 0
firstHeader.FragmentLength = firstHeader.Length
rawHeader, _ := firstHeader.Marshal()
messageEpoch := frags.fragmentByOffset[0].recordLayerHeader.Epoch
f.totalBufferSize -= int(frags.fragmentsLength)
f.totalFragmentCount -= len(frags.fragmentByOffset)
delete(f.cache, f.currentMessageSequenceNumber)
f.currentMessageSequenceNumber++
return append(rawHeader, rawMessage...), messageEpoch
}

185
vendor/github.com/pion/dtls/v3/handshake_cache.go generated vendored Normal file
View file

@ -0,0 +1,185 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"sync"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
)
type handshakeCacheItem struct {
typ handshake.Type
isClient bool
epoch uint16
messageSequence uint16
data []byte
}
type handshakeCachePullRule struct {
typ handshake.Type
epoch uint16
isClient bool
optional bool
}
type handshakeCache struct {
cache []*handshakeCacheItem
mu sync.Mutex
}
func newHandshakeCache() *handshakeCache {
return &handshakeCache{}
}
func (h *handshakeCache) push(data []byte, epoch, messageSequence uint16, typ handshake.Type, isClient bool) {
h.mu.Lock()
defer h.mu.Unlock()
h.cache = append(h.cache, &handshakeCacheItem{
data: append([]byte{}, data...),
epoch: epoch,
messageSequence: messageSequence,
typ: typ,
isClient: isClient,
})
}
// returns a list handshakes that match the requested rules
// the list will contain null entries for rules that can't be satisfied
// multiple entries may match a rule, but only the last match is returned (ie ClientHello with cookies).
func (h *handshakeCache) pull(rules ...handshakeCachePullRule) []*handshakeCacheItem {
h.mu.Lock()
defer h.mu.Unlock()
out := make([]*handshakeCacheItem, len(rules))
for i, r := range rules {
for _, c := range h.cache {
if c.typ == r.typ && c.isClient == r.isClient && c.epoch == r.epoch {
switch {
case out[i] == nil:
out[i] = c
case out[i].messageSequence < c.messageSequence:
out[i] = c
}
}
}
}
return out
}
// fullPullMap pulls all handshakes between rules[0] to rules[len(rules)-1] as map.
//
//nolint:cyclop
func (h *handshakeCache) fullPullMap(
startSeq int,
cipherSuite CipherSuite,
rules ...handshakeCachePullRule,
) (int, map[handshake.Type]handshake.Message, bool) {
h.mu.Lock()
defer h.mu.Unlock()
ci := make(map[handshake.Type]*handshakeCacheItem)
for _, rule := range rules {
var item *handshakeCacheItem
for _, c := range h.cache {
if c.typ == rule.typ && c.isClient == rule.isClient && c.epoch == rule.epoch {
switch {
case item == nil:
item = c
case item.messageSequence < c.messageSequence:
item = c
}
}
}
if !rule.optional && item == nil {
// Missing mandatory message.
return startSeq, nil, false
}
ci[rule.typ] = item
}
out := make(map[handshake.Type]handshake.Message)
seq := startSeq
ok := false
for _, r := range rules {
typ := r.typ
i := ci[typ]
if i == nil {
continue
}
var keyExchangeAlgorithm CipherSuiteKeyExchangeAlgorithm
if cipherSuite != nil {
keyExchangeAlgorithm = cipherSuite.KeyExchangeAlgorithm()
}
rawHandshake := &handshake.Handshake{
KeyExchangeAlgorithm: keyExchangeAlgorithm,
}
if err := rawHandshake.Unmarshal(i.data); err != nil {
return startSeq, nil, false
}
if uint16(seq) != rawHandshake.Header.MessageSequence { //nolint:gosec // G115
// There is a gap. Some messages are not arrived.
return startSeq, nil, false
}
seq++
ok = true
out[typ] = rawHandshake.Message
}
if !ok {
return seq, nil, false
}
return seq, out, true
}
// pullAndMerge calls pull and then merges the results, ignoring any null entries.
func (h *handshakeCache) pullAndMerge(rules ...handshakeCachePullRule) []byte {
merged := []byte{}
for _, p := range h.pull(rules...) {
if p != nil {
merged = append(merged, p.data...)
}
}
return merged
}
// sessionHash returns the session hash for Extended Master Secret support
// https://tools.ietf.org/html/draft-ietf-tls-session-hash-06#section-4
func (h *handshakeCache) sessionHash(hf prf.HashFunc, epoch uint16, additional ...[]byte) ([]byte, error) {
merged := []byte{}
// Order defined by https://tools.ietf.org/html/rfc5246#section-7.3
handshakeBuffer := h.pull(
handshakeCachePullRule{handshake.TypeClientHello, epoch, true, false},
handshakeCachePullRule{handshake.TypeServerHello, epoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, epoch, false, false},
handshakeCachePullRule{handshake.TypeServerKeyExchange, epoch, false, false},
handshakeCachePullRule{handshake.TypeCertificateRequest, epoch, false, false},
handshakeCachePullRule{handshake.TypeServerHelloDone, epoch, false, false},
handshakeCachePullRule{handshake.TypeCertificate, epoch, true, false},
handshakeCachePullRule{handshake.TypeClientKeyExchange, epoch, true, false},
)
for _, p := range handshakeBuffer {
if p == nil {
continue
}
merged = append(merged, p.data...)
}
for _, a := range additional {
merged = append(merged, a...)
}
hash := hf()
if _, err := hash.Write(merged); err != nil {
return []byte{}, err
}
return hash.Sum(nil), nil
}

364
vendor/github.com/pion/dtls/v3/handshaker.go generated vendored Normal file
View file

@ -0,0 +1,364 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"sync"
"time"
"github.com/pion/dtls/v3/pkg/crypto/elliptic"
"github.com/pion/dtls/v3/pkg/crypto/signaturehash"
"github.com/pion/dtls/v3/pkg/protocol/alert"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/logging"
)
// [RFC6347 Section-4.2.4]
// +-----------+
// +---> | PREPARING | <--------------------+
// | +-----------+ |
// | | |
// | | Buffer next flight |
// | | |
// | \|/ |
// | +-----------+ |
// | | SENDING |<------------------+ | Send
// | +-----------+ | | HelloRequest
// Receive | | | |
// next | | Send flight | | or
// flight | +--------+ | |
// | | | Set retransmit timer | | Receive
// | | \|/ | | HelloRequest
// | | +-----------+ | | Send
// +--)--| WAITING |-------------------+ | ClientHello
// | | +-----------+ Timer expires | |
// | | | | |
// | | +------------------------+ |
// Receive | | Send Read retransmit |
// last | | last |
// flight | | flight |
// | | |
// \|/\|/ |
// +-----------+ |
// | FINISHED | -------------------------------+
// +-----------+
// | /|\
// | |
// +---+
// Read retransmit
// Retransmit last flight
type handshakeState uint8
const (
handshakeErrored handshakeState = iota
handshakePreparing
handshakeSending
handshakeWaiting
handshakeFinished
)
func (s handshakeState) String() string {
switch s {
case handshakeErrored:
return "Errored"
case handshakePreparing:
return "Preparing"
case handshakeSending:
return "Sending"
case handshakeWaiting:
return "Waiting"
case handshakeFinished:
return "Finished"
default:
return "Unknown"
}
}
type handshakeFSM struct {
currentFlight flightVal
flights []*packet
retransmit bool
retransmitInterval time.Duration
state *State
cache *handshakeCache
cfg *handshakeConfig
closed chan struct{}
}
type handshakeConfig struct {
localPSKCallback PSKCallback
localPSKIdentityHint []byte
localCipherSuites []CipherSuite // Available CipherSuites
localSignatureSchemes []signaturehash.Algorithm // Available signature schemes
localCertSignatureSchemes []signaturehash.Algorithm // Available signature schemes for certificates
extendedMasterSecret ExtendedMasterSecretType // Policy for the Extended Master Support extension
localSRTPProtectionProfiles []SRTPProtectionProfile // Available SRTPProtectionProfiles, if empty no SRTP support
localSRTPMasterKeyIdentifier []byte
serverName string
supportedProtocols []string
clientAuth ClientAuthType // If we are a client should we request a client certificate
localCertificates []tls.Certificate
nameToCertificate map[string]*tls.Certificate
insecureSkipVerify bool
verifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
verifyConnection func(*State) error
sessionStore SessionStore
rootCAs *x509.CertPool
clientCAs *x509.CertPool
initialRetransmitInterval time.Duration
disableRetransmitBackoff bool
customCipherSuites func() []CipherSuite
ellipticCurves []elliptic.Curve
insecureSkipHelloVerify bool
connectionIDGenerator func() []byte
helloRandomBytesGenerator func() [handshake.RandomBytesLength]byte
onFlightState func(flightVal, handshakeState)
log logging.LeveledLogger
keyLogWriter io.Writer
localGetCertificate func(*ClientHelloInfo) (*tls.Certificate, error)
localGetClientCertificate func(*CertificateRequestInfo) (*tls.Certificate, error)
initialEpoch uint16
mu sync.Mutex
clientHelloMessageHook func(handshake.MessageClientHello) handshake.Message
serverHelloMessageHook func(handshake.MessageServerHello) handshake.Message
certificateRequestMessageHook func(handshake.MessageCertificateRequest) handshake.Message
resumeState *State
}
type flightConn interface {
notify(ctx context.Context, level alert.Level, desc alert.Description) error
writePackets(context.Context, []*packet) error
recvHandshake() <-chan recvHandshakeState
setLocalEpoch(epoch uint16)
handleQueuedPackets(context.Context) error
sessionKey() []byte
}
func (c *handshakeConfig) writeKeyLog(label string, clientRandom, secret []byte) {
if c.keyLogWriter == nil {
return
}
c.mu.Lock()
defer c.mu.Unlock()
_, err := fmt.Fprintf(c.keyLogWriter, "%s %x %x\n", label, clientRandom, secret)
if err != nil {
c.log.Debugf("failed to write key log file: %s", err)
}
}
func srvCliStr(isClient bool) string {
if isClient {
return "client"
}
return "server"
}
func newHandshakeFSM(
s *State, cache *handshakeCache, cfg *handshakeConfig,
initialFlight flightVal,
) *handshakeFSM {
return &handshakeFSM{
currentFlight: initialFlight,
state: s,
cache: cache,
cfg: cfg,
retransmitInterval: cfg.initialRetransmitInterval,
closed: make(chan struct{}),
}
}
func (s *handshakeFSM) Run(ctx context.Context, conn flightConn, initialState handshakeState) error {
state := initialState
defer func() {
close(s.closed)
}()
for {
s.cfg.log.Tracef("[handshake:%s] %s: %s", srvCliStr(s.state.isClient), s.currentFlight.String(), state.String())
if s.cfg.onFlightState != nil {
s.cfg.onFlightState(s.currentFlight, state)
}
var err error
switch state {
case handshakePreparing:
state, err = s.prepare(ctx, conn)
case handshakeSending:
state, err = s.send(ctx, conn)
case handshakeWaiting:
state, err = s.wait(ctx, conn)
case handshakeFinished:
state, err = s.finish(ctx, conn)
default:
return errInvalidFSMTransition
}
if err != nil {
return err
}
}
}
func (s *handshakeFSM) Done() <-chan struct{} {
return s.closed
}
func (s *handshakeFSM) prepare(ctx context.Context, conn flightConn) (handshakeState, error) {
s.flights = nil
// Prepare flights
var (
dtlsAlert *alert.Alert
err error
pkts []*packet
)
gen, retransmit, errFlight := s.currentFlight.getFlightGenerator()
if errFlight != nil {
err = errFlight
dtlsAlert = &alert.Alert{Level: alert.Fatal, Description: alert.InternalError}
} else {
pkts, dtlsAlert, err = gen(conn, s.state, s.cache, s.cfg)
s.retransmit = retransmit
}
if dtlsAlert != nil {
if alertErr := conn.notify(ctx, dtlsAlert.Level, dtlsAlert.Description); alertErr != nil {
if err != nil {
err = alertErr
}
}
}
if err != nil {
return handshakeErrored, err
}
s.flights = pkts
epoch := s.cfg.initialEpoch
nextEpoch := epoch
for _, p := range s.flights {
p.record.Header.Epoch += epoch
if p.record.Header.Epoch > nextEpoch {
nextEpoch = p.record.Header.Epoch
}
if h, ok := p.record.Content.(*handshake.Handshake); ok {
h.Header.MessageSequence = uint16(s.state.handshakeSendSequence) //nolint:gosec // G115
s.state.handshakeSendSequence++
}
}
if epoch != nextEpoch {
s.cfg.log.Tracef("[handshake:%s] -> changeCipherSpec (epoch: %d)", srvCliStr(s.state.isClient), nextEpoch)
conn.setLocalEpoch(nextEpoch)
}
return handshakeSending, nil
}
func (s *handshakeFSM) send(ctx context.Context, c flightConn) (handshakeState, error) {
// Send flights
if err := c.writePackets(ctx, s.flights); err != nil {
return handshakeErrored, err
}
if s.currentFlight.isLastSendFlight() {
return handshakeFinished, nil
}
return handshakeWaiting, nil
}
func (s *handshakeFSM) wait(ctx context.Context, conn flightConn) (handshakeState, error) { //nolint:gocognit,cyclop
parse, errFlight := s.currentFlight.getFlightParser()
if errFlight != nil {
if alertErr := conn.notify(ctx, alert.Fatal, alert.InternalError); alertErr != nil {
return handshakeErrored, alertErr
}
return handshakeErrored, errFlight
}
retransmitTimer := time.NewTimer(s.retransmitInterval)
for {
select {
case state := <-conn.recvHandshake():
if state.isRetransmit {
close(state.done)
// ignore incoming retransmit hints, only rely on the timer-driven path below
// https://github.com/pion/dtls/issues/758
continue
}
nextFlight, alert, err := parse(ctx, conn, s.state, s.cache, s.cfg)
s.retransmitInterval = s.cfg.initialRetransmitInterval
close(state.done)
if alert != nil {
if alertErr := conn.notify(ctx, alert.Level, alert.Description); alertErr != nil {
if err != nil {
err = alertErr
}
}
}
if err != nil {
return handshakeErrored, err
}
if nextFlight == 0 {
break
}
s.cfg.log.Tracef(
"[handshake:%s] %s -> %s",
srvCliStr(s.state.isClient),
s.currentFlight.String(),
nextFlight.String(),
)
if nextFlight.isLastRecvFlight() && s.currentFlight == nextFlight {
return handshakeFinished, nil
}
s.currentFlight = nextFlight
return handshakePreparing, nil
case <-retransmitTimer.C:
if !s.retransmit {
return handshakeWaiting, nil
}
// RFC 4347 4.2.4.1:
// Implementations SHOULD use an initial timer value of 1 second (the minimum defined in RFC 2988 [RFC2988])
// and double the value at each retransmission, up to no less than the RFC 2988 maximum of 60 seconds.
if !s.cfg.disableRetransmitBackoff {
s.retransmitInterval *= 2
}
if s.retransmitInterval > time.Second*60 {
s.retransmitInterval = time.Second * 60
}
return handshakeSending, nil
case <-ctx.Done():
s.retransmitInterval = s.cfg.initialRetransmitInterval
return handshakeErrored, ctx.Err()
}
}
}
func (s *handshakeFSM) finish(ctx context.Context, c flightConn) (handshakeState, error) {
select {
case state := <-c.recvHandshake():
close(state.done)
if s.state.isClient {
return handshakeFinished, nil
} else {
return handshakeSending, nil
}
case <-ctx.Done():
return handshakeErrored, ctx.Err()
}
}

View file

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
)
// Aes128Ccm is a base class used by multiple AES-CCM Ciphers.
type Aes128Ccm struct {
AesCcm
}
func newAes128Ccm(
clientCertificateType clientcertificate.Type,
id ID,
psk bool,
cryptoCCMTagLen ciphersuite.CCMTagLen,
keyExchangeAlgorithm KeyExchangeAlgorithm,
ecc bool,
) *Aes128Ccm {
return &Aes128Ccm{
AesCcm: AesCcm{
clientCertificateType: clientCertificateType,
id: id,
psk: psk,
cryptoCCMTagLen: cryptoCCMTagLen,
keyExchangeAlgorithm: keyExchangeAlgorithm,
ecc: ecc,
},
}
}
// Init initializes the internal Cipher with keying material.
func (c *Aes128Ccm) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error {
const prfKeyLen = 16
return c.AesCcm.Init(masterSecret, clientRandom, serverRandom, isClient, prfKeyLen)
}

View file

@ -0,0 +1,41 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
)
// Aes256Ccm is a base class used by multiple AES-CCM Ciphers.
type Aes256Ccm struct {
AesCcm
}
func newAes256Ccm(
clientCertificateType clientcertificate.Type,
id ID,
psk bool,
cryptoCCMTagLen ciphersuite.CCMTagLen,
keyExchangeAlgorithm KeyExchangeAlgorithm,
ecc bool,
) *Aes256Ccm {
return &Aes256Ccm{
AesCcm: AesCcm{
clientCertificateType: clientCertificateType,
id: id,
psk: psk,
cryptoCCMTagLen: cryptoCCMTagLen,
keyExchangeAlgorithm: keyExchangeAlgorithm,
ecc: ecc,
},
}
}
// Init initializes the internal Cipher with keying material.
func (c *Aes256Ccm) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error {
const prfKeyLen = 32
return c.AesCcm.Init(masterSecret, clientRandom, serverRandom, isClient, prfKeyLen)
}

View file

@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"crypto/sha256"
"fmt"
"hash"
"sync/atomic"
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
// AesCcm is a base class used by multiple AES-CCM Ciphers.
type AesCcm struct {
ccm atomic.Value // *cryptoCCM
clientCertificateType clientcertificate.Type
id ID
psk bool
keyExchangeAlgorithm KeyExchangeAlgorithm
cryptoCCMTagLen ciphersuite.CCMTagLen
ecc bool
}
// CertificateType returns what type of certificate this CipherSuite exchanges.
func (c *AesCcm) CertificateType() clientcertificate.Type {
return c.clientCertificateType
}
// ID returns the ID of the CipherSuite.
func (c *AesCcm) ID() ID {
return c.id
}
func (c *AesCcm) String() string {
return c.id.String()
}
// ECC uses Elliptic Curve Cryptography.
func (c *AesCcm) ECC() bool {
return c.ecc
}
// KeyExchangeAlgorithm controls what key exchange algorithm is using during the handshake.
func (c *AesCcm) KeyExchangeAlgorithm() KeyExchangeAlgorithm {
return c.keyExchangeAlgorithm
}
// HashFunc returns the hashing func for this CipherSuite.
func (c *AesCcm) HashFunc() func() hash.Hash {
return sha256.New
}
// AuthenticationType controls what authentication method is using during the handshake.
func (c *AesCcm) AuthenticationType() AuthenticationType {
if c.psk {
return AuthenticationTypePreSharedKey
}
return AuthenticationTypeCertificate
}
// IsInitialized returns if the CipherSuite has keying material and can
// encrypt/decrypt packets.
func (c *AesCcm) IsInitialized() bool {
return c.ccm.Load() != nil
}
// Init initializes the internal Cipher with keying material.
func (c *AesCcm) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool, prfKeyLen int) error {
const (
prfMacLen = 0
prfIvLen = 4
)
keys, err := prf.GenerateEncryptionKeys(
masterSecret, clientRandom, serverRandom, prfMacLen, prfKeyLen, prfIvLen, c.HashFunc(),
)
if err != nil {
return err
}
var ccm *ciphersuite.CCM
if isClient {
ccm, err = ciphersuite.NewCCM(
c.cryptoCCMTagLen, keys.ClientWriteKey, keys.ClientWriteIV, keys.ServerWriteKey, keys.ServerWriteIV,
)
} else {
ccm, err = ciphersuite.NewCCM(
c.cryptoCCMTagLen, keys.ServerWriteKey, keys.ServerWriteIV, keys.ClientWriteKey, keys.ClientWriteIV,
)
}
c.ccm.Store(ccm)
return err
}
// Encrypt encrypts a single TLS RecordLayer.
func (c *AesCcm) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
cipherSuite, ok := c.ccm.Load().(*ciphersuite.CCM)
if !ok {
return nil, fmt.Errorf("%w, unable to encrypt", errCipherSuiteNotInit)
}
return cipherSuite.Encrypt(pkt, raw)
}
// Decrypt decrypts a single TLS RecordLayer.
func (c *AesCcm) Decrypt(h recordlayer.Header, raw []byte) ([]byte, error) {
cipherSuite, ok := c.ccm.Load().(*ciphersuite.CCM)
if !ok {
return nil, fmt.Errorf("%w, unable to decrypt", errCipherSuiteNotInit)
}
return cipherSuite.Decrypt(h, raw)
}

View file

@ -0,0 +1,100 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package ciphersuite provides TLS Ciphers as registered with the IANA
// https://www.iana.org/assignments/tls-parameters/tls-parameters.xhtml#tls-parameters-4
package ciphersuite
import (
"errors"
"fmt"
"github.com/pion/dtls/v3/internal/ciphersuite/types"
"github.com/pion/dtls/v3/pkg/protocol"
)
//nolint:err113
var errCipherSuiteNotInit = &protocol.TemporaryError{Err: errors.New("CipherSuite has not been initialized")}
// ID is an ID for our supported CipherSuites.
type ID uint16
func (i ID) String() string { //nolint:cyclop
switch i {
case TLS_ECDHE_ECDSA_WITH_AES_128_CCM:
return "TLS_ECDHE_ECDSA_WITH_AES_128_CCM"
case TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8:
return "TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8"
case TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256:
return "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
case TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256:
return "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
case TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA:
return "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA"
case TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA:
return "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"
case TLS_PSK_WITH_AES_128_CCM:
return "TLS_PSK_WITH_AES_128_CCM"
case TLS_PSK_WITH_AES_128_CCM_8:
return "TLS_PSK_WITH_AES_128_CCM_8"
case TLS_PSK_WITH_AES_256_CCM_8:
return "TLS_PSK_WITH_AES_256_CCM_8"
case TLS_PSK_WITH_AES_128_GCM_SHA256:
return "TLS_PSK_WITH_AES_128_GCM_SHA256"
case TLS_PSK_WITH_AES_128_CBC_SHA256:
return "TLS_PSK_WITH_AES_128_CBC_SHA256"
case TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384:
return "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
case TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384:
return "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
case TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256:
return "TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256"
default:
return fmt.Sprintf("unknown(%v)", uint16(i))
}
}
// Supported Cipher Suites.
const (
// AES-128-CCM.
TLS_ECDHE_ECDSA_WITH_AES_128_CCM ID = 0xc0ac // nolint: revive,staticcheck
TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 ID = 0xc0ae // nolint: revive,staticcheck
// AES-128-GCM-SHA256.
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 ID = 0xc02b // nolint: revive,staticcheck
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ID = 0xc02f // nolint: revive,staticcheck
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384 ID = 0xc02c // nolint: revive,staticcheck
TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 ID = 0xc030 // nolint: revive,staticcheck
// AES-256-CBC-SHA.
TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA ID = 0xc00a // nolint: revive,staticcheck
TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA ID = 0xc014 // nolint: revive,staticcheck
TLS_PSK_WITH_AES_128_CCM ID = 0xc0a4 // nolint: revive,staticcheck
TLS_PSK_WITH_AES_128_CCM_8 ID = 0xc0a8 // nolint: revive,staticcheck
TLS_PSK_WITH_AES_256_CCM_8 ID = 0xc0a9 // nolint: revive,staticcheck
TLS_PSK_WITH_AES_128_GCM_SHA256 ID = 0x00a8 // nolint: revive,staticcheck
TLS_PSK_WITH_AES_128_CBC_SHA256 ID = 0x00ae // nolint: revive,staticcheck
TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 ID = 0xC037 // nolint: revive,staticcheck
)
// AuthenticationType controls what authentication method is using during the handshake.
type AuthenticationType = types.AuthenticationType
// AuthenticationType Enums.
const (
AuthenticationTypeCertificate AuthenticationType = types.AuthenticationTypeCertificate
AuthenticationTypePreSharedKey AuthenticationType = types.AuthenticationTypePreSharedKey
AuthenticationTypeAnonymous AuthenticationType = types.AuthenticationTypeAnonymous
)
// KeyExchangeAlgorithm controls what exchange algorithm was chosen.
type KeyExchangeAlgorithm = types.KeyExchangeAlgorithm
// KeyExchangeAlgorithm Bitmask.
const (
KeyExchangeAlgorithmNone KeyExchangeAlgorithm = types.KeyExchangeAlgorithmNone
KeyExchangeAlgorithmPsk KeyExchangeAlgorithm = types.KeyExchangeAlgorithmPsk
KeyExchangeAlgorithmEcdhe KeyExchangeAlgorithm = types.KeyExchangeAlgorithmEcdhe
)

View file

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
)
// NewTLSEcdheEcdsaWithAes128Ccm constructs a TLS_ECDHE_ECDSA_WITH_AES_128_CCM Cipher.
func NewTLSEcdheEcdsaWithAes128Ccm() *Aes128Ccm {
return newAes128Ccm(
clientcertificate.ECDSASign,
TLS_ECDHE_ECDSA_WITH_AES_128_CCM,
false,
ciphersuite.CCMTagLength,
KeyExchangeAlgorithmEcdhe,
true,
)
}

View file

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
)
// NewTLSEcdheEcdsaWithAes128Ccm8 creates a new TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8 CipherSuite.
func NewTLSEcdheEcdsaWithAes128Ccm8() *Aes128Ccm {
return newAes128Ccm(
clientcertificate.ECDSASign,
TLS_ECDHE_ECDSA_WITH_AES_128_CCM_8,
false,
ciphersuite.CCMTagLength8,
KeyExchangeAlgorithmEcdhe,
true,
)
}

View file

@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"crypto/sha256"
"fmt"
"hash"
"sync/atomic"
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
// TLSEcdheEcdsaWithAes128GcmSha256 represents a TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 CipherSuite.
type TLSEcdheEcdsaWithAes128GcmSha256 struct {
gcm atomic.Value // *cryptoGCM
}
// CertificateType returns what type of certficate this CipherSuite exchanges.
func (c *TLSEcdheEcdsaWithAes128GcmSha256) CertificateType() clientcertificate.Type {
return clientcertificate.ECDSASign
}
// KeyExchangeAlgorithm controls what key exchange algorithm is using during the handshake.
func (c *TLSEcdheEcdsaWithAes128GcmSha256) KeyExchangeAlgorithm() KeyExchangeAlgorithm {
return KeyExchangeAlgorithmEcdhe
}
// ECC uses Elliptic Curve Cryptography.
func (c *TLSEcdheEcdsaWithAes128GcmSha256) ECC() bool {
return true
}
// ID returns the ID of the CipherSuite.
func (c *TLSEcdheEcdsaWithAes128GcmSha256) ID() ID {
return TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
}
func (c *TLSEcdheEcdsaWithAes128GcmSha256) String() string {
return "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"
}
// HashFunc returns the hashing func for this CipherSuite.
func (c *TLSEcdheEcdsaWithAes128GcmSha256) HashFunc() func() hash.Hash {
return sha256.New
}
// AuthenticationType controls what authentication method is using during the handshake.
func (c *TLSEcdheEcdsaWithAes128GcmSha256) AuthenticationType() AuthenticationType {
return AuthenticationTypeCertificate
}
// IsInitialized returns if the CipherSuite has keying material and can
// encrypt/decrypt packets.
func (c *TLSEcdheEcdsaWithAes128GcmSha256) IsInitialized() bool {
return c.gcm.Load() != nil
}
func (c *TLSEcdheEcdsaWithAes128GcmSha256) init(
masterSecret, clientRandom, serverRandom []byte,
isClient bool,
prfMacLen, prfKeyLen, prfIvLen int,
hashFunc func() hash.Hash,
) error {
keys, err := prf.GenerateEncryptionKeys(
masterSecret, clientRandom, serverRandom, prfMacLen, prfKeyLen, prfIvLen, hashFunc,
)
if err != nil {
return err
}
var gcm *ciphersuite.GCM
if isClient {
gcm, err = ciphersuite.NewGCM(keys.ClientWriteKey, keys.ClientWriteIV, keys.ServerWriteKey, keys.ServerWriteIV)
} else {
gcm, err = ciphersuite.NewGCM(keys.ServerWriteKey, keys.ServerWriteIV, keys.ClientWriteKey, keys.ClientWriteIV)
}
c.gcm.Store(gcm)
return err
}
// Init initializes the internal Cipher with keying material.
func (c *TLSEcdheEcdsaWithAes128GcmSha256) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error {
const (
prfMacLen = 0
prfKeyLen = 16
prfIvLen = 4
)
return c.init(masterSecret, clientRandom, serverRandom, isClient, prfMacLen, prfKeyLen, prfIvLen, c.HashFunc())
}
// Encrypt encrypts a single TLS RecordLayer.
func (c *TLSEcdheEcdsaWithAes128GcmSha256) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
cipherSuite, ok := c.gcm.Load().(*ciphersuite.GCM)
if !ok {
return nil, fmt.Errorf("%w, unable to encrypt", errCipherSuiteNotInit)
}
return cipherSuite.Encrypt(pkt, raw)
}
// Decrypt decrypts a single TLS RecordLayer.
func (c *TLSEcdheEcdsaWithAes128GcmSha256) Decrypt(h recordlayer.Header, raw []byte) ([]byte, error) {
cipherSuite, ok := c.gcm.Load().(*ciphersuite.GCM)
if !ok {
return nil, fmt.Errorf("%w, unable to decrypt", errCipherSuiteNotInit)
}
return cipherSuite.Decrypt(h, raw)
}

View file

@ -0,0 +1,116 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"crypto/sha1" //nolint: gosec,gci
"crypto/sha256"
"fmt"
"hash"
"sync/atomic"
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
// TLSEcdheEcdsaWithAes256CbcSha represents a TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA CipherSuite.
type TLSEcdheEcdsaWithAes256CbcSha struct {
cbc atomic.Value // *cryptoCBC
}
// CertificateType returns what type of certficate this CipherSuite exchanges.
func (c *TLSEcdheEcdsaWithAes256CbcSha) CertificateType() clientcertificate.Type {
return clientcertificate.ECDSASign
}
// KeyExchangeAlgorithm controls what key exchange algorithm is using during the handshake.
func (c *TLSEcdheEcdsaWithAes256CbcSha) KeyExchangeAlgorithm() KeyExchangeAlgorithm {
return KeyExchangeAlgorithmEcdhe
}
// ECC uses Elliptic Curve Cryptography.
func (c *TLSEcdheEcdsaWithAes256CbcSha) ECC() bool {
return true
}
// ID returns the ID of the CipherSuite.
func (c *TLSEcdheEcdsaWithAes256CbcSha) ID() ID {
return TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
}
func (c *TLSEcdheEcdsaWithAes256CbcSha) String() string {
return "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA"
}
// HashFunc returns the hashing func for this CipherSuite.
func (c *TLSEcdheEcdsaWithAes256CbcSha) HashFunc() func() hash.Hash {
return sha256.New
}
// AuthenticationType controls what authentication method is using during the handshake.
func (c *TLSEcdheEcdsaWithAes256CbcSha) AuthenticationType() AuthenticationType {
return AuthenticationTypeCertificate
}
// IsInitialized returns if the CipherSuite has keying material and can
// encrypt/decrypt packets.
func (c *TLSEcdheEcdsaWithAes256CbcSha) IsInitialized() bool {
return c.cbc.Load() != nil
}
// Init initializes the internal Cipher with keying material.
func (c *TLSEcdheEcdsaWithAes256CbcSha) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error {
const (
prfMacLen = 20
prfKeyLen = 32
prfIvLen = 16
)
keys, err := prf.GenerateEncryptionKeys(
masterSecret, clientRandom, serverRandom, prfMacLen, prfKeyLen, prfIvLen, c.HashFunc(),
)
if err != nil {
return err
}
var cbc *ciphersuite.CBC
if isClient {
cbc, err = ciphersuite.NewCBC(
keys.ClientWriteKey, keys.ClientWriteIV, keys.ClientMACKey,
keys.ServerWriteKey, keys.ServerWriteIV, keys.ServerMACKey,
sha1.New,
)
} else {
cbc, err = ciphersuite.NewCBC(
keys.ServerWriteKey, keys.ServerWriteIV, keys.ServerMACKey,
keys.ClientWriteKey, keys.ClientWriteIV, keys.ClientMACKey,
sha1.New,
)
}
c.cbc.Store(cbc)
return err
}
// Encrypt encrypts a single TLS RecordLayer.
func (c *TLSEcdheEcdsaWithAes256CbcSha) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
cipherSuite, ok := c.cbc.Load().(*ciphersuite.CBC)
if !ok {
return nil, fmt.Errorf("%w, unable to encrypt", errCipherSuiteNotInit)
}
return cipherSuite.Encrypt(pkt, raw)
}
// Decrypt decrypts a single TLS RecordLayer.
func (c *TLSEcdheEcdsaWithAes256CbcSha) Decrypt(h recordlayer.Header, raw []byte) ([]byte, error) {
cipherSuite, ok := c.cbc.Load().(*ciphersuite.CBC)
if !ok {
return nil, fmt.Errorf("%w, unable to decrypt", errCipherSuiteNotInit)
}
return cipherSuite.Decrypt(h, raw)
}

View file

@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"crypto/sha512"
"hash"
)
// TLSEcdheEcdsaWithAes256GcmSha384 represents a TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256 CipherSuite.
type TLSEcdheEcdsaWithAes256GcmSha384 struct {
TLSEcdheEcdsaWithAes128GcmSha256
}
// ID returns the ID of the CipherSuite.
func (c *TLSEcdheEcdsaWithAes256GcmSha384) ID() ID {
return TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
}
func (c *TLSEcdheEcdsaWithAes256GcmSha384) String() string {
return "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"
}
// HashFunc returns the hashing func for this CipherSuite.
func (c *TLSEcdheEcdsaWithAes256GcmSha384) HashFunc() func() hash.Hash {
return sha512.New384
}
// Init initializes the internal Cipher with keying material.
func (c *TLSEcdheEcdsaWithAes256GcmSha384) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error {
const (
prfMacLen = 0
prfKeyLen = 32
prfIvLen = 4
)
return c.init(masterSecret, clientRandom, serverRandom, isClient, prfMacLen, prfKeyLen, prfIvLen, c.HashFunc())
}

View file

@ -0,0 +1,120 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"crypto/sha256"
"fmt"
"hash"
"sync/atomic"
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
// TLSEcdhePskWithAes128CbcSha256 implements the TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 CipherSuite.
type TLSEcdhePskWithAes128CbcSha256 struct {
cbc atomic.Value // *cryptoCBC
}
// NewTLSEcdhePskWithAes128CbcSha256 creates TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256 cipher.
func NewTLSEcdhePskWithAes128CbcSha256() *TLSEcdhePskWithAes128CbcSha256 {
return &TLSEcdhePskWithAes128CbcSha256{}
}
// CertificateType returns what type of certificate this CipherSuite exchanges.
func (c *TLSEcdhePskWithAes128CbcSha256) CertificateType() clientcertificate.Type {
return clientcertificate.Type(0)
}
// KeyExchangeAlgorithm controls what key exchange algorithm is using during the handshake.
func (c *TLSEcdhePskWithAes128CbcSha256) KeyExchangeAlgorithm() KeyExchangeAlgorithm {
return (KeyExchangeAlgorithmPsk | KeyExchangeAlgorithmEcdhe)
}
// ECC uses Elliptic Curve Cryptography.
func (c *TLSEcdhePskWithAes128CbcSha256) ECC() bool {
return true
}
// ID returns the ID of the CipherSuite.
func (c *TLSEcdhePskWithAes128CbcSha256) ID() ID {
return TLS_ECDHE_PSK_WITH_AES_128_CBC_SHA256
}
func (c *TLSEcdhePskWithAes128CbcSha256) String() string {
return "TLS-ECDHE-PSK-WITH-AES-128-CBC-SHA256"
}
// HashFunc returns the hashing func for this CipherSuite.
func (c *TLSEcdhePskWithAes128CbcSha256) HashFunc() func() hash.Hash {
return sha256.New
}
// AuthenticationType controls what authentication method is using during the handshake.
func (c *TLSEcdhePskWithAes128CbcSha256) AuthenticationType() AuthenticationType {
return AuthenticationTypePreSharedKey
}
// IsInitialized returns if the CipherSuite has keying material and can
// encrypt/decrypt packets.
func (c *TLSEcdhePskWithAes128CbcSha256) IsInitialized() bool {
return c.cbc.Load() != nil
}
// Init initializes the internal Cipher with keying material.
func (c *TLSEcdhePskWithAes128CbcSha256) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error {
const (
prfMacLen = 32
prfKeyLen = 16
prfIvLen = 16
)
keys, err := prf.GenerateEncryptionKeys(
masterSecret, clientRandom, serverRandom, prfMacLen, prfKeyLen, prfIvLen, c.HashFunc(),
)
if err != nil {
return err
}
var cbc *ciphersuite.CBC
if isClient {
cbc, err = ciphersuite.NewCBC(
keys.ClientWriteKey, keys.ClientWriteIV, keys.ClientMACKey,
keys.ServerWriteKey, keys.ServerWriteIV, keys.ServerMACKey,
c.HashFunc(),
)
} else {
cbc, err = ciphersuite.NewCBC(
keys.ServerWriteKey, keys.ServerWriteIV, keys.ServerMACKey,
keys.ClientWriteKey, keys.ClientWriteIV, keys.ClientMACKey,
c.HashFunc(),
)
}
c.cbc.Store(cbc)
return err
}
// Encrypt encrypts a single TLS RecordLayer.
func (c *TLSEcdhePskWithAes128CbcSha256) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
cipherSuite, ok := c.cbc.Load().(*ciphersuite.CBC)
if !ok { // !c.isInitialized()
return nil, fmt.Errorf("%w, unable to encrypt", errCipherSuiteNotInit)
}
return cipherSuite.Encrypt(pkt, raw)
}
// Decrypt decrypts a single TLS RecordLayer.
func (c *TLSEcdhePskWithAes128CbcSha256) Decrypt(h recordlayer.Header, raw []byte) ([]byte, error) {
cipherSuite, ok := c.cbc.Load().(*ciphersuite.CBC)
if !ok { // !c.isInitialized()
return nil, fmt.Errorf("%w, unable to decrypt", errCipherSuiteNotInit)
}
return cipherSuite.Decrypt(h, raw)
}

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import "github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
// TLSEcdheRsaWithAes128GcmSha256 implements the TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 CipherSuite.
type TLSEcdheRsaWithAes128GcmSha256 struct {
TLSEcdheEcdsaWithAes128GcmSha256
}
// CertificateType returns what type of certificate this CipherSuite exchanges.
func (c *TLSEcdheRsaWithAes128GcmSha256) CertificateType() clientcertificate.Type {
return clientcertificate.RSASign
}
// ID returns the ID of the CipherSuite.
func (c *TLSEcdheRsaWithAes128GcmSha256) ID() ID {
return TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
}
func (c *TLSEcdheRsaWithAes128GcmSha256) String() string {
return "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"
}

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import "github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
// TLSEcdheRsaWithAes256CbcSha implements the TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA CipherSuite.
type TLSEcdheRsaWithAes256CbcSha struct {
TLSEcdheEcdsaWithAes256CbcSha
}
// CertificateType returns what type of certificate this CipherSuite exchanges.
func (c *TLSEcdheRsaWithAes256CbcSha) CertificateType() clientcertificate.Type {
return clientcertificate.RSASign
}
// ID returns the ID of the CipherSuite.
func (c *TLSEcdheRsaWithAes256CbcSha) ID() ID {
return TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
}
func (c *TLSEcdheRsaWithAes256CbcSha) String() string {
return "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA"
}

View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import "github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
// TLSEcdheRsaWithAes256GcmSha384 implements the TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 CipherSuite.
type TLSEcdheRsaWithAes256GcmSha384 struct {
TLSEcdheEcdsaWithAes256GcmSha384
}
// CertificateType returns what type of certificate this CipherSuite exchanges.
func (c *TLSEcdheRsaWithAes256GcmSha384) CertificateType() clientcertificate.Type {
return clientcertificate.RSASign
}
// ID returns the ID of the CipherSuite.
func (c *TLSEcdheRsaWithAes256GcmSha384) ID() ID {
return TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
}
func (c *TLSEcdheRsaWithAes256GcmSha384) String() string {
return "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"
}

View file

@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"crypto/sha256"
"fmt"
"hash"
"sync/atomic"
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
// TLSPskWithAes128CbcSha256 implements the TLS_PSK_WITH_AES_128_CBC_SHA256 CipherSuite.
type TLSPskWithAes128CbcSha256 struct {
cbc atomic.Value // *cryptoCBC
}
// CertificateType returns what type of certificate this CipherSuite exchanges.
func (c *TLSPskWithAes128CbcSha256) CertificateType() clientcertificate.Type {
return clientcertificate.Type(0)
}
// KeyExchangeAlgorithm controls what key exchange algorithm is using during the handshake.
func (c *TLSPskWithAes128CbcSha256) KeyExchangeAlgorithm() KeyExchangeAlgorithm {
return KeyExchangeAlgorithmPsk
}
// ECC uses Elliptic Curve Cryptography.
func (c *TLSPskWithAes128CbcSha256) ECC() bool {
return false
}
// ID returns the ID of the CipherSuite.
func (c *TLSPskWithAes128CbcSha256) ID() ID {
return TLS_PSK_WITH_AES_128_CBC_SHA256
}
func (c *TLSPskWithAes128CbcSha256) String() string {
return "TLS_PSK_WITH_AES_128_CBC_SHA256"
}
// HashFunc returns the hashing func for this CipherSuite.
func (c *TLSPskWithAes128CbcSha256) HashFunc() func() hash.Hash {
return sha256.New
}
// AuthenticationType controls what authentication method is using during the handshake.
func (c *TLSPskWithAes128CbcSha256) AuthenticationType() AuthenticationType {
return AuthenticationTypePreSharedKey
}
// IsInitialized returns if the CipherSuite has keying material and can
// encrypt/decrypt packets.
func (c *TLSPskWithAes128CbcSha256) IsInitialized() bool {
return c.cbc.Load() != nil
}
// Init initializes the internal Cipher with keying material.
func (c *TLSPskWithAes128CbcSha256) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error {
const (
prfMacLen = 32
prfKeyLen = 16
prfIvLen = 16
)
keys, err := prf.GenerateEncryptionKeys(
masterSecret, clientRandom, serverRandom, prfMacLen, prfKeyLen, prfIvLen, c.HashFunc(),
)
if err != nil {
return err
}
var cbc *ciphersuite.CBC
if isClient {
cbc, err = ciphersuite.NewCBC(
keys.ClientWriteKey, keys.ClientWriteIV, keys.ClientMACKey,
keys.ServerWriteKey, keys.ServerWriteIV, keys.ServerMACKey,
c.HashFunc(),
)
} else {
cbc, err = ciphersuite.NewCBC(
keys.ServerWriteKey, keys.ServerWriteIV, keys.ServerMACKey,
keys.ClientWriteKey, keys.ClientWriteIV, keys.ClientMACKey,
c.HashFunc(),
)
}
c.cbc.Store(cbc)
return err
}
// Encrypt encrypts a single TLS RecordLayer.
func (c *TLSPskWithAes128CbcSha256) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
cipherSuite, ok := c.cbc.Load().(*ciphersuite.CBC)
if !ok {
return nil, fmt.Errorf("%w, unable to encrypt", errCipherSuiteNotInit)
}
return cipherSuite.Encrypt(pkt, raw)
}
// Decrypt decrypts a single TLS RecordLayer.
func (c *TLSPskWithAes128CbcSha256) Decrypt(h recordlayer.Header, raw []byte) ([]byte, error) {
cipherSuite, ok := c.cbc.Load().(*ciphersuite.CBC)
if !ok {
return nil, fmt.Errorf("%w, unable to decrypt", errCipherSuiteNotInit)
}
return cipherSuite.Decrypt(h, raw)
}

View file

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
)
// NewTLSPskWithAes128Ccm returns the TLS_PSK_WITH_AES_128_CCM CipherSuite.
func NewTLSPskWithAes128Ccm() *Aes128Ccm {
return newAes128Ccm(
clientcertificate.Type(0),
TLS_PSK_WITH_AES_128_CCM,
true,
ciphersuite.CCMTagLength,
KeyExchangeAlgorithmPsk,
false,
)
}

View file

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
)
// NewTLSPskWithAes128Ccm8 returns the TLS_PSK_WITH_AES_128_CCM_8 CipherSuite.
func NewTLSPskWithAes128Ccm8() *Aes128Ccm {
return newAes128Ccm(
clientcertificate.Type(0),
TLS_PSK_WITH_AES_128_CCM_8,
true,
ciphersuite.CCMTagLength8,
KeyExchangeAlgorithmPsk,
false,
)
}

View file

@ -0,0 +1,35 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import "github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
// TLSPskWithAes128GcmSha256 implements the TLS_PSK_WITH_AES_128_GCM_SHA256 CipherSuite.
type TLSPskWithAes128GcmSha256 struct {
TLSEcdheEcdsaWithAes128GcmSha256
}
// CertificateType returns what type of certificate this CipherSuite exchanges.
func (c *TLSPskWithAes128GcmSha256) CertificateType() clientcertificate.Type {
return clientcertificate.Type(0)
}
// KeyExchangeAlgorithm controls what key exchange algorithm is using during the handshake.
func (c *TLSPskWithAes128GcmSha256) KeyExchangeAlgorithm() KeyExchangeAlgorithm {
return KeyExchangeAlgorithmPsk
}
// ID returns the ID of the CipherSuite.
func (c *TLSPskWithAes128GcmSha256) ID() ID {
return TLS_PSK_WITH_AES_128_GCM_SHA256
}
func (c *TLSPskWithAes128GcmSha256) String() string {
return "TLS_PSK_WITH_AES_128_GCM_SHA256"
}
// AuthenticationType controls what authentication method is using during the handshake.
func (c *TLSPskWithAes128GcmSha256) AuthenticationType() AuthenticationType {
return AuthenticationTypePreSharedKey
}

View file

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"github.com/pion/dtls/v3/pkg/crypto/ciphersuite"
"github.com/pion/dtls/v3/pkg/crypto/clientcertificate"
)
// NewTLSPskWithAes256Ccm8 returns the TLS_PSK_WITH_AES_256_CCM_8 CipherSuite.
func NewTLSPskWithAes256Ccm8() *Aes256Ccm {
return newAes256Ccm(
clientcertificate.Type(0),
TLS_PSK_WITH_AES_256_CCM_8,
true,
ciphersuite.CCMTagLength8,
KeyExchangeAlgorithmPsk,
false,
)
}

View file

@ -0,0 +1,14 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package types
// AuthenticationType controls what authentication method is using during the handshake.
type AuthenticationType int
// AuthenticationType Enums.
const (
AuthenticationTypeCertificate AuthenticationType = iota + 1
AuthenticationTypePreSharedKey
AuthenticationTypeAnonymous
)

View file

@ -0,0 +1,20 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package types provides types for TLS Ciphers
package types // nolint:revive
// KeyExchangeAlgorithm controls what exchange algorithm was chosen.
type KeyExchangeAlgorithm int
// KeyExchangeAlgorithm Bitmask.
const (
KeyExchangeAlgorithmNone KeyExchangeAlgorithm = 0
KeyExchangeAlgorithmPsk KeyExchangeAlgorithm = iota << 1
KeyExchangeAlgorithmEcdhe
)
// Has check if keyExchangeAlgorithm is supported.
func (a KeyExchangeAlgorithm) Has(v KeyExchangeAlgorithm) bool {
return (a & v) == v
}

View file

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package closer provides signaling channel for shutdown
package closer
import (
"context"
)
// Closer allows for each signaling a channel for shutdown.
type Closer struct {
ctx context.Context //nolint:containedctx
closeFunc func()
}
// NewCloser creates a new instance of Closer.
func NewCloser() *Closer {
ctx, closeFunc := context.WithCancel(context.Background())
return &Closer{
ctx: ctx,
closeFunc: closeFunc,
}
}
// NewCloserWithParent creates a new instance of Closer with a parent context.
func NewCloserWithParent(ctx context.Context) *Closer {
ctx, closeFunc := context.WithCancel(ctx)
return &Closer{
ctx: ctx,
closeFunc: closeFunc,
}
}
// Done returns a channel signaling when it is done.
func (c *Closer) Done() <-chan struct{} {
return c.ctx.Done()
}
// Err returns an error of the context.
func (c *Closer) Err() error {
return c.ctx.Err()
}
// Close sends a signal to trigger the ctx done channel.
func (c *Closer) Close() {
c.closeFunc()
}

242
vendor/github.com/pion/dtls/v3/internal/net/buffer.go generated vendored Normal file
View file

@ -0,0 +1,242 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package net implements DTLS specific networking primitives.
// NOTE: this package is an adaption of pion/transport/packetio that allows for
// storing a remote address alongside each packet in the buffer and implements
// relevant methods of net.PacketConn. If possible, the updates made in this
// repository will be reflected back upstream. If not, it is likely that this
// will be moved to a public package in this repository.
//
// This package was migrated from pion/transport/packetio at
// https://github.com/pion/transport/commit/6890c795c807a617c054149eee40a69d7fdfbfdb
package net
import (
"bytes"
"errors"
"io"
"net"
"sync"
"time"
"github.com/pion/transport/v4/deadline"
)
// ErrTimeout indicates that deadline was reached before operation could be
// completed.
var ErrTimeout = errors.New("buffer: i/o timeout")
// AddrPacket is a packet payload and the associated remote address from which
// it was received.
type AddrPacket struct {
addr net.Addr
data bytes.Buffer
}
// PacketBuffer is a circular buffer for network packets. Each slot in the
// buffer contains the remote address from which the packet was received, as
// well as the packet data.
type PacketBuffer struct {
mutex sync.Mutex
packets []AddrPacket
write, read int
// full indicates whether the buffer is full, which is needed to distinguish
// when the write pointer and read pointer are at the same index.
full bool
notify chan struct{}
closed bool
readDeadline *deadline.Deadline
}
// NewPacketBuffer creates a new PacketBuffer.
func NewPacketBuffer() *PacketBuffer {
return &PacketBuffer{
readDeadline: deadline.New(),
// In the narrow context in which this package is currently used, there
// will always be at least one packet written to the buffer. Therefore,
// we opt to allocate with size of 1 during construction, rather than
// waiting until that first packet is written.
packets: make([]AddrPacket, 1),
full: false,
}
}
// WriteTo writes a single packet to the buffer. The supplied address will
// remain associated with the packet.
func (b *PacketBuffer) WriteTo(pkt []byte, addr net.Addr) (int, error) {
b.mutex.Lock()
if b.closed {
b.mutex.Unlock()
return 0, io.ErrClosedPipe
}
var notify chan struct{}
if b.notify != nil {
notify = b.notify
b.notify = nil
}
// Check to see if we are full.
if b.full {
// If so, grow AddrPacket buffer.
var newSize int
if len(b.packets) < 128 {
// Double the number of packets.
newSize = len(b.packets) * 2
} else {
// Increase the number of packets by 25%.
newSize = 5 * len(b.packets) / 4
}
newBuf := make([]AddrPacket, newSize)
var n int
if b.read < b.write {
n = copy(newBuf, b.packets[b.read:b.write])
} else {
n = copy(newBuf, b.packets[b.read:])
n += copy(newBuf[n:], b.packets[:b.write])
}
b.packets = newBuf
// Update read/write pointers and mark buffer as not full.
b.read = 0
b.write = n
b.full = false
}
// Store the packet at the write pointer.
packet := &b.packets[b.write]
packet.data.Reset()
n, err := packet.data.Write(pkt)
if err != nil {
b.mutex.Unlock()
return n, err
}
packet.addr = addr
// Increment write pointer.
b.write++
// If the write pointer is equal to the length of the buffer, wrap around.
if len(b.packets) == b.write {
b.write = 0
}
// If a write resulted in making write and read pointers equivalent, then we
// are full.
if b.write == b.read {
b.full = true
}
b.mutex.Unlock()
if notify != nil {
close(notify)
}
return n, nil
}
// ReadFrom reads a single packet from the buffer, or blocks until one is
// available.
func (b *PacketBuffer) ReadFrom(packet []byte) (n int, addr net.Addr, err error) { //nolint:cyclop
select {
case <-b.readDeadline.Done():
return 0, nil, ErrTimeout
default:
}
for {
b.mutex.Lock()
if b.read != b.write || b.full {
ap := b.packets[b.read]
if len(packet) < ap.data.Len() {
b.mutex.Unlock()
return 0, nil, io.ErrShortBuffer
}
// Copy packet data from buffer.
n, err := ap.data.Read(packet)
if err != nil {
b.mutex.Unlock()
return n, nil, err
}
// Advance read pointer.
b.read++
if len(b.packets) == b.read {
b.read = 0
}
// If we were full before reading and have successfully read, we are
// no longer full.
if b.full {
b.full = false
}
b.mutex.Unlock()
return n, ap.addr, nil
}
if b.closed {
b.mutex.Unlock()
return 0, nil, io.EOF
}
if b.notify == nil {
b.notify = make(chan struct{})
}
notify := b.notify
b.mutex.Unlock()
select {
case <-b.readDeadline.Done():
return 0, nil, ErrTimeout
case <-notify:
}
}
}
// Close closes the buffer, allowing unread packets to be read, but erroring on
// any new writes.
func (b *PacketBuffer) Close() (err error) {
b.mutex.Lock()
if b.closed {
b.mutex.Unlock()
return nil
}
notify := b.notify
b.notify = nil
b.closed = true
b.mutex.Unlock()
if notify != nil {
close(notify)
}
return nil
}
// SetReadDeadline sets the read deadline for the buffer.
func (b *PacketBuffer) SetReadDeadline(t time.Time) error {
b.readDeadline.Set(t)
return nil
}

View file

@ -0,0 +1,413 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package udp implements DTLS specific UDP networking primitives.
// NOTE: this package is an adaption of pion/transport/udp that allows for
// routing datagrams based on identifiers other than the remote address. The
// primary use case for this functionality is routing based on DTLS connection
// IDs. In order to allow for consumers of this package to treat connections as
// generic net.PackageConn, routing and identitier establishment is based on
// custom introspecion of datagrams, rather than direct intervention by
// consumers. If possible, the updates made in this repository will be reflected
// back upstream. If not, it is likely that this will be moved to a public
// package in this repository.
//
// This package was migrated from pion/transport/udp at
// https://github.com/pion/transport/commit/6890c795c807a617c054149eee40a69d7fdfbfdb
package udp
import (
"context"
"errors"
"net"
"sync"
"sync/atomic"
"time"
idtlsnet "github.com/pion/dtls/v3/internal/net"
dtlsnet "github.com/pion/dtls/v3/pkg/net"
"github.com/pion/transport/v4/deadline"
)
const (
receiveMTU = 8192
defaultListenBacklog = 128 // same as Linux default
)
// Typed errors.
var (
ErrClosedListener = errors.New("udp: listener closed")
ErrListenQueueExceeded = errors.New("udp: listen queue exceeded")
)
// listener augments a connection-oriented Listener over a UDP PacketConn.
type listener struct {
pConn *net.UDPConn
accepting atomic.Value // bool
acceptCh chan *PacketConn
doneCh chan struct{}
doneOnce sync.Once
acceptFilter func([]byte) bool
datagramRouter func([]byte) (string, bool)
connIdentifier func([]byte) (string, bool)
connLock sync.Mutex
conns map[string]*PacketConn
connWG sync.WaitGroup
readWG sync.WaitGroup
errClose atomic.Value // error
readDoneCh chan struct{}
errRead atomic.Value // error
}
// Accept waits for and returns the next connection to the listener.
func (l *listener) Accept() (net.PacketConn, net.Addr, error) {
select {
case c := <-l.acceptCh:
l.connWG.Add(1)
return c, c.raddr, nil
case <-l.readDoneCh:
err, _ := l.errRead.Load().(error)
return nil, nil, err
case <-l.doneCh:
return nil, nil, ErrClosedListener
}
}
// Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
func (l *listener) Close() error {
var err error
l.doneOnce.Do(func() {
l.accepting.Store(false)
close(l.doneCh)
l.connLock.Lock()
// Close unaccepted connections
lclose:
for {
select {
case c := <-l.acceptCh:
close(c.doneCh)
// If we have an alternate identifier, remove it from the connection
// map.
if id := c.id.Load(); id != nil {
delete(l.conns, id.(string)) //nolint:forcetypeassert
}
// If we haven't already removed the remote address, remove it
// from the connection map.
if c.rmraddr.Load() == nil {
delete(l.conns, c.raddr.String())
c.rmraddr.Store(true)
}
default:
break lclose
}
}
nConns := len(l.conns)
l.connLock.Unlock()
l.connWG.Done()
if nConns == 0 {
// Wait if this is the final connection.
l.readWG.Wait()
if errClose, ok := l.errClose.Load().(error); ok {
err = errClose
}
} else {
err = nil
}
})
return err
}
// Addr returns the listener's network address.
func (l *listener) Addr() net.Addr {
return l.pConn.LocalAddr()
}
// ListenConfig stores options for listening to an address.
type ListenConfig struct {
// Backlog defines the maximum length of the queue of pending
// connections. It is equivalent of the backlog argument of
// POSIX listen function.
// If a connection request arrives when the queue is full,
// the request will be silently discarded, unlike TCP.
// Set zero to use default value 128 which is same as Linux default.
Backlog int
// AcceptFilter determines whether the new conn should be made for
// the incoming packet. If not set, any packet creates new conn.
AcceptFilter func([]byte) bool
// DatagramRouter routes an incoming datagram to a connection by extracting
// an identifier from the its paylod
DatagramRouter func([]byte) (string, bool)
// ConnectionIdentifier extracts an identifier from an outgoing packet. If
// the identifier is not already associated with the connection, it will be
// added.
ConnectionIdentifier func([]byte) (string, bool)
}
// Listen creates a new listener based on the ListenConfig.
func (lc *ListenConfig) Listen(network string, laddr *net.UDPAddr) (dtlsnet.PacketListener, error) {
if lc.Backlog == 0 {
lc.Backlog = defaultListenBacklog
}
conn, err := net.ListenUDP(network, laddr)
if err != nil {
return nil, err
}
packetListener := &listener{
pConn: conn,
acceptCh: make(chan *PacketConn, lc.Backlog),
conns: make(map[string]*PacketConn),
doneCh: make(chan struct{}),
acceptFilter: lc.AcceptFilter,
datagramRouter: lc.DatagramRouter,
connIdentifier: lc.ConnectionIdentifier,
readDoneCh: make(chan struct{}),
}
packetListener.accepting.Store(true)
packetListener.connWG.Add(1)
packetListener.readWG.Add(2) // wait readLoop and Close execution routine
go packetListener.readLoop()
go func() {
packetListener.connWG.Wait()
if err := packetListener.pConn.Close(); err != nil {
packetListener.errClose.Store(err)
}
packetListener.readWG.Done()
}()
return packetListener, nil
}
// Listen creates a new listener using default ListenConfig.
func Listen(network string, laddr *net.UDPAddr) (dtlsnet.PacketListener, error) {
return (&ListenConfig{}).Listen(network, laddr)
}
// readLoop dispatches packets to the proper connection, creating a new one if
// necessary, until all connections are closed.
func (l *listener) readLoop() {
defer l.readWG.Done()
defer close(l.readDoneCh)
buf := make([]byte, receiveMTU)
for {
n, raddr, err := l.pConn.ReadFrom(buf)
if err != nil {
l.errRead.Store(err)
return
}
conn, ok, err := l.getConn(raddr, buf[:n])
if err != nil {
continue
}
if ok {
_, _ = conn.buffer.WriteTo(buf[:n], raddr)
}
}
}
// getConn gets an existing connection or creates a new one.
func (l *listener) getConn(raddr net.Addr, buf []byte) (*PacketConn, bool, error) { //nolint:cyclop
l.connLock.Lock()
defer l.connLock.Unlock()
// If we have a custom resolver, use it.
if l.datagramRouter != nil {
if id, ok := l.datagramRouter(buf); ok {
if conn, ok := l.conns[id]; ok {
return conn, true, nil
}
}
}
// If we don't have a custom resolver, or we were unable to find an
// associated connection, fall back to remote address.
conn, ok := l.conns[raddr.String()]
if !ok {
if isAccepting, ok := l.accepting.Load().(bool); !isAccepting || !ok {
return nil, false, ErrClosedListener
}
if l.acceptFilter != nil {
if !l.acceptFilter(buf) {
return nil, false, nil
}
}
conn = l.newPacketConn(raddr)
select {
case l.acceptCh <- conn:
l.conns[raddr.String()] = conn
default:
return nil, false, ErrListenQueueExceeded
}
}
return conn, true, nil
}
// PacketConn is a net.PacketConn implementation that is able to dictate its
// routing ID via an alternate identifier from its remote address. Internal
// buffering is performed for reads, and writes are passed through to the
// underlying net.PacketConn.
type PacketConn struct {
listener *listener
raddr net.Addr
rmraddr atomic.Value // bool
id atomic.Value // string
buffer *idtlsnet.PacketBuffer
doneCh chan struct{}
doneOnce sync.Once
writeDeadline *deadline.Deadline
}
// newPacketConn constructs a new PacketConn.
func (l *listener) newPacketConn(raddr net.Addr) *PacketConn {
return &PacketConn{
listener: l,
raddr: raddr,
buffer: idtlsnet.NewPacketBuffer(),
doneCh: make(chan struct{}),
writeDeadline: deadline.New(),
}
}
// ReadFrom reads a single packet payload and its associated remote address from
// the underlying buffer.
func (c *PacketConn) ReadFrom(buff []byte) (int, net.Addr, error) {
return c.buffer.ReadFrom(buff)
}
// WriteTo writes len(payload) bytes from payload to the specified address.
func (c *PacketConn) WriteTo(payload []byte, addr net.Addr) (n int, err error) {
// If we have a connection identifier, check to see if the outgoing packet
// sets it.
if c.listener.connIdentifier != nil {
id := c.id.Load()
// Only update establish identifier if we haven't already done so.
if id == nil {
candidate, ok := c.listener.connIdentifier(payload)
// If we have an identifier, add entry to connection map.
if ok {
c.listener.connLock.Lock()
c.listener.conns[candidate] = c
c.listener.connLock.Unlock()
c.id.Store(candidate)
}
}
// If we are writing to a remote address that differs from the initial,
// we have an alternate identifier established, and we haven't already
// freed the remote address, free the remote address to be used by
// another connection.
// Note: this strategy results in holding onto a remote address after it
// is potentially no longer in use by the client. However, releasing
// earlier means that we could miss some packets that should have been
// routed to this connection. Ideally, we would drop the connection
// entry for the remote address as soon as the client starts sending
// using an alternate identifier, but in practice this proves
// challenging because any client could spoof a connection identifier,
// resulting in the remote address entry being dropped prior to the
// "real" client transitioning to sending using the alternate
// identifier.
if id != nil && c.rmraddr.Load() == nil && addr.String() != c.raddr.String() {
c.listener.connLock.Lock()
delete(c.listener.conns, c.raddr.String())
c.rmraddr.Store(true)
c.listener.connLock.Unlock()
}
}
select {
case <-c.writeDeadline.Done():
return 0, context.DeadlineExceeded
default:
}
return c.listener.pConn.WriteTo(payload, addr)
}
// Close closes the conn and releases any Read calls.
func (c *PacketConn) Close() error {
var err error
c.doneOnce.Do(func() {
c.listener.connWG.Done()
close(c.doneCh)
c.listener.connLock.Lock()
// If we have an alternate identifier, remove it from the connection
// map.
if id := c.id.Load(); id != nil {
delete(c.listener.conns, id.(string)) //nolint:forcetypeassert
}
// If we haven't already removed the remote address, remove it from the
// connection map.
if c.rmraddr.Load() == nil {
delete(c.listener.conns, c.raddr.String())
c.rmraddr.Store(true)
}
nConns := len(c.listener.conns)
c.listener.connLock.Unlock()
if isAccepting, ok := c.listener.accepting.Load().(bool); nConns == 0 && !isAccepting && ok {
// Wait if this is the final connection
c.listener.readWG.Wait()
if errClose, ok := c.listener.errClose.Load().(error); ok {
err = errClose
}
} else {
err = nil
}
if errBuf := c.buffer.Close(); errBuf != nil && err == nil {
err = errBuf
}
})
return err
}
// LocalAddr implements net.PacketConn.LocalAddr.
func (c *PacketConn) LocalAddr() net.Addr {
return c.listener.pConn.LocalAddr()
}
// SetDeadline implements net.PacketConn.SetDeadline.
func (c *PacketConn) SetDeadline(t time.Time) error {
c.writeDeadline.Set(t)
return c.SetReadDeadline(t)
}
// SetReadDeadline implements net.PacketConn.SetReadDeadline.
func (c *PacketConn) SetReadDeadline(t time.Time) error {
return c.buffer.SetReadDeadline(t)
}
// SetWriteDeadline implements net.PacketConn.SetWriteDeadline.
func (c *PacketConn) SetWriteDeadline(t time.Time) error {
c.writeDeadline.Set(t)
// Write deadline of underlying connection should not be changed
// since the connection can be shared.
return nil
}

53
vendor/github.com/pion/dtls/v3/internal/util/util.go generated vendored Normal file
View file

@ -0,0 +1,53 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package util contains small helpers used across the repo
package util // nolint:revive
import (
"encoding/binary"
"golang.org/x/crypto/cryptobyte"
)
// BigEndianUint24 returns the value of a big endian uint24.
func BigEndianUint24(raw []byte) uint32 {
if len(raw) < 3 {
return 0
}
rawCopy := make([]byte, 4)
copy(rawCopy[1:], raw)
return binary.BigEndian.Uint32(rawCopy)
}
// PutBigEndianUint24 encodes a uint24 and places into out.
func PutBigEndianUint24(out []byte, in uint32) {
tmp := make([]byte, 4)
binary.BigEndian.PutUint32(tmp, in)
copy(out, tmp[1:])
}
// PutBigEndianUint48 encodes a uint64 and places into out.
func PutBigEndianUint48(out []byte, in uint64) {
tmp := make([]byte, 8)
binary.BigEndian.PutUint64(tmp, in)
copy(out, tmp[2:])
}
// Max returns the larger value.
func Max(a, b int) int {
if a > b {
return a
}
return b
}
// AddUint48 appends a big-endian, 48-bit value to the byte string.
// Remove if / when https://github.com/golang/crypto/pull/265 is merged
// upstream.
func AddUint48(b *cryptobyte.Builder, v uint64) {
b.AddBytes([]byte{byte(v >> 40), byte(v >> 32), byte(v >> 24), byte(v >> 16), byte(v >> 8), byte(v)})
}

115
vendor/github.com/pion/dtls/v3/listener.go generated vendored Normal file
View file

@ -0,0 +1,115 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"net"
"github.com/pion/dtls/v3/internal/net/udp"
dtlsnet "github.com/pion/dtls/v3/pkg/net"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
// Listen creates a DTLS listener.
//
// Deprecated: Use ListenWithOptions instead.
func Listen(network string, laddr *net.UDPAddr, config *Config) (net.Listener, error) {
if err := validateConfig(config); err != nil {
return nil, err
}
lc := udp.ListenConfig{
AcceptFilter: func(packet []byte) bool {
pkts, err := recordlayer.UnpackDatagram(packet)
if err != nil || len(pkts) < 1 {
return false
}
h := &recordlayer.Header{}
if err := h.Unmarshal(pkts[0]); err != nil {
return false
}
return h.ContentType == protocol.ContentTypeHandshake
},
}
// If connection ID support is enabled, then they must be supported in
// routing.
if config.ConnectionIDGenerator != nil {
lc.DatagramRouter = cidDatagramRouter(len(config.ConnectionIDGenerator()))
lc.ConnectionIdentifier = cidConnIdentifier()
}
parent, err := lc.Listen(network, laddr)
if err != nil {
return nil, err
}
return &listener{
config: config,
parent: parent,
}, nil
}
// ListenWithOptions creates a DTLS listener.
func ListenWithOptions(network string, laddr *net.UDPAddr, opts ...ServerOption) (net.Listener, error) {
config, err := buildServerConfig(opts...)
if err != nil {
return nil, err
}
return Listen(network, laddr, config)
}
// NewListener creates a DTLS listener which accepts connections from an inner Listener.
//
// Deprecated: Use NewListenerWithOptions instead.
func NewListener(inner dtlsnet.PacketListener, config *Config) (net.Listener, error) {
if err := validateConfig(config); err != nil {
return nil, err
}
return &listener{
config: config,
parent: inner,
}, nil
}
// NewListenerWithOptions creates a DTLS listener which accepts connections from an inner Listener.
func NewListenerWithOptions(inner dtlsnet.PacketListener, opts ...ServerOption) (net.Listener, error) {
config, err := buildServerConfig(opts...)
if err != nil {
return nil, err
}
return NewListener(inner, config)
}
// listener represents a DTLS listener.
type listener struct {
config *Config
parent dtlsnet.PacketListener
}
// Accept waits for and returns the next connection to the listener.
// You have to either close or read on all connection that are created.
func (l *listener) Accept() (net.Conn, error) {
c, raddr, err := l.parent.Accept()
if err != nil {
return nil, err
}
return serverWithConfig(c, raddr, l.config)
}
// Close closes the listener.
// Any blocked Accept operations will be unblocked and return errors.
// Already Accepted connections are not closed.
func (l *listener) Close() error {
return l.parent.Close()
}
// Addr returns the listener's network address.
func (l *listener) Addr() net.Addr {
return l.parent.Addr()
}

656
vendor/github.com/pion/dtls/v3/options.go generated vendored Normal file
View file

@ -0,0 +1,656 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"crypto/tls"
"crypto/x509"
"io"
"net"
"time"
"github.com/pion/dtls/v3/pkg/crypto/elliptic"
"github.com/pion/dtls/v3/pkg/protocol/handshake"
"github.com/pion/logging"
)
// ServerOption configures a DTLS server.
type ServerOption interface {
applyServer(*dtlsConfig) error
}
// ClientOption configures a DTLS client.
type ClientOption interface {
applyClient(*dtlsConfig) error
}
// Option is an option that can be used with both client and server.
// This is used for options that apply to both sides of a connection,
// such as in the Resume function where the side is determined at runtime.
type Option interface {
ServerOption
ClientOption
}
// defensiveCopy copies a slice. This prevents the caller from mutating
// the config after construction. Returns empty slice if input is empty.
func defensiveCopy[T any](t ...T) []T {
return append([]T{}, t...)
}
// dtlsConfig is the internal configuration structure.
// This will eventually replace the exported Config struct.
type dtlsConfig struct { //nolint:dupl
certificates []tls.Certificate
cipherSuites []CipherSuiteID
customCipherSuites func() []CipherSuite
signatureSchemes []tls.SignatureScheme
certificateSignatureSchemes []tls.SignatureScheme
srtpProtectionProfiles []SRTPProtectionProfile
srtpMasterKeyIdentifier []byte
clientAuth ClientAuthType
extendedMasterSecret ExtendedMasterSecretType
flightInterval time.Duration
disableRetransmitBackoff bool
psk PSKCallback
pskIdentityHint []byte
insecureSkipVerify bool
insecureHashes bool
verifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
verifyConnection func(*State) error
rootCAs *x509.CertPool
clientCAs *x509.CertPool
serverName string
loggerFactory logging.LoggerFactory
mtu int
replayProtectionWindow int
keyLogWriter io.Writer
sessionStore SessionStore
supportedProtocols []string
ellipticCurves []elliptic.Curve
getCertificate func(*ClientHelloInfo) (*tls.Certificate, error)
getClientCertificate func(*CertificateRequestInfo) (*tls.Certificate, error)
insecureSkipVerifyHello bool
connectionIDGenerator func() []byte
paddingLengthGenerator func(uint) uint
helloRandomBytesGenerator func() [handshake.RandomBytesLength]byte
clientHelloMessageHook func(handshake.MessageClientHello) handshake.Message
serverHelloMessageHook func(handshake.MessageServerHello) handshake.Message
certificateRequestMessageHook func(handshake.MessageCertificateRequest) handshake.Message
onConnectionAttempt func(net.Addr) error
}
// applyDefaults applies default values to the config.
func (c *dtlsConfig) applyDefaults() {
c.extendedMasterSecret = RequestExtendedMasterSecret
c.flightInterval = time.Second
c.mtu = defaultMTU
c.replayProtectionWindow = defaultReplayProtectionWindow
}
// toConfig converts internal dtlsConfig to the exported Config struct.
// This is for backward compatibility and will be removed when Config is deprecated.
// All slice fields are copied to ensure immutability.
func (c *dtlsConfig) toConfig() *Config {
config := &Config{
CustomCipherSuites: c.customCipherSuites,
ClientAuth: c.clientAuth,
ExtendedMasterSecret: c.extendedMasterSecret,
FlightInterval: c.flightInterval,
DisableRetransmitBackoff: c.disableRetransmitBackoff,
PSK: c.psk,
InsecureSkipVerify: c.insecureSkipVerify,
InsecureHashes: c.insecureHashes,
VerifyPeerCertificate: c.verifyPeerCertificate,
VerifyConnection: c.verifyConnection,
RootCAs: c.rootCAs,
ClientCAs: c.clientCAs,
ServerName: c.serverName,
LoggerFactory: c.loggerFactory,
MTU: c.mtu,
ReplayProtectionWindow: c.replayProtectionWindow,
KeyLogWriter: c.keyLogWriter,
SessionStore: c.sessionStore,
GetCertificate: c.getCertificate,
GetClientCertificate: c.getClientCertificate,
InsecureSkipVerifyHello: c.insecureSkipVerifyHello,
ConnectionIDGenerator: c.connectionIDGenerator,
PaddingLengthGenerator: c.paddingLengthGenerator,
HelloRandomBytesGenerator: c.helloRandomBytesGenerator,
ClientHelloMessageHook: c.clientHelloMessageHook,
ServerHelloMessageHook: c.serverHelloMessageHook,
CertificateRequestMessageHook: c.certificateRequestMessageHook,
OnConnectionAttempt: c.onConnectionAttempt,
}
if len(c.certificates) > 0 {
config.Certificates = append([]tls.Certificate(nil), c.certificates...)
}
if len(c.cipherSuites) > 0 {
config.CipherSuites = append([]CipherSuiteID(nil), c.cipherSuites...)
}
if len(c.signatureSchemes) > 0 {
config.SignatureSchemes = append([]tls.SignatureScheme(nil), c.signatureSchemes...)
}
if len(c.certificateSignatureSchemes) > 0 {
config.CertificateSignatureSchemes = append([]tls.SignatureScheme(nil), c.certificateSignatureSchemes...)
}
if len(c.srtpProtectionProfiles) > 0 {
config.SRTPProtectionProfiles = append([]SRTPProtectionProfile(nil), c.srtpProtectionProfiles...)
}
if len(c.srtpMasterKeyIdentifier) > 0 {
config.SRTPMasterKeyIdentifier = append([]byte(nil), c.srtpMasterKeyIdentifier...)
}
if len(c.pskIdentityHint) > 0 {
config.PSKIdentityHint = append([]byte(nil), c.pskIdentityHint...)
}
if len(c.supportedProtocols) > 0 {
config.SupportedProtocols = append([]string(nil), c.supportedProtocols...)
}
if len(c.ellipticCurves) > 0 {
config.EllipticCurves = append([]elliptic.Curve(nil), c.ellipticCurves...)
}
return config
}
// buildConfig builds a Config from the provided options, for mixed client/server cases.
func buildConfig(opts ...Option) (*Config, error) {
cfg := &dtlsConfig{}
cfg.applyDefaults()
for _, opt := range opts {
if err := opt.applyServer(cfg); err != nil {
return nil, err
}
}
return cfg.toConfig(), nil
}
// buildServerConfig builds a Config for server from the provided options.
func buildServerConfig(opts ...ServerOption) (*Config, error) {
cfg := &dtlsConfig{}
cfg.applyDefaults()
for _, opt := range opts {
if err := opt.applyServer(cfg); err != nil {
return nil, err
}
}
return cfg.toConfig(), nil
}
// buildClientConfig builds a Config for client from the provided options.
func buildClientConfig(opts ...ClientOption) (*Config, error) {
cfg := &dtlsConfig{}
cfg.applyDefaults()
for _, opt := range opts {
if err := opt.applyClient(cfg); err != nil {
return nil, err
}
}
return cfg.toConfig(), nil
}
// sharedOption wraps an apply function that works for both client and server.
// This eliminates code duplication for options that behave identically on both sides.
type sharedOption func(*dtlsConfig) error
func (o sharedOption) applyServer(c *dtlsConfig) error { return o(c) }
func (o sharedOption) applyClient(c *dtlsConfig) error { return o(c) }
// WithCertificates sets the certificate chain to present to the other side of the connection.
// For functional options, an explicitly empty slice is not allowed.
func WithCertificates(certs ...tls.Certificate) Option {
return sharedOption(func(c *dtlsConfig) error {
if len(certs) == 0 {
return errEmptyCertificates
}
c.certificates = defensiveCopy(certs...)
return nil
})
}
// WithCipherSuites sets the supported cipher suites.
// For functional options, an explicitly empty slice is not allowed.
func WithCipherSuites(suites ...CipherSuiteID) Option {
return sharedOption(func(c *dtlsConfig) error {
if len(suites) == 0 {
return errEmptyCipherSuites
}
c.cipherSuites = defensiveCopy(suites...)
return nil
})
}
// WithCustomCipherSuites sets the custom cipher suites provider.
// Returns an error if the provider is nil.
func WithCustomCipherSuites(fn func() []CipherSuite) Option {
return sharedOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilCustomCipherSuites
}
c.customCipherSuites = fn
return nil
})
}
// WithSignatureSchemes sets the signature schemes.
// For functional options, an explicitly empty slice is not allowed.
func WithSignatureSchemes(schemes ...tls.SignatureScheme) Option {
return sharedOption(func(c *dtlsConfig) error {
if len(schemes) == 0 {
return errEmptySignatureSchemes
}
c.signatureSchemes = defensiveCopy(schemes...)
return nil
})
}
// WithCertificateSignatureSchemes sets the signature and hash schemes that may be used
// in digital signatures for X.509 certificates. If not set, the signature_algorithms_cert
// extension is not sent, and SignatureSchemes is used for both handshake signatures and
// certificate chain validation, as specified in RFC 8446 Section 4.2.3.
// For functional options, an explicitly empty slice is not allowed.
func WithCertificateSignatureSchemes(schemes ...tls.SignatureScheme) Option {
return sharedOption(func(c *dtlsConfig) error {
if len(schemes) == 0 {
return errEmptyCertificateSignatureSchemes
}
c.certificateSignatureSchemes = defensiveCopy(schemes...)
return nil
})
}
// WithSRTPProtectionProfiles sets the SRTP protection profiles.
// For functional options, an explicitly empty slice is not allowed.
func WithSRTPProtectionProfiles(profiles ...SRTPProtectionProfile) Option {
return sharedOption(func(c *dtlsConfig) error {
if len(profiles) == 0 {
return errEmptySRTPProtectionProfiles
}
c.srtpProtectionProfiles = defensiveCopy(profiles...)
return nil
})
}
// WithSRTPMasterKeyIdentifier sets the SRTP master key identifier.
func WithSRTPMasterKeyIdentifier(identifier []byte) Option {
return sharedOption(func(c *dtlsConfig) error {
c.srtpMasterKeyIdentifier = defensiveCopy(identifier...)
return nil
})
}
// WithExtendedMasterSecret sets the extended master secret policy.
// Returns an error if the type is invalid.
func WithExtendedMasterSecret(ems ExtendedMasterSecretType) Option {
return sharedOption(func(c *dtlsConfig) error {
if ems < RequestExtendedMasterSecret || ems > DisableExtendedMasterSecret {
return errInvalidExtendedMasterSecretType
}
c.extendedMasterSecret = ems
return nil
})
}
// WithFlightInterval sets the flight interval for handshake messages.
// Returns an error if the interval is not positive.
func WithFlightInterval(interval time.Duration) Option {
return sharedOption(func(c *dtlsConfig) error {
if interval <= 0 {
return errInvalidFlightInterval
}
c.flightInterval = interval
return nil
})
}
// WithDisableRetransmitBackoff disables retransmit backoff.
func WithDisableRetransmitBackoff(disable bool) Option {
return sharedOption(func(c *dtlsConfig) error {
c.disableRetransmitBackoff = disable
return nil
})
}
// WithPSK sets the pre-shared key callback.
// Returns an error if the callback is nil.
func WithPSK(callback PSKCallback) Option {
return sharedOption(func(c *dtlsConfig) error {
if callback == nil {
return errNilPSKCallback
}
c.psk = callback
return nil
})
}
// WithPSKIdentityHint sets the PSK identity hint.
func WithPSKIdentityHint(hint []byte) Option {
return sharedOption(func(c *dtlsConfig) error {
c.pskIdentityHint = defensiveCopy(hint...)
return nil
})
}
// WithInsecureSkipVerify skips certificate verification.
// This should only be used for testing.
func WithInsecureSkipVerify(skip bool) Option {
return sharedOption(func(c *dtlsConfig) error {
c.insecureSkipVerify = skip
return nil
})
}
// WithInsecureHashes allows the use of insecure hash algorithms.
func WithInsecureHashes(allow bool) Option {
return sharedOption(func(c *dtlsConfig) error {
c.insecureHashes = allow
return nil
})
}
// WithVerifyPeerCertificate sets the peer certificate verification callback.
// Returns an error if the callback is nil.
func WithVerifyPeerCertificate(fn func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error) Option {
return sharedOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilVerifyPeerCertificate
}
c.verifyPeerCertificate = fn
return nil
})
}
// WithVerifyConnection sets the connection verification callback.
// Returns an error if the callback is nil.
func WithVerifyConnection(fn func(*State) error) Option {
return sharedOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilVerifyConnection
}
c.verifyConnection = fn
return nil
})
}
// WithRootCAs sets the root certificate authorities.
func WithRootCAs(pool *x509.CertPool) Option {
return sharedOption(func(c *dtlsConfig) error {
c.rootCAs = pool
return nil
})
}
// WithServerName sets the server name for certificate verification.
func WithServerName(name string) Option {
return sharedOption(func(c *dtlsConfig) error {
c.serverName = name
return nil
})
}
// WithLoggerFactory sets the logger factory for creating loggers.
func WithLoggerFactory(factory logging.LoggerFactory) Option {
return sharedOption(func(c *dtlsConfig) error {
c.loggerFactory = factory
return nil
})
}
// WithMTU sets the maximum transmission unit.
// Returns an error if the MTU is not positive.
func WithMTU(mtu int) Option {
return sharedOption(func(c *dtlsConfig) error {
if mtu <= 0 {
return errInvalidMTU
}
c.mtu = mtu
return nil
})
}
// WithReplayProtectionWindow sets the replay protection window size.
// Returns an error if the window size is negative.
func WithReplayProtectionWindow(window int) Option {
return sharedOption(func(c *dtlsConfig) error {
if window < 0 {
return errInvalidReplayProtectionWindow
}
c.replayProtectionWindow = window
return nil
})
}
// WithKeyLogWriter sets the key log writer for debugging.
// Use of KeyLogWriter compromises security and should only be used for debugging.
func WithKeyLogWriter(writer io.Writer) Option {
return sharedOption(func(c *dtlsConfig) error {
c.keyLogWriter = writer
return nil
})
}
// WithSessionStore sets the session store for resumption.
func WithSessionStore(store SessionStore) Option {
return sharedOption(func(c *dtlsConfig) error {
c.sessionStore = store
return nil
})
}
// WithSupportedProtocols sets the supported application protocols for ALPN.
// For functional options, an explicitly empty slice is not allowed.
func WithSupportedProtocols(protocols ...string) Option {
return sharedOption(func(c *dtlsConfig) error {
if len(protocols) == 0 {
return errEmptySupportedProtocols
}
c.supportedProtocols = defensiveCopy(protocols...)
return nil
})
}
// WithEllipticCurves sets the elliptic curves.
// For functional options, an explicitly empty slice is not allowed.
func WithEllipticCurves(curves ...elliptic.Curve) Option {
return sharedOption(func(c *dtlsConfig) error {
if len(curves) == 0 {
return errEmptyEllipticCurves
}
c.ellipticCurves = defensiveCopy(curves...)
return nil
})
}
// WithGetClientCertificate sets the client certificate getter callback.
// Returns an error if the callback is nil.
func WithGetClientCertificate(fn func(*CertificateRequestInfo) (*tls.Certificate, error)) Option {
return sharedOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilGetClientCertificate
}
c.getClientCertificate = fn
return nil
})
}
// WithConnectionIDGenerator sets the connection ID generator.
// Returns an error if the generator is nil.
func WithConnectionIDGenerator(fn func() []byte) Option {
return sharedOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilConnectionIDGenerator
}
c.connectionIDGenerator = fn
return nil
})
}
// WithPaddingLengthGenerator sets the padding length generator.
// Returns an error if the generator is nil.
func WithPaddingLengthGenerator(fn func(uint) uint) Option {
return sharedOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilPaddingLengthGenerator
}
c.paddingLengthGenerator = fn
return nil
})
}
// WithHelloRandomBytesGenerator sets the hello random bytes generator.
// Returns an error if the generator is nil.
func WithHelloRandomBytesGenerator(fn func() [handshake.RandomBytesLength]byte) Option {
return sharedOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilHelloRandomBytesGenerator
}
c.helloRandomBytesGenerator = fn
return nil
})
}
// WithClientHelloMessageHook sets the client hello message hook.
// Returns an error if the hook is nil.
func WithClientHelloMessageHook(fn func(handshake.MessageClientHello) handshake.Message) Option {
return sharedOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilClientHelloMessageHook
}
c.clientHelloMessageHook = fn
return nil
})
}
// serverOnlyOption wraps an apply function for server-only options.
type serverOnlyOption func(*dtlsConfig) error
func (o serverOnlyOption) applyServer(c *dtlsConfig) error { return o(c) }
// WithClientAuth sets the client authentication policy.
// Returns an error if the type is invalid.
// This option is only applicable to servers.
func WithClientAuth(auth ClientAuthType) ServerOption {
return serverOnlyOption(func(c *dtlsConfig) error {
if auth < NoClientCert || auth > RequireAndVerifyClientCert {
return errInvalidClientAuthType
}
c.clientAuth = auth
return nil
})
}
// WithClientCAs sets the client certificate authorities.
// This option is only applicable to servers.
func WithClientCAs(pool *x509.CertPool) ServerOption {
return serverOnlyOption(func(c *dtlsConfig) error {
c.clientCAs = pool
return nil
})
}
// WithGetCertificate sets the certificate getter callback.
// Returns an error if the callback is nil.
// This option is only applicable to servers.
func WithGetCertificate(fn func(*ClientHelloInfo) (*tls.Certificate, error)) ServerOption {
return serverOnlyOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilGetCertificate
}
c.getCertificate = fn
return nil
})
}
// WithInsecureSkipVerifyHello skips hello verify phase on the server.
// This has implication on DoS attack resistance.
// This option is only applicable to servers.
func WithInsecureSkipVerifyHello(skip bool) ServerOption {
return serverOnlyOption(func(c *dtlsConfig) error {
c.insecureSkipVerifyHello = skip
return nil
})
}
// WithServerHelloMessageHook sets the server hello message hook.
// Returns an error if the hook is nil.
// This option is only applicable to servers.
func WithServerHelloMessageHook(fn func(handshake.MessageServerHello) handshake.Message) ServerOption {
return serverOnlyOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilServerHelloMessageHook
}
c.serverHelloMessageHook = fn
return nil
})
}
// WithCertificateRequestMessageHook sets the certificate request message hook.
// Returns an error if the hook is nil.
// This option is only applicable to servers.
func WithCertificateRequestMessageHook(fn func(handshake.MessageCertificateRequest) handshake.Message) ServerOption {
return serverOnlyOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilCertificateRequestMessageHook
}
c.certificateRequestMessageHook = fn
return nil
})
}
// WithOnConnectionAttempt sets the connection attempt callback.
// Returns an error if the callback is nil.
// This option is only applicable to servers.
func WithOnConnectionAttempt(fn func(net.Addr) error) ServerOption {
return serverOnlyOption(func(c *dtlsConfig) error {
if fn == nil {
return errNilOnConnectionAttempt
}
c.onConnectionAttempt = fn
return nil
})
}

15
vendor/github.com/pion/dtls/v3/packet.go generated vendored Normal file
View file

@ -0,0 +1,15 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package dtls
import (
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
type packet struct {
record *recordlayer.RecordLayer
shouldEncrypt bool
shouldWrapCID bool
resetLocalSequenceNumber bool
}

260
vendor/github.com/pion/dtls/v3/pkg/crypto/ccm/ccm.go generated vendored Normal file
View file

@ -0,0 +1,260 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package ccm implements a CCM, Counter with CBC-MAC
// as per RFC 3610.
//
// See https://tools.ietf.org/html/rfc3610
//
// This code was lifted from https://github.com/bocajim/dtls/blob/a3300364a283fcb490d28a93d7fcfa7ba437fbbe/ccm/ccm.go
// and as such was not written by the Pions authors. Like Pions this
// code is licensed under MIT.
//
// A request for including CCM into the Go standard library
// can be found as issue #27484 on the https://github.com/golang/go/
// repository.
package ccm
import (
"crypto/cipher"
"crypto/subtle"
"encoding/binary"
"errors"
"math"
)
// ccm represents a Counter with CBC-MAC with a specific key.
type ccm struct {
b cipher.Block
M uint8
L uint8
}
const ccmBlockSize = 16
// CCM is a block cipher in Counter with CBC-MAC mode.
// Providing authenticated encryption with associated data via the cipher.AEAD interface.
type CCM interface {
cipher.AEAD
// MaxLength returns the maxium length of plaintext in calls to Seal.
// The maximum length of ciphertext in calls to Open is MaxLength()+Overhead().
// The maximum length is related to CCM's `L` parameter (15-noncesize) and
// is 1<<(8*L) - 1 (but also limited by the maxium size of an int).
MaxLength() int
}
var (
errInvalidBlockSize = errors.New("ccm: NewCCM requires 128-bit block cipher")
errInvalidTagSize = errors.New("ccm: tagsize must be 4, 6, 8, 10, 12, 14, or 16")
errInvalidNonceSize = errors.New("ccm: invalid nonce size")
)
// NewCCM returns the given 128-bit block cipher wrapped in CCM.
// The tagsize must be an even integer between 4 and 16 inclusive
// and is used as CCM's `M` parameter.
// The noncesize must be an integer between 7 and 13 inclusive,
// 15-noncesize is used as CCM's `L` parameter.
func NewCCM(b cipher.Block, tagsize, noncesize int) (CCM, error) {
if b.BlockSize() != ccmBlockSize {
return nil, errInvalidBlockSize
}
if tagsize < 4 || tagsize > 16 || tagsize&1 != 0 {
return nil, errInvalidTagSize
}
lensize := 15 - noncesize
if lensize < 2 || lensize > 8 {
return nil, errInvalidNonceSize
}
c := &ccm{b: b, M: uint8(tagsize), L: uint8(lensize)} //nolint:gosec // G114
return c, nil
}
func (c *ccm) NonceSize() int { return 15 - int(c.L) }
func (c *ccm) Overhead() int { return int(c.M) }
func (c *ccm) MaxLength() int { return maxlen(c.L, c.Overhead()) }
func maxlen(l uint8, tagsize int) int {
mLen := (uint64(1) << (8 * l)) - 1
if m64 := uint64(math.MaxInt64) - uint64(tagsize); l > 8 || mLen > m64 { //nolint:gosec // G114
mLen = m64 // The maximum lentgh on a 64bit arch
}
if mLen != uint64(int(mLen)) { //nolint:gosec // G114
return math.MaxInt32 - tagsize // We have only 32bit int's
}
return int(mLen) //nolint:gosec // G114
}
// MaxNonceLength returns the maximum nonce length for a given plaintext length.
// A return value <= 0 indicates that plaintext length is too large for
// any nonce length.
func MaxNonceLength(pdatalen int) int {
const tagsize = 16
for L := 2; L <= 8; L++ {
if maxlen(uint8(L), tagsize) >= pdatalen { //nolint:gosec // G115
return 15 - L
}
}
return 0
}
func (c *ccm) cbcRound(mac, data []byte) {
for i := 0; i < ccmBlockSize; i++ {
mac[i] ^= data[i]
}
c.b.Encrypt(mac, mac)
}
func (c *ccm) cbcData(mac, data []byte) {
for len(data) >= ccmBlockSize {
c.cbcRound(mac, data[:ccmBlockSize])
data = data[ccmBlockSize:]
}
if len(data) > 0 {
var block [ccmBlockSize]byte
copy(block[:], data)
c.cbcRound(mac, block[:])
}
}
var errPlaintextTooLong = errors.New("ccm: plaintext too large")
func (c *ccm) tag(nonce, plaintext, adata []byte) ([]byte, error) {
var mac [ccmBlockSize]byte
if len(adata) > 0 {
mac[0] |= 1 << 6
}
mac[0] |= (c.M - 2) << 2
mac[0] |= c.L - 1
if len(nonce) != c.NonceSize() {
return nil, errInvalidNonceSize
}
if len(plaintext) > c.MaxLength() {
return nil, errPlaintextTooLong
}
binary.BigEndian.PutUint64(mac[ccmBlockSize-8:], uint64(len(plaintext)))
copy(mac[1:ccmBlockSize-c.L], nonce)
c.b.Encrypt(mac[:], mac[:])
var block [ccmBlockSize]byte
if adataLength := uint64(len(adata)); adataLength > 0 { //nolint:nestif
// First adata block includes adata length
i := 2
if adataLength <= 0xfeff {
binary.BigEndian.PutUint16(block[:i], uint16(adataLength))
} else {
binary.BigEndian.PutUint16(block[0:2], 0xfeff)
if adataLength < uint64(1<<32) {
i = 2 + 4
binary.BigEndian.PutUint32(block[2:i], uint32(adataLength)) //nolint:gosec // G115
} else {
i = 2 + 8
binary.BigEndian.PutUint64(block[2:i], adataLength)
}
}
i = copy(block[i:], adata)
c.cbcRound(mac[:], block[:])
c.cbcData(mac[:], adata[i:])
}
if len(plaintext) > 0 {
c.cbcData(mac[:], plaintext)
}
return mac[:c.M], nil
}
// sliceForAppend takes a slice and a requested number of bytes. It returns a
// slice with the contents of the given slice followed by that many bytes and a
// second slice that aliases into it and contains only the extra bytes. If the
// original slice has sufficient capacity then no allocation is performed.
// From crypto/cipher/gcm.go
// .
func sliceForAppend(in []byte, n int) (head, tail []byte) {
if total := len(in) + n; cap(in) >= total {
head = in[:total]
} else {
head = make([]byte, total)
copy(head, in)
}
tail = head[len(in):]
return
}
// Seal encrypts and authenticates plaintext, authenticates the
// additional data and appends the result to dst, returning the updated
// slice. The nonce must be NonceSize() bytes long and unique for all
// time, for a given key.
// The plaintext must be no longer than MaxLength() bytes long.
//
// The plaintext and dst may alias exactly or not at all.
func (c *ccm) Seal(dst, nonce, plaintext, adata []byte) []byte {
tag, err := c.tag(nonce, plaintext, adata)
if err != nil {
// The cipher.AEAD interface doesn't allow for an error return.
panic(err) // nolint
}
var iv, s0 [ccmBlockSize]byte
iv[0] = c.L - 1
copy(iv[1:ccmBlockSize-c.L], nonce)
c.b.Encrypt(s0[:], iv[:])
for i := 0; i < int(c.M); i++ {
tag[i] ^= s0[i]
}
iv[len(iv)-1] |= 1
stream := cipher.NewCTR(c.b, iv[:])
ret, out := sliceForAppend(dst, len(plaintext)+int(c.M))
stream.XORKeyStream(out, plaintext)
copy(out[len(plaintext):], tag)
return ret
}
var (
errOpen = errors.New("ccm: message authentication failed")
errCiphertextTooShort = errors.New("ccm: ciphertext too short")
errCiphertextTooLong = errors.New("ccm: ciphertext too long")
)
func (c *ccm) Open(dst, nonce, ciphertext, adata []byte) ([]byte, error) {
if len(ciphertext) < int(c.M) {
return nil, errCiphertextTooShort
}
if len(ciphertext) > c.MaxLength()+c.Overhead() {
return nil, errCiphertextTooLong
}
tag := make([]byte, int(c.M))
copy(tag, ciphertext[len(ciphertext)-int(c.M):])
ciphertextWithoutTag := ciphertext[:len(ciphertext)-int(c.M)]
var iv, s0 [ccmBlockSize]byte
iv[0] = c.L - 1
copy(iv[1:ccmBlockSize-c.L], nonce)
c.b.Encrypt(s0[:], iv[:])
for i := 0; i < int(c.M); i++ {
tag[i] ^= s0[i]
}
iv[len(iv)-1] |= 1
stream := cipher.NewCTR(c.b, iv[:])
// Cannot decrypt directly to dst since we're not supposed to
// reveal the plaintext to the caller if authentication fails.
plaintext := make([]byte, len(ciphertextWithoutTag))
stream.XORKeyStream(plaintext, ciphertextWithoutTag)
expectedTag, err := c.tag(nonce, plaintext, adata)
if err != nil {
return nil, err
}
if subtle.ConstantTimeCompare(tag, expectedTag) != 1 {
return nil, errOpen
}
return append(dst, plaintext...), nil
}

View file

@ -0,0 +1,128 @@
# Ciphersuite Package
This package provides DTLS cipher suite implementations for GCM, CCM, and CBC modes.
## Benchmarking
The package includes comprehensive benchmarks for all cipher operations across multiple payload sizes.
**Note:** Benchmarks are excluded from regular test runs using build tags. You must specify `-tags=bench` to run them.
### Running all ciphersuite benchmarks
```bash
go test -tags=bench -bench=. -benchmem
```
### Running a specific benchmark
- GCM benchmarks only:
```bash
go test -tags=bench -bench=BenchmarkGCM -benchmem
```
- GCM `Encrypt` benchmark only:
```bash
go test -tags=bench -bench=BenchmarkGCMEncrypt -benchmem
```
- GCM `Decrypt` benchmark only:
```bash
go test -tags=bench -bench=BenchmarkGCMDecrypt -benchmem
```
- CCM benchmarks only:
```bash
go test -tags=bench -bench=BenchmarkCCM -benchmem
```
- CCM `Encrypt` benchmark only:
```bash
go test -tags=bench -bench=BenchmarkCCMEncrypt -benchmem
```
- CCM `Decrypt` benchmark only:
```bash
go test -tags=bench -bench=BenchmarkCCMDecrypt -benchmem
```
- CBC benchmarks only:
```bash
go test -tags=bench -bench=BenchmarkCBC -benchmem
```
- CBC `Encrypt` benchmark only:
```bash
go test -tags=bench -bench=BenchmarkCBCEncrypt -benchmem
```
- CBC `Decrypt` benchmark only:
```bash
go test -tags=bench -bench=BenchmarkCBCDecrypt -benchmem
```
- All ciphers, with 1KB payloads only
```bash
go test -tags=bench -bench=/1KB -benchmem
```
- All ciphers, with 16B payloads only
```bash
go test -tags=bench -bench=/16B -benchmem
```
### Benchmark Options
Increase benchmark time for more accurate results:
```bash
go test -tags=bench -bench=BenchmarkGCM -benchmem -benchtime=5s
```
Run benchmarks multiple times:
```bash
go test -tags=bench -bench=BenchmarkGCM -benchmem -count=5
```
### Understanding Results
Example output:
```
BenchmarkGCMEncrypt/016B-8 5895367 202.6 ns/op 78.99 MB/s 160 B/op 5 allocs/op
```
- `5895367`: Number of iterations
- `202.6 ns/op`: Time per operation
- `78.99 MB/s`: Throughput
- `160 B/op`: Bytes allocated per operation
- `5 allocs/op`: Number of allocations per operation
## Profiling
Generate CPU profile:
```bash
go test -tags=bench -bench=BenchmarkGCMEncrypt -benchmem -cpuprofile=cpu.prof
go tool pprof -top cpu.prof
```
Generate memory profile:
```bash
go test -tags=bench -bench=BenchmarkGCMEncrypt -benchmem -memprofile=mem.prof
go tool pprof -top -alloc_objects mem.prof
```

View file

@ -0,0 +1,250 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import ( //nolint:gci
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"encoding/binary"
"hash"
"github.com/pion/dtls/v3/internal/util"
"github.com/pion/dtls/v3/pkg/crypto/prf"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
"golang.org/x/crypto/cryptobyte"
)
// block ciphers using cipher block chaining.
type cbcMode interface {
cipher.BlockMode
SetIV([]byte)
}
// CBC Provides an API to Encrypt/Decrypt DTLS 1.2 Packets.
type CBC struct {
writeCBC, readCBC cbcMode
writeMac, readMac []byte
h prf.HashFunc
}
// NewCBC creates a DTLS CBC Cipher.
func NewCBC(
localKey, localWriteIV, localMac, remoteKey, remoteWriteIV, remoteMac []byte,
hashFunc prf.HashFunc,
) (*CBC, error) {
writeBlock, err := aes.NewCipher(localKey)
if err != nil {
return nil, err
}
readBlock, err := aes.NewCipher(remoteKey)
if err != nil {
return nil, err
}
writeCBC, ok := cipher.NewCBCEncrypter(writeBlock, localWriteIV).(cbcMode)
if !ok {
return nil, errFailedToCast
}
readCBC, ok := cipher.NewCBCDecrypter(readBlock, remoteWriteIV).(cbcMode)
if !ok {
return nil, errFailedToCast
}
return &CBC{
writeCBC: writeCBC,
writeMac: localMac,
readCBC: readCBC,
readMac: remoteMac,
h: hashFunc,
}, nil
}
// Encrypt encrypt a DTLS RecordLayer message.
func (c *CBC) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
payload := raw[pkt.Header.Size():]
raw = raw[:pkt.Header.Size()]
blockSize := c.writeCBC.BlockSize()
// Generate + Append MAC
h := pkt.Header
var err error
var mac []byte
if h.ContentType == protocol.ContentTypeConnectionID {
mac, err = c.hmacCID(h.Epoch, h.SequenceNumber, h.Version, payload, c.writeMac, c.h, h.ConnectionID)
} else {
mac, err = c.hmac(h.Epoch, h.SequenceNumber, h.ContentType, h.Version, payload, c.writeMac, c.h)
}
if err != nil {
return nil, err
}
payload = append(payload, mac...)
// Generate + Append padding
padding := make([]byte, blockSize-len(payload)%blockSize)
paddingLen := len(padding)
for i := 0; i < paddingLen; i++ {
padding[i] = byte(paddingLen - 1)
}
payload = append(payload, padding...)
// Generate IV
iv := make([]byte, blockSize)
if _, err := rand.Read(iv); err != nil {
return nil, err
}
// Set IV + Encrypt + Prepend IV
c.writeCBC.SetIV(iv)
c.writeCBC.CryptBlocks(payload, payload)
payload = append(iv, payload...) //nolint:makezero // todo: FIX
// Prepend unencrypted header with encrypted payload
raw = append(raw, payload...)
// Update recordLayer size to include IV+MAC+Padding
binary.BigEndian.PutUint16(raw[pkt.Header.Size()-2:], uint16(len(raw)-pkt.Header.Size())) //nolint:gosec //G115
return raw, nil
}
// Decrypt decrypts a DTLS RecordLayer message.
func (c *CBC) Decrypt(header recordlayer.Header, in []byte) ([]byte, error) {
blockSize := c.readCBC.BlockSize()
mac := c.h()
if err := header.Unmarshal(in); err != nil {
return nil, err
}
body := in[header.Size():]
switch {
case header.ContentType == protocol.ContentTypeChangeCipherSpec:
// Nothing to encrypt with ChangeCipherSpec
return in, nil
case len(body)%blockSize != 0 || len(body) < blockSize+util.Max(mac.Size()+1, blockSize):
return nil, errNotEnoughRoomForNonce
}
// Set + remove per record IV
c.readCBC.SetIV(body[:blockSize])
body = body[blockSize:]
// Decrypt
c.readCBC.CryptBlocks(body, body)
// Padding+MAC needs to be checked in constant time
// Otherwise we reveal information about the level of correctness
paddingLen, paddingGood := examinePadding(body)
if paddingGood != 255 {
return nil, errInvalidMAC
}
macSize := mac.Size()
if len(body) < macSize {
return nil, errInvalidMAC
}
dataEnd := len(body) - macSize - paddingLen
expectedMAC := body[dataEnd : dataEnd+macSize]
var err error
var actualMAC []byte
if header.ContentType == protocol.ContentTypeConnectionID {
actualMAC, err = c.hmacCID(
header.Epoch, header.SequenceNumber, header.Version, body[:dataEnd], c.readMac, c.h, header.ConnectionID,
)
} else {
actualMAC, err = c.hmac(
header.Epoch, header.SequenceNumber, header.ContentType, header.Version, body[:dataEnd], c.readMac, c.h,
)
}
// Compute Local MAC and compare
if err != nil || !hmac.Equal(actualMAC, expectedMAC) {
return nil, errInvalidMAC
}
return append(in[:header.Size()], body[:dataEnd]...), nil
}
func (c *CBC) hmac(
epoch uint16,
sequenceNumber uint64,
contentType protocol.ContentType,
protocolVersion protocol.Version,
payload []byte,
key []byte,
hf func() hash.Hash,
) ([]byte, error) {
hmacHash := hmac.New(hf, key)
msg := make([]byte, 13)
binary.BigEndian.PutUint16(msg, epoch)
util.PutBigEndianUint48(msg[2:], sequenceNumber)
msg[8] = byte(contentType)
msg[9] = protocolVersion.Major
msg[10] = protocolVersion.Minor
binary.BigEndian.PutUint16(msg[11:], uint16(len(payload))) //nolint:gosec //G115
if _, err := hmacHash.Write(msg); err != nil {
return nil, err
}
if _, err := hmacHash.Write(payload); err != nil {
return nil, err
}
return hmacHash.Sum(nil), nil
}
// hmacCID calculates a MAC according to
// https://datatracker.ietf.org/doc/html/rfc9146#section-5.1
func (c *CBC) hmacCID(
epoch uint16,
sequenceNumber uint64,
protocolVersion protocol.Version,
payload []byte,
key []byte,
hf func() hash.Hash,
cid []byte,
) ([]byte, error) {
// Must unmarshal inner plaintext in orde to perform MAC.
ip := &recordlayer.InnerPlaintext{}
if err := ip.Unmarshal(payload); err != nil {
return nil, err
}
hmacHash := hmac.New(hf, key)
var msg cryptobyte.Builder
msg.AddUint64(seqNumPlaceholder)
msg.AddUint8(uint8(protocol.ContentTypeConnectionID))
msg.AddUint8(uint8(len(cid))) //nolint:gosec //G115
msg.AddUint8(uint8(protocol.ContentTypeConnectionID))
msg.AddUint8(protocolVersion.Major)
msg.AddUint8(protocolVersion.Minor)
msg.AddUint16(epoch)
util.AddUint48(&msg, sequenceNumber)
msg.AddBytes(cid)
msg.AddUint16(uint16(len(payload))) //nolint:gosec //G115
msg.AddBytes(ip.Content)
msg.AddUint8(uint8(ip.RealType))
msg.AddBytes(make([]byte, ip.Zeros))
if _, err := hmacHash.Write(msg.BytesOrPanic()); err != nil {
return nil, err
}
if _, err := hmacHash.Write(payload); err != nil {
return nil, err
}
return hmacHash.Sum(nil), nil
}

View file

@ -0,0 +1,68 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"crypto/aes"
"github.com/pion/dtls/v3/pkg/crypto/ccm"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
// CCMTagLen is the length of Authentication Tag.
type CCMTagLen int
// CCM Enums.
const (
CCMTagLength8 CCMTagLen = 8
CCMTagLength CCMTagLen = 16
ccmNonceLength = 12
)
// CCM Provides an API to Encrypt/Decrypt DTLS 1.2 Packets.
type CCM struct {
aead *aead
}
// NewCCM creates a DTLS GCM Cipher.
func NewCCM(tagLen CCMTagLen, localKey, localWriteIV, remoteKey, remoteWriteIV []byte) (*CCM, error) {
localBlock, err := aes.NewCipher(localKey)
if err != nil {
return nil, err
}
localCCM, err := ccm.NewCCM(localBlock, int(tagLen), ccmNonceLength)
if err != nil {
return nil, err
}
remoteBlock, err := aes.NewCipher(remoteKey)
if err != nil {
return nil, err
}
remoteCCM, err := ccm.NewCCM(remoteBlock, int(tagLen), ccmNonceLength)
if err != nil {
return nil, err
}
return &CCM{
aead: newAEAD(
localCCM,
localWriteIV,
remoteCCM,
remoteWriteIV,
ccmNonceLength,
int(tagLen),
),
}, nil
}
// Encrypt encrypt a DTLS RecordLayer message.
func (c *CCM) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
return c.aead.encrypt(pkt, raw)
}
// Decrypt decrypts a DTLS RecordLayer message.
func (c *CCM) Decrypt(header recordlayer.Header, in []byte) ([]byte, error) {
return c.aead.decrypt(header, in)
}

View file

@ -0,0 +1,226 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
// Package ciphersuite provides the crypto operations needed for a DTLS CipherSuite
package ciphersuite
import (
"crypto/cipher"
"encoding/binary"
"errors"
"fmt"
"sync"
"github.com/pion/dtls/v3/internal/util"
"github.com/pion/dtls/v3/pkg/protocol"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
"golang.org/x/crypto/cryptobyte"
)
const (
// 8 bytes of 0xff.
// https://datatracker.ietf.org/doc/html/rfc9146#name-record-payload-protection
seqNumPlaceholder = 0xffffffffffffffff
)
var (
//nolint:err113
errNotEnoughRoomForNonce = &protocol.InternalError{Err: errors.New("buffer not long enough to contain nonce")}
//nolint:err113
errDecryptPacket = &protocol.TemporaryError{Err: errors.New("failed to decrypt packet")}
//nolint:err113
errInvalidMAC = &protocol.TemporaryError{Err: errors.New("invalid mac")}
//nolint:err113
errFailedToCast = &protocol.FatalError{Err: errors.New("failed to cast")}
)
// aead provides a generic API to Encrypt/Decrypt DTLS 1.2 Packets.
type aead struct {
localAEAD cipher.AEAD
remoteAEAD cipher.AEAD
localWriteIV []byte
remoteWriteIV []byte
nonceLength int
tagLength int
// buffer pool for (fixed-size) nonces.
nonceBufferPool sync.Pool
}
// newAEAD creates a generic DTLS AEAD-based Cipher.
func newAEAD(
localAEAD cipher.AEAD,
localWriteIV []byte,
remoteAEAD cipher.AEAD,
remoteWriteIV []byte,
nonceLength int,
tagLength int,
) *aead {
return &aead{
localAEAD: localAEAD,
localWriteIV: localWriteIV,
remoteAEAD: remoteAEAD,
remoteWriteIV: remoteWriteIV,
nonceLength: nonceLength,
tagLength: tagLength,
nonceBufferPool: sync.Pool{
New: func() any {
b := make([]byte, nonceLength)
return &b // nolint:nlreturn
},
},
}
}
// encrypt encrypts a DTLS RecordLayer message.
func (a *aead) encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
payload := raw[pkt.Header.Size():]
raw = raw[:pkt.Header.Size()]
// Get nonce buffer from pool
noncePtr := a.nonceBufferPool.Get().(*[]byte) // nolint:forcetypeassert
nonce := *noncePtr
copy(nonce, a.localWriteIV[:4])
// https://www.rfc-editor.org/rfc/rfc9325#name-nonce-reuse-in-tls-12
seq64 := (uint64(pkt.Header.Epoch) << 48) | (pkt.Header.SequenceNumber & 0x0000ffffffffffff)
binary.BigEndian.PutUint64(nonce[4:], seq64)
var additionalData []byte
if pkt.Header.ContentType == protocol.ContentTypeConnectionID {
additionalData = generateAEADAdditionalDataCID(&pkt.Header, len(payload))
} else {
additionalData = generateAEADAdditionalData(&pkt.Header, len(payload))
}
finalSize := len(raw) + 8 + len(payload) + a.tagLength
r := make([]byte, finalSize)
copy(r, raw)
copy(r[len(raw):], nonce[4:])
a.localAEAD.Seal(r[len(raw)+8:len(raw)+8], nonce, payload, additionalData)
// Update recordLayer size to include explicit nonce
binary.BigEndian.PutUint16(r[pkt.Header.Size()-2:], uint16(len(r)-pkt.Header.Size())) //nolint:gosec //G115
// Return nonce buffer to pool
a.nonceBufferPool.Put(noncePtr)
return r, nil
}
// decrypt decrypts a DTLS RecordLayer message.
func (a *aead) decrypt(header recordlayer.Header, in []byte) ([]byte, error) {
err := header.Unmarshal(in)
switch {
case err != nil:
return nil, err
case header.ContentType == protocol.ContentTypeChangeCipherSpec:
// Nothing to encrypt with ChangeCipherSpec
return in, nil
case len(in) <= (8 + header.Size()):
return nil, errNotEnoughRoomForNonce
}
// Get nonce buffer from pool
noncePtr := a.nonceBufferPool.Get().(*[]byte) // nolint:forcetypeassert
nonce := *noncePtr
copy(nonce[:4], a.remoteWriteIV[:4])
copy(nonce[4:], in[header.Size():header.Size()+8])
out := in[header.Size()+8:]
var additionalData []byte
if header.ContentType == protocol.ContentTypeConnectionID {
additionalData = generateAEADAdditionalDataCID(&header, len(out)-a.tagLength)
} else {
additionalData = generateAEADAdditionalData(&header, len(out)-a.tagLength)
}
out, err = a.remoteAEAD.Open(out[:0], nonce, out, additionalData)
if err != nil {
// Return nonce buffer to pool
a.nonceBufferPool.Put(noncePtr)
return nil, fmt.Errorf("%w: %v", errDecryptPacket, err) //nolint:errorlint
}
// Return nonce buffer to pool
a.nonceBufferPool.Put(noncePtr)
return append(in[:header.Size()], out...), nil
}
func generateAEADAdditionalData(h *recordlayer.Header, payloadLen int) []byte {
var additionalData [13]byte
// SequenceNumber MUST be set first
// we only want uint48, clobbering an extra 2 (using uint64, Golang doesn't have uint48)
binary.BigEndian.PutUint64(additionalData[:], h.SequenceNumber)
binary.BigEndian.PutUint16(additionalData[:], h.Epoch)
additionalData[8] = byte(h.ContentType)
additionalData[9] = h.Version.Major
additionalData[10] = h.Version.Minor
//nolint:gosec //G115
binary.BigEndian.PutUint16(additionalData[len(additionalData)-2:], uint16(payloadLen))
return additionalData[:]
}
// generateAEADAdditionalDataCID generates additional data for AEAD ciphers
// according to https://datatracker.ietf.org/doc/html/rfc9146#name-aead-ciphers
func generateAEADAdditionalDataCID(h *recordlayer.Header, payloadLen int) []byte {
var builder cryptobyte.Builder
builder.AddUint64(seqNumPlaceholder)
builder.AddUint8(uint8(protocol.ContentTypeConnectionID))
builder.AddUint8(uint8(len(h.ConnectionID))) //nolint:gosec //G115
builder.AddUint8(uint8(protocol.ContentTypeConnectionID))
builder.AddUint8(h.Version.Major)
builder.AddUint8(h.Version.Minor)
builder.AddUint16(h.Epoch)
util.AddUint48(&builder, h.SequenceNumber)
builder.AddBytes(h.ConnectionID)
builder.AddUint16(uint16(payloadLen)) //nolint:gosec //G115
return builder.BytesOrPanic()
}
// examinePadding returns, in constant time, the length of the padding to remove
// from the end of payload. It also returns a byte which is equal to 255 if the
// padding was valid and 0 otherwise. See RFC 2246, Section 6.2.3.2.
//
// https://github.com/golang/go/blob/039c2081d1178f90a8fa2f4e6958693129f8de33/src/crypto/tls/conn.go#L245
func examinePadding(payload []byte) (toRemove int, good byte) {
if len(payload) < 1 {
return 0, 0
}
paddingLen := payload[len(payload)-1]
t := uint(len(payload)-1) - uint(paddingLen) //nolint:gosec //G115
// if len(payload) >= (paddingLen - 1) then the MSB of t is zero
good = byte(int32(^t) >> 31) //nolint:gosec //G115
// The maximum possible padding length plus the actual length field
toCheck := min(
// The length of the padded data is public, so we can use an if here
256, len(payload))
for i := 0; i < toCheck; i++ {
t := uint(paddingLen) - uint(i) //nolint:gosec //G115
// if i <= paddingLen then the MSB of t is zero
mask := byte(int32(^t) >> 31) //nolint:gosec //G115
b := payload[len(payload)-1-i]
good &^= mask&paddingLen ^ mask&b
}
// We AND together the bits of good and replicate the result across
// all the bits.
good &= good << 4
good &= good << 2
good &= good << 1
good = uint8(int8(good) >> 7) //nolint:gosec //G115
toRemove = int(paddingLen) + 1
return toRemove, good
}

View file

@ -0,0 +1,63 @@
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
package ciphersuite
import (
"crypto/aes"
"crypto/cipher"
"github.com/pion/dtls/v3/pkg/protocol/recordlayer"
)
const (
gcmTagLength = 16
gcmNonceLength = 12
)
// GCM Provides an API to Encrypt/Decrypt DTLS 1.2 Packets.
type GCM struct {
aead *aead
}
// NewGCM creates a DTLS GCM Cipher.
func NewGCM(localKey, localWriteIV, remoteKey, remoteWriteIV []byte) (*GCM, error) {
localBlock, err := aes.NewCipher(localKey)
if err != nil {
return nil, err
}
localGCM, err := cipher.NewGCM(localBlock)
if err != nil {
return nil, err
}
remoteBlock, err := aes.NewCipher(remoteKey)
if err != nil {
return nil, err
}
remoteGCM, err := cipher.NewGCM(remoteBlock)
if err != nil {
return nil, err
}
return &GCM{
aead: newAEAD(
localGCM,
localWriteIV,
remoteGCM,
remoteWriteIV,
gcmNonceLength,
gcmTagLength,
),
}, nil
}
// Encrypt encrypts a DTLS RecordLayer message.
func (g *GCM) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {
return g.aead.encrypt(pkt, raw)
}
// Decrypt decrypts a DTLS RecordLayer message.
func (g *GCM) Decrypt(header recordlayer.Header, in []byte) ([]byte, error) {
return g.aead.decrypt(header, in)
}

Some files were not shown because too many files have changed in this diff Show more