Skip to content

Commit 4d51282

Browse files
committed
Update packet to RFC 9260 (after reverts)
This reverts commit 096446a.
1 parent 4d42391 commit 4d51282

File tree

3 files changed

+165
-28
lines changed

3 files changed

+165
-28
lines changed

chunk_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,13 +104,29 @@ func TestChromeChunk2InitAck(t *testing.T) {
104104
0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00, 0xca, 0x0c, 0x21, 0x11,
105105
0xce, 0xf4, 0xfc, 0xb3, 0x66, 0x99, 0x4f, 0xdb, 0x4f, 0x95, 0x6b, 0x6f, 0x3b, 0xb1, 0xdb, 0x5a,
106106
}
107+
107108
err := pkt.unmarshal(true, rawPkt)
108109
assert.NoError(t, err)
109110

110-
rawPkt2, err := pkt.marshal(true)
111+
// Strict marshal (CRC32c): should reproduce the original bit-for-bit.
112+
rawStrict, err := pkt.marshal(false)
111113
assert.NoError(t, err)
114+
assert.Equal(t, rawPkt, rawStrict)
112115

113-
assert.Equal(t, rawPkt, rawPkt2)
116+
// ZCA marshal (zero checksum allowed, and this packet has INIT-ACK, not restricted)
117+
rawZero, err := pkt.marshal(true)
118+
assert.NoError(t, err)
119+
120+
// Expect the same bytes as original, except checksum field zeroed.
121+
expZero := make([]byte, len(rawPkt))
122+
copy(expZero, rawPkt)
123+
expZero[8], expZero[9], expZero[10], expZero[11] = 0, 0, 0, 0
124+
assert.Equal(t, expZero, rawZero)
125+
126+
// Receiver behavior: zero-checksum packet is rejected in strict mode, accepted in ZCA mode.
127+
pkt2 := &packet{}
128+
assert.Error(t, pkt2.unmarshal(false, rawZero))
129+
assert.NoError(t, pkt2.unmarshal(true, rawZero))
114130
}
115131

116132
func TestInitMarshalUnmarshal(t *testing.T) {

packet.go

Lines changed: 54 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ var castagnoliTable = crc32.MakeTable(crc32.Castagnoli) // nolint:gochecknogloba
1818
var fourZeroes [4]byte // nolint:gochecknoglobals
1919

2020
/*
21-
Packet represents an SCTP packet, defined in https://tools.ietf.org/html/rfc4960#section-3
21+
Packet represents an SCTP packet, defined in https://tools.ietf.org/html/rfc9260#section-3
2222
An SCTP packet is composed of a common header and chunks. A chunk
2323
contains either control information or user data.
2424
@@ -39,7 +39,7 @@ contains either control information or user data.
3939
0 1 2 3
4040
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
4141
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
42-
| Source Value Number | Destination Value Number |
42+
| Source Port Number | Destination Port Number |
4343
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
4444
| Verification Tag |
4545
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
@@ -65,27 +65,39 @@ var (
6565
ErrChecksumMismatch = errors.New("checksum mismatch theirs")
6666
)
6767

68+
// unmarshal parses an SCTP packet and verifies its checksum.
69+
//
70+
// RFC 9260 section 6.8: every packet MUST be protected with CRC32c; receivers verify it.
71+
// RFC 9653 adds an extension (ZCA) that allows a receiver to accept a packet
72+
// whose checksum is incorrect but equal to zero, when an alternate error
73+
// detection method is active for the path (ex: SCTP-over-DTLS) and ZCA has
74+
// been negotiated.
75+
//
76+
// doChecksum indicates whether ZCA is active for the *receiving path*:
77+
// - doChecksum == true : accept a packet if the only checksum issue is that
78+
// it is exactly zero (ZCA + path constraints satisfied).
79+
// - doChecksum == false : strict 9260 verification; any checksum mismatch fails.
80+
//
81+
// Higher layers MUST only pass doChecksum=true if the peer advertised ZCA
82+
// in INIT/INIT-ACK and the current path meets the method’s constraints
83+
// (ex: DTLS). This function does not relax sender-side rules: packets that
84+
// contain INIT or COOKIE ECHO MUST still carry a correct CRC32c (enforced in marshal()).
85+
//
86+
// See RFC 9653 section 5.1 and section 5.3.
6887
func (p *packet) unmarshal(doChecksum bool, raw []byte) error { //nolint:cyclop
6988
if len(raw) < packetHeaderSize {
7089
return fmt.Errorf("%w: raw only %d bytes, %d is the minimum length", ErrPacketRawTooSmall, len(raw), packetHeaderSize)
7190
}
7291

7392
offset := packetHeaderSize
7493

75-
// Check if doing CRC32c is required.
76-
// Without having SCTP AUTH implemented, this depends only on the type
77-
// og the first chunk.
78-
if offset+chunkHeaderSize <= len(raw) {
79-
switch chunkType(raw[offset]) {
80-
case ctInit, ctCookieEcho:
81-
doChecksum = true
82-
default:
83-
}
84-
}
94+
// always compute CRC32c (RFC 9260 section 6.8).
8595
theirChecksum := binary.LittleEndian.Uint32(raw[8:])
86-
if theirChecksum != 0 || doChecksum {
87-
ourChecksum := generatePacketChecksum(raw)
88-
if theirChecksum != ourChecksum {
96+
ourChecksum := generatePacketChecksum(raw)
97+
if theirChecksum != ourChecksum {
98+
// RFC 9653: if (a) checksum is zero and (b) caller indicates ZCA is active
99+
// and method constraints are met, accept; otherwise error.
100+
if !doChecksum || theirChecksum != 0 {
89101
return fmt.Errorf("%w: %d ours: %d", ErrChecksumMismatch, theirChecksum, ourChecksum)
90102
}
91103
}
@@ -148,6 +160,16 @@ func (p *packet) unmarshal(doChecksum bool, raw []byte) error { //nolint:cyclop
148160
return nil
149161
}
150162

163+
// marshal builds an SCTP packet.
164+
//
165+
// If doChecksum == true, a zero checksum is written (RFC 9653) unless
166+
// the packet contains a restricted chunk (INIT or COOKIE ECHO), in which case
167+
// a correct CRC32c is always written (RFC 9653 section 5.2). If doChecksum == false,
168+
// a correct CRC32c is always written (RFC 9260 section 6.8).
169+
//
170+
// The caller sets doChecksum=true only when the peer has advertised ZCA and
171+
// the current path satisfies the alternate method’s constraints (e.g., DTLS).
172+
// For OOTB responses and other control paths, always marshal with doChecksum=false.
151173
func (p *packet) marshal(doChecksum bool) ([]byte, error) {
152174
raw := make([]byte, packetHeaderSize)
153175

@@ -171,19 +193,27 @@ func (p *packet) marshal(doChecksum bool) ([]byte, error) {
171193
}
172194
}
173195

174-
if doChecksum {
175-
// golang CRC32C uses reflected input and reflected output, the
176-
// net result of this is to have the bytes flipped compared to
177-
// the non reflected variant that the spec expects.
178-
//
179-
// Use LittleEndian.PutUint32 to avoid flipping the bytes in to
180-
// the spec compliant checksum order
196+
if doChecksum && !hasRestrictedChunk(p.chunks) {
197+
binary.LittleEndian.PutUint32(raw[8:], 0)
198+
} else {
181199
binary.LittleEndian.PutUint32(raw[8:], generatePacketChecksum(raw))
182200
}
183201

184202
return raw, nil
185203
}
186204

205+
// restrictedChunks per RFC 9653 section 5.2: INIT and COOKIE ECHO.
206+
func hasRestrictedChunk(chs []chunk) bool {
207+
for _, c := range chs {
208+
switch c.(type) {
209+
case *chunkInit, *chunkCookieEcho:
210+
return true
211+
}
212+
}
213+
214+
return false
215+
}
216+
187217
func generatePacketChecksum(raw []byte) (sum uint32) {
188218
// Fastest way to do a crc32 without allocating.
189219
sum = crc32.Update(sum, castagnoliTable, raw[0:8])
@@ -215,11 +245,13 @@ func (p *packet) String() string {
215245
// TryMarshalUnmarshal attempts to marshal and unmarshal a message. Added for fuzzing.
216246
func TryMarshalUnmarshal(msg []byte) int {
217247
p := &packet{}
248+
// Strict mode first (RFC 9260): require valid CRC32c.
218249
err := p.unmarshal(false, msg)
219250
if err != nil {
220251
return 0
221252
}
222253

254+
// Strict send (emit CRC32c).
223255
_, err = p.marshal(false)
224256
if err != nil {
225257
return 0

packet_test.go

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
package sctp
55

66
import (
7+
"encoding/binary"
78
"testing"
89

910
"github.com/stretchr/testify/assert"
@@ -46,11 +47,21 @@ func TestPacketMarshal(t *testing.T) {
4647
headerOnly := []byte{0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0x06, 0xa9, 0x00, 0xe1}
4748
assert.NoError(t, pkt.unmarshal(true, headerOnly), "Unmarshal failed for SCTP packet with no chunks")
4849

49-
headerOnlyMarshaled, err := pkt.marshal(true)
50+
// Marshal in strict mode (RFC 9260) so we emit the real CRC32c.
51+
headerOnlyMarshaled, err := pkt.marshal(false)
5052
if assert.NoError(t, err, "Marshal failed for SCTP packet with no chunks") {
51-
assert.Equal(t, headerOnly, headerOnlyMarshaled,
52-
"Unmarshal/Marshaled header only packet did not match \nheaderOnly: % 02x \nheaderOnlyMarshaled % 02x",
53-
headerOnly, headerOnlyMarshaled)
53+
assert.Equal(t, packetHeaderSize, len(headerOnlyMarshaled))
54+
55+
// First 8 bytes (ports + vtag) must match the original header.
56+
assert.Equal(t, headerOnly[:8], headerOnlyMarshaled[:8])
57+
58+
// The checksum we wrote must equal CRC32c over the header with the checksum field zeroed.
59+
cpy := make([]byte, len(headerOnlyMarshaled))
60+
copy(cpy, headerOnlyMarshaled)
61+
binary.LittleEndian.PutUint32(cpy[8:], 0)
62+
want := generatePacketChecksum(cpy)
63+
got := binary.LittleEndian.Uint32(headerOnlyMarshaled[8:])
64+
assert.Equal(t, want, got, "checksum must be correct CRC32c")
5465
}
5566
}
5667

@@ -61,3 +72,81 @@ func BenchmarkPacketGenerateChecksum(b *testing.B) {
6172
_ = generatePacketChecksum(data[:])
6273
}
6374
}
75+
76+
func FuzzPacket_StrictRoundTrip(f *testing.F) {
77+
// Seed with a tiny valid strict packet.
78+
if raw, err := (&packet{
79+
sourcePort: 5000,
80+
destinationPort: 5000,
81+
verificationTag: 0x01020304,
82+
chunks: []chunk{&chunkCookieAck{}},
83+
}).marshal(false); err == nil {
84+
f.Add(raw)
85+
}
86+
87+
f.Fuzz(func(t *testing.T, data []byte) {
88+
var p packet
89+
if err := p.unmarshal(false, data); err != nil {
90+
return // skip invalid packets
91+
}
92+
93+
out, err := p.marshal(false)
94+
assert.NoError(t, err, "marshal(strict) after strict unmarshal")
95+
96+
if err != nil {
97+
return
98+
}
99+
100+
var q packet
101+
assert.NoError(t, q.unmarshal(false, out), "strict unmarshal after marshal")
102+
})
103+
}
104+
105+
func FuzzPacket_ZeroChecksumAcceptance(f *testing.F) {
106+
// Seed with a simple non-restricted strict packet.
107+
if raw, err := (&packet{
108+
sourcePort: 9,
109+
destinationPort: 9,
110+
verificationTag: 1,
111+
chunks: []chunk{&chunkCookieAck{}},
112+
}).marshal(false); err == nil {
113+
f.Add(raw)
114+
}
115+
116+
f.Fuzz(func(t *testing.T, data []byte) {
117+
var pak packet
118+
if err := pak.unmarshal(false, data); err != nil {
119+
return // only start from valid strict inputs
120+
}
121+
122+
// Force checksum to zero.
123+
mut := make([]byte, len(data))
124+
copy(mut, data)
125+
binary.LittleEndian.PutUint32(mut[8:], 0)
126+
127+
// Strict must reject zero checksum.
128+
assert.Error(t, pak.unmarshal(false, mut), "strict receiver should reject zero checksum")
129+
130+
// ZCA must accept zero checksum.
131+
assert.NoError(t, pak.unmarshal(true, mut), "ZCA receiver should accept zero checksum")
132+
})
133+
}
134+
135+
func FuzzPacket_TryMarshalUnmarshal_NoPanic(f *testing.F) {
136+
f.Add([]byte{0, 0, 0, 0})
137+
138+
if raw, err := (&packet{
139+
sourcePort: 7,
140+
destinationPort: 7,
141+
verificationTag: 0xdeadbeef,
142+
chunks: []chunk{&chunkCookieAck{}},
143+
}).marshal(false); err == nil {
144+
f.Add(raw)
145+
}
146+
147+
f.Fuzz(func(t *testing.T, b []byte) {
148+
assert.NotPanics(t, func() {
149+
_ = TryMarshalUnmarshal(b)
150+
})
151+
})
152+
}

0 commit comments

Comments
 (0)