diff --git a/param_reconfig_response.go b/param_reconfig_response.go index 54e14179..187b5645 100644 --- a/param_reconfig_response.go +++ b/param_reconfig_response.go @@ -10,10 +10,10 @@ import ( ) // This parameter is used by the receiver of a Re-configuration Request -// Parameter to respond to the request. +// Parameter to respond to the request. (RFC 6525, which is referenced by RFC 9260) // -// 0 1 2 3 -// 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 +// 0 1 2 3 +// 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 // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ // | Parameter Type = 16 | Parameter Length | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -28,12 +28,20 @@ import ( type paramReconfigResponse struct { paramHeader + // This value is copied from the request parameter and is used by the // receiver of the Re-configuration Response Parameter to tie the // response to the request. reconfigResponseSequenceNumber uint32 + // This value describes the result of the processing of the request. result reconfigResult + + // Optional fields (RFC 6525). + senderNextTSNPresent bool + senderNextTSN uint32 + receiverNextTSNPresent bool + receiverNextTSN uint32 } type reconfigResult uint32 @@ -50,7 +58,9 @@ const ( // Reconfiguration response errors. var ( - ErrReconfigRespParamTooShort = errors.New("reconfig response parameter too short") + ErrReconfigRespParamTooShort = errors.New("reconfig response parameter too short") + ErrReconfigRespParamInvalidLength = errors.New("reconfig response parameter invalid length") + ErrReconfigRespParamInvalidCombo = errors.New("receiverNextTSN present requires senderNextTSN present") ) func (t reconfigResult) String() string { @@ -76,23 +86,65 @@ func (t reconfigResult) String() string { func (r *paramReconfigResponse) marshal() ([]byte, error) { r.typ = reconfigResp - r.raw = make([]byte, 8) - binary.BigEndian.PutUint32(r.raw, r.reconfigResponseSequenceNumber) + + // Enforce ordering: receiverNextTSN can only be present if senderNextTSN is present. + if r.receiverNextTSNPresent && !r.senderNextTSNPresent { + return nil, ErrReconfigRespParamInvalidCombo + } + + valueLen := 8 + if r.senderNextTSNPresent { + valueLen += 4 + } + if r.receiverNextTSNPresent { + valueLen += 4 + } + + r.raw = make([]byte, valueLen) + binary.BigEndian.PutUint32(r.raw[0:], r.reconfigResponseSequenceNumber) binary.BigEndian.PutUint32(r.raw[4:], uint32(r.result)) + off := 8 + if r.senderNextTSNPresent { + binary.BigEndian.PutUint32(r.raw[off:], r.senderNextTSN) + off += 4 + } + + if r.receiverNextTSNPresent { + binary.BigEndian.PutUint32(r.raw[off:], r.receiverNextTSN) + } + return r.paramHeader.marshal() } func (r *paramReconfigResponse) unmarshal(raw []byte) (param, error) { - err := r.paramHeader.unmarshal(raw) - if err != nil { + if err := r.paramHeader.unmarshal(raw); err != nil { return nil, err } + if len(r.raw) < 8 { return nil, ErrReconfigRespParamTooShort } + + switch len(r.raw) { + case 8, 12, 16: + default: + return nil, ErrReconfigRespParamInvalidLength + } + r.reconfigResponseSequenceNumber = binary.BigEndian.Uint32(r.raw) r.result = reconfigResult(binary.BigEndian.Uint32(r.raw[4:])) + // Optional fields: Sender's Next TSN, Receiver's Next TSN. + if len(r.raw) >= 12 { + r.senderNextTSNPresent = true + r.senderNextTSN = binary.BigEndian.Uint32(r.raw[8:]) + } + + if len(r.raw) == 16 { + r.receiverNextTSNPresent = true + r.receiverNextTSN = binary.BigEndian.Uint32(r.raw[12:]) + } + return r, nil } diff --git a/param_reconfig_response_test.go b/param_reconfig_response_test.go index 3403de5b..dfd73198 100644 --- a/param_reconfig_response_test.go +++ b/param_reconfig_response_test.go @@ -4,6 +4,7 @@ package sctp import ( + "encoding/binary" "testing" "github.com/stretchr/testify/assert" @@ -60,7 +61,7 @@ func TestParamReconfigResponse_Failure(t *testing.T) { } } -func TestReconfigResultStringer(t *testing.T) { +func TestReconfigResultString(t *testing.T) { tt := []struct { result reconfigResult expected string @@ -79,3 +80,223 @@ func TestReconfigResultStringer(t *testing.T) { assert.Equalf(t, tc.expected, actual, "Test case %d", i) } } + +func TestParamReconfigResponse_OptionalFields(t *testing.T) { + type tc struct { + name string + raw []byte + wantRSN uint32 + wantResult reconfigResult + wantSenderPresent bool + wantSender uint32 + wantReceiverPresent bool + wantReceiver uint32 + wantParamHeaderLen int + } + + // build a parameter with optional fields. + build := func(rsn uint32, res reconfigResult, sender *uint32, receiver *uint32) []byte { + // +4 if sender and +4 if receiver) + valLen := 8 + if sender != nil { + valLen += 4 + } + + if receiver != nil { + valLen += 4 + } + + total := 4 + valLen // header + value + b := make([]byte, total) + + // header + binary.BigEndian.PutUint16(b[0:], uint16(reconfigResp)) + binary.BigEndian.PutUint16(b[2:], uint16(total)) //nolint:gosec + + // value + binary.BigEndian.PutUint32(b[4:], rsn) + binary.BigEndian.PutUint32(b[8:], uint32(res)) + + off := 12 + if sender != nil { + binary.BigEndian.PutUint32(b[off:], *sender) + off += 4 + } + + if receiver != nil { + binary.BigEndian.PutUint32(b[off:], *receiver) + } + + return b + } + + senderOnly := uint32(0x01020304) + bothSender := uint32(0xA1B2C3D4) + bothReceiver := uint32(0x0A0B0C0D) + + tests := []tc{ + { + name: "sender_next_only", + raw: build(0x0000002A, reconfigResultErrorWrongSSN, &senderOnly, nil), + wantRSN: 0x0000002A, + wantResult: reconfigResultErrorWrongSSN, + wantSenderPresent: true, + wantSender: senderOnly, + wantReceiverPresent: false, + wantParamHeaderLen: 16, // 4 (hdr) + 12 (value) + }, + { + name: "sender_and_receiver_next", + raw: build(0x00000007, reconfigResultSuccessPerformed, &bothSender, &bothReceiver), + wantRSN: 0x00000007, + wantResult: reconfigResultSuccessPerformed, + wantSenderPresent: true, + wantSender: bothSender, + wantReceiverPresent: true, + wantReceiver: bothReceiver, + wantParamHeaderLen: 20, // 4 (hdr) + 16 (value) + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + var param paramReconfigResponse + + _, err := param.unmarshal(tt.raw) + assert.NoError(t, err) + + assert.Equal(t, reconfigResp, param.paramHeader.typ) + assert.Equal(t, tt.wantParamHeaderLen, param.paramHeader.len) + // param.paramHeader.raw should equal just the value part + assert.Equal(t, tt.raw[4:], param.paramHeader.raw) + + assert.Equal(t, tt.wantRSN, param.reconfigResponseSequenceNumber) + assert.Equal(t, tt.wantResult, param.result) + assert.Equal(t, tt.wantSenderPresent, param.senderNextTSNPresent) + + if tt.wantSenderPresent { + assert.Equal(t, tt.wantSender, param.senderNextTSN) + } + + assert.Equal(t, tt.wantReceiverPresent, param.receiverNextTSNPresent) + + if tt.wantReceiverPresent { + assert.Equal(t, tt.wantReceiver, param.receiverNextTSN) + } + + out, err := param.marshal() + assert.NoError(t, err) + assert.Equal(t, tt.raw, out) + }) + } +} + +func TestParamReconfigResponse_OptionalFields_Roundtrip(t *testing.T) { + tcs := []struct { + name string + input paramReconfigResponse + valLen int // value length excluding the 4-byte header (8/12/16) + }{ + { + name: "NoOptionals", + input: paramReconfigResponse{ + reconfigResponseSequenceNumber: 0x0A, + result: reconfigResultSuccessPerformed, + }, + valLen: 8, + }, + { + name: "SenderOnly", + input: paramReconfigResponse{ + reconfigResponseSequenceNumber: 0x0B, + result: reconfigResultDenied, + senderNextTSNPresent: true, + senderNextTSN: 0x12345678, + }, + valLen: 12, + }, + { + name: "SenderAndReceiver", + input: paramReconfigResponse{ + reconfigResponseSequenceNumber: 0x0C, + result: reconfigResultInProgress, + senderNextTSNPresent: true, + senderNextTSN: 0x11111111, + receiverNextTSNPresent: true, + receiverNextTSN: 0x22222222, + }, + valLen: 16, + }, + } + + for _, tc := range tcs { + t.Run(tc.name, func(t *testing.T) { + b, err := tc.input.marshal() + assert.NoError(t, err) + assert.GreaterOrEqual(t, len(b), 4) + + pt := binary.BigEndian.Uint16(b[0:2]) + pl := binary.BigEndian.Uint16(b[2:4]) + + assert.Equal(t, uint16(reconfigResp), pt) + assert.Equal(t, uint16(4+tc.valLen), pl) //nolint:gosec + assert.Equal(t, int(pl), len(b)) + + var out paramReconfigResponse + p, err := out.unmarshal(b) + + assert.NoError(t, err) + got, ok := p.(*paramReconfigResponse) + assert.True(t, ok, "expected *paramReconfigResponse, got %T", p) + + assert.Equal(t, tc.input.reconfigResponseSequenceNumber, got.reconfigResponseSequenceNumber) + assert.Equal(t, tc.input.result, got.result) + assert.Equal(t, tc.input.senderNextTSNPresent, got.senderNextTSNPresent) + assert.Equal(t, tc.input.senderNextTSN, got.senderNextTSN) + assert.Equal(t, tc.input.receiverNextTSNPresent, got.receiverNextTSNPresent) + assert.Equal(t, tc.input.receiverNextTSN, got.receiverNextTSN) + + b2, err := got.marshal() + assert.NoError(t, err) + assert.Equal(t, b, b2) + }) + } +} + +func TestParamReconfigResponse_Marshal_OrderingGuard(t *testing.T) { + // receiver present without sender present must fail + r := paramReconfigResponse{ + reconfigResponseSequenceNumber: 1, + result: reconfigResultSuccessPerformed, + receiverNextTSNPresent: true, + receiverNextTSN: 7, + } + + _, err := r.marshal() + assert.Error(t, err) + assert.ErrorIs(t, err, ErrReconfigRespParamInvalidCombo) +} + +func TestParamReconfigResponse_Unmarshal_InvalidLengths(t *testing.T) { + // too short + badShort := make([]byte, 10) + binary.BigEndian.PutUint16(badShort[0:], uint16(reconfigResp)) + binary.BigEndian.PutUint16(badShort[2:], uint16(10)) + + var a paramReconfigResponse + _, err := a.unmarshal(badShort) + + assert.Error(t, err) + assert.ErrorIs(t, err, ErrReconfigRespParamTooShort) + + // invalid value length + badLen := make([]byte, 14) + binary.BigEndian.PutUint16(badLen[0:], uint16(reconfigResp)) + binary.BigEndian.PutUint16(badLen[2:], uint16(14)) + + var b paramReconfigResponse + _, err = b.unmarshal(badLen) + + assert.Error(t, err) + assert.ErrorIs(t, err, ErrReconfigRespParamInvalidLength) +}