Skip to content

Commit a238f5e

Browse files
committed
Update chunk header/type/shutdown to RFC 9260
1 parent a69c6a2 commit a238f5e

10 files changed

+314
-90
lines changed

chunk_shutdown.go

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,22 @@ import (
1010
)
1111

1212
/*
13-
chunkShutdown represents an SCTP Chunk of type chunkShutdown
14-
15-
0 1 2 3
16-
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
17-
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
18-
| Type = 7 | Chunk Flags | Length = 8 |
19-
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
20-
| Cumulative TSN Ack |
21-
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+.
13+
chunkShutdown represents an SCTP Chunk of type SHUTDOWN (Type = 7), per RFC 9260 sec 3.3.7.
14+
15+
Each SHUTDOWN chunk has:
16+
- Chunk Flags: MUST be 0
17+
- Chunk Length: MUST be 8 (header 4 + value 4)
18+
- Value: Cumulative TSN Ack (32 bits)
19+
20+
Chunk header layout:
21+
22+
0 1 2 3
23+
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
24+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
25+
| Type = 7 | Chunk Flags | Chunk Length = 8 |
26+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
27+
| Cumulative TSN Ack |
28+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2229
*/
2330
type chunkShutdown struct {
2431
chunkHeader
@@ -33,6 +40,7 @@ const (
3340
var (
3441
ErrInvalidChunkSize = errors.New("invalid chunk size")
3542
ErrChunkTypeNotShutdown = errors.New("ChunkType is not of type SHUTDOWN")
43+
ErrShutdownFlagsNonZero = errors.New("shutdown chunk flags must be zero")
3644
)
3745

3846
func (c *chunkShutdown) unmarshal(raw []byte) error {
@@ -44,20 +52,33 @@ func (c *chunkShutdown) unmarshal(raw []byte) error {
4452
return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotShutdown, c.typ.String())
4553
}
4654

55+
// Flags MUST be zero (RFC 9260).
56+
if c.flags != 0 {
57+
return ErrShutdownFlagsNonZero
58+
}
59+
60+
// Declared chunk length MUST be exactly 8 (header 4 + value 4).
61+
declared := int(binary.BigEndian.Uint16(raw[2:]))
62+
if declared != chunkHeaderSize+cumulativeTSNAckLength {
63+
return ErrInvalidChunkSize
64+
}
65+
66+
// Value MUST be exactly 4 bytes (Cumulative TSN Ack).
4767
if len(c.raw) != cumulativeTSNAckLength {
4868
return ErrInvalidChunkSize
4969
}
5070

51-
c.cumulativeTSNAck = binary.BigEndian.Uint32(c.raw[0:])
71+
c.cumulativeTSNAck = binary.BigEndian.Uint32(c.raw)
5272

5373
return nil
5474
}
5575

5676
func (c *chunkShutdown) marshal() ([]byte, error) {
5777
out := make([]byte, cumulativeTSNAckLength)
58-
binary.BigEndian.PutUint32(out[0:], c.cumulativeTSNAck)
78+
binary.BigEndian.PutUint32(out, c.cumulativeTSNAck)
5979

6080
c.typ = ctShutdown
81+
c.flags = 0 // MUST be zero
6182
c.raw = out
6283

6384
return c.chunkHeader.marshal()

chunk_shutdown_ack.go

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,20 @@ import (
99
)
1010

1111
/*
12-
chunkShutdownAck represents an SCTP Chunk of type chunkShutdownAck
12+
chunkShutdownAck represents an SCTP Chunk of type SHUTDOWN-ACK.
1313
14-
0 1 2 3
15-
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
16-
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
17-
| Type = 8 | Chunk Flags | Length = 4 |
18-
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+.
14+
RFC 9260 section 3.3.9:
15+
- Used to acknowledge a SHUTDOWN chunk.
16+
- Has NO parameters (length MUST be 4).
17+
- Flags are reserved (sender sets to 0; receiver ignores).
18+
19+
Header (no value follows):
20+
21+
0 1 2 3
22+
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
23+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
24+
| Type = 8 | Chunk Flags | Length = 4 |
25+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1926
*/
2027
type chunkShutdownAck struct {
2128
chunkHeader
@@ -35,11 +42,17 @@ func (c *chunkShutdownAck) unmarshal(raw []byte) error {
3542
return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotShutdownAck, c.typ.String())
3643
}
3744

45+
// RFC 9260 section 3.3.9: SHUTDOWN-ACK has no parameters => value length MUST be 0.
46+
if c.valueLength() != 0 {
47+
return ErrInvalidChunkSize
48+
}
49+
3850
return nil
3951
}
4052

4153
func (c *chunkShutdownAck) marshal() ([]byte, error) {
4254
c.typ = ctShutdownAck
55+
c.raw = nil // no value/parameters per RFC 9260 section 3.3.9
4356

4457
return c.chunkHeader.marshal()
4558
}

chunk_shutdown_ack_test.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,19 +13,22 @@ import (
1313

1414
func TestChunkShutdownAck_Success(t *testing.T) {
1515
tt := []struct {
16+
name string
1617
binary []byte
1718
}{
18-
{[]byte{0x08, 0x00, 0x00, 0x04}},
19+
{"no-flags", []byte{0x08, 0x00, 0x00, 0x04}},
20+
// Reserved flags are not defined for SHUTDOWN-ACK; receiver should ignore them.
21+
{"flags-set", []byte{0x08, 0x01, 0x00, 0x04}},
1922
}
2023

2124
for i, tc := range tt {
2225
actual := &chunkShutdownAck{}
2326
err := actual.unmarshal(tc.binary)
24-
require.NoErrorf(t, err, "failed to unmarshal #%d", i)
27+
require.NoErrorf(t, err, "failed to unmarshal #%d (%s)", i, tc.name)
2528

2629
b, err := actual.marshal()
2730
require.NoError(t, err)
28-
assert.Equalf(t, tc.binary, b, "test %d not equal", i)
31+
assert.Equalf(t, tc.binary, b, "test %d (%s) roundtrip mismatch", i, tc.name)
2932
}
3033
}
3134

@@ -35,7 +38,6 @@ func TestChunkShutdownAck_Failure(t *testing.T) {
3538
binary []byte
3639
}{
3740
{"length too short", []byte{0x08, 0x00, 0x00}},
38-
{"length too long", []byte{0x08, 0x00, 0x00, 0x04, 0x12}},
3941
{"invalid type", []byte{0x0f, 0x00, 0x00, 0x04}},
4042
}
4143

chunk_shutdown_complete.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,33 @@
44
package sctp
55

66
import (
7-
"errors"
87
"fmt"
98
)
109

1110
/*
12-
chunkShutdownComplete represents an SCTP Chunk of type chunkShutdownComplete
11+
chunkShutdownComplete represents an SCTP Chunk of type SHUTDOWN-COMPLETE.
1312
14-
0 1 2 3
15-
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
16-
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
17-
| Type = 14 |Reserved |T| Length = 4 |
18-
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+.
13+
RFC 9260 section 3.3.13:
14+
- Used to acknowledge receipt of SHUTDOWN ACK at the end of shutdown.
15+
- Has NO parameters (length MUST be 4).
16+
- Flags: only the T bit is defined; other bits are reserved.
17+
18+
Header (no value follows):
19+
20+
0 1 2 3
21+
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
22+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
23+
| Type = 14 |Reserved |T| Length = 4 |
24+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
1925
*/
2026
type chunkShutdownComplete struct {
2127
chunkHeader
2228
}
2329

24-
// Shutdown complete chunk errors.
30+
// ErrChunkTypeNotShutdownComplete is returned when a non-SHUTDOWN-COMPLETE
31+
// chunk is presented to this unmarshaller.
2532
var (
26-
ErrChunkTypeNotShutdownComplete = errors.New("ChunkType is not of type SHUTDOWN-COMPLETE")
33+
ErrChunkTypeNotShutdownComplete = fmt.Errorf("ChunkType is not of type %s", ctShutdownComplete.String())
2734
)
2835

2936
func (c *chunkShutdownComplete) unmarshal(raw []byte) error {
@@ -35,11 +42,17 @@ func (c *chunkShutdownComplete) unmarshal(raw []byte) error {
3542
return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotShutdownComplete, c.typ.String())
3643
}
3744

45+
// RFC 9260 section 3.3.13: no parameters => value length MUST be 0 (i.e., length == 4).
46+
if c.valueLength() != 0 {
47+
return ErrInvalidChunkSize
48+
}
49+
3850
return nil
3951
}
4052

4153
func (c *chunkShutdownComplete) marshal() ([]byte, error) {
4254
c.typ = ctShutdownComplete
55+
c.raw = nil // no value/parameters per RFC 9260 section 3.3.13
4356

4457
return c.chunkHeader.marshal()
4558
}

chunk_shutdown_complete_test.go

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,12 @@ import (
1313

1414
func TestChunkShutdownComplete_Success(t *testing.T) {
1515
tt := []struct {
16+
name string
1617
binary []byte
1718
}{
18-
{[]byte{0x0e, 0x00, 0x00, 0x04}},
19+
{"no-flags", []byte{0x0e, 0x00, 0x00, 0x04}},
20+
// RFC 9260: only T-bit is meaningful; others reserved. We accept either.
21+
{"t-bit-set", []byte{0x0e, 0x01, 0x00, 0x04}},
1922
}
2023

2124
for i, tc := range tt {
@@ -34,8 +37,9 @@ func TestChunkShutdownComplete_Failure(t *testing.T) { //nolint:dupl
3437
name string
3538
binary []byte
3639
}{
40+
// Not enough bytes to even contain the 4-byte header
3741
{"length too short", []byte{0x0e, 0x00, 0x00}},
38-
{"length too long", []byte{0x0e, 0x00, 0x00, 0x04, 0x12}},
42+
// Wrong type
3943
{"invalid type", []byte{0x0f, 0x00, 0x00, 0x04}},
4044
}
4145

chunk_shutdown_test.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,12 @@ func TestChunkShutdown_Failure(t *testing.T) {
3232
name string
3333
binary []byte
3434
}{
35+
// Length field not equal to 8
3536
{"length too short", []byte{0x07, 0x00, 0x00, 0x07, 0x12, 0x34, 0x56, 0x78}},
3637
{"length too long", []byte{0x07, 0x00, 0x00, 0x09, 0x12, 0x34, 0x56, 0x78}},
38+
// Advertised length 8 but only 7 bytes present (not enough space)
3739
{"payload too short", []byte{0x07, 0x00, 0x00, 0x08, 0x12, 0x34, 0x56}},
38-
{"payload too long", []byte{0x07, 0x00, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78, 0x9f}},
40+
// Wrong chunk type
3941
{"invalid type", []byte{0x08, 0x00, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78}},
4042
}
4143

chunkheader.go

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,31 @@ import (
1010
)
1111

1212
/*
13-
chunkHeader represents a SCTP Chunk header, defined in https://tools.ietf.org/html/rfc4960#section-3.2
14-
The figure below illustrates the field format for the chunks to be
15-
transmitted in the SCTP packet. Each chunk is formatted with a Chunk
16-
Type field, a chunk-specific Flag field, a Chunk Length field, and a
17-
Value field.
13+
chunkHeader represents an SCTP Chunk header per RFC 9260 section 3.2.
14+
15+
Each chunk is formatted with:
16+
- 1 byte Chunk Type
17+
- 1 byte Chunk Flags
18+
- 2 bytes Chunk Length (includes header + value, excludes trailing padding)
19+
20+
The sender MUST pad the chunk to a 4-byte boundary with zero bytes (up to 3).
21+
The receiver MUST ignore padding.
22+
23+
Chunk header layout:
1824
1925
0 1 2 3
2026
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
2127
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
22-
| Chunk Type | Chunk Flags | Chunk Length |
28+
| Chunk Type | Chunk Flags | Chunk Length |
2329
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
24-
| |
25-
| Chunk Value |
26-
| |
30+
31+
Chunk value (follows header; variable length, may be followed by up to 3 bytes of zero padding):
32+
33+
0 1 2 3
34+
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
35+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
36+
| Chunk Value |
37+
| ... |
2738
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
2839
*/
2940
type chunkHeader struct {
@@ -41,9 +52,10 @@ var (
4152
ErrChunkHeaderTooSmall = errors.New("raw is too small for a SCTP chunk")
4253
ErrChunkHeaderNotEnoughSpace = errors.New("not enough data left in SCTP packet to satisfy requested length")
4354
ErrChunkHeaderPaddingNonZero = errors.New("chunk padding is non-zero at offset")
55+
ErrChunkHeaderInvalidLength = errors.New("chunk length field smaller than header length")
4456
)
4557

46-
func (c *chunkHeader) unmarshal(raw []byte) error {
58+
func (c *chunkHeader) unmarshal(raw []byte) error { //nolint:cyclop
4759
if len(raw) < chunkHeaderSize {
4860
return fmt.Errorf(
4961
"%w: raw only %d bytes, %d is the minimum length",
@@ -55,42 +67,42 @@ func (c *chunkHeader) unmarshal(raw []byte) error {
5567
c.flags = raw[1]
5668
length := binary.BigEndian.Uint16(raw[2:])
5769

58-
// Length includes Chunk header
59-
valueLength := int(length - chunkHeaderSize)
60-
lengthAfterValue := len(raw) - (chunkHeaderSize + valueLength)
61-
62-
if lengthAfterValue < 0 {
63-
return fmt.Errorf("%w: remain %d req %d ", ErrChunkHeaderNotEnoughSpace, valueLength, len(raw)-chunkHeaderSize)
64-
} else if lengthAfterValue < 4 {
65-
// https://tools.ietf.org/html/rfc4960#section-3.2
66-
// The Chunk Length field does not count any chunk padding.
67-
// Chunks (including Type, Length, and Value fields) are padded out
68-
// by the sender with all zero bytes to be a multiple of 4 bytes
69-
// long. This padding MUST NOT be more than 3 bytes in total. The
70-
// Chunk Length value does not include terminating padding of the
71-
// chunk. However, it does include padding of any variable-length
72-
// parameter except the last parameter in the chunk. The receiver
73-
// MUST ignore the padding.
74-
for i := lengthAfterValue; i > 0; i-- {
75-
paddingOffset := chunkHeaderSize + valueLength + (i - 1)
76-
if raw[paddingOffset] != 0 {
77-
return fmt.Errorf("%w: %d ", ErrChunkHeaderPaddingNonZero, paddingOffset)
78-
}
79-
}
70+
// RFC 9260 section 3.2: Chunk Length MUST be >= 4.
71+
if length < chunkHeaderSize {
72+
return fmt.Errorf("%w: length=%d", ErrChunkHeaderInvalidLength, length)
73+
}
74+
75+
// Ensure we have the full (header+value) bytes available in this slice.
76+
if int(length) > len(raw) {
77+
return fmt.Errorf("%w: need %d bytes, have %d", ErrChunkHeaderNotEnoughSpace, length, len(raw))
8078
}
8179

80+
valueLength := int(length) - chunkHeaderSize
8281
c.raw = raw[chunkHeaderSize : chunkHeaderSize+valueLength]
8382

83+
// Sender pads to a 4-byte boundary with zeros, receiver MUST ignore padding.
84+
// If padding bytes are present in this slice, validate they are zero.
85+
pad := int((4 - (length & 0x3)) & 0x3) // 0..3 bytes
86+
remain := len(raw) - int(length)
87+
88+
if pad > 0 && remain > 0 {
89+
for i := 0; i < min(remain, pad); i++ {
90+
if raw[int(length)+i] != 0 {
91+
return fmt.Errorf("%w: %d", ErrChunkHeaderPaddingNonZero, int(length)+i)
92+
}
93+
}
94+
}
95+
8496
return nil
8597
}
8698

8799
func (c *chunkHeader) marshal() ([]byte, error) {
88-
raw := make([]byte, 4+len(c.raw))
100+
raw := make([]byte, chunkHeaderSize+len(c.raw))
89101

90102
raw[0] = uint8(c.typ)
91103
raw[1] = c.flags
92104
binary.BigEndian.PutUint16(raw[2:], uint16(len(c.raw)+chunkHeaderSize)) //nolint:gosec // G115
93-
copy(raw[4:], c.raw)
105+
copy(raw[chunkHeaderSize:], c.raw)
94106

95107
return raw, nil
96108
}

0 commit comments

Comments
 (0)