diff --git a/config.go b/config.go index 0dc07a14e..71d387d21 100644 --- a/config.go +++ b/config.go @@ -191,7 +191,7 @@ type Config struct { //nolint:dupl // 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. + // This has implications on DoS attack resistance. InsecureSkipVerifyHello bool // ConnectionIDGenerator generates connection identifiers that should be @@ -234,6 +234,16 @@ type Config struct { //nolint:dupl // message is sent from a server. The returned handshake message replaces the original message. CertificateRequestMessageHook func(handshake.MessageCertificateRequest) handshake.Message + // outboundHandshakePacketInterceptor is an optional callback that can be set to + // intercept outgoing raw handshake packets. It is called with the raw packet bytes + // and a boolean flag specifying if this is the last packet of a flight. + // The interceptor can decide to drop the packet by returning true. + outboundHandshakePacketInterceptor func(packet []byte, end bool) bool + // inboundHandshakePacketNotifier is an optional callback that can be set to + // receive notifications about incoming raw handshake packets. It is called with + // the raw packet bytes after the packet has been processed. + inboundHandshakePacketNotifier func(packet []byte) + // 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, diff --git a/conn.go b/conn.go index 477de8524..3cb226ad2 100644 --- a/conn.go +++ b/conn.go @@ -91,6 +91,8 @@ type Conn struct { reading chan struct{} handshakeRecv chan recvHandshakeState + inboundPacketInject chan addrPkt + pendingRead chan readResult // in-flight nextConn read, owned by the read loop cancelHandshaker func() cancelHandshakeReader func() @@ -98,6 +100,11 @@ type Conn struct { replayProtectionWindow uint + // Allows intercepting and rerouting outgoing handshake packets. + outboundHandshakePacketInterceptor func(packet []byte, end bool) bool + // Allows getting notified about incoming handshake packets. + inboundHandshakePacketNotifier func(packet []byte) + handshakeConfig *handshakeConfig } @@ -252,12 +259,16 @@ func createConn( reading: make(chan struct{}, 1), handshakeRecv: make(chan recvHandshakeState), + inboundPacketInject: make(chan addrPkt), closed: closer.NewCloser(), cancelHandshaker: func() {}, cancelHandshakeReader: func() {}, replayProtectionWindow: uint(replayProtectionWindow), //nolint:gosec // G115 + outboundHandshakePacketInterceptor: config.outboundHandshakePacketInterceptor, + inboundHandshakePacketNotifier: config.inboundHandshakePacketNotifier, + state: State{ isClient: isClient, remoteSequenceNumber: &atomic.Uint64{}, @@ -572,12 +583,43 @@ func (c *Conn) writePackets(ctx context.Context, pkts []*packet) error { return nil } +// writeHandshakePackets writes DTLS handshake packets. When an outbound +// handshake packet interceptor is set (DTLS-in-STUN / SPED) it is offered each +// packet and may consume it instead of sending it on the wire. +// +//nolint:cyclop +func (c *Conn) writeHandshakePackets(ctx context.Context, pkts []*packet) error { + c.writeLock.Lock() + defer c.writeLock.Unlock() + + compactedRawPackets, rAddr, err := c.prepareRawPackets(pkts) + if err != nil { + return err + } + + for idx, compactedRawPacket := range compactedRawPackets { + if c.outboundHandshakePacketInterceptor != nil { + if c.outboundHandshakePacketInterceptor(compactedRawPacket, idx == len(compactedRawPackets)-1) { + continue + } + } + if _, err = c.nextConn.WriteToContext(ctx, compactedRawPacket, rAddr); err != nil { + if errors.Is(err, context.Canceled) && c.isConnectionClosed() { + return ErrConnClosed + } + + return netError(err) + } + } + + return nil +} + func (c *Conn) prepareRawPackets(pkts []*packet) ([][]byte, net.Addr, error) { c.lock.Lock() defer c.lock.Unlock() var rawPackets [][]byte - for _, pkt := range pkts { pktRawPackets, err := c.prepareRawPacket(pkt) if err != nil { @@ -586,6 +628,7 @@ func (c *Conn) prepareRawPackets(pkts []*packet) ([][]byte, net.Addr, error) { rawPackets = append(rawPackets, pktRawPackets...) } + if len(rawPackets) == 0 { return nil, nil, nil } @@ -904,20 +947,79 @@ var poolReadBuffer = sync.Pool{ //nolint:gochecknoglobals }, } -func (c *Conn) readAndBuffer(ctx context.Context) error { //nolint:cyclop - bufptr, ok := poolReadBuffer.Get().(*[]byte) - if !ok { - return errFailedToAccessPoolReadBuffer +func (c *Conn) InjectInboundPacket(p []byte, rAddr net.Addr) { + select { + case <-c.closed.Done(): + case c.inboundPacketInject <- addrPkt{rAddr, p}: + } +} + +type readResult struct { + data []byte + rAddr net.Addr + err error +} + +func (c *Conn) nextPacket(ctx context.Context) ([]byte, net.Addr, error) { + // An injected packet can win the select below. Keep the network read in + // flight for the next call instead of abandoning it, or whatever it reads + // next is dropped. + if c.pendingRead == nil { + c.pendingRead = c.startRead(ctx) } - defer poolReadBuffer.Put(bufptr) - b := *bufptr - i, rAddr, err := c.nextConn.ReadFromContext(ctx, b) + select { + case p := <-c.inboundPacketInject: + return p.data, p.rAddr, nil + case p := <-c.pendingRead: + c.pendingRead = nil + + return p.data, p.rAddr, p.err + case <-ctx.Done(): + return nil, nil, ctx.Err() + } +} + +func (c *Conn) startRead(ctx context.Context) chan readResult { + readCh := make(chan readResult, 1) + + go func() { + bufptr, ok := poolReadBuffer.Get().(*[]byte) + if !ok { + readCh <- readResult{err: errFailedToAccessPoolReadBuffer} + + return + } + b := *bufptr + + i, rAddr, err := c.nextConn.ReadFromContext(ctx, b) + if err != nil { + readCh <- readResult{err: err} + poolReadBuffer.Put(bufptr) + + return + } + + data := make([]byte, i) + copy(data, b[:i]) + poolReadBuffer.Put(bufptr) + + readCh <- readResult{ + data: data, + rAddr: rAddr, + } + }() + + return readCh +} + +func (c *Conn) readAndBuffer(ctx context.Context) error { //nolint:cyclop + data, rAddr, err := c.nextPacket(ctx) if err != nil { return netError(err) } - pkts, err := recordlayer.ContentAwareUnpackDatagram(b[:i], len(c.state.getLocalConnectionID())) + pkts, err := recordlayer.ContentAwareUnpackDatagram(data, len(c.state.getLocalConnectionID())) if err != nil { return err } @@ -949,6 +1051,11 @@ func (c *Conn) readAndBuffer(ctx context.Context) error { //nolint:cyclop } } if hasHandshake { + if c.inboundHandshakePacketNotifier != nil { + // Would it be useful to know this was injected? + // Should this work on a copy? + c.inboundHandshakePacketNotifier(data) + } s := recvHandshakeState{ done: make(chan struct{}), isRetransmit: isRetransmit, @@ -957,7 +1064,7 @@ func (c *Conn) readAndBuffer(ctx context.Context) error { //nolint:cyclop select { case c.handshakeRecv <- s: // If the other party may retransmit the flight, - // we should respond even if it not a new message. + // we should respond even if it is not a new message. <-s.done case <-c.fsm.Done(): } diff --git a/conn_test.go b/conn_test.go index c70f53fdc..5b189ff4e 100644 --- a/conn_test.go +++ b/conn_test.go @@ -196,7 +196,7 @@ func TestSequenceNumberOverflow(t *testing.T) { atomic.StoreUint64(&ca.state.localSequenceNumber[0], recordlayer.MaxSequenceNumber+1) // Try to send handshake packet. - werr := ca.writePackets(ctx, []*packet{ + werr := ca.writeHandshakePackets(ctx, []*packet{ { record: &recordlayer.RecordLayer{ Header: recordlayer.Header{ @@ -3724,6 +3724,165 @@ func TestCloseWithoutHandshake(t *testing.T) { assert.NoError(t, server.Close()) } +func TestOutboundInterceptor(t *testing.T) { + defer test.CheckRoutines(t)() + defer test.TimeOut(time.Second * 10).Stop() + + ca, cb := dpipe.Pipe() + serverCert, err := selfsign.GenerateSelfSigned() + assert.NoError(t, err) + + var client *Conn + server, err := Server(dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), &Config{ + Certificates: []tls.Certificate{serverCert}, + outboundHandshakePacketInterceptor: func(packet []byte, end bool) bool { + client.InjectInboundPacket(packet, ca.RemoteAddr()) + + return true + }, + InsecureSkipVerify: true, + InsecureSkipVerifyHello: true, + }) + assert.NoError(t, err) + + go func() { + _ = server.Handshake() + }() + + clientCert, err := selfsign.GenerateSelfSigned() + assert.NoError(t, err) + + client, err = Client(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), &Config{ + Certificates: []tls.Certificate{clientCert}, + outboundHandshakePacketInterceptor: func(packet []byte, end bool) bool { + server.InjectInboundPacket(packet, cb.RemoteAddr()) + + return true + }, + InsecureSkipVerify: true, + InsecureSkipVerifyHello: true, + }) + assert.NoError(t, err) + + assert.NoError(t, client.Handshake()) + assert.NoError(t, server.Close()) + assert.NoError(t, client.Close()) +} + +func TestOutboundInterceptorSmallMtuFlush(t *testing.T) { + defer test.CheckRoutines(t)() + defer test.TimeOut(time.Second * 10).Stop() + + ca, cb := dpipe.Pipe() + serverCert, err := selfsign.GenerateSelfSigned() + assert.NoError(t, err) + + var client *Conn + serverPackets, serverFlights := 0, 0 + server, err := Server(dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), &Config{ + Certificates: []tls.Certificate{serverCert}, + outboundHandshakePacketInterceptor: func(packet []byte, end bool) bool { + serverPackets++ + if end { + serverFlights++ + } + client.InjectInboundPacket(packet, ca.RemoteAddr()) + + return true + }, + InsecureSkipVerify: true, + InsecureSkipVerifyHello: true, + MTU: 400, + }) + assert.NoError(t, err) + + go func() { + _ = server.Handshake() + }() + + clientCert, err := selfsign.GenerateSelfSigned() + assert.NoError(t, err) + + clientPackets, clientFlights := 0, 0 + client, err = Client(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), &Config{ + Certificates: []tls.Certificate{clientCert}, + outboundHandshakePacketInterceptor: func(packet []byte, end bool) bool { + clientPackets++ + if end { + clientFlights++ + } + server.InjectInboundPacket(packet, cb.RemoteAddr()) + + return true + }, + InsecureSkipVerify: true, + InsecureSkipVerifyHello: true, + MTU: 500, + }) + assert.NoError(t, err) + + assert.NoError(t, client.Handshake()) + assert.NoError(t, server.Close()) + assert.NoError(t, client.Close()) + assert.Equal(t, 2, clientPackets) + assert.Equal(t, 2, clientFlights) + assert.Equal(t, 4, serverPackets) + assert.Equal(t, 2, serverFlights) +} + +func TestInboundNotifier(t *testing.T) { + defer test.CheckRoutines(t)() + defer test.TimeOut(time.Second * 10).Stop() + + ca, cb := dpipe.Pipe() + serverCert, err := selfsign.GenerateSelfSigned() + assert.NoError(t, err) + + var inboundHandshakePackets [][]byte + server, err := Server(dtlsnet.PacketConnFromConn(cb), cb.RemoteAddr(), &Config{ + Certificates: []tls.Certificate{serverCert}, + inboundHandshakePacketNotifier: func(packet []byte) { + data := make([]byte, len(packet)) + copy(data, packet) + inboundHandshakePackets = append(inboundHandshakePackets, data) + }, + InsecureSkipVerify: true, + InsecureSkipVerifyHello: true, + }) + assert.NoError(t, err) + + go func() { + _ = server.Handshake() + }() + + clientCert, err := selfsign.GenerateSelfSigned() + assert.NoError(t, err) + + var outboundHandshakePackets [][]byte + client, err := Client(dtlsnet.PacketConnFromConn(ca), ca.RemoteAddr(), &Config{ + Certificates: []tls.Certificate{clientCert}, + outboundHandshakePacketInterceptor: func(packet []byte, end bool) bool { + data := make([]byte, len(packet)) + copy(data, packet) + outboundHandshakePackets = append(outboundHandshakePackets, data) + + return false + }, + InsecureSkipVerify: true, + InsecureSkipVerifyHello: true, + }) + assert.NoError(t, err) + + assert.NoError(t, client.Handshake()) + assert.NoError(t, server.Close()) + assert.NoError(t, client.Close()) + assert.Equal(t, len(inboundHandshakePackets), len(outboundHandshakePackets)) + + for i := range inboundHandshakePackets { + assert.Equal(t, inboundHandshakePackets[i], outboundHandshakePackets[i]) + } +} + // Records queued for a later epoch must not share memory with the pooled // read buffer, which the next readAndBuffer call reuses. func TestEnqueueEncryptedPacketsCopiesData(t *testing.T) { diff --git a/errors.go b/errors.go index badfc4d58..c2ad7b739 100644 --- a/errors.go +++ b/errors.go @@ -214,6 +214,14 @@ var ( Err: errors.New("client hello message hook option requires a non-nil function"), } //nolint:err113 + errNilOutboundHandshakePacketInterceptor = &FatalError{ + Err: errors.New("outbound handshake packet interceptor option requires a non-nil function"), + } + //nolint:err113 + errNilInboundHandshakePacketNotifier = &FatalError{ + Err: errors.New("inbound handshake packet notifier 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{ diff --git a/flight1handler_test.go b/flight1handler_test.go index 9e96eb9e0..d1c595126 100644 --- a/flight1handler_test.go +++ b/flight1handler_test.go @@ -21,11 +21,14 @@ type flight1TestMockFlightConn struct{} func (f *flight1TestMockFlightConn) notify(context.Context, alert.Level, alert.Description) error { return nil } -func (f *flight1TestMockFlightConn) writePackets(context.Context, []*packet) error { return nil } -func (f *flight1TestMockFlightConn) recvHandshake() <-chan recvHandshakeState { return nil } -func (f *flight1TestMockFlightConn) setLocalEpoch(uint16) {} -func (f *flight1TestMockFlightConn) handleQueuedPackets(context.Context) error { return nil } -func (f *flight1TestMockFlightConn) sessionKey() []byte { return nil } + +func (f *flight1TestMockFlightConn) writeHandshakePackets(context.Context, []*packet) error { + return nil +} +func (f *flight1TestMockFlightConn) recvHandshake() <-chan recvHandshakeState { return nil } +func (f *flight1TestMockFlightConn) setLocalEpoch(uint16) {} +func (f *flight1TestMockFlightConn) handleQueuedPackets(context.Context) error { return nil } +func (f *flight1TestMockFlightConn) sessionKey() []byte { return nil } type flight1TestMockCipherSuite struct { ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256 diff --git a/flight4handler_test.go b/flight4handler_test.go index 6d75ca091..435aa9a62 100644 --- a/flight4handler_test.go +++ b/flight4handler_test.go @@ -32,11 +32,14 @@ type flight4TestMockFlightConn struct{} func (f *flight4TestMockFlightConn) notify(context.Context, alert.Level, alert.Description) error { return nil } -func (f *flight4TestMockFlightConn) writePackets(context.Context, []*packet) error { return nil } -func (f *flight4TestMockFlightConn) recvHandshake() <-chan recvHandshakeState { return nil } -func (f *flight4TestMockFlightConn) setLocalEpoch(uint16) {} -func (f *flight4TestMockFlightConn) handleQueuedPackets(context.Context) error { return nil } -func (f *flight4TestMockFlightConn) sessionKey() []byte { return nil } + +func (f *flight4TestMockFlightConn) writeHandshakePackets(context.Context, []*packet) error { + return nil +} +func (f *flight4TestMockFlightConn) recvHandshake() <-chan recvHandshakeState { return nil } +func (f *flight4TestMockFlightConn) setLocalEpoch(uint16) {} +func (f *flight4TestMockFlightConn) handleQueuedPackets(context.Context) error { return nil } +func (f *flight4TestMockFlightConn) sessionKey() []byte { return nil } type flight4TestMockCipherSuite struct { ciphersuite.TLSEcdheEcdsaWithAes128GcmSha256 diff --git a/handshaker.go b/handshaker.go index 86dfc9d06..409d60ead 100644 --- a/handshaker.go +++ b/handshaker.go @@ -146,7 +146,7 @@ type handshakeConfig struct { type flightConn interface { notify(ctx context.Context, level alert.Level, desc alert.Description) error - writePackets(context.Context, []*packet) error + writeHandshakePackets(context.Context, []*packet) error recvHandshake() <-chan recvHandshakeState setLocalEpoch(epoch uint16) handleQueuedPackets(context.Context) error @@ -272,7 +272,7 @@ func (s *handshakeFSM) prepare(ctx context.Context, conn flightConn) (handshakeS func (s *handshakeFSM) send(ctx context.Context, c flightConn) (handshakeState, error) { // Send flights - if err := c.writePackets(ctx, s.flights); err != nil { + if err := c.writeHandshakePackets(ctx, s.flights); err != nil { return handshakeErrored, err } diff --git a/handshaker_test.go b/handshaker_test.go index f24b01551..09e9c05c3 100644 --- a/handshaker_test.go +++ b/handshaker_test.go @@ -462,7 +462,7 @@ func (c *flightTestConn) notify(context.Context, alert.Level, alert.Description) return nil } -func (c *flightTestConn) writePackets(_ context.Context, pkts []*packet) error { //nolint:cyclop +func (c *flightTestConn) writeHandshakePackets(_ context.Context, pkts []*packet) error { //nolint:cyclop time.Sleep(c.delay) isRetransmit := false isClientHello := false diff --git a/options.go b/options.go index d06926a84..9db9c6623 100644 --- a/options.go +++ b/options.go @@ -43,46 +43,48 @@ func defensiveCopy[T any](t ...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 - listenConfig net.ListenConfig - minVersion protocol.Version - maxVersion protocol.Version + 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 + listenConfig net.ListenConfig + outboundHandshakePacketInterceptor func(packet []byte, end bool) bool + inboundHandshakePacketNotifier func(packet []byte) + minVersion protocol.Version + maxVersion protocol.Version } // applyDefaults applies default values to the config. @@ -98,37 +100,39 @@ func (c *dtlsConfig) applyDefaults() { // 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, - listenConfig: c.listenConfig, - minVersion: c.minVersion, - maxVersion: c.maxVersion, + 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, + listenConfig: c.listenConfig, + outboundHandshakePacketInterceptor: c.outboundHandshakePacketInterceptor, + inboundHandshakePacketNotifier: c.inboundHandshakePacketNotifier, + minVersion: c.minVersion, + maxVersion: c.maxVersion, } if len(c.certificates) > 0 { @@ -594,6 +598,32 @@ func withMaxVersion(version protocol.Version) Option { // nolint:unused }) } +// WithOutboundHandshakePacketInterceptor allows installing an outbound handshake +// packet interceptor. +func WithOutboundHandshakePacketInterceptor(fn func(packet []byte, end bool) bool) Option { + return sharedOption(func(c *dtlsConfig) error { + if fn == nil { + return errNilOutboundHandshakePacketInterceptor + } + c.outboundHandshakePacketInterceptor = fn + + return nil + }) +} + +// WithInboundHandshakePacketNotifier allows installing an inbound handshake +// packet notifier. +func WithInboundHandshakePacketNotifier(fn func(packet []byte)) Option { + return sharedOption(func(c *dtlsConfig) error { + if fn == nil { + return errNilInboundHandshakePacketNotifier + } + c.inboundHandshakePacketNotifier = fn + + return nil + }) +} + // serverOnlyOption wraps an apply function for server-only options. type serverOnlyOption func(*dtlsConfig) error diff --git a/options_test.go b/options_test.go index f7613ca2b..64d589a6f 100644 --- a/options_test.go +++ b/options_test.go @@ -195,6 +195,22 @@ func TestNilCallbackOptionsReturnError(t *testing.T) { _, err = buildServerConfig(WithClientHelloMessageHook(nil)) require.ErrorIs(t, err, errNilClientHelloMessageHook) }) + + t.Run("NilOutboundHandshakePacketInterceptor", func(t *testing.T) { + _, err := buildClientConfig(WithOutboundHandshakePacketInterceptor(nil)) + require.ErrorIs(t, err, errNilOutboundHandshakePacketInterceptor) + + _, err = buildServerConfig(WithOutboundHandshakePacketInterceptor(nil)) + require.ErrorIs(t, err, errNilOutboundHandshakePacketInterceptor) + }) + + t.Run("NilInboundHandshakePacketNotifier", func(t *testing.T) { + _, err := buildClientConfig(WithInboundHandshakePacketNotifier(nil)) + require.ErrorIs(t, err, errNilInboundHandshakePacketNotifier) + + _, err = buildServerConfig(WithInboundHandshakePacketNotifier(nil)) + require.ErrorIs(t, err, errNilInboundHandshakePacketNotifier) + }) } // TestServerOnlyNilCallbackOptionsReturnError verifies server-only options