├── .github ├── .gitignore ├── install-hooks.sh ├── workflows │ ├── api.yaml │ ├── lint.yaml │ ├── reuse.yml │ ├── release.yml │ ├── renovate-go-sum-fix.yaml │ ├── tidy-check.yaml │ ├── codeql-analysis.yml │ ├── fuzz.yaml │ └── test.yaml └── fetch-scripts.sh ├── chunkheader_test.go ├── .goreleaser.yml ├── renovate.json ├── sctp.go ├── chunk.go ├── .gitignore ├── codecov.yml ├── param_ecn_capable.go ├── param_random.go ├── .reuse └── dep5 ├── go.mod ├── param_heartbeat_info.go ├── examples └── ping-pong │ ├── README.md │ ├── ping │ ├── main.go │ └── conn.go │ └── pong │ ├── main.go │ └── conn.go ├── control_queue.go ├── param_chunk_list.go ├── error_cause_invalid_mandatory_parameter.go ├── param_supported_extensions.go ├── error_cause_unrecognized_chunk_type.go ├── LICENSES └── MIT.txt ├── LICENSE ├── chunktype_test.go ├── param_forward_tsn_supported.go ├── param_state_cookie.go ├── param_zero_checksum_test.go ├── chunk_shutdown_ack_test.go ├── param_test.go ├── chunk_shutdown_complete_test.go ├── chunk_init_test.go ├── chunk_shutdown_test.go ├── chunk_cookie_ack.go ├── util.go ├── chunk_shutdown_ack.go ├── queue.go ├── chunk_forward_tsn_test.go ├── param.go ├── param_ecn_capable_test.go ├── paramheader_test.go ├── chunk_shutdown_complete.go ├── param_forward_tsn_supported_test.go ├── chunk_cookie_echo.go ├── error_cause_header.go ├── error_cause_user_initiated_abort.go ├── param_zero_checksum.go ├── payload_queue.go ├── chunk_abort_test.go ├── chunktype.go ├── chunk_shutdown.go ├── error_cause_protocol_violation.go ├── chunk_reconfig_test.go ├── go.sum ├── windowedmin.go ├── param_requested_hmac_algorithm.go ├── windowedmin_test.go ├── chunk_error_test.go ├── param_outgoing_reset_request_test.go ├── param_reconfig_response_test.go ├── association_stats.go ├── stream_test.go ├── ack_timer.go ├── packet_test.go ├── queue_test.go ├── chunk_abort.go ├── ack_timer_test.go ├── chunk_error.go ├── paramtype_test.go ├── payload_queue_test.go ├── error_cause.go ├── chunk_reconfig.go ├── param_reconfig_response.go ├── chunkheader.go ├── paramheader.go ├── chunk_heartbeat.go ├── pending_queue.go ├── param_outgoing_reset_request.go ├── paramtype.go ├── util_test.go ├── README.md ├── chunk_heartbeat_ack.go ├── chunk_forward_tsn.go ├── chunk_init.go ├── chunk_init_ack.go ├── chunk_selective_ack.go ├── receive_payload_queue.go ├── chunk_heartbeat_ack_test.go └── receive_payload_queue_test.go /.github/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | .goassets 5 | -------------------------------------------------------------------------------- /chunkheader_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | builds: 5 | - skip: true 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>pion/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /sctp.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package sctp implements the SCTP spec 5 | package sctp 6 | -------------------------------------------------------------------------------- /chunk.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | type chunk interface { 7 | unmarshal(raw []byte) error 8 | marshal() ([]byte, error) 9 | check() (bool, error) 10 | 11 | valueLength() int 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | ### JetBrains IDE ### 5 | ##################### 6 | .idea/ 7 | 8 | ### Emacs Temporary Files ### 9 | ############################# 10 | *~ 11 | 12 | ### Folders ### 13 | ############### 14 | bin/ 15 | vendor/ 16 | node_modules/ 17 | 18 | ### Files ### 19 | ############# 20 | *.ivf 21 | *.ogg 22 | tags 23 | cover.out 24 | *.sw[poe] 25 | *.wasm 26 | examples/sfu-ws/cert.pem 27 | examples/sfu-ws/key.pem 28 | wasm_exec.js 29 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # 6 | # SPDX-FileCopyrightText: 2023 The Pion community 7 | # SPDX-License-Identifier: MIT 8 | 9 | coverage: 10 | status: 11 | project: 12 | default: 13 | # Allow decreasing 2% of total coverage to avoid noise. 14 | threshold: 2% 15 | patch: 16 | default: 17 | target: 70% 18 | only_pulls: true 19 | 20 | ignore: 21 | - "examples/*" 22 | - "examples/**/*" 23 | -------------------------------------------------------------------------------- /param_ecn_capable.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | type paramECNCapable struct { 7 | paramHeader 8 | } 9 | 10 | func (r *paramECNCapable) marshal() ([]byte, error) { 11 | r.typ = ecnCapable 12 | r.raw = []byte{} 13 | 14 | return r.paramHeader.marshal() 15 | } 16 | 17 | func (r *paramECNCapable) unmarshal(raw []byte) (param, error) { 18 | err := r.paramHeader.unmarshal(raw) 19 | if err != nil { 20 | return nil, err 21 | } 22 | 23 | return r, nil 24 | } 25 | -------------------------------------------------------------------------------- /param_random.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | type paramRandom struct { 7 | paramHeader 8 | randomData []byte 9 | } 10 | 11 | func (r *paramRandom) marshal() ([]byte, error) { 12 | r.typ = random 13 | r.raw = r.randomData 14 | 15 | return r.paramHeader.marshal() 16 | } 17 | 18 | func (r *paramRandom) unmarshal(raw []byte) (param, error) { 19 | err := r.paramHeader.unmarshal(raw) 20 | if err != nil { 21 | return nil, err 22 | } 23 | r.randomData = r.raw 24 | 25 | return r, nil 26 | } 27 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: Pion 3 | Source: https://github.com/pion/ 4 | 5 | Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json yarn.lock 6 | Copyright: 2023 The Pion community 7 | License: MIT 8 | 9 | Files: testdata/seed/* testdata/fuzz/* **/testdata/fuzz/* api/*.txt 10 | Copyright: 2023 The Pion community 11 | License: CC0-1.0 12 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pion/sctp 2 | 3 | require ( 4 | github.com/pion/logging v0.2.4 5 | github.com/pion/randutil v0.1.0 6 | github.com/pion/transport/v3 v3.1.1 7 | github.com/stretchr/testify v1.11.1 8 | ) 9 | 10 | require ( 11 | github.com/davecgh/go-spew v1.1.1 // indirect 12 | github.com/kr/pretty v0.1.0 // indirect 13 | github.com/pmezard/go-difflib v1.0.0 // indirect 14 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 15 | gopkg.in/yaml.v3 v3.0.1 // indirect 16 | ) 17 | 18 | go 1.21 19 | 20 | // Retract version with ZeroChecksum misinterpretation (bi-directional/global handling) 21 | retract v1.8.12 22 | -------------------------------------------------------------------------------- /param_heartbeat_info.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | type paramHeartbeatInfo struct { 7 | paramHeader 8 | heartbeatInformation []byte 9 | } 10 | 11 | func (h *paramHeartbeatInfo) marshal() ([]byte, error) { 12 | h.typ = heartbeatInfo 13 | h.raw = h.heartbeatInformation 14 | 15 | return h.paramHeader.marshal() 16 | } 17 | 18 | func (h *paramHeartbeatInfo) unmarshal(raw []byte) (param, error) { 19 | err := h.paramHeader.unmarshal(raw) 20 | if err != nil { 21 | return nil, err 22 | } 23 | h.heartbeatInformation = h.raw 24 | 25 | return h, nil 26 | } 27 | -------------------------------------------------------------------------------- /.github/install-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # DO NOT EDIT THIS FILE 5 | # 6 | # It is automatically copied from https://github.com/pion/.goassets repository. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | SCRIPT_PATH="$(realpath "$(dirname "$0")")" 15 | 16 | . ${SCRIPT_PATH}/fetch-scripts.sh 17 | 18 | cp "${GOASSETS_PATH}/hooks/commit-msg.sh" "${SCRIPT_PATH}/../.git/hooks/commit-msg" 19 | cp "${GOASSETS_PATH}/hooks/pre-commit.sh" "${SCRIPT_PATH}/../.git/hooks/pre-commit" 20 | -------------------------------------------------------------------------------- /.github/workflows/api.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: API 15 | on: 16 | pull_request: 17 | 18 | jobs: 19 | check: 20 | uses: pion/.goassets/.github/workflows/api.reusable.yml@master 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Lint 15 | on: 16 | pull_request: 17 | 18 | jobs: 19 | lint: 20 | uses: pion/.goassets/.github/workflows/lint.reusable.yml@master 21 | -------------------------------------------------------------------------------- /examples/ping-pong/README.md: -------------------------------------------------------------------------------- 1 | # ping-pong 2 | 3 | ping-pong is a sctp example that shows how you can send/recv messages. 4 | In this example, there are 2 types of peers: **ping** and **pong**. 5 | 6 | **Ping** will always send `ping ` messages to **pong** and receive `pong ` messages from **pong**. 7 | 8 | **Pong** will always receive `ping ` from **ping** and send `pong ` messages to **ping**. 9 | 10 | ## Instruction 11 | 12 | ### Run ping and pong 13 | 14 | ### Run pong 15 | 16 | ```sh 17 | go run github.com/pion/sctp/examples/ping-pong/pong@latest 18 | ``` 19 | 20 | 21 | ### Run ping 22 | 23 | ```sh 24 | go run github.com/pion/sctp/examples/ping-pong/ping@latest 25 | ``` 26 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: REUSE Compliance Check 15 | 16 | on: 17 | push: 18 | pull_request: 19 | 20 | jobs: 21 | lint: 22 | uses: pion/.goassets/.github/workflows/reuse.reusable.yml@master 23 | -------------------------------------------------------------------------------- /control_queue.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | // control queue 7 | 8 | type controlQueue struct { 9 | queue []*packet 10 | } 11 | 12 | func newControlQueue() *controlQueue { 13 | return &controlQueue{queue: []*packet{}} 14 | } 15 | 16 | func (q *controlQueue) push(c *packet) { 17 | q.queue = append(q.queue, c) 18 | } 19 | 20 | func (q *controlQueue) pushAll(packets []*packet) { 21 | q.queue = append(q.queue, packets...) 22 | } 23 | 24 | func (q *controlQueue) popAll() []*packet { 25 | packets := q.queue 26 | q.queue = []*packet{} 27 | 28 | return packets 29 | } 30 | 31 | func (q *controlQueue) size() int { 32 | return len(q.queue) 33 | } 34 | -------------------------------------------------------------------------------- /param_chunk_list.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | type paramChunkList struct { 7 | paramHeader 8 | chunkTypes []chunkType 9 | } 10 | 11 | func (c *paramChunkList) marshal() ([]byte, error) { 12 | c.typ = chunkList 13 | c.raw = make([]byte, len(c.chunkTypes)) 14 | for i, t := range c.chunkTypes { 15 | c.raw[i] = byte(t) 16 | } 17 | 18 | return c.paramHeader.marshal() 19 | } 20 | 21 | func (c *paramChunkList) unmarshal(raw []byte) (param, error) { 22 | err := c.paramHeader.unmarshal(raw) 23 | if err != nil { 24 | return nil, err 25 | } 26 | for _, t := range c.raw { 27 | c.chunkTypes = append(c.chunkTypes, chunkType(t)) 28 | } 29 | 30 | return c, nil 31 | } 32 | -------------------------------------------------------------------------------- /error_cause_invalid_mandatory_parameter.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | // errorCauseInvalidMandatoryParameter represents an SCTP error cause. 7 | type errorCauseInvalidMandatoryParameter struct { 8 | errorCauseHeader 9 | } 10 | 11 | func (e *errorCauseInvalidMandatoryParameter) marshal() ([]byte, error) { 12 | return e.errorCauseHeader.marshal() 13 | } 14 | 15 | func (e *errorCauseInvalidMandatoryParameter) unmarshal(raw []byte) error { 16 | return e.errorCauseHeader.unmarshal(raw) 17 | } 18 | 19 | // String makes errorCauseInvalidMandatoryParameter printable. 20 | func (e *errorCauseInvalidMandatoryParameter) String() string { 21 | return e.errorCauseHeader.String() 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Release 15 | on: 16 | push: 17 | tags: 18 | - 'v*' 19 | 20 | jobs: 21 | release: 22 | uses: pion/.goassets/.github/workflows/release.reusable.yml@master 23 | with: 24 | go-version: "1.25" # auto-update/latest-go-version 25 | -------------------------------------------------------------------------------- /.github/workflows/renovate-go-sum-fix.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Fix go.sum 15 | on: 16 | push: 17 | branches: 18 | - renovate/* 19 | 20 | jobs: 21 | fix: 22 | uses: pion/.goassets/.github/workflows/renovate-go-sum-fix.reusable.yml@master 23 | secrets: 24 | token: ${{ secrets.PIONBOT_PRIVATE_KEY }} 25 | -------------------------------------------------------------------------------- /param_supported_extensions.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | type paramSupportedExtensions struct { 7 | paramHeader 8 | ChunkTypes []chunkType 9 | } 10 | 11 | func (s *paramSupportedExtensions) marshal() ([]byte, error) { 12 | s.typ = supportedExt 13 | s.raw = make([]byte, len(s.ChunkTypes)) 14 | for i, c := range s.ChunkTypes { 15 | s.raw[i] = byte(c) 16 | } 17 | 18 | return s.paramHeader.marshal() 19 | } 20 | 21 | func (s *paramSupportedExtensions) unmarshal(raw []byte) (param, error) { 22 | err := s.paramHeader.unmarshal(raw) 23 | if err != nil { 24 | return nil, err 25 | } 26 | 27 | for _, t := range s.raw { 28 | s.ChunkTypes = append(s.ChunkTypes, chunkType(t)) 29 | } 30 | 31 | return s, nil 32 | } 33 | -------------------------------------------------------------------------------- /.github/workflows/tidy-check.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Go mod tidy 15 | on: 16 | pull_request: 17 | push: 18 | branches: 19 | - master 20 | 21 | jobs: 22 | tidy: 23 | uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master 24 | with: 25 | go-version: "1.25" # auto-update/latest-go-version 26 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: CodeQL 15 | 16 | on: 17 | workflow_dispatch: 18 | schedule: 19 | - cron: '23 5 * * 0' 20 | pull_request: 21 | branches: 22 | - master 23 | paths: 24 | - '**.go' 25 | 26 | jobs: 27 | analyze: 28 | uses: pion/.goassets/.github/workflows/codeql-analysis.reusable.yml@master 29 | -------------------------------------------------------------------------------- /.github/workflows/fuzz.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Fuzz 15 | on: 16 | push: 17 | branches: 18 | - master 19 | schedule: 20 | - cron: "0 */8 * * *" 21 | 22 | jobs: 23 | fuzz: 24 | uses: pion/.goassets/.github/workflows/fuzz.reusable.yml@master 25 | with: 26 | go-version: "1.25" # auto-update/latest-go-version 27 | fuzz-time: "60s" 28 | -------------------------------------------------------------------------------- /error_cause_unrecognized_chunk_type.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | // errorCauseUnrecognizedChunkType represents an SCTP error cause. 7 | type errorCauseUnrecognizedChunkType struct { 8 | errorCauseHeader 9 | unrecognizedChunk []byte 10 | } 11 | 12 | func (e *errorCauseUnrecognizedChunkType) marshal() ([]byte, error) { 13 | e.code = unrecognizedChunkType 14 | e.errorCauseHeader.raw = e.unrecognizedChunk 15 | 16 | return e.errorCauseHeader.marshal() 17 | } 18 | 19 | func (e *errorCauseUnrecognizedChunkType) unmarshal(raw []byte) error { 20 | err := e.errorCauseHeader.unmarshal(raw) 21 | if err != nil { 22 | return err 23 | } 24 | 25 | e.unrecognizedChunk = e.errorCauseHeader.raw 26 | 27 | return nil 28 | } 29 | 30 | // String makes errorCauseUnrecognizedChunkType printable. 31 | func (e *errorCauseUnrecognizedChunkType) String() string { 32 | return e.errorCauseHeader.String() 33 | } 34 | -------------------------------------------------------------------------------- /.github/fetch-scripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # DO NOT EDIT THIS FILE 5 | # 6 | # It is automatically copied from https://github.com/pion/.goassets repository. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | set -eu 15 | 16 | SCRIPT_PATH="$(realpath "$(dirname "$0")")" 17 | GOASSETS_PATH="${SCRIPT_PATH}/.goassets" 18 | 19 | GOASSETS_REF=${GOASSETS_REF:-master} 20 | 21 | if [ -d "${GOASSETS_PATH}" ]; then 22 | if ! git -C "${GOASSETS_PATH}" diff --exit-code; then 23 | echo "${GOASSETS_PATH} has uncommitted changes" >&2 24 | exit 1 25 | fi 26 | git -C "${GOASSETS_PATH}" fetch origin 27 | git -C "${GOASSETS_PATH}" checkout ${GOASSETS_REF} 28 | git -C "${GOASSETS_PATH}" reset --hard origin/${GOASSETS_REF} 29 | else 30 | git clone -b ${GOASSETS_REF} https://github.com/pion/.goassets.git "${GOASSETS_PATH}" 31 | fi 32 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The Pion community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /chunktype_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestChunkType_String(t *testing.T) { 13 | tt := []struct { 14 | chunkType chunkType 15 | expected string 16 | }{ 17 | {ctPayloadData, "DATA"}, 18 | {ctInit, "INIT"}, 19 | {ctInitAck, "INIT-ACK"}, 20 | {ctSack, "SACK"}, 21 | {ctHeartbeat, "HEARTBEAT"}, 22 | {ctHeartbeatAck, "HEARTBEAT-ACK"}, 23 | {ctAbort, "ABORT"}, 24 | {ctShutdown, "SHUTDOWN"}, 25 | {ctShutdownAck, "SHUTDOWN-ACK"}, 26 | {ctError, "ERROR"}, 27 | {ctCookieEcho, "COOKIE-ECHO"}, 28 | {ctCookieAck, "COOKIE-ACK"}, 29 | {ctCWR, "ECNE"}, 30 | {ctShutdownComplete, "SHUTDOWN-COMPLETE"}, 31 | {ctReconfig, "RECONFIG"}, 32 | {ctForwardTSN, "FORWARD-TSN"}, 33 | {chunkType(255), "Unknown ChunkType: 255"}, 34 | } 35 | 36 | for _, tc := range tt { 37 | assert.Equalf(t, tc.expected, tc.chunkType.String(), "chunkType %v should be %s", tc.chunkType, tc.expected) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /param_forward_tsn_supported.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | // At the initialization of the association, the sender of the INIT or 7 | // INIT ACK chunk MAY include this OPTIONAL parameter to inform its peer 8 | // that it is able to support the Forward TSN chunk 9 | // 10 | // 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 11 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 12 | // | Parameter Type = 49152 | Parameter Length = 4 | 13 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 14 | 15 | type paramForwardTSNSupported struct { 16 | paramHeader 17 | } 18 | 19 | func (f *paramForwardTSNSupported) marshal() ([]byte, error) { 20 | f.typ = forwardTSNSupp 21 | f.raw = []byte{} 22 | 23 | return f.paramHeader.marshal() 24 | } 25 | 26 | func (f *paramForwardTSNSupported) unmarshal(raw []byte) (param, error) { 27 | err := f.paramHeader.unmarshal(raw) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | return f, nil 33 | } 34 | -------------------------------------------------------------------------------- /param_state_cookie.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "crypto/rand" 8 | "fmt" 9 | ) 10 | 11 | type paramStateCookie struct { 12 | paramHeader 13 | cookie []byte 14 | } 15 | 16 | func newRandomStateCookie() (*paramStateCookie, error) { 17 | randCookie := make([]byte, 32) 18 | _, err := rand.Read(randCookie) 19 | // crypto/rand.Read returns n == len(b) if and only if err == nil. 20 | if err != nil { 21 | return nil, err 22 | } 23 | 24 | s := ¶mStateCookie{ 25 | cookie: randCookie, 26 | } 27 | 28 | return s, nil 29 | } 30 | 31 | func (s *paramStateCookie) marshal() ([]byte, error) { 32 | s.typ = stateCookie 33 | s.raw = s.cookie 34 | 35 | return s.paramHeader.marshal() 36 | } 37 | 38 | func (s *paramStateCookie) unmarshal(raw []byte) (param, error) { 39 | err := s.paramHeader.unmarshal(raw) 40 | if err != nil { 41 | return nil, err 42 | } 43 | s.cookie = s.raw 44 | 45 | return s, nil 46 | } 47 | 48 | // String makes paramStateCookie printable. 49 | func (s *paramStateCookie) String() string { 50 | return fmt.Sprintf("%s: %s", s.paramHeader, s.cookie) 51 | } 52 | -------------------------------------------------------------------------------- /param_zero_checksum_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestParamZeroChecksum(t *testing.T) { 14 | tt := []struct { 15 | binary []byte 16 | parsed *paramZeroChecksumAcceptable 17 | }{ 18 | { 19 | binary: []byte{0x80, 0x01, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01}, 20 | parsed: ¶mZeroChecksumAcceptable{ 21 | paramHeader: paramHeader{ 22 | typ: zeroChecksumAcceptable, 23 | unrecognizedAction: paramHeaderUnrecognizedActionSkip, 24 | len: 8, 25 | raw: []byte{0x00, 0x00, 0x00, 0x01}, 26 | }, 27 | edmid: 1, 28 | }, 29 | }, 30 | } 31 | 32 | for i, tc := range tt { 33 | t.Run(fmt.Sprintf("%v", i), func(t *testing.T) { 34 | actual := ¶mZeroChecksumAcceptable{} 35 | _, err := actual.unmarshal(tc.binary) 36 | assert.NoError(t, err) 37 | assert.Equal(t, tc.parsed, actual) 38 | b, err := actual.marshal() 39 | assert.NoError(t, err) 40 | assert.Equal(t, tc.binary, b) 41 | }) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /chunk_shutdown_ack_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //nolint:dupl 5 | package sctp 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestChunkShutdownAck_Success(t *testing.T) { 15 | tt := []struct { 16 | binary []byte 17 | }{ 18 | {[]byte{0x08, 0x00, 0x00, 0x04}}, 19 | } 20 | 21 | for i, tc := range tt { 22 | actual := &chunkShutdownAck{} 23 | err := actual.unmarshal(tc.binary) 24 | require.NoErrorf(t, err, "failed to unmarshal #%d", i) 25 | 26 | b, err := actual.marshal() 27 | require.NoError(t, err) 28 | assert.Equalf(t, tc.binary, b, "test %d not equal", i) 29 | } 30 | } 31 | 32 | func TestChunkShutdownAck_Failure(t *testing.T) { 33 | tt := []struct { 34 | name string 35 | binary []byte 36 | }{ 37 | {"length too short", []byte{0x08, 0x00, 0x00}}, 38 | {"length too long", []byte{0x08, 0x00, 0x00, 0x04, 0x12}}, 39 | {"invalid type", []byte{0x0f, 0x00, 0x00, 0x04}}, 40 | } 41 | 42 | for i, tc := range tt { 43 | actual := &chunkShutdownAck{} 44 | err := actual.unmarshal(tc.binary) 45 | assert.Errorf(t, err, "expected unmarshal #%d: '%s' to fail.", i, tc.name) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /param_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestBuildParam_Success(t *testing.T) { 13 | tt := []struct { 14 | binary []byte 15 | }{ 16 | {testChunkReconfigParamA()}, 17 | } 18 | 19 | for i, tc := range tt { 20 | pType, err := parseParamType(tc.binary) 21 | assert.NoErrorf(t, err, "failed to parse param type #%d", i) 22 | 23 | p, err := buildParam(pType, tc.binary) 24 | assert.NoErrorf(t, err, "failed to unmarshal #%d", i) 25 | 26 | b, err := p.marshal() 27 | assert.NoErrorf(t, err, "failed to marshal #%d", i) 28 | assert.Equal(t, tc.binary, b) 29 | } 30 | } 31 | 32 | func TestBuildParam_Failure(t *testing.T) { 33 | tt := []struct { 34 | name string 35 | binary []byte 36 | }{ 37 | {"invalid ParamType", []byte{0x0, 0x0}}, 38 | {"build failure", testChunkReconfigParamA()[:8]}, 39 | } 40 | 41 | for i, tc := range tt { 42 | pType, err := parseParamType(tc.binary) 43 | assert.NoErrorf(t, err, "failed to parse param type #%d", i) 44 | 45 | _, err = buildParam(pType, tc.binary) 46 | assert.Errorf(t, err, "expected buildParam #%d: '%s' to fail.", i, tc.name) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /chunk_shutdown_complete_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //nolint:dupl 5 | package sctp 6 | 7 | import ( 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | "github.com/stretchr/testify/require" 12 | ) 13 | 14 | func TestChunkShutdownComplete_Success(t *testing.T) { 15 | tt := []struct { 16 | binary []byte 17 | }{ 18 | {[]byte{0x0e, 0x00, 0x00, 0x04}}, 19 | } 20 | 21 | for i, tc := range tt { 22 | actual := &chunkShutdownComplete{} 23 | err := actual.unmarshal(tc.binary) 24 | require.NoErrorf(t, err, "failed to unmarshal #%d", i) 25 | 26 | b, err := actual.marshal() 27 | require.NoError(t, err) 28 | assert.Equalf(t, tc.binary, b, "test %d not equal", i) 29 | } 30 | } 31 | 32 | func TestChunkShutdownComplete_Failure(t *testing.T) { //nolint:dupl 33 | tt := []struct { 34 | name string 35 | binary []byte 36 | }{ 37 | {"length too short", []byte{0x0e, 0x00, 0x00}}, 38 | {"length too long", []byte{0x0e, 0x00, 0x00, 0x04, 0x12}}, 39 | {"invalid type", []byte{0x0f, 0x00, 0x00, 0x04}}, 40 | } 41 | 42 | for i, tc := range tt { 43 | actual := &chunkShutdownComplete{} 44 | err := actual.unmarshal(tc.binary) 45 | require.Errorf(t, err, "expected unmarshal #%d: '%s' to fail.", i, tc.name) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /chunk_init_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestChunkInit_UnrecognizedParameters(t *testing.T) { 13 | initChunkHeader := []byte{ 14 | 0x55, 0xb9, 0x64, 0xa5, 0x00, 0x02, 0x00, 0x00, 15 | 0x04, 0x00, 0x08, 0x00, 0xe8, 0x6d, 0x10, 0x30, 16 | } 17 | 18 | unrecognizedSkip := append([]byte{}, initChunkHeader...) 19 | unrecognizedSkip = append(unrecognizedSkip, byte(paramHeaderUnrecognizedActionSkip), 0xFF, 0x00, 0x04, 0x00) 20 | 21 | initCommonChunk := &chunkInitCommon{} 22 | assert.NoError(t, initCommonChunk.unmarshal(unrecognizedSkip)) 23 | assert.Equal(t, 1, len(initCommonChunk.unrecognizedParams)) 24 | assert.Equal(t, paramHeaderUnrecognizedActionSkip, initCommonChunk.unrecognizedParams[0].unrecognizedAction) 25 | 26 | unrecognizedStop := append([]byte{}, initChunkHeader...) 27 | unrecognizedStop = append(unrecognizedStop, byte(paramHeaderUnrecognizedActionStop), 0xFF, 0x00, 0x04, 0x00) 28 | 29 | initCommonChunk = &chunkInitCommon{} 30 | assert.NoError(t, initCommonChunk.unmarshal(unrecognizedStop)) 31 | assert.Equal(t, 1, len(initCommonChunk.unrecognizedParams)) 32 | assert.Equal(t, paramHeaderUnrecognizedActionStop, initCommonChunk.unrecognizedParams[0].unrecognizedAction) 33 | } 34 | -------------------------------------------------------------------------------- /chunk_shutdown_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestChunkShutdown_Success(t *testing.T) { 13 | tt := []struct { 14 | binary []byte 15 | }{ 16 | {[]byte{0x07, 0x00, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78}}, 17 | } 18 | 19 | for i, tc := range tt { 20 | actual := &chunkShutdown{} 21 | err := actual.unmarshal(tc.binary) 22 | assert.NoErrorf(t, err, "failed to unmarshal #%d: %v", i) 23 | 24 | b, err := actual.marshal() 25 | assert.NoError(t, err) 26 | assert.Equalf(t, tc.binary, b, "test %d not equal", i) 27 | } 28 | } 29 | 30 | func TestChunkShutdown_Failure(t *testing.T) { 31 | tt := []struct { 32 | name string 33 | binary []byte 34 | }{ 35 | {"length too short", []byte{0x07, 0x00, 0x00, 0x07, 0x12, 0x34, 0x56, 0x78}}, 36 | {"length too long", []byte{0x07, 0x00, 0x00, 0x09, 0x12, 0x34, 0x56, 0x78}}, 37 | {"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}}, 39 | {"invalid type", []byte{0x08, 0x00, 0x00, 0x08, 0x12, 0x34, 0x56, 0x78}}, 40 | } 41 | 42 | for i, tc := range tt { 43 | actual := &chunkShutdown{} 44 | err := actual.unmarshal(tc.binary) 45 | assert.Errorf(t, err, "expected unmarshal #%d: '%s' to fail.", i, tc.name) 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /chunk_cookie_ack.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | chunkCookieAck represents an SCTP Chunk of type chunkCookieAck 13 | 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 = 11 |Chunk Flags | Length = 4 | 18 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 19 | */ 20 | type chunkCookieAck struct { 21 | chunkHeader 22 | } 23 | 24 | // Cookie ack chunk errors. 25 | var ( 26 | ErrChunkTypeNotCookieAck = errors.New("ChunkType is not of type COOKIEACK") 27 | ) 28 | 29 | func (c *chunkCookieAck) unmarshal(raw []byte) error { 30 | if err := c.chunkHeader.unmarshal(raw); err != nil { 31 | return err 32 | } 33 | 34 | if c.typ != ctCookieAck { 35 | return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotCookieAck, c.typ.String()) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (c *chunkCookieAck) marshal() ([]byte, error) { 42 | c.chunkHeader.typ = ctCookieAck 43 | 44 | return c.chunkHeader.marshal() 45 | } 46 | 47 | func (c *chunkCookieAck) check() (abort bool, err error) { 48 | return false, nil 49 | } 50 | 51 | // String makes chunkCookieAck printable. 52 | func (c *chunkCookieAck) String() string { 53 | return c.chunkHeader.String() 54 | } 55 | -------------------------------------------------------------------------------- /util.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | const ( 7 | paddingMultiple = 4 8 | ) 9 | 10 | func getPadding(l int) int { 11 | return (paddingMultiple - (l % paddingMultiple)) % paddingMultiple 12 | } 13 | 14 | func padByte(in []byte, cnt int) []byte { 15 | if cnt < 0 { 16 | cnt = 0 17 | } 18 | padding := make([]byte, cnt) 19 | 20 | return append(in, padding...) 21 | } 22 | 23 | // Serial Number Arithmetic (RFC 1982). 24 | func sna32LT(i1, i2 uint32) bool { 25 | return (i1 < i2 && i2-i1 < 1<<31) || (i1 > i2 && i1-i2 > 1<<31) 26 | } 27 | 28 | func sna32LTE(i1, i2 uint32) bool { 29 | return i1 == i2 || sna32LT(i1, i2) 30 | } 31 | 32 | func sna32GT(i1, i2 uint32) bool { 33 | return (i1 < i2 && (i2-i1) >= 1<<31) || (i1 > i2 && (i1-i2) <= 1<<31) 34 | } 35 | 36 | func sna32GTE(i1, i2 uint32) bool { 37 | return i1 == i2 || sna32GT(i1, i2) 38 | } 39 | 40 | func sna32EQ(i1, i2 uint32) bool { 41 | return i1 == i2 42 | } 43 | 44 | func sna16LT(i1, i2 uint16) bool { 45 | return (i1 < i2 && (i2-i1) < 1<<15) || (i1 > i2 && (i1-i2) > 1<<15) 46 | } 47 | 48 | func sna16LTE(i1, i2 uint16) bool { 49 | return i1 == i2 || sna16LT(i1, i2) 50 | } 51 | 52 | func sna16GT(i1, i2 uint16) bool { 53 | return (i1 < i2 && (i2-i1) >= 1<<15) || (i1 > i2 && (i1-i2) <= 1<<15) 54 | } 55 | 56 | func sna16GTE(i1, i2 uint16) bool { 57 | return i1 == i2 || sna16GT(i1, i2) 58 | } 59 | 60 | func sna16EQ(i1, i2 uint16) bool { 61 | return i1 == i2 62 | } 63 | -------------------------------------------------------------------------------- /chunk_shutdown_ack.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | chunkShutdownAck represents an SCTP Chunk of type chunkShutdownAck 13 | 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 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+. 19 | */ 20 | type chunkShutdownAck struct { 21 | chunkHeader 22 | } 23 | 24 | // Shutdown ack chunk errors. 25 | var ( 26 | ErrChunkTypeNotShutdownAck = errors.New("ChunkType is not of type SHUTDOWN-ACK") 27 | ) 28 | 29 | func (c *chunkShutdownAck) unmarshal(raw []byte) error { 30 | if err := c.chunkHeader.unmarshal(raw); err != nil { 31 | return err 32 | } 33 | 34 | if c.typ != ctShutdownAck { 35 | return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotShutdownAck, c.typ.String()) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (c *chunkShutdownAck) marshal() ([]byte, error) { 42 | c.typ = ctShutdownAck 43 | 44 | return c.chunkHeader.marshal() 45 | } 46 | 47 | func (c *chunkShutdownAck) check() (abort bool, err error) { 48 | return false, nil 49 | } 50 | 51 | // String makes chunkShutdownAck printable. 52 | func (c *chunkShutdownAck) String() string { 53 | return c.chunkHeader.String() 54 | } 55 | -------------------------------------------------------------------------------- /queue.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | type queue[T any] struct { 7 | buf []T 8 | head int 9 | tail int 10 | count int 11 | } 12 | 13 | const minCap = 16 14 | 15 | func newQueue[T any](capacity int) *queue[T] { 16 | queueCap := minCap 17 | for queueCap < capacity { 18 | queueCap <<= 1 19 | } 20 | 21 | return &queue[T]{ 22 | buf: make([]T, queueCap), 23 | } 24 | } 25 | 26 | func (q *queue[T]) Len() int { 27 | return q.count 28 | } 29 | 30 | func (q *queue[T]) PushBack(ele T) { 31 | q.growIfFull() 32 | q.buf[q.tail] = ele 33 | q.tail = (q.tail + 1) % len(q.buf) 34 | q.count++ 35 | } 36 | 37 | func (q *queue[T]) PopFront() T { 38 | ele := q.buf[q.head] 39 | var zeroVal T 40 | q.buf[q.head] = zeroVal 41 | q.head = (q.head + 1) % len(q.buf) 42 | q.count-- 43 | 44 | return ele 45 | } 46 | 47 | func (q *queue[T]) Front() T { 48 | return q.buf[q.head] 49 | } 50 | 51 | func (q *queue[T]) Back() T { 52 | return q.buf[(q.tail-1+len(q.buf))%len(q.buf)] 53 | } 54 | 55 | func (q *queue[T]) At(i int) T { 56 | return q.buf[(q.head+i)%(len(q.buf))] 57 | } 58 | 59 | func (q *queue[T]) growIfFull() { 60 | if q.count < len(q.buf) { 61 | return 62 | } 63 | 64 | newBuf := make([]T, q.count<<1) 65 | if q.tail > q.head { 66 | copy(newBuf, q.buf[q.head:q.tail]) 67 | } else { 68 | n := copy(newBuf, q.buf[q.head:]) 69 | copy(newBuf[n:], q.buf[:q.tail]) 70 | } 71 | 72 | q.head = 0 73 | q.tail = q.count 74 | q.buf = newBuf 75 | } 76 | -------------------------------------------------------------------------------- /chunk_forward_tsn_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func testChunkForwardTSN() []byte { 13 | return []byte{0xc0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x3} 14 | } 15 | 16 | func TestChunkForwardTSN_Success(t *testing.T) { 17 | tt := []struct { 18 | binary []byte 19 | }{ 20 | {testChunkForwardTSN()}, 21 | {[]byte{0xc0, 0x0, 0x0, 0xc, 0x0, 0x0, 0x0, 0x3, 0x0, 0x4, 0x0, 0x5}}, 22 | {[]byte{0xc0, 0x0, 0x0, 0x10, 0x0, 0x0, 0x0, 0x3, 0x0, 0x4, 0x0, 0x5, 0x0, 0x6, 0x0, 0x7}}, 23 | } 24 | 25 | for i, tc := range tt { 26 | actual := &chunkForwardTSN{} 27 | err := actual.unmarshal(tc.binary) 28 | assert.NoErrorf(t, err, "failed to unmarshal #%d", i) 29 | 30 | b, err := actual.marshal() 31 | assert.NoError(t, err) 32 | assert.Equalf(t, tc.binary, b, "test %d not equal", i) 33 | } 34 | } 35 | 36 | func TestChunkForwardTSNUnmarshal_Failure(t *testing.T) { 37 | tt := []struct { 38 | name string 39 | binary []byte 40 | }{ 41 | {"chunk header to short", []byte{0xc0}}, 42 | {"missing New Cumulative TSN", []byte{0xc0, 0x0, 0x0, 0x4}}, 43 | {"missing stream sequence", []byte{0xc0, 0x0, 0x0, 0xe, 0x0, 0x0, 0x0, 0x3, 0x0, 0x4, 0x0, 0x5, 0x0, 0x6}}, 44 | } 45 | 46 | for i, tc := range tt { 47 | actual := &chunkForwardTSN{} 48 | err := actual.unmarshal(tc.binary) 49 | assert.Errorf(t, err, "expected unmarshal #%d: '%s' to fail.", i, tc.name) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /param.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | type param interface { 12 | marshal() ([]byte, error) 13 | length() int 14 | } 15 | 16 | // ErrParamTypeUnhandled is returned if unknown parameter type is specified. 17 | var ErrParamTypeUnhandled = errors.New("unhandled ParamType") 18 | 19 | func buildParam(typeParam paramType, rawParam []byte) (param, error) { //nolint:cyclop 20 | switch typeParam { 21 | case forwardTSNSupp: 22 | return (¶mForwardTSNSupported{}).unmarshal(rawParam) 23 | case supportedExt: 24 | return (¶mSupportedExtensions{}).unmarshal(rawParam) 25 | case ecnCapable: 26 | return (¶mECNCapable{}).unmarshal(rawParam) 27 | case random: 28 | return (¶mRandom{}).unmarshal(rawParam) 29 | case reqHMACAlgo: 30 | return (¶mRequestedHMACAlgorithm{}).unmarshal(rawParam) 31 | case chunkList: 32 | return (¶mChunkList{}).unmarshal(rawParam) 33 | case stateCookie: 34 | return (¶mStateCookie{}).unmarshal(rawParam) 35 | case heartbeatInfo: 36 | return (¶mHeartbeatInfo{}).unmarshal(rawParam) 37 | case outSSNResetReq: 38 | return (¶mOutgoingResetRequest{}).unmarshal(rawParam) 39 | case reconfigResp: 40 | return (¶mReconfigResponse{}).unmarshal(rawParam) 41 | case zeroChecksumAcceptable: 42 | return (¶mZeroChecksumAcceptable{}).unmarshal(rawParam) 43 | default: 44 | return nil, fmt.Errorf("%w: %v", ErrParamTypeUnhandled, typeParam) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /param_ecn_capable_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp // nolint:dupl 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func testParamECNCapabale() []byte { 13 | return []byte{0x80, 0x0, 0x0, 0x4} 14 | } 15 | 16 | func TestParamECNCapabale_Success(t *testing.T) { 17 | tt := []struct { 18 | binary []byte 19 | parsed *paramECNCapable 20 | }{ 21 | { 22 | testParamECNCapabale(), 23 | ¶mECNCapable{ 24 | paramHeader: paramHeader{ 25 | typ: ecnCapable, 26 | unrecognizedAction: paramHeaderUnrecognizedActionSkip, 27 | len: 4, 28 | raw: []byte{}, 29 | }, 30 | }, 31 | }, 32 | } 33 | 34 | for i, tc := range tt { 35 | actual := ¶mECNCapable{} 36 | _, err := actual.unmarshal(tc.binary) 37 | assert.NoErrorf(t, err, "failed to unmarshal #%d", i) 38 | assert.Equal(t, tc.parsed, actual) 39 | 40 | b, err := actual.marshal() 41 | assert.NoErrorf(t, err, "failed to unmarshal #%d", i) 42 | assert.Equal(t, tc.binary, b) 43 | } 44 | } 45 | 46 | func TestParamECNCapabale_Failure(t *testing.T) { 47 | tt := []struct { 48 | name string 49 | binary []byte 50 | }{ 51 | {"param too short", []byte{0x0, 0xd, 0x0}}, 52 | } 53 | 54 | for i, tc := range tt { 55 | actual := ¶mECNCapable{} 56 | _, err := actual.unmarshal(tc.binary) 57 | assert.Errorf(t, err, "expected unmarshal #%d: '%s' to fail.", i, tc.name) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /paramheader_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func testParamHeader() []byte { 13 | return []byte{0x0, 0x1, 0x0, 0x4} 14 | } 15 | 16 | func TestParamHeader_Success(t *testing.T) { 17 | tt := []struct { 18 | binary []byte 19 | parsed *paramHeader 20 | }{ 21 | { 22 | testParamHeader(), 23 | ¶mHeader{ 24 | typ: heartbeatInfo, 25 | len: 4, 26 | raw: []byte{}, 27 | }, 28 | }, 29 | } 30 | 31 | for i, tc := range tt { 32 | actual := ¶mHeader{} 33 | err := actual.unmarshal(tc.binary) 34 | assert.NoErrorf(t, err, "failed to unmarshal #%d", i) 35 | assert.Equal(t, tc.parsed, actual) 36 | 37 | b, err := actual.marshal() 38 | assert.NoErrorf(t, err, "failed to marshal #%d", i) 39 | assert.Equal(t, tc.binary, b) 40 | } 41 | } 42 | 43 | func TestParamHeaderUnmarshal_Failure(t *testing.T) { 44 | tt := []struct { 45 | name string 46 | binary []byte 47 | }{ 48 | {"header too short", testParamHeader()[:2]}, 49 | // {"wrong param type", []byte{0x0, 0x0, 0x0, 0x4}}, // Not possible to fail parseParamType atm. 50 | {"reported length below header length", []byte{0x0, 0xd, 0x0, 0x3}}, 51 | {"wrong reported length", testChunkReconfigParamA()[:4]}, 52 | } 53 | 54 | for i, tc := range tt { 55 | actual := ¶mHeader{} 56 | err := actual.unmarshal(tc.binary) 57 | assert.Errorf(t, err, "expected unmarshal #%d: '%s' to fail.", i, tc.name) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /chunk_shutdown_complete.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | chunkShutdownComplete represents an SCTP Chunk of type chunkShutdownComplete 13 | 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 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+. 19 | */ 20 | type chunkShutdownComplete struct { 21 | chunkHeader 22 | } 23 | 24 | // Shutdown complete chunk errors. 25 | var ( 26 | ErrChunkTypeNotShutdownComplete = errors.New("ChunkType is not of type SHUTDOWN-COMPLETE") 27 | ) 28 | 29 | func (c *chunkShutdownComplete) unmarshal(raw []byte) error { 30 | if err := c.chunkHeader.unmarshal(raw); err != nil { 31 | return err 32 | } 33 | 34 | if c.typ != ctShutdownComplete { 35 | return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotShutdownComplete, c.typ.String()) 36 | } 37 | 38 | return nil 39 | } 40 | 41 | func (c *chunkShutdownComplete) marshal() ([]byte, error) { 42 | c.typ = ctShutdownComplete 43 | 44 | return c.chunkHeader.marshal() 45 | } 46 | 47 | func (c *chunkShutdownComplete) check() (abort bool, err error) { 48 | return false, nil 49 | } 50 | 51 | // String makes chunkShutdownComplete printable. 52 | func (c *chunkShutdownComplete) String() string { 53 | return c.chunkHeader.String() 54 | } 55 | -------------------------------------------------------------------------------- /param_forward_tsn_supported_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp // nolint:dupl 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func testParamForwardTSNSupported() []byte { 13 | return []byte{0xc0, 0x0, 0x0, 0x4} 14 | } 15 | 16 | func TestParamForwardTSNSupported_Success(t *testing.T) { 17 | tt := []struct { 18 | binary []byte 19 | parsed *paramForwardTSNSupported 20 | }{ 21 | { 22 | testParamForwardTSNSupported(), 23 | ¶mForwardTSNSupported{ 24 | paramHeader: paramHeader{ 25 | typ: forwardTSNSupp, 26 | len: 4, 27 | unrecognizedAction: paramHeaderUnrecognizedActionSkipAndReport, 28 | raw: []byte{}, 29 | }, 30 | }, 31 | }, 32 | } 33 | 34 | for i, tc := range tt { 35 | actual := ¶mForwardTSNSupported{} 36 | _, err := actual.unmarshal(tc.binary) 37 | assert.NoErrorf(t, err, "failed to unmarshal #%d", i) 38 | assert.Equal(t, tc.parsed, actual) 39 | 40 | b, err := actual.marshal() 41 | assert.NoErrorf(t, err, "failed to unmarshal #%d", i) 42 | assert.Equal(t, tc.binary, b) 43 | } 44 | } 45 | 46 | func TestParamForwardTSNSupported_Failure(t *testing.T) { 47 | tt := []struct { 48 | name string 49 | binary []byte 50 | }{ 51 | {"param too short", []byte{0x0, 0xd, 0x0}}, 52 | } 53 | 54 | for i, tc := range tt { 55 | actual := ¶mForwardTSNSupported{} 56 | _, err := actual.unmarshal(tc.binary) 57 | assert.Errorf(t, err, "expected unmarshal #%d: '%s' to fail.", i, tc.name) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /chunk_cookie_echo.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | CookieEcho represents an SCTP Chunk of type CookieEcho 13 | 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 = 10 |Chunk Flags | Length | 18 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 19 | | Cookie | 20 | | | 21 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 22 | */ 23 | type chunkCookieEcho struct { 24 | chunkHeader 25 | cookie []byte 26 | } 27 | 28 | // Cookie echo chunk errors. 29 | var ( 30 | ErrChunkTypeNotCookieEcho = errors.New("ChunkType is not of type COOKIEECHO") 31 | ) 32 | 33 | func (c *chunkCookieEcho) unmarshal(raw []byte) error { 34 | if err := c.chunkHeader.unmarshal(raw); err != nil { 35 | return err 36 | } 37 | 38 | if c.typ != ctCookieEcho { 39 | return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotCookieEcho, c.typ.String()) 40 | } 41 | c.cookie = c.raw 42 | 43 | return nil 44 | } 45 | 46 | func (c *chunkCookieEcho) marshal() ([]byte, error) { 47 | c.chunkHeader.typ = ctCookieEcho 48 | c.chunkHeader.raw = c.cookie 49 | 50 | return c.chunkHeader.marshal() 51 | } 52 | 53 | func (c *chunkCookieEcho) check() (abort bool, err error) { 54 | return false, nil 55 | } 56 | -------------------------------------------------------------------------------- /error_cause_header.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | ) 10 | 11 | // errorCauseHeader represents the shared header that is shared by all error causes. 12 | type errorCauseHeader struct { 13 | code errorCauseCode 14 | len uint16 15 | raw []byte 16 | } 17 | 18 | const ( 19 | errorCauseHeaderLength = 4 20 | ) 21 | 22 | // ErrInvalidSCTPChunk is returned when an SCTP chunk is invalid. 23 | var ErrInvalidSCTPChunk = errors.New("invalid SCTP chunk") 24 | 25 | func (e *errorCauseHeader) marshal() ([]byte, error) { 26 | e.len = uint16(len(e.raw)) + uint16(errorCauseHeaderLength) //nolint:gosec // G115 27 | raw := make([]byte, e.len) 28 | binary.BigEndian.PutUint16(raw[0:], uint16(e.code)) 29 | binary.BigEndian.PutUint16(raw[2:], e.len) 30 | copy(raw[errorCauseHeaderLength:], e.raw) 31 | 32 | return raw, nil 33 | } 34 | 35 | func (e *errorCauseHeader) unmarshal(raw []byte) error { 36 | e.code = errorCauseCode(binary.BigEndian.Uint16(raw[0:])) 37 | e.len = binary.BigEndian.Uint16(raw[2:]) 38 | if e.len < errorCauseHeaderLength || int(e.len) > len(raw) { 39 | return ErrInvalidSCTPChunk 40 | } 41 | valueLength := e.len - errorCauseHeaderLength 42 | e.raw = raw[errorCauseHeaderLength : errorCauseHeaderLength+valueLength] 43 | 44 | return nil 45 | } 46 | 47 | func (e *errorCauseHeader) length() uint16 { 48 | return e.len 49 | } 50 | 51 | func (e *errorCauseHeader) errorCauseCode() errorCauseCode { 52 | return e.code 53 | } 54 | 55 | // String makes errorCauseHeader printable. 56 | func (e errorCauseHeader) String() string { 57 | return e.code.String() 58 | } 59 | -------------------------------------------------------------------------------- /error_cause_user_initiated_abort.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | /* 11 | This error cause MAY be included in ABORT chunks that are sent 12 | because of an upper-layer request. The upper layer can specify an 13 | Upper Layer Abort Reason that is transported by SCTP transparently 14 | and MAY be delivered to the upper-layer protocol at the peer. 15 | 16 | 0 1 2 3 17 | 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 18 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 19 | | Cause Code=12 | Cause Length=Variable | 20 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 21 | / Upper Layer Abort Reason / 22 | \ \ 23 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 24 | */ 25 | type errorCauseUserInitiatedAbort struct { 26 | errorCauseHeader 27 | upperLayerAbortReason []byte 28 | } 29 | 30 | func (e *errorCauseUserInitiatedAbort) marshal() ([]byte, error) { 31 | e.code = userInitiatedAbort 32 | e.errorCauseHeader.raw = e.upperLayerAbortReason 33 | 34 | return e.errorCauseHeader.marshal() 35 | } 36 | 37 | func (e *errorCauseUserInitiatedAbort) unmarshal(raw []byte) error { 38 | err := e.errorCauseHeader.unmarshal(raw) 39 | if err != nil { 40 | return err 41 | } 42 | 43 | e.upperLayerAbortReason = e.errorCauseHeader.raw 44 | 45 | return nil 46 | } 47 | 48 | // String makes errorCauseUserInitiatedAbort printable. 49 | func (e *errorCauseUserInitiatedAbort) String() string { 50 | return fmt.Sprintf("%s: %s", e.errorCauseHeader.String(), e.upperLayerAbortReason) 51 | } 52 | -------------------------------------------------------------------------------- /param_zero_checksum.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | ) 10 | 11 | // This parameter is used to inform the receiver that a sender is willing to 12 | // accept zero as checksum if some other error detection method is used 13 | // instead. 14 | // 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 = 0x8001 (suggested) | Length = 8 | 18 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 19 | // | Error Detection Method Identifier (EDMID) | 20 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 21 | 22 | type paramZeroChecksumAcceptable struct { 23 | paramHeader 24 | // The Error Detection Method Identifier (EDMID) specifies an alternate 25 | // error detection method the sender of this parameter is willing to use for 26 | // received packets. 27 | edmid uint32 28 | } 29 | 30 | // Zero Checksum parameter error. 31 | var ( 32 | ErrZeroChecksumParamTooShort = errors.New("zero checksum parameter too short") 33 | ) 34 | 35 | const ( 36 | dtlsErrorDetectionMethod uint32 = 1 37 | ) 38 | 39 | func (r *paramZeroChecksumAcceptable) marshal() ([]byte, error) { 40 | r.typ = zeroChecksumAcceptable 41 | r.raw = make([]byte, 4) 42 | binary.BigEndian.PutUint32(r.raw, r.edmid) 43 | 44 | return r.paramHeader.marshal() 45 | } 46 | 47 | func (r *paramZeroChecksumAcceptable) unmarshal(raw []byte) (param, error) { 48 | err := r.paramHeader.unmarshal(raw) 49 | if err != nil { 50 | return nil, err 51 | } 52 | if len(r.raw) < 4 { 53 | return nil, ErrZeroChecksumParamTooShort 54 | } 55 | r.edmid = binary.BigEndian.Uint32(r.raw) 56 | 57 | return r, nil 58 | } 59 | -------------------------------------------------------------------------------- /payload_queue.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | type payloadQueue struct { 7 | chunks *queue[*chunkPayloadData] 8 | nBytes int 9 | } 10 | 11 | func newPayloadQueue() *payloadQueue { 12 | return &payloadQueue{chunks: newQueue[*chunkPayloadData](128)} 13 | } 14 | 15 | func (q *payloadQueue) pushNoCheck(p *chunkPayloadData) { 16 | q.chunks.PushBack(p) 17 | q.nBytes += len(p.userData) 18 | } 19 | 20 | // pop pops only if the oldest chunk's TSN matches the given TSN. 21 | func (q *payloadQueue) pop(tsn uint32) (*chunkPayloadData, bool) { 22 | if q.chunks.Len() > 0 && tsn == q.chunks.Front().tsn { 23 | c := q.chunks.PopFront() 24 | q.nBytes -= len(c.userData) 25 | 26 | return c, true 27 | } 28 | 29 | return nil, false 30 | } 31 | 32 | // get returns reference to chunkPayloadData with the given TSN value. 33 | func (q *payloadQueue) get(tsn uint32) (*chunkPayloadData, bool) { 34 | length := q.chunks.Len() 35 | if length == 0 { 36 | return nil, false 37 | } 38 | head := q.chunks.Front().tsn 39 | if tsn < head || int(tsn-head) >= length { 40 | return nil, false 41 | } 42 | 43 | return q.chunks.At(int(tsn - head)), true 44 | } 45 | 46 | func (q *payloadQueue) markAsAcked(tsn uint32) int { 47 | var nBytesAcked int 48 | if c, ok := q.get(tsn); ok { 49 | c.acked = true 50 | c.retransmit = false 51 | nBytesAcked = len(c.userData) 52 | q.nBytes -= nBytesAcked 53 | c.userData = []byte{} 54 | } 55 | 56 | return nBytesAcked 57 | } 58 | 59 | func (q *payloadQueue) markAllToRetrasmit() { 60 | for i := 0; i < q.chunks.Len(); i++ { 61 | c := q.chunks.At(i) 62 | if c.acked || c.abandoned() { 63 | continue 64 | } 65 | c.retransmit = true 66 | } 67 | } 68 | 69 | func (q *payloadQueue) getNumBytes() int { 70 | return q.nBytes 71 | } 72 | 73 | func (q *payloadQueue) size() int { 74 | return q.chunks.Len() 75 | } 76 | -------------------------------------------------------------------------------- /chunk_abort_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestAbortChunk(t *testing.T) { 13 | t.Run("One error cause", func(t *testing.T) { 14 | abort1 := &chunkAbort{ 15 | errorCauses: []errorCause{&errorCauseProtocolViolation{ 16 | errorCauseHeader: errorCauseHeader{code: protocolViolation}, 17 | }}, 18 | } 19 | bytes, err := abort1.marshal() 20 | assert.NoError(t, err, "should succeed") 21 | 22 | abort2 := &chunkAbort{} 23 | err = abort2.unmarshal(bytes) 24 | assert.NoError(t, err, "should succeed") 25 | assert.Equal(t, 1, len(abort2.errorCauses), "should have only one cause") 26 | assert.Equal(t, 27 | abort1.errorCauses[0].errorCauseCode(), 28 | abort2.errorCauses[0].errorCauseCode(), 29 | "errorCause code should match") 30 | }) 31 | 32 | t.Run("Many error causes", func(t *testing.T) { 33 | abort1 := &chunkAbort{ 34 | errorCauses: []errorCause{ 35 | &errorCauseProtocolViolation{ 36 | errorCauseHeader: errorCauseHeader{code: invalidMandatoryParameter}, 37 | }, 38 | &errorCauseProtocolViolation{ 39 | errorCauseHeader: errorCauseHeader{code: unrecognizedChunkType}, 40 | }, 41 | &errorCauseProtocolViolation{ 42 | errorCauseHeader: errorCauseHeader{code: protocolViolation}, 43 | }, 44 | }, 45 | } 46 | bytes, err := abort1.marshal() 47 | assert.NoError(t, err, "should succeed") 48 | 49 | abort2 := &chunkAbort{} 50 | err = abort2.unmarshal(bytes) 51 | assert.NoError(t, err, "should succeed") 52 | assert.Equal(t, 3, len(abort2.errorCauses), "should have only one cause") 53 | for i, errorCause := range abort1.errorCauses { 54 | assert.Equal(t, 55 | errorCause.errorCauseCode(), 56 | abort2.errorCauses[i].errorCauseCode(), 57 | "errorCause code should match") 58 | } 59 | }) 60 | } 61 | -------------------------------------------------------------------------------- /examples/ping-pong/ping/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !pong 5 | // +build !pong 6 | 7 | package main 8 | 9 | import ( 10 | "fmt" 11 | "log" 12 | "net" 13 | "time" 14 | 15 | "github.com/pion/logging" 16 | "github.com/pion/sctp" 17 | ) 18 | 19 | func main() { //nolint:cyclop 20 | conn, err := net.Dial("udp", "127.0.0.1:9899") //nolint: noctx 21 | if err != nil { 22 | log.Panic(err) 23 | } 24 | defer func() { 25 | if closeErr := conn.Close(); closeErr != nil { 26 | log.Panic(err) 27 | } 28 | }() 29 | fmt.Println("dialed udp ponger") 30 | 31 | config := sctp.Config{ 32 | NetConn: conn, 33 | LoggerFactory: logging.NewDefaultLoggerFactory(), 34 | } 35 | a, err := sctp.Client(config) 36 | if err != nil { 37 | log.Panic(err) 38 | } 39 | defer func() { 40 | if closeErr := a.Close(); closeErr != nil { 41 | log.Panic(err) 42 | } 43 | }() 44 | fmt.Println("created a client") 45 | 46 | stream, err := a.OpenStream(0, sctp.PayloadTypeWebRTCString) 47 | if err != nil { 48 | log.Panic(err) 49 | } 50 | defer func() { 51 | if closeErr := stream.Close(); closeErr != nil { 52 | log.Panic(err) 53 | } 54 | }() 55 | fmt.Println("opened a stream") 56 | 57 | // set unordered = true and 10ms treshold for dropping packets 58 | stream.SetReliabilityParams(true, sctp.ReliabilityTypeTimed, 10) 59 | 60 | go func() { 61 | var pingSeqNum int 62 | for { 63 | pingMsg := fmt.Sprintf("ping %d", pingSeqNum) 64 | _, err = stream.Write([]byte(pingMsg)) 65 | if err != nil { 66 | log.Panic(err) 67 | } 68 | fmt.Println("sent:", pingMsg) 69 | pingSeqNum++ 70 | time.Sleep(time.Second) 71 | } 72 | }() 73 | 74 | for { 75 | buff := make([]byte, 1024) 76 | _, err = stream.Read(buff) 77 | if err != nil { 78 | log.Panic(err) 79 | } 80 | pongMsg := string(buff) 81 | fmt.Println("received:", pongMsg) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /chunktype.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import "fmt" 7 | 8 | // chunkType is an enum for SCTP Chunk Type field 9 | // This field identifies the type of information contained in the 10 | // Chunk Value field. 11 | type chunkType uint8 12 | 13 | // List of known chunkType enums. 14 | const ( 15 | ctPayloadData chunkType = 0 16 | ctInit chunkType = 1 17 | ctInitAck chunkType = 2 18 | ctSack chunkType = 3 19 | ctHeartbeat chunkType = 4 20 | ctHeartbeatAck chunkType = 5 21 | ctAbort chunkType = 6 22 | ctShutdown chunkType = 7 23 | ctShutdownAck chunkType = 8 24 | ctError chunkType = 9 25 | ctCookieEcho chunkType = 10 26 | ctCookieAck chunkType = 11 27 | ctCWR chunkType = 13 28 | ctShutdownComplete chunkType = 14 29 | ctReconfig chunkType = 130 30 | ctForwardTSN chunkType = 192 31 | ) 32 | 33 | func (c chunkType) String() string { //nolint:cyclop 34 | switch c { 35 | case ctPayloadData: 36 | return "DATA" 37 | case ctInit: 38 | return "INIT" 39 | case ctInitAck: 40 | return "INIT-ACK" 41 | case ctSack: 42 | return "SACK" 43 | case ctHeartbeat: 44 | return "HEARTBEAT" 45 | case ctHeartbeatAck: 46 | return "HEARTBEAT-ACK" 47 | case ctAbort: 48 | return "ABORT" 49 | case ctShutdown: 50 | return "SHUTDOWN" 51 | case ctShutdownAck: 52 | return "SHUTDOWN-ACK" 53 | case ctError: 54 | return "ERROR" 55 | case ctCookieEcho: 56 | return "COOKIE-ECHO" 57 | case ctCookieAck: 58 | return "COOKIE-ACK" 59 | case ctCWR: 60 | return "ECNE" // Explicit Congestion Notification Echo 61 | case ctShutdownComplete: 62 | return "SHUTDOWN-COMPLETE" 63 | case ctReconfig: 64 | return "RECONFIG" // Re-configuration 65 | case ctForwardTSN: 66 | return "FORWARD-TSN" 67 | default: 68 | return fmt.Sprintf("Unknown ChunkType: %d", c) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Test 15 | on: 16 | push: 17 | branches: 18 | - master 19 | pull_request: 20 | 21 | jobs: 22 | test: 23 | uses: pion/.goassets/.github/workflows/test.reusable.yml@master 24 | strategy: 25 | matrix: 26 | go: ["1.25", "1.24"] # auto-update/supported-go-version-list 27 | fail-fast: false 28 | with: 29 | go-version: ${{ matrix.go }} 30 | secrets: inherit 31 | 32 | test-i386: 33 | uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master 34 | strategy: 35 | matrix: 36 | go: ["1.25", "1.24"] # auto-update/supported-go-version-list 37 | fail-fast: false 38 | with: 39 | go-version: ${{ matrix.go }} 40 | 41 | test-windows: 42 | uses: pion/.goassets/.github/workflows/test-windows.reusable.yml@master 43 | strategy: 44 | matrix: 45 | go: ["1.25", "1.24"] # auto-update/supported-go-version-list 46 | fail-fast: false 47 | with: 48 | go-version: ${{ matrix.go }} 49 | 50 | test-macos: 51 | uses: pion/.goassets/.github/workflows/test-macos.reusable.yml@master 52 | strategy: 53 | matrix: 54 | go: ["1.25", "1.24"] # auto-update/supported-go-version-list 55 | fail-fast: false 56 | with: 57 | go-version: ${{ matrix.go }} 58 | 59 | test-wasm: 60 | uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master 61 | with: 62 | go-version: "1.25" # auto-update/latest-go-version 63 | secrets: inherit 64 | -------------------------------------------------------------------------------- /examples/ping-pong/pong/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package main 5 | 6 | import ( 7 | "fmt" 8 | "log" 9 | "net" 10 | "time" 11 | 12 | "github.com/pion/logging" 13 | "github.com/pion/sctp" 14 | ) 15 | 16 | func main() { //nolint:cyclop 17 | addr := net.UDPAddr{ 18 | IP: net.IPv4(127, 0, 0, 1), 19 | Port: 9899, 20 | } 21 | 22 | conn, err := net.ListenUDP("udp", &addr) 23 | if err != nil { 24 | log.Panic(err) 25 | } 26 | defer func() { 27 | if closeErr := conn.Close(); closeErr != nil { 28 | log.Panic(closeErr) 29 | } 30 | }() 31 | fmt.Println("created a udp listener") 32 | 33 | config := sctp.Config{ 34 | NetConn: &disconnectedPacketConn{pConn: conn}, 35 | LoggerFactory: logging.NewDefaultLoggerFactory(), 36 | } 37 | a, err := sctp.Server(config) 38 | if err != nil { 39 | log.Panic(err) 40 | } 41 | defer func() { 42 | if closeErr := a.Close(); closeErr != nil { 43 | log.Panic(closeErr) 44 | } 45 | }() 46 | defer fmt.Println("created a server") 47 | 48 | stream, err := a.AcceptStream() 49 | if err != nil { 50 | log.Panic(err) 51 | } 52 | defer func() { 53 | if closeErr := stream.Close(); closeErr != nil { 54 | log.Panic(closeErr) 55 | } 56 | }() 57 | fmt.Println("accepted a stream") 58 | 59 | // set unordered = true and 10ms treshold for dropping packets 60 | stream.SetReliabilityParams(true, sctp.ReliabilityTypeTimed, 10) 61 | var pongSeqNum int 62 | for { 63 | buff := make([]byte, 1024) 64 | _, err = stream.Read(buff) 65 | if err != nil { 66 | log.Panic(err) 67 | } 68 | pingMsg := string(buff) 69 | fmt.Println("received:", pingMsg) 70 | 71 | _, err = fmt.Sscanf(pingMsg, "ping %d", &pongSeqNum) 72 | if err != nil { 73 | log.Panic(err) 74 | } 75 | 76 | pongMsg := fmt.Sprintf("pong %d", pongSeqNum) 77 | _, err = stream.Write([]byte(pongMsg)) 78 | if err != nil { 79 | log.Panic(err) 80 | } 81 | fmt.Println("sent:", pongMsg) 82 | 83 | time.Sleep(time.Second) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /chunk_shutdown.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | /* 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 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+. 22 | */ 23 | type chunkShutdown struct { 24 | chunkHeader 25 | cumulativeTSNAck uint32 26 | } 27 | 28 | const ( 29 | cumulativeTSNAckLength = 4 30 | ) 31 | 32 | // Shutdown chunk errors. 33 | var ( 34 | ErrInvalidChunkSize = errors.New("invalid chunk size") 35 | ErrChunkTypeNotShutdown = errors.New("ChunkType is not of type SHUTDOWN") 36 | ) 37 | 38 | func (c *chunkShutdown) unmarshal(raw []byte) error { 39 | if err := c.chunkHeader.unmarshal(raw); err != nil { 40 | return err 41 | } 42 | 43 | if c.typ != ctShutdown { 44 | return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotShutdown, c.typ.String()) 45 | } 46 | 47 | if len(c.raw) != cumulativeTSNAckLength { 48 | return ErrInvalidChunkSize 49 | } 50 | 51 | c.cumulativeTSNAck = binary.BigEndian.Uint32(c.raw[0:]) 52 | 53 | return nil 54 | } 55 | 56 | func (c *chunkShutdown) marshal() ([]byte, error) { 57 | out := make([]byte, cumulativeTSNAckLength) 58 | binary.BigEndian.PutUint32(out[0:], c.cumulativeTSNAck) 59 | 60 | c.typ = ctShutdown 61 | c.raw = out 62 | 63 | return c.chunkHeader.marshal() 64 | } 65 | 66 | func (c *chunkShutdown) check() (abort bool, err error) { 67 | return false, nil 68 | } 69 | 70 | // String makes chunkShutdown printable. 71 | func (c *chunkShutdown) String() string { 72 | return c.chunkHeader.String() 73 | } 74 | -------------------------------------------------------------------------------- /error_cause_protocol_violation.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | This error cause MAY be included in ABORT chunks that are sent 13 | because an SCTP endpoint detects a protocol violation of the peer 14 | that is not covered by the error causes described in Section 3.3.10.1 15 | to Section 3.3.10.12. An implementation MAY provide additional 16 | information specifying what kind of protocol violation has been 17 | detected. 18 | 19 | 0 1 2 3 20 | 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 21 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 22 | | Cause Code=13 | Cause Length=Variable | 23 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 24 | / Additional Information / 25 | \ \ 26 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 27 | */ 28 | type errorCauseProtocolViolation struct { 29 | errorCauseHeader 30 | additionalInformation []byte 31 | } 32 | 33 | // Abort chunk errors. 34 | var ( 35 | ErrProtocolViolationUnmarshal = errors.New("unable to unmarshal Protocol Violation error") 36 | ) 37 | 38 | func (e *errorCauseProtocolViolation) marshal() ([]byte, error) { 39 | e.raw = e.additionalInformation 40 | 41 | return e.errorCauseHeader.marshal() 42 | } 43 | 44 | func (e *errorCauseProtocolViolation) unmarshal(raw []byte) error { 45 | err := e.errorCauseHeader.unmarshal(raw) 46 | if err != nil { 47 | return fmt.Errorf("%w: %v", ErrProtocolViolationUnmarshal, err) //nolint:errorlint 48 | } 49 | 50 | e.additionalInformation = e.raw 51 | 52 | return nil 53 | } 54 | 55 | // String makes errorCauseProtocolViolation printable. 56 | func (e *errorCauseProtocolViolation) String() string { 57 | return fmt.Sprintf("%s: %s", e.errorCauseHeader, e.additionalInformation) 58 | } 59 | -------------------------------------------------------------------------------- /chunk_reconfig_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestChunkReconfig_Success(t *testing.T) { 13 | tt := []struct { 14 | binary []byte 15 | }{ 16 | { 17 | // Note: chunk trailing padding is added in packet.marshal 18 | append( 19 | []byte{0x82, 0x0, 0x0, 0x1a}, testChunkReconfigParamA()..., 20 | ), 21 | }, 22 | {append([]byte{0x82, 0x0, 0x0, 0x14}, testChunkReconfigParamB()...)}, 23 | {append([]byte{0x82, 0x0, 0x0, 0x10}, testChunkReconfigResponce()...)}, 24 | { 25 | append( 26 | append([]byte{0x82, 0x0, 0x0, 0x2c}, padByte(testChunkReconfigParamA(), 2)...), 27 | testChunkReconfigParamB()...), 28 | }, 29 | { 30 | // Note: chunk trailing padding is added in packet.marshal 31 | append( 32 | append([]byte{0x82, 0x0, 0x0, 0x2a}, 33 | testChunkReconfigParamB()...), 34 | testChunkReconfigParamA()..., 35 | ), 36 | }, 37 | } 38 | 39 | for i, tc := range tt { 40 | actual := &chunkReconfig{} 41 | err := actual.unmarshal(tc.binary) 42 | assert.NoErrorf(t, err, "failed to unmarshal #%d: %v", i) 43 | 44 | b, err := actual.marshal() 45 | assert.NoError(t, err) 46 | assert.Equalf(t, tc.binary, b, "test %d not equal", i) 47 | } 48 | } 49 | 50 | func TestChunkReconfigUnmarshal_Failure(t *testing.T) { 51 | tt := []struct { 52 | name string 53 | binary []byte 54 | }{ 55 | {"chunk header to short", []byte{0x82}}, 56 | {"missing parse param type (A)", []byte{0x82, 0x0, 0x0, 0x4}}, 57 | {"wrong param (A)", []byte{0x82, 0x0, 0x0, 0x8, 0x0, 0xd, 0x0, 0x0}}, 58 | { 59 | "wrong param (B)", 60 | append(append([]byte{0x82, 0x0, 0x0, 0x18}, 61 | testChunkReconfigParamB()...), 62 | []byte{0x0, 0xd, 0x0, 0x0}...), 63 | }, 64 | } 65 | 66 | for i, tc := range tt { 67 | actual := &chunkReconfig{} 68 | err := actual.unmarshal(tc.binary) 69 | assert.Errorf(t, err, "expected unmarshal #%d: '%s' to fail.", i, tc.name) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 4 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 5 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 6 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 7 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 8 | github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= 9 | github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= 10 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 11 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 12 | github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= 13 | github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= 14 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 17 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 18 | golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= 19 | golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= 20 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 21 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 22 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 24 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 25 | -------------------------------------------------------------------------------- /windowedmin.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "sort" 8 | "time" 9 | ) 10 | 11 | // windowedMin maintains a monotonic deque of (time,value) to answer 12 | // the minimum over a sliding window efficiently. 13 | // Not thread-safe; caller must synchronize (Association already does). 14 | type windowedMin struct { 15 | rackMinRTTWnd time.Duration 16 | deque []entry 17 | } 18 | 19 | type entry struct { 20 | t time.Time 21 | v time.Duration 22 | } 23 | 24 | func newWindowedMin(window time.Duration) *windowedMin { 25 | if window <= 0 { 26 | window = 30 * time.Second 27 | } 28 | 29 | return &windowedMin{rackMinRTTWnd: window} 30 | } 31 | 32 | // prune removes elements older than (now - wnd). 33 | func (window *windowedMin) prune(now time.Time) { 34 | if len(window.deque) == 0 { 35 | return 36 | } 37 | 38 | cutoff := now.Add(-window.rackMinRTTWnd) 39 | 40 | firstValidTSAfterCutoff := sort.Search(len(window.deque), func(i int) bool { 41 | return !window.deque[i].t.Before(cutoff) // no builtin func for >= cutoff time 42 | }) 43 | 44 | if firstValidTSAfterCutoff > 0 { 45 | window.deque = window.deque[firstValidTSAfterCutoff:] 46 | } 47 | } 48 | 49 | // Push inserts a new sample and preserves monotonic non-increasing values. 50 | // It maintains minimum values by removing larger entries. 51 | func (window *windowedMin) Push(now time.Time, v time.Duration) { 52 | window.prune(now) 53 | 54 | for i := len(window.deque); i > 0 && window.deque[i-1].v >= v; i-- { 55 | window.deque = window.deque[:i-1] 56 | } 57 | 58 | window.deque = append( 59 | window.deque, 60 | entry{ 61 | t: now, 62 | v: v, 63 | }, 64 | ) 65 | } 66 | 67 | // Min returns the minimum value in the current window or 0 if empty. 68 | func (window *windowedMin) Min(now time.Time) time.Duration { 69 | window.prune(now) 70 | 71 | if len(window.deque) == 0 { 72 | return 0 73 | } 74 | 75 | return window.deque[0].v 76 | } 77 | 78 | // Len is only for tests/diagnostics. 79 | func (window *windowedMin) Len() int { 80 | return len(window.deque) 81 | } 82 | -------------------------------------------------------------------------------- /param_requested_hmac_algorithm.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | type hmacAlgorithm uint16 13 | 14 | const ( 15 | hmacResv1 hmacAlgorithm = 0 16 | hmacSHA128 hmacAlgorithm = 1 17 | hmacResv2 hmacAlgorithm = 2 18 | hmacSHA256 hmacAlgorithm = 3 19 | ) 20 | 21 | // ErrInvalidAlgorithmType is returned if unknown auth algorithm is specified. 22 | var ErrInvalidAlgorithmType = errors.New("invalid algorithm type") 23 | 24 | // ErrInvalidChunkLength is returned if the chunk length is invalid. 25 | var ErrInvalidChunkLength = errors.New("invalid chunk length") 26 | 27 | func (c hmacAlgorithm) String() string { 28 | switch c { 29 | case hmacResv1: 30 | return "HMAC Reserved (0x00)" 31 | case hmacSHA128: 32 | return "HMAC SHA-128" 33 | case hmacResv2: 34 | return "HMAC Reserved (0x02)" 35 | case hmacSHA256: 36 | return "HMAC SHA-256" 37 | default: 38 | return fmt.Sprintf("Unknown HMAC Algorithm type: %d", c) 39 | } 40 | } 41 | 42 | type paramRequestedHMACAlgorithm struct { 43 | paramHeader 44 | availableAlgorithms []hmacAlgorithm 45 | } 46 | 47 | func (r *paramRequestedHMACAlgorithm) marshal() ([]byte, error) { 48 | r.typ = reqHMACAlgo 49 | r.raw = make([]byte, len(r.availableAlgorithms)*2) 50 | i := 0 51 | for _, a := range r.availableAlgorithms { 52 | binary.BigEndian.PutUint16(r.raw[i:], uint16(a)) 53 | i += 2 54 | } 55 | 56 | return r.paramHeader.marshal() 57 | } 58 | 59 | func (r *paramRequestedHMACAlgorithm) unmarshal(raw []byte) (param, error) { 60 | err := r.paramHeader.unmarshal(raw) 61 | if err != nil { 62 | return nil, err 63 | } 64 | if len(r.raw)%2 == 1 { 65 | return nil, ErrInvalidChunkLength 66 | } 67 | 68 | i := 0 69 | for i < len(r.raw) { 70 | a := hmacAlgorithm(binary.BigEndian.Uint16(r.raw[i:])) 71 | switch a { 72 | case hmacSHA128: 73 | fallthrough 74 | case hmacSHA256: 75 | r.availableAlgorithms = append(r.availableAlgorithms, a) 76 | default: 77 | return nil, fmt.Errorf("%w: %v", ErrInvalidAlgorithmType, a) 78 | } 79 | 80 | i += 2 81 | } 82 | 83 | return r, nil 84 | } 85 | -------------------------------------------------------------------------------- /examples/ping-pong/ping/conn.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package main implements a simple ping-pong example 5 | package main 6 | 7 | import ( 8 | "net" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // Reference: https://github.com/pion/sctp/blob/master/association_test.go 14 | // Since UDP is connectionless, as a server, it doesn't know how to reply 15 | // simply using the `Write` method. So, to make it work, `disconnectedPacketConn` 16 | // will infer the last packet that it reads as the reply address for `Write` 17 | 18 | type disconnectedPacketConn struct { // nolint: unused 19 | mu sync.RWMutex 20 | rAddr net.Addr 21 | pConn net.PacketConn 22 | } 23 | 24 | // Read. 25 | func (c *disconnectedPacketConn) Read(p []byte) (int, error) { //nolint:unused 26 | i, rAddr, err := c.pConn.ReadFrom(p) 27 | if err != nil { 28 | return 0, err 29 | } 30 | 31 | c.mu.Lock() 32 | c.rAddr = rAddr 33 | c.mu.Unlock() 34 | 35 | return i, err 36 | } 37 | 38 | // Write writes len(p) bytes from p to the DTLS connection. 39 | func (c *disconnectedPacketConn) Write(p []byte) (n int, err error) { //nolint:unused 40 | return c.pConn.WriteTo(p, c.RemoteAddr()) 41 | } 42 | 43 | // Close closes the conn and releases any Read calls. 44 | func (c *disconnectedPacketConn) Close() error { //nolint:unused 45 | return c.pConn.Close() 46 | } 47 | 48 | // LocalAddr is a stub. 49 | func (c *disconnectedPacketConn) LocalAddr() net.Addr { //nolint:unused 50 | if c.pConn != nil { 51 | return c.pConn.LocalAddr() 52 | } 53 | 54 | return nil 55 | } 56 | 57 | // RemoteAddr is a stub. 58 | func (c *disconnectedPacketConn) RemoteAddr() net.Addr { //nolint:unused 59 | c.mu.RLock() 60 | defer c.mu.RUnlock() 61 | 62 | return c.rAddr 63 | } 64 | 65 | // SetDeadline is a stub. 66 | func (c *disconnectedPacketConn) SetDeadline(time.Time) error { //nolint:unused 67 | return nil 68 | } 69 | 70 | // SetReadDeadline is a stub. 71 | func (c *disconnectedPacketConn) SetReadDeadline(time.Time) error { //nolint:unused 72 | return nil 73 | } 74 | 75 | // SetWriteDeadline is a stub. 76 | func (c *disconnectedPacketConn) SetWriteDeadline(time.Time) error { //nolint:unused 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /examples/ping-pong/pong/conn.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package main implements a simple ping-pong example 5 | package main 6 | 7 | import ( 8 | "net" 9 | "sync" 10 | "time" 11 | ) 12 | 13 | // Reference: https://github.com/pion/sctp/blob/master/association_test.go 14 | // Since UDP is connectionless, as a server, it doesn't know how to reply 15 | // simply using the `Write` method. So, to make it work, `disconnectedPacketConn` 16 | // will infer the last packet that it reads as the reply address for `Write` 17 | 18 | type disconnectedPacketConn struct { // nolint: unused 19 | mu sync.RWMutex 20 | rAddr net.Addr 21 | pConn net.PacketConn 22 | } 23 | 24 | // Read. 25 | func (c *disconnectedPacketConn) Read(p []byte) (int, error) { //nolint:unused 26 | i, rAddr, err := c.pConn.ReadFrom(p) 27 | if err != nil { 28 | return 0, err 29 | } 30 | 31 | c.mu.Lock() 32 | c.rAddr = rAddr 33 | c.mu.Unlock() 34 | 35 | return i, err 36 | } 37 | 38 | // Write writes len(p) bytes from p to the DTLS connection. 39 | func (c *disconnectedPacketConn) Write(p []byte) (n int, err error) { //nolint:unused 40 | return c.pConn.WriteTo(p, c.RemoteAddr()) 41 | } 42 | 43 | // Close closes the conn and releases any Read calls. 44 | func (c *disconnectedPacketConn) Close() error { //nolint:unused 45 | return c.pConn.Close() 46 | } 47 | 48 | // LocalAddr is a stub. 49 | func (c *disconnectedPacketConn) LocalAddr() net.Addr { //nolint:unused 50 | if c.pConn != nil { 51 | return c.pConn.LocalAddr() 52 | } 53 | 54 | return nil 55 | } 56 | 57 | // RemoteAddr is a stub. 58 | func (c *disconnectedPacketConn) RemoteAddr() net.Addr { //nolint:unused 59 | c.mu.RLock() 60 | defer c.mu.RUnlock() 61 | 62 | return c.rAddr 63 | } 64 | 65 | // SetDeadline is a stub. 66 | func (c *disconnectedPacketConn) SetDeadline(time.Time) error { //nolint:unused 67 | return nil 68 | } 69 | 70 | // SetReadDeadline is a stub. 71 | func (c *disconnectedPacketConn) SetReadDeadline(time.Time) error { //nolint:unused 72 | return nil 73 | } 74 | 75 | // SetWriteDeadline is a stub. 76 | func (c *disconnectedPacketConn) SetWriteDeadline(time.Time) error { //nolint:unused 77 | return nil 78 | } 79 | -------------------------------------------------------------------------------- /windowedmin_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestWindowedMin_Basic(t *testing.T) { 14 | window := newWindowedMin(100 * time.Millisecond) 15 | base := time.Unix(0, 0) 16 | 17 | window.Push(base, 30*time.Millisecond) 18 | assert.Equal(t, 30*time.Millisecond, window.Min(base)) 19 | 20 | window.Push(base.Add(10*time.Millisecond), 20*time.Millisecond) 21 | assert.Equal(t, 20*time.Millisecond, window.Min(base.Add(10*time.Millisecond))) 22 | 23 | // larger value shouldn't change min 24 | window.Push(base.Add(20*time.Millisecond), 40*time.Millisecond) 25 | assert.Equal(t, 20*time.Millisecond, window.Min(base.Add(20*time.Millisecond))) 26 | 27 | // decreasing again shrinks min 28 | window.Push(base.Add(30*time.Millisecond), 10*time.Millisecond) 29 | assert.Equal(t, 10*time.Millisecond, window.Min(base.Add(30*time.Millisecond))) 30 | } 31 | 32 | func TestWindowedMin_WindowExpiry(t *testing.T) { 33 | window := newWindowedMin(50 * time.Millisecond) 34 | base := time.Unix(0, 0) 35 | 36 | window.Push(base, 10*time.Millisecond) // t=0 37 | window.Push(base.Add(10*time.Millisecond), 20*time.Millisecond) // t=10ms 38 | 39 | // at t=60ms, first sample is expired, m becomes 20ms 40 | m := window.Min(base.Add(60 * time.Millisecond)) 41 | assert.Equal(t, 20*time.Millisecond, m) 42 | 43 | // at t=200ms, all are expired -> 0 44 | m = window.Min(base.Add(200 * time.Millisecond)) 45 | assert.Zero(t, m) 46 | } 47 | 48 | func TestWindowedMin_EqualValues(t *testing.T) { 49 | window := newWindowedMin(1 * time.Second) 50 | base := time.Unix(0, 0) 51 | 52 | window.Push(base, 15*time.Millisecond) 53 | window.Push(base.Add(1*time.Millisecond), 15*time.Millisecond) 54 | 55 | assert.Equal(t, 1, window.Len()) 56 | assert.Equal(t, 15*time.Millisecond, window.Min(base.Add(2*time.Millisecond))) 57 | } 58 | 59 | func TestWindowedMin_DefaultWindow30s(t *testing.T) { 60 | zeroWnd := newWindowedMin(0) 61 | negativeWnd := newWindowedMin(-5 * time.Second) 62 | 63 | assert.Equal(t, 30*time.Second, zeroWnd.rackMinRTTWnd) 64 | assert.Equal(t, 30*time.Second, negativeWnd.rackMinRTTWnd) 65 | } 66 | -------------------------------------------------------------------------------- /chunk_error_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "reflect" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestChunkErrorUnrecognizedChunkType(t *testing.T) { 14 | const chunkFlags byte = 0x00 15 | orgUnrecognizedChunk := []byte{0xc0, 0x0, 0x0, 0x8, 0x0, 0x0, 0x0, 0x3} 16 | rawIn := append([]byte{byte(ctError), chunkFlags, 0x00, 0x10, 0x00, 0x06, 0x00, 0x0c}, orgUnrecognizedChunk...) 17 | 18 | t.Run("unmarshal", func(t *testing.T) { 19 | c := &chunkError{} 20 | err := c.unmarshal(rawIn) 21 | assert.Nil(t, err, "unmarshal should succeed") 22 | assert.Equal(t, ctError, c.typ, "chunk type should be ERROR") 23 | assert.Equal(t, 1, len(c.errorCauses), "there should be on errorCause") 24 | 25 | ec := c.errorCauses[0] 26 | assert.Equal(t, unrecognizedChunkType, ec.errorCauseCode(), "cause code should be unrecognizedChunkType") 27 | ecUnrecognizedChunkType, ok := ec.(*errorCauseUnrecognizedChunkType) 28 | assert.True(t, ok) 29 | unrecognizedChunk := ecUnrecognizedChunkType.unrecognizedChunk 30 | assert.True(t, reflect.DeepEqual(unrecognizedChunk, orgUnrecognizedChunk), "should have valid unrecognizedChunk") 31 | }) 32 | 33 | t.Run("marshal", func(t *testing.T) { 34 | ecUnrecognizedChunkType := &errorCauseUnrecognizedChunkType{ 35 | unrecognizedChunk: orgUnrecognizedChunk, 36 | } 37 | 38 | ec := &chunkError{ 39 | errorCauses: []errorCause{ 40 | errorCause(ecUnrecognizedChunkType), 41 | }, 42 | } 43 | 44 | raw, err := ec.marshal() 45 | assert.Nil(t, err, "marshal should succeed") 46 | assert.True(t, reflect.DeepEqual(raw, rawIn), "unexpected serialization result") 47 | }) 48 | 49 | t.Run("marshal with cause value being nil", func(t *testing.T) { 50 | expected := []byte{byte(ctError), chunkFlags, 0x00, 0x08, 0x00, 0x06, 0x00, 0x04} 51 | ecUnrecognizedChunkType := &errorCauseUnrecognizedChunkType{} 52 | 53 | ec := &chunkError{ 54 | errorCauses: []errorCause{ 55 | errorCause(ecUnrecognizedChunkType), 56 | }, 57 | } 58 | 59 | raw, err := ec.marshal() 60 | assert.Nil(t, err, "marshal should succeed") 61 | assert.True(t, reflect.DeepEqual(raw, expected), "unexpected serialization result") 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /param_outgoing_reset_request_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func testChunkReconfigParamA() []byte { 13 | return []byte{ 14 | 0x00, 0x0d, 0x00, 0x16, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 15 | 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x04, 0x00, 0x05, 0x00, 0x06, 16 | } 17 | } 18 | 19 | func testChunkReconfigParamB() []byte { 20 | return []byte{0x0, 0xd, 0x0, 0x10, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x3} 21 | } 22 | 23 | func TestParamOutgoingResetRequest_Success(t *testing.T) { 24 | tt := []struct { 25 | binary []byte 26 | parsed *paramOutgoingResetRequest 27 | }{ 28 | { 29 | testChunkReconfigParamA(), 30 | ¶mOutgoingResetRequest{ 31 | paramHeader: paramHeader{ 32 | typ: outSSNResetReq, 33 | len: 22, 34 | raw: testChunkReconfigParamA()[4:], 35 | }, 36 | reconfigRequestSequenceNumber: 1, 37 | reconfigResponseSequenceNumber: 2, 38 | senderLastTSN: 3, 39 | streamIdentifiers: []uint16{4, 5, 6}, 40 | }, 41 | }, 42 | { 43 | testChunkReconfigParamB(), 44 | ¶mOutgoingResetRequest{ 45 | paramHeader: paramHeader{ 46 | typ: outSSNResetReq, 47 | len: 16, 48 | raw: testChunkReconfigParamB()[4:], 49 | }, 50 | reconfigRequestSequenceNumber: 1, 51 | reconfigResponseSequenceNumber: 2, 52 | senderLastTSN: 3, 53 | streamIdentifiers: []uint16{}, 54 | }, 55 | }, 56 | } 57 | 58 | for i, tc := range tt { 59 | actual := ¶mOutgoingResetRequest{} 60 | _, err := actual.unmarshal(tc.binary) 61 | assert.NoErrorf(t, err, "failed to unmarshal #%d", i) 62 | assert.Equal(t, tc.parsed, actual) 63 | 64 | b, err := actual.marshal() 65 | assert.NoErrorf(t, err, "failed to marshal #%d", i) 66 | assert.Equal(t, tc.binary, b) 67 | } 68 | } 69 | 70 | func TestParamOutgoingResetRequest_Failure(t *testing.T) { 71 | tt := []struct { 72 | name string 73 | binary []byte 74 | }{ 75 | {"packet too short", testChunkReconfigParamA()[:8]}, 76 | {"param too short", []byte{0x0, 0xd, 0x0, 0x4}}, 77 | } 78 | 79 | for i, tc := range tt { 80 | actual := ¶mOutgoingResetRequest{} 81 | _, err := actual.unmarshal(tc.binary) 82 | assert.Errorf(t, err, "expected unmarshal #%d: '%s' to fail.", i, tc.name) 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /param_reconfig_response_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func testChunkReconfigResponce() []byte { 13 | return []byte{0x0, 0x10, 0x0, 0xc, 0x0, 0x0, 0x0, 0x1, 0x0, 0x0, 0x0, 0x1} 14 | } 15 | 16 | func TestParamReconfigResponse_Success(t *testing.T) { 17 | tt := []struct { 18 | binary []byte 19 | parsed *paramReconfigResponse 20 | }{ 21 | { 22 | testChunkReconfigResponce(), 23 | ¶mReconfigResponse{ 24 | paramHeader: paramHeader{ 25 | typ: reconfigResp, 26 | len: 12, 27 | raw: testChunkReconfigResponce()[4:], 28 | }, 29 | reconfigResponseSequenceNumber: 1, 30 | result: reconfigResultSuccessPerformed, 31 | }, 32 | }, 33 | } 34 | 35 | for i, tc := range tt { 36 | actual := ¶mReconfigResponse{} 37 | _, err := actual.unmarshal(tc.binary) 38 | assert.NoErrorf(t, err, "failed to unmarshal #%d: %v", i) 39 | assert.Equal(t, tc.parsed, actual) 40 | 41 | b, err := actual.marshal() 42 | assert.NoErrorf(t, err, "failed to marshal #%d: %v", i) 43 | assert.Equal(t, tc.binary, b) 44 | } 45 | } 46 | 47 | func TestParamReconfigResponse_Failure(t *testing.T) { 48 | tt := []struct { 49 | name string 50 | binary []byte 51 | }{ 52 | {"packet too short", testChunkReconfigParamA()[:8]}, 53 | {"param too short", []byte{0x0, 0x10, 0x0, 0x4}}, 54 | } 55 | 56 | for i, tc := range tt { 57 | actual := ¶mReconfigResponse{} 58 | _, err := actual.unmarshal(tc.binary) 59 | assert.Errorf(t, err, "expected unmarshal #%d: '%s' to fail.", i, tc.name) 60 | } 61 | } 62 | 63 | func TestReconfigResultStringer(t *testing.T) { 64 | tt := []struct { 65 | result reconfigResult 66 | expected string 67 | }{ 68 | {reconfigResultSuccessNOP, "0: Success - Nothing to do"}, 69 | {reconfigResultSuccessPerformed, "1: Success - Performed"}, 70 | {reconfigResultDenied, "2: Denied"}, 71 | {reconfigResultErrorWrongSSN, "3: Error - Wrong SSN"}, 72 | {reconfigResultErrorRequestAlreadyInProgress, "4: Error - Request already in progress"}, 73 | {reconfigResultErrorBadSequenceNumber, "5: Error - Bad Sequence Number"}, 74 | {reconfigResultInProgress, "6: In progress"}, 75 | } 76 | 77 | for i, tc := range tt { 78 | actual := tc.result.String() 79 | assert.Equalf(t, tc.expected, actual, "Test case %d", i) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /association_stats.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "sync/atomic" 8 | ) 9 | 10 | type associationStats struct { 11 | nPacketsReceived uint64 12 | nPacketsSent uint64 13 | nDATAs uint64 14 | nSACKsReceived uint64 15 | nSACKsSent uint64 16 | nT3Timeouts uint64 17 | nAckTimeouts uint64 18 | nFastRetrans uint64 19 | } 20 | 21 | func (s *associationStats) incPacketsReceived() { 22 | atomic.AddUint64(&s.nPacketsReceived, 1) 23 | } 24 | 25 | func (s *associationStats) getNumPacketsReceived() uint64 { 26 | return atomic.LoadUint64(&s.nPacketsReceived) 27 | } 28 | 29 | func (s *associationStats) incPacketsSent() { 30 | atomic.AddUint64(&s.nPacketsSent, 1) 31 | } 32 | 33 | func (s *associationStats) getNumPacketsSent() uint64 { 34 | return atomic.LoadUint64(&s.nPacketsSent) 35 | } 36 | 37 | func (s *associationStats) incDATAs() { 38 | atomic.AddUint64(&s.nDATAs, 1) 39 | } 40 | 41 | func (s *associationStats) getNumDATAs() uint64 { 42 | return atomic.LoadUint64(&s.nDATAs) 43 | } 44 | 45 | func (s *associationStats) incSACKsReceived() { 46 | atomic.AddUint64(&s.nSACKsReceived, 1) 47 | } 48 | 49 | func (s *associationStats) getNumSACKsReceived() uint64 { 50 | return atomic.LoadUint64(&s.nSACKsReceived) 51 | } 52 | 53 | func (s *associationStats) incSACKsSent() { 54 | atomic.AddUint64(&s.nSACKsSent, 1) 55 | } 56 | 57 | func (s *associationStats) getNumSACKsSent() uint64 { 58 | return atomic.LoadUint64(&s.nSACKsSent) 59 | } 60 | 61 | func (s *associationStats) incT3Timeouts() { 62 | atomic.AddUint64(&s.nT3Timeouts, 1) 63 | } 64 | 65 | func (s *associationStats) getNumT3Timeouts() uint64 { 66 | return atomic.LoadUint64(&s.nT3Timeouts) 67 | } 68 | 69 | func (s *associationStats) incAckTimeouts() { 70 | atomic.AddUint64(&s.nAckTimeouts, 1) 71 | } 72 | 73 | func (s *associationStats) getNumAckTimeouts() uint64 { 74 | return atomic.LoadUint64(&s.nAckTimeouts) 75 | } 76 | 77 | func (s *associationStats) incFastRetrans() { 78 | atomic.AddUint64(&s.nFastRetrans, 1) 79 | } 80 | 81 | func (s *associationStats) getNumFastRetrans() uint64 { 82 | return atomic.LoadUint64(&s.nFastRetrans) 83 | } 84 | 85 | func (s *associationStats) reset() { 86 | atomic.StoreUint64(&s.nPacketsReceived, 0) 87 | atomic.StoreUint64(&s.nPacketsSent, 0) 88 | atomic.StoreUint64(&s.nDATAs, 0) 89 | atomic.StoreUint64(&s.nSACKsReceived, 0) 90 | atomic.StoreUint64(&s.nSACKsSent, 0) 91 | atomic.StoreUint64(&s.nT3Timeouts, 0) 92 | atomic.StoreUint64(&s.nAckTimeouts, 0) 93 | atomic.StoreUint64(&s.nFastRetrans, 0) 94 | } 95 | -------------------------------------------------------------------------------- /stream_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pion/logging" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestSessionBufferedAmount(t *testing.T) { 14 | t.Run("bufferedAmount", func(t *testing.T) { 15 | s := &Stream{ 16 | log: logging.NewDefaultLoggerFactory().NewLogger("sctp-test"), 17 | } 18 | 19 | assert.Equal(t, uint64(0), s.BufferedAmount()) 20 | assert.Equal(t, uint64(0), s.BufferedAmountLowThreshold()) 21 | 22 | s.bufferedAmount = 8192 23 | s.SetBufferedAmountLowThreshold(2048) 24 | assert.Equal(t, uint64(8192), s.BufferedAmount(), "unexpected bufferedAmount") 25 | assert.Equal(t, uint64(2048), s.BufferedAmountLowThreshold(), "unexpected threshold") 26 | }) 27 | 28 | t.Run("OnBufferedAmountLow", func(t *testing.T) { 29 | stream := &Stream{ 30 | log: logging.NewDefaultLoggerFactory().NewLogger("sctp-test"), 31 | } 32 | 33 | stream.bufferedAmount = 4096 34 | stream.SetBufferedAmountLowThreshold(2048) 35 | 36 | nCbs := 0 37 | 38 | stream.OnBufferedAmountLow(func() { 39 | nCbs++ 40 | }) 41 | 42 | // Negative value should be ignored (by design) 43 | stream.onBufferReleased(-32) // bufferedAmount = 3072 44 | assert.Equal(t, uint64(4096), stream.BufferedAmount(), "unexpected bufferedAmount") 45 | assert.Equal(t, 0, nCbs, "callback count mismatch") 46 | 47 | // Above to above, no callback 48 | stream.onBufferReleased(1024) // bufferedAmount = 3072 49 | assert.Equal(t, uint64(3072), stream.BufferedAmount(), "unexpected bufferedAmount") 50 | assert.Equal(t, 0, nCbs, "callback count mismatch") 51 | 52 | // Above to equal, callback should be made 53 | stream.onBufferReleased(1024) // bufferedAmount = 2048 54 | assert.Equal(t, uint64(2048), stream.BufferedAmount(), "unexpected bufferedAmount") 55 | assert.Equal(t, 1, nCbs, "callback count mismatch") 56 | 57 | // Eaual to below, no callback 58 | stream.onBufferReleased(1024) // bufferedAmount = 1024 59 | assert.Equal(t, uint64(1024), stream.BufferedAmount(), "unexpected bufferedAmount") 60 | assert.Equal(t, 1, nCbs, "callback count mismatch") 61 | 62 | // Blow to below, no callback 63 | stream.onBufferReleased(1024) // bufferedAmount = 0 64 | assert.Equal(t, uint64(0), stream.BufferedAmount(), "unexpected bufferedAmount") 65 | assert.Equal(t, 1, nCbs, "callback count mismatch") 66 | 67 | // Capped at 0, no callback 68 | stream.onBufferReleased(1024) // bufferedAmount = 0 69 | assert.Equal(t, uint64(0), stream.BufferedAmount(), "unexpected bufferedAmount") 70 | assert.Equal(t, 1, nCbs, "callback count mismatch") 71 | }) 72 | } 73 | -------------------------------------------------------------------------------- /ack_timer.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "math" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | const ( 13 | ackInterval time.Duration = 200 * time.Millisecond 14 | ) 15 | 16 | // ackTimerObserver is the inteface to an ack timer observer. 17 | type ackTimerObserver interface { 18 | onAckTimeout() 19 | } 20 | 21 | type ackTimerState uint8 22 | 23 | const ( 24 | ackTimerStopped ackTimerState = iota 25 | ackTimerStarted 26 | ackTimerClosed 27 | ) 28 | 29 | // ackTimer provides the retnransmission timer conforms with RFC 4960 Sec 6.3.1. 30 | type ackTimer struct { 31 | timer *time.Timer 32 | observer ackTimerObserver 33 | mutex sync.Mutex 34 | state ackTimerState 35 | pending uint8 36 | } 37 | 38 | // newAckTimer creates a new acknowledgement timer used to enable delayed ack. 39 | func newAckTimer(observer ackTimerObserver) *ackTimer { 40 | t := &ackTimer{observer: observer} 41 | t.timer = time.AfterFunc(math.MaxInt64, t.timeout) 42 | t.timer.Stop() 43 | 44 | return t 45 | } 46 | 47 | func (t *ackTimer) timeout() { 48 | t.mutex.Lock() 49 | if t.pending--; t.pending == 0 && t.state == ackTimerStarted { 50 | t.state = ackTimerStopped 51 | defer t.observer.onAckTimeout() 52 | } 53 | t.mutex.Unlock() 54 | } 55 | 56 | // start starts the timer. 57 | func (t *ackTimer) start() bool { 58 | t.mutex.Lock() 59 | defer t.mutex.Unlock() 60 | 61 | // this timer is already closed or already running 62 | if t.state != ackTimerStopped { 63 | return false 64 | } 65 | 66 | t.state = ackTimerStarted 67 | t.pending++ 68 | t.timer.Reset(ackInterval) 69 | 70 | return true 71 | } 72 | 73 | // stops the timer. this is similar to stop() but subsequent start() call 74 | // will fail (the timer is no longer usable). 75 | func (t *ackTimer) stop() { 76 | t.mutex.Lock() 77 | defer t.mutex.Unlock() 78 | 79 | if t.state == ackTimerStarted { 80 | if t.timer.Stop() { 81 | t.pending-- 82 | } 83 | t.state = ackTimerStopped 84 | } 85 | } 86 | 87 | // closes the timer. this is similar to stop() but subsequent start() call 88 | // will fail (the timer is no longer usable). 89 | func (t *ackTimer) close() { 90 | t.mutex.Lock() 91 | defer t.mutex.Unlock() 92 | 93 | if t.state == ackTimerStarted && t.timer.Stop() { 94 | t.pending-- 95 | } 96 | t.state = ackTimerClosed 97 | } 98 | 99 | // isRunning tests if the timer is running. 100 | // Debug purpose only. 101 | func (t *ackTimer) isRunning() bool { 102 | t.mutex.Lock() 103 | defer t.mutex.Unlock() 104 | 105 | return t.state == ackTimerStarted 106 | } 107 | -------------------------------------------------------------------------------- /packet_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestPacketUnmarshal(t *testing.T) { 13 | pkt := &packet{} 14 | 15 | assert.Error(t, pkt.unmarshal(true, []byte{}), "Unmarshal should fail when a packet is too small to be SCTP") 16 | 17 | headerOnly := []byte{0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0x06, 0xa9, 0x00, 0xe1} 18 | err := pkt.unmarshal(true, headerOnly) 19 | assert.NoError(t, err, "Unmarshal failed for SCTP packet with no chunks") 20 | 21 | assert.Equal(t, uint16(defaultSCTPSrcDstPort), pkt.sourcePort, 22 | "Unmarshal passed for SCTP packet, but got incorrect source port exp: %d act: %d", 23 | defaultSCTPSrcDstPort, pkt.sourcePort) 24 | assert.Equal(t, uint16(defaultSCTPSrcDstPort), pkt.destinationPort, 25 | "Unmarshal passed for SCTP packet, but got incorrect destination port exp: %d act: %d", 26 | defaultSCTPSrcDstPort, pkt.destinationPort) 27 | assert.Equal(t, uint32(0), pkt.verificationTag, 28 | "Unmarshal passed for SCTP packet, but got incorrect verification tag exp: %d act: %d", 29 | 0, pkt.verificationTag) 30 | 31 | rawChunk := []byte{ 32 | 0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0x81, 0x46, 0x9d, 0xfc, 0x01, 0x00, 0x00, 0x56, 0x55, 33 | 0xb9, 0x64, 0xa5, 0x00, 0x02, 0x00, 0x00, 0x04, 0x00, 0x08, 0x00, 0xe8, 0x6d, 0x10, 0x30, 0xc0, 0x00, 34 | 0x00, 0x04, 0x80, 0x08, 0x00, 0x09, 0xc0, 0x0f, 0xc1, 0x80, 0x82, 0x00, 0x00, 0x00, 0x80, 0x02, 0x00, 35 | 0x24, 0x9f, 0xeb, 0xbb, 0x5c, 0x50, 0xc9, 0xbf, 0x75, 0x9c, 0xb1, 0x2c, 0x57, 0x4f, 0xa4, 0x5a, 0x51, 36 | 0xba, 0x60, 0x17, 0x78, 0x27, 0x94, 0x5c, 0x31, 0xe6, 0x5d, 0x5b, 0x09, 0x47, 0xe2, 0x22, 0x06, 0x80, 37 | 0x04, 0x00, 0x06, 0x00, 0x01, 0x00, 0x00, 0x80, 0x03, 0x00, 0x06, 0x80, 0xc1, 0x00, 0x00, 38 | } 39 | 40 | assert.NoError(t, pkt.unmarshal(true, rawChunk)) 41 | } 42 | 43 | func TestPacketMarshal(t *testing.T) { 44 | pkt := &packet{} 45 | 46 | headerOnly := []byte{0x13, 0x88, 0x13, 0x88, 0x00, 0x00, 0x00, 0x00, 0x06, 0xa9, 0x00, 0xe1} 47 | assert.NoError(t, pkt.unmarshal(true, headerOnly), "Unmarshal failed for SCTP packet with no chunks") 48 | 49 | headerOnlyMarshaled, err := pkt.marshal(true) 50 | 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) 54 | } 55 | } 56 | 57 | func BenchmarkPacketGenerateChecksum(b *testing.B) { 58 | var data [1024]byte 59 | 60 | for i := 0; i < b.N; i++ { 61 | _ = generatePacketChecksum(data[:]) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /queue_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "runtime" 8 | "runtime/debug" 9 | "sync/atomic" 10 | "testing" 11 | "time" 12 | 13 | "github.com/stretchr/testify/assert" 14 | ) 15 | 16 | func TestQueue(t *testing.T) { 17 | queu := newQueue[int](32) 18 | assert.Zero(t, queu.Len()) 19 | 20 | // test push & pop 21 | for i := 1; i < 33; i++ { 22 | queu.PushBack(i) 23 | } 24 | assert.Equal(t, 32, queu.Len()) 25 | assert.Equal(t, 5, queu.At(4)) 26 | for i := 1; i < 33; i++ { 27 | assert.Equal(t, i, queu.Front()) 28 | assert.Equal(t, i, queu.PopFront()) 29 | } 30 | assert.Zero(t, queu.Len()) 31 | 32 | queu.PushBack(10) 33 | queu.PushBack(11) 34 | assert.Equal(t, 2, queu.Len()) 35 | assert.Equal(t, 11, queu.At(1)) 36 | assert.Equal(t, 10, queu.Front()) 37 | assert.Equal(t, 10, queu.PopFront()) 38 | assert.Equal(t, 11, queu.PopFront()) 39 | 40 | // test grow capacity 41 | for i := 0; i < 64; i++ { 42 | queu.PushBack(i) 43 | } 44 | assert.Equal(t, 64, queu.Len()) 45 | assert.Equal(t, 2, queu.At(2)) 46 | for i := 0; i < 64; i++ { 47 | assert.Equal(t, i, queu.Front()) 48 | assert.Equal(t, i, queu.PopFront()) 49 | } 50 | } 51 | 52 | // waitForFinalizers spins until at least target have run or timeout hits. 53 | func waitForFinalizers(got *int32, target int32, timeout time.Duration) bool { 54 | deadline := time.Now().Add(timeout) 55 | for time.Now().Before(deadline) { 56 | runtime.GC() 57 | 58 | if atomic.LoadInt32(got) >= target { 59 | return true 60 | } 61 | 62 | time.Sleep(10 * time.Millisecond) 63 | } 64 | 65 | return atomic.LoadInt32(got) >= target 66 | } 67 | 68 | func TestPendingBaseQueuePopReleasesReferences(t *testing.T) { 69 | // Make GC more aggressive for the duration of this test. 70 | prev := debug.SetGCPercent(10) 71 | defer debug.SetGCPercent(prev) 72 | 73 | bufSize := 256 << 10 74 | queue := newPendingBaseQueue() 75 | var finalized int32 76 | 77 | // add 64 chunks, each with a finalizer to count collection. 78 | for i := 0; i < 64; i++ { 79 | c := &chunkPayloadData{ 80 | userData: make([]byte, bufSize), 81 | } 82 | 83 | // count when the chunk struct becomes unreachable. 84 | runtime.SetFinalizer(c, func(*chunkPayloadData) { 85 | atomic.AddInt32(&finalized, 1) 86 | }) 87 | queue.push(c) 88 | } 89 | 90 | // pop 63 chunks so only 1 is left 91 | for i := 0; i < 63; i++ { 92 | queue.pop() 93 | } 94 | 95 | assert.Equal(t, queue.size(), 1) 96 | 97 | wantAtLeast := int32(64 - 4) // wait for GC scheduling 98 | ok := waitForFinalizers(&finalized, wantAtLeast, 3*time.Second) 99 | assert.True(t, ok) 100 | 101 | // Now pop the last element; queue should be empty. 102 | queue.pop() 103 | assert.Equal(t, queue.size(), 0) 104 | } 105 | -------------------------------------------------------------------------------- /chunk_abort.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp // nolint:dupl 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | Abort represents an SCTP Chunk of type ABORT 13 | 14 | The ABORT chunk is sent to the peer of an association to close the 15 | association. The ABORT chunk may contain Cause Parameters to inform 16 | the receiver about the reason of the abort. DATA chunks MUST NOT be 17 | bundled with ABORT. Control chunks (except for INIT, INIT ACK, and 18 | SHUTDOWN COMPLETE) MAY be bundled with an ABORT, but they MUST be 19 | placed before the ABORT in the SCTP packet or they will be ignored by 20 | the receiver. 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 = 6 |Reserved |T| Length | 26 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 27 | | | 28 | | zero or more Error Causes | 29 | | | 30 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 31 | */ 32 | type chunkAbort struct { 33 | chunkHeader 34 | errorCauses []errorCause 35 | } 36 | 37 | // Abort chunk errors. 38 | var ( 39 | ErrChunkTypeNotAbort = errors.New("ChunkType is not of type ABORT") 40 | ErrBuildAbortChunkFailed = errors.New("failed build Abort Chunk") 41 | ) 42 | 43 | func (a *chunkAbort) unmarshal(raw []byte) error { 44 | if err := a.chunkHeader.unmarshal(raw); err != nil { 45 | return err 46 | } 47 | 48 | if a.typ != ctAbort { 49 | return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotAbort, a.typ.String()) 50 | } 51 | 52 | offset := chunkHeaderSize 53 | for len(raw)-offset >= 4 { 54 | e, err := buildErrorCause(raw[offset:]) 55 | if err != nil { 56 | return fmt.Errorf("%w: %v", ErrBuildAbortChunkFailed, err) //nolint:errorlint 57 | } 58 | 59 | offset += int(e.length()) 60 | a.errorCauses = append(a.errorCauses, e) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (a *chunkAbort) marshal() ([]byte, error) { 67 | a.chunkHeader.typ = ctAbort 68 | a.flags = 0x00 69 | a.raw = []byte{} 70 | for _, ec := range a.errorCauses { 71 | raw, err := ec.marshal() 72 | if err != nil { 73 | return nil, err 74 | } 75 | a.raw = append(a.raw, raw...) 76 | } 77 | 78 | return a.chunkHeader.marshal() 79 | } 80 | 81 | func (a *chunkAbort) check() (abort bool, err error) { 82 | return false, nil 83 | } 84 | 85 | // String makes chunkAbort printable. 86 | func (a *chunkAbort) String() string { 87 | res := a.chunkHeader.String() 88 | 89 | for _, cause := range a.errorCauses { 90 | res += fmt.Sprintf("\n - %s", cause) 91 | } 92 | 93 | return res 94 | } 95 | -------------------------------------------------------------------------------- /ack_timer_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | ) 13 | 14 | type onAckTO func() 15 | 16 | type testAckTimerObserver struct { 17 | onAckTO onAckTO 18 | } 19 | 20 | func (o *testAckTimerObserver) onAckTimeout() { 21 | o.onAckTO() 22 | } 23 | 24 | func TestAckTimer(t *testing.T) { 25 | t.Run("start and close", func(t *testing.T) { 26 | var nCbs uint32 27 | rt := newAckTimer(&testAckTimerObserver{ 28 | onAckTO: func() { 29 | t.Log("ack timed out") 30 | atomic.AddUint32(&nCbs, 1) 31 | }, 32 | }) 33 | 34 | for i := 0; i < 2; i++ { 35 | // should start ok 36 | ok := rt.start() 37 | assert.True(t, ok, "start() should succeed") 38 | assert.True(t, rt.isRunning(), "should be running") 39 | 40 | // subsequent start is a noop 41 | ok = rt.start() 42 | assert.False(t, ok, "start() should NOT succeed once closed") 43 | assert.True(t, rt.isRunning(), "should be running") 44 | 45 | // Sleep more than 2 * 200msec interval to test if it times out only once 46 | time.Sleep(ackInterval*2 + 50*time.Millisecond) 47 | 48 | assert.Equalf(t, uint32(1), atomic.LoadUint32(&nCbs), 49 | "should be called once (actual: %d)", atomic.LoadUint32(&nCbs)) 50 | 51 | atomic.StoreUint32(&nCbs, 0) 52 | } 53 | 54 | // should close ok 55 | rt.close() 56 | assert.False(t, rt.isRunning(), "should not be running") 57 | 58 | // once closed, it cannot start 59 | ok := rt.start() 60 | assert.False(t, ok, "start() should NOT succeed once closed") 61 | assert.False(t, rt.isRunning(), "should not be running") 62 | }) 63 | 64 | t.Run("start and stop", func(t *testing.T) { 65 | var nCbs uint32 66 | rt := newAckTimer(&testAckTimerObserver{ 67 | onAckTO: func() { 68 | t.Log("ack timed out") 69 | atomic.AddUint32(&nCbs, 1) 70 | }, 71 | }) 72 | 73 | for i := 0; i < 2; i++ { 74 | // should start ok 75 | ok := rt.start() 76 | assert.True(t, ok, "start() should succeed") 77 | assert.True(t, rt.isRunning(), "should be running") 78 | 79 | // stop immedidately 80 | rt.stop() 81 | assert.False(t, rt.isRunning(), "should not be running") 82 | } 83 | 84 | // Sleep more than 200msec of interval to test if it never times out 85 | time.Sleep(ackInterval + 50*time.Millisecond) 86 | 87 | assert.Equalf(t, uint32(0), atomic.LoadUint32(&nCbs), 88 | "should not be timed out (actual: %d)", atomic.LoadUint32(&nCbs)) 89 | 90 | // can start again 91 | ok := rt.start() 92 | assert.True(t, ok, "start() should succeed again") 93 | assert.True(t, rt.isRunning(), "should be running") 94 | 95 | // should close ok 96 | rt.close() 97 | assert.False(t, rt.isRunning(), "should not be running") 98 | }) 99 | } 100 | -------------------------------------------------------------------------------- /chunk_error.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp // nolint:dupl 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | Operation Error (ERROR) (9) 13 | 14 | An endpoint sends this chunk to its peer endpoint to notify it of 15 | certain error conditions. It contains one or more error causes. An 16 | Operation Error is not considered fatal in and of itself, but may be 17 | used with an ERROR chunk to report a fatal condition. It has the 18 | following parameters: 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 = 9 | Chunk Flags | Length | 24 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 25 | \ \ 26 | / one or more Error Causes / 27 | \ \ 28 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 29 | 30 | Chunk Flags: 8 bits 31 | 32 | Set to 0 on transmit and ignored on receipt. 33 | 34 | Length: 16 bits (unsigned integer) 35 | 36 | Set to the size of the chunk in bytes, including the chunk header 37 | and all the Error Cause fields present. 38 | */ 39 | type chunkError struct { 40 | chunkHeader 41 | errorCauses []errorCause 42 | } 43 | 44 | // Error chunk errors. 45 | var ( 46 | ErrChunkTypeNotCtError = errors.New("ChunkType is not of type ctError") 47 | ErrBuildErrorChunkFailed = errors.New("failed build Error Chunk") 48 | ) 49 | 50 | func (a *chunkError) unmarshal(raw []byte) error { 51 | if err := a.chunkHeader.unmarshal(raw); err != nil { 52 | return err 53 | } 54 | 55 | if a.typ != ctError { 56 | return fmt.Errorf("%w, actually is %s", ErrChunkTypeNotCtError, a.typ.String()) 57 | } 58 | 59 | offset := chunkHeaderSize 60 | for len(raw)-offset >= 4 { 61 | e, err := buildErrorCause(raw[offset:]) 62 | if err != nil { 63 | return fmt.Errorf("%w: %v", ErrBuildErrorChunkFailed, err) //nolint:errorlint 64 | } 65 | 66 | offset += int(e.length()) 67 | a.errorCauses = append(a.errorCauses, e) 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (a *chunkError) marshal() ([]byte, error) { 74 | a.chunkHeader.typ = ctError 75 | a.flags = 0x00 76 | a.raw = []byte{} 77 | for _, ec := range a.errorCauses { 78 | raw, err := ec.marshal() 79 | if err != nil { 80 | return nil, err 81 | } 82 | a.raw = append(a.raw, raw...) 83 | } 84 | 85 | return a.chunkHeader.marshal() 86 | } 87 | 88 | func (a *chunkError) check() (abort bool, err error) { 89 | return false, nil 90 | } 91 | 92 | // String makes chunkError printable. 93 | func (a *chunkError) String() string { 94 | res := a.chunkHeader.String() 95 | 96 | for _, cause := range a.errorCauses { 97 | res += fmt.Sprintf("\n - %s", cause) 98 | } 99 | 100 | return res 101 | } 102 | -------------------------------------------------------------------------------- /paramtype_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "fmt" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestParseParamType_Success(t *testing.T) { 14 | tt := []struct { 15 | binary []byte 16 | expected paramType 17 | }{ 18 | {[]byte{0x0, 0x1}, heartbeatInfo}, 19 | {[]byte{0x0, 0xd}, outSSNResetReq}, 20 | } 21 | 22 | for i, tc := range tt { 23 | pType, err := parseParamType(tc.binary) 24 | assert.NoErrorf(t, err, "failed to parse paramType #%d", i) 25 | assert.Equal(t, tc.expected, pType) 26 | } 27 | } 28 | 29 | func TestParseParamType_Failure(t *testing.T) { 30 | tt := []struct { 31 | name string 32 | binary []byte 33 | }{ 34 | {"empty packet", []byte{}}, 35 | } 36 | 37 | for i, tc := range tt { 38 | _, err := parseParamType(tc.binary) 39 | assert.Errorf(t, err, "expected parseParamType #%d: '%s' to fail.", i, tc.name) 40 | } 41 | } 42 | 43 | func TestParamType_String(t *testing.T) { 44 | tests := []struct { 45 | name string 46 | in paramType 47 | want string 48 | }{ 49 | {"heartbeatInfo", heartbeatInfo, "Heartbeat Info"}, 50 | {"ipV4Addr", ipV4Addr, "IPv4 IP"}, 51 | {"ipV6Addr", ipV6Addr, "IPv6 IP"}, 52 | {"stateCookie", stateCookie, "State Cookie"}, 53 | {"unrecognizedParam", unrecognizedParam, "Unrecognized Parameters"}, 54 | {"cookiePreservative", cookiePreservative, "Cookie Preservative"}, 55 | {"hostNameAddr", hostNameAddr, "Host Name Address"}, 56 | {"supportedAddrTypes", supportedAddrTypes, "Supported IP Types"}, 57 | {"outSSNResetReq", outSSNResetReq, "Outgoing SSN Reset Request Parameter"}, 58 | {"incSSNResetReq", incSSNResetReq, "Incoming SSN Reset Request Parameter"}, 59 | {"ssnTSNResetReq", ssnTSNResetReq, "SSN/TSN Reset Request Parameter"}, 60 | {"reconfigResp", reconfigResp, "Re-configuration Response Parameter"}, 61 | {"addOutStreamsReq", addOutStreamsReq, "Add Outgoing Streams Request Parameter"}, 62 | {"addIncStreamsReq", addIncStreamsReq, "Add Incoming Streams Request Parameter"}, 63 | {"ecnCapable", ecnCapable, "ECN Capable"}, 64 | {"zeroChecksumAcceptable", zeroChecksumAcceptable, "Zero Checksum Acceptable"}, 65 | {"random", random, "Random"}, 66 | {"chunkList", chunkList, "Chunk List"}, 67 | {"reqHMACAlgo", reqHMACAlgo, "Requested HMAC Algorithm Parameter"}, 68 | {"padding", padding, "Padding"}, 69 | {"supportedExt", supportedExt, "Supported Extensions"}, 70 | {"forwardTSNSupp", forwardTSNSupp, "Forward TSN supported"}, 71 | {"addIPAddr", addIPAddr, "Add IP Address"}, 72 | {"delIPAddr", delIPAddr, "Delete IP Address"}, 73 | {"errClauseInd", errClauseInd, "Error Cause Indication"}, 74 | {"setPriAddr", setPriAddr, "Set Primary IP"}, 75 | {"successInd", successInd, "Success Indication"}, 76 | {"adaptLayerInd", adaptLayerInd, "Adaptation Layer Indication"}, 77 | {"unknownValue", paramType(0x4242), fmt.Sprintf("Unknown ParamType: %d", 0x4242)}, 78 | } 79 | 80 | for _, tc := range tests { 81 | t.Run(tc.name, func(t *testing.T) { 82 | got := tc.in.String() 83 | assert.Equal(t, tc.want, got) 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /payload_queue_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func makePayload(tsn uint32, nBytes int) *chunkPayloadData { 13 | return &chunkPayloadData{tsn: tsn, userData: make([]byte, nBytes)} 14 | } 15 | 16 | func TestPayloadQueue(t *testing.T) { 17 | t.Run("pushNoCheck", func(t *testing.T) { 18 | pq := newPayloadQueue() 19 | pq.pushNoCheck(makePayload(0, 10)) 20 | assert.Equal(t, 10, pq.getNumBytes(), "total bytes mismatch") 21 | assert.Equal(t, 1, pq.size(), "item count mismatch") 22 | pq.pushNoCheck(makePayload(1, 11)) 23 | assert.Equal(t, 21, pq.getNumBytes(), "total bytes mismatch") 24 | assert.Equal(t, 2, pq.size(), "item count mismatch") 25 | pq.pushNoCheck(makePayload(2, 12)) 26 | assert.Equal(t, 33, pq.getNumBytes(), "total bytes mismatch") 27 | assert.Equal(t, 3, pq.size(), "item count mismatch") 28 | 29 | for i := uint32(0); i < 3; i++ { 30 | c, ok := pq.pop(i) 31 | assert.True(t, ok, "pop should succeed") 32 | assert.Equal(t, i, c.tsn, "TSN should match") 33 | } 34 | 35 | assert.Equal(t, 0, pq.getNumBytes(), "total bytes mismatch") 36 | assert.Equal(t, 0, pq.size(), "item count mismatch") 37 | 38 | pq.pushNoCheck(makePayload(3, 13)) 39 | assert.Equal(t, 13, pq.getNumBytes(), "total bytes mismatch") 40 | pq.pushNoCheck(makePayload(4, 14)) 41 | assert.Equal(t, 27, pq.getNumBytes(), "total bytes mismatch") 42 | 43 | for i := uint32(3); i < 5; i++ { 44 | c, ok := pq.pop(i) 45 | assert.True(t, ok, "pop should succeed") 46 | assert.Equal(t, i, c.tsn, "TSN should match") 47 | } 48 | 49 | assert.Equal(t, 0, pq.getNumBytes(), "total bytes mismatch") 50 | assert.Equal(t, 0, pq.size(), "item count mismatch") 51 | }) 52 | 53 | t.Run("markAllToRetrasmit", func(t *testing.T) { 54 | pq := newPayloadQueue() 55 | for i := 0; i < 3; i++ { 56 | pq.pushNoCheck(makePayload(uint32(i+1), 10)) //nolint:gosec // G115 57 | } 58 | pq.markAsAcked(2) 59 | pq.markAllToRetrasmit() 60 | 61 | c, ok := pq.get(1) 62 | assert.True(t, ok, "should be true") 63 | assert.True(t, c.retransmit, "should be marked as retransmit") 64 | c, ok = pq.get(2) 65 | assert.True(t, ok, "should be true") 66 | assert.False(t, c.retransmit, "should NOT be marked as retransmit") 67 | c, ok = pq.get(3) 68 | assert.True(t, ok, "should be true") 69 | assert.True(t, c.retransmit, "should be marked as retransmit") 70 | }) 71 | 72 | t.Run("reset retransmit flag on ack", func(t *testing.T) { 73 | pq := newPayloadQueue() 74 | for i := 0; i < 4; i++ { 75 | pq.pushNoCheck(makePayload(uint32(i+1), 10)) //nolint:gosec // G115 76 | } 77 | 78 | pq.markAllToRetrasmit() 79 | pq.markAsAcked(2) // should cancel retransmission for TSN 2 80 | pq.markAsAcked(4) // should cancel retransmission for TSN 4 81 | 82 | c, ok := pq.get(1) 83 | assert.True(t, ok, "should be true") 84 | assert.True(t, c.retransmit, "should be marked as retransmit") 85 | c, ok = pq.get(2) 86 | assert.True(t, ok, "should be true") 87 | assert.False(t, c.retransmit, "should NOT be marked as retransmit") 88 | c, ok = pq.get(3) 89 | assert.True(t, ok, "should be true") 90 | assert.True(t, c.retransmit, "should be marked as retransmit") 91 | c, ok = pq.get(4) 92 | assert.True(t, ok, "should be true") 93 | assert.False(t, c.retransmit, "should NOT be marked as retransmit") 94 | }) 95 | } 96 | -------------------------------------------------------------------------------- /error_cause.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // errorCauseCode is a cause code that appears in either a ERROR or ABORT chunk. 13 | type errorCauseCode uint16 14 | 15 | type errorCause interface { 16 | unmarshal([]byte) error 17 | marshal() ([]byte, error) 18 | length() uint16 19 | String() string 20 | 21 | errorCauseCode() errorCauseCode 22 | } 23 | 24 | // Error and abort chunk errors. 25 | var ( 26 | ErrBuildErrorCaseHandle = errors.New("BuildErrorCause does not handle") 27 | ) 28 | 29 | // buildErrorCause delegates the building of a error cause from raw bytes to the correct structure. 30 | func buildErrorCause(raw []byte) (errorCause, error) { 31 | var errCause errorCause 32 | 33 | c := errorCauseCode(binary.BigEndian.Uint16(raw[0:])) 34 | switch c { 35 | case invalidMandatoryParameter: 36 | errCause = &errorCauseInvalidMandatoryParameter{} 37 | case unrecognizedChunkType: 38 | errCause = &errorCauseUnrecognizedChunkType{} 39 | case protocolViolation: 40 | errCause = &errorCauseProtocolViolation{} 41 | case userInitiatedAbort: 42 | errCause = &errorCauseUserInitiatedAbort{} 43 | default: 44 | return nil, fmt.Errorf("%w: %s", ErrBuildErrorCaseHandle, c.String()) 45 | } 46 | 47 | if err := errCause.unmarshal(raw); err != nil { 48 | return nil, err 49 | } 50 | 51 | return errCause, nil 52 | } 53 | 54 | const ( 55 | invalidStreamIdentifier errorCauseCode = 1 56 | missingMandatoryParameter errorCauseCode = 2 57 | staleCookieError errorCauseCode = 3 58 | outOfResource errorCauseCode = 4 59 | unresolvableAddress errorCauseCode = 5 60 | unrecognizedChunkType errorCauseCode = 6 61 | invalidMandatoryParameter errorCauseCode = 7 62 | unrecognizedParameters errorCauseCode = 8 63 | noUserData errorCauseCode = 9 64 | cookieReceivedWhileShuttingDown errorCauseCode = 10 65 | restartOfAnAssociationWithNewAddresses errorCauseCode = 11 66 | userInitiatedAbort errorCauseCode = 12 67 | protocolViolation errorCauseCode = 13 68 | ) 69 | 70 | func (e errorCauseCode) String() string { //nolint:cyclop 71 | switch e { 72 | case invalidStreamIdentifier: 73 | return "Invalid Stream Identifier" 74 | case missingMandatoryParameter: 75 | return "Missing Mandatory Parameter" 76 | case staleCookieError: 77 | return "Stale Cookie Error" 78 | case outOfResource: 79 | return "Out Of Resource" 80 | case unresolvableAddress: 81 | return "Unresolvable IP" 82 | case unrecognizedChunkType: 83 | return "Unrecognized Chunk Type" 84 | case invalidMandatoryParameter: 85 | return "Invalid Mandatory Parameter" 86 | case unrecognizedParameters: 87 | return "Unrecognized Parameters" 88 | case noUserData: 89 | return "No User Data" 90 | case cookieReceivedWhileShuttingDown: 91 | return "Cookie Received While Shutting Down" 92 | case restartOfAnAssociationWithNewAddresses: 93 | return "Restart Of An Association With New Addresses" 94 | case userInitiatedAbort: 95 | return "User Initiated Abort" 96 | case protocolViolation: 97 | return "Protocol Violation" 98 | default: 99 | return fmt.Sprintf("Unknown CauseCode: %d", e) 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /chunk_reconfig.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | // https://tools.ietf.org/html/rfc6525#section-3.1 12 | // chunkReconfig represents an SCTP Chunk used to reconfigure streams. 13 | // 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 = 130 | Chunk Flags | Chunk Length | 18 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 19 | // \ \ 20 | // / Re-configuration Parameter / 21 | // \ \ 22 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 23 | // \ \ 24 | // / Re-configuration Parameter (optional) / 25 | // \ \ 26 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 27 | 28 | type chunkReconfig struct { 29 | chunkHeader 30 | paramA param 31 | paramB param 32 | } 33 | 34 | // Reconfigure chunk errors. 35 | var ( 36 | ErrChunkParseParamTypeFailed = errors.New("failed to parse param type") 37 | ErrChunkMarshalParamAReconfigFailed = errors.New("unable to marshal parameter A for reconfig") 38 | ErrChunkMarshalParamBReconfigFailed = errors.New("unable to marshal parameter B for reconfig") 39 | ) 40 | 41 | func (c *chunkReconfig) unmarshal(raw []byte) error { 42 | if err := c.chunkHeader.unmarshal(raw); err != nil { 43 | return err 44 | } 45 | pType, err := parseParamType(c.raw) 46 | if err != nil { 47 | return fmt.Errorf("%w: %v", ErrChunkParseParamTypeFailed, err) //nolint:errorlint 48 | } 49 | a, err := buildParam(pType, c.raw) 50 | if err != nil { 51 | return err 52 | } 53 | c.paramA = a 54 | 55 | padding := getPadding(a.length()) 56 | offset := a.length() + padding 57 | if len(c.raw) > offset { 58 | pType, err := parseParamType(c.raw[offset:]) 59 | if err != nil { 60 | return fmt.Errorf("%w: %v", ErrChunkParseParamTypeFailed, err) //nolint:errorlint 61 | } 62 | b, err := buildParam(pType, c.raw[offset:]) 63 | if err != nil { 64 | return err 65 | } 66 | c.paramB = b 67 | } 68 | 69 | return nil 70 | } 71 | 72 | func (c *chunkReconfig) marshal() ([]byte, error) { 73 | out, err := c.paramA.marshal() 74 | if err != nil { 75 | return nil, fmt.Errorf("%w: %v", ErrChunkMarshalParamAReconfigFailed, err) //nolint:errorlint 76 | } 77 | if c.paramB != nil { 78 | // Pad param A 79 | out = padByte(out, getPadding(len(out))) 80 | 81 | outB, err := c.paramB.marshal() 82 | if err != nil { 83 | return nil, fmt.Errorf("%w: %v", ErrChunkMarshalParamBReconfigFailed, err) //nolint:errorlint 84 | } 85 | 86 | out = append(out, outB...) 87 | } 88 | 89 | c.typ = ctReconfig 90 | c.raw = out 91 | 92 | return c.chunkHeader.marshal() 93 | } 94 | 95 | func (c *chunkReconfig) check() (abort bool, err error) { 96 | // nolint:godox 97 | // TODO: check allowed combinations: 98 | // https://tools.ietf.org/html/rfc6525#section-3.1 99 | return true, nil 100 | } 101 | 102 | // String makes chunkReconfig printable. 103 | func (c *chunkReconfig) String() string { 104 | res := fmt.Sprintf("Param A:\n %s", c.paramA) 105 | if c.paramB != nil { 106 | res += fmt.Sprintf("Param B:\n %s", c.paramB) 107 | } 108 | 109 | return res 110 | } 111 | -------------------------------------------------------------------------------- /param_reconfig_response.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // This parameter is used by the receiver of a Re-configuration Request 13 | // Parameter to respond to the request. 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 | // | Parameter Type = 16 | Parameter Length | 19 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 20 | // | Re-configuration Response Sequence Number | 21 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 22 | // | Result | 23 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 24 | // | Sender's Next TSN (optional) | 25 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 26 | // | Receiver's Next TSN (optional) | 27 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 28 | 29 | type paramReconfigResponse struct { 30 | paramHeader 31 | // This value is copied from the request parameter and is used by the 32 | // receiver of the Re-configuration Response Parameter to tie the 33 | // response to the request. 34 | reconfigResponseSequenceNumber uint32 35 | // This value describes the result of the processing of the request. 36 | result reconfigResult 37 | } 38 | 39 | type reconfigResult uint32 40 | 41 | const ( 42 | reconfigResultSuccessNOP reconfigResult = 0 43 | reconfigResultSuccessPerformed reconfigResult = 1 44 | reconfigResultDenied reconfigResult = 2 45 | reconfigResultErrorWrongSSN reconfigResult = 3 46 | reconfigResultErrorRequestAlreadyInProgress reconfigResult = 4 47 | reconfigResultErrorBadSequenceNumber reconfigResult = 5 48 | reconfigResultInProgress reconfigResult = 6 49 | ) 50 | 51 | // Reconfiguration response errors. 52 | var ( 53 | ErrReconfigRespParamTooShort = errors.New("reconfig response parameter too short") 54 | ) 55 | 56 | func (t reconfigResult) String() string { 57 | switch t { 58 | case reconfigResultSuccessNOP: 59 | return "0: Success - Nothing to do" 60 | case reconfigResultSuccessPerformed: 61 | return "1: Success - Performed" 62 | case reconfigResultDenied: 63 | return "2: Denied" 64 | case reconfigResultErrorWrongSSN: 65 | return "3: Error - Wrong SSN" 66 | case reconfigResultErrorRequestAlreadyInProgress: 67 | return "4: Error - Request already in progress" 68 | case reconfigResultErrorBadSequenceNumber: 69 | return "5: Error - Bad Sequence Number" 70 | case reconfigResultInProgress: 71 | return "6: In progress" 72 | default: 73 | return fmt.Sprintf("Unknown reconfigResult: %d", t) 74 | } 75 | } 76 | 77 | func (r *paramReconfigResponse) marshal() ([]byte, error) { 78 | r.typ = reconfigResp 79 | r.raw = make([]byte, 8) 80 | binary.BigEndian.PutUint32(r.raw, r.reconfigResponseSequenceNumber) 81 | binary.BigEndian.PutUint32(r.raw[4:], uint32(r.result)) 82 | 83 | return r.paramHeader.marshal() 84 | } 85 | 86 | func (r *paramReconfigResponse) unmarshal(raw []byte) (param, error) { 87 | err := r.paramHeader.unmarshal(raw) 88 | if err != nil { 89 | return nil, err 90 | } 91 | if len(r.raw) < 8 { 92 | return nil, ErrReconfigRespParamTooShort 93 | } 94 | r.reconfigResponseSequenceNumber = binary.BigEndian.Uint32(r.raw) 95 | r.result = reconfigResult(binary.BigEndian.Uint32(r.raw[4:])) 96 | 97 | return r, nil 98 | } 99 | -------------------------------------------------------------------------------- /chunkheader.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | /* 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. 18 | 19 | 0 1 2 3 20 | 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 21 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 22 | | Chunk Type | Chunk Flags | Chunk Length | 23 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 24 | | | 25 | | Chunk Value | 26 | | | 27 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 28 | */ 29 | type chunkHeader struct { 30 | typ chunkType 31 | flags byte 32 | raw []byte 33 | } 34 | 35 | const ( 36 | chunkHeaderSize = 4 37 | ) 38 | 39 | // SCTP chunk header errors. 40 | var ( 41 | ErrChunkHeaderTooSmall = errors.New("raw is too small for a SCTP chunk") 42 | ErrChunkHeaderNotEnoughSpace = errors.New("not enough data left in SCTP packet to satisfy requested length") 43 | ErrChunkHeaderPaddingNonZero = errors.New("chunk padding is non-zero at offset") 44 | ) 45 | 46 | func (c *chunkHeader) unmarshal(raw []byte) error { 47 | if len(raw) < chunkHeaderSize { 48 | return fmt.Errorf( 49 | "%w: raw only %d bytes, %d is the minimum length", 50 | ErrChunkHeaderTooSmall, len(raw), chunkHeaderSize, 51 | ) 52 | } 53 | 54 | c.typ = chunkType(raw[0]) 55 | c.flags = raw[1] 56 | length := binary.BigEndian.Uint16(raw[2:]) 57 | 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 | } 80 | } 81 | 82 | c.raw = raw[chunkHeaderSize : chunkHeaderSize+valueLength] 83 | 84 | return nil 85 | } 86 | 87 | func (c *chunkHeader) marshal() ([]byte, error) { 88 | raw := make([]byte, 4+len(c.raw)) 89 | 90 | raw[0] = uint8(c.typ) 91 | raw[1] = c.flags 92 | binary.BigEndian.PutUint16(raw[2:], uint16(len(c.raw)+chunkHeaderSize)) //nolint:gosec // G115 93 | copy(raw[4:], c.raw) 94 | 95 | return raw, nil 96 | } 97 | 98 | func (c *chunkHeader) valueLength() int { 99 | return len(c.raw) 100 | } 101 | 102 | // String makes chunkHeader printable. 103 | func (c chunkHeader) String() string { 104 | return c.typ.String() 105 | } 106 | -------------------------------------------------------------------------------- /paramheader.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "encoding/hex" 9 | "errors" 10 | "fmt" 11 | ) 12 | 13 | type paramHeaderUnrecognizedAction byte 14 | 15 | type paramHeader struct { 16 | typ paramType 17 | unrecognizedAction paramHeaderUnrecognizedAction 18 | len int 19 | raw []byte 20 | } 21 | 22 | /* 23 | The Parameter Types are encoded such that the highest-order 2 bits specify 24 | the action that is taken if the processing endpoint does not recognize the 25 | Parameter Type. 26 | 27 | 00 - Stop processing this parameter and do not process any further parameters within this chunk. 28 | 29 | 01 - Stop processing this parameter, do not process any further parameters within this chunk, and 30 | report the unrecognized parameter, as described in Section 3.2.2. 31 | 32 | 10 - Skip this parameter and continue processing. 33 | 34 | 11 - Skip this parameter and continue processing, but report the unrecognized 35 | parameter, as described in Section 3.2.2. 36 | 37 | https://www.rfc-editor.org/rfc/rfc9260.html#section-3.2.1 38 | */ 39 | 40 | const ( 41 | paramHeaderUnrecognizedActionMask = 0b11000000 42 | paramHeaderUnrecognizedActionStop paramHeaderUnrecognizedAction = 0b00000000 43 | paramHeaderUnrecognizedActionStopAndReport paramHeaderUnrecognizedAction = 0b01000000 44 | paramHeaderUnrecognizedActionSkip paramHeaderUnrecognizedAction = 0b10000000 45 | paramHeaderUnrecognizedActionSkipAndReport paramHeaderUnrecognizedAction = 0b11000000 46 | 47 | paramHeaderLength = 4 48 | ) 49 | 50 | // Parameter header parse errors. 51 | var ( 52 | ErrParamHeaderTooShort = errors.New("param header too short") 53 | ErrParamHeaderSelfReportedLengthShorter = errors.New("param self reported length is shorter than header length") 54 | ErrParamHeaderSelfReportedLengthLonger = errors.New("param self reported length is longer than header length") 55 | ErrParamHeaderParseFailed = errors.New("failed to parse param type") 56 | ) 57 | 58 | func (p *paramHeader) marshal() ([]byte, error) { 59 | paramLengthPlusHeader := paramHeaderLength + len(p.raw) 60 | 61 | rawParam := make([]byte, paramLengthPlusHeader) 62 | binary.BigEndian.PutUint16(rawParam[0:], uint16(p.typ)) 63 | binary.BigEndian.PutUint16(rawParam[2:], uint16(paramLengthPlusHeader)) //nolint:gosec // G115 64 | copy(rawParam[paramHeaderLength:], p.raw) 65 | 66 | return rawParam, nil 67 | } 68 | 69 | func (p *paramHeader) unmarshal(raw []byte) error { 70 | if len(raw) < paramHeaderLength { 71 | return ErrParamHeaderTooShort 72 | } 73 | 74 | paramLengthPlusHeader := binary.BigEndian.Uint16(raw[2:]) 75 | if int(paramLengthPlusHeader) < paramHeaderLength { 76 | return fmt.Errorf( 77 | "%w: param self reported length (%d) shorter than header length (%d)", 78 | ErrParamHeaderSelfReportedLengthShorter, int(paramLengthPlusHeader), paramHeaderLength, 79 | ) 80 | } 81 | if len(raw) < int(paramLengthPlusHeader) { 82 | return fmt.Errorf( 83 | "%w: param length (%d) shorter than its self reported length (%d)", 84 | ErrParamHeaderSelfReportedLengthLonger, len(raw), int(paramLengthPlusHeader), 85 | ) 86 | } 87 | 88 | typ, err := parseParamType(raw[0:]) 89 | if err != nil { 90 | return fmt.Errorf("%w: %v", ErrParamHeaderParseFailed, err) //nolint:errorlint 91 | } 92 | p.typ = typ 93 | p.unrecognizedAction = paramHeaderUnrecognizedAction(raw[0] & paramHeaderUnrecognizedActionMask) 94 | p.raw = raw[paramHeaderLength:paramLengthPlusHeader] 95 | p.len = int(paramLengthPlusHeader) 96 | 97 | return nil 98 | } 99 | 100 | func (p *paramHeader) length() int { 101 | return p.len 102 | } 103 | 104 | // String makes paramHeader printable. 105 | func (p paramHeader) String() string { 106 | return fmt.Sprintf("%s (%d): %s", p.typ, p.len, hex.Dump(p.raw)) 107 | } 108 | -------------------------------------------------------------------------------- /chunk_heartbeat.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | chunkHeartbeat represents an SCTP Chunk of type HEARTBEAT (RFC 9260 section 3.3.6) 13 | 14 | An endpoint sends this chunk to probe reachability of a destination address. 15 | The chunk MUST contain exactly one variable-length parameter: 16 | 17 | Variable Parameters Status Type Value 18 | ------------------------------------------------------------- 19 | Heartbeat Info Mandatory 1 20 | 21 | nolint:godot 22 | */ 23 | type chunkHeartbeat struct { 24 | chunkHeader 25 | params []param 26 | } 27 | 28 | // Heartbeat chunk errors. 29 | var ( 30 | ErrChunkTypeNotHeartbeat = errors.New("ChunkType is not of type HEARTBEAT") 31 | ErrHeartbeatNotLongEnoughInfo = errors.New("heartbeat is not long enough to contain Heartbeat Info") 32 | ErrParseParamTypeFailed = errors.New("failed to parse param type") 33 | ErrHeartbeatParam = errors.New("heartbeat should only have HEARTBEAT param") 34 | ErrHeartbeatChunkUnmarshal = errors.New("failed unmarshalling param in Heartbeat Chunk") 35 | ErrHeartbeatExtraNonZero = errors.New("heartbeat has non-zero trailing bytes after last parameter") 36 | ErrHeartbeatMarshalNoInfo = errors.New("heartbeat marshal requires exactly one Heartbeat Info parameter") 37 | ) 38 | 39 | func (h *chunkHeartbeat) unmarshal(raw []byte) error { //nolint:cyclop 40 | if err := h.chunkHeader.unmarshal(raw); err != nil { 41 | return err 42 | } 43 | 44 | if h.typ != ctHeartbeat { 45 | return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotHeartbeat, h.typ.String()) 46 | } 47 | 48 | // if the body is completely empty, accept it but don't populate params. 49 | if len(h.raw) == 0 { 50 | return nil 51 | } 52 | 53 | // need at least a parameter header present (TLV: 4 bytes minimum). 54 | if len(h.raw) < initOptionalVarHeaderLength { 55 | return fmt.Errorf("%w: %d", ErrHeartbeatNotLongEnoughInfo, len(h.raw)) 56 | } 57 | 58 | pType, err := parseParamType(h.raw) 59 | if err != nil { 60 | return fmt.Errorf("%w: %v", ErrParseParamTypeFailed, err) //nolint:errorlint 61 | } 62 | if pType != heartbeatInfo { 63 | return fmt.Errorf("%w: instead have %s", ErrHeartbeatParam, pType.String()) 64 | } 65 | 66 | var pHeader paramHeader 67 | if e := pHeader.unmarshal(h.raw); e != nil { 68 | return fmt.Errorf("%w: %v", ErrParseParamTypeFailed, e) //nolint:errorlint 69 | } 70 | 71 | plen := pHeader.length() 72 | if plen < initOptionalVarHeaderLength || plen > len(h.raw) { 73 | return ErrHeartbeatNotLongEnoughInfo 74 | } 75 | 76 | p, err := buildParam(pType, h.raw[:plen]) 77 | if err != nil { 78 | return fmt.Errorf("%w: %v", ErrHeartbeatChunkUnmarshal, err) //nolint:errorlint 79 | } 80 | h.params = append(h.params, p) 81 | 82 | // any trailing bytes beyond the single param must be all zeros. 83 | if rem := h.raw[plen:]; len(rem) > 0 && !allZero(rem) { 84 | return ErrHeartbeatExtraNonZero 85 | } 86 | 87 | return nil 88 | } 89 | 90 | func (h *chunkHeartbeat) Marshal() ([]byte, error) { 91 | // exactly one Heartbeat Info param is required. 92 | if len(h.params) != 1 { 93 | return nil, ErrHeartbeatMarshalNoInfo 94 | } 95 | 96 | // enforce correct concrete type via type assertion (param interface has no type getter). 97 | if _, ok := h.params[0].(*paramHeartbeatInfo); !ok { 98 | return nil, ErrHeartbeatParam 99 | } 100 | 101 | pp, err := h.params[0].marshal() 102 | if err != nil { 103 | return nil, fmt.Errorf("%w: %v", ErrHeartbeatChunkUnmarshal, err) //nolint:errorlint 104 | } 105 | 106 | // single TLV, no inter-parameter padding within the chunk body. 107 | h.chunkHeader.typ = ctHeartbeat 108 | h.chunkHeader.flags = 0 // sender MUST set to 0 109 | h.chunkHeader.raw = append([]byte(nil), pp...) 110 | 111 | return h.chunkHeader.marshal() 112 | } 113 | 114 | func (h *chunkHeartbeat) check() (abort bool, err error) { 115 | return false, nil 116 | } 117 | -------------------------------------------------------------------------------- /pending_queue.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "errors" 8 | ) 9 | 10 | // pendingBaseQueue 11 | 12 | type pendingBaseQueue struct { 13 | queue []*chunkPayloadData 14 | } 15 | 16 | func newPendingBaseQueue() *pendingBaseQueue { 17 | return &pendingBaseQueue{queue: []*chunkPayloadData{}} 18 | } 19 | 20 | func (q *pendingBaseQueue) push(c *chunkPayloadData) { 21 | q.queue = append(q.queue, c) 22 | } 23 | 24 | func (q *pendingBaseQueue) pop() *chunkPayloadData { 25 | if len(q.queue) == 0 { 26 | return nil 27 | } 28 | 29 | c := q.queue[0] 30 | q.queue[0] = nil 31 | 32 | if len(q.queue) == 0 { 33 | q.queue = nil 34 | } else { 35 | q.queue = q.queue[1:] 36 | } 37 | 38 | return c 39 | } 40 | 41 | func (q *pendingBaseQueue) get(i int) *chunkPayloadData { 42 | if len(q.queue) == 0 || i < 0 || i >= len(q.queue) { 43 | return nil 44 | } 45 | 46 | return q.queue[i] 47 | } 48 | 49 | func (q *pendingBaseQueue) size() int { 50 | return len(q.queue) 51 | } 52 | 53 | // pendingQueue 54 | 55 | type pendingQueue struct { 56 | unorderedQueue *pendingBaseQueue 57 | orderedQueue *pendingBaseQueue 58 | nBytes int 59 | selected bool 60 | unorderedIsSelected bool 61 | } 62 | 63 | // Pending queue errors. 64 | var ( 65 | ErrUnexpectedChunkPoppedUnordered = errors.New("unexpected chunk popped (unordered)") 66 | ErrUnexpectedChunkPoppedOrdered = errors.New("unexpected chunk popped (ordered)") 67 | ErrUnexpectedQState = errors.New("unexpected q state (should've been selected)") 68 | 69 | // Deprecated: use ErrUnexpectedChunkPoppedUnordered. 70 | ErrUnexpectedChuckPoppedUnordered = ErrUnexpectedChunkPoppedUnordered 71 | // Deprecated: use ErrUnexpectedChunkPoppedOrdered. 72 | ErrUnexpectedChuckPoppedOrdered = ErrUnexpectedChunkPoppedOrdered 73 | ) 74 | 75 | func newPendingQueue() *pendingQueue { 76 | return &pendingQueue{ 77 | unorderedQueue: newPendingBaseQueue(), 78 | orderedQueue: newPendingBaseQueue(), 79 | } 80 | } 81 | 82 | func (q *pendingQueue) push(c *chunkPayloadData) { 83 | if c.unordered { 84 | q.unorderedQueue.push(c) 85 | } else { 86 | q.orderedQueue.push(c) 87 | } 88 | q.nBytes += len(c.userData) 89 | } 90 | 91 | func (q *pendingQueue) peek() *chunkPayloadData { 92 | if q.selected { 93 | if q.unorderedIsSelected { 94 | return q.unorderedQueue.get(0) 95 | } 96 | 97 | return q.orderedQueue.get(0) 98 | } 99 | 100 | if c := q.unorderedQueue.get(0); c != nil { 101 | return c 102 | } 103 | 104 | return q.orderedQueue.get(0) 105 | } 106 | 107 | func (q *pendingQueue) pop(chunkPayload *chunkPayloadData) error { //nolint:cyclop 108 | if q.selected { //nolint:nestif 109 | var popped *chunkPayloadData 110 | if q.unorderedIsSelected { 111 | popped = q.unorderedQueue.pop() 112 | if popped != chunkPayload { 113 | return ErrUnexpectedChunkPoppedUnordered 114 | } 115 | } else { 116 | popped = q.orderedQueue.pop() 117 | if popped != chunkPayload { 118 | return ErrUnexpectedChunkPoppedOrdered 119 | } 120 | } 121 | if popped.endingFragment { 122 | q.selected = false 123 | } 124 | } else { 125 | if !chunkPayload.beginningFragment { 126 | return ErrUnexpectedQState 127 | } 128 | if chunkPayload.unordered { 129 | popped := q.unorderedQueue.pop() 130 | if popped != chunkPayload { 131 | return ErrUnexpectedChunkPoppedUnordered 132 | } 133 | if !popped.endingFragment { 134 | q.selected = true 135 | q.unorderedIsSelected = true 136 | } 137 | } else { 138 | popped := q.orderedQueue.pop() 139 | if popped != chunkPayload { 140 | return ErrUnexpectedChunkPoppedOrdered 141 | } 142 | if !popped.endingFragment { 143 | q.selected = true 144 | q.unorderedIsSelected = false 145 | } 146 | } 147 | } 148 | 149 | // guard against negative values (should never happen, but just in case). 150 | q.nBytes -= len(chunkPayload.userData) 151 | if q.nBytes < 0 { 152 | q.nBytes = 0 153 | } 154 | 155 | return nil 156 | } 157 | 158 | func (q *pendingQueue) getNumBytes() int { 159 | return q.nBytes 160 | } 161 | 162 | func (q *pendingQueue) size() int { 163 | return q.unorderedQueue.size() + q.orderedQueue.size() 164 | } 165 | -------------------------------------------------------------------------------- /param_outgoing_reset_request.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | ) 10 | 11 | const ( 12 | paramOutgoingResetRequestStreamIdentifiersOffset = 12 13 | ) 14 | 15 | // This parameter is used by the sender to request the reset of some or 16 | // all outgoing streams. 17 | // 0 1 2 3 18 | // 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 19 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 20 | // | Parameter Type = 13 | Parameter Length = 16 + 2 * N | 21 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 22 | // | Re-configuration Request Sequence Number | 23 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 24 | // | Re-configuration Response Sequence Number | 25 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 26 | // | Sender's Last Assigned TSN | 27 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 28 | // | Stream Number 1 (optional) | Stream Number 2 (optional) | 29 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 30 | // / ...... / 31 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 32 | // | Stream Number N-1 (optional) | Stream Number N (optional) | 33 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 34 | 35 | type paramOutgoingResetRequest struct { 36 | paramHeader 37 | // reconfigRequestSequenceNumber is used to identify the request. It is a monotonically 38 | // increasing number that is initialized to the same value as the 39 | // initial TSN. It is increased by 1 whenever sending a new Re- 40 | // configuration Request Parameter. 41 | reconfigRequestSequenceNumber uint32 42 | // When this Outgoing SSN Reset Request Parameter is sent in response 43 | // to an Incoming SSN Reset Request Parameter, this parameter is also 44 | // an implicit response to the incoming request. This field then 45 | // holds the Re-configuration Request Sequence Number of the incoming 46 | // request. In other cases, it holds the next expected 47 | // Re-configuration Request Sequence Number minus 1. 48 | reconfigResponseSequenceNumber uint32 49 | // This value holds the next TSN minus 1 -- in other words, the last 50 | // TSN that this sender assigned. 51 | senderLastTSN uint32 52 | // This optional field, if included, is used to indicate specific 53 | // streams that are to be reset. If no streams are listed, then all 54 | // streams are to be reset. 55 | streamIdentifiers []uint16 56 | } 57 | 58 | // Outgoing reset request parameter errors. 59 | var ( 60 | ErrSSNResetRequestParamTooShort = errors.New("outgoing SSN reset request parameter too short") 61 | ) 62 | 63 | func (r *paramOutgoingResetRequest) marshal() ([]byte, error) { 64 | r.typ = outSSNResetReq 65 | r.raw = make([]byte, paramOutgoingResetRequestStreamIdentifiersOffset+2*len(r.streamIdentifiers)) 66 | binary.BigEndian.PutUint32(r.raw, r.reconfigRequestSequenceNumber) 67 | binary.BigEndian.PutUint32(r.raw[4:], r.reconfigResponseSequenceNumber) 68 | binary.BigEndian.PutUint32(r.raw[8:], r.senderLastTSN) 69 | for i, sID := range r.streamIdentifiers { 70 | binary.BigEndian.PutUint16(r.raw[paramOutgoingResetRequestStreamIdentifiersOffset+2*i:], sID) 71 | } 72 | 73 | return r.paramHeader.marshal() 74 | } 75 | 76 | func (r *paramOutgoingResetRequest) unmarshal(raw []byte) (param, error) { 77 | err := r.paramHeader.unmarshal(raw) 78 | if err != nil { 79 | return nil, err 80 | } 81 | if len(r.raw) < paramOutgoingResetRequestStreamIdentifiersOffset { 82 | return nil, ErrSSNResetRequestParamTooShort 83 | } 84 | r.reconfigRequestSequenceNumber = binary.BigEndian.Uint32(r.raw) 85 | r.reconfigResponseSequenceNumber = binary.BigEndian.Uint32(r.raw[4:]) 86 | r.senderLastTSN = binary.BigEndian.Uint32(r.raw[8:]) 87 | 88 | lim := (len(r.raw) - paramOutgoingResetRequestStreamIdentifiersOffset) / 2 89 | r.streamIdentifiers = make([]uint16, lim) 90 | for i := 0; i < lim; i++ { 91 | r.streamIdentifiers[i] = binary.BigEndian.Uint16(r.raw[paramOutgoingResetRequestStreamIdentifiersOffset+2*i:]) 92 | } 93 | 94 | return r, nil 95 | } 96 | -------------------------------------------------------------------------------- /paramtype.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // paramType represents a SCTP INIT/INITACK parameter. 13 | type paramType uint16 14 | 15 | const ( 16 | heartbeatInfo paramType = 1 // Heartbeat Info [RFC9260] 17 | ipV4Addr paramType = 5 // IPv4 IP [RFC9260] 18 | ipV6Addr paramType = 6 // IPv6 IP [RFC9260] 19 | stateCookie paramType = 7 // State Cookie [RFC9260] 20 | unrecognizedParam paramType = 8 // Unrecognized Parameters [RFC9260] 21 | cookiePreservative paramType = 9 // Cookie Preservative [RFC9260] 22 | hostNameAddr paramType = 11 // Host Name Address [RFC9260] 23 | supportedAddrTypes paramType = 12 // Supported IP Types [RFC9260] 24 | outSSNResetReq paramType = 13 // Outgoing SSN Reset Request Parameter [RFC6525] 25 | incSSNResetReq paramType = 14 // Incoming SSN Reset Request Parameter [RFC6525] 26 | ssnTSNResetReq paramType = 15 // SSN/TSN Reset Request Parameter [RFC6525] 27 | reconfigResp paramType = 16 // Re-configuration Response Parameter [RFC6525] 28 | addOutStreamsReq paramType = 17 // Add Outgoing Streams Request Parameter [RFC6525] 29 | addIncStreamsReq paramType = 18 // Add Incoming Streams Request Parameter [RFC6525] 30 | ecnCapable paramType = 32768 // ECN Capable (0x8000) [RFC2960] 31 | zeroChecksumAcceptable paramType = 32769 // Zero Checksum Acceptable [draft-ietf-tsvwg-sctp-zero-checksum-00] 32 | random paramType = 32770 // Random (0x8002) [RFC4895] 33 | chunkList paramType = 32771 // Chunk List (0x8003) [RFC4895] 34 | reqHMACAlgo paramType = 32772 // Requested HMAC Algorithm Parameter (0x8004) [RFC4895] 35 | padding paramType = 32773 // Padding (0x8005) 36 | supportedExt paramType = 32776 // Supported Extensions (0x8008) [RFC5061] 37 | forwardTSNSupp paramType = 49152 // Forward TSN supported (0xC000) [RFC3758] 38 | addIPAddr paramType = 49153 // Add IP Address (0xC001) [RFC5061] 39 | delIPAddr paramType = 49154 // Delete IP Address (0xC002) [RFC5061] 40 | errClauseInd paramType = 49155 // Error Cause Indication (0xC003) [RFC5061] 41 | setPriAddr paramType = 49156 // Set Primary IP (0xC004) [RFC5061] 42 | successInd paramType = 49157 // Success Indication (0xC005) [RFC5061] 43 | adaptLayerInd paramType = 49158 // Adaptation Layer Indication (0xC006) [RFC5061] 44 | ) 45 | 46 | // Parameter packet errors. 47 | var ( 48 | ErrParamPacketTooShort = errors.New("packet too short") 49 | ) 50 | 51 | func parseParamType(raw []byte) (paramType, error) { 52 | if len(raw) < 2 { 53 | return paramType(0), ErrParamPacketTooShort 54 | } 55 | 56 | return paramType(binary.BigEndian.Uint16(raw)), nil 57 | } 58 | 59 | func (p paramType) String() string { //nolint:cyclop 60 | switch p { 61 | case heartbeatInfo: 62 | return "Heartbeat Info" 63 | case ipV4Addr: 64 | return "IPv4 IP" 65 | case ipV6Addr: 66 | return "IPv6 IP" 67 | case stateCookie: 68 | return "State Cookie" 69 | case unrecognizedParam: 70 | return "Unrecognized Parameters" 71 | case cookiePreservative: 72 | return "Cookie Preservative" 73 | case hostNameAddr: 74 | return "Host Name Address" 75 | case supportedAddrTypes: 76 | return "Supported IP Types" 77 | case outSSNResetReq: 78 | return "Outgoing SSN Reset Request Parameter" 79 | case incSSNResetReq: 80 | return "Incoming SSN Reset Request Parameter" 81 | case ssnTSNResetReq: 82 | return "SSN/TSN Reset Request Parameter" 83 | case reconfigResp: 84 | return "Re-configuration Response Parameter" 85 | case addOutStreamsReq: 86 | return "Add Outgoing Streams Request Parameter" 87 | case addIncStreamsReq: 88 | return "Add Incoming Streams Request Parameter" 89 | case ecnCapable: 90 | return "ECN Capable" 91 | case zeroChecksumAcceptable: 92 | return "Zero Checksum Acceptable" 93 | case random: 94 | return "Random" 95 | case chunkList: 96 | return "Chunk List" 97 | case reqHMACAlgo: 98 | return "Requested HMAC Algorithm Parameter" 99 | case padding: 100 | return "Padding" 101 | case supportedExt: 102 | return "Supported Extensions" 103 | case forwardTSNSupp: 104 | return "Forward TSN supported" 105 | case addIPAddr: 106 | return "Add IP Address" 107 | case delIPAddr: 108 | return "Delete IP Address" 109 | case errClauseInd: 110 | return "Error Cause Indication" 111 | case setPriAddr: 112 | return "Set Primary IP" 113 | case successInd: 114 | return "Success Indication" 115 | case adaptLayerInd: 116 | return "Adaptation Layer Indication" 117 | default: 118 | return fmt.Sprintf("Unknown ParamType: %d", p) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /util_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestPadByte_Success(t *testing.T) { 13 | tt := []struct { 14 | value []byte 15 | padLen int 16 | expected []byte 17 | }{ 18 | {[]byte{0x1, 0x2}, 0, []byte{0x1, 0x2}}, 19 | {[]byte{0x1, 0x2}, 1, []byte{0x1, 0x2, 0x0}}, 20 | {[]byte{0x1, 0x2}, 2, []byte{0x1, 0x2, 0x0, 0x0}}, 21 | {[]byte{0x1, 0x2}, 3, []byte{0x1, 0x2, 0x0, 0x0, 0x0}}, 22 | {[]byte{0x1, 0x2}, -1, []byte{0x1, 0x2}}, 23 | } 24 | 25 | for i, tc := range tt { 26 | actual := padByte(tc.value, tc.padLen) 27 | 28 | assert.Equalf(t, tc.expected, actual, "test %d not equal", i) 29 | } 30 | } 31 | 32 | func TestSerialNumberArithmetic(t *testing.T) { 33 | const div int = 16 34 | 35 | t.Run("32-bit", func(t *testing.T) { // nolint:dupl 36 | const serialBits uint32 = 32 37 | const interval uint32 = uint32((uint64(1) << uint64(serialBits)) / uint64(div)) 38 | const maxForwardDistance uint32 = 1<<(serialBits-1) - 1 39 | const maxBackwardDistance uint32 = 1 << (serialBits - 1) 40 | 41 | for i := uint32(0); i < uint32(div); i++ { 42 | s1 := i * interval 43 | s2f := s1 + maxForwardDistance 44 | s2b := s1 + maxBackwardDistance 45 | 46 | assert.Truef(t, sna32LT(s1, s2f), "s1 < s2 should be true: s1=0x%x s2=0x%x", s1, s2f) 47 | assert.Falsef(t, sna32LT(s1, s2b), "s1 < s2 should be false: s1=0x%x s2=0x%x", s1, s2b) 48 | 49 | assert.Falsef(t, sna32GT(s1, s2f), "s1 > s2 should be fales: s1=0x%x s2=0x%x", s1, s2f) 50 | assert.Truef(t, sna32GT(s1, s2b), "s1 > s2 should be true: s1=0x%x s2=0x%x", s1, s2b) 51 | 52 | assert.Truef(t, sna32LTE(s1, s2f), "s1 <= s2 should be true: s1=0x%x s2=0x%x", s1, s2f) 53 | assert.Falsef(t, sna32LTE(s1, s2b), "s1 <= s2 should be false: s1=0x%x s2=0x%x", s1, s2b) 54 | 55 | assert.Falsef(t, sna32GTE(s1, s2f), "s1 >= s2 should be fales: s1=0x%x s2=0x%x", s1, s2f) 56 | assert.Truef(t, sna32GTE(s1, s2b), "s1 >= s2 should be true: s1=0x%x s2=0x%x", s1, s2b) 57 | 58 | assert.Truef(t, sna32EQ(s1, s1), "s1 == s1 should be true: s1=0x%x s2=0x%x", s1, s1) 59 | assert.Truef(t, sna32EQ(s2b, s2b), "s2 == s2 should be true: s2=0x%x s2=0x%x", s2b, s2b) 60 | assert.Falsef(t, sna32EQ(s1, s1+1), "s1 == s1+1 should be false: s1=0x%x s1+1=0x%x", s1, s1+1) 61 | assert.Falsef(t, sna32EQ(s1, s1-1), "s1 == s1-1 hould be false: s1=0x%x s1-1=0x%x", s1, s1-1) 62 | 63 | assert.Truef(t, sna32LTE(s1, s1), "s1 == s1 should be true: s1=0x%x s2=0x%x", s1, s1) 64 | assert.Truef(t, sna32LTE(s2b, s2b), "s2 == s2 should be true: s2=0x%x s2=0x%x", s2b, s2b) 65 | 66 | assert.Truef(t, sna32GTE(s1, s1), "s1 == s1 should be true: s1=0x%x s2=0x%x", s1, s1) 67 | assert.Truef(t, sna32GTE(s2b, s2b), "s2 == s2 should be true: s2=0x%x s2=0x%x", s2b, s2b) 68 | } 69 | }) 70 | 71 | t.Run("16-bit", func(t *testing.T) { // nolint:dupl 72 | const serialBits uint16 = 16 73 | const interval uint16 = uint16((uint64(1) << uint64(serialBits)) / uint64(div)) 74 | const maxForwardDistance uint16 = 1<<(serialBits-1) - 1 75 | const maxBackwardDistance uint16 = 1 << (serialBits - 1) 76 | 77 | for i := uint16(0); i < uint16(div); i++ { 78 | s1 := i * interval 79 | s2f := s1 + maxForwardDistance 80 | s2b := s1 + maxBackwardDistance 81 | 82 | assert.Truef(t, sna16LT(s1, s2f), "s1 < s2 should be true: s1=0x%x s2=0x%x", s1, s2f) 83 | assert.Falsef(t, sna16LT(s1, s2b), "s1 < s2 should be false: s1=0x%x s2=0x%x", s1, s2b) 84 | 85 | assert.Falsef(t, sna16GT(s1, s2f), "s1 > s2 should be fales: s1=0x%x s2=0x%x", s1, s2f) 86 | assert.Truef(t, sna16GT(s1, s2b), "s1 > s2 should be true: s1=0x%x s2=0x%x", s1, s2b) 87 | 88 | assert.Truef(t, sna16LTE(s1, s2f), "s1 <= s2 should be true: s1=0x%x s2=0x%x", s1, s2f) 89 | assert.Falsef(t, sna16LTE(s1, s2b), "s1 <= s2 should be false: s1=0x%x s2=0x%x", s1, s2b) 90 | 91 | assert.Falsef(t, sna16GTE(s1, s2f), "s1 >= s2 should be fales: s1=0x%x s2=0x%x", s1, s2f) 92 | assert.Truef(t, sna16GTE(s1, s2b), "s1 >= s2 should be true: s1=0x%x s2=0x%x", s1, s2b) 93 | 94 | assert.Truef(t, sna16EQ(s1, s1), "s1 == s1 should be true: s1=0x%x s2=0x%x", s1, s1) 95 | assert.Truef(t, sna16EQ(s2b, s2b), "s2 == s2 should be true: s2=0x%x s2=0x%x", s2b, s2b) 96 | assert.Falsef(t, sna16EQ(s1, s1+1), "s1 == s1+1 should be false: s1=0x%x s1+1=0x%x", s1, s1+1) 97 | assert.Falsef(t, sna16EQ(s1, s1-1), "s1 == s1-1 hould be false: s1=0x%x s1-1=0x%x", s1, s1-1) 98 | 99 | assert.Truef(t, sna16LTE(s1, s1), "s1 == s1 should be true: s1=0x%x s2=0x%x", s1, s1) 100 | assert.Truef(t, sna16LTE(s2b, s2b), "s2 == s2 should be true: s2=0x%x s2=0x%x", s2b, s2b) 101 | 102 | assert.Truef(t, sna16GTE(s1, s1), "s1 == s1 should be true: s1=0x%x s2=0x%x", s1, s1) 103 | assert.Truef(t, sna16GTE(s2b, s2b), "s2 == s2 should be true: s2=0x%x s2=0x%x", s2b, s2b) 104 | } 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Pion SCTP 4 |
5 |

6 |

A Go implementation of SCTP

7 |

8 | Pion SCTP 9 | join us on Discord Follow us on Bluesky 10 |
11 | GitHub Workflow Status 12 | Go Reference 13 | Coverage Status 14 | Go Report Card 15 | License: MIT 16 |

17 |
18 | 19 | ### Implemented 20 | - [RFC 6525](https://www.rfc-editor.org/rfc/rfc6525.html) — Stream Control Transmission Protocol (SCTP) Stream Reconfiguration 21 | - [RFC 3758](https://www.rfc-editor.org/rfc/rfc3758.html) — Stream Control Transmission Protocol (SCTP) Partial Reliability Extension 22 | - [RFC 5061](https://www.rfc-editor.org/rfc/rfc5061.html) — Stream Control Transmission Protocol (SCTP) Dynamic Address Reconfiguration 23 | - [RFC 4895](https://www.rfc-editor.org/rfc/rfc4895.html) — Authenticated Chunks for the Stream Control Transmission Protocol (SCTP) 24 | - [RFC 1982](https://www.rfc-editor.org/rfc/rfc1982.html) — Serial Number Arithmetic 25 | 26 | ### Partial implementations 27 | Pion only implements the subset of RFC 4960 that is required for WebRTC. 28 | 29 | - [RFC 4960](https://www.rfc-editor.org/rfc/rfc4960.html) — Stream Control Transmission Protocol [Obsoleted by 9260, above] 30 | - [RFC 2960](https://www.rfc-editor.org/rfc/rfc2960.html) — Stream Control Transmission Protocol [Obsoleted by 4960, above] 31 | 32 | The update to [RFC 9260](https://www.rfc-editor.org/rfc/rfc9260) — Stream Control Transmission Protocol is currently a [work in progress](https://github.com/pion/sctp/issues/402). 33 | 34 | ### Potential future implementations 35 | Ideally, we would like to add the following features as part of a [v2 refresh](https://github.com/pion/sctp/issues/314): 36 | 37 | Feature | Reference | Progress 38 | --- | --- | --- 39 | RACK (tail loss probing) | [Paper](https://icnp20.cs.ucr.edu/proceedings/nipaa/RACK%20for%20SCTP.pdf), [Comment](https://github.com/pion/sctp/issues/206#issuecomment-968265853)| [In review](https://github.com/pion/sctp/pull/390) 40 | Adaptive burst mitigation | [Paper, see section 5A](https://icnp20.cs.ucr.edu/proceedings/nipaa/RACK%20for%20SCTP.pdf)| [In review](https://github.com/pion/sctp/pull/394) 41 | Update to RFC 9260 | [Parent issue](https://github.com/pion/sctp/issues/402) | [In progress](https://github.com/pion/sctp/issues/402) 42 | Implement RFC 8260 | [Issue](https://github.com/pion/sctp/issues/435) | In progress (no PR available yet) 43 | Blocking writes | [1](https://github.com/pion/sctp/issues/77), [2](https://github.com/pion/sctp/issues/357) | [Potentially in progress](https://github.com/pion/sctp/issues/357#issuecomment-3382050767) 44 | association.listener (and better docs) | [1](https://github.com/pion/sctp/issues/74), [2](https://github.com/pion/sctp/issues/173) | Not started, [blocked by above](https://github.com/pion/sctp/issues/74#issuecomment-545550714) 45 | 46 | RFCs of interest: 47 | - [RFC 9438](https://datatracker.ietf.org/doc/rfc9438/) as it addresses the low utilization problem of [RFC 4960](https://www.rfc-editor.org/rfc/rfc4960.html) in fast long-distance networks as mentioned [here](https://github.com/pion/sctp/issues/218#issuecomment-3329690797). 48 | 49 | ### Roadmap 50 | The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones. 51 | 52 | ### Community 53 | Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt). 54 | 55 | Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. 56 | 57 | We are always looking to support **your projects**. Please reach out if you have something to build! 58 | If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly) 59 | 60 | ### Contributing 61 | Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible 62 | 63 | ### License 64 | MIT License - see [LICENSE](LICENSE) for full text 65 | -------------------------------------------------------------------------------- /chunk_heartbeat_ack.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | chunkHeartbeatAck represents an SCTP Chunk of type HEARTBEAT ACK 13 | 14 | An endpoint should send this chunk to its peer endpoint as a response 15 | to a HEARTBEAT chunk (see Section 8.3). A HEARTBEAT ACK is always 16 | sent to the source IP address of the IP datagram containing the 17 | HEARTBEAT chunk to which this ack is responding. 18 | 19 | The parameter field contains a variable-length opaque data structure. 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 = 5 | Chunk Flags | Heartbeat Ack Length | 25 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 26 | | | 27 | | Heartbeat Information TLV (Variable-Length) | 28 | | | 29 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 30 | 31 | Defined as a variable-length parameter using the format described 32 | in Section 3.2.1, i.e.: 33 | 34 | Variable Parameters Status Type Value 35 | ------------------------------------------------------------- 36 | Heartbeat Info Mandatory 1 37 | . 38 | */ 39 | type chunkHeartbeatAck struct { 40 | chunkHeader 41 | params []param 42 | } 43 | 44 | // Heartbeat ack chunk errors. 45 | var ( 46 | // Deprecated: this error is no longer used but is kept for compatibility. 47 | ErrUnimplemented = errors.New("unimplemented") 48 | ErrChunkTypeNotHeartbeatAck = errors.New("chunk type is not of type HEARTBEAT ACK") 49 | ErrHeartbeatAckParams = errors.New("heartbeat Ack must have one param") 50 | ErrHeartbeatAckNotHeartbeatInfo = errors.New("heartbeat Ack must have one param, and it should be a HeartbeatInfo") 51 | ErrHeartbeatAckMarshalParam = errors.New("unable to marshal parameter for Heartbeat Ack") 52 | ) 53 | 54 | func (h *chunkHeartbeatAck) unmarshal(raw []byte) error { //nolint:cyclop 55 | if err := h.chunkHeader.unmarshal(raw); err != nil { 56 | return err 57 | } 58 | 59 | if h.typ != ctHeartbeatAck { 60 | return fmt.Errorf("%w %s", ErrChunkTypeNotHeartbeatAck, h.typ.String()) 61 | } 62 | 63 | // allow for an empty heartbeat: no RTT info -> ActiveHeartbeat just won't update SRTT. 64 | if len(h.raw) == 0 { 65 | h.params = nil 66 | 67 | return nil 68 | } 69 | 70 | if len(h.raw) < initOptionalVarHeaderLength { 71 | return fmt.Errorf("%w: %d", ErrHeartbeatAckParams, len(h.raw)) 72 | } 73 | 74 | pType, err := parseParamType(h.raw) 75 | if err != nil { 76 | return fmt.Errorf("%w: %v", ErrHeartbeatAckParams, err) //nolint:errorlint 77 | } 78 | if pType != heartbeatInfo { 79 | return fmt.Errorf("%w: instead have %s", ErrHeartbeatAckNotHeartbeatInfo, pType.String()) 80 | } 81 | 82 | var pHeader paramHeader 83 | if e := pHeader.unmarshal(h.raw); e != nil { 84 | return fmt.Errorf("%w: %v", ErrHeartbeatAckParams, e) //nolint:errorlint 85 | } 86 | plen := pHeader.length() 87 | if plen < initOptionalVarHeaderLength || plen > len(h.raw) { 88 | return fmt.Errorf("%w: %d", ErrHeartbeatAckParams, plen) 89 | } 90 | 91 | p, err := buildParam(pType, h.raw[:plen]) 92 | if err != nil { 93 | return fmt.Errorf("%w: %v", ErrHeartbeatAckMarshalParam, err) //nolint:errorlint 94 | } 95 | h.params = []param{p} 96 | 97 | // Any trailing bytes beyond the single param must be zero. 98 | if rem := h.raw[plen:]; len(rem) > 0 && !allZero(rem) { 99 | return ErrHeartbeatExtraNonZero 100 | } 101 | 102 | return nil 103 | } 104 | 105 | func (h *chunkHeartbeatAck) marshal() ([]byte, error) { 106 | if len(h.params) != 1 { 107 | return nil, ErrHeartbeatAckParams 108 | } 109 | 110 | switch h.params[0].(type) { 111 | case *paramHeartbeatInfo: 112 | // ParamHeartbeatInfo is valid 113 | default: 114 | return nil, ErrHeartbeatAckNotHeartbeatInfo 115 | } 116 | 117 | out := make([]byte, 0) 118 | for idx, p := range h.params { 119 | pp, err := p.marshal() 120 | if err != nil { 121 | return nil, fmt.Errorf("%w: %v", ErrHeartbeatAckMarshalParam, err) //nolint:errorlint 122 | } 123 | 124 | out = append(out, pp...) 125 | 126 | // Chunks (including Type, Length, and Value fields) are padded out 127 | // by the sender with all zero bytes to be a multiple of 4 bytes 128 | // long. This padding MUST NOT be more than 3 bytes in total. The 129 | // Chunk Length value does not include terminating padding of the 130 | // chunk. *However, it does include padding of any variable-length 131 | // parameter except the last parameter in the chunk.* The receiver 132 | // MUST ignore the padding. 133 | if idx != len(h.params)-1 { 134 | out = padByte(out, getPadding(len(pp))) 135 | } 136 | } 137 | 138 | h.chunkHeader.typ = ctHeartbeatAck 139 | h.chunkHeader.raw = out 140 | 141 | return h.chunkHeader.marshal() 142 | } 143 | 144 | func (h *chunkHeartbeatAck) check() (abort bool, err error) { 145 | return false, nil 146 | } 147 | -------------------------------------------------------------------------------- /chunk_forward_tsn.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | // This chunk shall be used by the data sender to inform the data 13 | // receiver to adjust its cumulative received TSN point forward because 14 | // some missing TSNs are associated with data chunks that SHOULD NOT be 15 | // transmitted or retransmitted by the sender. 16 | // 17 | // 0 1 2 3 18 | // 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 19 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 20 | // | Type = 192 | Flags = 0x00 | Length = Variable | 21 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 22 | // | New Cumulative TSN | 23 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 24 | // | Stream-1 | Stream Sequence-1 | 25 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 26 | // \ / 27 | // / \ 28 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 29 | // | Stream-N | Stream Sequence-N | 30 | // +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 31 | 32 | type chunkForwardTSN struct { 33 | chunkHeader 34 | 35 | // This indicates the new cumulative TSN to the data receiver. Upon 36 | // the reception of this value, the data receiver MUST consider 37 | // any missing TSNs earlier than or equal to this value as received, 38 | // and stop reporting them as gaps in any subsequent SACKs. 39 | newCumulativeTSN uint32 40 | 41 | streams []chunkForwardTSNStream 42 | } 43 | 44 | const ( 45 | newCumulativeTSNLength = 4 46 | forwardTSNStreamLength = 4 47 | ) 48 | 49 | // Forward TSN chunk errors. 50 | var ( 51 | ErrMarshalStreamFailed = errors.New("failed to marshal stream") 52 | ErrChunkTooShort = errors.New("chunk too short") 53 | ) 54 | 55 | func (c *chunkForwardTSN) unmarshal(raw []byte) error { 56 | if err := c.chunkHeader.unmarshal(raw); err != nil { 57 | return err 58 | } 59 | 60 | if len(c.raw) < newCumulativeTSNLength { 61 | return ErrChunkTooShort 62 | } 63 | 64 | c.newCumulativeTSN = binary.BigEndian.Uint32(c.raw[0:]) 65 | 66 | offset := newCumulativeTSNLength 67 | remaining := len(c.raw) - offset 68 | for remaining > 0 { 69 | s := chunkForwardTSNStream{} 70 | 71 | if err := s.unmarshal(c.raw[offset:]); err != nil { 72 | return fmt.Errorf("%w: %v", ErrMarshalStreamFailed, err) //nolint:errorlint 73 | } 74 | 75 | c.streams = append(c.streams, s) 76 | 77 | offset += s.length() 78 | remaining -= s.length() 79 | } 80 | 81 | return nil 82 | } 83 | 84 | func (c *chunkForwardTSN) marshal() ([]byte, error) { 85 | out := make([]byte, newCumulativeTSNLength) 86 | binary.BigEndian.PutUint32(out[0:], c.newCumulativeTSN) 87 | 88 | for _, s := range c.streams { 89 | b, err := s.marshal() 90 | if err != nil { 91 | return nil, fmt.Errorf("%w: %v", ErrMarshalStreamFailed, err) //nolint:errorlint 92 | } 93 | out = append(out, b...) //nolint:makezero // TODO: fix 94 | } 95 | 96 | c.typ = ctForwardTSN 97 | c.raw = out 98 | 99 | return c.chunkHeader.marshal() 100 | } 101 | 102 | func (c *chunkForwardTSN) check() (abort bool, err error) { 103 | return true, nil 104 | } 105 | 106 | // String makes chunkForwardTSN printable. 107 | func (c *chunkForwardTSN) String() string { 108 | res := fmt.Sprintf("New Cumulative TSN: %d\n", c.newCumulativeTSN) 109 | for _, s := range c.streams { 110 | res += fmt.Sprintf(" - si=%d, ssn=%d\n", s.identifier, s.sequence) 111 | } 112 | 113 | return res 114 | } 115 | 116 | type chunkForwardTSNStream struct { 117 | // This field holds a stream number that was skipped by this 118 | // FWD-TSN. 119 | identifier uint16 120 | 121 | // This field holds the sequence number associated with the stream 122 | // that was skipped. The stream sequence field holds the largest 123 | // stream sequence number in this stream being skipped. The receiver 124 | // of the FWD-TSN's can use the Stream-N and Stream Sequence-N fields 125 | // to enable delivery of any stranded TSN's that remain on the stream 126 | // re-ordering queues. This field MUST NOT report TSN's corresponding 127 | // to DATA chunks that are marked as unordered. For ordered DATA 128 | // chunks this field MUST be filled in. 129 | sequence uint16 130 | } 131 | 132 | func (s *chunkForwardTSNStream) length() int { 133 | return forwardTSNStreamLength 134 | } 135 | 136 | func (s *chunkForwardTSNStream) unmarshal(raw []byte) error { 137 | if len(raw) < forwardTSNStreamLength { 138 | return ErrChunkTooShort 139 | } 140 | s.identifier = binary.BigEndian.Uint16(raw[0:]) 141 | s.sequence = binary.BigEndian.Uint16(raw[2:]) 142 | 143 | return nil 144 | } 145 | 146 | func (s *chunkForwardTSNStream) marshal() ([]byte, error) { // nolint:unparam 147 | out := make([]byte, forwardTSNStreamLength) 148 | 149 | binary.BigEndian.PutUint16(out[0:], s.identifier) 150 | binary.BigEndian.PutUint16(out[2:], s.sequence) 151 | 152 | return out, nil 153 | } 154 | -------------------------------------------------------------------------------- /chunk_init.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp // nolint:dupl 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | Init represents an SCTP Chunk of type INIT 13 | 14 | See chunkInitCommon for the fixed headers 15 | 16 | Variable Parameters Status Type Value 17 | ------------------------------------------------------------- 18 | IPv4 IP (Note 1) Optional 5 19 | IPv6 IP (Note 1) Optional 6 20 | Cookie Preservative Optional 9 21 | Reserved for ECN Capable (Note 2) Optional 32768 (0x8000) 22 | Host Name IP (Note 3) Optional 11 23 | Supported IP Types (Note 4) Optional 12 24 | */ 25 | type chunkInit struct { 26 | chunkHeader 27 | chunkInitCommon 28 | } 29 | 30 | // Init chunk errors. 31 | var ( 32 | ErrChunkTypeNotTypeInit = errors.New("ChunkType is not of type INIT") 33 | ErrChunkValueNotLongEnough = errors.New("chunk Value isn't long enough for mandatory parameters exp") 34 | ErrChunkTypeInitFlagZero = errors.New("ChunkType of type INIT flags must be all 0") 35 | ErrChunkTypeInitUnmarshalFailed = errors.New("failed to unmarshal INIT body") 36 | ErrChunkTypeInitMarshalFailed = errors.New("failed marshaling INIT common data") 37 | ErrChunkTypeInitInitateTagZero = errors.New("ChunkType of type INIT ACK InitiateTag must not be 0") 38 | ErrInitInboundStreamRequestZero = errors.New("INIT ACK inbound stream request must be > 0") 39 | ErrInitOutboundStreamRequestZero = errors.New("INIT ACK outbound stream request must be > 0") 40 | ErrInitAdvertisedReceiver1500 = errors.New("INIT ACK Advertised Receiver Window Credit (a_rwnd) must be >= 1500") 41 | ErrInitUnknownParam = errors.New("INIT with unknown param") 42 | ) 43 | 44 | func (i *chunkInit) unmarshal(raw []byte) error { 45 | if err := i.chunkHeader.unmarshal(raw); err != nil { 46 | return err 47 | } 48 | 49 | if i.typ != ctInit { 50 | return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotTypeInit, i.typ.String()) 51 | } else if len(i.raw) < initChunkMinLength { 52 | return fmt.Errorf("%w: %d actual: %d", ErrChunkValueNotLongEnough, initChunkMinLength, len(i.raw)) 53 | } 54 | 55 | // The Chunk Flags field in INIT is reserved, and all bits in it should 56 | // be set to 0 by the sender and ignored by the receiver. The sequence 57 | // of parameters within an INIT can be processed in any order. 58 | if i.flags != 0 { 59 | return ErrChunkTypeInitFlagZero 60 | } 61 | 62 | if err := i.chunkInitCommon.unmarshal(i.raw); err != nil { 63 | return fmt.Errorf("%w: %v", ErrChunkTypeInitUnmarshalFailed, err) //nolint:errorlint 64 | } 65 | 66 | return nil 67 | } 68 | 69 | func (i *chunkInit) marshal() ([]byte, error) { 70 | initShared, err := i.chunkInitCommon.marshal() 71 | if err != nil { 72 | return nil, fmt.Errorf("%w: %v", ErrChunkTypeInitMarshalFailed, err) //nolint:errorlint 73 | } 74 | 75 | i.chunkHeader.typ = ctInit 76 | i.chunkHeader.raw = initShared 77 | 78 | return i.chunkHeader.marshal() 79 | } 80 | 81 | func (i *chunkInit) check() (abort bool, err error) { 82 | // The receiver of the INIT (the responding end) records the value of 83 | // the Initiate Tag parameter. This value MUST be placed into the 84 | // Verification Tag field of every SCTP packet that the receiver of 85 | // the INIT transmits within this association. 86 | // 87 | // The Initiate Tag is allowed to have any value except 0. See 88 | // Section 5.3.1 for more on the selection of the tag value. 89 | // 90 | // If the value of the Initiate Tag in a received INIT chunk is found 91 | // to be 0, the receiver MUST treat it as an error and close the 92 | // association by transmitting an ABORT. 93 | if i.initiateTag == 0 { 94 | return true, ErrChunkTypeInitInitateTagZero 95 | } 96 | 97 | // Defines the maximum number of streams the sender of this INIT 98 | // chunk allows the peer end to create in this association. The 99 | // value 0 MUST NOT be used. 100 | // 101 | // Note: There is no negotiation of the actual number of streams but 102 | // instead the two endpoints will use the min(requested, offered). 103 | // See Section 5.1.1 for details. 104 | // 105 | // Note: A receiver of an INIT with the MIS value of 0 SHOULD abort 106 | // the association. 107 | if i.numInboundStreams == 0 { 108 | return true, ErrInitInboundStreamRequestZero 109 | } 110 | 111 | // Defines the number of outbound streams the sender of this INIT 112 | // chunk wishes to create in this association. The value of 0 MUST 113 | // NOT be used. 114 | // 115 | // Note: A receiver of an INIT with the OS value set to 0 SHOULD 116 | // abort the association. 117 | 118 | if i.numOutboundStreams == 0 { 119 | return true, ErrInitOutboundStreamRequestZero 120 | } 121 | 122 | // An SCTP receiver MUST be able to receive a minimum of 1500 bytes in 123 | // one SCTP packet. This means that an SCTP endpoint MUST NOT indicate 124 | // less than 1500 bytes in its initial a_rwnd sent in the INIT or INIT 125 | // ACK. 126 | if i.advertisedReceiverWindowCredit < 1500 { 127 | return true, ErrInitAdvertisedReceiver1500 128 | } 129 | 130 | for _, p := range i.unrecognizedParams { 131 | if p.unrecognizedAction == paramHeaderUnrecognizedActionStop || 132 | p.unrecognizedAction == paramHeaderUnrecognizedActionStopAndReport { 133 | return true, ErrInitUnknownParam 134 | } 135 | } 136 | 137 | return false, nil 138 | } 139 | 140 | // String makes chunkInit printable. 141 | func (i *chunkInit) String() string { 142 | return fmt.Sprintf("%s\n%s", i.chunkHeader, i.chunkInitCommon) 143 | } 144 | -------------------------------------------------------------------------------- /chunk_init_ack.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp // nolint:dupl 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | ) 10 | 11 | /* 12 | chunkInitAck represents an SCTP Chunk of type INIT ACK 13 | 14 | See chunkInitCommon for the fixed headers 15 | 16 | Variable Parameters Status Type Value 17 | ------------------------------------------------------------- 18 | State Cookie Mandatory 7 19 | IPv4 IP (Note 1) Optional 5 20 | IPv6 IP (Note 1) Optional 6 21 | Unrecognized Parameter Optional 8 22 | Reserved for ECN Capable (Note 2) Optional 32768 (0x8000) 23 | Host Name IP (Note 3) Optional 11 24 | */ 25 | type chunkInitAck struct { 26 | chunkHeader 27 | chunkInitCommon 28 | } 29 | 30 | // Init ack chunk errors. 31 | var ( 32 | ErrChunkTypeNotInitAck = errors.New("ChunkType is not of type INIT ACK") 33 | ErrChunkNotLongEnoughForParams = errors.New("chunk Value isn't long enough for mandatory parameters exp") 34 | ErrChunkTypeInitAckFlagZero = errors.New("ChunkType of type INIT ACK flags must be all 0") 35 | ErrInitAckUnmarshalFailed = errors.New("failed to unmarshal INIT body") 36 | ErrInitCommonDataMarshalFailed = errors.New("failed marshaling INIT common data") 37 | ErrChunkTypeInitAckInitateTagZero = errors.New("ChunkType of type INIT ACK InitiateTag must not be 0") 38 | ErrInitAckInboundStreamRequestZero = errors.New("INIT ACK inbound stream request must be > 0") 39 | ErrInitAckOutboundStreamRequestZero = errors.New("INIT ACK outbound stream request must be > 0") 40 | ErrInitAckAdvertisedReceiver1500 = errors.New("INIT ACK Advertised Receiver Window Credit (a_rwnd) must be >= 1500") 41 | ) 42 | 43 | func (i *chunkInitAck) unmarshal(raw []byte) error { 44 | if err := i.chunkHeader.unmarshal(raw); err != nil { 45 | return err 46 | } 47 | 48 | if i.typ != ctInitAck { 49 | return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotInitAck, i.typ.String()) 50 | } else if len(i.raw) < initChunkMinLength { 51 | return fmt.Errorf("%w: %d actual: %d", ErrChunkNotLongEnoughForParams, initChunkMinLength, len(i.raw)) 52 | } 53 | 54 | // The Chunk Flags field in INIT is reserved, and all bits in it should 55 | // be set to 0 by the sender and ignored by the receiver. The sequence 56 | // of parameters within an INIT can be processed in any order. 57 | if i.flags != 0 { 58 | return ErrChunkTypeInitAckFlagZero 59 | } 60 | 61 | if err := i.chunkInitCommon.unmarshal(i.raw); err != nil { 62 | return fmt.Errorf("%w: %v", ErrInitAckUnmarshalFailed, err) //nolint:errorlint 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (i *chunkInitAck) marshal() ([]byte, error) { 69 | initShared, err := i.chunkInitCommon.marshal() 70 | if err != nil { 71 | return nil, fmt.Errorf("%w: %v", ErrInitCommonDataMarshalFailed, err) //nolint:errorlint 72 | } 73 | 74 | i.chunkHeader.typ = ctInitAck 75 | i.chunkHeader.raw = initShared 76 | 77 | return i.chunkHeader.marshal() 78 | } 79 | 80 | func (i *chunkInitAck) check() (abort bool, err error) { 81 | // The receiver of the INIT ACK records the value of the Initiate Tag 82 | // parameter. This value MUST be placed into the Verification Tag 83 | // field of every SCTP packet that the INIT ACK receiver transmits 84 | // within this association. 85 | // 86 | // The Initiate Tag MUST NOT take the value 0. See Section 5.3.1 for 87 | // more on the selection of the Initiate Tag value. 88 | // 89 | // If the value of the Initiate Tag in a received INIT ACK chunk is 90 | // found to be 0, the receiver MUST destroy the association 91 | // discarding its TCB. The receiver MAY send an ABORT for debugging 92 | // purpose. 93 | if i.initiateTag == 0 { 94 | abort = true 95 | 96 | return abort, ErrChunkTypeInitAckInitateTagZero 97 | } 98 | 99 | // Defines the maximum number of streams the sender of this INIT ACK 100 | // chunk allows the peer end to create in this association. The 101 | // value 0 MUST NOT be used. 102 | // 103 | // Note: There is no negotiation of the actual number of streams but 104 | // instead the two endpoints will use the min(requested, offered). 105 | // See Section 5.1.1 for details. 106 | // 107 | // Note: A receiver of an INIT ACK with the MIS value set to 0 SHOULD 108 | // destroy the association discarding its TCB. 109 | if i.numInboundStreams == 0 { 110 | abort = true 111 | 112 | return abort, ErrInitAckInboundStreamRequestZero 113 | } 114 | 115 | // Defines the number of outbound streams the sender of this INIT ACK 116 | // chunk wishes to create in this association. The value of 0 MUST 117 | // NOT be used, and the value MUST NOT be greater than the MIS value 118 | // sent in the INIT chunk. 119 | // 120 | // Note: A receiver of an INIT ACK with the OS value set to 0 SHOULD 121 | // destroy the association discarding its TCB. 122 | 123 | if i.numOutboundStreams == 0 { 124 | abort = true 125 | 126 | return abort, ErrInitAckOutboundStreamRequestZero 127 | } 128 | 129 | // An SCTP receiver MUST be able to receive a minimum of 1500 bytes in 130 | // one SCTP packet. This means that an SCTP endpoint MUST NOT indicate 131 | // less than 1500 bytes in its initial a_rwnd sent in the INIT or INIT 132 | // ACK. 133 | if i.advertisedReceiverWindowCredit < 1500 { 134 | abort = true 135 | 136 | return abort, ErrInitAckAdvertisedReceiver1500 137 | } 138 | 139 | return false, nil 140 | } 141 | 142 | // String makes chunkInitAck printable. 143 | func (i *chunkInitAck) String() string { 144 | return fmt.Sprintf("%s\n%s", i.chunkHeader, i.chunkInitCommon) 145 | } 146 | -------------------------------------------------------------------------------- /chunk_selective_ack.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "encoding/binary" 8 | "errors" 9 | "fmt" 10 | ) 11 | 12 | /* 13 | chunkSelectiveAck represents an SCTP Chunk of type SACK 14 | 15 | This chunk is sent to the peer endpoint to acknowledge received DATA 16 | chunks and to inform the peer endpoint of gaps in the received 17 | subsequences of DATA chunks as represented by their TSNs. 18 | 0 1 2 3 19 | 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 20 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 21 | | Type = 3 |Chunk Flags | Chunk Length | 22 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 23 | | Cumulative TSN Ack | 24 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 25 | | Advertised Receiver Window Credit (a_rwnd) | 26 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 27 | | Number of Gap Ack Blocks = N | Number of Duplicate TSNs = X | 28 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 29 | | Gap Ack Block #1 Start | Gap Ack Block #1 End | 30 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 31 | / / 32 | \ ... \ 33 | / / 34 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 35 | | Gap Ack Block #N Start | Gap Ack Block #N End | 36 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 37 | | Duplicate TSN 1 | 38 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 39 | / / 40 | \ ... \ 41 | / / 42 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 43 | | Duplicate TSN X | 44 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ 45 | */ 46 | 47 | type gapAckBlock struct { 48 | start uint16 49 | end uint16 50 | } 51 | 52 | // Selective ack chunk errors. 53 | var ( 54 | ErrChunkTypeNotSack = errors.New("ChunkType is not of type SACK") 55 | ErrSackSizeNotLargeEnoughInfo = errors.New("SACK Chunk size is not large enough to contain header") 56 | ErrSackSizeNotMatchPredicted = errors.New("SACK Chunk size does not match predicted amount from header values") 57 | ) 58 | 59 | // String makes gapAckBlock printable. 60 | func (g gapAckBlock) String() string { 61 | return fmt.Sprintf("%d - %d", g.start, g.end) 62 | } 63 | 64 | type chunkSelectiveAck struct { 65 | chunkHeader 66 | cumulativeTSNAck uint32 67 | advertisedReceiverWindowCredit uint32 68 | gapAckBlocks []gapAckBlock 69 | duplicateTSN []uint32 70 | } 71 | 72 | const ( 73 | selectiveAckHeaderSize = 12 74 | ) 75 | 76 | func (s *chunkSelectiveAck) unmarshal(raw []byte) error { 77 | if err := s.chunkHeader.unmarshal(raw); err != nil { 78 | return err 79 | } 80 | 81 | if s.typ != ctSack { 82 | return fmt.Errorf("%w: actually is %s", ErrChunkTypeNotSack, s.typ.String()) 83 | } 84 | 85 | if len(s.raw) < selectiveAckHeaderSize { 86 | return fmt.Errorf("%w: %v remaining, needs %v bytes", ErrSackSizeNotLargeEnoughInfo, 87 | len(s.raw), selectiveAckHeaderSize) 88 | } 89 | 90 | s.cumulativeTSNAck = binary.BigEndian.Uint32(s.raw[0:]) 91 | s.advertisedReceiverWindowCredit = binary.BigEndian.Uint32(s.raw[4:]) 92 | s.gapAckBlocks = make([]gapAckBlock, binary.BigEndian.Uint16(s.raw[8:])) 93 | s.duplicateTSN = make([]uint32, binary.BigEndian.Uint16(s.raw[10:])) 94 | 95 | if len(s.raw) != selectiveAckHeaderSize+(4*len(s.gapAckBlocks)+(4*len(s.duplicateTSN))) { 96 | return ErrSackSizeNotMatchPredicted 97 | } 98 | 99 | offset := selectiveAckHeaderSize 100 | for i := range s.gapAckBlocks { 101 | s.gapAckBlocks[i].start = binary.BigEndian.Uint16(s.raw[offset:]) 102 | s.gapAckBlocks[i].end = binary.BigEndian.Uint16(s.raw[offset+2:]) 103 | offset += 4 104 | } 105 | for i := range s.duplicateTSN { 106 | s.duplicateTSN[i] = binary.BigEndian.Uint32(s.raw[offset:]) 107 | offset += 4 108 | } 109 | 110 | return nil 111 | } 112 | 113 | func (s *chunkSelectiveAck) marshal() ([]byte, error) { 114 | sackRaw := make([]byte, selectiveAckHeaderSize+(4*len(s.gapAckBlocks)+(4*len(s.duplicateTSN)))) 115 | binary.BigEndian.PutUint32(sackRaw[0:], s.cumulativeTSNAck) 116 | binary.BigEndian.PutUint32(sackRaw[4:], s.advertisedReceiverWindowCredit) 117 | binary.BigEndian.PutUint16(sackRaw[8:], uint16(len(s.gapAckBlocks))) //nolint:gosec // G115 118 | binary.BigEndian.PutUint16(sackRaw[10:], uint16(len(s.duplicateTSN))) //nolint:gosec // G115 119 | offset := selectiveAckHeaderSize 120 | for _, g := range s.gapAckBlocks { 121 | binary.BigEndian.PutUint16(sackRaw[offset:], g.start) 122 | binary.BigEndian.PutUint16(sackRaw[offset+2:], g.end) 123 | offset += 4 124 | } 125 | for _, t := range s.duplicateTSN { 126 | binary.BigEndian.PutUint32(sackRaw[offset:], t) 127 | offset += 4 128 | } 129 | 130 | s.chunkHeader.typ = ctSack 131 | s.chunkHeader.raw = sackRaw 132 | 133 | return s.chunkHeader.marshal() 134 | } 135 | 136 | func (s *chunkSelectiveAck) check() (abort bool, err error) { 137 | return false, nil 138 | } 139 | 140 | // String makes chunkSelectiveAck printable. 141 | func (s *chunkSelectiveAck) String() string { 142 | res := fmt.Sprintf("SACK cumTsnAck=%d arwnd=%d dupTsn=%d", 143 | s.cumulativeTSNAck, 144 | s.advertisedReceiverWindowCredit, 145 | s.duplicateTSN) 146 | 147 | for _, gap := range s.gapAckBlocks { 148 | res = fmt.Sprintf("%s\n gap ack: %s", res, gap) 149 | } 150 | 151 | return res 152 | } 153 | -------------------------------------------------------------------------------- /receive_payload_queue.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "fmt" 8 | "math/bits" 9 | ) 10 | 11 | type receivePayloadQueue struct { 12 | tailTSN uint32 13 | chunkSize int 14 | tsnBitmask []uint64 15 | dupTSN []uint32 16 | maxTSNOffset uint32 17 | 18 | cumulativeTSN uint32 19 | } 20 | 21 | func newReceivePayloadQueue(maxTSNOffset uint32) *receivePayloadQueue { 22 | maxTSNOffset = ((maxTSNOffset + 63) / 64) * 64 23 | 24 | return &receivePayloadQueue{ 25 | tsnBitmask: make([]uint64, maxTSNOffset/64), 26 | maxTSNOffset: maxTSNOffset, 27 | } 28 | } 29 | 30 | func (q *receivePayloadQueue) init(cumulativeTSN uint32) { 31 | q.cumulativeTSN = cumulativeTSN 32 | q.tailTSN = cumulativeTSN 33 | q.chunkSize = 0 34 | for i := range q.tsnBitmask { 35 | q.tsnBitmask[i] = 0 36 | } 37 | q.dupTSN = q.dupTSN[:0] 38 | } 39 | 40 | func (q *receivePayloadQueue) hasChunk(tsn uint32) bool { 41 | if q.chunkSize == 0 || sna32LTE(tsn, q.cumulativeTSN) || sna32GT(tsn, q.tailTSN) { 42 | return false 43 | } 44 | 45 | index, offset := int(tsn/64)%len(q.tsnBitmask), tsn%64 46 | 47 | return q.tsnBitmask[index]&(1<> uint64(start)) //nolint:gosec // G115 197 | 198 | return i + start, i+start < end 199 | } 200 | 201 | func getFirstZeroBit(val uint64, start, end int) (int, bool) { 202 | return getFirstNonZeroBit(^val, start, end) 203 | } 204 | -------------------------------------------------------------------------------- /chunk_heartbeat_ack_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/assert" 10 | ) 11 | 12 | func TestChunkHeartbeatAck_UnmarshalMarshal_Success(t *testing.T) { 13 | tt := []struct { 14 | name string 15 | info []byte 16 | }{ 17 | {"empty-info", []byte{}}, 18 | {"aligned-4", []byte{0x01, 0x02, 0x03, 0x04}}, 19 | {"non-aligned-5", []byte{0xaa, 0xbb, 0xcc, 0xdd, 0xee}}, 20 | } 21 | 22 | for _, tc := range tt { 23 | p := ¶mHeartbeatInfo{heartbeatInformation: tc.info} 24 | pp, err := p.marshal() 25 | if !assert.NoErrorf(t, err, "marshal paramHeartbeatInfo for %s", tc.name) { 26 | continue 27 | } 28 | 29 | ch := chunkHeader{ 30 | typ: ctHeartbeatAck, 31 | flags: 0, 32 | raw: pp, 33 | } 34 | 35 | encoded, err := ch.marshal() 36 | if !assert.NoErrorf(t, err, "marshal chunkHeader for %s", tc.name) { 37 | continue 38 | } 39 | 40 | hbAck := &chunkHeartbeatAck{} 41 | err = hbAck.unmarshal(encoded) 42 | if !assert.NoErrorf(t, err, "unmarshal HeartbeatAck for %s", tc.name) { 43 | continue 44 | } 45 | 46 | assert.Equalf(t, ctHeartbeatAck, hbAck.typ, "chunk type for %s", tc.name) 47 | if assert.Lenf(t, hbAck.params, 1, "params length for %s", tc.name) { 48 | got, ok := hbAck.params[0].(*paramHeartbeatInfo) 49 | if assert.Truef(t, ok, "param type for %s", tc.name) { 50 | assert.Equalf(t, tc.info, got.heartbeatInformation, "heartbeat info for %s", tc.name) 51 | } 52 | } 53 | 54 | roundTrip, err := hbAck.marshal() 55 | if !assert.NoErrorf(t, err, "marshal HeartbeatAck (round-trip) for %s", tc.name) { 56 | continue 57 | } 58 | assert.Equalf(t, encoded, roundTrip, "round-trip bytes for %s", tc.name) 59 | } 60 | } 61 | 62 | func TestChunkHeartbeatAck_Unmarshal_EmptyBody(t *testing.T) { 63 | ch := chunkHeader{ 64 | typ: ctHeartbeatAck, 65 | flags: 0, 66 | raw: nil, 67 | } 68 | 69 | raw, err := ch.marshal() 70 | if !assert.NoError(t, err) { 71 | return 72 | } 73 | 74 | hbAck := &chunkHeartbeatAck{} 75 | err = hbAck.unmarshal(raw) 76 | if !assert.NoError(t, err) { 77 | return 78 | } 79 | 80 | assert.Nil(t, hbAck.params) 81 | } 82 | 83 | func TestChunkHeartbeatAck_Unmarshal_Failure_WrongChunkType(t *testing.T) { 84 | p := ¶mHeartbeatInfo{heartbeatInformation: []byte{0x01, 0x02, 0x03, 0x04}} 85 | pp, err := p.marshal() 86 | if !assert.NoError(t, err) { 87 | return 88 | } 89 | 90 | ch := chunkHeader{ 91 | typ: ctHeartbeat, // wrong type on purpose 92 | flags: 0, 93 | raw: pp, 94 | } 95 | 96 | raw, err := ch.marshal() 97 | if !assert.NoError(t, err) { 98 | return 99 | } 100 | 101 | hbAck := &chunkHeartbeatAck{} 102 | err = hbAck.unmarshal(raw) 103 | assert.Error(t, err) 104 | assert.ErrorIs(t, err, ErrChunkTypeNotHeartbeatAck) 105 | } 106 | 107 | func TestChunkHeartbeatAck_Unmarshal_Failure_BodyTooShort(t *testing.T) { 108 | // less than initOptionalVarHeaderLength bytes in the body. 109 | body := []byte{0xaa, 0xbb, 0xcc} 110 | 111 | ch := chunkHeader{ 112 | typ: ctHeartbeatAck, 113 | flags: 0, 114 | raw: body, 115 | } 116 | 117 | raw, err := ch.marshal() 118 | if !assert.NoError(t, err) { 119 | return 120 | } 121 | 122 | hbAck := &chunkHeartbeatAck{} 123 | err = hbAck.unmarshal(raw) 124 | assert.Error(t, err) 125 | assert.ErrorIs(t, err, ErrHeartbeatAckParams) 126 | } 127 | 128 | func TestChunkHeartbeatAck_Unmarshal_Failure_TruncatedParamBody(t *testing.T) { 129 | // build a valid HeartbeatInfo TLV, then truncate it so the advertised 130 | // length is larger than the available bytes. 131 | info := []byte{0x11, 0x22, 0x33, 0x44} 132 | p := ¶mHeartbeatInfo{heartbeatInformation: info} 133 | pp, err := p.marshal() 134 | if !assert.NoError(t, err) { 135 | return 136 | } 137 | 138 | if !assert.GreaterOrEqual(t, len(pp), initOptionalVarHeaderLength+1) { 139 | return 140 | } 141 | 142 | truncated := pp[:len(pp)-1] 143 | 144 | ch := chunkHeader{ 145 | typ: ctHeartbeatAck, 146 | flags: 0, 147 | raw: truncated, 148 | } 149 | 150 | raw, err := ch.marshal() 151 | if !assert.NoError(t, err) { 152 | return 153 | } 154 | 155 | hbAck := &chunkHeartbeatAck{} 156 | err = hbAck.unmarshal(raw) 157 | assert.Error(t, err) 158 | assert.ErrorIs(t, err, ErrHeartbeatAckParams) 159 | } 160 | 161 | func TestChunkHeartbeatAck_Unmarshal_Failure_WrongParamType(t *testing.T) { 162 | // construct a TLV with a param type that is *not* heartbeatInfo but is otherwise valid. 163 | plen := uint16(initOptionalVarHeaderLength) 164 | body := []byte{ 165 | 0x00, 0x02, // some param type != heartbeatInfo (which is 1 per RFC) 166 | byte(plen >> 8), 167 | byte(plen & 0xff), 168 | } 169 | 170 | ch := chunkHeader{ 171 | typ: ctHeartbeatAck, 172 | flags: 0, 173 | raw: body, 174 | } 175 | 176 | raw, err := ch.marshal() 177 | if !assert.NoError(t, err) { 178 | return 179 | } 180 | 181 | hbAck := &chunkHeartbeatAck{} 182 | err = hbAck.unmarshal(raw) 183 | assert.Error(t, err) 184 | assert.ErrorIs(t, err, ErrHeartbeatAckNotHeartbeatInfo) 185 | } 186 | 187 | func TestChunkHeartbeatAck_Unmarshal_Failure_TrailingNonZero(t *testing.T) { 188 | info := []byte{0x01, 0x02, 0x03, 0x04} 189 | p := ¶mHeartbeatInfo{heartbeatInformation: info} 190 | pp, err := p.marshal() 191 | if !assert.NoError(t, err) { 192 | return 193 | } 194 | 195 | // append a non-zero byte after the single parameter. 196 | body := append(append([]byte{}, pp...), 0x01) 197 | 198 | ch := chunkHeader{ 199 | typ: ctHeartbeatAck, 200 | flags: 0, 201 | raw: body, 202 | } 203 | 204 | raw, err := ch.marshal() 205 | if !assert.NoError(t, err) { 206 | return 207 | } 208 | 209 | hbAck := &chunkHeartbeatAck{} 210 | err = hbAck.unmarshal(raw) 211 | assert.Error(t, err) 212 | assert.ErrorIs(t, err, ErrHeartbeatExtraNonZero) 213 | } 214 | 215 | func TestChunkHeartbeatAck_Marshal_Failure_ParamCount(t *testing.T) { 216 | // no params 217 | hbAck := &chunkHeartbeatAck{} 218 | _, err := hbAck.marshal() 219 | assert.Error(t, err) 220 | assert.ErrorIs(t, err, ErrHeartbeatAckParams) 221 | 222 | // too many params 223 | p := ¶mHeartbeatInfo{} 224 | hbAck = &chunkHeartbeatAck{ 225 | params: []param{p, p}, 226 | } 227 | _, err = hbAck.marshal() 228 | assert.Error(t, err) 229 | assert.ErrorIs(t, err, ErrHeartbeatAckParams) 230 | } 231 | -------------------------------------------------------------------------------- /receive_payload_queue_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package sctp 5 | 6 | import ( 7 | "math" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestReceivePayloadQueue(t *testing.T) { 14 | maxOffset := uint32(512) 15 | payloadQueue := newReceivePayloadQueue(maxOffset) 16 | initTSN := uint32(math.MaxUint32 - 10) 17 | payloadQueue.init(initTSN - 2) 18 | assert.Equal(t, initTSN-2, payloadQueue.getcumulativeTSN()) 19 | assert.Zero(t, payloadQueue.size()) 20 | _, ok := payloadQueue.getLastTSNReceived() 21 | assert.False(t, ok) 22 | assert.Empty(t, payloadQueue.getGapAckBlocks()) 23 | // force pop empy queue to advance cumulative TSN 24 | assert.False(t, payloadQueue.pop(true)) 25 | assert.Equal(t, initTSN-1, payloadQueue.getcumulativeTSN()) 26 | assert.Zero(t, payloadQueue.size()) 27 | assert.Empty(t, payloadQueue.getGapAckBlocks()) 28 | 29 | nextTSN := initTSN + maxOffset - 1 30 | assert.True(t, payloadQueue.push(nextTSN)) 31 | assert.Equal(t, 1, payloadQueue.size()) 32 | lastTSN, ok := payloadQueue.getLastTSNReceived() 33 | assert.Truef(t, lastTSN == nextTSN && ok, "lastTSN:%d, ok:%t", lastTSN, ok) 34 | assert.True(t, payloadQueue.hasChunk(nextTSN)) 35 | 36 | assert.True(t, payloadQueue.push(initTSN)) 37 | assert.False(t, payloadQueue.canPush(initTSN-1)) 38 | assert.False(t, payloadQueue.canPush(initTSN+maxOffset)) 39 | assert.False(t, payloadQueue.push(initTSN+maxOffset)) 40 | assert.True(t, payloadQueue.canPush(nextTSN-1)) 41 | assert.Equal(t, 2, payloadQueue.size()) 42 | 43 | gaps := payloadQueue.getGapAckBlocks() 44 | assert.EqualValues(t, []gapAckBlock{ 45 | {start: uint16(1), end: uint16(1)}, 46 | {start: uint16(maxOffset), end: uint16(maxOffset)}, 47 | }, gaps) 48 | 49 | assert.True(t, payloadQueue.pop(false)) 50 | assert.Equal(t, 1, payloadQueue.size()) 51 | assert.Equal(t, initTSN, payloadQueue.cumulativeTSN) 52 | assert.False(t, payloadQueue.pop(false)) 53 | assert.Equal(t, initTSN, payloadQueue.cumulativeTSN) 54 | 55 | size := payloadQueue.size() 56 | // push tsn with two gap 57 | // tsnRange [[start,end]...] 58 | tsnRange := [][]uint32{ 59 | {initTSN + 5, initTSN + 6}, 60 | {initTSN + 9, initTSN + 140}, 61 | } 62 | range0, range1 := tsnRange[0], tsnRange[1] 63 | for tsn := range0[0]; sna32LTE(tsn, range0[1]); tsn++ { 64 | assert.True(t, payloadQueue.push(tsn)) 65 | assert.False(t, payloadQueue.pop(false)) 66 | assert.True(t, payloadQueue.hasChunk(tsn)) 67 | } 68 | size += int(range0[1] - range0[0] + 1) 69 | 70 | for tsn := range1[0]; sna32LTE(tsn, range1[1]); tsn++ { 71 | assert.True(t, payloadQueue.push(tsn)) 72 | assert.False(t, payloadQueue.pop(false)) 73 | assert.True(t, payloadQueue.hasChunk(tsn)) 74 | } 75 | size += int(range1[1] - range1[0] + 1) 76 | 77 | assert.Equal(t, size, payloadQueue.size()) 78 | gaps = payloadQueue.getGapAckBlocks() 79 | assert.EqualValues(t, []gapAckBlock{ 80 | //nolint:gosec // G115 81 | {start: uint16(range0[0] - initTSN), end: uint16(range0[1] - initTSN)}, 82 | //nolint:gosec // G115 83 | {start: uint16(range1[0] - initTSN), end: uint16(range1[1] - initTSN)}, 84 | //nolint:gosec // G115 85 | {start: uint16(nextTSN - initTSN), end: uint16(nextTSN - initTSN)}, 86 | }, gaps) 87 | 88 | // push duplicate tsns 89 | assert.False(t, payloadQueue.push(initTSN-2)) 90 | assert.False(t, payloadQueue.push(range0[0])) 91 | assert.False(t, payloadQueue.push(range0[0])) 92 | assert.False(t, payloadQueue.push(nextTSN)) 93 | assert.False(t, payloadQueue.push(initTSN+maxOffset+1)) 94 | duplicates := payloadQueue.popDuplicates() 95 | assert.EqualValues(t, []uint32{initTSN - 2, range0[0], range0[0], nextTSN}, duplicates) 96 | 97 | // force pop to advance cumulativeTSN to fill the gap [initTSN, initTSN+4] 98 | for tsn := initTSN + 1; sna32LT(tsn, range0[0]); tsn++ { 99 | assert.False(t, payloadQueue.pop(true)) 100 | assert.Equal(t, size, payloadQueue.size()) 101 | assert.Equal(t, tsn, payloadQueue.cumulativeTSN) 102 | } 103 | 104 | for tsn := range0[0]; sna32LTE(tsn, range0[1]); tsn++ { 105 | assert.True(t, payloadQueue.pop(false)) 106 | assert.Equal(t, tsn, payloadQueue.getcumulativeTSN()) 107 | } 108 | assert.False(t, payloadQueue.pop(false)) 109 | cumulativeTSN := payloadQueue.getcumulativeTSN() 110 | assert.Equal(t, range0[1], cumulativeTSN) 111 | gaps = payloadQueue.getGapAckBlocks() 112 | assert.EqualValues(t, []gapAckBlock{ 113 | //nolint:gosec // G115 114 | {start: uint16(range1[0] - range0[1]), end: uint16(range1[1] - range0[1])}, 115 | //nolint:gosec // G115 116 | {start: uint16(nextTSN - range0[1]), end: uint16(nextTSN - range0[1])}, 117 | }, gaps) 118 | 119 | // fill the gap with received tsn 120 | for tsn := range0[1] + 1; sna32LT(tsn, range1[0]); tsn++ { 121 | assert.True(t, payloadQueue.push(tsn), tsn) 122 | } 123 | for tsn := range0[1] + 1; sna32LTE(tsn, range1[1]); tsn++ { 124 | assert.True(t, payloadQueue.pop(false)) 125 | assert.Equal(t, tsn, payloadQueue.getcumulativeTSN()) 126 | } 127 | assert.False(t, payloadQueue.pop(false)) 128 | assert.Equal(t, range1[1], payloadQueue.getcumulativeTSN()) 129 | gaps = payloadQueue.getGapAckBlocks() 130 | assert.EqualValues(t, []gapAckBlock{ 131 | //nolint:gosec // G115 132 | {start: uint16(nextTSN - range1[1]), end: uint16(nextTSN - range1[1])}, 133 | }, gaps) 134 | 135 | // gap block cross end tsn 136 | endTSN := maxOffset - 1 137 | for tsn := nextTSN + 1; sna32LTE(tsn, endTSN); tsn++ { 138 | assert.True(t, payloadQueue.push(tsn)) 139 | } 140 | gaps = payloadQueue.getGapAckBlocks() 141 | assert.EqualValues(t, []gapAckBlock{ 142 | //nolint:gosec // G115 143 | {start: uint16(nextTSN - range1[1]), end: uint16(endTSN - range1[1])}, 144 | }, gaps) 145 | 146 | assert.NotEmpty(t, payloadQueue.getGapAckBlocksString()) 147 | } 148 | 149 | func TestBitfunc(t *testing.T) { 150 | idx, ok := getFirstNonZeroBit(0xf, 0, 20) 151 | assert.True(t, ok) 152 | assert.Equal(t, 0, idx) 153 | _, ok = getFirstNonZeroBit(0xf<<20, 0, 20) 154 | assert.False(t, ok) 155 | idx, ok = getFirstNonZeroBit(0xf<<20, 5, 25) 156 | assert.True(t, ok) 157 | assert.Equal(t, 20, idx) 158 | _, ok = getFirstNonZeroBit(0xf<<20, 30, 40) 159 | assert.False(t, ok) 160 | _, ok = getFirstNonZeroBit(0, 0, 64) 161 | assert.False(t, ok) 162 | 163 | idx, ok = getFirstZeroBit(0xf, 0, 20) 164 | assert.True(t, ok) 165 | assert.Equal(t, 4, idx) 166 | idx, ok = getFirstZeroBit(0xf<<20, 0, 20) 167 | assert.True(t, ok) 168 | assert.Equal(t, 0, idx) 169 | _, ok = getFirstZeroBit(0xf<<20, 20, 24) 170 | assert.False(t, ok) 171 | idx, ok = getFirstZeroBit(0xf<<20, 30, 40) 172 | assert.True(t, ok) 173 | assert.Equal(t, 30, idx) 174 | _, ok = getFirstZeroBit(math.MaxUint64, 0, 64) 175 | assert.False(t, ok) 176 | } 177 | --------------------------------------------------------------------------------