Skip to content

Commit 66e7036

Browse files
committed
Implement RFC 9653 and add fuzz tests
1 parent 4d42391 commit 66e7036

12 files changed

+548
-28
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
- [RFC 5061](https://www.rfc-editor.org/rfc/rfc5061.html) — Stream Control Transmission Protocol (SCTP) Dynamic Address Reconfiguration
2323
- [RFC 4895](https://www.rfc-editor.org/rfc/rfc4895.html) — Authenticated Chunks for the Stream Control Transmission Protocol (SCTP)
2424
- [RFC 1982](https://www.rfc-editor.org/rfc/rfc1982.html) — Serial Number Arithmetic
25+
- [RFC 9653](https://www.rfc-editor.org/rfc/rfc9653.html) — Zero Checksum for the Stream Control Transmission Protocol
2526

2627
### Partial implementations
2728
Pion only implements the subset of RFC 4960 that is required for WebRTC.

association.go

Lines changed: 17 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -269,10 +269,11 @@ type Config struct {
269269
NetConn net.Conn
270270
MaxReceiveBufferSize uint32
271271
MaxMessageSize uint32
272-
EnableZeroChecksum bool
273-
LoggerFactory logging.LoggerFactory
274-
BlockWrite bool
275-
MTU uint32
272+
// EnableZeroChecksum enables RFC 9653 and should only be used when SCTP is carried over DTLS.
273+
EnableZeroChecksum bool
274+
LoggerFactory logging.LoggerFactory
275+
BlockWrite bool
276+
MTU uint32
276277

277278
// congestion control configuration
278279
// RTOMax is the maximum retransmission timeout in milliseconds
@@ -724,24 +725,16 @@ func (a *Association) unregisterStream(s *Stream, err error) {
724725
s.readNotifier.Broadcast()
725726
}
726727

727-
func chunkMandatoryChecksum(cc []chunk) bool {
728-
for _, c := range cc {
729-
switch c.(type) {
730-
case *chunkInit, *chunkCookieEcho:
731-
return true
732-
}
733-
}
734-
735-
return false
736-
}
737-
738728
func (a *Association) marshalPacket(p *packet) ([]byte, error) {
739-
return p.marshal(!a.sendZeroChecksum || chunkMandatoryChecksum(p.chunks))
729+
// RFC 9653: pass whether zero checksum is allowed on this path.
730+
// packet.marshal() will still force a correct CRC for INIT/COOKIE ECHO.
731+
return p.marshal(a.sendZeroChecksum)
740732
}
741733

742734
func (a *Association) unmarshalPacket(raw []byte) (*packet, error) {
743735
p := &packet{}
744-
if err := p.unmarshal(!a.recvZeroChecksum, raw); err != nil {
736+
// Unmarshal with ZCA mode: if a.recvZeroChecksum == true, accept zero checksums per RFC 9653.
737+
if err := p.unmarshal(a.recvZeroChecksum, raw); err != nil {
745738
return nil, err
746739
}
747740

@@ -1279,7 +1272,8 @@ func (a *Association) handleInit(pkt *packet, initChunk *chunkInit) ([]*packet,
12791272
}
12801273
}
12811274
case *paramZeroChecksumAcceptable:
1282-
a.sendZeroChecksum = v.edmid == dtlsErrorDetectionMethod
1275+
// Only send zero if we allow ZCA and the peer accepts it.
1276+
a.sendZeroChecksum = a.recvZeroChecksum && (v.edmid == dtlsErrorDetectionMethod)
12831277
}
12841278
}
12851279

@@ -1365,18 +1359,19 @@ func (a *Association) handleInitAck(pkt *packet, initChunkAck *chunkInitAck) err
13651359

13661360
var cookieParam *paramStateCookie
13671361
for _, param := range initChunkAck.params {
1368-
switch v := param.(type) {
1362+
switch paramType := param.(type) {
13691363
case *paramStateCookie:
1370-
cookieParam = v
1364+
cookieParam = paramType
13711365
case *paramSupportedExtensions:
1372-
for _, t := range v.ChunkTypes {
1366+
for _, t := range paramType.ChunkTypes {
13731367
if t == ctForwardTSN {
13741368
a.log.Debugf("[%s] use ForwardTSN (on initAck)", a.name)
13751369
a.useForwardTSN = true
13761370
}
13771371
}
13781372
case *paramZeroChecksumAcceptable:
1379-
a.sendZeroChecksum = v.edmid == dtlsErrorDetectionMethod
1373+
// Only send zero if we allow ZCA and the peer accepts it.
1374+
a.sendZeroChecksum = a.recvZeroChecksum && (paramType.edmid == dtlsErrorDetectionMethod)
13801375
}
13811376
}
13821377

association_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3188,7 +3188,7 @@ func TestAssociationFastRtxWnd(t *testing.T) {
31883188
for i := 11; i < 14; i++ {
31893189
ack.gapAckBlocks[0].end = uint16(i) //nolint:gosec // G115
31903190
pkt := a1.createPacket([]chunk{&ack})
3191-
pktBuf, err1 := pkt.marshal(true)
3191+
pktBuf, err1 := pkt.marshal(false)
31923192
require.NoError(t, err1)
31933193
dbConn1.inboundHandler(pktBuf)
31943194
}
@@ -3219,7 +3219,7 @@ func TestAssociationFastRtxWnd(t *testing.T) {
32193219
//nolint:gosec // G115
32203220
ack.gapAckBlocks = append(ack.gapAckBlocks, gapAckBlock{start: uint16(end), end: uint16(end)})
32213221
pkt := a1.createPacket([]chunk{&ack})
3222-
pktBuf, err := pkt.marshal(true)
3222+
pktBuf, err := pkt.marshal(false)
32233223
require.NoError(t, err)
32243224
dbConn1.inboundHandler(pktBuf)
32253225
require.Eventually(t, func() bool {
@@ -3258,7 +3258,7 @@ func TestAssociationMaxTSNOffset(t *testing.T) {
32583258
chunk.tsn = tsn
32593259
pp := a1.bundleDataChunksIntoPackets(chunks)
32603260
for _, p := range pp {
3261-
raw, err := p.marshal(true)
3261+
raw, err := p.marshal(false)
32623262
assert.NoError(t, err)
32633263

32643264
_, err = a1.netConn.Write(raw)

chunk_cookie_echo.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,13 +38,17 @@ func (c *chunkCookieEcho) unmarshal(raw []byte) error {
3838
if c.typ != ctCookieEcho {
3939
return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotCookieEcho, c.typ.String())
4040
}
41+
42+
// RFC 9260: flags are reserved; sender sets 0, receiver ignores.
43+
// Do not fail if flags != 0.
4144
c.cookie = c.raw
4245

4346
return nil
4447
}
4548

4649
func (c *chunkCookieEcho) marshal() ([]byte, error) {
4750
c.chunkHeader.typ = ctCookieEcho
51+
c.chunkHeader.flags = 0 // sender sets 0 (reserved)
4852
c.chunkHeader.raw = c.cookie
4953

5054
return c.chunkHeader.marshal()

chunk_cookie_echo_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
2+
// SPDX-License-Identifier: MIT
3+
4+
package sctp
5+
6+
import (
7+
"encoding/binary"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func TestRFC9653_SenderRule_CookieEchoForcesCRC(t *testing.T) {
14+
p := &packet{
15+
sourcePort: 7000,
16+
destinationPort: 7001,
17+
verificationTag: 0x01020304,
18+
chunks: []chunk{
19+
&chunkCookieEcho{cookie: []byte{0xDE, 0xAD, 0xBE, 0xEF}},
20+
},
21+
}
22+
23+
// ZCA enabled: even so, COOKIE ECHO requires a correct CRC32c (non-zero).
24+
raw, err := p.marshal(true)
25+
assert.NoError(t, err)
26+
27+
got := binary.LittleEndian.Uint32(raw[8:])
28+
assert.NotZero(t, got, "checksum must not be zero when COOKIE ECHO present")
29+
30+
// Verify correctness of the CRC
31+
cp := make([]byte, len(raw))
32+
copy(cp, raw)
33+
binary.LittleEndian.PutUint32(cp[8:], 0)
34+
want := generatePacketChecksum(cp)
35+
assert.Equal(t, want, got, "checksum must be the correct CRC32c")
36+
}

chunk_init.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package sctp // nolint:dupl
55

66
import (
7+
"encoding/binary"
78
"errors"
89
"fmt"
910
)
@@ -21,10 +22,14 @@ See chunkInitCommon for the fixed headers
2122
Reserved for ECN Capable (Note 2) Optional 32768 (0x8000)
2223
Host Name IP (Note 3) Optional 11
2324
Supported IP Types (Note 4) Optional 12
25+
Zero Checksum Acceptable (RFC 9653) Optional 32769 (0x8001)
2426
*/
2527
type chunkInit struct {
2628
chunkHeader
2729
chunkInitCommon
30+
// RFC 9653 Zero Checksum Acceptable negotiation
31+
zcaPresent bool
32+
zcaEDMID uint32
2833
}
2934

3035
// Init chunk errors.
@@ -63,6 +68,13 @@ func (i *chunkInit) unmarshal(raw []byte) error {
6368
return fmt.Errorf("%w: %v", ErrChunkTypeInitUnmarshalFailed, err) //nolint:errorlint
6469
}
6570

71+
// Parse RFC 9653 ZCA TLV (optional, at most once). Parameters start
72+
// immediately after the fixed INIT common body.
73+
if len(i.raw) >= initChunkMinLength {
74+
present, edmid := scanZCAParamBytes(i.raw)
75+
i.zcaPresent, i.zcaEDMID = present, edmid
76+
}
77+
6678
return nil
6779
}
6880

@@ -72,6 +84,13 @@ func (i *chunkInit) marshal() ([]byte, error) {
7284
return nil, fmt.Errorf("%w: %v", ErrChunkTypeInitMarshalFailed, err) //nolint:errorlint
7385
}
7486

87+
// Optionally append ZCA (avoid duplicates if chunkInitCommon already added one).
88+
if i.zcaPresent {
89+
if ok, _ := scanZCAParamBytes(initShared); !ok {
90+
initShared = appendZCAParam(initShared, i.zcaEDMID)
91+
}
92+
}
93+
7594
i.chunkHeader.typ = ctInit
7695
i.chunkHeader.raw = initShared
7796

@@ -141,3 +160,56 @@ func (i *chunkInit) check() (abort bool, err error) {
141160
func (i *chunkInit) String() string {
142161
return fmt.Sprintf("%s\n%s", i.chunkHeader, i.chunkInitCommon)
143162
}
163+
164+
// ZeroChecksumEDMID returns (EDMID, present) negotiated via RFC 9653 ZCA.
165+
func (i *chunkInit) ZeroChecksumEDMID() (uint32, bool) {
166+
return i.zcaEDMID, i.zcaPresent
167+
}
168+
169+
// scanZCAParamBytes scans a parameter list (in the INIT/INIT-ACK value bytes)
170+
// for a single ZCA TLV and returns (isPresent, edmid, isDup).
171+
func scanZCAParamBytes(b []byte) (bool, uint32) {
172+
const typZCA = uint16(zeroChecksumAcceptable)
173+
174+
off := initFixedValueLen
175+
for {
176+
if off+4 > len(b) { // not enough for a param header
177+
return false, 0
178+
}
179+
180+
typ := binary.BigEndian.Uint16(b[off : off+2])
181+
length := int(binary.BigEndian.Uint16(b[off+2 : off+4]))
182+
183+
if length < 4 || off+length > len(b) { // malformed/truncated
184+
return false, 0
185+
}
186+
187+
if typ == typZCA && length == 8 {
188+
edmid := binary.BigEndian.Uint32(b[off+4 : off+8])
189+
190+
return true, edmid
191+
}
192+
193+
nxt := off + length
194+
195+
if rem := length & 3; rem != 0 { // 4-byte padding
196+
nxt += 4 - rem
197+
}
198+
199+
if nxt <= off || nxt > len(b) {
200+
return false, 0
201+
}
202+
203+
off = nxt
204+
}
205+
}
206+
207+
// appendZCAParam appends a single ZCA TLV.
208+
func appendZCAParam(dst []byte, edmid uint32) []byte {
209+
var tlv [8]byte
210+
binary.BigEndian.PutUint16(tlv[0:2], uint16(zeroChecksumAcceptable))
211+
binary.BigEndian.PutUint16(tlv[2:4], 8)
212+
binary.BigEndian.PutUint32(tlv[4:8], edmid)
213+
214+
return append(dst, tlv[:]...)
215+
}

chunk_init_ack.go

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,11 +20,15 @@ See chunkInitCommon for the fixed headers
2020
IPv6 IP (Note 1) Optional 6
2121
Unrecognized Parameter Optional 8
2222
Reserved for ECN Capable (Note 2) Optional 32768 (0x8000)
23-
Host Name IP (Note 3) Optional 11<Paste>
23+
Host Name IP (Note 3) Optional 11
24+
Zero Checksum Acceptable (RFC 9653) Optional 32769 (0x8001)
2425
*/
2526
type chunkInitAck struct {
2627
chunkHeader
2728
chunkInitCommon
29+
// RFC 9653 Zero Checksum Acceptable negotiation
30+
zcaPresent bool
31+
zcaEDMID uint32
2832
}
2933

3034
// Init ack chunk errors.
@@ -62,6 +66,13 @@ func (i *chunkInitAck) unmarshal(raw []byte) error {
6266
return fmt.Errorf("%w: %v", ErrInitAckUnmarshalFailed, err) //nolint:errorlint
6367
}
6468

69+
// Parse RFC 9653 ZCA TLV (optional, at most once). Parameters start
70+
// immediately after the fixed INIT-ACK common body.
71+
if len(i.raw) >= initChunkMinLength {
72+
present, edmid := scanZCAParamBytes(i.raw)
73+
i.zcaPresent, i.zcaEDMID = present, edmid
74+
}
75+
6576
return nil
6677
}
6778

@@ -71,6 +82,13 @@ func (i *chunkInitAck) marshal() ([]byte, error) {
7182
return nil, fmt.Errorf("%w: %v", ErrInitCommonDataMarshalFailed, err) //nolint:errorlint
7283
}
7384

85+
// Optionally append ZCA (avoid duplicates).
86+
if i.zcaPresent {
87+
if ok, _ := scanZCAParamBytes(initShared); !ok {
88+
initShared = appendZCAParam(initShared, i.zcaEDMID)
89+
}
90+
}
91+
7492
i.chunkHeader.typ = ctInitAck
7593
i.chunkHeader.raw = initShared
7694

@@ -143,3 +161,7 @@ func (i *chunkInitAck) check() (abort bool, err error) {
143161
func (i *chunkInitAck) String() string {
144162
return fmt.Sprintf("%s\n%s", i.chunkHeader, i.chunkInitCommon)
145163
}
164+
165+
func (i *chunkInitAck) ZeroChecksumEDMID() (uint32, bool) {
166+
return i.zcaEDMID, i.zcaPresent
167+
}

chunk_init_common.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,9 @@ type chunkInitCommon struct {
5555
const (
5656
initChunkMinLength = 16
5757
initOptionalVarHeaderLength = 4
58+
59+
// RFC 9260 section 3.3.2 & section 3.3.3: INIT/INIT-ACK have a 16-byte fixed value before params.
60+
initFixedValueLen = 16
5861
)
5962

6063
// Init chunk errors.

0 commit comments

Comments
 (0)