Skip to content

Commit 079c015

Browse files
committed
Implement RFC 9653 and add fuzz tests
1 parent 803e488 commit 079c015

11 files changed

+464
-24
lines changed

association.go

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -718,24 +718,16 @@ func (a *Association) unregisterStream(s *Stream, err error) {
718718
s.readNotifier.Broadcast()
719719
}
720720

721-
func chunkMandatoryChecksum(cc []chunk) bool {
722-
for _, c := range cc {
723-
switch c.(type) {
724-
case *chunkInit, *chunkCookieEcho:
725-
return true
726-
}
727-
}
728-
729-
return false
730-
}
731-
732721
func (a *Association) marshalPacket(p *packet) ([]byte, error) {
733-
return p.marshal(!a.sendZeroChecksum || chunkMandatoryChecksum(p.chunks))
722+
// RFC 9653: pass whether zero checksum is allowed on this path.
723+
// packet.marshal() will still force a correct CRC for INIT/COOKIE ECHO.
724+
return p.marshal(a.sendZeroChecksum)
734725
}
735726

736727
func (a *Association) unmarshalPacket(raw []byte) (*packet, error) {
737728
p := &packet{}
738-
if err := p.unmarshal(!a.recvZeroChecksum, raw); err != nil {
729+
// In packet.unmarshal: doChecksum==true => accept ZERO checksum.
730+
if err := p.unmarshal(a.recvZeroChecksum, raw); err != nil {
739731
return nil, err
740732
}
741733

@@ -1273,7 +1265,8 @@ func (a *Association) handleInit(pkt *packet, initChunk *chunkInit) ([]*packet,
12731265
}
12741266
}
12751267
case *paramZeroChecksumAcceptable:
1276-
a.sendZeroChecksum = v.edmid == dtlsErrorDetectionMethod
1268+
// Only send zero if we allow ZCA and the peer accepts it.
1269+
a.sendZeroChecksum = a.recvZeroChecksum && (v.edmid == dtlsErrorDetectionMethod)
12771270
}
12781271
}
12791272

@@ -1359,18 +1352,20 @@ func (a *Association) handleInitAck(pkt *packet, initChunkAck *chunkInitAck) err
13591352

13601353
var cookieParam *paramStateCookie
13611354
for _, param := range initChunkAck.params {
1362-
switch v := param.(type) {
1355+
switch paramType := param.(type) {
13631356
case *paramStateCookie:
1364-
cookieParam = v
1357+
cookieParam = paramType
13651358
case *paramSupportedExtensions:
1366-
for _, t := range v.ChunkTypes {
1359+
for _, t := range paramType.ChunkTypes {
13671360
if t == ctForwardTSN {
13681361
a.log.Debugf("[%s] use ForwardTSN (on initAck)", a.name)
13691362
a.useForwardTSN = true
13701363
}
13711364
}
13721365
case *paramZeroChecksumAcceptable:
1373-
a.sendZeroChecksum = v.edmid == dtlsErrorDetectionMethod
1366+
// Client: if the server advertised ZCA, we may send zero checksums.
1367+
// (Cookie Echo & INIT are still forced to real CRC in packet.marshal)
1368+
a.sendZeroChecksum = (paramType.edmid == dtlsErrorDetectionMethod)
13741369
}
13751370
}
13761371

association_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3034,7 +3034,7 @@ func TestAssociationFastRtxWnd(t *testing.T) {
30343034
for i := 11; i < 14; i++ {
30353035
ack.gapAckBlocks[0].end = uint16(i) //nolint:gosec // G115
30363036
pkt := a1.createPacket([]chunk{&ack})
3037-
pktBuf, err1 := pkt.marshal(true)
3037+
pktBuf, err1 := pkt.marshal(false)
30383038
require.NoError(t, err1)
30393039
dbConn1.inboundHandler(pktBuf)
30403040
}
@@ -3065,7 +3065,7 @@ func TestAssociationFastRtxWnd(t *testing.T) {
30653065
//nolint:gosec // G115
30663066
ack.gapAckBlocks = append(ack.gapAckBlocks, gapAckBlock{start: uint16(end), end: uint16(end)})
30673067
pkt := a1.createPacket([]chunk{&ack})
3068-
pktBuf, err := pkt.marshal(true)
3068+
pktBuf, err := pkt.marshal(false)
30693069
require.NoError(t, err)
30703070
dbConn1.inboundHandler(pktBuf)
30713071
require.Eventually(t, func() bool {
@@ -3104,7 +3104,7 @@ func TestAssociationMaxTSNOffset(t *testing.T) {
31043104
chunk.tsn = tsn
31053105
pp := a1.bundleDataChunksIntoPackets(chunks)
31063106
for _, p := range pp {
3107-
raw, err := p.marshal(true)
3107+
raw, err := p.marshal(false)
31083108
assert.NoError(t, err)
31093109

31103110
_, 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 {
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)