pax_global_header00006660000000000000000000000064144455215000014512gustar00rootroot0000000000000052 comment=e56f1f82cd67337fd13713c396d4e942d10c5c36 stun-0.6.1/000077500000000000000000000000001444552150000125075ustar00rootroot00000000000000stun-0.6.1/.github/000077500000000000000000000000001444552150000140475ustar00rootroot00000000000000stun-0.6.1/.github/.ci.conf000077500000000000000000000005461444552150000153770ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT TEST_BENCH_OPTION= TEST_HOOK=_test_hook EXCLUDED_CONTRIBUTORS=('fossabot' 'John Bradley') function _test_hook(){ set -e # test with "debug" tag go test -tags debug ./... # test concurrency go test -race -cpu=1,2,4 -run TestClient_DoConcurrent } stun-0.6.1/.github/.gitignore000066400000000000000000000001561444552150000160410ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT .goassets stun-0.6.1/.github/fetch-scripts.sh000077500000000000000000000016001444552150000171610ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT set -eu SCRIPT_PATH="$(realpath "$(dirname "$0")")" GOASSETS_PATH="${SCRIPT_PATH}/.goassets" GOASSETS_REF=${GOASSETS_REF:-master} if [ -d "${GOASSETS_PATH}" ]; then if ! git -C "${GOASSETS_PATH}" diff --exit-code; then echo "${GOASSETS_PATH} has uncommitted changes" >&2 exit 1 fi git -C "${GOASSETS_PATH}" fetch origin git -C "${GOASSETS_PATH}" checkout ${GOASSETS_REF} git -C "${GOASSETS_PATH}" reset --hard origin/${GOASSETS_REF} else git clone -b ${GOASSETS_REF} https://github.com/pion/.goassets.git "${GOASSETS_PATH}" fi stun-0.6.1/.github/install-hooks.sh000077500000000000000000000012421444552150000171740ustar00rootroot00000000000000#!/bin/sh # # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT SCRIPT_PATH="$(realpath "$(dirname "$0")")" . ${SCRIPT_PATH}/fetch-scripts.sh cp "${GOASSETS_PATH}/hooks/commit-msg.sh" "${SCRIPT_PATH}/../.git/hooks/commit-msg" cp "${GOASSETS_PATH}/hooks/pre-commit.sh" "${SCRIPT_PATH}/../.git/hooks/pre-commit" cp "${GOASSETS_PATH}/hooks/pre-push.sh" "${SCRIPT_PATH}/../.git/hooks/pre-push" stun-0.6.1/.github/workflows/000077500000000000000000000000001444552150000161045ustar00rootroot00000000000000stun-0.6.1/.github/workflows/codeql-analysis.yml000066400000000000000000000013201444552150000217130ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: CodeQL on: workflow_dispatch: schedule: - cron: '23 5 * * 0' pull_request: branches: - master paths: - '**.go' jobs: analyze: uses: pion/.goassets/.github/workflows/codeql-analysis.reusable.yml@master stun-0.6.1/.github/workflows/generate-authors.yml000066400000000000000000000012471444552150000221100ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Generate Authors on: pull_request: jobs: generate: uses: pion/.goassets/.github/workflows/generate-authors.reusable.yml@master secrets: token: ${{ secrets.PIONBOT_PRIVATE_KEY }} stun-0.6.1/.github/workflows/lint.yaml000066400000000000000000000011151444552150000177340ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Lint on: pull_request: jobs: lint: uses: pion/.goassets/.github/workflows/lint.reusable.yml@master stun-0.6.1/.github/workflows/release.yml000066400000000000000000000012501444552150000202450ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Release on: push: tags: - 'v*' jobs: release: uses: pion/.goassets/.github/workflows/release.reusable.yml@master with: go-version: '1.20' # auto-update/latest-go-version stun-0.6.1/.github/workflows/renovate-go-sum-fix.yaml000066400000000000000000000012671444552150000226120ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Fix go.sum on: push: branches: - renovate/* jobs: fix: uses: pion/.goassets/.github/workflows/renovate-go-sum-fix.reusable.yml@master secrets: token: ${{ secrets.PIONBOT_PRIVATE_KEY }} stun-0.6.1/.github/workflows/reuse.yml000066400000000000000000000011511444552150000177500ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: REUSE Compliance Check on: push: pull_request: jobs: lint: uses: pion/.goassets/.github/workflows/reuse.reusable.yml@master stun-0.6.1/.github/workflows/test.yaml000066400000000000000000000022551444552150000177530ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Test on: push: branches: - master pull_request: jobs: test: uses: pion/.goassets/.github/workflows/test.reusable.yml@master strategy: matrix: go: ['1.20', '1.19'] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-i386: uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master strategy: matrix: go: ['1.20', '1.19'] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} test-wasm: uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master with: go-version: '1.20' # auto-update/latest-go-version stun-0.6.1/.github/workflows/tidy-check.yaml000066400000000000000000000013021444552150000210100ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # If this repository should have package specific CI config, # remove the repository name from .goassets/.github/workflows/assets-sync.yml. # # If you want to update the shared CI config, send a PR to # https://github.com/pion/.goassets instead of this repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT name: Go mod tidy on: pull_request: push: branches: - master jobs: tidy: uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master with: go-version: '1.20' # auto-update/latest-go-version stun-0.6.1/.gitignore000066400000000000000000000006321444552150000145000ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT ### JetBrains IDE ### ##################### .idea/ ### Emacs Temporary Files ### ############################# *~ ### Folders ### ############### bin/ vendor/ node_modules/ ### Files ### ############# *.ivf *.ogg tags cover.out *.sw[poe] *.wasm examples/sfu-ws/cert.pem examples/sfu-ws/key.pem wasm_exec.js stun-0.6.1/.golangci.yml000066400000000000000000000202341444552150000150740ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT linters-settings: govet: check-shadowing: true misspell: locale: US exhaustive: default-signifies-exhaustive: true gomodguard: blocked: modules: - github.com/pkg/errors: recommendations: - errors forbidigo: forbid: - ^fmt.Print(f|ln)?$ - ^log.(Panic|Fatal|Print)(f|ln)?$ - ^os.Exit$ - ^panic$ - ^print(ln)?$ linters: enable: - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers - bidichk # Checks for dangerous unicode character sequences - bodyclose # checks whether HTTP response body is closed successfully - contextcheck # check the function whether use a non-inherited context - decorder # check declaration order and count of types, constants, variables and functions - depguard # Go linter that checks if package imports are in a list of acceptable packages - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) - dupl # Tool for code clone detection - durationcheck # check for two durations multiplied together - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases - errchkjson # Checks types passed to the json encoding functions. Reports unsupported types and optionally reports occations, where the check for the returned error can be omitted. - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. - errorlint # errorlint is a linter for that can be used to find code that will cause problems with the error wrapping scheme introduced in Go 1.13. - exhaustive # check exhaustiveness of enum switch statements - exportloopref # checks for pointers to enclosing loop variables - forbidigo # Forbids identifiers - forcetypeassert # finds forced type assertions - gci # Gci control golang package import order and make it always deterministic. - gochecknoglobals # Checks that no globals are present in Go code - gochecknoinits # Checks that no init functions are present in Go code - gocognit # Computes and checks the cognitive complexity of functions - goconst # Finds repeated strings that could be replaced by a constant - gocritic # The most opinionated Go source code linter - godox # Tool for detection of FIXME, TODO and other comment keywords - goerr113 # Golang linter to check the errors handling expressions - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification - gofumpt # Gofumpt checks whether code was gofumpt-ed. - goheader # Checks is file header matches to pattern - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. - gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations. - goprintffuncname # Checks that printf-like functions are named with `f` at the end - gosec # Inspects source code for security problems - gosimple # Linter for Go source code that specializes in simplifying a code - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string - grouper # An analyzer to analyze expression groups. - importas # Enforces consistent import aliases - ineffassign # Detects when assignments to existing variables are not used - misspell # Finds commonly misspelled English words in comments - nakedret # Finds naked returns in functions greater than a specified function length - nilerr # Finds the code that returns nil even if it checks that the error is not nil. - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. - noctx # noctx finds sending http request without context.Context - predeclared # find code that shadows one of Go's predeclared identifiers - revive # golint replacement, finds style mistakes - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks - stylecheck # Stylecheck is a replacement for golint - tagliatelle # Checks the struct tags. - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code - unconvert # Remove unnecessary type conversions - unparam # Reports unused function parameters - unused # Checks Go code for unused constants, variables, functions and types - wastedassign # wastedassign finds wasted assignment statements - whitespace # Tool for detection of leading and trailing whitespace disable: - containedctx # containedctx is a linter that detects struct contained context.Context field - cyclop # checks function and package cyclomatic complexity - exhaustivestruct # Checks if all struct's fields are initialized - funlen # Tool for detection of long functions - gocyclo # Computes and checks the cyclomatic complexity of functions - godot # Check if comments end in a period - gomnd # An analyzer to detect magic numbers. - ifshort # Checks that your code uses short syntax for if-statements whenever possible - ireturn # Accept Interfaces, Return Concrete Types - lll # Reports long lines - maintidx # maintidx measures the maintainability index of each function. - makezero # Finds slice declarations with non-zero initial length - maligned # Tool to detect Go structs that would take less memory if their fields were sorted - nestif # Reports deeply nested if statements - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity - nolintlint # Reports ill-formed or insufficient nolint directives - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test - prealloc # Finds slice declarations that could potentially be preallocated - promlinter # Check Prometheus metrics naming via promlint - rowserrcheck # checks whether Err of rows is checked successfully - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. - testpackage # linter that makes you use a separate _test package - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers - varnamelen # checks that the length of a variable's name matches its scope - wrapcheck # Checks that errors returned from external packages are wrapped - wsl # Whitespace Linter - Forces you to use empty lines! issues: exclude-use-default: false exclude-rules: # Allow complex tests, better to be self contained - path: _test\.go linters: - gocognit - forbidigo # Allow complex main function in examples - path: examples text: "of func `main` is high" linters: - gocognit # Allow forbidden identifiers in examples - path: examples linters: - forbidigo # Allow forbidden identifiers in CLI commands - path: cmd linters: - forbidigo run: skip-dirs-use-default: false stun-0.6.1/.goreleaser.yml000066400000000000000000000001711444552150000154370ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT builds: - skip: true stun-0.6.1/.reuse/000077500000000000000000000000001444552150000137105ustar00rootroot00000000000000stun-0.6.1/.reuse/dep5000066400000000000000000000007011444552150000144660ustar00rootroot00000000000000Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ Upstream-Name: Pion Source: https://github.com/pion/ Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum .eslintrc.json package.json examples/examples.json Copyright: 2023 The Pion community License: MIT Files: testdata/fuzz/* **/testdata/fuzz/* api/*.txt Copyright: 2023 The Pion community License: CC0-1.0 stun-0.6.1/AUTHORS.txt000066400000000000000000000027301444552150000143770ustar00rootroot00000000000000# Thank you to everyone that made Pion possible. If you are interested in contributing # we would love to have you https://github.com/pion/webrtc/wiki/Contributing # # This file is auto generated, using git to list all individuals contributors. # see https://github.com/pion/.goassets/blob/master/scripts/generate-authors.sh for the scripting Adam Kiss Aleksandr Razumov Aleksandr Razumov Atsushi Watanabe backkem Cecylia Bocovich Christian Muehlhaeuser David-dp- ernado ernado fossabot Frank Dietrich Hugo Arregui Jerry Tao jinleileiking John Bradley Juliusz Chroboczek Maanas Royy Moises Marangoni Raphael Randschau Sean DuBois Sean DuBois Sean DuBois songjiayang Steffen Vogel Vladislav Yarmak Will LE Y.Horie Yutaka Takeda ZHENK # List of contributors not appearing in Git history Aliaksandr Valialkin The IETF Trust The gortc project stun-0.6.1/LICENSE000066400000000000000000000021051444552150000135120ustar00rootroot00000000000000MIT License Copyright (c) 2023 The Pion community 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. stun-0.6.1/LICENSES/000077500000000000000000000000001444552150000137145ustar00rootroot00000000000000stun-0.6.1/LICENSES/BSD-3-Clause.txt000066400000000000000000000026641444552150000164470ustar00rootroot00000000000000Copyright (c) . Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. stun-0.6.1/LICENSES/CC0-1.0.txt000066400000000000000000000156101444552150000153210ustar00rootroot00000000000000Creative Commons Legal Code CC0 1.0 Universal CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED HEREUNDER. Statement of Purpose The laws of most jurisdictions throughout the world automatically confer exclusive Copyright and Related Rights (defined below) upon the creator and subsequent owner(s) (each and all, an "owner") of an original work of authorship and/or a database (each, a "Work"). Certain owners wish to permanently relinquish those rights to a Work for the purpose of contributing to a commons of creative, cultural and scientific works ("Commons") that the public can reliably and without fear of later claims of infringement build upon, modify, incorporate in other works, reuse and redistribute as freely as possible in any form whatsoever and for any purposes, including without limitation commercial purposes. These owners may contribute to the Commons to promote the ideal of a free culture and the further production of creative, cultural and scientific works, or to gain reputation or greater distribution for their Work in part through the use and efforts of others. For these and/or other purposes and motivations, and without any expectation of additional consideration or compensation, the person associating CC0 with a Work (the "Affirmer"), to the extent that he or she is an owner of Copyright and Related Rights in the Work, voluntarily elects to apply CC0 to the Work and publicly distribute the Work under its terms, with knowledge of his or her Copyright and Related Rights in the Work and the meaning and intended legal effect of CC0 on those rights. 1. Copyright and Related Rights. A Work made available under CC0 may be protected by copyright and related or neighboring rights ("Copyright and Related Rights"). Copyright and Related Rights include, but are not limited to, the following: i. the right to reproduce, adapt, distribute, perform, display, communicate, and translate a Work; ii. moral rights retained by the original author(s) and/or performer(s); iii. publicity and privacy rights pertaining to a person's image or likeness depicted in a Work; iv. rights protecting against unfair competition in regards to a Work, subject to the limitations in paragraph 4(a), below; v. rights protecting the extraction, dissemination, use and reuse of data in a Work; vi. database rights (such as those arising under Directive 96/9/EC of the European Parliament and of the Council of 11 March 1996 on the legal protection of databases, and under any national implementation thereof, including any amended or successor version of such directive); and vii. other similar, equivalent or corresponding rights throughout the world based on applicable law or treaty, and any national implementations thereof. 2. Waiver. To the greatest extent permitted by, but not in contravention of, applicable law, Affirmer hereby overtly, fully, permanently, irrevocably and unconditionally waives, abandons, and surrenders all of Affirmer's Copyright and Related Rights and associated claims and causes of action, whether now known or unknown (including existing as well as future claims and causes of action), in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each member of the public at large and to the detriment of Affirmer's heirs and successors, fully intending that such Waiver shall not be subject to revocation, rescission, cancellation, termination, or any other legal or equitable action to disrupt the quiet enjoyment of the Work by the public as contemplated by Affirmer's express Statement of Purpose. 3. Public License Fallback. Should any part of the Waiver for any reason be judged legally invalid or ineffective under applicable law, then the Waiver shall be preserved to the maximum extent permitted taking into account Affirmer's express Statement of Purpose. In addition, to the extent the Waiver is so judged Affirmer hereby grants to each affected person a royalty-free, non transferable, non sublicensable, non exclusive, irrevocable and unconditional license to exercise Affirmer's Copyright and Related Rights in the Work (i) in all territories worldwide, (ii) for the maximum duration provided by applicable law or treaty (including future time extensions), (iii) in any current or future medium and for any number of copies, and (iv) for any purpose whatsoever, including without limitation commercial, advertising or promotional purposes (the "License"). The License shall be deemed effective as of the date CC0 was applied by Affirmer to the Work. Should any part of the License for any reason be judged legally invalid or ineffective under applicable law, such partial invalidity or ineffectiveness shall not invalidate the remainder of the License, and in such case Affirmer hereby affirms that he or she will not (i) exercise any of his or her remaining Copyright and Related Rights in the Work or (ii) assert any associated claims and causes of action with respect to the Work, in either case contrary to Affirmer's express Statement of Purpose. 4. Limitations and Disclaimers. a. No trademark or patent rights held by Affirmer are waived, abandoned, surrendered, licensed or otherwise affected by this document. b. Affirmer offers the Work as-is and makes no representations or warranties of any kind concerning the Work, express, implied, statutory or otherwise, including without limitation warranties of title, merchantability, fitness for a particular purpose, non infringement, or the absence of latent or other defects, accuracy, or the present or absence of errors, whether or not discoverable, all to the greatest extent permissible under applicable law. c. Affirmer disclaims responsibility for clearing rights of other persons that may apply to the Work or any use thereof, including without limitation any person's Copyright and Related Rights in the Work. Further, Affirmer disclaims responsibility for obtaining any necessary consents, permissions or other rights required for any use of the Work. d. Affirmer understands and acknowledges that Creative Commons is not a party to this document and has no duty or obligation with respect to this CC0 or use of the Work. stun-0.6.1/LICENSES/MIT.txt000066400000000000000000000020661444552150000151120ustar00rootroot00000000000000MIT License Copyright (c) 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: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 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. stun-0.6.1/Makefile000066400000000000000000000020361444552150000141500ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT VERSION := $(shell git describe --tags | sed -e 's/^v//g' | awk -F "-" '{print $$1}') ITERATION := $(shell git describe --tags --long | awk -F "-" '{print $$2}') GO_VERSION=$(shell gobuild -v) GO := $(or $(GOROOT),/usr/lib/go)/bin/go PROCS := $(shell nproc) cores: @echo "cores: $(PROCS)" bench: go test -bench . bench-record: $(GO) test -bench . > "benchmarks/stun-go-$(GO_VERSION).txt" lint: @golangci-lint run ./... @echo "ok" escape: @echo "Not escapes, except autogenerated:" @go build -gcflags '-m -l' 2>&1 \ | grep -v "" \ | grep escapes format: goimports -w . bench-compare: go test -bench . > bench.go-16 go-tip test -bench . > bench.go-tip @benchcmp bench.go-16 bench.go-tip install: go get gortc.io/api go get -u github.com/golangci/golangci-lint/cmd/golangci-lint test-integration: @cd e2e && bash ./test.sh prepush: test lint test-integration check-api: @cd api && bash ./check.sh test: @./go.test.sh clean: stun-0.6.1/README.md000066400000000000000000000252011444552150000137660ustar00rootroot00000000000000


Pion STUN

A Go implementation of STUN

Pion stun Slack Widget
GitHub Workflow Status Go Reference Coverage Status Go Report Card License: MIT


Package `stun` implements Session Traversal Utilities for NAT (STUN) ([RFC 5389][rfc5389]) protocol and [client](https://pkg.go.dev/github.com/pion/stun#Client) with no external dependencies and zero allocations in hot paths. Client [supports](https://pkg.go.dev/github.com/pion/stun#WithRTO) automatic request retransmissions. ### Example You can get your current IP address from any STUN server by sending binding request. See more idiomatic example at `cmd/stun-client`. ```go package main import ( "fmt" "github.com/pion/stun" ) func main() { // Parse a STUN URI u, err := stun.ParseURI("stun:stun.l.google.com:19302") if err != nil { panic(err) } // Creating a "connection" to STUN server. c, err := stun.DialURI(u, &stun.DialConfig{}) if err != nil { panic(err) } // Building binding request with random transaction id. message := stun.MustBuild(stun.TransactionID, stun.BindingRequest) // Sending request to STUN server, waiting for response message. if err := c.Do(message, func(res stun.Event) { if res.Error != nil { panic(res.Error) } // Decoding XOR-MAPPED-ADDRESS attribute from message. var xorAddr stun.XORMappedAddress if err := xorAddr.GetFrom(res.Message); err != nil { panic(err) } fmt.Println("your IP is", xorAddr.IP) }); err != nil { panic(err) } } ``` ### RFCs #### Implemented - **RFC 5389**: [Session Traversal Utilities for NAT (STUN)][rfc5389] - **RFC 5769**: [Test Vectors for Session Traversal Utilities for NAT (STUN)][rfc5769] - **RFC 6062**: [Traversal Using Relays around NAT (TURN) Extensions for TCP Allocations][rfc6062] - **RFC 7064**: [URI Scheme for the Session Traversal Utilities for NAT (STUN) Protocol][rfc7064] - **RFC 7065**: [Traversal Using Relays around NAT (TURN) Uniform Resource Identifiers][rfc7065] - **RFC 5780**: [NAT Behavior Discovery Using Session Traversal Utilities for NAT (STUN)][rfc5780] via [cmd/stun-nat-behaviour](cmd/stun-nat-behaviour) - (TLS-over-)TCP client support #### Planned - **RFC 5389**: [ALTERNATE-SERVER](https://tools.ietf.org/html/rfc5389#section-11) support [#48](https://github.com/pion/stun/issues/48) #### Compatability notes [RFC 5389][rfc5389] obsoletes [RFC 3489][rfc3489], so implementation was ignored by purpose, however, [RFC 3489][rfc3489] can be easily implemented as separate package. [rfc3489]: https://tools.ietf.org/html/rfc3489 [rfc5389]: https://tools.ietf.org/html/rfc5389 [rfc5769]: https://tools.ietf.org/html/rfc5769 [rfc5780]: https://tools.ietf.org/html/rfc5780 [rfc6062]: https://tools.ietf.org/html/rfc6062 [rfc7064]: https://tools.ietf.org/html/rfc7064 [rfc7065]: https://tools.ietf.org/html/rfc7065 ### Stability Package is currently stable, no backward incompatible changes are expected with exception of critical bugs or security fixes. Additional attributes are unlikely to be implemented in scope of stun package, the only exception is constants for attribute or message types. ### Requirements Go 1.12 is currently supported and tested in CI. ### Testing Client behavior is tested and verified in many ways: * End-To-End with long-term credentials * **coturn**: The coturn [server](https://github.com/coturn/coturn/wiki/turnserver) (linux) * Bunch of code static checkers (linters) * Standard unit-tests with coverage reporting (linux {amd64, **arm**64}, windows and darwin) * Explicit API backward compatibility [check](https://github.com/gortc/api), see `api` directory See [TeamCity project](https://tc.gortc.io/project.html?projectId=stun&guest=1) and `e2e` directory for more information. Also the Wireshark `.pcap` files are available for e2e test in artifacts for build. ### Benchmarks Intel(R) Core(TM) i7-8700K: ``` version: 1.22.2 goos: linux goarch: amd64 pkg: github.com/pion/stun PASS benchmark iter time/iter throughput bytes alloc allocs --------- ---- --------- ---------- ----------- ------ BenchmarkMappedAddress_AddTo-12 32489450 38.30 ns/op 0 B/op 0 allocs/op BenchmarkAlternateServer_AddTo-12 31230991 39.00 ns/op 0 B/op 0 allocs/op BenchmarkAgent_GC-12 431390 2918.00 ns/op 0 B/op 0 allocs/op BenchmarkAgent_Process-12 35901940 36.20 ns/op 0 B/op 0 allocs/op BenchmarkMessage_GetNotFound-12 242004358 5.19 ns/op 0 B/op 0 allocs/op BenchmarkMessage_Get-12 230520343 5.21 ns/op 0 B/op 0 allocs/op BenchmarkClient_Do-12 1282231 943.00 ns/op 0 B/op 0 allocs/op BenchmarkErrorCode_AddTo-12 16318916 75.50 ns/op 0 B/op 0 allocs/op BenchmarkErrorCodeAttribute_AddTo-12 21584140 54.80 ns/op 0 B/op 0 allocs/op BenchmarkErrorCodeAttribute_GetFrom-12 100000000 11.10 ns/op 0 B/op 0 allocs/op BenchmarkFingerprint_AddTo-12 19368768 64.00 ns/op 687.81 MB/s 0 B/op 0 allocs/op BenchmarkFingerprint_Check-12 24167007 49.10 ns/op 1057.99 MB/s 0 B/op 0 allocs/op BenchmarkBuildOverhead/Build-12 5486252 224.00 ns/op 0 B/op 0 allocs/op BenchmarkBuildOverhead/BuildNonPointer-12 2496544 517.00 ns/op 100 B/op 4 allocs/op BenchmarkBuildOverhead/Raw-12 6652118 181.00 ns/op 0 B/op 0 allocs/op BenchmarkMessage_ForEach-12 28254212 35.90 ns/op 0 B/op 0 allocs/op BenchmarkMessageIntegrity_AddTo-12 1000000 1179.00 ns/op 16.96 MB/s 0 B/op 0 allocs/op BenchmarkMessageIntegrity_Check-12 975954 1219.00 ns/op 26.24 MB/s 0 B/op 0 allocs/op BenchmarkMessage_Write-12 41040598 30.40 ns/op 922.13 MB/s 0 B/op 0 allocs/op BenchmarkMessageType_Value-12 1000000000 0.53 ns/op 0 B/op 0 allocs/op BenchmarkMessage_WriteTo-12 94942935 11.30 ns/op 0 B/op 0 allocs/op BenchmarkMessage_ReadFrom-12 43437718 29.30 ns/op 682.87 MB/s 0 B/op 0 allocs/op BenchmarkMessage_ReadBytes-12 74693397 15.90 ns/op 1257.42 MB/s 0 B/op 0 allocs/op BenchmarkIsMessage-12 1000000000 1.20 ns/op 16653.64 MB/s 0 B/op 0 allocs/op BenchmarkMessage_NewTransactionID-12 521121 2450.00 ns/op 0 B/op 0 allocs/op BenchmarkMessageFull-12 5389495 221.00 ns/op 0 B/op 0 allocs/op BenchmarkMessageFullHardcore-12 12715876 94.40 ns/op 0 B/op 0 allocs/op BenchmarkMessage_WriteHeader-12 100000000 11.60 ns/op 0 B/op 0 allocs/op BenchmarkMessage_CloneTo-12 30199020 41.80 ns/op 1626.66 MB/s 0 B/op 0 allocs/op BenchmarkMessage_AddTo-12 415257625 2.97 ns/op 0 B/op 0 allocs/op BenchmarkDecode-12 49573747 23.60 ns/op 0 B/op 0 allocs/op BenchmarkUsername_AddTo-12 56282674 22.50 ns/op 0 B/op 0 allocs/op BenchmarkUsername_GetFrom-12 100000000 10.10 ns/op 0 B/op 0 allocs/op BenchmarkNonce_AddTo-12 39419097 35.80 ns/op 0 B/op 0 allocs/op BenchmarkNonce_AddTo_BadLength-12 196291666 6.04 ns/op 0 B/op 0 allocs/op BenchmarkNonce_GetFrom-12 120857732 9.93 ns/op 0 B/op 0 allocs/op BenchmarkUnknownAttributes/AddTo-12 28881430 37.20 ns/op 0 B/op 0 allocs/op BenchmarkUnknownAttributes/GetFrom-12 64907534 19.80 ns/op 0 B/op 0 allocs/op BenchmarkXOR-12 32868506 32.20 ns/op 31836.66 MB/s BenchmarkXORSafe-12 5185776 234.00 ns/op 4378.74 MB/s BenchmarkXORFast-12 30975679 32.50 ns/op 31525.28 MB/s BenchmarkXORMappedAddress_AddTo-12 21518028 54.50 ns/op 0 B/op 0 allocs/op BenchmarkXORMappedAddress_GetFrom-12 35597667 34.40 ns/op 0 B/op 0 allocs/op ok github.com/pion/stun 60.973s ``` ### Roadmap 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. ### Community Pion has an active community on the [Slack](https://pion.ly/slack). Follow the [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. We are always looking to support **your projects**. Please reach out if you have something to build! 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) ### Contributing Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible: [AUTHORS.txt](./AUTHORS.txt) ### License MIT License - see [LICENSE](LICENSE) for full text stun-0.6.1/addr.go000066400000000000000000000073451444552150000137610ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "fmt" "io" "net" "strconv" ) // MappedAddress represents MAPPED-ADDRESS attribute. // // This attribute is used only by servers for achieving backwards // compatibility with RFC 3489 clients. // // RFC 5389 Section 15.1 type MappedAddress struct { IP net.IP Port int } // AlternateServer represents ALTERNATE-SERVER attribute. // // RFC 5389 Section 15.11 type AlternateServer struct { IP net.IP Port int } // ResponseOrigin represents RESPONSE-ORIGIN attribute. // // RFC 5780 Section 7.3 type ResponseOrigin struct { IP net.IP Port int } // OtherAddress represents OTHER-ADDRESS attribute. // // RFC 5780 Section 7.4 type OtherAddress struct { IP net.IP Port int } // AddTo adds ALTERNATE-SERVER attribute to message. func (s *AlternateServer) AddTo(m *Message) error { a := (*MappedAddress)(s) return a.AddToAs(m, AttrAlternateServer) } // GetFrom decodes ALTERNATE-SERVER from message. func (s *AlternateServer) GetFrom(m *Message) error { a := (*MappedAddress)(s) return a.GetFromAs(m, AttrAlternateServer) } func (a MappedAddress) String() string { return net.JoinHostPort(a.IP.String(), strconv.Itoa(a.Port)) } // GetFromAs decodes MAPPED-ADDRESS value in message m as an attribute of type t. func (a *MappedAddress) GetFromAs(m *Message, t AttrType) error { v, err := m.Get(t) if err != nil { return err } if len(v) <= 4 { return io.ErrUnexpectedEOF } family := bin.Uint16(v[0:2]) if family != familyIPv6 && family != familyIPv4 { return newDecodeErr("xor-mapped address", "family", fmt.Sprintf("bad value %d", family), ) } ipLen := net.IPv4len if family == familyIPv6 { ipLen = net.IPv6len } // Ensuring len(a.IP) == ipLen and reusing a.IP. if len(a.IP) < ipLen { a.IP = a.IP[:cap(a.IP)] for len(a.IP) < ipLen { a.IP = append(a.IP, 0) } } a.IP = a.IP[:ipLen] for i := range a.IP { a.IP[i] = 0 } a.Port = int(bin.Uint16(v[2:4])) copy(a.IP, v[4:]) return nil } // AddToAs adds MAPPED-ADDRESS value to m as t attribute. func (a *MappedAddress) AddToAs(m *Message, t AttrType) error { var ( family = familyIPv4 ip = a.IP ) if len(a.IP) == net.IPv6len { if isIPv4(ip) { ip = ip[12:16] // like in ip.To4() } else { family = familyIPv6 } } else if len(ip) != net.IPv4len { return ErrBadIPLength } value := make([]byte, 128) value[0] = 0 // first 8 bits are zeroes bin.PutUint16(value[0:2], family) bin.PutUint16(value[2:4], uint16(a.Port)) copy(value[4:], ip) m.Add(t, value[:4+len(ip)]) return nil } // AddTo adds MAPPED-ADDRESS to message. func (a *MappedAddress) AddTo(m *Message) error { return a.AddToAs(m, AttrMappedAddress) } // GetFrom decodes MAPPED-ADDRESS from message. func (a *MappedAddress) GetFrom(m *Message) error { return a.GetFromAs(m, AttrMappedAddress) } // AddTo adds OTHER-ADDRESS attribute to message. func (o *OtherAddress) AddTo(m *Message) error { a := (*MappedAddress)(o) return a.AddToAs(m, AttrOtherAddress) } // GetFrom decodes OTHER-ADDRESS from message. func (o *OtherAddress) GetFrom(m *Message) error { a := (*MappedAddress)(o) return a.GetFromAs(m, AttrOtherAddress) } func (o OtherAddress) String() string { return net.JoinHostPort(o.IP.String(), strconv.Itoa(o.Port)) } // AddTo adds RESPONSE-ORIGIN attribute to message. func (o *ResponseOrigin) AddTo(m *Message) error { a := (*MappedAddress)(o) return a.AddToAs(m, AttrResponseOrigin) } // GetFrom decodes RESPONSE-ORIGIN from message. func (o *ResponseOrigin) GetFrom(m *Message) error { a := (*MappedAddress)(o) return a.GetFromAs(m, AttrResponseOrigin) } func (o ResponseOrigin) String() string { return net.JoinHostPort(o.IP.String(), strconv.Itoa(o.Port)) } stun-0.6.1/addr_test.go000066400000000000000000000077531444552150000150230ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "io" "net" "testing" ) func TestMappedAddress(t *testing.T) { m := new(Message) addr := &MappedAddress{ IP: net.ParseIP("122.12.34.5"), Port: 5412, } if addr.String() != "122.12.34.5:5412" { t.Error("bad string", addr) } t.Run("Bad length", func(t *testing.T) { badAddr := &MappedAddress{ IP: net.IP{1, 2, 3}, } if err := badAddr.AddTo(m); err == nil { t.Error("should error") } }) t.Run("AddTo", func(t *testing.T) { if err := addr.AddTo(m); err != nil { t.Error(err) } t.Run("GetFrom", func(t *testing.T) { got := new(MappedAddress) if err := got.GetFrom(m); err != nil { t.Error(err) } if !got.IP.Equal(addr.IP) { t.Error("got bad IP: ", got.IP) } t.Run("Not found", func(t *testing.T) { message := new(Message) if err := got.GetFrom(message); !errors.Is(err, ErrAttributeNotFound) { t.Error("should be not found: ", err) } }) t.Run("Bad family", func(t *testing.T) { v, _ := m.Attributes.Get(AttrMappedAddress) v.Value[0] = 32 if err := got.GetFrom(m); err == nil { t.Error("should error") } }) t.Run("Bad length", func(t *testing.T) { message := new(Message) message.Add(AttrMappedAddress, []byte{1, 2, 3}) if err := got.GetFrom(message); !errors.Is(err, io.ErrUnexpectedEOF) { t.Errorf("<%s> should be <%s>", err, io.ErrUnexpectedEOF) } }) }) }) } func TestMappedAddressV6(t *testing.T) { //nolint:dupl m := new(Message) addr := &MappedAddress{ IP: net.ParseIP("::"), Port: 5412, } t.Run("AddTo", func(t *testing.T) { if err := addr.AddTo(m); err != nil { t.Error(err) } t.Run("GetFrom", func(t *testing.T) { got := new(MappedAddress) if err := got.GetFrom(m); err != nil { t.Error(err) } if !got.IP.Equal(addr.IP) { t.Error("got bad IP: ", got.IP) } t.Run("Not found", func(t *testing.T) { message := new(Message) if err := got.GetFrom(message); !errors.Is(err, ErrAttributeNotFound) { t.Error("should be not found: ", err) } }) }) }) } func TestAlternateServer(t *testing.T) { //nolint:dupl m := new(Message) addr := &AlternateServer{ IP: net.ParseIP("122.12.34.5"), Port: 5412, } t.Run("AddTo", func(t *testing.T) { if err := addr.AddTo(m); err != nil { t.Error(err) } t.Run("GetFrom", func(t *testing.T) { got := new(AlternateServer) if err := got.GetFrom(m); err != nil { t.Error(err) } if !got.IP.Equal(addr.IP) { t.Error("got bad IP: ", got.IP) } t.Run("Not found", func(t *testing.T) { message := new(Message) if err := got.GetFrom(message); !errors.Is(err, ErrAttributeNotFound) { t.Error("should be not found: ", err) } }) }) }) } func TestOtherAddress(t *testing.T) { //nolint:dupl m := new(Message) addr := &OtherAddress{ IP: net.ParseIP("122.12.34.5"), Port: 5412, } t.Run("AddTo", func(t *testing.T) { if err := addr.AddTo(m); err != nil { t.Error(err) } t.Run("GetFrom", func(t *testing.T) { got := new(OtherAddress) if err := got.GetFrom(m); err != nil { t.Error(err) } if !got.IP.Equal(addr.IP) { t.Error("got bad IP: ", got.IP) } t.Run("Not found", func(t *testing.T) { message := new(Message) if err := got.GetFrom(message); !errors.Is(err, ErrAttributeNotFound) { t.Error("should be not found: ", err) } }) }) }) } func BenchmarkMappedAddress_AddTo(b *testing.B) { m := new(Message) b.ReportAllocs() addr := &MappedAddress{ IP: net.ParseIP("122.12.34.5"), Port: 5412, } for i := 0; i < b.N; i++ { if err := addr.AddTo(m); err != nil { b.Fatal(err) } m.Reset() } } func BenchmarkAlternateServer_AddTo(b *testing.B) { m := new(Message) b.ReportAllocs() addr := &AlternateServer{ IP: net.ParseIP("122.12.34.5"), Port: 5412, } for i := 0; i < b.N; i++ { if err := addr.AddTo(m); err != nil { b.Fatal(err) } m.Reset() } } stun-0.6.1/agent.go000066400000000000000000000141171444552150000141400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "sync" "time" ) // NoopHandler just discards any event. func NoopHandler() Handler { return func(e Event) {} } // NewAgent initializes and returns new Agent with provided handler. // If h is nil, the NoopHandler will be used. func NewAgent(h Handler) *Agent { if h == nil { h = NoopHandler() } a := &Agent{ transactions: make(map[transactionID]agentTransaction), handler: h, } return a } // Agent is low-level abstraction over transaction list that // handles concurrency (all calls are goroutine-safe) and // time outs (via Collect call). type Agent struct { // transactions is map of transactions that are currently // in progress. Event handling is done in such way when // transaction is unregistered before agentTransaction access, // minimizing mux lock and protecting agentTransaction from // data races via unexpected concurrent access. transactions map[transactionID]agentTransaction closed bool // all calls are invalid if true mux sync.Mutex // protects transactions and closed handler Handler // handles transactions } // Handler handles state changes of transaction. // // Handler is called on transaction state change. // Usage of e is valid only during call, user must // copy needed fields explicitly. type Handler func(e Event) // Event is passed to Handler describing the transaction event. // Do not reuse outside Handler. type Event struct { TransactionID [TransactionIDSize]byte Message *Message Error error } // agentTransaction represents transaction in progress. // Concurrent access is invalid. type agentTransaction struct { id transactionID deadline time.Time } var ( // ErrTransactionStopped indicates that transaction was manually stopped. ErrTransactionStopped = errors.New("transaction is stopped") // ErrTransactionNotExists indicates that agent failed to find transaction. ErrTransactionNotExists = errors.New("transaction not exists") // ErrTransactionExists indicates that transaction with same id is already // registered. ErrTransactionExists = errors.New("transaction exists with same id") ) // StopWithError removes transaction from list and calls handler with // provided error. Can return ErrTransactionNotExists and ErrAgentClosed. func (a *Agent) StopWithError(id [TransactionIDSize]byte, err error) error { a.mux.Lock() if a.closed { a.mux.Unlock() return ErrAgentClosed } t, exists := a.transactions[id] delete(a.transactions, id) h := a.handler a.mux.Unlock() if !exists { return ErrTransactionNotExists } h(Event{ TransactionID: t.id, Error: err, }) return nil } // Stop stops transaction by id with ErrTransactionStopped, blocking // until handler returns. func (a *Agent) Stop(id [TransactionIDSize]byte) error { return a.StopWithError(id, ErrTransactionStopped) } // ErrAgentClosed indicates that agent is in closed state and is unable // to handle transactions. var ErrAgentClosed = errors.New("agent is closed") // Start registers transaction with provided id and deadline. // Could return ErrAgentClosed, ErrTransactionExists. // // Agent handler is guaranteed to be eventually called. func (a *Agent) Start(id [TransactionIDSize]byte, deadline time.Time) error { a.mux.Lock() defer a.mux.Unlock() if a.closed { return ErrAgentClosed } _, exists := a.transactions[id] if exists { return ErrTransactionExists } a.transactions[id] = agentTransaction{ id: id, deadline: deadline, } return nil } // agentCollectCap is initial capacity for Agent.Collect slices, // sufficient to make function zero-alloc in most cases. const agentCollectCap = 100 // ErrTransactionTimeOut indicates that transaction has reached deadline. var ErrTransactionTimeOut = errors.New("transaction is timed out") // Collect terminates all transactions that have deadline before provided // time, blocking until all handlers will process ErrTransactionTimeOut. // Will return ErrAgentClosed if agent is already closed. // // It is safe to call Collect concurrently but makes no sense. func (a *Agent) Collect(gcTime time.Time) error { toRemove := make([]transactionID, 0, agentCollectCap) a.mux.Lock() if a.closed { // Doing nothing if agent is closed. // All transactions should be already closed // during Close() call. a.mux.Unlock() return ErrAgentClosed } // Adding all transactions with deadline before gcTime // to toCall and toRemove slices. // No allocs if there are less than agentCollectCap // timed out transactions. for id, t := range a.transactions { if t.deadline.Before(gcTime) { toRemove = append(toRemove, id) } } // Un-registering timed out transactions. for _, id := range toRemove { delete(a.transactions, id) } // Calling handler does not require locked mutex, // reducing lock time. h := a.handler a.mux.Unlock() // Sending ErrTransactionTimeOut to handler for all transactions, // blocking until last one. event := Event{ Error: ErrTransactionTimeOut, } for _, id := range toRemove { event.TransactionID = id h(event) } return nil } // Process incoming message, synchronously passing it to handler. func (a *Agent) Process(m *Message) error { e := Event{ TransactionID: m.TransactionID, Message: m, } a.mux.Lock() if a.closed { a.mux.Unlock() return ErrAgentClosed } h := a.handler delete(a.transactions, m.TransactionID) a.mux.Unlock() h(e) return nil } // SetHandler sets agent handler to h. func (a *Agent) SetHandler(h Handler) error { a.mux.Lock() if a.closed { a.mux.Unlock() return ErrAgentClosed } a.handler = h a.mux.Unlock() return nil } // Close terminates all transactions with ErrAgentClosed and renders Agent to // closed state. func (a *Agent) Close() error { e := Event{ Error: ErrAgentClosed, } a.mux.Lock() if a.closed { a.mux.Unlock() return ErrAgentClosed } for _, t := range a.transactions { e.TransactionID = t.id a.handler(e) } a.transactions = nil a.closed = true a.handler = nil a.mux.Unlock() return nil } type transactionID [TransactionIDSize]byte stun-0.6.1/agent_test.go000066400000000000000000000122061444552150000151740ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "testing" "time" ) func TestAgent_ProcessInTransaction(t *testing.T) { m := New() a := NewAgent(func(e Event) { if e.Error != nil { t.Errorf("got error: %s", e.Error) } if !e.Message.Equal(m) { t.Errorf("%s (got) != %s (expected)", e.Message, m) } }) if err := m.NewTransactionID(); err != nil { t.Fatal(err) } if err := a.Start(m.TransactionID, time.Time{}); err != nil { t.Fatal(err) } if err := a.Process(m); err != nil { t.Error(err) } if err := a.Close(); err != nil { t.Error(err) } } func TestAgent_Process(t *testing.T) { m := New() a := NewAgent(func(e Event) { if e.Error != nil { t.Errorf("got error: %s", e.Error) } if !e.Message.Equal(m) { t.Errorf("%s (got) != %s (expected)", e.Message, m) } }) if err := m.NewTransactionID(); err != nil { t.Fatal(err) } if err := a.Process(m); err != nil { t.Error(err) } if err := a.Close(); err != nil { t.Error(err) } if err := a.Process(m); !errors.Is(err, ErrAgentClosed) { t.Errorf("closed agent should return <%s>, but got <%s>", ErrAgentClosed, err, ) } } func TestAgent_Start(t *testing.T) { a := NewAgent(nil) id := NewTransactionID() deadline := time.Now().AddDate(0, 0, 1) if err := a.Start(id, deadline); err != nil { t.Errorf("failed to statt transaction: %s", err) } if err := a.Start(id, deadline); !errors.Is(err, ErrTransactionExists) { t.Errorf("duplicate start should return <%s>, got <%s>", ErrTransactionExists, err, ) } if err := a.Close(); err != nil { t.Error(err) } id = NewTransactionID() if err := a.Start(id, deadline); !errors.Is(err, ErrAgentClosed) { t.Errorf("start on closed agent should return <%s>, got <%s>", ErrAgentClosed, err, ) } if err := a.SetHandler(nil); !errors.Is(err, ErrAgentClosed) { t.Errorf("SetHandler on closed agent should return <%s>, got <%s>", ErrAgentClosed, err, ) } } func TestAgent_Stop(t *testing.T) { called := make(chan Event, 1) a := NewAgent(func(e Event) { called <- e }) if err := a.Stop(transactionID{}); !errors.Is(err, ErrTransactionNotExists) { t.Fatalf("unexpected error: %s, should be %s", err, ErrTransactionNotExists) } id := NewTransactionID() timeout := time.Millisecond * 200 if err := a.Start(id, time.Now().Add(timeout)); err != nil { t.Fatal(err) } if err := a.Stop(id); err != nil { t.Fatal(err) } select { case e := <-called: if !errors.Is(e.Error, ErrTransactionStopped) { t.Fatalf("unexpected error: %s, should be %s", e.Error, ErrTransactionStopped, ) } case <-time.After(timeout * 2): t.Fatal("timed out") } if err := a.Close(); err != nil { t.Fatal(err) } if err := a.Close(); !errors.Is(err, ErrAgentClosed) { t.Fatalf("a.Close returned %s instead of %s", err, ErrAgentClosed) } if err := a.Stop(transactionID{}); !errors.Is(err, ErrAgentClosed) { t.Fatalf("unexpected error: %s, should be %s", err, ErrAgentClosed) } } func TestAgent_GC(t *testing.T) { a := NewAgent(nil) shouldTimeOutID := make(map[transactionID]bool) deadline := time.Date(2027, time.November, 21, 23, 0, 0, 0, time.UTC, ) gcDeadline := deadline.Add(-time.Second) deadlineNotGC := gcDeadline.AddDate(0, 0, -1) a.SetHandler(func(e Event) { //nolint:errcheck,gosec id := e.TransactionID shouldTimeOut, found := shouldTimeOutID[id] if !found { t.Error("unexpected transaction ID") } if shouldTimeOut && !errors.Is(e.Error, ErrTransactionTimeOut) { t.Errorf("%x should time out, but got %v", id, e.Error) } if !shouldTimeOut && errors.Is(e.Error, ErrTransactionTimeOut) { t.Errorf("%x should not time out, but got %v", id, e.Error) } }) for i := 0; i < 5; i++ { id := NewTransactionID() shouldTimeOutID[id] = false if err := a.Start(id, deadline); err != nil { t.Fatal(err) } } for i := 0; i < 5; i++ { id := NewTransactionID() shouldTimeOutID[id] = true if err := a.Start(id, deadlineNotGC); err != nil { t.Fatal(err) } } if err := a.Collect(gcDeadline); err != nil { t.Fatal(err) } if err := a.Close(); err != nil { t.Error(err) } if err := a.Collect(gcDeadline); !errors.Is(err, ErrAgentClosed) { t.Errorf("should <%s>, but got <%s>", ErrAgentClosed, err) } } func BenchmarkAgent_GC(b *testing.B) { a := NewAgent(nil) deadline := time.Now().AddDate(0, 0, 1) for i := 0; i < agentCollectCap; i++ { if err := a.Start(NewTransactionID(), deadline); err != nil { b.Fatal(err) } } defer func() { if err := a.Close(); err != nil { b.Error(err) } }() b.ReportAllocs() gcDeadline := deadline.Add(-time.Second) for i := 0; i < b.N; i++ { if err := a.Collect(gcDeadline); err != nil { b.Fatal(err) } } } func BenchmarkAgent_Process(b *testing.B) { a := NewAgent(nil) deadline := time.Now().AddDate(0, 0, 1) for i := 0; i < 1000; i++ { if err := a.Start(NewTransactionID(), deadline); err != nil { b.Fatal(err) } } defer func() { if err := a.Close(); err != nil { b.Error(err) } }() b.ReportAllocs() m := MustBuild(TransactionID) for i := 0; i < b.N; i++ { if err := a.Process(m); err != nil { b.Fatal(err) } } } stun-0.6.1/api/000077500000000000000000000000001444552150000132605ustar00rootroot00000000000000stun-0.6.1/api/check.sh000077500000000000000000000003121444552150000146700ustar00rootroot00000000000000#!/usr/bin/env bash # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT api -c $(ls -dm stun*.txt | tr -d ' \n') -except except.txt github.com/pion/stun stun-0.6.1/api/except.txt000066400000000000000000000033541444552150000153160ustar00rootroot00000000000000pkg github.com/pion/stun, type Handler interface { HandleEvent } pkg github.com/pion/stun, type Handler interface, HandleEvent(Event) pkg github.com/pion/stun, type HandlerFunc func(Event) pkg github.com/pion/stun, method (HandlerFunc) HandleEvent(Event) pkg github.com/pion/stun, method (*Client) Do(*Message, time.Time, func(Event)) error pkg github.com/pion/stun, method (*Client) Start(*Message, time.Time, Handler) error pkg github.com/pion/stun, method (*Client) Start(*Message, time.Time, Handler) error pkg github.com/pion/stun, type ClientAgent interface { Close, Collect, Process, Start, Stop } pkg github.com/pion/stun, method (*Agent) SetHandler(Handler) error pkg github.com/pion/stun, method (*Agent) Start([12]uint8, time.Time, Handler) error pkg github.com/pion/stun, func NewAgent(AgentOptions) *Agent pkg github.com/pion/stun, type ClientAgent interface, Start([12]uint8, time.Time, Handler) error pkg github.com/pion/stun, var ErrAttrSizeInvalid error pkg github.com/pion/stun, type AgentOptions struct pkg github.com/pion/stun, type AgentOptions struct, Handler Handler pkg github.com/pion/stun, func NewClient(ClientOptions) (*Client, error) pkg github.com/pion/stun, type ClientOptions struct pkg github.com/pion/stun, type ClientOptions struct, Agent ClientAgent pkg github.com/pion/stun, type ClientOptions struct, Clock Clock pkg github.com/pion/stun, type ClientOptions struct, Collector Collector pkg github.com/pion/stun, type ClientOptions struct, Connection Connection pkg github.com/pion/stun, type ClientOptions struct, Handler Handler pkg github.com/pion/stun, type ClientOptions struct, RTO time.Duration pkg github.com/pion/stun, type ClientOptions struct, TimeoutRate time.Duration pkg github.com/pion/stun, const CodeRoleConflict = 478stun-0.6.1/api/stun1.0.0.txt000066400000000000000000000467331444552150000154040ustar00rootroot00000000000000pkg github.com/pion/stun, const AttrAlternateServer = 32803 pkg github.com/pion/stun, const AttrAlternateServer AttrType pkg github.com/pion/stun, const AttrChannelNumber = 12 pkg github.com/pion/stun, const AttrChannelNumber AttrType pkg github.com/pion/stun, const AttrData = 19 pkg github.com/pion/stun, const AttrData AttrType pkg github.com/pion/stun, const AttrDontFragment = 26 pkg github.com/pion/stun, const AttrDontFragment AttrType pkg github.com/pion/stun, const AttrErrorCode = 9 pkg github.com/pion/stun, const AttrErrorCode AttrType pkg github.com/pion/stun, const AttrEvenPort = 24 pkg github.com/pion/stun, const AttrEvenPort AttrType pkg github.com/pion/stun, const AttrFingerprint = 32808 pkg github.com/pion/stun, const AttrFingerprint AttrType pkg github.com/pion/stun, const AttrICEControlled = 32809 pkg github.com/pion/stun, const AttrICEControlled AttrType pkg github.com/pion/stun, const AttrICEControlling = 32810 pkg github.com/pion/stun, const AttrICEControlling AttrType pkg github.com/pion/stun, const AttrLifetime = 13 pkg github.com/pion/stun, const AttrLifetime AttrType pkg github.com/pion/stun, const AttrMappedAddress = 1 pkg github.com/pion/stun, const AttrMappedAddress AttrType pkg github.com/pion/stun, const AttrMessageIntegrity = 8 pkg github.com/pion/stun, const AttrMessageIntegrity AttrType pkg github.com/pion/stun, const AttrNonce = 21 pkg github.com/pion/stun, const AttrNonce AttrType pkg github.com/pion/stun, const AttrOrigin = 32815 pkg github.com/pion/stun, const AttrOrigin AttrType pkg github.com/pion/stun, const AttrPriority = 36 pkg github.com/pion/stun, const AttrPriority AttrType pkg github.com/pion/stun, const AttrRealm = 20 pkg github.com/pion/stun, const AttrRealm AttrType pkg github.com/pion/stun, const AttrRequestedTransport = 25 pkg github.com/pion/stun, const AttrRequestedTransport AttrType pkg github.com/pion/stun, const AttrReservationToken = 34 pkg github.com/pion/stun, const AttrReservationToken AttrType pkg github.com/pion/stun, const AttrSoftware = 32802 pkg github.com/pion/stun, const AttrSoftware AttrType pkg github.com/pion/stun, const AttrUnknownAttributes = 10 pkg github.com/pion/stun, const AttrUnknownAttributes AttrType pkg github.com/pion/stun, const AttrUseCandidate = 37 pkg github.com/pion/stun, const AttrUseCandidate AttrType pkg github.com/pion/stun, const AttrUsername = 6 pkg github.com/pion/stun, const AttrUsername AttrType pkg github.com/pion/stun, const AttrXORMappedAddress = 32 pkg github.com/pion/stun, const AttrXORMappedAddress AttrType pkg github.com/pion/stun, const AttrXORPeerAddress = 18 pkg github.com/pion/stun, const AttrXORPeerAddress AttrType pkg github.com/pion/stun, const AttrXORRelayedAddress = 22 pkg github.com/pion/stun, const AttrXORRelayedAddress AttrType pkg github.com/pion/stun, const ClassErrorResponse = 3 pkg github.com/pion/stun, const ClassErrorResponse MessageClass pkg github.com/pion/stun, const ClassIndication = 1 pkg github.com/pion/stun, const ClassIndication MessageClass pkg github.com/pion/stun, const ClassRequest = 0 pkg github.com/pion/stun, const ClassRequest MessageClass pkg github.com/pion/stun, const ClassSuccessResponse = 2 pkg github.com/pion/stun, const ClassSuccessResponse MessageClass pkg github.com/pion/stun, const CodeAllocMismatch = 437 pkg github.com/pion/stun, const CodeAllocMismatch ErrorCode pkg github.com/pion/stun, const CodeAllocQuotaReached = 486 pkg github.com/pion/stun, const CodeAllocQuotaReached ErrorCode pkg github.com/pion/stun, const CodeBadRequest = 400 pkg github.com/pion/stun, const CodeBadRequest ErrorCode pkg github.com/pion/stun, const CodeForbidden = 403 pkg github.com/pion/stun, const CodeForbidden ErrorCode pkg github.com/pion/stun, const CodeInsufficientCapacity = 508 pkg github.com/pion/stun, const CodeInsufficientCapacity ErrorCode pkg github.com/pion/stun, const CodeRoleConflict = 478 pkg github.com/pion/stun, const CodeRoleConflict ErrorCode pkg github.com/pion/stun, const CodeServerError = 500 pkg github.com/pion/stun, const CodeServerError ErrorCode pkg github.com/pion/stun, const CodeStaleNonce = 438 pkg github.com/pion/stun, const CodeStaleNonce ErrorCode pkg github.com/pion/stun, const CodeTryAlternate = 300 pkg github.com/pion/stun, const CodeTryAlternate ErrorCode pkg github.com/pion/stun, const CodeUnauthorised = 401 pkg github.com/pion/stun, const CodeUnauthorised ErrorCode pkg github.com/pion/stun, const CodeUnknownAttribute = 420 pkg github.com/pion/stun, const CodeUnknownAttribute ErrorCode pkg github.com/pion/stun, const CodeUnsupportedTransProto = 442 pkg github.com/pion/stun, const CodeUnsupportedTransProto ErrorCode pkg github.com/pion/stun, const CodeWrongCredentials = 441 pkg github.com/pion/stun, const CodeWrongCredentials ErrorCode pkg github.com/pion/stun, const DefaultPort = 3478 pkg github.com/pion/stun, const DefaultPort ideal-int pkg github.com/pion/stun, const DefaultTLSPort = 5349 pkg github.com/pion/stun, const DefaultTLSPort ideal-int pkg github.com/pion/stun, const MethodAllocate = 3 pkg github.com/pion/stun, const MethodAllocate Method pkg github.com/pion/stun, const MethodBinding = 1 pkg github.com/pion/stun, const MethodBinding Method pkg github.com/pion/stun, const MethodChannelBind = 9 pkg github.com/pion/stun, const MethodChannelBind Method pkg github.com/pion/stun, const MethodCreatePermission = 8 pkg github.com/pion/stun, const MethodCreatePermission Method pkg github.com/pion/stun, const MethodData = 7 pkg github.com/pion/stun, const MethodData Method pkg github.com/pion/stun, const MethodRefresh = 4 pkg github.com/pion/stun, const MethodRefresh Method pkg github.com/pion/stun, const MethodSend = 6 pkg github.com/pion/stun, const MethodSend Method pkg github.com/pion/stun, const TransactionIDSize = 12 pkg github.com/pion/stun, const TransactionIDSize ideal-int pkg github.com/pion/stun, func Build(...Setter) (*Message, error) pkg github.com/pion/stun, func CheckOverflow(AttrType, int, int) error pkg github.com/pion/stun, func CheckSize(AttrType, int, int) error pkg github.com/pion/stun, func Decode([]uint8, *Message) error pkg github.com/pion/stun, func Dial(string, string) (*Client, error) pkg github.com/pion/stun, func FingerprintValue([]uint8) uint32 pkg github.com/pion/stun, func IsAttrSizeInvalid(error) bool pkg github.com/pion/stun, func IsAttrSizeOverflow(error) bool pkg github.com/pion/stun, func IsMessage([]uint8) bool pkg github.com/pion/stun, func MustBuild(...Setter) *Message pkg github.com/pion/stun, func New() *Message pkg github.com/pion/stun, func NewAgent(AgentOptions) *Agent pkg github.com/pion/stun, func NewClient(ClientOptions) (*Client, error) pkg github.com/pion/stun, func NewLongTermIntegrity(string, string, string) MessageIntegrity pkg github.com/pion/stun, func NewNonce(string) Nonce pkg github.com/pion/stun, func NewRealm(string) Realm pkg github.com/pion/stun, func NewShortTermIntegrity(string) MessageIntegrity pkg github.com/pion/stun, func NewSoftware(string) Software pkg github.com/pion/stun, func NewTransactionID() [12]uint8 pkg github.com/pion/stun, func NewTransactionIDSetter([12]uint8) Setter pkg github.com/pion/stun, func NewType(Method, MessageClass) MessageType pkg github.com/pion/stun, func NewUsername(string) Username pkg github.com/pion/stun, method (*Agent) Close() error pkg github.com/pion/stun, method (*Agent) Collect(time.Time) error pkg github.com/pion/stun, method (*Agent) Process(*Message) error pkg github.com/pion/stun, method (*Agent) Start([12]uint8, time.Time, Handler) error pkg github.com/pion/stun, method (*Agent) Stop([12]uint8) error pkg github.com/pion/stun, method (*Agent) StopWithError([12]uint8, error) error pkg github.com/pion/stun, method (*AlternateServer) AddTo(*Message) error pkg github.com/pion/stun, method (*AlternateServer) GetFrom(*Message) error pkg github.com/pion/stun, method (*Client) Close() error pkg github.com/pion/stun, method (*Client) Do(*Message, time.Time, func(Event)) error pkg github.com/pion/stun, method (*Client) Indicate(*Message) error pkg github.com/pion/stun, method (*Client) Start(*Message, time.Time, Handler) error pkg github.com/pion/stun, method (*ErrorCodeAttribute) GetFrom(*Message) error pkg github.com/pion/stun, method (*MappedAddress) AddTo(*Message) error pkg github.com/pion/stun, method (*MappedAddress) GetFrom(*Message) error pkg github.com/pion/stun, method (*Message) Add(AttrType, []uint8) pkg github.com/pion/stun, method (*Message) AddTo(*Message) error pkg github.com/pion/stun, method (*Message) Build(...Setter) error pkg github.com/pion/stun, method (*Message) Check(...Checker) error pkg github.com/pion/stun, method (*Message) CloneTo(*Message) error pkg github.com/pion/stun, method (*Message) Contains(AttrType) bool pkg github.com/pion/stun, method (*Message) Decode() error pkg github.com/pion/stun, method (*Message) Encode() pkg github.com/pion/stun, method (*Message) Equal(*Message) bool pkg github.com/pion/stun, method (*Message) ForEach(AttrType, func(*Message) error) error pkg github.com/pion/stun, method (*Message) Get(AttrType) ([]uint8, error) pkg github.com/pion/stun, method (*Message) NewTransactionID() error pkg github.com/pion/stun, method (*Message) Parse(...Getter) error pkg github.com/pion/stun, method (*Message) ReadFrom(io.Reader) (int64, error) pkg github.com/pion/stun, method (*Message) Reset() pkg github.com/pion/stun, method (*Message) SetType(MessageType) pkg github.com/pion/stun, method (*Message) String() string pkg github.com/pion/stun, method (*Message) Write([]uint8) (int, error) pkg github.com/pion/stun, method (*Message) WriteAttributes() pkg github.com/pion/stun, method (*Message) WriteHeader() pkg github.com/pion/stun, method (*Message) WriteLength() pkg github.com/pion/stun, method (*Message) WriteTo(io.Writer) (int64, error) pkg github.com/pion/stun, method (*Message) WriteTransactionID() pkg github.com/pion/stun, method (*Message) WriteType() pkg github.com/pion/stun, method (*MessageType) ReadValue(uint16) pkg github.com/pion/stun, method (*Nonce) GetFrom(*Message) error pkg github.com/pion/stun, method (*Realm) GetFrom(*Message) error pkg github.com/pion/stun, method (*Software) GetFrom(*Message) error pkg github.com/pion/stun, method (*TextAttribute) GetFromAs(*Message, AttrType) error pkg github.com/pion/stun, method (*UnknownAttributes) GetFrom(*Message) error pkg github.com/pion/stun, method (*Username) GetFrom(*Message) error pkg github.com/pion/stun, method (*XORMappedAddress) GetFrom(*Message) error pkg github.com/pion/stun, method (*XORMappedAddress) GetFromAs(*Message, AttrType) error pkg github.com/pion/stun, method (AttrType) Optional() bool pkg github.com/pion/stun, method (AttrType) Required() bool pkg github.com/pion/stun, method (AttrType) String() string pkg github.com/pion/stun, method (AttrType) Value() uint16 pkg github.com/pion/stun, method (Attributes) Get(AttrType) (RawAttribute, bool) pkg github.com/pion/stun, method (CloseErr) Error() string pkg github.com/pion/stun, method (DecodeErr) Error() string pkg github.com/pion/stun, method (DecodeErr) IsInvalidCookie() bool pkg github.com/pion/stun, method (DecodeErr) IsPlace(DecodeErrPlace) bool pkg github.com/pion/stun, method (DecodeErr) IsPlaceChildren(string) bool pkg github.com/pion/stun, method (DecodeErr) IsPlaceParent(string) bool pkg github.com/pion/stun, method (DecodeErrPlace) String() string pkg github.com/pion/stun, method (ErrorCode) AddTo(*Message) error pkg github.com/pion/stun, method (ErrorCodeAttribute) AddTo(*Message) error pkg github.com/pion/stun, method (ErrorCodeAttribute) String() string pkg github.com/pion/stun, method (FingerprintAttr) AddTo(*Message) error pkg github.com/pion/stun, method (FingerprintAttr) Check(*Message) error pkg github.com/pion/stun, method (HandlerFunc) HandleEvent(Event) pkg github.com/pion/stun, method (MappedAddress) String() string pkg github.com/pion/stun, method (MessageClass) String() string pkg github.com/pion/stun, method (MessageIntegrity) AddTo(*Message) error pkg github.com/pion/stun, method (MessageIntegrity) Check(*Message) error pkg github.com/pion/stun, method (MessageIntegrity) String() string pkg github.com/pion/stun, method (MessageType) AddTo(*Message) error pkg github.com/pion/stun, method (MessageType) String() string pkg github.com/pion/stun, method (MessageType) Value() uint16 pkg github.com/pion/stun, method (Method) String() string pkg github.com/pion/stun, method (Nonce) AddTo(*Message) error pkg github.com/pion/stun, method (Nonce) String() string pkg github.com/pion/stun, method (RawAttribute) Equal(RawAttribute) bool pkg github.com/pion/stun, method (RawAttribute) String() string pkg github.com/pion/stun, method (Realm) AddTo(*Message) error pkg github.com/pion/stun, method (Realm) String() string pkg github.com/pion/stun, method (Software) AddTo(*Message) error pkg github.com/pion/stun, method (Software) String() string pkg github.com/pion/stun, method (StopErr) Error() string pkg github.com/pion/stun, method (TextAttribute) AddToAs(*Message, AttrType, int) error pkg github.com/pion/stun, method (UnknownAttributes) AddTo(*Message) error pkg github.com/pion/stun, method (UnknownAttributes) String() string pkg github.com/pion/stun, method (Username) AddTo(*Message) error pkg github.com/pion/stun, method (Username) String() string pkg github.com/pion/stun, method (XORMappedAddress) AddTo(*Message) error pkg github.com/pion/stun, method (XORMappedAddress) AddToAs(*Message, AttrType) error pkg github.com/pion/stun, method (XORMappedAddress) String() string pkg github.com/pion/stun, type Agent struct pkg github.com/pion/stun, type AgentOptions struct pkg github.com/pion/stun, type AgentOptions struct, Handler Handler pkg github.com/pion/stun, type AlternateServer struct pkg github.com/pion/stun, type AlternateServer struct, IP net.IP pkg github.com/pion/stun, type AlternateServer struct, Port int pkg github.com/pion/stun, type AttrType uint16 pkg github.com/pion/stun, type Attributes []RawAttribute pkg github.com/pion/stun, type Checker interface { Check } pkg github.com/pion/stun, type Checker interface, Check(*Message) error pkg github.com/pion/stun, type Client struct pkg github.com/pion/stun, type ClientAgent interface { Close, Collect, Process, Start, Stop } pkg github.com/pion/stun, type ClientAgent interface, Close() error pkg github.com/pion/stun, type ClientAgent interface, Collect(time.Time) error pkg github.com/pion/stun, type ClientAgent interface, Process(*Message) error pkg github.com/pion/stun, type ClientAgent interface, Start([12]uint8, time.Time, Handler) error pkg github.com/pion/stun, type ClientAgent interface, Stop([12]uint8) error pkg github.com/pion/stun, type ClientOptions struct pkg github.com/pion/stun, type ClientOptions struct, Agent ClientAgent pkg github.com/pion/stun, type ClientOptions struct, Connection Connection pkg github.com/pion/stun, type ClientOptions struct, TimeoutRate time.Duration pkg github.com/pion/stun, type CloseErr struct pkg github.com/pion/stun, type CloseErr struct, AgentErr error pkg github.com/pion/stun, type CloseErr struct, ConnectionErr error pkg github.com/pion/stun, type Connection interface { Close, Read, Write } pkg github.com/pion/stun, type Connection interface, Close() error pkg github.com/pion/stun, type Connection interface, Read([]uint8) (int, error) pkg github.com/pion/stun, type Connection interface, Write([]uint8) (int, error) pkg github.com/pion/stun, type DecodeErr struct pkg github.com/pion/stun, type DecodeErr struct, Message string pkg github.com/pion/stun, type DecodeErr struct, Place DecodeErrPlace pkg github.com/pion/stun, type DecodeErrPlace struct pkg github.com/pion/stun, type DecodeErrPlace struct, Children string pkg github.com/pion/stun, type DecodeErrPlace struct, Parent string pkg github.com/pion/stun, type ErrorCode int pkg github.com/pion/stun, type ErrorCodeAttribute struct pkg github.com/pion/stun, type ErrorCodeAttribute struct, Code ErrorCode pkg github.com/pion/stun, type ErrorCodeAttribute struct, Reason []uint8 pkg github.com/pion/stun, type Event struct pkg github.com/pion/stun, type Event struct, Error error pkg github.com/pion/stun, type Event struct, Message *Message pkg github.com/pion/stun, type FingerprintAttr struct pkg github.com/pion/stun, type Getter interface { GetFrom } pkg github.com/pion/stun, type Getter interface, GetFrom(*Message) error pkg github.com/pion/stun, type Handler interface { HandleEvent } pkg github.com/pion/stun, type Handler interface, HandleEvent(Event) pkg github.com/pion/stun, type HandlerFunc func(Event) pkg github.com/pion/stun, type MappedAddress struct pkg github.com/pion/stun, type MappedAddress struct, IP net.IP pkg github.com/pion/stun, type MappedAddress struct, Port int pkg github.com/pion/stun, type Message struct pkg github.com/pion/stun, type Message struct, Attributes Attributes pkg github.com/pion/stun, type Message struct, Length uint32 pkg github.com/pion/stun, type Message struct, Raw []uint8 pkg github.com/pion/stun, type Message struct, TransactionID [12]uint8 pkg github.com/pion/stun, type Message struct, Type MessageType pkg github.com/pion/stun, type MessageClass uint8 pkg github.com/pion/stun, type MessageIntegrity []uint8 pkg github.com/pion/stun, type MessageType struct pkg github.com/pion/stun, type MessageType struct, Class MessageClass pkg github.com/pion/stun, type MessageType struct, Method Method pkg github.com/pion/stun, type Method uint16 pkg github.com/pion/stun, type Nonce []uint8 pkg github.com/pion/stun, type RawAttribute struct pkg github.com/pion/stun, type RawAttribute struct, Length uint16 pkg github.com/pion/stun, type RawAttribute struct, Type AttrType pkg github.com/pion/stun, type RawAttribute struct, Value []uint8 pkg github.com/pion/stun, type Realm []uint8 pkg github.com/pion/stun, type Setter interface { AddTo } pkg github.com/pion/stun, type Setter interface, AddTo(*Message) error pkg github.com/pion/stun, type Software []uint8 pkg github.com/pion/stun, type StopErr struct pkg github.com/pion/stun, type StopErr struct, Cause error pkg github.com/pion/stun, type StopErr struct, Err error pkg github.com/pion/stun, type TextAttribute []uint8 pkg github.com/pion/stun, type UnknownAttributes []AttrType pkg github.com/pion/stun, type Username []uint8 pkg github.com/pion/stun, type XORMappedAddress struct pkg github.com/pion/stun, type XORMappedAddress struct, IP net.IP pkg github.com/pion/stun, type XORMappedAddress struct, Port int pkg github.com/pion/stun, var BindingError MessageType pkg github.com/pion/stun, var BindingRequest MessageType pkg github.com/pion/stun, var BindingSuccess MessageType pkg github.com/pion/stun, var ErrAgentClosed error pkg github.com/pion/stun, var ErrAttrSizeInvalid error pkg github.com/pion/stun, var ErrAttributeNotFound error pkg github.com/pion/stun, var ErrAttributeSizeInvalid error pkg github.com/pion/stun, var ErrAttributeSizeOverflow error pkg github.com/pion/stun, var ErrBadIPLength error pkg github.com/pion/stun, var ErrBadUnknownAttrsSize error pkg github.com/pion/stun, var ErrClientClosed error pkg github.com/pion/stun, var ErrClientNotInitialized error pkg github.com/pion/stun, var ErrDecodeToNil error pkg github.com/pion/stun, var ErrFingerprintBeforeIntegrity error pkg github.com/pion/stun, var ErrFingerprintMismatch error pkg github.com/pion/stun, var ErrIntegrityMismatch error pkg github.com/pion/stun, var ErrNoConnection error pkg github.com/pion/stun, var ErrNoDefaultReason error pkg github.com/pion/stun, var ErrTransactionExists error pkg github.com/pion/stun, var ErrTransactionNotExists error pkg github.com/pion/stun, var ErrTransactionStopped error pkg github.com/pion/stun, var ErrTransactionTimeOut error pkg github.com/pion/stun, var ErrUnexpectedHeaderEOF error pkg github.com/pion/stun, var Fingerprint FingerprintAttr pkg github.com/pion/stun, var TransactionID Setter stun-0.6.1/api/stun1.14.0.txt000066400000000000000000000027151444552150000154610ustar00rootroot00000000000000pkg github.com/pion/stun, type Handler func(Event) pkg github.com/pion/stun, method (*Client) Do(*Message, func(Event)) error pkg github.com/pion/stun, method (*Client) SetRTO(time.Duration) pkg github.com/pion/stun, method (*Client) Start(*Message, Handler) error pkg github.com/pion/stun, type Clock interface { Now } pkg github.com/pion/stun, type Clock interface, Now() time.Time pkg github.com/pion/stun, type Collector interface { Close, Start } pkg github.com/pion/stun, type Collector interface, Close() error pkg github.com/pion/stun, type Collector interface, Start(time.Duration, func(time.Time)) error pkg github.com/pion/stun, type Event struct, TransactionID [12]uint8 pkg github.com/pion/stun, method (*Agent) SetHandler(Handler) error pkg github.com/pion/stun, method (*Agent) Start([12]uint8, time.Time) error pkg github.com/pion/stun, type ClientAgent interface { Close, Collect, Process, SetHandler, Start, Stop } pkg github.com/pion/stun, type ClientAgent interface, SetHandler(Handler) error pkg github.com/pion/stun, type ClientAgent interface, Start([12]uint8, time.Time) error pkg github.com/pion/stun, type ClientOptions struct, Handler Handler pkg github.com/pion/stun, type ClientOptions struct, Clock Clock pkg github.com/pion/stun, type ClientOptions struct, Collector Collector pkg github.com/pion/stun, type ClientOptions struct, RTO time.Duration pkg github.com/pion/stun, func NewAgent(Handler) *Agent pkg github.com/pion/stun, var NoopHandler Handler stun-0.6.1/api/stun1.15.0.txt000066400000000000000000000006201444552150000154530ustar00rootroot00000000000000pkg github.com/pion/stun, const AttrRequestedAddressFamily = 23 pkg github.com/pion/stun, const AttrRequestedAddressFamily AttrType pkg github.com/pion/stun, const CodeAddrFamilyNotSupported = 440 pkg github.com/pion/stun, const CodeAddrFamilyNotSupported ErrorCode pkg github.com/pion/stun, const CodePeerAddrFamilyMismatch = 443 pkg github.com/pion/stun, const CodePeerAddrFamilyMismatch ErrorCode stun-0.6.1/api/stun1.16.0.txt000066400000000000000000000011361444552150000154570ustar00rootroot00000000000000pkg github.com/pion/stun, func NewClient(Connection, ...ClientOption) (*Client, error) pkg github.com/pion/stun, func WithAgent(ClientAgent) ClientOption pkg github.com/pion/stun, func WithClock(Clock) ClientOption pkg github.com/pion/stun, func WithCollector(Collector) ClientOption pkg github.com/pion/stun, func WithHandler(Handler) ClientOption pkg github.com/pion/stun, func WithRTO(time.Duration) ClientOption pkg github.com/pion/stun, func WithTimeoutRate(time.Duration) ClientOption pkg github.com/pion/stun, func WithNoRetransmit(*Client) pkg github.com/pion/stun, type ClientOption func(*Client) stun-0.6.1/api/stun1.17.0.txt000066400000000000000000000010271444552150000154570ustar00rootroot00000000000000pkg github.com/pion/stun, const Scheme = "stun" pkg github.com/pion/stun, const Scheme ideal-string pkg github.com/pion/stun, const SchemeSecure = "stuns" pkg github.com/pion/stun, const SchemeSecure ideal-string pkg github.com/pion/stun, func ParseURI(string) (URI, error) pkg github.com/pion/stun, method (URI) String() string pkg github.com/pion/stun, type URI struct pkg github.com/pion/stun, type URI struct, Host string pkg github.com/pion/stun, type URI struct, Port int pkg github.com/pion/stun, type URI struct, Scheme string stun-0.6.1/api/stun1.17.3.txt000066400000000000000000000013101444552150000154550ustar00rootroot00000000000000pkg github.com/pion/stun, const AttrConnectionID = 42 pkg github.com/pion/stun, const AttrConnectionID AttrType pkg github.com/pion/stun, const CodeConnAlreadyExists = 446 pkg github.com/pion/stun, const CodeConnAlreadyExists ErrorCode pkg github.com/pion/stun, const CodeConnTimeoutOrFailure = 447 pkg github.com/pion/stun, const CodeConnTimeoutOrFailure ErrorCode pkg github.com/pion/stun, const MethodConnect = 10 pkg github.com/pion/stun, const MethodConnect Method pkg github.com/pion/stun, const MethodConnectionAttempt = 12 pkg github.com/pion/stun, const MethodConnectionAttempt Method pkg github.com/pion/stun, const MethodConnectionBind = 11 pkg github.com/pion/stun, const MethodConnectionBind Method stun-0.6.1/api/stun1.18.0.txt000066400000000000000000000001061444552150000154550ustar00rootroot00000000000000pkg github.com/pion/stun, method (RawAttribute) AddTo(*Message) error stun-0.6.1/api/stun1.18.1.txt000066400000000000000000000002511444552150000154570ustar00rootroot00000000000000pkg github.com/pion/stun, const CodeRoleConflict = 487 pkg github.com/pion/stun, const CodeUnauthorized = 401 pkg github.com/pion/stun, const CodeUnauthorized ErrorCode stun-0.6.1/api/stun1.19.0.txt000066400000000000000000000000731444552150000154610ustar00rootroot00000000000000pkg github.com/pion/stun, var WithNoConnClose ClientOption stun-0.6.1/attributes.go000066400000000000000000000201061444552150000152230ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "fmt" ) // Attributes is list of message attributes. type Attributes []RawAttribute // Get returns first attribute from list by the type. // If attribute is present the RawAttribute is returned and the // boolean is true. Otherwise the returned RawAttribute will be // empty and boolean will be false. func (a Attributes) Get(t AttrType) (RawAttribute, bool) { for _, candidate := range a { if candidate.Type == t { return candidate, true } } return RawAttribute{}, false } // AttrType is attribute type. type AttrType uint16 // Required returns true if type is from comprehension-required range (0x0000-0x7FFF). func (t AttrType) Required() bool { return t <= 0x7FFF } // Optional returns true if type is from comprehension-optional range (0x8000-0xFFFF). func (t AttrType) Optional() bool { return t >= 0x8000 } // Attributes from comprehension-required range (0x0000-0x7FFF). const ( AttrMappedAddress AttrType = 0x0001 // MAPPED-ADDRESS AttrUsername AttrType = 0x0006 // USERNAME AttrMessageIntegrity AttrType = 0x0008 // MESSAGE-INTEGRITY AttrErrorCode AttrType = 0x0009 // ERROR-CODE AttrUnknownAttributes AttrType = 0x000A // UNKNOWN-ATTRIBUTES AttrRealm AttrType = 0x0014 // REALM AttrNonce AttrType = 0x0015 // NONCE AttrXORMappedAddress AttrType = 0x0020 // XOR-MAPPED-ADDRESS ) // Attributes from comprehension-optional range (0x8000-0xFFFF). const ( AttrSoftware AttrType = 0x8022 // SOFTWARE AttrAlternateServer AttrType = 0x8023 // ALTERNATE-SERVER AttrFingerprint AttrType = 0x8028 // FINGERPRINT ) // Attributes from RFC 5245 ICE. const ( AttrPriority AttrType = 0x0024 // PRIORITY AttrUseCandidate AttrType = 0x0025 // USE-CANDIDATE AttrICEControlled AttrType = 0x8029 // ICE-CONTROLLED AttrICEControlling AttrType = 0x802A // ICE-CONTROLLING ) // Attributes from RFC 5766 TURN. const ( AttrChannelNumber AttrType = 0x000C // CHANNEL-NUMBER AttrLifetime AttrType = 0x000D // LIFETIME AttrXORPeerAddress AttrType = 0x0012 // XOR-PEER-ADDRESS AttrData AttrType = 0x0013 // DATA AttrXORRelayedAddress AttrType = 0x0016 // XOR-RELAYED-ADDRESS AttrEvenPort AttrType = 0x0018 // EVEN-PORT AttrRequestedTransport AttrType = 0x0019 // REQUESTED-TRANSPORT AttrDontFragment AttrType = 0x001A // DONT-FRAGMENT AttrReservationToken AttrType = 0x0022 // RESERVATION-TOKEN ) // Attributes from RFC 5780 NAT Behavior Discovery const ( AttrChangeRequest AttrType = 0x0003 // CHANGE-REQUEST AttrPadding AttrType = 0x0026 // PADDING AttrResponsePort AttrType = 0x0027 // RESPONSE-PORT AttrCacheTimeout AttrType = 0x8027 // CACHE-TIMEOUT AttrResponseOrigin AttrType = 0x802b // RESPONSE-ORIGIN AttrOtherAddress AttrType = 0x802C // OTHER-ADDRESS ) // Attributes from RFC 3489, removed by RFC 5389, // // but still used by RFC5389-implementing software like Vovida.org, reTURNServer, etc. const ( AttrSourceAddress AttrType = 0x0004 // SOURCE-ADDRESS AttrChangedAddress AttrType = 0x0005 // CHANGED-ADDRESS ) // Attributes from RFC 6062 TURN Extensions for TCP Allocations. const ( AttrConnectionID AttrType = 0x002a // CONNECTION-ID ) // Attributes from RFC 6156 TURN IPv6. const ( AttrRequestedAddressFamily AttrType = 0x0017 // REQUESTED-ADDRESS-FAMILY ) // Attributes from An Origin Attribute for the STUN Protocol. const ( AttrOrigin AttrType = 0x802F ) // Attributes from RFC 8489 STUN. const ( AttrMessageIntegritySHA256 AttrType = 0x001C // MESSAGE-INTEGRITY-SHA256 AttrPasswordAlgorithm AttrType = 0x001D // PASSWORD-ALGORITHM AttrUserhash AttrType = 0x001E // USERHASH AttrPasswordAlgorithms AttrType = 0x8002 // PASSWORD-ALGORITHMS AttrAlternateDomain AttrType = 0x8003 // ALTERNATE-DOMAIN ) // Value returns uint16 representation of attribute type. func (t AttrType) Value() uint16 { return uint16(t) } func attrNames() map[AttrType]string { return map[AttrType]string{ AttrMappedAddress: "MAPPED-ADDRESS", AttrUsername: "USERNAME", AttrErrorCode: "ERROR-CODE", AttrMessageIntegrity: "MESSAGE-INTEGRITY", AttrUnknownAttributes: "UNKNOWN-ATTRIBUTES", AttrRealm: "REALM", AttrNonce: "NONCE", AttrXORMappedAddress: "XOR-MAPPED-ADDRESS", AttrSoftware: "SOFTWARE", AttrAlternateServer: "ALTERNATE-SERVER", AttrFingerprint: "FINGERPRINT", AttrPriority: "PRIORITY", AttrUseCandidate: "USE-CANDIDATE", AttrICEControlled: "ICE-CONTROLLED", AttrICEControlling: "ICE-CONTROLLING", AttrChannelNumber: "CHANNEL-NUMBER", AttrLifetime: "LIFETIME", AttrXORPeerAddress: "XOR-PEER-ADDRESS", AttrData: "DATA", AttrXORRelayedAddress: "XOR-RELAYED-ADDRESS", AttrEvenPort: "EVEN-PORT", AttrRequestedTransport: "REQUESTED-TRANSPORT", AttrDontFragment: "DONT-FRAGMENT", AttrReservationToken: "RESERVATION-TOKEN", AttrConnectionID: "CONNECTION-ID", AttrRequestedAddressFamily: "REQUESTED-ADDRESS-FAMILY", AttrMessageIntegritySHA256: "MESSAGE-INTEGRITY-SHA256", AttrPasswordAlgorithm: "PASSWORD-ALGORITHM", AttrUserhash: "USERHASH", AttrPasswordAlgorithms: "PASSWORD-ALGORITHMS", AttrAlternateDomain: "ALTERNATE-DOMAIN", } } func (t AttrType) String() string { s, ok := attrNames()[t] if !ok { // Just return hex representation of unknown attribute type. return fmt.Sprintf("0x%x", uint16(t)) } return s } // RawAttribute is a Type-Length-Value (TLV) object that // can be added to a STUN message. Attributes are divided into two // types: comprehension-required and comprehension-optional. STUN // agents can safely ignore comprehension-optional attributes they // don't understand, but cannot successfully process a message if it // contains comprehension-required attributes that are not // understood. type RawAttribute struct { Type AttrType Length uint16 // ignored while encoding Value []byte } // AddTo implements Setter, adding attribute as a.Type with a.Value and ignoring // the Length field. func (a RawAttribute) AddTo(m *Message) error { m.Add(a.Type, a.Value) return nil } // Equal returns true if a == b. func (a RawAttribute) Equal(b RawAttribute) bool { if a.Type != b.Type { return false } if a.Length != b.Length { return false } if len(b.Value) != len(a.Value) { return false } for i, v := range a.Value { if b.Value[i] != v { return false } } return true } func (a RawAttribute) String() string { return fmt.Sprintf("%s: 0x%x", a.Type, a.Value) } // ErrAttributeNotFound means that attribute with provided attribute // type does not exist in message. var ErrAttributeNotFound = errors.New("attribute not found") // Get returns byte slice that represents attribute value, // if there is no attribute with such type, // ErrAttributeNotFound is returned. func (m *Message) Get(t AttrType) ([]byte, error) { v, ok := m.Attributes.Get(t) if !ok { return nil, ErrAttributeNotFound } return v.Value, nil } // STUN aligns attributes on 32-bit boundaries, attributes whose content // is not a multiple of 4 bytes are padded with 1, 2, or 3 bytes of // padding so that its value contains a multiple of 4 bytes. The // padding bits are ignored, and may be any value. // // https://tools.ietf.org/html/rfc5389#section-15 const padding = 4 func nearestPaddedValueLength(l int) int { n := padding * (l / padding) if n < l { n += padding } return n } // This method converts uint16 vlue to AttrType. If it finds an old attribute // type value, it also translates it to the new value to enable backward // compatibility. (See: https://github.com/pion/stun/issues/21) func compatAttrType(val uint16) AttrType { if val == 0x8020 { // draft-ietf-behave-rfc3489bis-02, MS-TURN return AttrXORMappedAddress // new: 0x0020 (from draft-ietf-behave-rfc3489bis-03 on) } return AttrType(val) } stun-0.6.1/attributes_debug.go000066400000000000000000000013221444552150000163700ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build debug // +build debug package stun import "fmt" // AttrOverflowErr occurs when len(v) > Max. type AttrOverflowErr struct { Type AttrType Max int Got int } func (e AttrOverflowErr) Error() string { return fmt.Sprintf("incorrect length of %s attribute: %d exceeds maximum %d", e.Type, e.Got, e.Max, ) } // AttrLengthErr means that length for attribute is invalid. type AttrLengthErr struct { Attr AttrType Got int Expected int } func (e AttrLengthErr) Error() string { return fmt.Sprintf("incorrect length of %s attribute: got %d, expected %d", e.Attr, e.Got, e.Expected, ) } stun-0.6.1/attributes_debug_test.go000066400000000000000000000012451444552150000174330ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build debug // +build debug package stun import "testing" func TestAttrOverflowErr_Error(t *testing.T) { err := AttrOverflowErr{ Got: 100, Max: 50, Type: AttrLifetime, } if err.Error() != "incorrect length of LIFETIME attribute: 100 exceeds maximum 50" { t.Error("bad error string", err) } } func TestAttrLengthErr_Error(t *testing.T) { err := AttrLengthErr{ Attr: AttrErrorCode, Expected: 15, Got: 99, } if err.Error() != "incorrect length of ERROR-CODE attribute: got 99, expected 15" { t.Errorf("bad error string: %s", err) } } stun-0.6.1/attributes_test.go000066400000000000000000000044501444552150000162660ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "bytes" "testing" ) func BenchmarkMessage_GetNotFound(b *testing.B) { m := New() b.ReportAllocs() for i := 0; i < b.N; i++ { m.Get(AttrRealm) //nolint:errcheck,gosec } } func BenchmarkMessage_Get(b *testing.B) { m := New() m.Add(AttrUsername, []byte{1, 2, 3, 4, 5, 6, 7}) b.ReportAllocs() for i := 0; i < b.N; i++ { m.Get(AttrUsername) //nolint:errcheck,gosec } } func TestRawAttribute_AddTo(t *testing.T) { v := []byte{1, 2, 3, 4} m, err := Build(RawAttribute{ Type: AttrData, Value: v, }) if err != nil { t.Fatal(err) } gotV, gotErr := m.Get(AttrData) if gotErr != nil { t.Fatal(gotErr) } if !bytes.Equal(gotV, v) { t.Error("value mismatch") } } func TestMessage_GetNoAllocs(t *testing.T) { m := New() NewSoftware("c").AddTo(m) //nolint:errcheck,gosec m.WriteHeader() t.Run("Default", func(t *testing.T) { allocs := testing.AllocsPerRun(10, func() { m.Get(AttrSoftware) //nolint:errcheck,gosec }) if allocs > 0 { t.Error("allocated memory, but should not") } }) t.Run("Not found", func(t *testing.T) { allocs := testing.AllocsPerRun(10, func() { m.Get(AttrOrigin) //nolint:errcheck,gosec }) if allocs > 0 { t.Error("allocated memory, but should not") } }) } func TestPadding(t *testing.T) { tt := []struct { in, out int }{ {4, 4}, // 0 {2, 4}, // 1 {5, 8}, // 2 {8, 8}, // 3 {11, 12}, // 4 {1, 4}, // 5 {3, 4}, // 6 {6, 8}, // 7 {7, 8}, // 8 {0, 0}, // 9 {40, 40}, // 10 } for i, c := range tt { if got := nearestPaddedValueLength(c.in); got != c.out { t.Errorf("[%d]: padd(%d) %d (got) != %d (expected)", i, c.in, got, c.out, ) } } } func TestAttrTypeRange(t *testing.T) { for _, a := range []AttrType{ AttrPriority, AttrErrorCode, AttrUseCandidate, AttrEvenPort, AttrRequestedAddressFamily, } { a := a t.Run(a.String(), func(t *testing.T) { a := a if a.Optional() || !a.Required() { t.Error("should be required") } }) } for _, a := range []AttrType{ AttrSoftware, AttrICEControlled, AttrOrigin, } { a := a t.Run(a.String(), func(t *testing.T) { if a.Required() || !a.Optional() { t.Error("should be optional") } }) } } stun-0.6.1/checks.go000066400000000000000000000022711444552150000143000ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !debug // +build !debug package stun import ( "errors" "github.com/pion/stun/internal/hmac" ) // CheckSize returns ErrAttrSizeInvalid if got is not equal to expected. func CheckSize(_ AttrType, got, expected int) error { if got == expected { return nil } return ErrAttributeSizeInvalid } func checkHMAC(got, expected []byte) error { if hmac.Equal(got, expected) { return nil } return ErrIntegrityMismatch } func checkFingerprint(got, expected uint32) error { if got == expected { return nil } return ErrFingerprintMismatch } // IsAttrSizeInvalid returns true if error means that attribute size is invalid. func IsAttrSizeInvalid(err error) bool { return errors.Is(err, ErrAttributeSizeInvalid) } // CheckOverflow returns ErrAttributeSizeOverflow if got is bigger that max. func CheckOverflow(_ AttrType, got, max int) error { if got <= max { return nil } return ErrAttributeSizeOverflow } // IsAttrSizeOverflow returns true if error means that attribute size is too big. func IsAttrSizeOverflow(err error) bool { return errors.Is(err, ErrAttributeSizeOverflow) } stun-0.6.1/checks_debug.go000066400000000000000000000024511444552150000154460ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build debug // +build debug package stun import "github.com/pion/stun/internal/hmac" // CheckSize returns *AttrLengthError if got is not equal to expected. func CheckSize(a AttrType, got, expected int) error { if got == expected { return nil } return &AttrLengthErr{ Got: got, Expected: expected, Attr: a, } } func checkHMAC(got, expected []byte) error { if hmac.Equal(got, expected) { return nil } return &IntegrityErr{ Expected: expected, Actual: got, } } func checkFingerprint(got, expected uint32) error { if got == expected { return nil } return &CRCMismatch{ Actual: got, Expected: expected, } } // IsAttrSizeInvalid returns true if error means that attribute size is invalid. func IsAttrSizeInvalid(err error) bool { _, ok := err.(*AttrLengthErr) return ok } // CheckOverflow returns *AttrOverflowErr if got is bigger that max. func CheckOverflow(t AttrType, got, max int) error { if got <= max { return nil } return &AttrOverflowErr{ Type: t, Got: got, Max: max, } } // IsAttrSizeOverflow returns true if error means that attribute size is too big. func IsAttrSizeOverflow(err error) bool { _, ok := err.(*AttrOverflowErr) return ok } stun-0.6.1/client.go000066400000000000000000000403501444552150000143160ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "crypto/tls" "errors" "fmt" "io" "log" "net" "runtime" "strconv" "sync" "sync/atomic" "time" "github.com/pion/dtls/v2" "github.com/pion/transport/v2" "github.com/pion/transport/v2/stdnet" ) // ErrUnsupportedURI is an error thrown if the user passes an unsupported STUN or TURN URI var ErrUnsupportedURI = fmt.Errorf("invalid schema or transport") // Dial connects to the address on the named network and then // initializes Client on that connection, returning error if any. func Dial(network, address string) (*Client, error) { conn, err := net.Dial(network, address) if err != nil { return nil, err } return NewClient(conn) } // DialConfig is used to pass configuration to DialURI() type DialConfig struct { DTLSConfig dtls.Config TLSConfig tls.Config Net transport.Net } // DialURI connect to the STUN/TURN URI and then // initializes Client on that connection, returning error if any. func DialURI(uri *URI, cfg *DialConfig) (*Client, error) { var conn Connection var err error nw := cfg.Net if nw == nil { nw, err = stdnet.NewNet() if err != nil { return nil, fmt.Errorf("failed to create net: %w", err) } } addr := net.JoinHostPort(uri.Host, strconv.Itoa(uri.Port)) switch { case uri.Scheme == SchemeTypeSTUN: if conn, err = nw.Dial("udp", addr); err != nil { return nil, fmt.Errorf("failed to listen: %w", err) } case uri.Scheme == SchemeTypeTURN: network := "udp" //nolint:goconst if uri.Proto == ProtoTypeTCP { network = "tcp" //nolint:goconst } if conn, err = nw.Dial(network, addr); err != nil { return nil, fmt.Errorf("failed to dial: %w", err) } case uri.Scheme == SchemeTypeTURNS && uri.Proto == ProtoTypeUDP: dtlsCfg := cfg.DTLSConfig // Copy dtlsCfg.ServerName = uri.Host udpConn, err := nw.Dial("udp", addr) if err != nil { return nil, fmt.Errorf("failed to dial: %w", err) } if conn, err = dtls.Client(udpConn, &dtlsCfg); err != nil { return nil, fmt.Errorf("failed to connect to '%s': %w", addr, err) } case (uri.Scheme == SchemeTypeTURNS || uri.Scheme == SchemeTypeSTUNS) && uri.Proto == ProtoTypeTCP: tlsCfg := cfg.TLSConfig //nolint:govet tlsCfg.ServerName = uri.Host tcpConn, err := nw.Dial("tcp", addr) if err != nil { return nil, fmt.Errorf("failed to dial: %w", err) } conn = tls.Client(tcpConn, &tlsCfg) default: return nil, ErrUnsupportedURI } return NewClient(conn) } // ErrNoConnection means that ClientOptions.Connection is nil. var ErrNoConnection = errors.New("no connection provided") // ClientOption sets some client option. type ClientOption func(c *Client) // WithHandler sets client handler which is called if Agent emits the Event // with TransactionID that is not currently registered by Client. // Useful for handling Data indications from TURN server. func WithHandler(h Handler) ClientOption { return func(c *Client) { c.handler = h } } // WithRTO sets client RTO as defined in STUN RFC. func WithRTO(rto time.Duration) ClientOption { return func(c *Client) { c.rto = int64(rto) } } // WithClock sets Clock of client, the source of current time. // Also clock is passed to default collector if set. func WithClock(clock Clock) ClientOption { return func(c *Client) { c.clock = clock } } // WithTimeoutRate sets RTO timer minimum resolution. func WithTimeoutRate(d time.Duration) ClientOption { return func(c *Client) { c.rtoRate = d } } // WithAgent sets client STUN agent. // // Defaults to agent implementation in current package, // see agent.go. func WithAgent(a ClientAgent) ClientOption { return func(c *Client) { c.a = a } } // WithCollector rests client timeout collector, the implementation // of ticker which calls function on each tick. func WithCollector(coll Collector) ClientOption { return func(c *Client) { c.collector = coll } } // WithNoConnClose prevents client from closing underlying connection when // the Close() method is called. func WithNoConnClose() ClientOption { return func(c *Client) { c.closeConn = false } } // WithNoRetransmit disables retransmissions and sets RTO to // defaultMaxAttempts * defaultRTO which will be effectively time out // if not set. // // Useful for TCP connections where transport handles RTO. func WithNoRetransmit(c *Client) { c.maxAttempts = 0 if c.rto == 0 { c.rto = defaultMaxAttempts * int64(defaultRTO) } } const ( defaultTimeoutRate = time.Millisecond * 5 defaultRTO = time.Millisecond * 300 defaultMaxAttempts = 7 ) // NewClient initializes new Client from provided options, // starting internal goroutines and using default options fields // if necessary. Call Close method after using Client to close conn and // release resources. // // The conn will be closed on Close call. Use WithNoConnClose option to // prevent that. // // Note that user should handle the protocol multiplexing, client does not // provide any API for it, so if you need to read application data, wrap the // connection with your (de-)multiplexer and pass the wrapper as conn. func NewClient(conn Connection, options ...ClientOption) (*Client, error) { c := &Client{ close: make(chan struct{}), c: conn, clock: systemClock(), rto: int64(defaultRTO), rtoRate: defaultTimeoutRate, t: make(map[transactionID]*clientTransaction, 100), maxAttempts: defaultMaxAttempts, closeConn: true, } for _, o := range options { o(c) } if c.c == nil { return nil, ErrNoConnection } if c.a == nil { c.a = NewAgent(nil) } if err := c.a.SetHandler(c.handleAgentCallback); err != nil { return nil, err } if c.collector == nil { c.collector = &tickerCollector{ close: make(chan struct{}), clock: c.clock, } } if err := c.collector.Start(c.rtoRate, func(t time.Time) { closedOrPanic(c.a.Collect(t)) }); err != nil { return nil, err } c.wg.Add(1) go c.readUntilClosed() runtime.SetFinalizer(c, clientFinalizer) return c, nil } func clientFinalizer(c *Client) { if c == nil { return } err := c.Close() if errors.Is(err, ErrClientClosed) { return } if err == nil { log.Println("client: called finalizer on non-closed client") // nolint return } log.Println("client: called finalizer on non-closed client:", err) // nolint } // Connection wraps Reader, Writer and Closer interfaces. type Connection interface { io.Reader io.Writer io.Closer } // ClientAgent is Agent implementation that is used by Client to // process transactions. type ClientAgent interface { Process(*Message) error Close() error Start(id [TransactionIDSize]byte, deadline time.Time) error Stop(id [TransactionIDSize]byte) error Collect(time.Time) error SetHandler(h Handler) error } // Client simulates "connection" to STUN server. type Client struct { rto int64 // time.Duration a ClientAgent c Connection close chan struct{} rtoRate time.Duration maxAttempts int32 closed bool closeConn bool // should call c.Close() while closing wg sync.WaitGroup clock Clock handler Handler collector Collector t map[transactionID]*clientTransaction // mux guards closed and t mux sync.RWMutex } // clientTransaction represents transaction in progress. // If transaction is succeed or failed, f will be called // provided by event. // Concurrent access is invalid. type clientTransaction struct { id transactionID attempt int32 calls int32 h Handler start time.Time rto time.Duration raw []byte } func (t *clientTransaction) handle(e Event) { if atomic.AddInt32(&t.calls, 1) == 1 { t.h(e) } } var clientTransactionPool = &sync.Pool{ //nolint:gochecknoglobals New: func() interface{} { return &clientTransaction{ raw: make([]byte, 1500), } }, } func acquireClientTransaction() *clientTransaction { return clientTransactionPool.Get().(*clientTransaction) //nolint:forcetypeassert } func putClientTransaction(t *clientTransaction) { t.raw = t.raw[:0] t.start = time.Time{} t.attempt = 0 t.id = transactionID{} clientTransactionPool.Put(t) } func (t *clientTransaction) nextTimeout(now time.Time) time.Time { return now.Add(time.Duration(t.attempt+1) * t.rto) } // start registers transaction. // // Could return ErrClientClosed, ErrTransactionExists. func (c *Client) start(t *clientTransaction) error { c.mux.Lock() defer c.mux.Unlock() if c.closed { return ErrClientClosed } _, exists := c.t[t.id] if exists { return ErrTransactionExists } c.t[t.id] = t return nil } // Clock abstracts the source of current time. type Clock interface { Now() time.Time } type systemClockService struct{} func (systemClockService) Now() time.Time { return time.Now() } func systemClock() systemClockService { return systemClockService{} } // SetRTO sets current RTO value. func (c *Client) SetRTO(rto time.Duration) { atomic.StoreInt64(&c.rto, int64(rto)) } // StopErr occurs when Client fails to stop transaction while // processing error. // //nolint:errname type StopErr struct { Err error // value returned by Stop() Cause error // error that caused Stop() call } func (e StopErr) Error() string { return fmt.Sprintf("error while stopping due to %s: %s", sprintErr(e.Cause), sprintErr(e.Err)) } // CloseErr indicates client close failure. // //nolint:errname type CloseErr struct { AgentErr error ConnectionErr error } func sprintErr(err error) string { if err == nil { return "" //nolint:goconst } return err.Error() } func (c CloseErr) Error() string { return fmt.Sprintf("failed to close: %s (connection), %s (agent)", sprintErr(c.ConnectionErr), sprintErr(c.AgentErr)) } func (c *Client) readUntilClosed() { defer c.wg.Done() m := new(Message) m.Raw = make([]byte, 1024) for { select { case <-c.close: return default: } _, err := m.ReadFrom(c.c) if err == nil { if pErr := c.a.Process(m); errors.Is(pErr, ErrAgentClosed) { return } } } } func closedOrPanic(err error) { if err == nil || errors.Is(err, ErrAgentClosed) { return } panic(err) //nolint } type tickerCollector struct { close chan struct{} wg sync.WaitGroup clock Clock } // Collector calls function f with constant rate. // // The simple Collector is ticker which calls function on each tick. type Collector interface { Start(rate time.Duration, f func(now time.Time)) error Close() error } func (a *tickerCollector) Start(rate time.Duration, f func(now time.Time)) error { t := time.NewTicker(rate) a.wg.Add(1) go func() { defer a.wg.Done() for { select { case <-a.close: t.Stop() return case <-t.C: f(a.clock.Now()) } } }() return nil } func (a *tickerCollector) Close() error { close(a.close) a.wg.Wait() return nil } // ErrClientClosed indicates that client is closed. var ErrClientClosed = errors.New("client is closed") // Close stops internal connection and agent, returning CloseErr on error. func (c *Client) Close() error { if err := c.checkInit(); err != nil { return err } c.mux.Lock() if c.closed { c.mux.Unlock() return ErrClientClosed } c.closed = true c.mux.Unlock() if closeErr := c.collector.Close(); closeErr != nil { return closeErr } var connErr error agentErr := c.a.Close() if c.closeConn { connErr = c.c.Close() } close(c.close) c.wg.Wait() if agentErr == nil && connErr == nil { return nil } return CloseErr{ AgentErr: agentErr, ConnectionErr: connErr, } } // Indicate sends indication m to server. Shorthand to Start call // with zero deadline and callback. func (c *Client) Indicate(m *Message) error { return c.Start(m, nil) } // callbackWaitHandler blocks on wait() call until callback is called. type callbackWaitHandler struct { handler Handler callback func(event Event) cond *sync.Cond processed bool } func (s *callbackWaitHandler) HandleEvent(e Event) { s.cond.L.Lock() if s.callback == nil { panic("s.callback is nil") //nolint } s.callback(e) s.processed = true s.cond.Broadcast() s.cond.L.Unlock() } func (s *callbackWaitHandler) wait() { s.cond.L.Lock() for !s.processed { s.cond.Wait() } s.processed = false s.callback = nil s.cond.L.Unlock() } func (s *callbackWaitHandler) setCallback(f func(event Event)) { if f == nil { panic("f is nil") //nolint } s.cond.L.Lock() s.callback = f if s.handler == nil { s.handler = s.HandleEvent } s.cond.L.Unlock() } var callbackWaitHandlerPool = sync.Pool{ //nolint:gochecknoglobals New: func() interface{} { return &callbackWaitHandler{ cond: sync.NewCond(new(sync.Mutex)), } }, } // ErrClientNotInitialized means that client connection or agent is nil. var ErrClientNotInitialized = errors.New("client not initialized") func (c *Client) checkInit() error { if c == nil || c.c == nil || c.a == nil || c.close == nil { return ErrClientNotInitialized } return nil } // Do is Start wrapper that waits until callback is called. If no callback // provided, Indicate is called instead. // // Do has cpu overhead due to blocking, see BenchmarkClient_Do. // Use Start method for less overhead. func (c *Client) Do(m *Message, f func(Event)) error { if err := c.checkInit(); err != nil { return err } if f == nil { return c.Indicate(m) } h := callbackWaitHandlerPool.Get().(*callbackWaitHandler) //nolint:forcetypeassert h.setCallback(f) defer func() { callbackWaitHandlerPool.Put(h) }() if err := c.Start(m, h.handler); err != nil { return err } h.wait() return nil } func (c *Client) delete(id transactionID) { c.mux.Lock() if c.t != nil { delete(c.t, id) } c.mux.Unlock() } type buffer struct { buf []byte } var bufferPool = &sync.Pool{ //nolint:gochecknoglobals New: func() interface{} { return &buffer{buf: make([]byte, 2048)} }, } func (c *Client) handleAgentCallback(e Event) { c.mux.Lock() if c.closed { c.mux.Unlock() return } t, found := c.t[e.TransactionID] if found { delete(c.t, t.id) } c.mux.Unlock() if !found { if c.handler != nil && !errors.Is(e.Error, ErrTransactionStopped) { c.handler(e) } // Ignoring. return } if atomic.LoadInt32(&c.maxAttempts) <= t.attempt || e.Error == nil { // Transaction completed. t.handle(e) putClientTransaction(t) return } // Doing re-transmission. t.attempt++ b := bufferPool.Get().(*buffer) //nolint:forcetypeassert b.buf = b.buf[:copy(b.buf[:cap(b.buf)], t.raw)] defer bufferPool.Put(b) var ( now = c.clock.Now() timeOut = t.nextTimeout(now) id = t.id ) // Starting client transaction. if startErr := c.start(t); startErr != nil { c.delete(id) e.Error = startErr t.handle(e) putClientTransaction(t) return } // Starting agent transaction. if startErr := c.a.Start(id, timeOut); startErr != nil { c.delete(id) e.Error = startErr t.handle(e) putClientTransaction(t) return } // Writing message to connection again. _, writeErr := c.c.Write(b.buf) if writeErr != nil { c.delete(id) e.Error = writeErr // Stopping agent transaction instead of waiting until it's deadline. // This will call handleAgentCallback with "ErrTransactionStopped" error // which will be ignored. if stopErr := c.a.Stop(id); stopErr != nil { // Failed to stop agent transaction. Wrapping the error in StopError. e.Error = StopErr{ Err: stopErr, Cause: writeErr, } } t.handle(e) putClientTransaction(t) return } } // Start starts transaction (if h set) and writes message to server, handler // is called asynchronously. func (c *Client) Start(m *Message, h Handler) error { if err := c.checkInit(); err != nil { return err } c.mux.RLock() closed := c.closed c.mux.RUnlock() if closed { return ErrClientClosed } if h != nil { // Starting transaction only if h is set. Useful for indications. t := acquireClientTransaction() t.id = m.TransactionID t.start = c.clock.Now() t.h = h t.rto = time.Duration(atomic.LoadInt64(&c.rto)) t.attempt = 0 t.raw = append(t.raw[:0], m.Raw...) t.calls = 0 d := t.nextTimeout(t.start) if err := c.start(t); err != nil { return err } if err := c.a.Start(m.TransactionID, d); err != nil { return err } } _, err := m.WriteTo(c.c) if err != nil && h != nil { c.delete(m.TransactionID) // Stopping transaction instead of waiting until deadline. if stopErr := c.a.Stop(m.TransactionID); stopErr != nil { return StopErr{ Err: stopErr, Cause: err, } } } return err } stun-0.6.1/client_test.go000066400000000000000000000766611444552150000153730ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package stun import ( "bufio" "bytes" "errors" "fmt" "io" "log" "net" "os" "sync" "testing" "time" ) var ( errClientAlreadyStopped = errors.New("already stopped") errClientReadTimedOut = errors.New("read timed out") errClientWriteTimedOut = errors.New("write timed out") errClientCloseError = errors.New("close error") errClientSetHandler = errors.New("set handler error") errClientStart = errors.New("start error") errClientAgentCantStop = errors.New("agent does not want to stop") errClientStartRefused = errors.New("start refused") ) type TestAgent struct { h Handler e chan Event } func (n *TestAgent) SetHandler(h Handler) error { n.h = h return nil } func (n *TestAgent) Close() error { close(n.e) return nil } func (TestAgent) Collect(time.Time) error { return nil } func (TestAgent) Process(*Message) error { return nil } func (n *TestAgent) Start(id [TransactionIDSize]byte, _ time.Time) error { n.e <- Event{ TransactionID: id, } return nil } func (n *TestAgent) Stop([TransactionIDSize]byte) error { return nil } type noopConnection struct{} func (noopConnection) Write(b []byte) (int, error) { return len(b), nil } func (noopConnection) Read([]byte) (int, error) { time.Sleep(time.Millisecond) return 0, io.EOF } func (noopConnection) Close() error { return nil } func BenchmarkClient_Do(b *testing.B) { b.ReportAllocs() agent := &TestAgent{ e: make(chan Event, 1000), } client, err := NewClient(noopConnection{}, WithAgent(agent), ) if err != nil { log.Fatal(err) } defer func() { if closeErr := client.Close(); closeErr != nil { panic(closeErr) } }() noopF := func(event Event) { // pass } b.RunParallel(func(pb *testing.PB) { go func() { for e := range agent.e { agent.h(e) } }() m := New() m.NewTransactionID() //nolint:errcheck,gosec m.Encode() for pb.Next() { if err := client.Do(m, noopF); err != nil { b.Error(err) } } }) } type testConnection struct { write func([]byte) (int, error) read func([]byte) (int, error) close func() error b []byte stopped bool stoppedMux sync.Mutex } func (t *testConnection) Write(b []byte) (int, error) { return t.write(b) } func (t *testConnection) Close() error { if t.close != nil { return t.close() } t.stoppedMux.Lock() defer t.stoppedMux.Unlock() if t.stopped { return errClientAlreadyStopped } t.stopped = true return nil } func (t *testConnection) Read(b []byte) (int, error) { t.stoppedMux.Lock() defer t.stoppedMux.Unlock() if t.stopped { return 0, io.EOF } if t.read != nil { return t.read(b) } return copy(b, t.b), nil } func TestClosedOrPanic(t *testing.T) { closedOrPanic(nil) closedOrPanic(ErrAgentClosed) func() { defer func() { r, ok := recover().(error) if !ok || !errors.Is(r, io.EOF) { t.Error(r) } }() closedOrPanic(io.EOF) }() } func TestClient_Start(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() write := make(chan struct{}, 1) read := make(chan struct{}, 1) conn := &testConnection{ b: response.Raw, read: func(i []byte) (int, error) { t.Log("waiting for read") select { case <-read: t.Log("reading") copy(i, response.Raw) return len(response.Raw), nil case <-time.After(time.Millisecond * 10): return 0, errClientReadTimedOut } }, write: func(bytes []byte) (int, error) { t.Log("waiting for write") select { case <-write: t.Log("writing") return len(bytes), nil case <-time.After(time.Millisecond * 10): return 0, errClientWriteTimedOut } }, } c, err := NewClient(conn) if err != nil { log.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } if err := c.Close(); err == nil { t.Error("second close should fail") } if err := c.Do(MustBuild(TransactionID), nil); err == nil { t.Error("Do after Close should fail") } }() m := MustBuild(response, BindingRequest) t.Log("init") got := make(chan struct{}) write <- struct{}{} t.Log("starting the first transaction") if err := c.Start(m, func(event Event) { t.Log("got first transaction callback") if event.Error != nil { t.Error(event.Error) } got <- struct{}{} }); err != nil { t.Error(err) } t.Log("starting the second transaction") if err := c.Start(m, func(e Event) { t.Error("should not be called") }); !errors.Is(err, ErrTransactionExists) { t.Errorf("unexpected error %v", err) } read <- struct{}{} select { case <-got: // pass case <-time.After(time.Millisecond * 10): t.Error("timed out") } } func TestClient_Do(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() conn := &testConnection{ b: response.Raw, write: func(bytes []byte) (int, error) { return len(bytes), nil }, } c, err := NewClient(conn) if err != nil { log.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } if err := c.Close(); err == nil { t.Error("second close should fail") } if err := c.Do(MustBuild(TransactionID), nil); err == nil { t.Error("Do after Close should fail") } }() m := MustBuild( NewTransactionIDSetter(response.TransactionID), ) if err := c.Do(m, func(event Event) { if event.Error != nil { t.Error(event.Error) } }); err != nil { t.Error(err) } m = MustBuild(TransactionID) if err := c.Do(m, nil); err != nil { t.Error(err) } } func TestCloseErr_Error(t *testing.T) { for id, c := range []struct { Err CloseErr Out string }{ {CloseErr{}, "failed to close: (connection), (agent)"}, {CloseErr{ AgentErr: io.ErrUnexpectedEOF, }, "failed to close: (connection), unexpected EOF (agent)"}, {CloseErr{ ConnectionErr: io.ErrUnexpectedEOF, }, "failed to close: unexpected EOF (connection), (agent)"}, } { if out := c.Err.Error(); out != c.Out { t.Errorf("[%d]: Error(%#v) %q (got) != %q (expected)", id, c.Err, out, c.Out, ) } } } func TestStopErr_Error(t *testing.T) { for id, c := range []struct { Err StopErr Out string }{ {StopErr{}, "error while stopping due to : "}, {StopErr{ Err: io.ErrUnexpectedEOF, }, "error while stopping due to : unexpected EOF"}, {StopErr{ Cause: io.ErrUnexpectedEOF, }, "error while stopping due to unexpected EOF: "}, } { if out := c.Err.Error(); out != c.Out { t.Errorf("[%d]: Error(%#v) %q (got) != %q (expected)", id, c.Err, out, c.Out, ) } } } type errorAgent struct { startErr error stopErr error closeErr error setHandlerError error } func (a errorAgent) SetHandler(Handler) error { return a.setHandlerError } func (a errorAgent) Close() error { return a.closeErr } func (errorAgent) Collect(time.Time) error { return nil } func (errorAgent) Process(*Message) error { return nil } func (a errorAgent) Start([TransactionIDSize]byte, time.Time) error { return a.startErr } func (a errorAgent) Stop([TransactionIDSize]byte) error { return a.stopErr } func TestClientAgentError(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() conn := &testConnection{ b: response.Raw, write: func(bytes []byte) (int, error) { return len(bytes), nil }, } c, err := NewClient(conn, WithAgent(errorAgent{ startErr: io.ErrUnexpectedEOF, }), ) if err != nil { log.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() m := MustBuild(NewTransactionIDSetter(response.TransactionID)) if err := c.Do(m, nil); err != nil { t.Error(err) } if err := c.Do(m, func(event Event) { if event.Error == nil { t.Error("error expected") } }); !errors.Is(err, io.ErrUnexpectedEOF) { t.Error("error expected") } } func TestClientConnErr(t *testing.T) { conn := &testConnection{ write: func(bytes []byte) (int, error) { return 0, io.ErrClosedPipe }, } c, err := NewClient(conn) if err != nil { log.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() m := MustBuild(TransactionID) if err := c.Do(m, nil); err == nil { t.Error("error expected") } if err := c.Do(m, NoopHandler()); err == nil { t.Error("error expected") } } func TestClientConnErrStopErr(t *testing.T) { conn := &testConnection{ write: func(bytes []byte) (int, error) { return 0, io.ErrClosedPipe }, } c, err := NewClient(conn, WithAgent(errorAgent{ stopErr: io.ErrUnexpectedEOF, }), ) if err != nil { log.Fatal(err) } defer func() { if err := c.Close(); err != nil { t.Error(err) } }() m := MustBuild(TransactionID) if err := c.Do(m, NoopHandler()); err == nil { t.Error("error expected") } } func TestCallbackWaitHandler_setCallback(t *testing.T) { c := callbackWaitHandler{} defer func() { if err := recover(); err == nil { t.Error("should panic") } }() c.setCallback(nil) } func TestCallbackWaitHandler_HandleEvent(t *testing.T) { c := &callbackWaitHandler{ cond: sync.NewCond(new(sync.Mutex)), } defer func() { if err := recover(); err == nil { t.Error("should panic") } }() c.HandleEvent(Event{}) } func TestNewClientNoConnection(t *testing.T) { c, err := NewClient(nil) if c != nil { t.Error("c should be nil") } if !errors.Is(err, ErrNoConnection) { t.Error("bad error") } } func TestDial(t *testing.T) { c, err := Dial("udp4", "localhost:3458") if err != nil { t.Fatal(err) } defer func() { if err = c.Close(); err != nil { t.Error(err) } }() } func TestDialURI(t *testing.T) { u, err := ParseURI("stun:localhost") if err != nil { t.Fatal(err) } c, err := DialURI(u, &DialConfig{}) if err != nil { t.Fatal(err) } defer func() { if err = c.Close(); err != nil { t.Error(err) } }() } func TestDialError(t *testing.T) { _, err := Dial("bad?network", "?????") if err == nil { t.Fatal("error expected") } } func TestClientCloseErr(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() conn := &testConnection{ b: response.Raw, write: func(bytes []byte) (int, error) { return len(bytes), nil }, } c, err := NewClient(conn, WithAgent(errorAgent{ closeErr: io.ErrUnexpectedEOF, }), ) if err != nil { log.Fatal(err) } defer func() { if err, ok := c.Close().(CloseErr); !ok || !errors.Is(err.AgentErr, io.ErrUnexpectedEOF) { //nolint:errorlint t.Error("unexpected close err") } }() } func TestWithNoConnClose(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() closeErr := errClientCloseError conn := &testConnection{ b: response.Raw, close: func() error { return closeErr }, } c, err := NewClient(conn, WithAgent(errorAgent{ closeErr: nil, }), WithNoConnClose(), ) if err != nil { log.Fatal(err) } if err := c.Close(); err != nil { t.Error("unexpected non-nil error") } } type gcWaitAgent struct { gc chan struct{} } func (a *gcWaitAgent) SetHandler(Handler) error { return nil } func (a *gcWaitAgent) Stop([TransactionIDSize]byte) error { return nil } func (a *gcWaitAgent) Close() error { close(a.gc) return nil } func (a *gcWaitAgent) Collect(time.Time) error { a.gc <- struct{}{} return nil } func (a *gcWaitAgent) Process(*Message) error { return nil } func (a *gcWaitAgent) Start([TransactionIDSize]byte, time.Time) error { return nil } func TestClientGC(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() conn := &testConnection{ b: response.Raw, write: func(bytes []byte) (int, error) { return len(bytes), nil }, } agent := &gcWaitAgent{ gc: make(chan struct{}), } c, err := NewClient(conn, WithAgent(agent), WithTimeoutRate(time.Millisecond), ) if err != nil { log.Fatal(err) } defer func() { if err = c.Close(); err != nil { t.Error(err) } }() select { case <-agent.gc: case <-time.After(time.Millisecond * 200): t.Error("timed out") } } func TestClientCheckInit(t *testing.T) { if err := (&Client{}).Indicate(nil); !errors.Is(err, ErrClientNotInitialized) { t.Error("unexpected error") } if err := (&Client{}).Do(nil, nil); !errors.Is(err, ErrClientNotInitialized) { t.Error("unexpected error") } } func captureLog() (*bytes.Buffer, func()) { var buf bytes.Buffer log.SetOutput(&buf) f := log.Flags() log.SetFlags(0) return &buf, func() { log.SetFlags(f) log.SetOutput(os.Stderr) } } func TestClientFinalizer(t *testing.T) { buf, stopCapture := captureLog() defer stopCapture() clientFinalizer(nil) // should not panic clientFinalizer(&Client{}) conn := &testConnection{ write: func(bytes []byte) (int, error) { return 0, io.ErrClosedPipe }, } c, err := NewClient(conn) if err != nil { log.Panic(err) } clientFinalizer(c) clientFinalizer(c) response := MustBuild(TransactionID, BindingSuccess) response.Encode() conn = &testConnection{ b: response.Raw, write: func(bytes []byte) (int, error) { return len(bytes), nil }, } c, err = NewClient(conn, WithAgent(errorAgent{ closeErr: io.ErrUnexpectedEOF, }), ) if err != nil { log.Panic(err) } clientFinalizer(c) reader := bufio.NewScanner(buf) var lines int expectedLines := []string{ "client: called finalizer on non-closed client: client not initialized", "client: called finalizer on non-closed client", "client: called finalizer on non-closed client: failed to close: " + " (connection), unexpected EOF (agent)", } for reader.Scan() { if reader.Text() != expectedLines[lines] { t.Error(reader.Text(), "!=", expectedLines[lines]) } lines++ } if reader.Err() != nil { t.Error(err) } if lines != 3 { t.Error("incorrect count of log lines:", lines) } } func TestCallbackWaitHandler(*testing.T) { h := callbackWaitHandlerPool.Get().(*callbackWaitHandler) //nolint:forcetypeassert for i := 0; i < 100; i++ { h.setCallback(func(event Event) {}) go func() { time.Sleep(time.Microsecond * 100) h.HandleEvent(Event{}) }() h.wait() } } type manualCollector struct { f func(t time.Time) } func (m *manualCollector) Collect(t time.Time) { m.f(t) } func (m *manualCollector) Start(_ time.Duration, f func(t time.Time)) error { m.f = f return nil } func (m *manualCollector) Close() error { return nil } type manualClock struct { mux sync.Mutex current time.Time } func (m *manualClock) Add(d time.Duration) time.Time { m.mux.Lock() v := m.current.Add(d) m.current = v m.mux.Unlock() return v } func (m *manualClock) Now() time.Time { m.mux.Lock() defer m.mux.Unlock() return m.current } type manualAgent struct { start func(id [TransactionIDSize]byte, deadline time.Time) error stop func(id [TransactionIDSize]byte) error process func(m *Message) error h Handler } func (n *manualAgent) SetHandler(h Handler) error { n.h = h return nil } func (n *manualAgent) Close() error { return nil } func (manualAgent) Collect(time.Time) error { return nil } func (n *manualAgent) Process(m *Message) error { if n.process != nil { return n.process(m) } return nil } func (n *manualAgent) Start(id [TransactionIDSize]byte, deadline time.Time) error { return n.start(id, deadline) } func (n *manualAgent) Stop(id [TransactionIDSize]byte) error { if n.stop != nil { return n.stop(id) } return nil } func TestClientRetransmission(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() connL, connR := net.Pipe() defer func() { if closeErr := connL.Close(); closeErr != nil { panic(closeErr) } }() collector := new(manualCollector) clock := &manualClock{current: time.Now()} agent := &manualAgent{} attempt := 0 agent.start = func(id [TransactionIDSize]byte, deadline time.Time) error { if attempt == 0 { attempt++ go agent.h(Event{ TransactionID: id, Error: ErrTransactionTimeOut, }) } else { go agent.h(Event{ TransactionID: id, Message: response, }) } return nil } c, err := NewClient(connR, WithAgent(agent), WithClock(clock), WithCollector(collector), WithRTO(time.Millisecond), ) if err != nil { t.Fatal(err) } c.SetRTO(time.Second) gotReads := make(chan struct{}) go func() { buf := make([]byte, 1500) readN, readErr := connL.Read(buf) if readErr != nil { t.Error(readErr) } if !IsMessage(buf[:readN]) { t.Error("should be STUN") } readN, readErr = connL.Read(buf) if readErr != nil { t.Error(readErr) } if !IsMessage(buf[:readN]) { t.Error("should be STUN") } gotReads <- struct{}{} }() if doErr := c.Do(MustBuild(response, BindingRequest), func(event Event) { if event.Error != nil { t.Error("failed") } }); doErr != nil { t.Fatal(doErr) } <-gotReads } func testClientDoConcurrent(t *testing.T, concurrency int) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() connL, connR := net.Pipe() defer func() { if closeErr := connL.Close(); closeErr != nil { panic(closeErr) } }() collector := new(manualCollector) clock := &manualClock{current: time.Now()} agent := &manualAgent{} agent.start = func(id [TransactionIDSize]byte, deadline time.Time) error { go agent.h(Event{ TransactionID: id, Message: response, }) return nil } c, err := NewClient(connR, WithAgent(agent), WithClock(clock), WithCollector(collector), ) if err != nil { t.Fatal(err) } c.SetRTO(time.Second) conns := new(sync.WaitGroup) wg := new(sync.WaitGroup) for i := 0; i < concurrency; i++ { conns.Add(1) go func() { defer conns.Done() buf := make([]byte, 1500) for { readN, readErr := connL.Read(buf) if readErr != nil { if errors.Is(readErr, io.EOF) { break } t.Error(readErr) } if !IsMessage(buf[:readN]) { t.Error("should be STUN") } } }() wg.Add(1) go func() { defer wg.Done() if doErr := c.Do(MustBuild(TransactionID, BindingRequest), func(event Event) { if event.Error != nil { t.Error("failed") } }); doErr != nil { t.Error(doErr) } }() } wg.Wait() if connErr := connR.Close(); connErr != nil { t.Error(connErr) } conns.Wait() } func TestClient_DoConcurrent(t *testing.T) { for _, concurrency := range []int{ 1, 5, 10, 25, 100, 500, } { concurrency := concurrency t.Run(fmt.Sprintf("%d", concurrency), func(t *testing.T) { testClientDoConcurrent(t, concurrency) }) } } type errorCollector struct { startError error closeError error } func (c errorCollector) Start(time.Duration, func(now time.Time)) error { return c.startError } func (c errorCollector) Close() error { return c.closeError } func TestNewClient(t *testing.T) { t.Run("SetCallbackError", func(t *testing.T) { setHandlerError := errClientSetHandler if _, createErr := NewClient(noopConnection{}, WithAgent(&errorAgent{ setHandlerError: setHandlerError, }), ); !errors.Is(createErr, setHandlerError) { t.Errorf("unexpected error returned: %v", createErr) } }) t.Run("CollectorStartError", func(t *testing.T) { startError := errClientStart if _, createErr := NewClient(noopConnection{}, WithAgent(&TestAgent{}), WithCollector(errorCollector{ startError: startError, }), ); !errors.Is(createErr, startError) { t.Errorf("unexpected error returned: %v", createErr) } }) } func TestClient_Close(t *testing.T) { t.Run("CollectorCloseError", func(t *testing.T) { closeErr := errClientStart c, createErr := NewClient(noopConnection{}, WithCollector(errorCollector{ closeError: closeErr, }), WithAgent(&TestAgent{}), ) if createErr != nil { t.Errorf("unexpected create error returned: %v", createErr) } gotCloseErr := c.Close() if !errors.Is(gotCloseErr, closeErr) { t.Errorf("unexpected close error returned: %v", gotCloseErr) } }) } func TestClientDefaultHandler(t *testing.T) { a := &TestAgent{ e: make(chan Event), } id := NewTransactionID() handlerCalled := make(chan struct{}) called := false c, createErr := NewClient(noopConnection{}, WithAgent(a), WithHandler(func(e Event) { if called { t.Error("should not be called twice") } called = true if e.TransactionID != id { t.Error("wrong transaction ID") } handlerCalled <- struct{}{} }), ) if createErr != nil { t.Fatal(createErr) } go func() { a.h(Event{ TransactionID: id, }) }() select { case <-handlerCalled: // pass case <-time.After(time.Millisecond * 100): t.Fatal("timed out") } if closeErr := c.Close(); closeErr != nil { t.Error(closeErr) } // Handler call should be ignored. a.h(Event{}) } func TestClientClosedStart(t *testing.T) { a := &TestAgent{ e: make(chan Event), } c, createErr := NewClient(noopConnection{}, WithAgent(a), ) if createErr != nil { t.Fatal(createErr) } if closeErr := c.Close(); closeErr != nil { t.Error(closeErr) } if startErr := c.start(&clientTransaction{}); !errors.Is(startErr, ErrClientClosed) { t.Error("should error") } } func TestWithNoRetransmit(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() connL, connR := net.Pipe() defer func() { if closeErr := connL.Close(); closeErr != nil { panic(closeErr) } }() collector := new(manualCollector) clock := &manualClock{current: time.Now()} agent := &manualAgent{} attempt := 0 agent.start = func(id [TransactionIDSize]byte, deadline time.Time) error { if attempt == 0 { attempt++ go agent.h(Event{ TransactionID: id, Error: ErrTransactionTimeOut, }) } else { t.Error("there should be no second attempt") go agent.h(Event{ TransactionID: id, Error: ErrTransactionTimeOut, }) } return nil } c, err := NewClient(connR, WithAgent(agent), WithClock(clock), WithCollector(collector), WithRTO(0), WithNoRetransmit, ) if err != nil { t.Fatal(err) } gotReads := make(chan struct{}) go func() { buf := make([]byte, 1500) readN, readErr := connL.Read(buf) if readErr != nil { t.Error(readErr) } if !IsMessage(buf[:readN]) { t.Error("should be STUN") } gotReads <- struct{}{} }() if doErr := c.Do(MustBuild(response, BindingRequest), func(event Event) { if !errors.Is(event.Error, ErrTransactionTimeOut) { t.Error("unexpected error") } }); doErr != nil { t.Fatal(err) } <-gotReads } type callbackClock func() time.Time func (c callbackClock) Now() time.Time { return c() } func TestClientRTOStartErr(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() connL, connR := net.Pipe() defer func() { if closeErr := connL.Close(); closeErr != nil { panic(closeErr) } }() collector := new(manualCollector) shouldWait := false shouldWaitMux := new(sync.RWMutex) clockWait := make(chan struct{}) clockLocked := make(chan struct{}) clock := callbackClock(func() time.Time { shouldWaitMux.RLock() waiting := shouldWait t.Log("waiting:", waiting) time.Sleep(time.Millisecond * 100) shouldWaitMux.RUnlock() if waiting { t.Log("clock waiting for log ack") clockLocked <- struct{}{} t.Log("clock waiting for unlock") <-clockWait t.Log("clock returned after waiting") } else { t.Log("clock returned") } return time.Now() }) agent := &manualAgent{} attempt := 0 gotReads := make(chan struct{}) var ( c *Client startClientErr error ) agent.start = func(id [TransactionIDSize]byte, deadline time.Time) error { t.Log("start", attempt) if attempt == 0 { attempt++ go agent.h(Event{ TransactionID: id, Error: ErrTransactionTimeOut, }) } else { go func() { <-gotReads shouldWaitMux.Lock() shouldWait = true shouldWaitMux.Unlock() go agent.h(Event{ TransactionID: id, Error: ErrTransactionTimeOut, }) t.Log("clock locked") <-clockLocked t.Log("closing client") if closeErr := c.Close(); closeErr != nil { t.Error(closeErr) } t.Log("client closed, unlocking clock") clockWait <- struct{}{} t.Log("clock unlocked") }() } return nil } c, startClientErr = NewClient(connR, WithAgent(agent), WithClock(clock), WithCollector(collector), WithRTO(time.Millisecond), ) if startClientErr != nil { t.Fatal(startClientErr) } go func() { buf := make([]byte, 1500) readN, readErr := connL.Read(buf) if readErr != nil { t.Error(readErr) } if !IsMessage(buf[:readN]) { t.Error("should be STUN") } readN, readErr = connL.Read(buf) if readErr != nil { t.Error(readErr) } if !IsMessage(buf[:readN]) { t.Error("should be STUN") } gotReads <- struct{}{} }() t.Log("starting") done := make(chan struct{}) go func() { if doErr := c.Do(MustBuild(response, BindingRequest), func(event Event) { if !errors.Is(event.Error, ErrClientClosed) { t.Error(event.Error) } }); doErr != nil { t.Error(doErr) } done <- struct{}{} }() select { case <-done: // ok case <-time.After(time.Second * 5): t.Error("timeout") } } func TestClientRTOWriteErr(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() connL, connR := net.Pipe() defer func() { if closeErr := connL.Close(); closeErr != nil { panic(closeErr) } }() collector := new(manualCollector) shouldWait := false shouldWaitMux := new(sync.RWMutex) clockWait := make(chan struct{}) clockLocked := make(chan struct{}) clock := callbackClock(func() time.Time { shouldWaitMux.RLock() waiting := shouldWait t.Log("waiting:", waiting) time.Sleep(time.Millisecond * 100) shouldWaitMux.RUnlock() if waiting { t.Log("clock waiting for log ack") clockLocked <- struct{}{} t.Log("clock waiting for unlock") <-clockWait t.Log("clock returned after waiting") } else { t.Log("clock returned") } return time.Now() }) agent := &manualAgent{} attempt := 0 gotReads := make(chan struct{}) var ( c *Client startClientErr error ) agentStopErr := errClientAgentCantStop agent.stop = func(id [TransactionIDSize]byte) error { return agentStopErr } agent.start = func(id [TransactionIDSize]byte, deadline time.Time) error { t.Log("start", attempt) if attempt == 0 { attempt++ go agent.h(Event{ TransactionID: id, Error: ErrTransactionTimeOut, }) } else { go func() { <-gotReads shouldWaitMux.Lock() shouldWait = true shouldWaitMux.Unlock() go agent.h(Event{ TransactionID: id, Error: ErrTransactionTimeOut, }) t.Log("clock locked") <-clockLocked t.Log("closing connection") if closeErr := connL.Close(); closeErr != nil { panic(closeErr) } t.Log("connection closed, unlocking clock") clockWait <- struct{}{} t.Log("clock unlocked") }() } return nil } c, startClientErr = NewClient(connR, WithAgent(agent), WithClock(clock), WithCollector(collector), WithRTO(time.Millisecond), ) if startClientErr != nil { t.Fatal(startClientErr) } go func() { buf := make([]byte, 1500) readN, readErr := connL.Read(buf) if readErr != nil { t.Error(readErr) } if !IsMessage(buf[:readN]) { t.Error("should be STUN") } readN, readErr = connL.Read(buf) if readErr != nil { t.Error(readErr) } if !IsMessage(buf[:readN]) { t.Error("should be STUN") } gotReads <- struct{}{} }() t.Log("starting") done := make(chan struct{}) go func() { if doErr := c.Do(MustBuild(response, BindingRequest), func(event Event) { var e StopErr if !errors.As(event.Error, &e) { t.Error(event.Error) } else { if !errors.Is(e.Err, agentStopErr) { t.Error("incorrect agent error") } if !errors.Is(e.Cause, io.ErrClosedPipe) { t.Error("incorrect connection error") } } }); doErr != nil { t.Error(doErr) } done <- struct{}{} }() select { case <-done: // ok case <-time.After(time.Second * 5): t.Error("timeout") } } func TestClientRTOAgentErr(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() connL, connR := net.Pipe() defer func() { if closeErr := connL.Close(); closeErr != nil { panic(closeErr) } }() collector := new(manualCollector) clock := callbackClock(time.Now) agent := &manualAgent{} attempt := 0 gotReads := make(chan struct{}) var ( c *Client startClientErr error ) agentStartErr := errClientStartRefused agent.start = func(id [TransactionIDSize]byte, deadline time.Time) error { t.Log("start", attempt) if attempt == 0 { attempt++ go agent.h(Event{ TransactionID: id, Error: ErrTransactionTimeOut, }) } else { return agentStartErr } return nil } c, startClientErr = NewClient(connR, WithAgent(agent), WithClock(clock), WithCollector(collector), WithRTO(time.Millisecond), ) if startClientErr != nil { t.Fatal(startClientErr) } go func() { buf := make([]byte, 1500) readN, readErr := connL.Read(buf) if readErr != nil { t.Error(readErr) } if !IsMessage(buf[:readN]) { t.Error("should be STUN") } gotReads <- struct{}{} }() t.Log("starting") if doErr := c.Do(MustBuild(response, BindingRequest), func(event Event) { if !errors.Is(event.Error, agentStartErr) { t.Error(event.Error) } }); doErr != nil { t.Error(doErr) } select { case <-gotReads: // ok case <-time.After(time.Second * 5): t.Error("reads timeout") } } func TestClient_HandleProcessError(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) response.Encode() connL, connR := net.Pipe() defer func() { if closeErr := connL.Close(); closeErr != nil { panic(closeErr) } }() collector := new(manualCollector) clock := callbackClock(time.Now) agent := &manualAgent{} gotWrites := make(chan struct{}) processCalled := make(chan struct{}, 1) agent.process = func(m *Message) error { processCalled <- struct{}{} return ErrAgentClosed } c, startClientErr := NewClient(connR, WithAgent(agent), WithClock(clock), WithCollector(collector), WithRTO(time.Millisecond), ) if startClientErr != nil { t.Fatal(startClientErr) } go func() { _, readErr := connL.Write(response.Raw) if readErr != nil { t.Error(readErr) } gotWrites <- struct{}{} }() t.Log("starting") select { case <-gotWrites: // ok case <-time.After(time.Second * 5): t.Error("reads timeout") } if closeErr := c.Close(); closeErr != nil { t.Error(closeErr) } } func TestClientImmediateTimeout(t *testing.T) { response := MustBuild(TransactionID, BindingSuccess) connL, connR := net.Pipe() defer func() { if closeErr := connL.Close(); closeErr != nil { panic(closeErr) } }() collector := new(manualCollector) clock := &manualClock{current: time.Now()} rto := time.Second * 1 agent := &manualAgent{} attempt := 0 agent.start = func(id [TransactionIDSize]byte, deadline time.Time) error { if attempt == 0 { if deadline.Before(clock.current.Add(rto / 2)) { t.Error("deadline too fast") } attempt++ go agent.h(Event{ TransactionID: id, Message: response, }) } else { t.Error("there should be no second attempt") go agent.h(Event{ TransactionID: id, Error: ErrTransactionTimeOut, }) } return nil } c, err := NewClient(connR, WithAgent(agent), WithClock(clock), WithCollector(collector), WithRTO(rto), ) if err != nil { t.Fatal(err) } gotReads := make(chan struct{}) go func() { buf := make([]byte, 1500) readN, readErr := connL.Read(buf) if readErr != nil { t.Error(readErr) } if !IsMessage(buf[:readN]) { t.Error("should be STUN") } gotReads <- struct{}{} }() c.Start(MustBuild(response, BindingRequest), func(e Event) { //nolint:errcheck,gosec if errors.Is(e.Error, ErrTransactionTimeOut) { t.Error("unexpected error") } }) <-gotReads } stun-0.6.1/cmd/000077500000000000000000000000001444552150000132525ustar00rootroot00000000000000stun-0.6.1/cmd/stun-bench/000077500000000000000000000000001444552150000153205ustar00rootroot00000000000000stun-0.6.1/cmd/stun-bench/main.go000066400000000000000000000077351444552150000166070ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements benchmarks for the STUN package package main import ( "context" "crypto/rand" "errors" "flag" "log" mathRand "math/rand" "os" "os/signal" "runtime" "runtime/pprof" "sync/atomic" "time" "github.com/pion/stun" ) var ( workers = flag.Int("w", runtime.GOMAXPROCS(0), "concurrent workers") //nolint:gochecknoglobals uriStr = flag.String("uri", "stun:localhost:3478", "URI of STUN server") //nolint:gochecknoglobals duration = flag.Duration("d", time.Minute, "benchmark duration") //nolint:gochecknoglobals cpuProfile = flag.String("cpuprofile", "", "file output of pprof cpu profile") //nolint:gochecknoglobals memProfile = flag.String("memprofile", "", "file output of pprof memory profile") //nolint:gochecknoglobals realRand = flag.Bool("crypt", false, "use crypto/rand as random source") //nolint:gochecknoglobals ) func main() { //nolint:gocognit flag.Parse() uri, err := stun.ParseURI(*uriStr) if err != nil { log.Fatalf("Failed to parse URI '%s': %s", *uriStr, err) } signals := make(chan os.Signal, 1) signal.Notify(signals, os.Interrupt) start := time.Now() var ( request int64 requestOK int64 requestErr int64 ) if *cpuProfile != "" { f, createErr := os.Create(*cpuProfile) if createErr != nil { log.Fatalf("Failed to create CPU profile output file: %s", createErr) } if pprofErr := pprof.StartCPUProfile(f); pprofErr != nil { log.Fatalf("Failed to start pprof CPU profiling: %s", pprofErr) } defer func() { pprof.StopCPUProfile() if closeErr := f.Close(); closeErr != nil { log.Printf("Failed to close CPU profile output file: %s", closeErr) } else { log.Printf("Saved cpu profile to: %s", *cpuProfile) } }() } if *memProfile != "" { f, createErr := os.Create(*memProfile) if createErr != nil { log.Panicf("Failed to create memory profile output file: %s", createErr) } defer func() { if pprofErr := pprof.Lookup("heap").WriteTo(f, 1); pprofErr != nil { log.Fatalf("Failed to write pprof memory profiling: %s", pprofErr) } if closeErr := f.Close(); closeErr != nil { log.Printf("Failed to close memory profile output file: %s", closeErr) } else { log.Printf("Saved memory profile to %s", *memProfile) } }() } ctx, cancel := context.WithTimeout(context.Background(), *duration) go func() { for sig := range signals { log.Printf("Stopping on %s", sig) cancel() } }() if *realRand { log.Print("Using crypto/rand as random source for transaction id") } for i := 0; i < *workers; i++ { c, clientErr := stun.DialURI(uri, &stun.DialConfig{}) if clientErr != nil { log.Panicf("Failed to create client: %s", clientErr) } go func() { req := stun.New() for { if *realRand { if _, err := rand.Read(req.TransactionID[:]); err != nil { //nolint:gosec log.Fatalf("Failed to generate transaction ID: %s", err) } } else { mathRand.Read(req.TransactionID[:]) //nolint:gosec } req.Type = stun.BindingRequest req.WriteHeader() atomic.AddInt64(&request, 1) if doErr := c.Do(req, func(event stun.Event) { if event.Error != nil { if !errors.Is(event.Error, stun.ErrTransactionTimeOut) { log.Printf("Failed STUN transaction: %s", event.Error) } atomic.AddInt64(&requestErr, 1) return } atomic.AddInt64(&requestOK, 1) }); doErr != nil { if !errors.Is(doErr, stun.ErrTransactionExists) { log.Printf("Failed STUN transaction: %s", doErr) } atomic.AddInt64(&requestErr, 1) } } }() } log.Print("Workers started") <-ctx.Done() stop := time.Now() rps := int(float64(atomic.LoadInt64(&requestOK)) / stop.Sub(start).Seconds()) log.Printf("RPS: %v", rps) if reqErr := atomic.LoadInt64(&requestErr); requestErr != 0 { log.Printf("Errors: %d", reqErr) } log.Printf("Total: %d", atomic.LoadInt64(&request)) } stun-0.6.1/cmd/stun-client/000077500000000000000000000000001444552150000155175ustar00rootroot00000000000000stun-0.6.1/cmd/stun-client/stun_client.go000066400000000000000000000024211444552150000203740ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a CLI tool which acts as a STUN client package main import ( "flag" "fmt" "log" "os" "github.com/pion/stun" ) func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", os.Args[0]) fmt.Fprintln(os.Stderr, os.Args[0], "stun:stun.l.google.com:19302") } flag.Parse() uriStr := flag.Arg(0) if uriStr == "" { uriStr = "stun:stun.l.google.com:19302" } uri, err := stun.ParseURI(uriStr) if err != nil { log.Fatalf("Invalid URI '%s': %s", uriStr, err) } // we only try the first address, so restrict ourselves to IPv4 c, err := stun.DialURI(uri, &stun.DialConfig{}) if err != nil { log.Fatalf("Failed to dial: %s", err) } if err = c.Do(stun.MustBuild(stun.TransactionID, stun.BindingRequest), func(res stun.Event) { if res.Error != nil { log.Fatalf("Failed STUN transaction: %s", res.Error) } var xorAddr stun.XORMappedAddress if getErr := xorAddr.GetFrom(res.Message); getErr != nil { log.Fatalf("Failed to get XOR-MAPPED-ADDRESS: %s", getErr) } log.Print(xorAddr) }); err != nil { log.Fatal("Do:", err) } if err := c.Close(); err != nil { log.Fatalf("Failed to close connection: %s", err) } } stun-0.6.1/cmd/stun-decode/000077500000000000000000000000001444552150000154645ustar00rootroot00000000000000stun-0.6.1/cmd/stun-decode/main.go000066400000000000000000000015501444552150000167400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a simple CLI tool to decode STUN messages package main import ( "encoding/base64" "flag" "fmt" "log" "os" "github.com/pion/stun" ) func main() { flag.Usage = func() { fmt.Fprintf(os.Stderr, "Usage of %s:\n", "stun-decode") fmt.Fprintln(os.Stderr, "stun-decode AAEAHCESpEJML0JTQWsyVXkwcmGALwAWaHR0cDovL2xvY2FsaG9zdDozMDAwLwAA") fmt.Fprintln(os.Stderr, "First argument must be a base64.StdEncoding-encoded message") flag.PrintDefaults() } flag.Parse() data, err := base64.StdEncoding.DecodeString(flag.Arg(0)) if err != nil { log.Fatalln("Unable to decode bas64 value:", err) } m := new(stun.Message) m.Raw = data if err = m.Decode(); err != nil { log.Fatalln("Unable to decode message:", err) } fmt.Println(m) } stun-0.6.1/cmd/stun-multiplex/000077500000000000000000000000001444552150000162645ustar00rootroot00000000000000stun-0.6.1/cmd/stun-multiplex/README.md000066400000000000000000000015571444552150000175530ustar00rootroot00000000000000# Multiplex An example of doing UDP connection multiplexing that splits incoming UDP packets to two streams, "STUN Data" and "Application Data". Usage: ```sh $ go install github.com/pion/stun/cmd/stun-multiplex@latest ``` On "server": ```sh $ stun-multiplex local addr: 0.0.0.0:34690 stun server addr: 64.233.161.127:19302 public addr: 123.131.100.200:34690 Acting as server. Use following command to connect: stun-multiplex 123.131.100.200:34690 ``` On "client": ```sh $ stun-multiplex 123.131.100.200:34690 local addr: 0.0.0.0:37551 stun server addr: 66.102.1.127:19302 public addr: 159.69.13.15:37551 Acting as client. Connecting to 123.131.100.200:34690 Writing 123.131.100.200:34690 demultiplex: [123.131.100.200:34690]: Hello peer Got response from 123.131.100.200:34690: Hello peer ``` On "server" you will see `demultiplex: [159.69.13.15:37551]: Hello peer` message.stun-0.6.1/cmd/stun-multiplex/main.go000066400000000000000000000126461444552150000175500ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Command stun-multiplex is example of doing UDP connection multiplexing // that splits incoming UDP packets to two streams, "STUN Data" and // "Application Data". package main import ( "flag" "io" "log" "net" "os" "os/signal" "syscall" "time" "github.com/pion/stun" ) func copyAddr(dst *stun.XORMappedAddress, src stun.XORMappedAddress) { dst.IP = append(dst.IP, src.IP...) dst.Port = src.Port } func keepAlive(c *stun.Client) { // Keep-alive for NAT binding. t := time.NewTicker(time.Second * 5) for range t.C { if err := c.Do(stun.MustBuild(stun.TransactionID, stun.BindingRequest), func(res stun.Event) { if res.Error != nil { log.Panicf("Failed STUN transaction: %s", res.Error) } }); err != nil { log.Panicf("Failed STUN transaction: %s", err) } } } type message struct { text string addr net.Addr } func demultiplex(conn *net.UDPConn, stunConn io.Writer, messages chan message) { buf := make([]byte, 1024) for { n, raddr, err := conn.ReadFrom(buf) if err != nil { log.Panicf("Failed to read: %s", err) } // De-multiplexing incoming packets. if stun.IsMessage(buf[:n]) { // If buf looks like STUN message, send it to STUN client connection. if _, err = stunConn.Write(buf[:n]); err != nil { log.Panicf("Failed to write: %s", err) } } else { // If not, it is application data. log.Printf("Demultiplex: [%s]: %s", raddr, buf[:n]) messages <- message{ text: string(buf[:n]), addr: raddr, } } } } func multiplex(conn *net.UDPConn, stunAddr net.Addr, stunConn io.Reader) { // Sending all data from stun client to stun server. buf := make([]byte, 1024) for { n, err := stunConn.Read(buf) if err != nil { log.Panicf("Failed to read: %s", err) } if _, err = conn.WriteTo(buf[:n], stunAddr); err != nil { log.Panicf("Failed to write: %s", err) } } } var stunServer = flag.String("stun", "stun.l.google.com:19302", "STUN Server to use") //nolint:gochecknoglobals func main() { isServer := flag.Arg(0) == "" flag.Parse() // Allocating local UDP socket that will be used both for STUN and // our application data. addr, err := net.ResolveUDPAddr("udp4", "0.0.0.0:0") if err != nil { log.Panicf("Failed to resolve: %s", err) } conn, err := net.ListenUDP("udp4", addr) if err != nil { log.Panicf("Failed to listen: %s", err) } // Resolving STUN server address. stunAddr, err := net.ResolveUDPAddr("udp4", *stunServer) if err != nil { log.Panicf("Failed to resolve '%s': %s", *stunServer, err) } log.Printf("Local address: %s", conn.LocalAddr()) log.Printf("STUN server address: %s", stunAddr) stunL, stunR := net.Pipe() c, err := stun.NewClient(stunR) if err != nil { log.Panicf("Failed to create client: %s", err) } // Starting multiplexing (writing back STUN messages) with de-multiplexing // (passing STUN messages to STUN client and processing application // data separately). // // stunL and stunR are virtual connections, see net.Pipe for reference. messages := make(chan message) go demultiplex(conn, stunL, messages) go multiplex(conn, stunAddr, stunL) // Getting our "real" IP address from STUN Server. // This will create a NAT binding on your provider/router NAT Server, // and the STUN server will return allocated public IP for that binding. // // This can fail if your NAT Server is strict and will use separate ports // for application data and STUN var gotAddr stun.XORMappedAddress if err = c.Do(stun.MustBuild(stun.TransactionID, stun.BindingRequest), func(res stun.Event) { if res.Error != nil { log.Panicf("Failed STUN transaction: %s", res.Error) } var xorAddr stun.XORMappedAddress if getErr := xorAddr.GetFrom(res.Message); getErr != nil { log.Panicf("Failed to get XOR-MAPPED-ADDRESS: %s", getErr) } copyAddr(&gotAddr, xorAddr) }); err != nil { log.Panicf("Failed STUN transaction: %s", err) } log.Printf("Public address: %s", gotAddr) // Keep-alive is needed to keep our NAT port allocated. // Any ping-pong will work, but we are just making binding requests. // Note that STUN Server is not mandatory for keep alive, application // data will keep alive that binding too. go keepAlive(c) notify := make(chan os.Signal, 1) signal.Notify(notify, os.Interrupt, syscall.SIGTERM) if isServer { log.Printf("Acting as server. Use following command to connect: %s %s", os.Args[0], gotAddr) for { select { case m := <-messages: if _, err = conn.WriteTo([]byte(m.text), m.addr); err != nil { log.Panicf("Failed to write: %s", err) } case <-notify: log.Println("Stopping") return } } } else { peerAddr, err := net.ResolveUDPAddr("udp4", flag.Arg(0)) if err != nil { log.Panicf("Failed to resolve '%s': %s", flag.Arg(0), err) } log.Printf("Acting as client. Connecting to %s", peerAddr) msg := "Hello peer" sendMsg := func() { log.Printf("Writing to: %s", peerAddr) if _, err = conn.WriteTo([]byte(msg), peerAddr); err != nil { log.Panicf("Failed to write: %s", err) } } sendMsg() deadline := time.After(time.Second * 10) for { select { case <-deadline: log.Fatal("Failed to connect: deadline reached.") case <-time.After(time.Second): // Retry. sendMsg() case m := <-messages: log.Printf("Got response from %s: %s", m.addr, m.text) return case <-notify: log.Print("Stopping") return } } } } stun-0.6.1/cmd/stun-nat-behaviour/000077500000000000000000000000001444552150000170055ustar00rootroot00000000000000stun-0.6.1/cmd/stun-nat-behaviour/README.md000066400000000000000000000113571444552150000202730ustar00rootroot00000000000000# NAT behaviour discovery using STUN ([RFC 5780](https://tools.ietf.org/html/rfc5780)) This is an example of how to use the pion/stun package for client-side NAT behaviour discovery. It performs two types of tests: one to determine the client's NAT mapping behaviour, and one to determine the NAT filtering behaviour. ### Usage ```sh $ go install github.com/pion/stun/cmd/stun-nat-behaviour@latest $ $GOPATH/bin/stun-nat-behaviour [options] [--server IP:port] ``` If `$GOPATH` is unset it defaults to `~/go` The default value `--server` is stun.voipgate.com:3478 Use `-h` to see all options ### Output For a successful run you will see output like the following. ``` connecting to STUN server: stun.voipgate.com:3478 ... Mapping Test I: Regular binding request Received XOR-MAPPED-ADDRESS: ...:... ... Mapping Test II: Send binding request to the other address but primary port ... => NAT mapping behavior: endpoint independent connecting to STUN server: stun.voipgate.com:3478 ... Filtering Test I: Regular binding request ... Filtering Test II: Request to change both IP and port ... Filtering Test III: Request to change port only ... => NAT filtering behavior: address and port dependent ``` These tests are defined in [RFC 5780 section 4](https://tools.ietf.org/html/rfc5780#section-4) and the asserted behaviours of NAT are defined in [RFC 4787](https://tools.ietf.org/html/rfc4787). #### `XOR-MAPPED-ADDRESS` This is how the STUN server sees the request. The IP/Port tuple is the hole punched in your NAT, and how other client sees the request. #### `NAT mapping behaviour` ([RFC 4787 section 4](https://tools.ietf.org/html/rfc4787#section-4)) For each request your NAT will create a temporary mapping (hole). These are the different rules for creating and maintaining it. * **`endpoint independent`** If you send two UDP packets from the same port to different places on the other side of the NAT, the NAT will use the same port to send those packets. This is the only RTC-friendly mapping behavior, and RFC 4787 requires it as a best practice (REQ-1) on all NATs. If both NATs do this, ICE will be able to connect directly, for all of the filtering behaviors described below. This is the most important setting to get right. This mapping type corresponds to the "cone NATs" in the classic STUN defined in [RFC 3489 section 5](https://tools.ietf.org/html/rfc3489#section-5). * **`address dependent`** and **`address and port dependent`** If you send two UDP packets from the same port to different places on the other side of the NAT, the NAT will use the same port to send those packets ff the destination address is the same; the destination port does not matter. Sending to two different hosts will use different ports. This will require the use of a TURN server if both sides are using it, unless neither side is doing port-specific filtering, and at least one is doing `endpoint independent filtering`. If you want webrtc or anything else that uses P2P UDP networking, ***do not configure your NAT like this***. This mapping type (loosely) corresponds to the "symmetric NATs" in the classic STUN defined in [RFC 3489 section 5](https://tools.ietf.org/html/rfc3489#section-5). #### `NAT filtering behavior` ([RFC 4787 section 5](https://tools.ietf.org/html/rfc4787#section-5)) Each hole punch will also have rules around what external traffic is accepted (and routed back to the hole creator). * **`endpoint independent`** This is the most permissive of the three. Once you have sent a UDP packet to anywhere on the other side of the NAT, anything else on the other side of the NAT can send UDP packets back to you on that port. This filtering policy gives sysadmins cold sweats, but RFC 4787 recommends its use when real-time-communication (or other things that require "application transparency"; eg gaming) is a priority. Note that this will not do a very good job of compensating if your NAT's mapping behavior is misconfigured. It is more important to get the mapping behavior right. * **`address dependent`** This is a middle ground that sysadmins have an easier time justifying, but my impression is that it is harder to configure. Once you have sent a UDP packet to a host on the other side of the NAT, the NAT will allow return UDP traffic from that host, regardless of the port that host sends from, but will not allow inbound UDP traffic from other addresses. If your mapping behavior is configured appropriately, this should function as well as `endpoint independent filtering`. * **`address and port dependent`** This is the strictest of the three. Your NAT will only allow return traffic from exactly where you sent your UDP packet. Using this is ***not recommended***, even if you configure mapping behavior correctly, because it will work poorly when the other NAT is misconfigured (fairly common). stun-0.6.1/cmd/stun-nat-behaviour/main.go000066400000000000000000000226001444552150000202600ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // This package implements RFC5780's tests: // - 4.3. Determining NAT Mapping Behavior // - 4.4. Determining NAT Filtering Behavior package main import ( "errors" "flag" "net" "os" "time" "github.com/pion/logging" "github.com/pion/stun" ) type stunServerConn struct { conn net.PacketConn LocalAddr net.Addr RemoteAddr *net.UDPAddr OtherAddr *net.UDPAddr messageChan chan *stun.Message } func (c *stunServerConn) Close() error { return c.conn.Close() } var ( addrStrPtr = flag.String("server", "stun.voipgate.com:3478", "STUN server address") //nolint:gochecknoglobals timeoutPtr = flag.Int("timeout", 3, "the number of seconds to wait for STUN server's response") //nolint:gochecknoglobals verbose = flag.Int("verbose", 1, "the verbosity level") //nolint:gochecknoglobals log logging.LeveledLogger //nolint:gochecknoglobals ) const ( messageHeaderSize = 20 ) var ( errResponseMessage = errors.New("error reading from response message channel") errTimedOut = errors.New("timed out waiting for response") errNoOtherAddress = errors.New("no OTHER-ADDRESS in message") ) func main() { flag.Parse() var logLevel logging.LogLevel switch *verbose { case 0: logLevel = logging.LogLevelWarn case 1: logLevel = logging.LogLevelInfo // default case 2: logLevel = logging.LogLevelDebug case 3: logLevel = logging.LogLevelTrace } log = logging.NewDefaultLeveledLoggerForScope("", logLevel, os.Stdout) if err := mappingTests(*addrStrPtr); err != nil { log.Warn("NAT mapping behavior: inconclusive") } if err := filteringTests(*addrStrPtr); err != nil { log.Warn("NAT filtering behavior: inconclusive") } } // RFC5780: 4.3. Determining NAT Mapping Behavior func mappingTests(addrStr string) error { mapTestConn, err := connect(addrStr) if err != nil { log.Warnf("Error creating STUN connection: %s", err) return err } // Test I: Regular binding request log.Info("Mapping Test I: Regular binding request") request := stun.MustBuild(stun.TransactionID, stun.BindingRequest) resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr) if err != nil { return err } // Parse response message for XOR-MAPPED-ADDRESS and make sure OTHER-ADDRESS valid resps1 := parse(resp) if resps1.xorAddr == nil || resps1.otherAddr == nil { log.Info("Error: NAT discovery feature not supported by this server") return errNoOtherAddress } addr, err := net.ResolveUDPAddr("udp4", resps1.otherAddr.String()) if err != nil { log.Infof("Failed resolving OTHER-ADDRESS: %v", resps1.otherAddr) return err } mapTestConn.OtherAddr = addr log.Infof("Received XOR-MAPPED-ADDRESS: %v", resps1.xorAddr) // Assert mapping behavior if resps1.xorAddr.String() == mapTestConn.LocalAddr.String() { log.Warn("=> NAT mapping behavior: endpoint independent (no NAT)") return nil } // Test II: Send binding request to the other address but primary port log.Info("Mapping Test II: Send binding request to the other address but primary port") oaddr := *mapTestConn.OtherAddr oaddr.Port = mapTestConn.RemoteAddr.Port resp, err = mapTestConn.roundTrip(request, &oaddr) if err != nil { return err } // Assert mapping behavior resps2 := parse(resp) log.Infof("Received XOR-MAPPED-ADDRESS: %v", resps2.xorAddr) if resps2.xorAddr.String() == resps1.xorAddr.String() { log.Warn("=> NAT mapping behavior: endpoint independent") return nil } // Test III: Send binding request to the other address and port log.Info("Mapping Test III: Send binding request to the other address and port") resp, err = mapTestConn.roundTrip(request, mapTestConn.OtherAddr) if err != nil { return err } // Assert mapping behavior resps3 := parse(resp) log.Infof("Received XOR-MAPPED-ADDRESS: %v", resps3.xorAddr) if resps3.xorAddr.String() == resps2.xorAddr.String() { log.Warn("=> NAT mapping behavior: address dependent") } else { log.Warn("=> NAT mapping behavior: address and port dependent") } return mapTestConn.Close() } // RFC5780: 4.4. Determining NAT Filtering Behavior func filteringTests(addrStr string) error { mapTestConn, err := connect(addrStr) if err != nil { log.Warnf("Error creating STUN connection: %s", err) return err } // Test I: Regular binding request log.Info("Filtering Test I: Regular binding request") request := stun.MustBuild(stun.TransactionID, stun.BindingRequest) resp, err := mapTestConn.roundTrip(request, mapTestConn.RemoteAddr) if err != nil || errors.Is(err, errTimedOut) { return err } resps := parse(resp) if resps.xorAddr == nil || resps.otherAddr == nil { log.Warn("Error: NAT discovery feature not supported by this server") return errNoOtherAddress } addr, err := net.ResolveUDPAddr("udp4", resps.otherAddr.String()) if err != nil { log.Infof("Failed resolving OTHER-ADDRESS: %v", resps.otherAddr) return err } mapTestConn.OtherAddr = addr // Test II: Request to change both IP and port log.Info("Filtering Test II: Request to change both IP and port") request = stun.MustBuild(stun.TransactionID, stun.BindingRequest) request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x06}) resp, err = mapTestConn.roundTrip(request, mapTestConn.RemoteAddr) if err == nil { parse(resp) // just to print out the resp log.Warn("=> NAT filtering behavior: endpoint independent") return nil } else if !errors.Is(err, errTimedOut) { return err // something else went wrong } // Test III: Request to change port only log.Info("Filtering Test III: Request to change port only") request = stun.MustBuild(stun.TransactionID, stun.BindingRequest) request.Add(stun.AttrChangeRequest, []byte{0x00, 0x00, 0x00, 0x02}) resp, err = mapTestConn.roundTrip(request, mapTestConn.RemoteAddr) if err == nil { parse(resp) // just to print out the resp log.Warn("=> NAT filtering behavior: address dependent") } else if errors.Is(err, errTimedOut) { log.Warn("=> NAT filtering behavior: address and port dependent") } return mapTestConn.Close() } // Parse a STUN message func parse(msg *stun.Message) (ret struct { xorAddr *stun.XORMappedAddress otherAddr *stun.OtherAddress respOrigin *stun.ResponseOrigin mappedAddr *stun.MappedAddress software *stun.Software }, ) { ret.mappedAddr = &stun.MappedAddress{} ret.xorAddr = &stun.XORMappedAddress{} ret.respOrigin = &stun.ResponseOrigin{} ret.otherAddr = &stun.OtherAddress{} ret.software = &stun.Software{} if ret.xorAddr.GetFrom(msg) != nil { ret.xorAddr = nil } if ret.otherAddr.GetFrom(msg) != nil { ret.otherAddr = nil } if ret.respOrigin.GetFrom(msg) != nil { ret.respOrigin = nil } if ret.mappedAddr.GetFrom(msg) != nil { ret.mappedAddr = nil } if ret.software.GetFrom(msg) != nil { ret.software = nil } log.Debugf("%v", msg) log.Debugf("\tMAPPED-ADDRESS: %v", ret.mappedAddr) log.Debugf("\tXOR-MAPPED-ADDRESS: %v", ret.xorAddr) log.Debugf("\tRESPONSE-ORIGIN: %v", ret.respOrigin) log.Debugf("\tOTHER-ADDRESS: %v", ret.otherAddr) log.Debugf("\tSOFTWARE: %v", ret.software) for _, attr := range msg.Attributes { switch attr.Type { case stun.AttrXORMappedAddress, stun.AttrOtherAddress, stun.AttrResponseOrigin, stun.AttrMappedAddress, stun.AttrSoftware: break //nolint:staticcheck default: log.Debugf("\t%v (l=%v)", attr, attr.Length) } } return ret } // Given an address string, returns a StunServerConn func connect(addrStr string) (*stunServerConn, error) { log.Infof("Connecting to STUN server: %s", addrStr) addr, err := net.ResolveUDPAddr("udp4", addrStr) if err != nil { log.Warnf("Error resolving address: %s", err) return nil, err } c, err := net.ListenUDP("udp4", nil) if err != nil { return nil, err } log.Infof("Local address: %s", c.LocalAddr()) log.Infof("Remote address: %s", addr.String()) mChan := listen(c) return &stunServerConn{ conn: c, LocalAddr: c.LocalAddr(), RemoteAddr: addr, messageChan: mChan, }, nil } // Send request and wait for response or timeout func (c *stunServerConn) roundTrip(msg *stun.Message, addr net.Addr) (*stun.Message, error) { _ = msg.NewTransactionID() log.Infof("Sending to %v: (%v bytes)", addr, msg.Length+messageHeaderSize) log.Debugf("%v", msg) for _, attr := range msg.Attributes { log.Debugf("\t%v (l=%v)", attr, attr.Length) } _, err := c.conn.WriteTo(msg.Raw, addr) if err != nil { log.Warnf("Error sending request to %v", addr) return nil, err } // Wait for response or timeout select { case m, ok := <-c.messageChan: if !ok { return nil, errResponseMessage } return m, nil case <-time.After(time.Duration(*timeoutPtr) * time.Second): log.Infof("Timed out waiting for response from server %v", addr) return nil, errTimedOut } } // taken from https://github.com/pion/stun/blob/master/cmd/stun-traversal/main.go func listen(conn *net.UDPConn) (messages chan *stun.Message) { messages = make(chan *stun.Message) go func() { for { buf := make([]byte, 1024) n, addr, err := conn.ReadFromUDP(buf) if err != nil { close(messages) return } log.Infof("Response from %v: (%v bytes)", addr, n) buf = buf[:n] m := new(stun.Message) m.Raw = buf err = m.Decode() if err != nil { log.Infof("Error decoding message: %v", err) close(messages) return } messages <- m } }() return } stun-0.6.1/cmd/stun-traversal/000077500000000000000000000000001444552150000162445ustar00rootroot00000000000000stun-0.6.1/cmd/stun-traversal/README.md000066400000000000000000000001751444552150000175260ustar00rootroot00000000000000stun-traversal is a small NAT traversal proof of concept using package pion/stun. Peer exchange is done manually using stdin.stun-0.6.1/cmd/stun-traversal/main.go000066400000000000000000000075001444552150000175210ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implements a simple CLI tools to perform NAT traversal via STUN package main import ( "bufio" "flag" "fmt" "log" "net" "os" "strings" "time" "github.com/pion/stun" ) var server = flag.String("server", "stun:stun.voipgate.com:3478", "Stun server address") //nolint:gochecknoglobals const ( udp = "udp4" pingMsg = "ping" pongMsg = "pong" timeoutMillis = 500 ) func main() { //nolint:gocognit flag.Parse() srvAddr, err := net.ResolveUDPAddr(udp, *server) if err != nil { log.Fatalf("Failed to resolve server addr: %s", err) } conn, err := net.ListenUDP(udp, nil) if err != nil { log.Fatalf("Failed to listen: %s", err) } defer func() { _ = conn.Close() }() log.Printf("Listening on %s", conn.LocalAddr()) var publicAddr stun.XORMappedAddress var peerAddr *net.UDPAddr messageChan := listen(conn) var peerAddrChan <-chan string keepalive := time.Tick(timeoutMillis * time.Millisecond) keepaliveMsg := pingMsg var quit <-chan time.Time gotPong := false sentPong := false for { select { case message, ok := <-messageChan: if !ok { return } switch { case string(message) == pingMsg: keepaliveMsg = pongMsg case string(message) == pongMsg: if !gotPong { log.Println("Received pong message.") } // One client may skip sending ping if it receives // a ping message before knowning the peer address. keepaliveMsg = pongMsg gotPong = true case stun.IsMessage(message): m := new(stun.Message) m.Raw = message decErr := m.Decode() if decErr != nil { log.Println("decode:", decErr) break } var xorAddr stun.XORMappedAddress if getErr := xorAddr.GetFrom(m); getErr != nil { log.Println("getFrom:", getErr) break } if publicAddr.String() != xorAddr.String() { log.Printf("My public address: %s\n", xorAddr) publicAddr = xorAddr peerAddrChan = getPeerAddr() } default: log.Panicln("unknown message", message) } case peerStr := <-peerAddrChan: peerAddr, err = net.ResolveUDPAddr(udp, peerStr) if err != nil { log.Panicln("resolve peeraddr:", err) } case <-keepalive: // Keep NAT binding alive using STUN server or the peer once it's known if peerAddr == nil { err = sendBindingRequest(conn, srvAddr) } else { err = sendStr(keepaliveMsg, conn, peerAddr) if keepaliveMsg == pongMsg { sentPong = true } } if err != nil { log.Panicln("keepalive:", err) } case <-quit: _ = conn.Close() } if quit == nil && gotPong && sentPong { log.Println("Success! Quitting in two seconds.") quit = time.After(2 * time.Second) } } } func getPeerAddr() <-chan string { result := make(chan string) go func() { reader := bufio.NewReader(os.Stdin) log.Println("Enter remote peer address:") peer, _ := reader.ReadString('\n') result <- strings.Trim(peer, " \r\n") }() return result } func listen(conn *net.UDPConn) <-chan []byte { messages := make(chan []byte) go func() { for { buf := make([]byte, 1024) n, _, err := conn.ReadFromUDP(buf) if err != nil { close(messages) return } buf = buf[:n] messages <- buf } }() return messages } func sendBindingRequest(conn *net.UDPConn, addr *net.UDPAddr) error { m := stun.MustBuild(stun.TransactionID, stun.BindingRequest) err := send(m.Raw, conn, addr) if err != nil { return fmt.Errorf("binding: %w", err) } return nil } func send(msg []byte, conn *net.UDPConn, addr *net.UDPAddr) error { _, err := conn.WriteToUDP(msg, addr) if err != nil { return fmt.Errorf("send: %w", err) } return nil } func sendStr(msg string, conn *net.UDPConn, addr *net.UDPAddr) error { return send([]byte(msg), conn, addr) } stun-0.6.1/codecov.yml000066400000000000000000000007151444552150000146570ustar00rootroot00000000000000# # DO NOT EDIT THIS FILE # # It is automatically copied from https://github.com/pion/.goassets repository. # # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT coverage: status: project: default: # Allow decreasing 2% of total coverage to avoid noise. threshold: 2% patch: default: target: 70% only_pulls: true ignore: - "examples/*" - "examples/**/*" stun-0.6.1/e2e/000077500000000000000000000000001444552150000131625ustar00rootroot00000000000000stun-0.6.1/e2e/capture.sh000066400000000000000000000003741444552150000151650ustar00rootroot00000000000000#!/usr/bin/env bash # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT echo "net: $INTERFACE $SUBNET" tcpdump -U -v -i $INTERFACE \ src net $SUBNET and dst net $SUBNET \ -w /root/dump/dump.pcap stun-0.6.1/e2e/client.Dockerfile000066400000000000000000000004011444552150000164240ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT ARG CI_GO_VERSION FROM golang:${CI_GO_VERSION} ADD . /go/src/github.com/pion/stun WORKDIR /go/src/github.com/pion/stun/e2e RUN go install . CMD ["e2e"] stun-0.6.1/e2e/docker-compose.yml000066400000000000000000000010251444552150000166150ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT version: '3' services: stun-client: depends_on: - stun-server links: - stun-server build: context: .. dockerfile: e2e/client.Dockerfile args: CI_GO_VERSION: ${CI_GO_VERSION} stun-server: build: context: .. dockerfile: e2e/server.Dockerfile volumes: - ./turnserver.conf:/etc/turnserver.conf networks: default: external: name: stun_e2e_coturn stun-0.6.1/e2e/main.go000066400000000000000000000072401444552150000144400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package main implement end-to-end tests package main import ( "fmt" "log" "net" "strings" "time" "github.com/pion/stun" ) func test(network string) { addr := resolve(network) fmt.Println("START", strings.ToUpper(addr.Network())) //nolint var ( nonce stun.Nonce realm stun.Realm ) const ( username = "user" password = "secret" ) conn, err := net.Dial(addr.Network(), addr.String()) if err != nil { log.Fatalln("failed to dial conn:", err) //nolint } var options []stun.ClientOption if network == "tcp" { // Switching to "NO-RTO" mode. fmt.Println("using WithNoRetransmit for TCP") //nolint options = append(options, stun.WithNoRetransmit) } client, err := stun.NewClient(conn, options...) if err != nil { log.Fatal(err) //nolint } // First request should error. request, err := stun.Build(stun.BindingRequest, stun.TransactionID, stun.Fingerprint) if err != nil { log.Fatalln("failed to build:", err) //nolint } if err = client.Do(request, func(event stun.Event) { if event.Error != nil { log.Fatalln("got event with error:", event.Error) //nolint } response := event.Message if response.Type != stun.BindingError { log.Fatalln("bad message", response) //nolint } var errCode stun.ErrorCodeAttribute if codeErr := errCode.GetFrom(response); codeErr != nil { log.Fatalln("failed to get error code:", codeErr) //nolint } if errCode.Code != stun.CodeUnauthorized { log.Fatalln("unexpected error code:", errCode) //nolint } if parseErr := response.Parse(&nonce, &realm); parseErr != nil { log.Fatalln("failed to parse:", parseErr) //nolint } fmt.Println("Got nonce", nonce, "and realm", realm) //nolint }); err != nil { log.Fatalln("failed to Do:", err) //nolint } // Authenticating and sending second request. request, err = stun.Build(stun.TransactionID, stun.BindingRequest, stun.NewUsername(username), nonce, realm, stun.NewLongTermIntegrity(username, realm.String(), password), stun.Fingerprint, ) if err != nil { log.Fatalln(err) //nolint } if err = client.Do(request, func(event stun.Event) { if event.Error != nil { log.Fatalln("got event with error:", event.Error) //nolint } response := event.Message if response.Type != stun.BindingSuccess { var errCode stun.ErrorCodeAttribute if codeErr := errCode.GetFrom(response); codeErr != nil { log.Fatalln("failed to get error code:", codeErr) //nolint } log.Fatalln("bad message", response, errCode) //nolint } var xorMapped stun.XORMappedAddress if err = response.Parse(&xorMapped); err != nil { log.Fatalln("failed to parse xor mapped address:", err) //nolint } if conn.LocalAddr().String() != xorMapped.String() { log.Fatalln(conn.LocalAddr(), "!=", xorMapped) //nolint } fmt.Println("OK", response, "GOT", xorMapped) //nolint }); err != nil { log.Fatalln("failed to Do:", err) //nolint } if err := client.Close(); err != nil { log.Fatalln("failed to close client:", err) //nolint } fmt.Println("OK", strings.ToUpper(addr.Network())) //nolint } func resolve(network string) net.Addr { addr := fmt.Sprintf("%s:%d", "stun-server", stun.DefaultPort) var ( resolved net.Addr resolveErr error ) for i := 0; i < 10; i++ { switch network { case "udp": resolved, resolveErr = net.ResolveUDPAddr("udp", addr) case "tcp": resolved, resolveErr = net.ResolveTCPAddr("tcp", addr) default: panic("unknown network") //nolint } if resolveErr == nil { return resolved } time.Sleep(time.Millisecond * 300 * time.Duration(i)) } panic(resolveErr) //nolint } func main() { test("udp") test("tcp") } stun-0.6.1/e2e/server.Dockerfile000066400000000000000000000003351444552150000164620ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT FROM ubuntu:latest RUN apt-get update RUN apt-get install -y coturn USER turnserver ENTRYPOINT ["/usr/bin/turnserver"]stun-0.6.1/e2e/tcpdump.Dockerfile000066400000000000000000000004201444552150000166230ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT FROM ubuntu RUN apt-get update && apt-get install -y tcpdump RUN apt-get install net-tools -y ADD capture.sh /root/capture.sh ENTRYPOINT ["/bin/bash", "/root/capture.sh"] stun-0.6.1/e2e/test.sh000077500000000000000000000044061444552150000145040ustar00rootroot00000000000000#!/usr/bin/env bash # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT export CURRENT_GO_VERSION=$(echo -n "$(go version)" | grep -o 'go1\.[0-9|\.]*' || true) CURRENT_GO_VERSION=${CURRENT_GO_VERSION#go} GO_VERSION=${GO_VERSION:-$CURRENT_GO_VERSION} # set golang version from env export CI_GO_VERSION="${GO_VERSION:-latest}" # define some colors to use for output RED='\033[0;31m' GREEN='\033[0;32m' NC='\033[0m' printf "${GREEN}Go version \"${CI_GO_VERSION}\"${NC}\n" # kill and remove any running containers cleanup () { docker stop ci_stun-tcpdump docker rm -f ci_stun-tcpdump docker-compose -p ci kill docker-compose -p ci rm -f docker network rm stun_e2e_coturn } # catch unexpected failures, do cleanup and output an error message trap 'cleanup ; printf "${RED}Tests Failed For Unexpected Reasons${NC}\n"'\ HUP INT QUIT PIPE TERM # PREPARING NETWORK CAPTURE docker network create stun_e2e_coturn --internal docker build -t pion/tcpdump -f tcpdump.Dockerfile . NETWORK_ID=`docker network inspect stun_e2e_coturn -f "{{.Id}}"` NETWORK_SUBNET=`docker network inspect stun_e2e_coturn -f "{{(index .IPAM.Config 0).Subnet}}"` CAPTURE_INTERFACE="br-${NETWORK_ID:0:12}" echo "will capture traffic on $CAPTURE_INTERFACE$" docker run -e INTERFACE=${CAPTURE_INTERFACE} -e SUBNET=${NETWORK_SUBNET} -d \ -v $(pwd):/root/dump \ --name ci_stun-tcpdump --net=host pion/tcpdump # build and run the composed services docker-compose -p ci build && docker-compose -p ci up -d if [ $? -ne 0 ] ; then printf "${RED}Docker Compose Failed${NC}\n" exit -1 fi # wait for the test service to complete and grab the exit code TEST_EXIT_CODE=`docker wait ci_stun-client_1` docker logs ci_stun-client_1 &> log-client.txt docker logs ci_stun-server_1 &> log-server.txt docker logs ci_stun-tcpdump &> log-tcpdump.txt # output the logs for the test (for clarity) cat log-client.txt # inspect the output of the test and display respective message if [ -z ${TEST_EXIT_CODE+x} ] || [ "$TEST_EXIT_CODE" -ne 0 ] ; then printf "${RED}Tests Failed${NC} - Exit Code: $TEST_EXIT_CODE\n" else printf "${GREEN}Tests Passed${NC}\n" fi # call the cleanup function cleanup # exit the script with the same code as the test service code exit ${TEST_EXIT_CODE} stun-0.6.1/e2e/turnserver.conf000066400000000000000000000003741444552150000162540ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT Verbose fingerprint log-file=stdout lt-cred-mech user=user:secret realm=realm no-cli no-tls no-dtls no-tcp-relay stun-only secure-stun stale-nonce stun-0.6.1/errorcode.go000066400000000000000000000107741444552150000150330ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "fmt" "io" ) // ErrorCodeAttribute represents ERROR-CODE attribute. // // RFC 5389 Section 15.6 type ErrorCodeAttribute struct { Code ErrorCode Reason []byte } func (c ErrorCodeAttribute) String() string { return fmt.Sprintf("%d: %s", c.Code, c.Reason) } // constants for ERROR-CODE encoding. const ( errorCodeReasonStart = 4 errorCodeClassByte = 2 errorCodeNumberByte = 3 errorCodeReasonMaxB = 763 errorCodeModulo = 100 ) // AddTo adds ERROR-CODE to m. func (c ErrorCodeAttribute) AddTo(m *Message) error { value := make([]byte, 0, errorCodeReasonStart+errorCodeReasonMaxB) if err := CheckOverflow(AttrErrorCode, len(c.Reason)+errorCodeReasonStart, errorCodeReasonMaxB+errorCodeReasonStart, ); err != nil { return err } value = value[:errorCodeReasonStart+len(c.Reason)] number := byte(c.Code % errorCodeModulo) // error code modulo 100 class := byte(c.Code / errorCodeModulo) // hundred digit value[errorCodeClassByte] = class value[errorCodeNumberByte] = number copy(value[errorCodeReasonStart:], c.Reason) m.Add(AttrErrorCode, value) return nil } // GetFrom decodes ERROR-CODE from m. Reason is valid until m.Raw is valid. func (c *ErrorCodeAttribute) GetFrom(m *Message) error { v, err := m.Get(AttrErrorCode) if err != nil { return err } if len(v) < errorCodeReasonStart { return io.ErrUnexpectedEOF } var ( class = uint16(v[errorCodeClassByte]) number = uint16(v[errorCodeNumberByte]) code = int(class*errorCodeModulo + number) ) c.Code = ErrorCode(code) c.Reason = v[errorCodeReasonStart:] return nil } // ErrorCode is code for ERROR-CODE attribute. type ErrorCode int // ErrNoDefaultReason means that default reason for provided error code // is not defined in RFC. var ErrNoDefaultReason = errors.New("no default reason for ErrorCode") // AddTo adds ERROR-CODE with default reason to m. If there // is no default reason, returns ErrNoDefaultReason. func (c ErrorCode) AddTo(m *Message) error { reason := errorReasons[c] if reason == nil { return ErrNoDefaultReason } a := &ErrorCodeAttribute{ Code: c, Reason: reason, } return a.AddTo(m) } // Possible error codes. const ( CodeTryAlternate ErrorCode = 300 CodeBadRequest ErrorCode = 400 CodeUnauthorized ErrorCode = 401 CodeUnknownAttribute ErrorCode = 420 CodeStaleNonce ErrorCode = 438 CodeRoleConflict ErrorCode = 487 CodeServerError ErrorCode = 500 ) // DEPRECATED constants. const ( // DEPRECATED, use CodeUnauthorized. CodeUnauthorised = CodeUnauthorized ) // Error codes from RFC 5766. // // RFC 5766 Section 15 const ( CodeForbidden ErrorCode = 403 // Forbidden CodeAllocMismatch ErrorCode = 437 // Allocation Mismatch CodeWrongCredentials ErrorCode = 441 // Wrong Credentials CodeUnsupportedTransProto ErrorCode = 442 // Unsupported Transport Protocol CodeAllocQuotaReached ErrorCode = 486 // Allocation Quota Reached CodeInsufficientCapacity ErrorCode = 508 // Insufficient Capacity ) // Error codes from RFC 6062. // // RFC 6062 Section 6.3 const ( CodeConnAlreadyExists ErrorCode = 446 CodeConnTimeoutOrFailure ErrorCode = 447 ) // Error codes from RFC 6156. // // RFC 6156 Section 10.2 const ( CodeAddrFamilyNotSupported ErrorCode = 440 // Address Family not Supported CodePeerAddrFamilyMismatch ErrorCode = 443 // Peer Address Family Mismatch ) //nolint:gochecknoglobals var errorReasons = map[ErrorCode][]byte{ CodeTryAlternate: []byte("Try Alternate"), CodeBadRequest: []byte("Bad Request"), CodeUnauthorized: []byte("Unauthorized"), CodeUnknownAttribute: []byte("Unknown Attribute"), CodeStaleNonce: []byte("Stale Nonce"), CodeServerError: []byte("Server Error"), CodeRoleConflict: []byte("Role Conflict"), // RFC 5766. CodeForbidden: []byte("Forbidden"), CodeAllocMismatch: []byte("Allocation Mismatch"), CodeWrongCredentials: []byte("Wrong Credentials"), CodeUnsupportedTransProto: []byte("Unsupported Transport Protocol"), CodeAllocQuotaReached: []byte("Allocation Quota Reached"), CodeInsufficientCapacity: []byte("Insufficient Capacity"), // RFC 6062. CodeConnAlreadyExists: []byte("Connection Already Exists"), CodeConnTimeoutOrFailure: []byte("Connection Timeout or Failure"), // RFC 6156. CodeAddrFamilyNotSupported: []byte("Address Family not Supported"), CodePeerAddrFamilyMismatch: []byte("Peer Address Family Mismatch"), } stun-0.6.1/errorcode_test.go000066400000000000000000000046241444552150000160670ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package stun import ( "encoding/base64" "errors" "io" "testing" ) func BenchmarkErrorCode_AddTo(b *testing.B) { m := New() b.ReportAllocs() for i := 0; i < b.N; i++ { CodeStaleNonce.AddTo(m) //nolint:errcheck,gosec m.Reset() } } func BenchmarkErrorCodeAttribute_AddTo(b *testing.B) { m := New() b.ReportAllocs() a := &ErrorCodeAttribute{ Code: 404, Reason: []byte("not found!"), } for i := 0; i < b.N; i++ { a.AddTo(m) //nolint:errcheck,gosec m.Reset() } } func BenchmarkErrorCodeAttribute_GetFrom(b *testing.B) { m := New() b.ReportAllocs() a := &ErrorCodeAttribute{ Code: 404, Reason: []byte("not found!"), } a.AddTo(m) //nolint:errcheck,gosec for i := 0; i < b.N; i++ { a.GetFrom(m) //nolint:errcheck,gosec } } func TestErrorCodeAttribute_GetFrom(t *testing.T) { m := New() m.Add(AttrErrorCode, []byte{1}) c := new(ErrorCodeAttribute) if err := c.GetFrom(m); !errors.Is(err, io.ErrUnexpectedEOF) { t.Errorf("GetFrom should return <%s>, but got <%s>", io.ErrUnexpectedEOF, err, ) } } func TestMessage_AddErrorCode(t *testing.T) { m := New() transactionID, err := base64.StdEncoding.DecodeString("jxhBARZwX+rsC6er") if err != nil { t.Error(err) } copy(m.TransactionID[:], transactionID) expectedCode := ErrorCode(438) expectedReason := "Stale Nonce" CodeStaleNonce.AddTo(m) //nolint:errcheck,gosec m.WriteHeader() mRes := New() if _, err = mRes.ReadFrom(m.reader()); err != nil { t.Fatal(err) } errCodeAttr := new(ErrorCodeAttribute) if err = errCodeAttr.GetFrom(mRes); err != nil { t.Error(err) } code := errCodeAttr.Code if err != nil { t.Error(err) } if code != expectedCode { t.Error("bad code", code) } if string(errCodeAttr.Reason) != expectedReason { t.Error("bad reason", string(errCodeAttr.Reason)) } } func TestErrorCode(t *testing.T) { a := &ErrorCodeAttribute{ Code: 404, Reason: []byte("not found!"), } if a.String() != "404: not found!" { t.Error("bad string", a) } m := New() cod := ErrorCode(666) if err := cod.AddTo(m); !errors.Is(err, ErrNoDefaultReason) { t.Error("should be ErrNoDefaultReason", err) } if err := a.GetFrom(m); err == nil { t.Error("attr should not be in message") } a.Reason = make([]byte, 2048) if err := a.AddTo(m); err == nil { t.Error("should error") } } stun-0.6.1/errors.go000066400000000000000000000033231444552150000143530ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import "errors" // DecodeErr records an error and place when it is occurred. // //nolint:errname type DecodeErr struct { Place DecodeErrPlace Message string } // IsInvalidCookie returns true if error means that magic cookie // value is invalid. func (e DecodeErr) IsInvalidCookie() bool { return e.Place == DecodeErrPlace{"message", "cookie"} } // IsPlaceParent reports if error place parent is p. func (e DecodeErr) IsPlaceParent(p string) bool { return e.Place.Parent == p } // IsPlaceChildren reports if error place children is c. func (e DecodeErr) IsPlaceChildren(c string) bool { return e.Place.Children == c } // IsPlace reports if error place is p. func (e DecodeErr) IsPlace(p DecodeErrPlace) bool { return e.Place == p } // DecodeErrPlace records a place where error is occurred. type DecodeErrPlace struct { Parent string Children string } func (p DecodeErrPlace) String() string { return p.Parent + "/" + p.Children } func (e DecodeErr) Error() string { return "BadFormat for " + e.Place.String() + ": " + e.Message } func newDecodeErr(parent, children, message string) *DecodeErr { return &DecodeErr{ Place: DecodeErrPlace{Parent: parent, Children: children}, Message: message, } } func newAttrDecodeErr(children, message string) *DecodeErr { return newDecodeErr("attribute", children, message) } // ErrAttributeSizeInvalid means that decoded attribute size is invalid. var ErrAttributeSizeInvalid = errors.New("attribute size is invalid") // ErrAttributeSizeOverflow means that decoded attribute size is too big. var ErrAttributeSizeOverflow = errors.New("attribute size overflow") stun-0.6.1/errors_test.go000066400000000000000000000014721444552150000154150ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "testing" ) func TestDecodeErr_IsInvalidCookie(t *testing.T) { m := new(Message) m.WriteHeader() decoded := new(Message) m.Raw[4] = 55 _, err := decoded.Write(m.Raw) if err == nil { t.Fatal("should error") } expected := "BadFormat for message/cookie: " + "3712a442 is invalid magic cookie (should be 2112a442)" if err.Error() != expected { t.Error(err, "!=", expected) } var dErr *DecodeErr if !errors.As(err, &dErr) { t.Error("not decode error") } if !dErr.IsInvalidCookie() { t.Error("IsInvalidCookie = false, should be true") } if !dErr.IsPlaceChildren("cookie") { t.Error("bad children") } if !dErr.IsPlaceParent("message") { t.Error("bad parent") } } stun-0.6.1/fingerprint.go000066400000000000000000000041241444552150000153660ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "hash/crc32" ) // FingerprintAttr represents FINGERPRINT attribute. // // RFC 5389 Section 15.5 type FingerprintAttr struct{} // ErrFingerprintMismatch means that computed fingerprint differs from expected. var ErrFingerprintMismatch = errors.New("fingerprint check failed") // Fingerprint is shorthand for FingerprintAttr. // // Example: // // m := New() // Fingerprint.AddTo(m) var Fingerprint FingerprintAttr //nolint:gochecknoglobals const ( fingerprintXORValue uint32 = 0x5354554e //nolint:staticcheck fingerprintSize = 4 // 32 bit ) // FingerprintValue returns CRC-32 of b XOR-ed by 0x5354554e. // // The value of the attribute is computed as the CRC-32 of the STUN message // up to (but excluding) the FINGERPRINT attribute itself, XOR'ed with // the 32-bit value 0x5354554e (the XOR helps in cases where an // application packet is also using CRC-32 in it). func FingerprintValue(b []byte) uint32 { return crc32.ChecksumIEEE(b) ^ fingerprintXORValue // XOR } // AddTo adds fingerprint to message. func (FingerprintAttr) AddTo(m *Message) error { l := m.Length // length in header should include size of fingerprint attribute m.Length += fingerprintSize + attributeHeaderSize // increasing length m.WriteLength() // writing Length to Raw b := make([]byte, fingerprintSize) val := FingerprintValue(m.Raw) bin.PutUint32(b, val) m.Length = l m.Add(AttrFingerprint, b) return nil } // Check reads fingerprint value from m and checks it, returning error if any. // Can return *AttrLengthErr, ErrAttributeNotFound, and *CRCMismatch. func (FingerprintAttr) Check(m *Message) error { b, err := m.Get(AttrFingerprint) if err != nil { return err } if err = CheckSize(AttrFingerprint, len(b), fingerprintSize); err != nil { return err } val := bin.Uint32(b) attrStart := len(m.Raw) - (fingerprintSize + attributeHeaderSize) expected := FingerprintValue(m.Raw[:attrStart]) return checkFingerprint(val, expected) } stun-0.6.1/fingerprint_debug.go000066400000000000000000000006251444552150000165360ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build debug // +build debug package stun import "fmt" // CRCMismatch represents CRC check error. type CRCMismatch struct { Expected uint32 Actual uint32 } func (m CRCMismatch) Error() string { return fmt.Sprintf("CRC mismatch: %x (expected) != %x (actual)", m.Expected, m.Actual, ) } stun-0.6.1/fingerprint_test.go000066400000000000000000000033551444552150000164320ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package stun import ( "net" "testing" ) func BenchmarkFingerprint_AddTo(b *testing.B) { b.ReportAllocs() m := new(Message) s := NewSoftware("software") addr := &XORMappedAddress{ IP: net.IPv4(213, 1, 223, 5), } addAttr(b, m, addr) addAttr(b, m, s) b.SetBytes(int64(len(m.Raw))) for i := 0; i < b.N; i++ { Fingerprint.AddTo(m) //nolint:errcheck,gosec m.WriteLength() m.Length -= attributeHeaderSize + fingerprintSize m.Raw = m.Raw[:m.Length+messageHeaderSize] m.Attributes = m.Attributes[:len(m.Attributes)-1] } } func TestFingerprint_Check(t *testing.T) { m := new(Message) addAttr(t, m, NewSoftware("software")) m.WriteHeader() Fingerprint.AddTo(m) //nolint:errcheck,gosec m.WriteHeader() if err := Fingerprint.Check(m); err != nil { t.Error(err) } m.Raw[3]++ if err := Fingerprint.Check(m); err == nil { t.Error("should error") } } func TestFingerprint_CheckBad(t *testing.T) { m := new(Message) addAttr(t, m, NewSoftware("software")) m.WriteHeader() if err := Fingerprint.Check(m); err == nil { t.Error("should error") } m.Add(AttrFingerprint, []byte{1, 2, 3}) if !IsAttrSizeInvalid(Fingerprint.Check(m)) { t.Error("IsAttrSizeInvalid should be true") } } func BenchmarkFingerprint_Check(b *testing.B) { b.ReportAllocs() m := new(Message) s := NewSoftware("software") addr := &XORMappedAddress{ IP: net.IPv4(213, 1, 223, 5), } addAttr(b, m, addr) addAttr(b, m, s) m.WriteHeader() Fingerprint.AddTo(m) //nolint:errcheck,gosec m.WriteHeader() b.SetBytes(int64(len(m.Raw))) for i := 0; i < b.N; i++ { if err := Fingerprint.Check(m); err != nil { b.Fatal(err) } } } stun-0.6.1/fuzz_test.go000066400000000000000000000055321444552150000151000ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "encoding/binary" "errors" "testing" ) func FuzzMessage(f *testing.F) { msg1 := New() f.Fuzz(func(t *testing.T, data []byte) { msg1.Reset() // Fuzzer does not know about cookies if len(data) >= 8 { binary.BigEndian.PutUint32(data[4:8], magicCookie) } // Trying to read data as message if _, err := msg1.Write(data); err != nil { return // We expect invalid messages to fail here } msg2 := New() if _, err := msg2.Write(msg1.Raw); err != nil { t.Fatalf("Failed to write: %s", err) } if msg2.TransactionID != msg1.TransactionID { t.Fatal("Transaction ID mismatch") } if msg2.Type != msg1.Type { t.Fatal("Type mismatch") } if len(msg2.Attributes) != len(msg1.Attributes) { t.Fatal("Attributes length mismatch") } }) } func FuzzType(f *testing.F) { f.Fuzz(func(t *testing.T, data uint16) { v := data & 0x1fff // First 3 bits are empty t1 := MessageType{} t1.ReadValue(v) v2 := t1.Value() if v != v2 { t.Fatal("v != v2") } t2 := MessageType{} t2.ReadValue(v2) if t2 != t1 { t.Fatal("t2 != t1") } }) } func FuzzSetters(f *testing.F) { f.Fuzz(func(t *testing.T, firstByte byte, value []byte) { var ( m1 = &Message{ Raw: make([]byte, 0, 2048), } m2 = &Message{ Raw: make([]byte, 0, 2048), } m3 = &Message{ Raw: make([]byte, 0, 2048), } ) attrs := attributes{ {new(Realm), AttrRealm}, {new(XORMappedAddress), AttrXORMappedAddress}, {new(Nonce), AttrNonce}, {new(Software), AttrSoftware}, {new(AlternateServer), AttrAlternateServer}, {new(ErrorCodeAttribute), AttrErrorCode}, {new(UnknownAttributes), AttrUnknownAttributes}, {new(Username), AttrUsername}, {new(MappedAddress), AttrMappedAddress}, {new(Realm), AttrRealm}, } attr := attrs.pick(firstByte) m1.WriteHeader() m1.Add(attr.t, value) err := attr.g.GetFrom(m1) if errors.Is(err, ErrAttributeNotFound) { t.Fatalf("Unexpected 404: %s", err) } if err != nil { return } m2.WriteHeader() if err = attr.g.AddTo(m2); err != nil { // We allow decoding some text attributes // when their length is too big, but // not encoding. if !IsAttrSizeOverflow(err) { t.Fatal(err) } return } m3.WriteHeader() v, err := m2.Get(attr.t) if err != nil { t.Fatal(err) } m3.Add(attr.t, v) if !m2.Equal(m3) { t.Fatalf("Not equal: %s != %s", m2, m3) } }) } func TestAttrPick(*testing.T) { attrs := attributes{ {new(XORMappedAddress), AttrXORMappedAddress}, } for i := byte(0); i < 255; i++ { attrs.pick(i) } } type attr interface { Getter Setter } type attributes []struct { g attr t AttrType } func (a attributes) pick(v byte) struct { g attr t AttrType } { idx := int(v) % len(a) return a[idx] } stun-0.6.1/go.mod000066400000000000000000000002731444552150000136170ustar00rootroot00000000000000module github.com/pion/stun go 1.12 require ( github.com/pion/dtls/v2 v2.2.7 github.com/pion/logging v0.2.2 github.com/pion/transport/v2 v2.2.1 github.com/stretchr/testify v1.8.4 ) stun-0.6.1/go.sum000066400000000000000000000130221444552150000136400ustar00rootroot00000000000000github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0 h1:aWJ/m6xSmxWBx+V0XRHTlrYrPG56jKsLdTFmsSsCzOM= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= stun-0.6.1/helpers.go000066400000000000000000000050611444552150000145020ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun // Interfaces that are implemented by message attributes, shorthands for them, // or helpers for message fields as type or transaction id. type ( // Setter sets *Message attribute. Setter interface { AddTo(m *Message) error } // Getter parses attribute from *Message. Getter interface { GetFrom(m *Message) error } // Checker checks *Message attribute. Checker interface { Check(m *Message) error } ) // Build resets message and applies setters to it in batch, returning on // first error. To prevent allocations, pass pointers to values. // // Example: // // var ( // t = BindingRequest // username = NewUsername("username") // nonce = NewNonce("nonce") // realm = NewRealm("example.org") // ) // m := new(Message) // m.Build(t, username, nonce, realm) // 4 allocations // m.Build(&t, &username, &nonce, &realm) // 0 allocations // // See BenchmarkBuildOverhead. func (m *Message) Build(setters ...Setter) error { m.Reset() m.WriteHeader() for _, s := range setters { if err := s.AddTo(m); err != nil { return err } } return nil } // Check applies checkers to message in batch, returning on first error. func (m *Message) Check(checkers ...Checker) error { for _, c := range checkers { if err := c.Check(m); err != nil { return err } } return nil } // Parse applies getters to message in batch, returning on first error. func (m *Message) Parse(getters ...Getter) error { for _, c := range getters { if err := c.GetFrom(m); err != nil { return err } } return nil } // MustBuild wraps Build call and panics on error. func MustBuild(setters ...Setter) *Message { m, err := Build(setters...) if err != nil { panic(err) //nolint } return m } // Build wraps Message.Build method. func Build(setters ...Setter) (*Message, error) { m := new(Message) if err := m.Build(setters...); err != nil { return nil, err } return m, nil } // ForEach is helper that iterates over message attributes allowing to call // Getter in f callback to get all attributes of type t and returning on first // f error. // // The m.Get method inside f will be returning next attribute on each f call. // Does not error if there are no results. func (m *Message) ForEach(t AttrType, f func(m *Message) error) error { attrs := m.Attributes defer func() { m.Attributes = attrs }() for i, a := range attrs { if a.Type != t { continue } m.Attributes = attrs[i:] if err := f(m); err != nil { return err } } return nil } stun-0.6.1/helpers_test.go000066400000000000000000000122061444552150000155400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "fmt" "testing" "github.com/pion/stun/internal/testutil" ) func BenchmarkBuildOverhead(b *testing.B) { var ( t = BindingRequest username = NewUsername("username") nonce = NewNonce("nonce") realm = NewRealm("example.org") ) b.Run("Build", func(b *testing.B) { b.ReportAllocs() m := new(Message) for i := 0; i < b.N; i++ { m.Build(&t, &username, &nonce, &realm, &Fingerprint) //nolint:errcheck,gosec } }) b.Run("BuildNonPointer", func(b *testing.B) { b.ReportAllocs() m := new(Message) for i := 0; i < b.N; i++ { m.Build(t, username, nonce, realm, Fingerprint) //nolint:errcheck,gosec //nolint:errcheck,gosec } }) b.Run("Raw", func(b *testing.B) { b.ReportAllocs() m := new(Message) for i := 0; i < b.N; i++ { m.Reset() m.WriteHeader() m.SetType(t) username.AddTo(m) //nolint:errcheck,gosec nonce.AddTo(m) //nolint:errcheck,gosec realm.AddTo(m) //nolint:errcheck,gosec Fingerprint.AddTo(m) //nolint:errcheck,gosec } }) } func TestMessage_Apply(t *testing.T) { var ( integrity = NewShortTermIntegrity("password") decoded = new(Message) ) m, err := Build(BindingRequest, TransactionID, NewUsername("username"), NewNonce("nonce"), NewRealm("example.org"), integrity, Fingerprint, ) if err != nil { t.Fatal("failed to build:", err) } if err = m.Check(Fingerprint, integrity); err != nil { t.Fatal(err) } if _, err := decoded.Write(m.Raw); err != nil { t.Fatal(err) } if !decoded.Equal(m) { t.Error("not equal") } if err := integrity.Check(decoded); err != nil { t.Fatal(err) } } type errReturner struct { Err error } var errTError = errors.New("tError") func (e errReturner) AddTo(*Message) error { return e.Err } func (e errReturner) Check(*Message) error { return e.Err } func (e errReturner) GetFrom(*Message) error { return e.Err } func TestHelpersErrorHandling(t *testing.T) { m := New() e := errReturner{Err: errTError} if err := m.Build(e); !errors.Is(err, e.Err) { t.Error(err, "!=", e.Err) } if err := m.Check(e); !errors.Is(err, e.Err) { t.Error(err, "!=", e.Err) } if err := m.Parse(e); !errors.Is(err, e.Err) { t.Error(err, "!=", e.Err) } t.Run("MustBuild", func(t *testing.T) { t.Run("Positive", func(t *testing.T) { MustBuild(NewTransactionIDSetter(transactionID{})) }) defer func() { if p, ok := recover().(error); !ok || !errors.Is(p, e.Err) { t.Errorf("%s != %s", p, e.Err, ) } }() MustBuild(e) }) } func TestMessage_ForEach(t *testing.T) { initial := New() if err := initial.Build( NewRealm("realm1"), NewRealm("realm2"), ); err != nil { t.Fatal(err) } newMessage := func() *Message { m := New() if err := m.Build( NewRealm("realm1"), NewRealm("realm2"), ); err != nil { t.Fatal(err) } return m } t.Run("NoResults", func(t *testing.T) { m := newMessage() if !m.Equal(initial) { t.Error("m should be equal to initial") } if err := m.ForEach(AttrUsername, func(m *Message) error { t.Error("should not be called") return nil }); err != nil { t.Fatal(err) } if !m.Equal(initial) { t.Error("m should be equal to initial") } }) t.Run("ReturnOnError", func(t *testing.T) { m := newMessage() var calls int if err := m.ForEach(AttrRealm, func(m *Message) error { if calls > 0 { t.Error("called multiple times") } calls++ return ErrAttributeNotFound }); !errors.Is(err, ErrAttributeNotFound) { t.Fatal(err) } if !m.Equal(initial) { t.Error("m should be equal to initial") } }) t.Run("Positive", func(t *testing.T) { m := newMessage() var realms []string if err := m.ForEach(AttrRealm, func(m *Message) error { var realm Realm if err := realm.GetFrom(m); err != nil { return err } realms = append(realms, realm.String()) return nil }); err != nil { t.Fatal(err) } if len(realms) != 2 { t.Fatal("expected 2 realms") } if realms[0] != "realm1" { t.Error("bad value for 1 realm") } if realms[1] != "realm2" { t.Error("bad value for 2 realm") } if !m.Equal(initial) { t.Error("m should be equal to initial") } t.Run("ZeroAlloc", func(t *testing.T) { m = newMessage() var realm Realm testutil.ShouldNotAllocate(t, func() { if err := m.ForEach(AttrRealm, realm.GetFrom); err != nil { t.Fatal(err) } }) if !m.Equal(initial) { t.Error("m should be equal to initial") } }) }) } func ExampleMessage_ForEach() { m := MustBuild(NewRealm("realm1"), NewRealm("realm2")) if err := m.ForEach(AttrRealm, func(m *Message) error { var r Realm if err := r.GetFrom(m); err != nil { return err } fmt.Println(r) return nil }); err != nil { fmt.Println("error:", err) } // Output: // realm1 // realm2 } func BenchmarkMessage_ForEach(b *testing.B) { b.ReportAllocs() m := MustBuild( NewRealm("realm1"), NewRealm("realm2"), NewRealm("realm3"), NewRealm("realm4"), ) for i := 0; i < b.N; i++ { if err := m.ForEach(AttrRealm, func(m *Message) error { return nil }); err != nil { b.Fatal(err) } } } stun-0.6.1/iana_test.go000066400000000000000000000050641444552150000150120ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package stun import ( "bytes" "encoding/csv" "strconv" "strings" "testing" ) func loadCSV(t testing.TB, name string) [][]string { t.Helper() data := loadData(t, name) r := csv.NewReader(bytes.NewReader(data)) r.Comment = '#' records, err := r.ReadAll() if err != nil { t.Fatal(err) } return records } func TestIANA(t *testing.T) { t.Run("Methods", func(t *testing.T) { records := loadCSV(t, "stun-parameters-2.csv") m := make(map[string]Method) for _, r := range records[1:] { var ( v = r[0] name = r[1] ) if strings.Contains(v, "-") { continue } val, parseErr := strconv.ParseInt(v[2:], 16, 64) if parseErr != nil { t.Fatal(parseErr) } t.Logf("value: 0x%x, name: %s", val, name) m[name] = Method(val) } for val, name := range methodName() { mapped, ok := m[name] if !ok { t.Errorf("failed to find method %s in IANA", name) } if mapped != val { t.Errorf("%s: IANA %d != actual %d", name, mapped, val) } } }) t.Run("Attributes", func(t *testing.T) { records := loadCSV(t, "stun-parameters-4.csv") m := map[string]AttrType{} for _, r := range records[1:] { var ( v = r[0] name = r[1] ) if strings.Contains(v, "-") { continue } val, parseErr := strconv.ParseInt(v[2:], 16, 64) if parseErr != nil { t.Fatal(parseErr) } t.Logf("value: 0x%x, name: %s", val, name) m[name] = AttrType(val) } // Not registered in IANA. for k, v := range map[string]AttrType{ "ORIGIN": 0x802F, } { m[k] = v } for val, name := range attrNames() { mapped, ok := m[name] if !ok { t.Errorf("failed to find attribute %s in IANA", name) } if mapped != val { t.Errorf("%s: IANA %d != actual %d", name, mapped, val) } } }) t.Run("ErrorCodes", func(t *testing.T) { records := loadCSV(t, "stun-parameters-6.csv") m := map[string]ErrorCode{} for _, r := range records[1:] { var ( v = r[0] name = r[1] ) if strings.Contains(v, "-") { continue } val, parseErr := strconv.ParseInt(v, 10, 64) if parseErr != nil { t.Fatal(parseErr) } t.Logf("value: 0x%x, name: %s", val, name) m[name] = ErrorCode(val) } for val, nameB := range errorReasons { name := string(nameB) mapped, ok := m[name] if !ok { t.Errorf("failed to find error code %s in IANA", name) } if mapped != val { t.Errorf("%s: IANA %d != actual %d", name, mapped, val) } } }) } stun-0.6.1/integrity.go000066400000000000000000000075451444552150000150670ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( //nolint:gci "crypto/md5" //nolint:gosec "crypto/sha1" //nolint:gosec "errors" "fmt" "strings" "github.com/pion/stun/internal/hmac" ) // separator for credentials. const credentialsSep = ":" // NewLongTermIntegrity returns new MessageIntegrity with key for long-term // credentials. Password, username, and realm must be SASL-prepared. func NewLongTermIntegrity(username, realm, password string) MessageIntegrity { k := strings.Join([]string{username, realm, password}, credentialsSep) h := md5.New() //nolint:gosec fmt.Fprint(h, k) return MessageIntegrity(h.Sum(nil)) } // NewShortTermIntegrity returns new MessageIntegrity with key for short-term // credentials. Password must be SASL-prepared. func NewShortTermIntegrity(password string) MessageIntegrity { return MessageIntegrity(password) } // MessageIntegrity represents MESSAGE-INTEGRITY attribute. // // AddTo and Check methods are using zero-allocation version of hmac, see // newHMAC function and internal/hmac/pool.go. // // RFC 5389 Section 15.4 type MessageIntegrity []byte func newHMAC(key, message, buf []byte) []byte { mac := hmac.AcquireSHA1(key) writeOrPanic(mac, message) defer hmac.PutSHA1(mac) return mac.Sum(buf) } func (i MessageIntegrity) String() string { return fmt.Sprintf("KEY: 0x%x", []byte(i)) } const messageIntegritySize = 20 // ErrFingerprintBeforeIntegrity means that FINGERPRINT attribute is already in // message, so MESSAGE-INTEGRITY attribute cannot be added. var ErrFingerprintBeforeIntegrity = errors.New("FINGERPRINT before MESSAGE-INTEGRITY attribute") // AddTo adds MESSAGE-INTEGRITY attribute to message. // // CPU costly, see BenchmarkMessageIntegrity_AddTo. func (i MessageIntegrity) AddTo(m *Message) error { for _, a := range m.Attributes { // Message should not contain FINGERPRINT attribute // before MESSAGE-INTEGRITY. if a.Type == AttrFingerprint { return ErrFingerprintBeforeIntegrity } } // The text used as input to HMAC is the STUN message, // including the header, up to and including the attribute preceding the // MESSAGE-INTEGRITY attribute. length := m.Length // Adjusting m.Length to contain MESSAGE-INTEGRITY TLV. m.Length += messageIntegritySize + attributeHeaderSize m.WriteLength() // writing length to m.Raw v := newHMAC(i, m.Raw, m.Raw[len(m.Raw):]) // calculating HMAC for adjusted m.Raw m.Length = length // changing m.Length back // Copy hmac value to temporary variable to protect it from resetting // while processing m.Add call. vBuf := make([]byte, sha1.Size) copy(vBuf, v) m.Add(AttrMessageIntegrity, vBuf) return nil } // ErrIntegrityMismatch means that computed HMAC differs from expected. var ErrIntegrityMismatch = errors.New("integrity check failed") // Check checks MESSAGE-INTEGRITY attribute. // // CPU costly, see BenchmarkMessageIntegrity_Check. func (i MessageIntegrity) Check(m *Message) error { v, err := m.Get(AttrMessageIntegrity) if err != nil { return err } // Adjusting length in header to match m.Raw that was // used when computing HMAC. var ( length = m.Length afterIntegrity = false sizeReduced int ) for _, a := range m.Attributes { if afterIntegrity { sizeReduced += nearestPaddedValueLength(int(a.Length)) sizeReduced += attributeHeaderSize } if a.Type == AttrMessageIntegrity { afterIntegrity = true } } m.Length -= uint32(sizeReduced) m.WriteLength() // startOfHMAC should be first byte of integrity attribute. startOfHMAC := messageHeaderSize + m.Length - (attributeHeaderSize + messageIntegritySize) b := m.Raw[:startOfHMAC] // data before integrity attribute expected := newHMAC(i, b, m.Raw[len(m.Raw):]) m.Length = length m.WriteLength() // writing length back return checkHMAC(v, expected) } stun-0.6.1/integrity_debug.go000066400000000000000000000006751444552150000162320ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build debug // +build debug package stun import "fmt" // IntegrityErr occurs when computed HMAC differs from expected. type IntegrityErr struct { Expected []byte Actual []byte } func (i *IntegrityErr) Error() string { return fmt.Sprintf( "Integrity check failed: 0x%x (expected) !- 0x%x (actual)", i.Expected, i.Actual, ) } stun-0.6.1/integrity_test.go000066400000000000000000000054241444552150000161200ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "bytes" "encoding/hex" "testing" ) func TestMessageIntegrity_AddTo_Simple(t *testing.T) { i := NewLongTermIntegrity("user", "realm", "pass") expected, err := hex.DecodeString("8493fbc53ba582fb4c044c456bdc40eb") if err != nil { t.Fatal(err) } if !bytes.Equal(expected, i) { t.Error(ErrIntegrityMismatch) } t.Run("Check", func(t *testing.T) { m := new(Message) m.WriteHeader() if err := i.AddTo(m); err != nil { t.Error(err) } NewSoftware("software").AddTo(m) //nolint:errcheck,gosec m.WriteHeader() dM := new(Message) dM.Raw = m.Raw if err := dM.Decode(); err != nil { t.Error(err) } if err := i.Check(dM); err != nil { t.Error(err) } dM.Raw[24] += 12 // HMAC now invalid if i.Check(dM) == nil { t.Error("should be invalid") } }) } func TestMessageIntegrityWithFingerprint(t *testing.T) { m := new(Message) m.TransactionID = [TransactionIDSize]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11} m.WriteHeader() NewSoftware("software").AddTo(m) //nolint:errcheck,gosec i := NewShortTermIntegrity("pwd") if i.String() != "KEY: 0x707764" { t.Error("bad string", i) } if err := i.Check(m); err == nil { t.Error("should error") } if err := i.AddTo(m); err != nil { t.Fatal(err) } if err := Fingerprint.AddTo(m); err != nil { t.Fatal(err) } if err := i.Check(m); err != nil { t.Fatal(err) } m.Raw[24] = 33 if err := i.Check(m); err == nil { t.Fatal("mismatch expected") } } func TestMessageIntegrity(t *testing.T) { m := new(Message) i := NewShortTermIntegrity("password") m.WriteHeader() if err := i.AddTo(m); err != nil { t.Error(err) } _, err := m.Get(AttrMessageIntegrity) if err != nil { t.Error(err) } } func TestMessageIntegrityBeforeFingerprint(t *testing.T) { m := new(Message) m.WriteHeader() Fingerprint.AddTo(m) //nolint:errcheck,gosec i := NewShortTermIntegrity("password") if err := i.AddTo(m); err == nil { t.Error("should error") } } func BenchmarkMessageIntegrity_AddTo(b *testing.B) { m := new(Message) integrity := NewShortTermIntegrity("password") m.WriteHeader() b.ReportAllocs() b.SetBytes(int64(len(m.Raw))) for i := 0; i < b.N; i++ { m.WriteHeader() if err := integrity.AddTo(m); err != nil { b.Error(err) } m.Reset() } } func BenchmarkMessageIntegrity_Check(b *testing.B) { m := new(Message) m.Raw = make([]byte, 0, 1024) NewSoftware("software").AddTo(m) //nolint:errcheck,gosec integrity := NewShortTermIntegrity("password") b.ReportAllocs() m.WriteHeader() b.SetBytes(int64(len(m.Raw))) if err := integrity.AddTo(m); err != nil { b.Error(err) } m.WriteLength() for i := 0; i < b.N; i++ { if err := integrity.Check(m); err != nil { b.Fatal(err) } } } stun-0.6.1/internal/000077500000000000000000000000001444552150000143235ustar00rootroot00000000000000stun-0.6.1/internal/hmac/000077500000000000000000000000001444552150000152335ustar00rootroot00000000000000stun-0.6.1/internal/hmac/hmac.go000066400000000000000000000104641444552150000164770ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2009 The Go Authors. All rights reserved. // SPDX-License-Identifier: BSD-3-Clause /* Package hmac implements the Keyed-Hash Message Authentication Code (HMAC) as defined in U.S. Federal Information Processing Standards Publication 198. An HMAC is a cryptographic hash that uses a key to sign a message. The receiver verifies the hash by recomputing it using the same key. Receivers should be careful to use Equal to compare MACs in order to avoid timing side-channels: // ValidMAC reports whether messageMAC is a valid HMAC tag for message. func ValidMAC(message, messageMAC, key []byte) bool { mac := hmac.New(sha256.New, key) mac.Write(message) expectedMAC := mac.Sum(nil) return hmac.Equal(messageMAC, expectedMAC) } */ package hmac import ( "crypto/subtle" "hash" ) // FIPS 198-1: // https://csrc.nist.gov/publications/fips/fips198-1/FIPS-198-1_final.pdf // key is zero padded to the block size of the hash function // ipad = 0x36 byte repeated for key length // opad = 0x5c byte repeated for key length // hmac = H([key ^ opad] H([key ^ ipad] text)) // Marshalable is the combination of encoding.BinaryMarshaler and // encoding.BinaryUnmarshaler. Their method definitions are repeated here to // avoid a dependency on the encoding package. type marshalable interface { MarshalBinary() ([]byte, error) UnmarshalBinary([]byte) error } type hmac struct { opad, ipad []byte outer, inner hash.Hash // If marshaled is true, then opad and ipad do not contain a padded // copy of the key, but rather the marshaled state of outer/inner after // opad/ipad has been fed into it. marshaled bool } func (h *hmac) Sum(in []byte) []byte { origLen := len(in) in = h.inner.Sum(in) if h.marshaled { if err := h.outer.(marshalable).UnmarshalBinary(h.opad); err != nil { //nolint:forcetypeassert panic(err) //nolint } } else { h.outer.Reset() h.outer.Write(h.opad) //nolint:errcheck,gosec } h.outer.Write(in[origLen:]) //nolint:errcheck,gosec return h.outer.Sum(in[:origLen]) } func (h *hmac) Write(p []byte) (n int, err error) { return h.inner.Write(p) } func (h *hmac) Size() int { return h.outer.Size() } func (h *hmac) BlockSize() int { return h.inner.BlockSize() } func (h *hmac) Reset() { if h.marshaled { if err := h.inner.(marshalable).UnmarshalBinary(h.ipad); err != nil { //nolint:forcetypeassert panic(err) //nolint } return } h.inner.Reset() h.inner.Write(h.ipad) //nolint:errcheck,gosec // If the underlying hash is marshalable, we can save some time by // saving a copy of the hash state now, and restoring it on future // calls to Reset and Sum instead of writing ipad/opad every time. // // If either hash is unmarshalable for whatever reason, // it's safe to bail out here. marshalableInner, innerOK := h.inner.(marshalable) if !innerOK { return } marshalableOuter, outerOK := h.outer.(marshalable) if !outerOK { return } imarshal, err := marshalableInner.MarshalBinary() if err != nil { return } h.outer.Reset() h.outer.Write(h.opad) //nolint:errcheck,gosec omarshal, err := marshalableOuter.MarshalBinary() if err != nil { return } // Marshaling succeeded; save the marshaled state for later h.ipad = imarshal h.opad = omarshal h.marshaled = true } // New returns a new HMAC hash using the given hash.Hash type and key. // Note that unlike other hash implementations in the standard library, // the returned Hash does not implement encoding.BinaryMarshaler // or encoding.BinaryUnmarshaler. func New(h func() hash.Hash, key []byte) hash.Hash { hm := new(hmac) hm.outer = h() hm.inner = h() blocksize := hm.inner.BlockSize() hm.ipad = make([]byte, blocksize) hm.opad = make([]byte, blocksize) if len(key) > blocksize { // If key is too big, hash it. hm.outer.Write(key) //nolint:errcheck,gosec key = hm.outer.Sum(nil) } copy(hm.ipad, key) copy(hm.opad, key) for i := range hm.ipad { hm.ipad[i] ^= 0x36 } for i := range hm.opad { hm.opad[i] ^= 0x5c } hm.inner.Write(hm.ipad) //nolint:errcheck,gosec return hm } // Equal compares two MACs for equality without leaking timing information. func Equal(mac1, mac2 []byte) bool { // We don't have to be constant time if the lengths of the MACs are // different as that suggests that a completely different hash function // was used. return subtle.ConstantTimeCompare(mac1, mac2) == 1 } stun-0.6.1/internal/hmac/hmac_test.go000066400000000000000000000501031444552150000175300ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2009 The Go Authors. All rights reserved. // SPDX-License-Identifier: BSD-3-Clause package hmac import ( //nolint:gci "crypto/md5" //nolint:gosec "crypto/sha1" //nolint:gosec "crypto/sha256" "crypto/sha512" "fmt" "hash" "testing" ) type hmacTest struct { hash func() hash.Hash key []byte in []byte out string size int blocksize int } func hmacTests() []hmacTest { return []hmacTest{ // Tests from US FIPS 198 // https://csrc.nist.gov/publications/fips/fips198/fips-198a.pdf { sha1.New, []byte{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, }, []byte("Sample #1"), "4f4ca3d5d68ba7cc0a1208c9c61e9c5da0403c0a", sha1.Size, sha1.BlockSize, }, { sha1.New, []byte{ 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, 0x40, 0x41, 0x42, 0x43, }, []byte("Sample #2"), "0922d3405faa3d194f82a45830737d5cc6c75d24", sha1.Size, sha1.BlockSize, }, { sha1.New, []byte{ 0x50, 0x51, 0x52, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f, 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, 0x8a, 0x8b, 0x8c, 0x8d, 0x8e, 0x8f, 0x90, 0x91, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, 0x99, 0x9a, 0x9b, 0x9c, 0x9d, 0x9e, 0x9f, 0xa0, 0xa1, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xab, 0xac, 0xad, 0xae, 0xaf, 0xb0, 0xb1, 0xb2, 0xb3, }, []byte("Sample #3"), "bcf41eab8bb2d802f3d05caf7cb092ecf8d1a3aa", sha1.Size, sha1.BlockSize, }, // Test from Plan 9. { md5.New, []byte("Jefe"), []byte("what do ya want for nothing?"), "750c783e6ab0b503eaa86e310a5db738", md5.Size, md5.BlockSize, }, // Tests from RFC 4231 { sha256.New, []byte{ 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, 0x0b, }, []byte("Hi There"), "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", sha256.Size, sha256.BlockSize, }, { sha256.New, []byte("Jefe"), []byte("what do ya want for nothing?"), "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", sha256.Size, sha256.BlockSize, }, { sha256.New, []byte{ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, }, []byte{ 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, 0xdd, }, "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe", sha256.Size, sha256.BlockSize, }, { sha256.New, []byte{ 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, }, []byte{ 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, 0xcd, }, "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b", sha256.Size, sha256.BlockSize, }, { sha256.New, []byte{ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, }, []byte("Test Using Larger Than Block-Size Key - Hash Key First"), "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54", sha256.Size, sha256.BlockSize, }, { sha256.New, []byte{ 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, 0xaa, }, []byte("This is a test using a larger than block-size key " + "and a larger than block-size data. The key needs to " + "be hashed before being used by the HMAC algorithm."), "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2", sha256.Size, sha256.BlockSize, }, // Tests from https://csrc.nist.gov/groups/ST/toolkit/examples.html // (truncated tag tests are left out) { sha1.New, []byte{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0x3e, 0x3f, }, []byte("Sample message for keylen=blocklen"), "5fd596ee78d5553c8ff4e72d266dfd192366da29", sha1.Size, sha1.BlockSize, }, { sha1.New, []byte{ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, }, []byte("Sample message for keylen // SPDX-License-Identifier: MIT package hmac import ( //nolint:gci "crypto/sha1" //nolint:gosec "crypto/sha256" "hash" "sync" ) func (h *hmac) resetTo(key []byte) { h.outer.Reset() h.inner.Reset() blocksize := h.inner.BlockSize() // Reset size and zero of ipad and opad. h.ipad = append(h.ipad[:0], make([]byte, blocksize)...) h.opad = append(h.opad[:0], make([]byte, blocksize)...) if len(key) > blocksize { // If key is too big, hash it. h.outer.Write(key) //nolint:errcheck,gosec key = h.outer.Sum(nil) } copy(h.ipad, key) copy(h.opad, key) for i := range h.ipad { h.ipad[i] ^= 0x36 } for i := range h.opad { h.opad[i] ^= 0x5c } h.inner.Write(h.ipad) //nolint:errcheck,gosec h.marshaled = false } var hmacSHA1Pool = &sync.Pool{ //nolint:gochecknoglobals New: func() interface{} { h := New(sha1.New, make([]byte, sha1.BlockSize)) return h }, } // AcquireSHA1 returns new HMAC from pool. func AcquireSHA1(key []byte) hash.Hash { h := hmacSHA1Pool.Get().(*hmac) //nolint:forcetypeassert assertHMACSize(h, sha1.Size, sha1.BlockSize) h.resetTo(key) return h } // PutSHA1 puts h to pool. func PutSHA1(h hash.Hash) { hm := h.(*hmac) //nolint:forcetypeassert assertHMACSize(hm, sha1.Size, sha1.BlockSize) hmacSHA1Pool.Put(hm) } var hmacSHA256Pool = &sync.Pool{ //nolint:gochecknoglobals New: func() interface{} { h := New(sha256.New, make([]byte, sha256.BlockSize)) return h }, } // AcquireSHA256 returns new HMAC from SHA256 pool. func AcquireSHA256(key []byte) hash.Hash { h := hmacSHA256Pool.Get().(*hmac) //nolint:forcetypeassert assertHMACSize(h, sha256.Size, sha256.BlockSize) h.resetTo(key) return h } // PutSHA256 puts h to SHA256 pool. func PutSHA256(h hash.Hash) { hm := h.(*hmac) //nolint:forcetypeassert assertHMACSize(hm, sha256.Size, sha256.BlockSize) hmacSHA256Pool.Put(hm) } // assertHMACSize panics if h.size != size or h.blocksize != blocksize. // // Put and Acquire functions are internal functions to project, so // checking it via such assert is optimal. func assertHMACSize(h *hmac, size, blocksize int) { //nolint:unparam if h.Size() != size || h.BlockSize() != blocksize { panic("BUG: hmac size invalid") //nolint } } stun-0.6.1/internal/hmac/pool_test.go000066400000000000000000000075131444552150000176000ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package hmac import ( //nolint:gci "crypto/sha1" //nolint:gosec "crypto/sha256" "fmt" "testing" ) func BenchmarkHMACSHA1_512(b *testing.B) { key := make([]byte, 32) buf := make([]byte, 512) b.ReportAllocs() h := AcquireSHA1(key) b.SetBytes(int64(len(buf))) for i := 0; i < b.N; i++ { h.Write(buf) //nolint:errcheck,gosec h.Reset() mac := h.Sum(nil) buf[0] = mac[0] } } func BenchmarkHMACSHA1_512_Pool(b *testing.B) { key := make([]byte, 32) buf := make([]byte, 512) tBuf := make([]byte, 0, 512) b.ReportAllocs() b.SetBytes(int64(len(buf))) for i := 0; i < b.N; i++ { h := AcquireSHA1(key) h.Write(buf) //nolint:errcheck,gosec h.Reset() mac := h.Sum(tBuf) buf[0] = mac[0] PutSHA1(h) } } func TestHMACReset(t *testing.T) { for i, tt := range hmacTests() { h := New(tt.hash, tt.key) h.(*hmac).resetTo(tt.key) //nolint:forcetypeassert if s := h.Size(); s != tt.size { t.Errorf("Size: got %v, want %v", s, tt.size) } if b := h.BlockSize(); b != tt.blocksize { t.Errorf("BlockSize: got %v, want %v", b, tt.blocksize) } for j := 0; j < 2; j++ { n, err := h.Write(tt.in) if n != len(tt.in) || err != nil { t.Errorf("test %d.%d: Write(%d) = %d, %v", i, j, len(tt.in), n, err) continue } // Repetitive Sum() calls should return the same value for k := 0; k < 2; k++ { sum := fmt.Sprintf("%x", h.Sum(nil)) if sum != tt.out { t.Errorf("test %d.%d.%d: have %s want %s", i, j, k, sum, tt.out) } } // Second iteration: make sure reset works. h.Reset() } } } func TestHMACPool_SHA1(t *testing.T) { //nolint:dupl for i, tt := range hmacTests() { if tt.blocksize != sha1.BlockSize || tt.size != sha1.Size { continue } h := AcquireSHA1(tt.key) if s := h.Size(); s != tt.size { t.Errorf("Size: got %v, want %v", s, tt.size) } if b := h.BlockSize(); b != tt.blocksize { t.Errorf("BlockSize: got %v, want %v", b, tt.blocksize) } for j := 0; j < 2; j++ { n, err := h.Write(tt.in) if n != len(tt.in) || err != nil { t.Errorf("test %d.%d: Write(%d) = %d, %v", i, j, len(tt.in), n, err) continue } // Repetitive Sum() calls should return the same value for k := 0; k < 2; k++ { sum := fmt.Sprintf("%x", h.Sum(nil)) if sum != tt.out { t.Errorf("test %d.%d.%d: have %s want %s", i, j, k, sum, tt.out) } } // Second iteration: make sure reset works. h.Reset() } PutSHA1(h) } } func TestHMACPool_SHA256(t *testing.T) { //nolint:dupl for i, tt := range hmacTests() { if tt.blocksize != sha256.BlockSize || tt.size != sha256.Size { continue } h := AcquireSHA256(tt.key) if s := h.Size(); s != tt.size { t.Errorf("Size: got %v, want %v", s, tt.size) } if b := h.BlockSize(); b != tt.blocksize { t.Errorf("BlockSize: got %v, want %v", b, tt.blocksize) } for j := 0; j < 2; j++ { n, err := h.Write(tt.in) if n != len(tt.in) || err != nil { t.Errorf("test %d.%d: Write(%d) = %d, %v", i, j, len(tt.in), n, err) continue } // Repetitive Sum() calls should return the same value for k := 0; k < 2; k++ { sum := fmt.Sprintf("%x", h.Sum(nil)) if sum != tt.out { t.Errorf("test %d.%d.%d: have %s want %s", i, j, k, sum, tt.out) } } // Second iteration: make sure reset works. h.Reset() } PutSHA256(h) } } func TestAssertBlockSize(t *testing.T) { t.Run("Positive", func(t *testing.T) { h := AcquireSHA1(make([]byte, 0, 1024)) assertHMACSize(h.(*hmac), sha1.Size, sha1.BlockSize) //nolint:forcetypeassert }) t.Run("Negative", func(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("should panic") } }() h := AcquireSHA256(make([]byte, 0, 1024)) assertHMACSize(h.(*hmac), sha1.Size, sha1.BlockSize) //nolint:forcetypeassert }) } stun-0.6.1/internal/hmac/vendor.sh000077500000000000000000000003061444552150000170660ustar00rootroot00000000000000#!/bin/env bash # SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: MIT cp -v $GOROOT/src/crypto/hmac/{hmac,hmac_test}.go . git diff {hmac,hmac_test}.go stun-0.6.1/internal/testutil/000077500000000000000000000000001444552150000162005ustar00rootroot00000000000000stun-0.6.1/internal/testutil/allocs.go000066400000000000000000000007161444552150000200100ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package testutil contains helpers and utilities for writing tests package testutil import ( "testing" ) // ShouldNotAllocate fails if f allocates. func ShouldNotAllocate(t *testing.T, f func()) { if Race { t.Skip("skip while running with -race") return } if a := testing.AllocsPerRun(10, f); a > 0 { t.Errorf("Allocations detected: %f", a) } } stun-0.6.1/internal/testutil/norace.go000066400000000000000000000003361444552150000200000ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !race // +build !race package testutil // Race reports if the race detector is enabled. const Race = false stun-0.6.1/internal/testutil/race.go000066400000000000000000000003331444552150000174400ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build race // +build race package testutil // Race reports if the race detector is enabled. const Race = true stun-0.6.1/message.go000066400000000000000000000415571444552150000144760ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "crypto/rand" "encoding/base64" "errors" "fmt" "io" ) const ( // magicCookie is fixed value that aids in distinguishing STUN packets // from packets of other protocols when STUN is multiplexed with those // other protocols on the same Port. // // The magic cookie field MUST contain the fixed value 0x2112A442 in // network byte order. // // Defined in "STUN Message Structure", section 6. magicCookie = 0x2112A442 attributeHeaderSize = 4 messageHeaderSize = 20 // TransactionIDSize is length of transaction id array (in bytes). TransactionIDSize = 12 // 96 bit ) // NewTransactionID returns new random transaction ID using crypto/rand // as source. func NewTransactionID() (b [TransactionIDSize]byte) { readFullOrPanic(rand.Reader, b[:]) return b } // IsMessage returns true if b looks like STUN message. // Useful for multiplexing. IsMessage does not guarantee // that decoding will be successful. func IsMessage(b []byte) bool { return len(b) >= messageHeaderSize && bin.Uint32(b[4:8]) == magicCookie } // New returns *Message with pre-allocated Raw. func New() *Message { const defaultRawCapacity = 120 return &Message{ Raw: make([]byte, messageHeaderSize, defaultRawCapacity), } } // ErrDecodeToNil occurs on Decode(data, nil) call. var ErrDecodeToNil = errors.New("attempt to decode to nil message") // Decode decodes Message from data to m, returning error if any. func Decode(data []byte, m *Message) error { if m == nil { return ErrDecodeToNil } m.Raw = append(m.Raw[:0], data...) return m.Decode() } // Message represents a single STUN packet. It uses aggressive internal // buffering to enable zero-allocation encoding and decoding, // so there are some usage constraints: // // Message, its fields, results of m.Get or any attribute a.GetFrom // are valid only until Message.Raw is not modified. type Message struct { Type MessageType Length uint32 // len(Raw) not including header TransactionID [TransactionIDSize]byte Attributes Attributes Raw []byte } // MarshalBinary implements the encoding.BinaryMarshaler interface. func (m Message) MarshalBinary() (data []byte, err error) { // We can't return m.Raw, allocation is expected by implicit interface // contract induced by other implementations. b := make([]byte, len(m.Raw)) copy(b, m.Raw) return b, nil } // UnmarshalBinary implements the encoding.BinaryUnmarshaler interface. func (m *Message) UnmarshalBinary(data []byte) error { // We can't retain data, copy is expected by interface contract. m.Raw = append(m.Raw[:0], data...) return m.Decode() } // GobEncode implements the gob.GobEncoder interface. func (m Message) GobEncode() ([]byte, error) { return m.MarshalBinary() } // GobDecode implements the gob.GobDecoder interface. func (m *Message) GobDecode(data []byte) error { return m.UnmarshalBinary(data) } // AddTo sets b.TransactionID to m.TransactionID. // // Implements Setter to aid in crafting responses. func (m *Message) AddTo(b *Message) error { b.TransactionID = m.TransactionID b.WriteTransactionID() return nil } // NewTransactionID sets m.TransactionID to random value from crypto/rand // and returns error if any. func (m *Message) NewTransactionID() error { _, err := io.ReadFull(rand.Reader, m.TransactionID[:]) if err == nil { m.WriteTransactionID() } return err } func (m *Message) String() string { tID := base64.StdEncoding.EncodeToString(m.TransactionID[:]) aInfo := "" for k, a := range m.Attributes { aInfo += fmt.Sprintf("attr%d=%s ", k, a.Type) } return fmt.Sprintf("%s l=%d attrs=%d id=%s, %s", m.Type, m.Length, len(m.Attributes), tID, aInfo) } // Reset resets Message, attributes and underlying buffer length. func (m *Message) Reset() { m.Raw = m.Raw[:0] m.Length = 0 m.Attributes = m.Attributes[:0] } // grow ensures that internal buffer has n length. func (m *Message) grow(n int) { if len(m.Raw) >= n { return } if cap(m.Raw) >= n { m.Raw = m.Raw[:n] return } m.Raw = append(m.Raw, make([]byte, n-len(m.Raw))...) } // Add appends new attribute to message. Not goroutine-safe. // // Value of attribute is copied to internal buffer so // it is safe to reuse v. func (m *Message) Add(t AttrType, v []byte) { // Allocating buffer for TLV (type-length-value). // T = t, L = len(v), V = v. // m.Raw will look like: // [0:20] <- message header // [20:20+m.Length] <- existing message attributes // [20+m.Length:20+m.Length+len(v) + 4] <- allocated buffer for new TLV // [first:last] <- same as previous // [0 1|2 3|4 4 + len(v)] <- mapping for allocated buffer // T L V allocSize := attributeHeaderSize + len(v) // ~ len(TLV) = len(TL) + len(V) first := messageHeaderSize + int(m.Length) // first byte number last := first + allocSize // last byte number m.grow(last) // growing cap(Raw) to fit TLV m.Raw = m.Raw[:last] // now len(Raw) = last m.Length += uint32(allocSize) // rendering length change // Sub-slicing internal buffer to simplify encoding. buf := m.Raw[first:last] // slice for TLV value := buf[attributeHeaderSize:] // slice for V attr := RawAttribute{ Type: t, // T Length: uint16(len(v)), // L Value: value, // V } // Encoding attribute TLV to allocated buffer. bin.PutUint16(buf[0:2], attr.Type.Value()) // T bin.PutUint16(buf[2:4], attr.Length) // L copy(value, v) // V // Checking that attribute value needs padding. if attr.Length%padding != 0 { // Performing padding. bytesToAdd := nearestPaddedValueLength(len(v)) - len(v) last += bytesToAdd m.grow(last) // setting all padding bytes to zero // to prevent data leak from previous // data in next bytesToAdd bytes buf = m.Raw[last-bytesToAdd : last] for i := range buf { buf[i] = 0 } m.Raw = m.Raw[:last] // increasing buffer length m.Length += uint32(bytesToAdd) // rendering length change } m.Attributes = append(m.Attributes, attr) m.WriteLength() } func attrSliceEqual(a, b Attributes) bool { for _, attr := range a { found := false for _, attrB := range b { if attrB.Type != attr.Type { continue } if attrB.Equal(attr) { found = true break } } if !found { return false } } return true } func attrEqual(a, b Attributes) bool { if a == nil && b == nil { return true } if a == nil || b == nil { return false } if len(a) != len(b) { return false } if !attrSliceEqual(a, b) { return false } if !attrSliceEqual(b, a) { return false } return true } // Equal returns true if Message b equals to m. // Ignores m.Raw. func (m *Message) Equal(b *Message) bool { if m == nil && b == nil { return true } if m == nil || b == nil { return false } if m.Type != b.Type { return false } if m.TransactionID != b.TransactionID { return false } if m.Length != b.Length { return false } if !attrEqual(m.Attributes, b.Attributes) { return false } return true } // WriteLength writes m.Length to m.Raw. func (m *Message) WriteLength() { m.grow(4) bin.PutUint16(m.Raw[2:4], uint16(m.Length)) } // WriteHeader writes header to underlying buffer. Not goroutine-safe. func (m *Message) WriteHeader() { m.grow(messageHeaderSize) _ = m.Raw[:messageHeaderSize] // early bounds check to guarantee safety of writes below m.WriteType() m.WriteLength() bin.PutUint32(m.Raw[4:8], magicCookie) // magic cookie copy(m.Raw[8:messageHeaderSize], m.TransactionID[:]) // transaction ID } // WriteTransactionID writes m.TransactionID to m.Raw. func (m *Message) WriteTransactionID() { copy(m.Raw[8:messageHeaderSize], m.TransactionID[:]) // transaction ID } // WriteAttributes encodes all m.Attributes to m. func (m *Message) WriteAttributes() { attributes := m.Attributes m.Attributes = attributes[:0] for _, a := range attributes { m.Add(a.Type, a.Value) } m.Attributes = attributes } // WriteType writes m.Type to m.Raw. func (m *Message) WriteType() { m.grow(2) bin.PutUint16(m.Raw[0:2], m.Type.Value()) // message type } // SetType sets m.Type and writes it to m.Raw. func (m *Message) SetType(t MessageType) { m.Type = t m.WriteType() } // Encode re-encodes message into m.Raw. func (m *Message) Encode() { m.Raw = m.Raw[:0] m.WriteHeader() m.Length = 0 m.WriteAttributes() } // WriteTo implements WriterTo via calling Write(m.Raw) on w and returning // call result. func (m *Message) WriteTo(w io.Writer) (int64, error) { n, err := w.Write(m.Raw) return int64(n), err } // ReadFrom implements ReaderFrom. Reads message from r into m.Raw, // Decodes it and return error if any. If m.Raw is too small, will return // ErrUnexpectedEOF, ErrUnexpectedHeaderEOF or *DecodeErr. // // Can return *DecodeErr while decoding too. func (m *Message) ReadFrom(r io.Reader) (int64, error) { tBuf := m.Raw[:cap(m.Raw)] var ( n int err error ) if n, err = r.Read(tBuf); err != nil { return int64(n), err } m.Raw = tBuf[:n] return int64(n), m.Decode() } // ErrUnexpectedHeaderEOF means that there were not enough bytes in // m.Raw to read header. var ErrUnexpectedHeaderEOF = errors.New("unexpected EOF: not enough bytes to read header") // Decode decodes m.Raw into m. func (m *Message) Decode() error { // decoding message header buf := m.Raw if len(buf) < messageHeaderSize { return ErrUnexpectedHeaderEOF } var ( t = bin.Uint16(buf[0:2]) // first 2 bytes size = int(bin.Uint16(buf[2:4])) // second 2 bytes cookie = bin.Uint32(buf[4:8]) // last 4 bytes fullSize = messageHeaderSize + size // len(m.Raw) ) if cookie != magicCookie { msg := fmt.Sprintf("%x is invalid magic cookie (should be %x)", cookie, magicCookie) return newDecodeErr("message", "cookie", msg) } if len(buf) < fullSize { msg := fmt.Sprintf("buffer length %d is less than %d (expected message size)", len(buf), fullSize) return newAttrDecodeErr("message", msg) } // saving header data m.Type.ReadValue(t) m.Length = uint32(size) copy(m.TransactionID[:], buf[8:messageHeaderSize]) m.Attributes = m.Attributes[:0] var ( offset = 0 b = buf[messageHeaderSize:fullSize] ) for offset < size { // checking that we have enough bytes to read header if len(b) < attributeHeaderSize { msg := fmt.Sprintf("buffer length %d is less than %d (expected header size)", len(b), attributeHeaderSize) return newAttrDecodeErr("header", msg) } var ( a = RawAttribute{ Type: compatAttrType(bin.Uint16(b[0:2])), // first 2 bytes Length: bin.Uint16(b[2:4]), // second 2 bytes } aL = int(a.Length) // attribute length aBuffL = nearestPaddedValueLength(aL) // expected buffer length (with padding) ) b = b[attributeHeaderSize:] // slicing again to simplify value read offset += attributeHeaderSize if len(b) < aBuffL { // checking size msg := fmt.Sprintf("buffer length %d is less than %d (expected value size for %s)", len(b), aBuffL, a.Type) return newAttrDecodeErr("value", msg) } a.Value = b[:aL] offset += aBuffL b = b[aBuffL:] m.Attributes = append(m.Attributes, a) } return nil } // Write decodes message and return error if any. // // Any error is unrecoverable, but message could be partially decoded. func (m *Message) Write(tBuf []byte) (int, error) { m.Raw = append(m.Raw[:0], tBuf...) return len(tBuf), m.Decode() } // CloneTo clones m to b securing any further m mutations. func (m *Message) CloneTo(b *Message) error { b.Raw = append(b.Raw[:0], m.Raw...) return b.Decode() } // MessageClass is 8-bit representation of 2-bit class of STUN Message Class. type MessageClass byte // Possible values for message class in STUN Message Type. const ( ClassRequest MessageClass = 0x00 // 0b00 ClassIndication MessageClass = 0x01 // 0b01 ClassSuccessResponse MessageClass = 0x02 // 0b10 ClassErrorResponse MessageClass = 0x03 // 0b11 ) // Common STUN message types. var ( // Binding request message type. BindingRequest = NewType(MethodBinding, ClassRequest) //nolint:gochecknoglobals // Binding success response message type BindingSuccess = NewType(MethodBinding, ClassSuccessResponse) //nolint:gochecknoglobals // Binding error response message type. BindingError = NewType(MethodBinding, ClassErrorResponse) //nolint:gochecknoglobals ) func (c MessageClass) String() string { switch c { case ClassRequest: return "request" case ClassIndication: return "indication" case ClassSuccessResponse: return "success response" case ClassErrorResponse: return "error response" default: panic("unknown message class") //nolint } } // Method is uint16 representation of 12-bit STUN method. type Method uint16 // Possible methods for STUN Message. const ( MethodBinding Method = 0x001 MethodAllocate Method = 0x003 MethodRefresh Method = 0x004 MethodSend Method = 0x006 MethodData Method = 0x007 MethodCreatePermission Method = 0x008 MethodChannelBind Method = 0x009 ) // Methods from RFC 6062. const ( MethodConnect Method = 0x000a MethodConnectionBind Method = 0x000b MethodConnectionAttempt Method = 0x000c ) func methodName() map[Method]string { return map[Method]string{ MethodBinding: "Binding", MethodAllocate: "Allocate", MethodRefresh: "Refresh", MethodSend: "Send", MethodData: "Data", MethodCreatePermission: "CreatePermission", MethodChannelBind: "ChannelBind", // RFC 6062. MethodConnect: "Connect", MethodConnectionBind: "ConnectionBind", MethodConnectionAttempt: "ConnectionAttempt", } } func (m Method) String() string { s, ok := methodName()[m] if !ok { // Falling back to hex representation. s = fmt.Sprintf("0x%x", uint16(m)) } return s } // MessageType is STUN Message Type Field. type MessageType struct { Method Method // e.g. binding Class MessageClass // e.g. request } // AddTo sets m type to t. func (t MessageType) AddTo(m *Message) error { m.SetType(t) return nil } // NewType returns new message type with provided method and class. func NewType(method Method, class MessageClass) MessageType { return MessageType{ Method: method, Class: class, } } const ( methodABits = 0xf // 0b0000000000001111 methodBBits = 0x70 // 0b0000000001110000 methodDBits = 0xf80 // 0b0000111110000000 methodBShift = 1 methodDShift = 2 firstBit = 0x1 secondBit = 0x2 c0Bit = firstBit c1Bit = secondBit classC0Shift = 4 classC1Shift = 7 ) // Value returns bit representation of messageType. func (t MessageType) Value() uint16 { // 0 1 // 2 3 4 5 6 7 8 9 0 1 2 3 4 5 // +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ // |M |M |M|M|M|C|M|M|M|C|M|M|M|M| // |11|10|9|8|7|1|6|5|4|0|3|2|1|0| // +--+--+-+-+-+-+-+-+-+-+-+-+-+-+ // Figure 3: Format of STUN Message Type Field // Warning: Abandon all hope ye who enter here. // Splitting M into A(M0-M3), B(M4-M6), D(M7-M11). m := uint16(t.Method) a := m & methodABits // A = M * 0b0000000000001111 (right 4 bits) b := m & methodBBits // B = M * 0b0000000001110000 (3 bits after A) d := m & methodDBits // D = M * 0b0000111110000000 (5 bits after B) // Shifting to add "holes" for C0 (at 4 bit) and C1 (8 bit). m = a + (b << methodBShift) + (d << methodDShift) // C0 is zero bit of C, C1 is first bit. // C0 = C * 0b01, C1 = (C * 0b10) >> 1 // Ct = C0 << 4 + C1 << 8. // Optimizations: "((C * 0b10) >> 1) << 8" as "(C * 0b10) << 7" // We need C0 shifted by 4, and C1 by 8 to fit "11" and "7" positions // (see figure 3). c := uint16(t.Class) c0 := (c & c0Bit) << classC0Shift c1 := (c & c1Bit) << classC1Shift class := c0 + c1 return m + class } // ReadValue decodes uint16 into MessageType. func (t *MessageType) ReadValue(v uint16) { // Decoding class. // We are taking first bit from v >> 4 and second from v >> 7. c0 := (v >> classC0Shift) & c0Bit c1 := (v >> classC1Shift) & c1Bit class := c0 + c1 t.Class = MessageClass(class) // Decoding method. a := v & methodABits // A(M0-M3) b := (v >> methodBShift) & methodBBits // B(M4-M6) d := (v >> methodDShift) & methodDBits // D(M7-M11) m := a + b + d t.Method = Method(m) } func (t MessageType) String() string { return fmt.Sprintf("%s %s", t.Method, t.Class) } // Contains return true if message contain t attribute. func (m *Message) Contains(t AttrType) bool { for _, a := range m.Attributes { if a.Type == t { return true } } return false } type transactionIDValueSetter [TransactionIDSize]byte // NewTransactionIDSetter returns new Setter that sets message transaction id // to provided value. func NewTransactionIDSetter(value [TransactionIDSize]byte) Setter { return transactionIDValueSetter(value) } func (t transactionIDValueSetter) AddTo(m *Message) error { m.TransactionID = t m.WriteTransactionID() return nil } stun-0.6.1/message_test.go000066400000000000000000000606661444552150000155370ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package stun import ( "bytes" "encoding/base64" "encoding/binary" "encoding/csv" "errors" "fmt" "hash/crc64" "io" "io/ioutil" "net" "os" "path/filepath" "strconv" "strings" "testing" ) type attributeEncoder interface { AddTo(m *Message) error } func addAttr(t testing.TB, m *Message, a attributeEncoder) { if err := a.AddTo(m); err != nil { t.Error(err) } } func bUint16(v uint16) string { return fmt.Sprintf("0b%016s", strconv.FormatUint(uint64(v), 2)) } func (m *Message) reader() *bytes.Reader { return bytes.NewReader(m.Raw) } func TestMessageBuffer(t *testing.T) { m := New() m.Type = MessageType{Method: MethodBinding, Class: ClassRequest} m.TransactionID = NewTransactionID() m.Add(AttrErrorCode, []byte{0xff, 0xfe, 0xfa}) m.WriteHeader() mDecoded := New() if _, err := mDecoded.ReadFrom(bytes.NewReader(m.Raw)); err != nil { t.Error(err) } if !mDecoded.Equal(m) { t.Error(mDecoded, "!", m) } } func BenchmarkMessage_Write(b *testing.B) { b.ReportAllocs() attributeValue := []byte{0xff, 0x11, 0x12, 0x34} b.SetBytes(int64(len(attributeValue) + messageHeaderSize + attributeHeaderSize)) transactionID := NewTransactionID() m := New() for i := 0; i < b.N; i++ { m.Add(AttrErrorCode, attributeValue) m.TransactionID = transactionID m.Type = MessageType{Method: MethodBinding, Class: ClassRequest} m.WriteHeader() m.Reset() } } func TestMessageType_Value(t *testing.T) { tests := []struct { in MessageType out uint16 }{ {MessageType{Method: MethodBinding, Class: ClassRequest}, 0x0001}, {MessageType{Method: MethodBinding, Class: ClassSuccessResponse}, 0x0101}, {MessageType{Method: MethodBinding, Class: ClassErrorResponse}, 0x0111}, {MessageType{Method: 0xb6d, Class: 0x3}, 0x2ddd}, } for _, tt := range tests { b := tt.in.Value() if b != tt.out { t.Errorf("Value(%s) -> %s, want %s", tt.in, bUint16(b), bUint16(tt.out)) } } } func TestMessageType_ReadValue(t *testing.T) { tests := []struct { in uint16 out MessageType }{ {0x0001, MessageType{Method: MethodBinding, Class: ClassRequest}}, {0x0101, MessageType{Method: MethodBinding, Class: ClassSuccessResponse}}, {0x0111, MessageType{Method: MethodBinding, Class: ClassErrorResponse}}, } for _, tt := range tests { m := MessageType{} m.ReadValue(tt.in) if m != tt.out { t.Errorf("ReadValue(%s) -> %s, want %s", bUint16(tt.in), m, tt.out) } } } func TestMessageType_ReadWriteValue(t *testing.T) { tests := []MessageType{ {Method: MethodBinding, Class: ClassRequest}, {Method: MethodBinding, Class: ClassSuccessResponse}, {Method: MethodBinding, Class: ClassErrorResponse}, {Method: 0x12, Class: ClassErrorResponse}, } for _, tt := range tests { m := MessageType{} v := tt.Value() m.ReadValue(v) if m != tt { t.Errorf("ReadValue(%s -> %s) = %s, should be %s", tt, bUint16(v), m, tt) if m.Method != tt.Method { t.Errorf("%s != %s", bUint16(uint16(m.Method)), bUint16(uint16(tt.Method))) } } } } func TestMessage_WriteTo(t *testing.T) { m := New() m.Type = MessageType{Method: MethodBinding, Class: ClassRequest} m.TransactionID = NewTransactionID() m.Add(AttrErrorCode, []byte{0xff, 0xfe, 0xfa}) m.WriteHeader() buf := new(bytes.Buffer) if _, err := m.WriteTo(buf); err != nil { t.Fatal(err) } mDecoded := New() if _, err := mDecoded.ReadFrom(buf); err != nil { t.Error(err) } if !mDecoded.Equal(m) { t.Error(mDecoded, "!", m) } } func TestMessage_Cookie(t *testing.T) { buf := make([]byte, 20) mDecoded := New() if _, err := mDecoded.ReadFrom(bytes.NewReader(buf)); err == nil { t.Error("should error") } } func TestMessage_LengthLessHeaderSize(t *testing.T) { buf := make([]byte, 8) mDecoded := New() if _, err := mDecoded.ReadFrom(bytes.NewReader(buf)); err == nil { t.Error("should error") } } func TestMessage_BadLength(t *testing.T) { mType := MessageType{Method: MethodBinding, Class: ClassRequest} m := &Message{ Type: mType, Length: 4, TransactionID: [TransactionIDSize]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, } m.Add(0x1, []byte{1, 2}) m.WriteHeader() m.Raw[20+3] = 10 // set attr length = 10 mDecoded := New() if _, err := mDecoded.Write(m.Raw); err == nil { t.Error("should error") } } func TestMessage_AttrLengthLessThanHeader(t *testing.T) { mType := MessageType{Method: MethodBinding, Class: ClassRequest} messageAttribute := RawAttribute{Length: 2, Value: []byte{1, 2}, Type: 0x1} messageAttributes := Attributes{ messageAttribute, } m := &Message{ Type: mType, TransactionID: NewTransactionID(), Attributes: messageAttributes, } m.Encode() mDecoded := New() binary.BigEndian.PutUint16(m.Raw[2:4], 2) // rewrite to bad length _, err := mDecoded.ReadFrom(bytes.NewReader(m.Raw[:20+2])) var e *DecodeErr if errors.As(err, &e) { if !e.IsPlace(DecodeErrPlace{"attribute", "header"}) { t.Error(e, "bad place") } } else { t.Error(err, "should be bad format") } } func TestMessage_AttrSizeLessThanLength(t *testing.T) { mType := MessageType{Method: MethodBinding, Class: ClassRequest} messageAttribute := RawAttribute{ Length: 4, Value: []byte{1, 2, 3, 4}, Type: 0x1, } messageAttributes := Attributes{ messageAttribute, } m := &Message{ Type: mType, TransactionID: NewTransactionID(), Attributes: messageAttributes, } m.WriteAttributes() m.WriteHeader() bin.PutUint16(m.Raw[2:4], 5) // rewrite to bad length mDecoded := New() _, err := mDecoded.ReadFrom(bytes.NewReader(m.Raw[:20+5])) var e *DecodeErr if errors.As(err, &e) { if !e.IsPlace(DecodeErrPlace{"attribute", "value"}) { t.Error(e, "bad place") } } else { t.Error(err, "should be bad format") } } type unexpectedEOFReader struct{} func (r unexpectedEOFReader) Read([]byte) (int, error) { return 0, io.ErrUnexpectedEOF } func TestMessage_ReadFromError(t *testing.T) { mDecoded := New() _, err := mDecoded.ReadFrom(unexpectedEOFReader{}) if !errors.Is(err, io.ErrUnexpectedEOF) { t.Error(err, "should be", io.ErrUnexpectedEOF) } } func BenchmarkMessageType_Value(b *testing.B) { m := MessageType{Method: MethodBinding, Class: ClassRequest} b.ReportAllocs() for i := 0; i < b.N; i++ { m.Value() } } func BenchmarkMessage_WriteTo(b *testing.B) { mType := MessageType{Method: MethodBinding, Class: ClassRequest} m := &Message{ Type: mType, Length: 0, TransactionID: [TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, }, } m.WriteHeader() buf := new(bytes.Buffer) b.ReportAllocs() for i := 0; i < b.N; i++ { m.WriteTo(buf) //nolint:errcheck,gosec buf.Reset() } } func BenchmarkMessage_ReadFrom(b *testing.B) { mType := MessageType{Method: MethodBinding, Class: ClassRequest} m := &Message{ Type: mType, Length: 0, TransactionID: [TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, }, } m.WriteHeader() b.ReportAllocs() b.SetBytes(int64(len(m.Raw))) reader := m.reader() mRec := New() for i := 0; i < b.N; i++ { if _, err := mRec.ReadFrom(reader); err != nil { b.Fatal(err) } reader.Reset(m.Raw) mRec.Reset() } } func BenchmarkMessage_ReadBytes(b *testing.B) { mType := MessageType{Method: MethodBinding, Class: ClassRequest} m := &Message{ Type: mType, Length: 0, TransactionID: [TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, }, } m.WriteHeader() b.ReportAllocs() b.SetBytes(int64(len(m.Raw))) mRec := New() for i := 0; i < b.N; i++ { if _, err := mRec.Write(m.Raw); err != nil { b.Fatal(err) } mRec.Reset() } } func TestMessageClass_String(t *testing.T) { defer func() { if err := recover(); err == nil { t.Error(err, "should be not nil") } }() v := [...]MessageClass{ ClassRequest, ClassErrorResponse, ClassSuccessResponse, ClassIndication, } for _, k := range v { if k.String() == "" { t.Error(k, "bad stringer") } } // should panic p := MessageClass(0x05).String() t.Error("should panic!", p) } func TestAttrType_String(t *testing.T) { v := [...]AttrType{ AttrMappedAddress, AttrUsername, AttrErrorCode, AttrMessageIntegrity, AttrUnknownAttributes, AttrRealm, AttrNonce, AttrXORMappedAddress, AttrSoftware, AttrAlternateServer, AttrFingerprint, } for _, k := range v { if k.String() == "" { t.Error(k, "bad stringer") } if strings.HasPrefix(k.String(), "0x") { t.Error(k, "bad stringer") } } vNonStandard := AttrType(0x512) if !strings.HasPrefix(vNonStandard.String(), "0x512") { t.Error(vNonStandard, "bad prefix") } } func TestMethod_String(t *testing.T) { if MethodBinding.String() != "Binding" { t.Error("binding is not binding!") } if Method(0x616).String() != "0x616" { t.Error("Bad stringer", Method(0x616)) } } func TestAttribute_Equal(t *testing.T) { a := RawAttribute{Length: 2, Value: []byte{0x1, 0x2}} b := RawAttribute{Length: 2, Value: []byte{0x1, 0x2}} if !a.Equal(b) { t.Error("should equal") } if a.Equal(RawAttribute{Type: 0x2}) { t.Error("should not equal") } if a.Equal(RawAttribute{Length: 0x2}) { t.Error("should not equal") } if a.Equal(RawAttribute{Length: 0x3}) { t.Error("should not equal") } if a.Equal(RawAttribute{Length: 2, Value: []byte{0x1, 0x3}}) { t.Error("should not equal") } } func TestMessage_Equal(t *testing.T) { attr := RawAttribute{Length: 2, Value: []byte{0x1, 0x2}, Type: 0x1} attrs := Attributes{attr} a := &Message{Attributes: attrs, Length: 4 + 2} b := &Message{Attributes: attrs, Length: 4 + 2} if !a.Equal(b) { t.Error("should equal") } if a.Equal(&Message{Type: MessageType{Class: 128}}) { t.Error("should not equal") } tID := [TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, } if a.Equal(&Message{TransactionID: tID}) { t.Error("should not equal") } if a.Equal(&Message{Length: 3}) { t.Error("should not equal") } tAttrs := Attributes{ {Length: 1, Value: []byte{0x1}, Type: 0x1}, } if a.Equal(&Message{Attributes: tAttrs, Length: 4 + 2}) { t.Error("should not equal") } tAttrs = Attributes{ {Length: 2, Value: []byte{0x1, 0x1}, Type: 0x2}, } if a.Equal(&Message{Attributes: tAttrs, Length: 4 + 2}) { t.Error("should not equal") } if !(*Message)(nil).Equal(nil) { t.Error("nil should be equal to nil") } if a.Equal(nil) { t.Error("non-nil should not be equal to nil") } t.Run("Nil attributes", func(t *testing.T) { a := &Message{ Attributes: nil, Length: 4 + 2, } b := &Message{ Attributes: attrs, Length: 4 + 2, } if a.Equal(b) { t.Error("should not equal") } if b.Equal(a) { t.Error("should not equal") } b.Attributes = nil if !a.Equal(b) { t.Error("should equal") } }) t.Run("Attributes length", func(t *testing.T) { attr := RawAttribute{Length: 2, Value: []byte{0x1, 0x2}, Type: 0x1} attr1 := RawAttribute{Length: 2, Value: []byte{0x1, 0x2}, Type: 0x1} a := &Message{Attributes: Attributes{attr}, Length: 4 + 2} b := &Message{Attributes: Attributes{attr, attr1}, Length: 4 + 2} if a.Equal(b) { t.Error("should not equal") } }) t.Run("Attributes values", func(t *testing.T) { attr := RawAttribute{Length: 2, Value: []byte{0x1, 0x2}, Type: 0x1} attr1 := RawAttribute{Length: 2, Value: []byte{0x1, 0x1}, Type: 0x1} a := &Message{Attributes: Attributes{attr, attr}, Length: 4 + 2} b := &Message{Attributes: Attributes{attr, attr1}, Length: 4 + 2} if a.Equal(b) { t.Error("should not equal") } }) } func TestMessageGrow(t *testing.T) { m := New() m.grow(512) if len(m.Raw) < 512 { t.Error("Bad length", len(m.Raw)) } } func TestMessageGrowSmaller(t *testing.T) { m := New() m.grow(2) if cap(m.Raw) < 20 { t.Error("Bad capacity", cap(m.Raw)) } if len(m.Raw) < 20 { t.Error("Bad length", len(m.Raw)) } } func TestMessage_String(t *testing.T) { m := New() if m.String() == "" { t.Error("bad string") } } func TestIsMessage(t *testing.T) { m := New() NewSoftware("software").AddTo(m) //nolint:errcheck,gosec m.WriteHeader() tt := [...]struct { in []byte out bool }{ {nil, false}, // 0 {[]byte{1, 2, 3}, false}, // 1 {[]byte{1, 2, 4}, false}, // 2 {[]byte{1, 2, 4, 5, 6, 7, 8, 9, 20}, false}, // 3 {m.Raw, true}, // 5 {[]byte{ 0, 0, 0, 0, 33, 18, 164, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, }, true}, // 6 } for i, v := range tt { if got := IsMessage(v.in); got != v.out { t.Errorf("tt[%d]: IsMessage(%+v) %v != %v", i, v.in, got, v.out) } } } func BenchmarkIsMessage(b *testing.B) { m := New() m.Type = MessageType{Method: MethodBinding, Class: ClassRequest} m.TransactionID = NewTransactionID() NewSoftware("cydev/stun test").AddTo(m) //nolint:errcheck,gosec m.WriteHeader() b.SetBytes(int64(messageHeaderSize)) b.ReportAllocs() for i := 0; i < b.N; i++ { if !IsMessage(m.Raw) { b.Fatal("Should be message") } } } func loadData(tb testing.TB, name string) []byte { name = filepath.Join("testdata", name) f, err := os.Open(name) //nolint:gosec if err != nil { tb.Fatal(err) } defer func() { if errClose := f.Close(); errClose != nil { tb.Fatal(errClose) } }() v, err := ioutil.ReadAll(f) if err != nil { tb.Fatal(err) } return v } func TestExampleChrome(t *testing.T) { buf := loadData(t, "ex1_chrome.stun") m := New() _, err := m.Write(buf) if err != nil { t.Errorf("Failed to parse ex1_chrome: %s", err) } } func TestMessageFromBrowsers(t *testing.T) { // file contains udp-packets captured from browsers (WebRTC) reader := csv.NewReader(bytes.NewReader(loadData(t, "frombrowsers.csv"))) reader.Comment = '#' _, err := reader.Read() // skipping header if err != nil { t.Fatal("failed to skip header of csv: ", err) } crcTable := crc64.MakeTable(crc64.ISO) m := New() for { line, err := reader.Read() if errors.Is(err, io.EOF) { break } if err != nil { t.Fatal("failed to read csv line: ", err) } data, err := base64.StdEncoding.DecodeString(line[1]) if err != nil { t.Fatal("failed to decode ", line[1], " as base64: ", err) } b, err := strconv.ParseUint(line[2], 10, 64) if err != nil { t.Fatal(err) } if b != crc64.Checksum(data, crcTable) { t.Error("crc64 check failed for ", line[1]) } if _, err = m.Write(data); err != nil { t.Error("failed to decode ", line[1], " as message: ", err) } m.Reset() } } func BenchmarkMessage_NewTransactionID(b *testing.B) { b.ReportAllocs() m := new(Message) m.WriteHeader() for i := 0; i < b.N; i++ { if err := m.NewTransactionID(); err != nil { b.Fatal(err) } } } func BenchmarkMessageFull(b *testing.B) { b.ReportAllocs() m := new(Message) s := NewSoftware("software") addr := &XORMappedAddress{ IP: net.IPv4(213, 1, 223, 5), } for i := 0; i < b.N; i++ { if err := addr.AddTo(m); err != nil { b.Fatal(err) } if err := s.AddTo(m); err != nil { b.Fatal(err) } m.WriteAttributes() m.WriteHeader() Fingerprint.AddTo(m) //nolint:errcheck,gosec m.WriteHeader() m.Reset() } } func BenchmarkMessageFullHardcore(b *testing.B) { b.ReportAllocs() m := new(Message) s := NewSoftware("software") addr := &XORMappedAddress{ IP: net.IPv4(213, 1, 223, 5), } for i := 0; i < b.N; i++ { if err := addr.AddTo(m); err != nil { b.Fatal(err) } if err := s.AddTo(m); err != nil { b.Fatal(err) } m.WriteHeader() m.Reset() } } func BenchmarkMessage_WriteHeader(b *testing.B) { m := &Message{ TransactionID: NewTransactionID(), Raw: make([]byte, 120), Type: MessageType{ Class: ClassRequest, Method: MethodBinding, }, } b.ReportAllocs() for i := 0; i < b.N; i++ { m.WriteHeader() } } func TestMessage_Contains(t *testing.T) { m := new(Message) m.Add(AttrSoftware, []byte("value")) if !m.Contains(AttrSoftware) { t.Error("message should contain software") } if m.Contains(AttrNonce) { t.Error("message should not contain nonce") } } func ExampleMessage() { buf := new(bytes.Buffer) m := new(Message) m.Build(BindingRequest, //nolint:errcheck,gosec NewTransactionIDSetter([TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, }), NewSoftware("ernado/stun"), NewLongTermIntegrity("username", "realm", "password"), Fingerprint, ) // Instead of calling Build, use AddTo(m) directly for all setters // to reduce allocations. // For example: // software := NewSoftware("ernado/stun") // software.AddTo(m) // no allocations // Or pass software as follows: // m.Build(&software) // no allocations // If you pass software as value, there will be 1 allocation. // This rule is correct for all setters. fmt.Println(m, "buff length:", len(m.Raw)) n, err := m.WriteTo(buf) fmt.Println("wrote", n, "err", err) // Decoding from buf new *Message. decoded := new(Message) decoded.Raw = make([]byte, 0, 1024) // for ReadFrom that reuses m.Raw // ReadFrom does not allocate internal buffer for reading from io.Reader, // instead it uses m.Raw, expanding it length to capacity. decoded.ReadFrom(buf) //nolint:errcheck,gosec fmt.Println("has software:", decoded.Contains(AttrSoftware)) fmt.Println("has nonce:", decoded.Contains(AttrNonce)) var software Software decoded.Parse(&software) //nolint:errcheck,gosec // Rule for Parse method is same as for Build. fmt.Println("software:", software) if err := Fingerprint.Check(decoded); err == nil { fmt.Println("fingerprint is correct") } else { fmt.Println("fingerprint is incorrect:", err) } // Checking integrity i := NewLongTermIntegrity("username", "realm", "password") if err := i.Check(decoded); err == nil { fmt.Println("integrity ok") } else { fmt.Println("integrity bad:", err) } fmt.Println("for corrupted message:") decoded.Raw[22] = 33 if Fingerprint.Check(decoded) == nil { fmt.Println("fingerprint: ok") } else { fmt.Println("fingerprint: failed") } // Output: // Binding request l=48 attrs=3 id=AQIDBAUGBwgJAAEA, attr0=SOFTWARE attr1=MESSAGE-INTEGRITY attr2=FINGERPRINT buff length: 68 // wrote 68 err // has software: true // has nonce: false // software: ernado/stun // fingerprint is correct // integrity ok // for corrupted message: // fingerprint: failed } func TestAllocations(t *testing.T) { // Not testing AttrMessageIntegrity because it allocates. setters := []Setter{ BindingRequest, TransactionID, Fingerprint, NewNonce("nonce"), NewUsername("username"), XORMappedAddress{ IP: net.IPv4(11, 22, 33, 44), Port: 334, }, UnknownAttributes{AttrLifetime, AttrChannelNumber}, CodeInsufficientCapacity, ErrorCodeAttribute{ Code: 200, Reason: []byte("hello"), }, } m := New() for i, s := range setters { s := s i := i allocs := testing.AllocsPerRun(10, func() { m.Reset() m.WriteHeader() if err := s.AddTo(m); err != nil { t.Errorf("[%d] failed to add", i) } }) if allocs > 0 { t.Errorf("[%d] allocated %.0f", i, allocs) } } } func TestAllocationsGetters(t *testing.T) { // Not testing AttrMessageIntegrity because it allocates. setters := []Setter{ BindingRequest, TransactionID, NewNonce("nonce"), NewUsername("username"), XORMappedAddress{ IP: net.IPv4(11, 22, 33, 44), Port: 334, }, UnknownAttributes{AttrLifetime, AttrChannelNumber}, CodeInsufficientCapacity, ErrorCodeAttribute{ Code: 200, Reason: []byte("hello"), }, NewShortTermIntegrity("pwd"), Fingerprint, } m := New() if err := m.Build(setters...); err != nil { t.Error("failed to build", err) } getters := []Getter{ new(Nonce), new(Username), new(XORMappedAddress), new(UnknownAttributes), new(ErrorCodeAttribute), } for i, g := range getters { g := g i := i allocs := testing.AllocsPerRun(10, func() { if err := g.GetFrom(m); err != nil { t.Errorf("[%d] failed to get", i) } }) if allocs > 0 { t.Errorf("[%d] allocated %.0f", i, allocs) } } } func TestMessageFullSize(t *testing.T) { m := new(Message) if err := m.Build(BindingRequest, NewTransactionIDSetter([TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, }), NewSoftware("pion/stun"), NewLongTermIntegrity("username", "realm", "password"), Fingerprint, ); err != nil { t.Fatal(err) } m.Raw = m.Raw[:len(m.Raw)-10] decoder := new(Message) decoder.Raw = m.Raw[:len(m.Raw)-10] if err := decoder.Decode(); err == nil { t.Error("decode on truncated buffer should error") } } func TestMessage_CloneTo(t *testing.T) { m := new(Message) if err := m.Build(BindingRequest, NewTransactionIDSetter([TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, }), NewSoftware("pion/stun"), NewLongTermIntegrity("username", "realm", "password"), Fingerprint, ); err != nil { t.Fatal(err) } m.Encode() b := new(Message) if err := m.CloneTo(b); err != nil { t.Fatal(err) } if !b.Equal(m) { t.Fatal("not equal") } // Corrupting m and checking that b is not corrupted. s, ok := b.Attributes.Get(AttrSoftware) if !ok { t.Fatal("no software attribute") } s.Value[0] = 'k' if b.Equal(m) { t.Fatal("should not be equal") } } func BenchmarkMessage_CloneTo(b *testing.B) { b.ReportAllocs() m := new(Message) if err := m.Build(BindingRequest, NewTransactionIDSetter([TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, }), NewSoftware("pion/stun"), NewLongTermIntegrity("username", "realm", "password"), Fingerprint, ); err != nil { b.Fatal(err) } b.SetBytes(int64(len(m.Raw))) a := new(Message) m.CloneTo(a) //nolint:errcheck,gosec for i := 0; i < b.N; i++ { if err := m.CloneTo(a); err != nil { b.Fatal(err) } } } func TestMessage_AddTo(t *testing.T) { m := new(Message) if err := m.Build(BindingRequest, NewTransactionIDSetter([TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, }), Fingerprint, ); err != nil { t.Fatal(err) } m.Encode() b := new(Message) if err := m.CloneTo(b); err != nil { t.Fatal(err) } m.TransactionID = [TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 2, } if b.Equal(m) { t.Fatal("should not be equal") } m.AddTo(b) //nolint:errcheck,gosec if !b.Equal(m) { t.Fatal("should be equal") } } func BenchmarkMessage_AddTo(b *testing.B) { b.ReportAllocs() m := new(Message) if err := m.Build(BindingRequest, NewTransactionIDSetter([TransactionIDSize]byte{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 0, 1, }), Fingerprint, ); err != nil { b.Fatal(err) } a := new(Message) m.CloneTo(a) //nolint:errcheck,gosec for i := 0; i < b.N; i++ { if err := m.AddTo(a); err != nil { b.Fatal(err) } } } func TestDecode(t *testing.T) { t.Run("Nil", func(t *testing.T) { if err := Decode(nil, nil); !errors.Is(err, ErrDecodeToNil) { t.Errorf("unexpected error: %v", err) } }) m := New() m.Type = MessageType{Method: MethodBinding, Class: ClassRequest} m.TransactionID = NewTransactionID() m.Add(AttrErrorCode, []byte{0xff, 0xfe, 0xfa}) m.WriteHeader() mDecoded := New() if err := Decode(m.Raw, mDecoded); err != nil { t.Errorf("unexpected error: %v", err) } if !mDecoded.Equal(m) { t.Error("decoded result is not equal to encoded message") } t.Run("ZeroAlloc", func(t *testing.T) { allocs := testing.AllocsPerRun(10, func() { mDecoded.Reset() if err := Decode(m.Raw, mDecoded); err != nil { t.Error(err) } }) if allocs > 0 { t.Error("unexpected allocations") } }) } func BenchmarkDecode(b *testing.B) { m := New() m.Type = MessageType{Method: MethodBinding, Class: ClassRequest} m.TransactionID = NewTransactionID() m.Add(AttrErrorCode, []byte{0xff, 0xfe, 0xfa}) m.WriteHeader() mDecoded := New() b.ResetTimer() b.ReportAllocs() for i := 0; i < b.N; i++ { mDecoded.Reset() if err := Decode(m.Raw, mDecoded); err != nil { b.Fatal(err) } } } func TestMessage_MarshalBinary(t *testing.T) { m := MustBuild( NewSoftware("software"), &XORMappedAddress{ IP: net.IPv4(213, 1, 223, 5), }, ) data, err := m.MarshalBinary() if err != nil { t.Fatal(err) } // Reset m.Raw to check retention. for i := range m.Raw { m.Raw[i] = 0 } if err := m.UnmarshalBinary(data); err != nil { t.Fatal(err) } // Reset data to check retention. for i := range data { data[i] = 0 } if err := m.Decode(); err != nil { t.Fatal(err) } } func TestMessage_GobDecode(t *testing.T) { m := MustBuild( NewSoftware("software"), &XORMappedAddress{ IP: net.IPv4(213, 1, 223, 5), }, ) data, err := m.GobEncode() if err != nil { t.Fatal(err) } // Reset m.Raw to check retention. for i := range m.Raw { m.Raw[i] = 0 } if err := m.GobDecode(data); err != nil { t.Fatal(err) } // Reset data to check retention. for i := range data { data[i] = 0 } if err := m.Decode(); err != nil { t.Fatal(err) } } stun-0.6.1/renovate.json000066400000000000000000000001731444552150000152260ustar00rootroot00000000000000{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "github>pion/renovate-config" ] } stun-0.6.1/rfc5769_test.go000066400000000000000000000122201444552150000151770ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "net" "testing" ) func TestRFC5769(t *testing.T) { // Test Vectors for Session Traversal Utilities for NAT (STUN) // see https://tools.ietf.org/html/rfc5769 t.Run("Request", func(t *testing.T) { m := &Message{ Raw: []byte("\x00\x01\x00\x58" + "\x21\x12\xa4\x42" + "\xb7\xe7\xa7\x01\xbc\x34\xd6\x86\xfa\x87\xdf\xae" + "\x80\x22\x00\x10" + "STUN test client" + "\x00\x24\x00\x04" + "\x6e\x00\x01\xff" + "\x80\x29\x00\x08" + "\x93\x2f\xf9\xb1\x51\x26\x3b\x36" + "\x00\x06\x00\x09" + "\x65\x76\x74\x6a\x3a\x68\x36\x76\x59\x20\x20\x20" + "\x00\x08\x00\x14" + "\x9a\xea\xa7\x0c\xbf\xd8\xcb\x56\x78\x1e\xf2\xb5" + "\xb2\xd3\xf2\x49\xc1\xb5\x71\xa2" + "\x80\x28\x00\x04" + "\xe5\x7a\x3b\xcf", ), } if err := m.Decode(); err != nil { t.Error(err) } software := new(Software) if err := software.GetFrom(m); err != nil { t.Error(err) } if software.String() != "STUN test client" { t.Error("bad software: ", software) } if err := Fingerprint.Check(m); err != nil { t.Error("check failed: ", err) } t.Run("Long-Term credentials", func(t *testing.T) { m := &Message{ Raw: []byte("\x00\x01\x00\x60" + "\x21\x12\xa4\x42" + "\x78\xad\x34\x33\xc6\xad\x72\xc0\x29\xda\x41\x2e" + "\x00\x06\x00\x12" + "\xe3\x83\x9e\xe3\x83\x88\xe3\x83\xaa\xe3\x83\x83" + "\xe3\x82\xaf\xe3\x82\xb9\x00\x00" + "\x00\x15\x00\x1c" + "\x66\x2f\x2f\x34\x39\x39\x6b\x39\x35\x34\x64\x36" + "\x4f\x4c\x33\x34\x6f\x4c\x39\x46\x53\x54\x76\x79" + "\x36\x34\x73\x41" + "\x00\x14\x00\x0b" + "\x65\x78\x61\x6d\x70\x6c\x65\x2e\x6f\x72\x67\x00" + "\x00\x08\x00\x14" + "\xf6\x70\x24\x65\x6d\xd6\x4a\x3e\x02\xb8\xe0\x71" + "\x2e\x85\xc9\xa2\x8c\xa8\x96\x66", ), } if err := m.Decode(); err != nil { t.Error(err) } u := new(Username) if err := u.GetFrom(m); err != nil { t.Error(err) } expectedUsername := "\u30DE\u30C8\u30EA\u30C3\u30AF\u30B9" if u.String() != expectedUsername { t.Errorf("username: %q (got) != %q (exp)", u, expectedUsername) } n := new(Nonce) if err := n.GetFrom(m); err != nil { t.Error(err) } if n.String() != "f//499k954d6OL34oL9FSTvy64sA" { t.Error("bad nonce") } r := new(Realm) if err := r.GetFrom(m); err != nil { t.Error(err) } if r.String() != "example.org" { //nolint:goconst t.Error("bad realm") } // checking HMAC i := NewLongTermIntegrity( "\u30DE\u30C8\u30EA\u30C3\u30AF\u30B9", "example.org", "TheMatrIX", ) if err := i.Check(m); err != nil { t.Error(err) } }) }) t.Run("Response", func(t *testing.T) { t.Run("IPv4", func(t *testing.T) { m := &Message{ Raw: []byte("\x01\x01\x00\x3c" + "\x21\x12\xa4\x42" + "\xb7\xe7\xa7\x01\xbc\x34\xd6\x86\xfa\x87\xdf\xae" + "\x80\x22\x00\x0b" + "\x74\x65\x73\x74\x20\x76\x65\x63\x74\x6f\x72\x20" + "\x00\x20\x00\x08" + "\x00\x01\xa1\x47\xe1\x12\xa6\x43" + "\x00\x08\x00\x14" + "\x2b\x91\xf5\x99\xfd\x9e\x90\xc3\x8c\x74\x89\xf9" + "\x2a\xf9\xba\x53\xf0\x6b\xe7\xd7" + "\x80\x28\x00\x04" + "\xc0\x7d\x4c\x96", ), } if err := m.Decode(); err != nil { t.Error(err) } software := new(Software) if err := software.GetFrom(m); err != nil { t.Error(err) } if software.String() != "test vector" { t.Error("bad software: ", software) } if err := Fingerprint.Check(m); err != nil { t.Error("Check failed: ", err) } addr := new(XORMappedAddress) if err := addr.GetFrom(m); err != nil { t.Error(err) } if !addr.IP.Equal(net.ParseIP("192.0.2.1")) { t.Error("bad IP") } if addr.Port != 32853 { t.Error("bad Port") } if err := Fingerprint.Check(m); err != nil { t.Error("check failed: ", err) } }) t.Run("IPv6", func(t *testing.T) { m := &Message{ Raw: []byte("\x01\x01\x00\x48" + "\x21\x12\xa4\x42" + "\xb7\xe7\xa7\x01\xbc\x34\xd6\x86\xfa\x87\xdf\xae" + "\x80\x22\x00\x0b" + "\x74\x65\x73\x74\x20\x76\x65\x63\x74\x6f\x72\x20" + "\x00\x20\x00\x14" + "\x00\x02\xa1\x47" + "\x01\x13\xa9\xfa\xa5\xd3\xf1\x79" + "\xbc\x25\xf4\xb5\xbe\xd2\xb9\xd9" + "\x00\x08\x00\x14" + "\xa3\x82\x95\x4e\x4b\xe6\x7b\xf1\x17\x84\xc9\x7c" + "\x82\x92\xc2\x75\xbf\xe3\xed\x41" + "\x80\x28\x00\x04" + "\xc8\xfb\x0b\x4c", ), } if err := m.Decode(); err != nil { t.Error(err) } software := new(Software) if err := software.GetFrom(m); err != nil { t.Error(err) } if software.String() != "test vector" { t.Error("bad software: ", software) } if err := Fingerprint.Check(m); err != nil { t.Error("Check failed: ", err) } addr := new(XORMappedAddress) if err := addr.GetFrom(m); err != nil { t.Error(err) } if !addr.IP.Equal(net.ParseIP("2001:db8:1234:5678:11:2233:4455:6677")) { t.Error("bad IP") } if addr.Port != 32853 { t.Error("bad Port") } if err := Fingerprint.Check(m); err != nil { t.Error("check failed: ", err) } }) }) } stun-0.6.1/stun.go000066400000000000000000000025361444552150000140350ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package stun implements Session Traversal Utilities for NAT (STUN) RFC 5389. // // The stun package is intended to use by package that implements extension // to STUN (e.g. TURN) or client/server applications. // // Most methods are designed to be zero allocations. If it is not enough, // low-level methods are available. On other hand, there are helpers that // reduce code repeat. // // See examples for Message for basic usage, or https://github.com/pion/turn // package for example of stun extension implementation. package stun import ( "encoding/binary" "io" ) // bin is shorthand to binary.BigEndian. var bin = binary.BigEndian //nolint:gochecknoglobals func readFullOrPanic(r io.Reader, v []byte) int { n, err := io.ReadFull(r, v) if err != nil { panic(err) //nolint } return n } func writeOrPanic(w io.Writer, v []byte) int { n, err := w.Write(v) if err != nil { panic(err) //nolint } return n } // IANA assigned ports for "stun" protocol. const ( DefaultPort = 3478 DefaultTLSPort = 5349 ) type transactionIDSetter struct{} func (transactionIDSetter) AddTo(m *Message) error { return m.NewTransactionID() } // TransactionID is Setter for m.TransactionID. var TransactionID Setter = transactionIDSetter{} //nolint:gochecknoglobals stun-0.6.1/stun_test.go000066400000000000000000000015301444552150000150650ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "testing" ) type errorReader struct{} var ( errErrorReaderFailedToRead = errors.New("failed to read") errErrorReaderFailedToWrite = errors.New("failed to write") ) func (errorReader) Read([]byte) (int, error) { return 0, errErrorReaderFailedToRead } func TestReadFullHelper(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("should panic") } }() readFullOrPanic(errorReader{}, make([]byte, 1)) } type errorWriter struct{} func (errorWriter) Write([]byte) (int, error) { return 0, errErrorReaderFailedToWrite } func TestWriteHelper(t *testing.T) { defer func() { if r := recover(); r == nil { t.Error("should panic") } }() writeOrPanic(errorWriter{}, make([]byte, 1)) } stun-0.6.1/stuntest/000077500000000000000000000000001444552150000144005ustar00rootroot00000000000000stun-0.6.1/stuntest/udp_server.go000066400000000000000000000034771444552150000171200ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT // Package stuntest contains helpers for testing STUN clients package stuntest import ( "errors" "fmt" "net" "testing" ) var errUDPServerUnsupportedNetwork = errors.New("unsupported network") // NewUDPServer creates an udp server for testing. The supplied handler function will be called with the request // and should be used to emulate the server behavior. func NewUDPServer( t *testing.T, network string, maxMessageSize int, handler func(req []byte) ([]byte, error), ) (net.Addr, func(t *testing.T), error) { var ip string switch network { case "udp4": ip = "127.0.0.1" case "udp6": ip = "[::1]" default: return nil, nil, fmt.Errorf("%w: %s", errUDPServerUnsupportedNetwork, network) } udpConn, err := net.ListenUDP(network, &net.UDPAddr{IP: net.ParseIP(ip), Port: 0}) if err != nil { t.Fatal(err) //nolint:forbidigo } // Necessary for IPv6 address := fmt.Sprintf("%s:%d", ip, udpConn.LocalAddr().(*net.UDPAddr).Port) //nolint:forcetypeassert serverAddr, err := net.ResolveUDPAddr(network, address) if err != nil { return nil, nil, fmt.Errorf("failed to resolve stun host: %s: %w", address, err) } errCh := make(chan error, 1) go func() { for { bs := make([]byte, maxMessageSize) n, addr, err := udpConn.ReadFrom(bs) if err != nil { errCh <- err return } resp, err := handler(bs[:n]) if err != nil { errCh <- err return } _, err = udpConn.WriteTo(resp, addr) if err != nil { errCh <- err return } } }() return serverAddr, func(t *testing.T) { select { case err := <-errCh: if err != nil { t.Fatal(err) //nolint return } default: } err := udpConn.Close() if err != nil { t.Fatal(err) //nolint } <-errCh }, nil } stun-0.6.1/testdata/000077500000000000000000000000001444552150000143205ustar00rootroot00000000000000stun-0.6.1/testdata/README.md000066400000000000000000000002501444552150000155740ustar00rootroot00000000000000# Test data testdata directory contains data that is used as input for tests. Data was gathered from real world implementations of STUN in Firefox and Chrome browsers.stun-0.6.1/testdata/ex1_chrome.stun000066400000000000000000000000601444552150000172610ustar00rootroot00000000000000!¤BdtIhi6vBo93f€/http://localhost:3000/stun-0.6.1/testdata/ex1_chrome.stun.license000066400000000000000000000001421444552150000207030ustar00rootroot00000000000000SPDX-FileCopyrightText: 2023 The Pion community SPDX-License-Identifier: CC0-1.0stun-0.6.1/testdata/frombrowsers.csv000066400000000000000000000031721444552150000175720ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: CC0-1.0 ip,message,crc,browser,version,system 213.87.135.35:5127,AAEAACESpEJaU3lNekUycUIveEc=,14955568470663943731,Chrome,55.0.2883.91,Android 5.1.1 213.87.135.35:35359,AAEACCESpEL/qLJHuDKc5PsGghOAKAAEqgN+GQ==,10276095995276579762,Firefox,50.0,Mobile 185.79.100.8:8337,AAEAACESpEIrY1h5L3p1UytKQkE=,12881067838541718227,Chrome,55.0.2883.91,Android 5.1.1 185.79.100.8:8422,AAEAACESpEI1Y1NTdm9yQ3NOMng=,11436996053979164376,Chrome,55.0.2883.91,Android 5.1.1 185.79.100.8:7986,AAEACCESpEJb5g0r4yyFhiAECkaAKAAEcmtDIA==,4014551455069599512,Firefox,50.0,Mobile 185.79.100.22:13841,AAEAACESpEJzVFJiOEFpdzZkUDM=,17727708163432284316,Chrome,55.0.2883.91,Android 5.1.1 213.87.161.132:10323,AAEAACESpEJUdGs3ckUvQkVwQTg=,4195490818417829438,Chrome,55.0.2883.91,Android 5.1.1 213.87.161.132:49166,AAEAACESpEJBT2J3VXZmVEZGa24=,210244277659580892,Chrome,55.0.2883.91,Android 5.1.1 91.208.134.1:59285,AAEACCESpEL8J+y5nwi+zNxNyuGAKAAEriExcw==,17182568428950801870,Firefox,51.0,Ubuntu 91.208.134.1:35576,AAEACCESpELD+wYQw3maWaeQD+yAKAAEmQBHaw==,1298525217870091633,Firefox,51.0,Ubuntu 51.15.40.89:39281,AAEAGCESpEJraER2VEl6aWJ1ZGiALwARaHR0cHM6Ly9jeWRldi5ydS8AAAA=,2038078914284839991,Chrome,55.0.2883.87,Linux x86_64 51.15.40.89:52341,AAEAGCESpEJHbHEveWt0dzB4cHOALwARaHR0cHM6Ly9jeWRldi5ydS8AAAA=,15407862181367978708,Chrome,55.0.2883.87,Linux x86_64 51.15.40.89:53197,AAEAGCESpEJQVkdXVHBjbjhBWlWALwARaHR0cHM6Ly9jeWRldi5ydS8AAAA=,17607978280141252653,Chrome,55.0.2883.87,Linux x86_64 51.15.40.89:58269,AAEACCESpEIB2FbktZDEiHN0z0iAKAAEK2VeXA==,3485408176062718742,Firefox,51.0,Ubuntu stun-0.6.1/testdata/fuzz/000077500000000000000000000000001444552150000153165ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessage/000077500000000000000000000000001444552150000175615ustar00rootroot000000000000000e2295391b7f3eb958c9f4d01172dbfb7a521ae80b57ef7742534c4629439160000066400000000000000000000002321444552150000277350ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("11\x00\x17\x1aä\xbdC���:O\xbd\xbf\x00\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00")245b929694bde0bb71c22ec2591f9355b891c85fd8b955034c53b453413a8a1e000066400000000000000000000001311444552150000300110ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("be\x00\x03,\x9b\xfb\x84\xb6\xa0\x00\x80\x00.L+|nfin\x00\x00\x00")27e983676cba4e3d4c6b268de4dc3c2a4962db616979175fdf675d438afb3c8a000066400000000000000000000004461444552150000304320ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("-97759226\xd5m.\x91\xec\xfeI\xd7\x00 \x1aä\xbd\x00\x14\xbf\xbd俽\uffc0\x00\x00\x00\x00\x00\xbf\x00\x00\x00\x9d\x00\xbf\xbd:O\xbd\xbf\x80\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00expected quoted string\x00\x00\xbf\x00\x00\xbf\x00\x00\x00\x00\xbf\x00\x00\x00\x00")36fa5d9fd7d9d66e1cc16e489c8a3b6dd67cbd1227a3d1a23ddb2b0468981b62000066400000000000000000000001121444552150000305270ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("14\x00\x04float6\xbd\xbf\xefO\xbd\xbfソ\xef4191")40d19a226a06d95c9406438b12bf7ecd24f943086796dcf2078cf2fe7ec3fa0f000066400000000000000000000000611444552150000303170ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("145519152283668U\x03806")4bd12c454bb66377b94285a54dde136f0f8308a09769a10a21cbf4d19d390874000066400000000000000000000002001444552150000277760ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("14\x00\x0e\x10\x00\xbf\xbdC���:O\xbd\xbf\x00\x00\x00\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00")4e2d258e3aa19e4c6de65b0f7fb498a4588b53451e7817fa11d3f3d47c9ee2db000066400000000000000000000000561444552150000304710ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("bHnary.L+infinityndi")562054225de70935c37ac5b9ab326e2cfafd5857d7aad5f5e47e7e5575827ef4000066400000000000000000000000721444552150000303360ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("be\x00\x00\x80\x00.L+|nfinityndi")66239baa5358757f4655497e3a0d48af4784af901050bfc809480aad65d702dd000066400000000000000000000000641444552150000300230ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("00\x00\x000000000000000000")6eb62aaac87690a5ae3bed8946876b05e4d5d5acd1b2acb558e03fc2e949dada000066400000000000000000000003231444552150000310240ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("11\x00!\x1aä\xbd\x00\x14\xbf\xbd쿽�:O\xbd\xbf\x00\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\xbf\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00")8038d52e604bf435d73221bdb5c6e28557d915219ab77652d3e4210337b70fba000066400000000000000000000001321444552150000277110ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("14\x00\n=�C���:O\x91\xbf\x00\x00\x01\x00\x00\x00\x00\x01")8c2645e17b9f6f545c79c33d8ee68ad83030b32367d73d0bbd77c2affdf3a294000066400000000000000000000002201444552150000304030ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("11\x00\x14\x10\x00\xbf\xbdC���:O\xbd\xbf\x00\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00")93044ca1e00c907b9992a5d0119090c11ed40ddc29a2115ab17423c76ce131e4000066400000000000000000000001161444552150000277340ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("14\x00\n=�C���:O\xbd\xbf\x00\x00\x01\x004191")95d2ccdd0016d21042194e0ea6d4efff0ca3f66d611bf4fa3230ca505b5b93e5000066400000000000000000000001571444552150000304700ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("be\x00\x05\xff\xff\x05\x84\xb6\xa0\x00\xf5,\x9b\x00,\x9b\x00.Lf\x00\x00\x00in\x00\x00")9ae6a162122bc543a5c59d712fdce29a93c21d018e5fd61ebc4c2f71379e834f000066400000000000000000000001121444552150000303560ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("1\x00\x00\x01\x00�C���:O\xbd\xbf\x00\x00")9d01ca09c33fbc7fad599dbc7cd5205b97ae8bd6cb9e321bbe992acb6839f399000066400000000000000000000001501444552150000307700ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("14\x00\n=�C��\xef\x00\x00\x00@\xbd\xbf\x00\x00\x00\x01\x00\x00\x01\x0041")9f92a54747b065e4b2df1cc3203e82b43853533fb2c2ae9155f31568235f143e000066400000000000000000000001421444552150000277200ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("14\x00\n=�C���:O\xbd\xbf\x00\x00\xbf\x00\x00\x00\x00\x00\x00\x00")bea9013452ffd1e80cd8c4b372b2149c1bf8c77b81c754a9ba35e0720bda0fba000066400000000000000000000003031444552150000305520ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("11\x00 \x1aä\xbd\x00\x14\xbf\xbd俽�:O\xbd\xbf\x00\x00\x00\x00\x00\x00\xbf\x00\x00\x00\xbf\x00\x00\x00\x00\xbf\x00\x00\xbf\x00\x00\x00\x00\xbf\x00\x00\x00\x00\x00\x00")d40a98862ed393eb712e47a91bcef18e6f24cf368bb4bd248c7a7101ef8e178d000066400000000000000000000000321444552150000304630ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzMessagego test fuzz v1 []byte("")stun-0.6.1/testdata/fuzz/FuzzSetters/000077500000000000000000000000001444552150000176265ustar00rootroot00000000000000337822ea9b278f300ec686794913e39fbb540cb4f216da38283b08b85b4fdfa3000066400000000000000000000003661444552150000302330ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('t') []byte(".gsErP_IH_ol7_mN7_8_r43e_tKnO__a_9TwJV7G_rjz8_c_Mq3y_5H__61__ \t>tt.xcount >txcount0N59lpU_n47gwRq6d0DS__j_wW__9Qu4Oz__uXMR7_g5__O_m__C_4 > 0G0G0N59lpU_n47gwRq6d0DS__j_wW__9Qu4Oz__uXMR7_g5__O_m__C_4 >\t 0G0G")651f4211e426e9bc0b4ed8fecdcd545d3bec97850fad9cf19c06a0294b1c62c3000066400000000000000000000001541444552150000306470ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('t') []byte(".xcount >t.0N59lpU_n47gwRq6d0DS__j_wW__9Qu4Oz__uXMR7_g5__O_m__C_4 > 0G0G")8d3066943508ff56500191c92915039470fdda0830ff3e3f5d1dd88c6fb8550a000066400000000000000000000000651444552150000300120ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('G') []byte("\x00\x01\xbf\xef{")8e994fd03c509dc7e9b1c6e4c0db222e722d6a89fddbcda99c4164e3c11bf8f5000066400000000000000000000001311444552150000307450ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('8') []byte("N_eX6_xy_g_3_.t> \tHcSg__j_Wi3_YV_4oalCf6U90ZzDyR_zh")9021dd11399c310d0ba5545906ee49ae796564a4c4c4878b3e07a08e33664370000066400000000000000000000000601444552150000276330ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('t') []byte(".xcount > 0G")a9e2bda709147e3e5c5a288c61714b4270649c5f050e5581369e0994a50597e7000066400000000000000000000001251444552150000276570ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('t') []byte("\xc0\x85\xefZ\xbc\xfb\x01\xe1yp.size >amd64p32 4G")acea713459e1e476795562801858bee9fd467f25eddb6183e3e4f4ddf8931ba4000066400000000000000000000002711444552150000304170ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('8') []byte("N_eX6_xy_g_3_._7943P6Sn6z2_a_w74tPp9a_nf4A__Z__SUo_iRi1__luyG4_H0y__qsrUYi5Q9WvPpw25nK9_nZK4_9i5t_4Z_5im8e_5aW0> \tHcSg__j_Wi3_YV_4oalCf6U90ZzDyR_zh")c995214f9f5fd033ca5a11295663eff0e72e2037ba4146b162b91c3594c97b4a000066400000000000000000000005251444552150000301340ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('t') []byte(" t.xcount >8_Q.0N59lpU_n47gwRq6d0DS__j_wW__9Qu4Oz__uXMR7_g5__O_m__C_4 >0G0G5R_Jk3K_4C___7__6AksK_1_EnJk__nhuq_t_L_8Bs_hP7D___bz_MaN587__c_ArhrW_F_11KJ_j7u1_5W8\x01._7943P6Sn6z2_a_w74tPp9a_nf4A__Z__SUo_iRi1__luyG4_H0y__qsrUYi5Q9WvPpw25nK9_nZK4_9i5t_4Z_5im8e_5aW0> \t \tHcSg__j_Wi3_YV_4oalCf6U90ZzDyR_zh0G")dbf9c906416f494d427c7268f4d3c5be67d82281443e7ae68f9acfb5b0069b7d000066400000000000000000000000711444552150000304120ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('~') []byte("\xf0\xf1\x9d^\x0f\bL.")eccfde8fecaeceec3a9b7967ec3533321b1cf9865e938546b35e3b9ad8e8d6bb000066400000000000000000000000441444552150000312120ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('G') []byte("")fd0cec4a969fa1eb5d8ba153dc60ee11ca5b7ccfcc446ebc90f659eb9977cb0b000066400000000000000000000014411444552150000313150ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzSettersgo test fuzz v1 byte('7') []byte("00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000") stun-0.6.1/testdata/fuzz/FuzzType/000077500000000000000000000000001444552150000171165ustar00rootroot00000000000000stun-0.6.1/testdata/fuzz/FuzzType/347c9496feb1d208c5626b387906542d3dcb6b1cf1657fbada66bd026e28ec86000066400000000000000000000000321444552150000277320ustar00rootroot00000000000000go test fuzz v1 uint16(0) stun-0.6.1/testdata/fuzz/FuzzType/40c5d2bd406be92969bbd57b505e7986120ddf3bda729dbd191b8e3b14317819000066400000000000000000000000371444552150000276470ustar00rootroot00000000000000go test fuzz v1 uint16(0xbe9c) stun-0.6.1/testdata/fuzz/FuzzType/5f8810e2071f50b3b84ae1f315314e1bab6cd5f91365ca5fa595ca36d1f00703000066400000000000000000000000371444552150000276070ustar00rootroot00000000000000go test fuzz v1 uint16(0xbdef) stun-0.6.1/testdata/fuzz/FuzzType/aa7c06e3e31d31a87ed29f708129c5b803f5179228bdf73dd7ed0d604fc9bd45000066400000000000000000000000371444552150000300120ustar00rootroot00000000000000go test fuzz v1 uint16(0x92fa) stun-0.6.1/testdata/fuzz/FuzzType/de25b2beeb5e9f721e1858cb0c8228df9d189cde30ee83b23f677156468a614d000066400000000000000000000000371444552150000300300ustar00rootroot00000000000000go test fuzz v1 uint16(0x5d5b) stun-0.6.1/testdata/stun-parameters-2.csv000066400000000000000000000012441444552150000203270ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: CC0-1.0 Value,Name,Reference 0x000,Reserved,[RFC5389] 0x001,Binding,[RFC5389] 0x002,Reserved; was SharedSecret,[RFC5389] 0x003,Allocate,[RFC5766] 0x004,Refresh,[RFC5766] 0x005,Unassigned, 0x006,Send,[RFC5766] 0x007,Data,[RFC5766] 0x008,CreatePermission,[RFC5766] 0x009,ChannelBind,[RFC5766] 0x00A,Connect,[RFC6062] 0x00B,ConnectionBind,[RFC6062] 0x00C,ConnectionAttempt,[RFC6062] 0x00D-0x0FF,Unassigned, 0x100-0xFFF,"Reserved (For DTLS-SRTP multiplexing collision avoidance, see [RFC7983]. Cannot be made available for assignment without IETF Review.)",[RFC7983] stun-0.6.1/testdata/stun-parameters-4.csv000066400000000000000000000055021444552150000203320ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: CC0-1.0 Value,Name,Reference 0x0000,Reserved,[RFC8489] 0x0001,MAPPED-ADDRESS,[RFC8489] 0x0002,Reserved; was RESPONSE-ADDRESS prior to [RFC5389],[RFC8489] 0x0003,Reserved; was CHANGE-REQUEST prior to [RFC5389],[RFC5780][RFC Errata 4233] 0x0004,Reserved; was SOURCE-ADDRESS prior to [RFC5389],[RFC8489] 0x0005,Reserved; was CHANGED-ADDRESS prior to [RFC5389],[RFC8489] 0x0006,USERNAME,[RFC8489] 0x0007,Reserved; was PASSWORD prior to [RFC5389],[RFC8489] 0x0008,MESSAGE-INTEGRITY,[RFC8489] 0x0009,ERROR-CODE,[RFC8489] 0x000A,UNKNOWN-ATTRIBUTES,[RFC8489] 0x000B,Reserved; was REFLECTED-FROM prior to [RFC5389],[RFC8489] 0x000C,CHANNEL-NUMBER,[RFC8656] 0x000D,LIFETIME,[RFC8656] 0x000E-0x000F,Reserved, 0x0010,Reserved (was BANDWIDTH),[RFC8656] 0x0011,Reserved, 0x0012,XOR-PEER-ADDRESS,[RFC8656] 0x0013,DATA,[RFC8656] 0x0014,REALM,[RFC8489] 0x0015,NONCE,[RFC8489] 0x0016,XOR-RELAYED-ADDRESS,[RFC8656] 0x0017,REQUESTED-ADDRESS-FAMILY,[RFC8656] 0x0018,EVEN-PORT,[RFC8656] 0x0019,REQUESTED-TRANSPORT,[RFC8656] 0x001A,DONT-FRAGMENT,[RFC8656] 0x001B,ACCESS-TOKEN,[RFC7635] 0x001C,MESSAGE-INTEGRITY-SHA256,[RFC8489] 0x001D,PASSWORD-ALGORITHM,[RFC8489] 0x001E,USERHASH,[RFC8489] 0x001F-0x001F,Unassigned, 0x0020,XOR-MAPPED-ADDRESS,[RFC8489] 0x0021,Reserved (was TIMER-VAL),[RFC8656] 0x0022,RESERVATION-TOKEN,[RFC8656] 0x0023,Reserved, 0x0024,PRIORITY,[RFC8445] 0x0025,USE-CANDIDATE,[RFC8445] 0x0026,PADDING,[RFC5780] 0x0027,RESPONSE-PORT,[RFC5780] 0x0028-0x0029,Reserved, 0x002A,CONNECTION-ID,[RFC6062] 0x002B-0x002F,Unassigned, 0x0030,Reserved, 0x0031-0x7FFF,Unassigned, 0x8000,ADDITIONAL-ADDRESS-FAMILY,[RFC8656] 0x8001,ADDRESS-ERROR-CODE,[RFC8656] 0x8002,PASSWORD-ALGORITHMS,[RFC8489] 0x8003,ALTERNATE-DOMAIN,[RFC8489] 0x8004,ICMP,[RFC8656] 0x8005-0x8021,Unassigned, 0x8022,SOFTWARE,[RFC8489] 0x8023,ALTERNATE-SERVER,[RFC8489] 0x8024,Reserved, 0x8025,TRANSACTION_TRANSMIT_COUNTER,[RFC7982] 0x8026,Reserved, 0x8027,CACHE-TIMEOUT,[RFC5780] 0x8028,FINGERPRINT,[RFC8489] 0x8029,ICE-CONTROLLED,[RFC8445] 0x802A,ICE-CONTROLLING,[RFC8445] 0x802B,RESPONSE-ORIGIN,[RFC5780] 0x802C,OTHER-ADDRESS,[RFC5780] 0x802D,ECN-CHECK STUN,[RFC6679] 0x802E,THIRD-PARTY-AUTHORIZATION,[RFC7635] 0x802F,Unassigned, 0x8030,MOBILITY-TICKET,[RFC8016] 0x8031-0xBFFF,Unassigned, 0xC000,CISCO-STUN-FLOWDATA,[Dan_Wing] 0xC001,ENF-FLOW-DESCRIPTION,[PÃ¥l_Erik_Martinsen] 0xC002,ENF-NETWORK-STATUS,[PÃ¥l_Erik_Martinsen] 0xC003-0xC056,Unassigned, 0xC057,GOOG-NETWORK-INFO,[Jonas_Oreland] 0xC058,GOOG-LAST-ICE-CHECK-RECEIVED,[Jonas_Oreland] 0xC059,GOOG-MISC-INFO,[Jonas_Oreland] 0xC05A,GOOG-OBSOLETE-1,[Jonas_Oreland] 0xC05B,GOOG-CONNECTION-ID,[Jonas_Oreland] 0xC05C,GOOG-DELTA,[Jonas_Oreland] 0xC05D,GOOG-DELTA-ACK,[Jonas_Oreland] 0xC05E-0xC05F,Unassigned, 0xC060,GOOG-MESSAGE-INTEGRITY-32,[Jonas_Oreland] 0xC061-0xFFFF,Unassigned, stun-0.6.1/testdata/stun-parameters-6.csv000066400000000000000000000016731444552150000203410ustar00rootroot00000000000000# SPDX-FileCopyrightText: 2023 The Pion community # SPDX-License-Identifier: CC0-1.0 Value,Name,Reference 0-299,Reserved, 300,Try Alternate,[RFC5389] 301-399,Unassigned, 400,Bad Request,[RFC5389] 401,Unauthorized,[RFC5389] 402,Unassigned, 403,Forbidden,[RFC5766] 404,Unassigned, 405,Mobility Forbidden,[RFC8016] 406-419,Unassigned, 420,Unknown Attribute,[RFC5389] 421-436,Unassigned, 437,Allocation Mismatch,[RFC5766] 438,Stale Nonce,[RFC5389] 439,Unassigned, 440,Address Family not Supported,[RFC6156] 441,Wrong Credentials,[RFC5766] 442,Unsupported Transport Protocol,[RFC5766] 443,Peer Address Family Mismatch,[RFC6156] 444-445,Unassigned, 446,Connection Already Exists,[RFC6062] 447,Connection Timeout or Failure,[RFC6062] 448-485,Unassigned, 486,Allocation Quota Reached,[RFC5766] 487,Role Conflict,[RFC8445] 488-499,Unassigned, 500,Server Error,[RFC5389] 501-507,Unassigned, 508,Insufficient Capacity,[RFC5766] 509-699,Unassigned, stun-0.6.1/textattrs.go000066400000000000000000000057011444552150000151030ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun // NewUsername returns Username with provided value. func NewUsername(username string) Username { return Username(username) } // Username represents USERNAME attribute. // // RFC 5389 Section 15.3 type Username []byte func (u Username) String() string { return string(u) } const maxUsernameB = 513 // AddTo adds USERNAME attribute to message. func (u Username) AddTo(m *Message) error { return TextAttribute(u).AddToAs(m, AttrUsername, maxUsernameB) } // GetFrom gets USERNAME from message. func (u *Username) GetFrom(m *Message) error { return (*TextAttribute)(u).GetFromAs(m, AttrUsername) } // NewRealm returns Realm with provided value. // Must be SASL-prepared. func NewRealm(realm string) Realm { return Realm(realm) } // Realm represents REALM attribute. // // RFC 5389 Section 15.7 type Realm []byte func (n Realm) String() string { return string(n) } const maxRealmB = 763 // AddTo adds NONCE to message. func (n Realm) AddTo(m *Message) error { return TextAttribute(n).AddToAs(m, AttrRealm, maxRealmB) } // GetFrom gets REALM from message. func (n *Realm) GetFrom(m *Message) error { return (*TextAttribute)(n).GetFromAs(m, AttrRealm) } const softwareRawMaxB = 763 // Software is SOFTWARE attribute. // // RFC 5389 Section 15.10 type Software []byte func (s Software) String() string { return string(s) } // NewSoftware returns *Software from string. func NewSoftware(software string) Software { return Software(software) } // AddTo adds Software attribute to m. func (s Software) AddTo(m *Message) error { return TextAttribute(s).AddToAs(m, AttrSoftware, softwareRawMaxB) } // GetFrom decodes Software from m. func (s *Software) GetFrom(m *Message) error { return (*TextAttribute)(s).GetFromAs(m, AttrSoftware) } // Nonce represents NONCE attribute. // // RFC 5389 Section 15.8 type Nonce []byte // NewNonce returns new Nonce from string. func NewNonce(nonce string) Nonce { return Nonce(nonce) } func (n Nonce) String() string { return string(n) } const maxNonceB = 763 // AddTo adds NONCE to message. func (n Nonce) AddTo(m *Message) error { return TextAttribute(n).AddToAs(m, AttrNonce, maxNonceB) } // GetFrom gets NONCE from message. func (n *Nonce) GetFrom(m *Message) error { return (*TextAttribute)(n).GetFromAs(m, AttrNonce) } // TextAttribute is helper for adding and getting text attributes. type TextAttribute []byte // AddToAs adds attribute with type t to m, checking maximum length. If maxLen // is less than 0, no check is performed. func (v TextAttribute) AddToAs(m *Message, t AttrType, maxLen int) error { if err := CheckOverflow(t, len(v), maxLen); err != nil { return err } m.Add(t, v) return nil } // GetFromAs gets t attribute from m and appends its value to reseted v. func (v *TextAttribute) GetFromAs(m *Message, t AttrType) error { a, err := m.Get(t) if err != nil { return err } *v = a return nil } stun-0.6.1/textattrs_test.go000066400000000000000000000135641444552150000161500ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package stun import ( "errors" "strings" "testing" ) func TestSoftware_GetFrom(t *testing.T) { m := New() v := "Client v0.0.1" m.Add(AttrSoftware, []byte(v)) m.WriteHeader() m2 := &Message{ Raw: make([]byte, 0, 256), } software := new(Software) if _, err := m2.ReadFrom(m.reader()); err != nil { t.Error(err) } if err := software.GetFrom(m); err != nil { t.Fatal(err) } if software.String() != v { t.Errorf("Expected %q, got %q.", v, software) } sAttr, ok := m.Attributes.Get(AttrSoftware) if !ok { t.Error("software attribute should be found") } s := sAttr.String() if !strings.HasPrefix(s, "SOFTWARE:") { t.Error("bad string representation", s) } } func TestSoftware_AddTo_Invalid(t *testing.T) { m := New() s := make(Software, 1024) if err := s.AddTo(m); !IsAttrSizeOverflow(err) { t.Errorf("AddTo should return *AttrOverflowErr, got: %v", err) } if err := s.GetFrom(m); !errors.Is(err, ErrAttributeNotFound) { t.Errorf("GetFrom should return %q, got: %v", ErrAttributeNotFound, err) } } func TestSoftware_AddTo_Regression(t *testing.T) { // s.AddTo checked len(m.Raw) instead of len(s.Raw). m := &Message{Raw: make([]byte, 2048)} s := make(Software, 100) if err := s.AddTo(m); err != nil { t.Errorf("AddTo should return , got: %v", err) } } func BenchmarkUsername_AddTo(b *testing.B) { b.ReportAllocs() m := new(Message) u := Username("test") for i := 0; i < b.N; i++ { if err := u.AddTo(m); err != nil { b.Fatal(err) } m.Reset() } } func BenchmarkUsername_GetFrom(b *testing.B) { b.ReportAllocs() m := new(Message) Username("test").AddTo(m) //nolint:errcheck,gosec var u Username b.ResetTimer() for i := 0; i < b.N; i++ { if err := u.GetFrom(m); err != nil { b.Fatal(err) } u = u[:0] } } func TestUsername(t *testing.T) { username := "username" u := NewUsername(username) m := new(Message) m.WriteHeader() t.Run("Bad length", func(t *testing.T) { badU := make(Username, 600) if err := badU.AddTo(m); !IsAttrSizeOverflow(err) { t.Errorf("AddTo should return *AttrOverflowErr, got: %v", err) } }) t.Run("AddTo", func(t *testing.T) { if err := u.AddTo(m); err != nil { t.Error("errored:", err) } t.Run("GetFrom", func(t *testing.T) { got := new(Username) if err := got.GetFrom(m); err != nil { t.Error("errored:", err) } if got.String() != username { t.Errorf("expedted: %s, got: %s", username, got) } t.Run("Not found", func(t *testing.T) { m := new(Message) u := new(Username) if err := u.GetFrom(m); !errors.Is(err, ErrAttributeNotFound) { t.Error("Should error") } }) }) }) t.Run("No allocations", func(t *testing.T) { m := new(Message) m.WriteHeader() u := NewUsername("username") if allocs := testing.AllocsPerRun(10, func() { if err := u.AddTo(m); err != nil { t.Error(err) } m.Reset() }); allocs > 0 { t.Errorf("got %f allocations, zero expected", allocs) } }) } func TestRealm_GetFrom(t *testing.T) { m := New() v := "realm" m.Add(AttrRealm, []byte(v)) m.WriteHeader() m2 := &Message{ Raw: make([]byte, 0, 256), } r := new(Realm) if err := r.GetFrom(m2); !errors.Is(err, ErrAttributeNotFound) { t.Errorf("GetFrom should return %q, got: %v", ErrAttributeNotFound, err) } if _, err := m2.ReadFrom(m.reader()); err != nil { t.Error(err) } if err := r.GetFrom(m); err != nil { t.Fatal(err) } if r.String() != v { t.Errorf("Expected %q, got %q.", v, r) } rAttr, ok := m.Attributes.Get(AttrRealm) if !ok { t.Error("realm attribute should be found") } s := rAttr.String() if !strings.HasPrefix(s, "REALM:") { t.Error("bad string representation", s) } } func TestRealm_AddTo_Invalid(t *testing.T) { m := New() r := make(Realm, 1024) if err := r.AddTo(m); !IsAttrSizeOverflow(err) { t.Errorf("AddTo should return *AttrOverflowErr, got: %v", err) } if err := r.GetFrom(m); !errors.Is(err, ErrAttributeNotFound) { t.Errorf("GetFrom should return %q, got: %v", ErrAttributeNotFound, err) } } func TestNonce_GetFrom(t *testing.T) { m := New() v := "example.org" m.Add(AttrNonce, []byte(v)) m.WriteHeader() m2 := &Message{ Raw: make([]byte, 0, 256), } var nonce Nonce if _, err := m2.ReadFrom(m.reader()); err != nil { t.Error(err) } if err := nonce.GetFrom(m); err != nil { t.Fatal(err) } if nonce.String() != v { t.Errorf("Expected %q, got %q.", v, nonce) } nAttr, ok := m.Attributes.Get(AttrNonce) if !ok { t.Error("nonce attribute should be found") } s := nAttr.String() if !strings.HasPrefix(s, "NONCE:") { t.Error("bad string representation", s) } } func TestNonce_AddTo_Invalid(t *testing.T) { m := New() n := make(Nonce, 1024) if err := n.AddTo(m); !IsAttrSizeOverflow(err) { t.Errorf("AddTo should return *AttrOverflowErr, got: %v", err) } if err := n.GetFrom(m); !errors.Is(err, ErrAttributeNotFound) { t.Errorf("GetFrom should return %q, got: %v", ErrAttributeNotFound, err) } } func TestNonce_AddTo(t *testing.T) { m := New() n := Nonce("example.org") if err := n.AddTo(m); err != nil { t.Error(err) } v, err := m.Get(AttrNonce) if err != nil { t.Error(err) } if string(v) != "example.org" { t.Errorf("bad nonce %q", v) } } func BenchmarkNonce_AddTo(b *testing.B) { b.ReportAllocs() m := New() n := NewNonce("nonce") for i := 0; i < b.N; i++ { if err := n.AddTo(m); err != nil { b.Fatal(err) } m.Reset() } } func BenchmarkNonce_AddTo_BadLength(b *testing.B) { b.ReportAllocs() m := New() n := make(Nonce, 2048) for i := 0; i < b.N; i++ { if err := n.AddTo(m); err == nil { b.Fatal("should error") } m.Reset() } } func BenchmarkNonce_GetFrom(b *testing.B) { b.ReportAllocs() m := New() n := NewNonce("nonce") n.AddTo(m) //nolint:errcheck,gosec for i := 0; i < b.N; i++ { n.GetFrom(m) //nolint:errcheck,gosec } } stun-0.6.1/uattrs.go000066400000000000000000000030051444552150000143560ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import "errors" // UnknownAttributes represents UNKNOWN-ATTRIBUTES attribute. // // RFC 5389 Section 15.9 type UnknownAttributes []AttrType func (a UnknownAttributes) String() string { s := "" if len(a) == 0 { return "" } last := len(a) - 1 for i, t := range a { s += t.String() if i != last { s += ", " } } return s } // type size is 16 bit. const attrTypeSize = 4 // AddTo adds UNKNOWN-ATTRIBUTES attribute to message. func (a UnknownAttributes) AddTo(m *Message) error { v := make([]byte, 0, attrTypeSize*20) // 20 should be enough // If len(a.Types) > 20, there will be allocations. for i, t := range a { v = append(v, 0, 0, 0, 0) // 4 times by 0 (16 bits) first := attrTypeSize * i last := first + attrTypeSize bin.PutUint16(v[first:last], t.Value()) } m.Add(AttrUnknownAttributes, v) return nil } // ErrBadUnknownAttrsSize means that UNKNOWN-ATTRIBUTES attribute value // has invalid length. var ErrBadUnknownAttrsSize = errors.New("bad UNKNOWN-ATTRIBUTES size") // GetFrom parses UNKNOWN-ATTRIBUTES from message. func (a *UnknownAttributes) GetFrom(m *Message) error { v, err := m.Get(AttrUnknownAttributes) if err != nil { return err } if len(v)%attrTypeSize != 0 { return ErrBadUnknownAttrsSize } *a = (*a)[:0] first := 0 for first < len(v) { last := first + attrTypeSize *a = append(*a, AttrType(bin.Uint16(v[first:last]))) first = last } return nil } stun-0.6.1/uattrs_test.go000066400000000000000000000030751444552150000154240ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "testing" ) func TestUnknownAttributes(t *testing.T) { m := new(Message) a := &UnknownAttributes{ AttrDontFragment, AttrChannelNumber, } if a.String() != "DONT-FRAGMENT, CHANNEL-NUMBER" { t.Error("bad String:", a) } if (UnknownAttributes{}).String() != "" { t.Error("bad blank string") } if err := a.AddTo(m); err != nil { t.Error(err) } t.Run("GetFrom", func(t *testing.T) { attrs := make(UnknownAttributes, 10) if err := attrs.GetFrom(m); err != nil { t.Error(err) } for i, at := range *a { if at != attrs[i] { t.Error("expected", at, "!=", attrs[i]) } } mBlank := new(Message) if err := attrs.GetFrom(mBlank); err == nil { t.Error("should error") } mBlank.Add(AttrUnknownAttributes, []byte{1, 2, 3}) if err := attrs.GetFrom(mBlank); err == nil { t.Error("should error") } }) } func BenchmarkUnknownAttributes(b *testing.B) { m := new(Message) a := UnknownAttributes{ AttrDontFragment, AttrChannelNumber, AttrRealm, AttrMessageIntegrity, } b.Run("AddTo", func(b *testing.B) { b.ReportAllocs() for i := 0; i < b.N; i++ { if err := a.AddTo(m); err != nil { b.Fatal(err) } m.Reset() } }) b.Run("GetFrom", func(b *testing.B) { b.ReportAllocs() if err := a.AddTo(m); err != nil { b.Fatal(err) } attrs := make(UnknownAttributes, 0, 10) for i := 0; i < b.N; i++ { if err := attrs.GetFrom(m); err != nil { b.Fatal(err) } attrs = attrs[:0] } }) } stun-0.6.1/uri.go000066400000000000000000000137251444552150000136450ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "net" "net/url" "strconv" ) var ( // ErrUnknownType indicates an error with Unknown info. ErrUnknownType = errors.New("Unknown") // ErrSchemeType indicates the scheme type could not be parsed. ErrSchemeType = errors.New("unknown scheme type") // ErrSTUNQuery indicates query arguments are provided in a STUN URL. ErrSTUNQuery = errors.New("queries not supported in stun address") // ErrInvalidQuery indicates an malformed query is provided. ErrInvalidQuery = errors.New("invalid query") // ErrHost indicates malformed hostname is provided. ErrHost = errors.New("invalid hostname") // ErrPort indicates malformed port is provided. ErrPort = errors.New("invalid port") // ErrProtoType indicates an unsupported transport type was provided. ErrProtoType = errors.New("invalid transport protocol type") ) // SchemeType indicates the type of server used in the ice.URL structure. type SchemeType int const ( // SchemeTypeUnknown indicates an unknown or unsupported scheme. SchemeTypeUnknown SchemeType = iota // SchemeTypeSTUN indicates the URL represents a STUN server. SchemeTypeSTUN // SchemeTypeSTUNS indicates the URL represents a STUNS (secure) server. SchemeTypeSTUNS // SchemeTypeTURN indicates the URL represents a TURN server. SchemeTypeTURN // SchemeTypeTURNS indicates the URL represents a TURNS (secure) server. SchemeTypeTURNS ) // NewSchemeType defines a procedure for creating a new SchemeType from a raw // string naming the scheme type. func NewSchemeType(raw string) SchemeType { switch raw { case "stun": return SchemeTypeSTUN case "stuns": return SchemeTypeSTUNS case "turn": return SchemeTypeTURN case "turns": return SchemeTypeTURNS default: return SchemeTypeUnknown } } func (t SchemeType) String() string { switch t { case SchemeTypeSTUN: return "stun" case SchemeTypeSTUNS: return "stuns" case SchemeTypeTURN: return "turn" case SchemeTypeTURNS: return "turns" default: return ErrUnknownType.Error() } } // ProtoType indicates the transport protocol type that is used in the ice.URL // structure. type ProtoType int const ( // ProtoTypeUnknown indicates an unknown or unsupported protocol. ProtoTypeUnknown ProtoType = iota // ProtoTypeUDP indicates the URL uses a UDP transport. ProtoTypeUDP // ProtoTypeTCP indicates the URL uses a TCP transport. ProtoTypeTCP ) // NewProtoType defines a procedure for creating a new ProtoType from a raw // string naming the transport protocol type. func NewProtoType(raw string) ProtoType { switch raw { case "udp": return ProtoTypeUDP case "tcp": return ProtoTypeTCP default: return ProtoTypeUnknown } } func (t ProtoType) String() string { switch t { case ProtoTypeUDP: return "udp" case ProtoTypeTCP: return "tcp" default: return ErrUnknownType.Error() } } // URI represents a STUN (rfc7064) or TURN (rfc7065) URI type URI struct { Scheme SchemeType Host string Port int Username string Password string Proto ProtoType } // ParseURI parses a STUN or TURN urls following the ABNF syntax described in // https://tools.ietf.org/html/rfc7064 and https://tools.ietf.org/html/rfc7065 // respectively. func ParseURI(raw string) (*URI, error) { //nolint:gocognit rawParts, err := url.Parse(raw) if err != nil { return nil, err } var u URI u.Scheme = NewSchemeType(rawParts.Scheme) if u.Scheme == SchemeTypeUnknown { return nil, ErrSchemeType } var rawPort string if u.Host, rawPort, err = net.SplitHostPort(rawParts.Opaque); err != nil { var e *net.AddrError if errors.As(err, &e) { if e.Err == "missing port in address" { nextRawURL := u.Scheme.String() + ":" + rawParts.Opaque switch { case u.Scheme == SchemeTypeSTUN || u.Scheme == SchemeTypeTURN: nextRawURL += ":3478" if rawParts.RawQuery != "" { nextRawURL += "?" + rawParts.RawQuery } return ParseURI(nextRawURL) case u.Scheme == SchemeTypeSTUNS || u.Scheme == SchemeTypeTURNS: nextRawURL += ":5349" if rawParts.RawQuery != "" { nextRawURL += "?" + rawParts.RawQuery } return ParseURI(nextRawURL) } } } return nil, err } if u.Host == "" { return nil, ErrHost } if u.Port, err = strconv.Atoi(rawPort); err != nil { return nil, ErrPort } switch u.Scheme { case SchemeTypeSTUN: qArgs, err := url.ParseQuery(rawParts.RawQuery) if err != nil || len(qArgs) > 0 { return nil, ErrSTUNQuery } u.Proto = ProtoTypeUDP case SchemeTypeSTUNS: qArgs, err := url.ParseQuery(rawParts.RawQuery) if err != nil || len(qArgs) > 0 { return nil, ErrSTUNQuery } u.Proto = ProtoTypeTCP case SchemeTypeTURN: proto, err := parseProto(rawParts.RawQuery) if err != nil { return nil, err } u.Proto = proto if u.Proto == ProtoTypeUnknown { u.Proto = ProtoTypeUDP } case SchemeTypeTURNS: proto, err := parseProto(rawParts.RawQuery) if err != nil { return nil, err } u.Proto = proto if u.Proto == ProtoTypeUnknown { u.Proto = ProtoTypeTCP } case SchemeTypeUnknown: } return &u, nil } func parseProto(raw string) (ProtoType, error) { qArgs, err := url.ParseQuery(raw) if err != nil || len(qArgs) > 1 { return ProtoTypeUnknown, ErrInvalidQuery } var proto ProtoType if rawProto := qArgs.Get("transport"); rawProto != "" { if proto = NewProtoType(rawProto); proto == ProtoType(0) { return ProtoTypeUnknown, ErrProtoType } return proto, nil } if len(qArgs) > 0 { return ProtoTypeUnknown, ErrInvalidQuery } return proto, nil } func (u URI) String() string { rawURL := u.Scheme.String() + ":" + net.JoinHostPort(u.Host, strconv.Itoa(u.Port)) if u.Scheme == SchemeTypeTURN || u.Scheme == SchemeTypeTURNS { rawURL += "?transport=" + u.Proto.String() } return rawURL } // IsSecure returns whether the this URL's scheme describes secure scheme or not. func (u URI) IsSecure() bool { return u.Scheme == SchemeTypeSTUNS || u.Scheme == SchemeTypeTURNS } stun-0.6.1/uri_test.go000066400000000000000000000064271444552150000147050ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "fmt" "net" "net/url" "testing" "github.com/stretchr/testify/assert" ) var ( errMissingProtocolScheme = errors.New("missing protocol scheme") errTooManyColonsAddr = errors.New("too many colons in address") ) func TestParseURL(t *testing.T) { t.Run("Success", func(t *testing.T) { testCases := []struct { rawURL string expectedURLString string expectedScheme SchemeType expectedSecure bool expectedHost string expectedPort int expectedProto ProtoType }{ {"stun:google.de", "stun:google.de:3478", SchemeTypeSTUN, false, "google.de", 3478, ProtoTypeUDP}, {"stun:google.de:1234", "stun:google.de:1234", SchemeTypeSTUN, false, "google.de", 1234, ProtoTypeUDP}, {"stuns:google.de", "stuns:google.de:5349", SchemeTypeSTUNS, true, "google.de", 5349, ProtoTypeTCP}, {"stun:[::1]:123", "stun:[::1]:123", SchemeTypeSTUN, false, "::1", 123, ProtoTypeUDP}, {"turn:google.de", "turn:google.de:3478?transport=udp", SchemeTypeTURN, false, "google.de", 3478, ProtoTypeUDP}, {"turns:google.de", "turns:google.de:5349?transport=tcp", SchemeTypeTURNS, true, "google.de", 5349, ProtoTypeTCP}, {"turn:google.de?transport=udp", "turn:google.de:3478?transport=udp", SchemeTypeTURN, false, "google.de", 3478, ProtoTypeUDP}, {"turns:google.de?transport=tcp", "turns:google.de:5349?transport=tcp", SchemeTypeTURNS, true, "google.de", 5349, ProtoTypeTCP}, } for i, testCase := range testCases { url, err := ParseURI(testCase.rawURL) assert.Nil(t, err, "testCase: %d %v", i, testCase) if err != nil { return } assert.Equal(t, testCase.expectedScheme, url.Scheme, "testCase: %d %v", i, testCase) assert.Equal(t, testCase.expectedURLString, url.String(), "testCase: %d %v", i, testCase) assert.Equal(t, testCase.expectedSecure, url.IsSecure(), "testCase: %d %v", i, testCase) assert.Equal(t, testCase.expectedHost, url.Host, "testCase: %d %v", i, testCase) assert.Equal(t, testCase.expectedPort, url.Port, "testCase: %d %v", i, testCase) assert.Equal(t, testCase.expectedProto, url.Proto, "testCase: %d %v", i, testCase) } }) t.Run("Failure", func(t *testing.T) { testCases := []struct { rawURL string expectedErr error }{ {"", ErrSchemeType}, {":::", errMissingProtocolScheme}, {"stun:[::1]:123:", errTooManyColonsAddr}, {"stun:[::1]:123a", ErrPort}, {"google.de", ErrSchemeType}, {"stun:", ErrHost}, {"stun:google.de:abc", ErrPort}, {"stun:google.de?transport=udp", ErrSTUNQuery}, {"stuns:google.de?transport=udp", ErrSTUNQuery}, {"turn:google.de?trans=udp", ErrInvalidQuery}, {"turns:google.de?trans=udp", ErrInvalidQuery}, {"turns:google.de?transport=udp&another=1", ErrInvalidQuery}, {"turn:google.de?transport=ip", ErrProtoType}, } for i, testCase := range testCases { _, err := ParseURI(testCase.rawURL) var ( urlError *url.Error addrError *net.AddrError ) switch { case errors.As(err, &urlError): err = urlError.Err case errors.As(err, &addrError): err = fmt.Errorf(addrError.Err) //nolint:goerr113 } assert.EqualError(t, err, testCase.expectedErr.Error(), "testCase: %d %v", i, testCase) } }) } stun-0.6.1/xoraddr.go000066400000000000000000000075331444552150000145110ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT package stun import ( "errors" "fmt" "io" "net" "strconv" "github.com/pion/transport/v2/utils/xor" ) const ( familyIPv4 uint16 = 0x01 familyIPv6 uint16 = 0x02 ) // XORMappedAddress implements XOR-MAPPED-ADDRESS attribute. // // RFC 5389 Section 15.2 type XORMappedAddress struct { IP net.IP Port int } func (a XORMappedAddress) String() string { return net.JoinHostPort(a.IP.String(), strconv.Itoa(a.Port)) } // isIPv4 returns true if ip with len of net.IPv6Len seems to be ipv4. func isIPv4(ip net.IP) bool { // Optimized for performance. Copied from net.IP.To4. return isZeros(ip[0:10]) && ip[10] == 0xff && ip[11] == 0xff } // Is p all zeros? func isZeros(p net.IP) bool { for i := 0; i < len(p); i++ { if p[i] != 0 { return false } } return true } // ErrBadIPLength means that len(IP) is not net.{IPv6len,IPv4len}. var ErrBadIPLength = errors.New("invalid length of IP value") // AddToAs adds XOR-MAPPED-ADDRESS value to m as t attribute. func (a XORMappedAddress) AddToAs(m *Message, t AttrType) error { var ( family = familyIPv4 ip = a.IP ) if len(a.IP) == net.IPv6len { if isIPv4(ip) { ip = ip[12:16] // like in ip.To4() } else { family = familyIPv6 } } else if len(ip) != net.IPv4len { return ErrBadIPLength } value := make([]byte, 32+128) value[0] = 0 // first 8 bits are zeroes xorValue := make([]byte, net.IPv6len) copy(xorValue[4:], m.TransactionID[:]) bin.PutUint32(xorValue[0:4], magicCookie) bin.PutUint16(value[0:2], family) bin.PutUint16(value[2:4], uint16(a.Port^magicCookie>>16)) xor.XorBytes(value[4:4+len(ip)], ip, xorValue) m.Add(t, value[:4+len(ip)]) return nil } // AddTo adds XOR-MAPPED-ADDRESS to m. Can return ErrBadIPLength // if len(a.IP) is invalid. func (a XORMappedAddress) AddTo(m *Message) error { return a.AddToAs(m, AttrXORMappedAddress) } // GetFromAs decodes XOR-MAPPED-ADDRESS attribute value in message // getting it as for t type. func (a *XORMappedAddress) GetFromAs(m *Message, t AttrType) error { v, err := m.Get(t) if err != nil { return err } family := bin.Uint16(v[0:2]) if family != familyIPv6 && family != familyIPv4 { return newDecodeErr("xor-mapped address", "family", fmt.Sprintf("bad value %d", family), ) } ipLen := net.IPv4len if family == familyIPv6 { ipLen = net.IPv6len } // Ensuring len(a.IP) == ipLen and reusing a.IP. if len(a.IP) < ipLen { a.IP = a.IP[:cap(a.IP)] for len(a.IP) < ipLen { a.IP = append(a.IP, 0) } } a.IP = a.IP[:ipLen] for i := range a.IP { a.IP[i] = 0 } if len(v) <= 4 { return io.ErrUnexpectedEOF } if err := CheckOverflow(t, len(v[4:]), len(a.IP)); err != nil { return err } a.Port = int(bin.Uint16(v[2:4])) ^ (magicCookie >> 16) xorValue := make([]byte, 4+TransactionIDSize) bin.PutUint32(xorValue[0:4], magicCookie) copy(xorValue[4:], m.TransactionID[:]) xor.XorBytes(a.IP, v[4:], xorValue) return nil } // GetFrom decodes XOR-MAPPED-ADDRESS attribute in message and returns // error if any. While decoding, a.IP is reused if possible and can be // rendered to invalid state (e.g. if a.IP was set to IPv6 and then // IPv4 value were decoded into it), be careful. // // Example: // // expectedIP := net.ParseIP("213.141.156.236") // expectedIP.String() // 213.141.156.236, 16 bytes, first 12 of them are zeroes // expectedPort := 21254 // addr := &XORMappedAddress{ // IP: expectedIP, // Port: expectedPort, // } // // addr were added to message that is decoded as newMessage // // ... // // addr.GetFrom(newMessage) // addr.IP.String() // 213.141.156.236, net.IPv4Len // expectedIP.String() // d58d:9cec::ffff:d58d:9cec, 16 bytes, first 4 are IPv4 // // now we have len(expectedIP) = 16 and len(addr.IP) = 4. func (a *XORMappedAddress) GetFrom(m *Message) error { return a.GetFromAs(m, AttrXORMappedAddress) } stun-0.6.1/xoraddr_test.go000066400000000000000000000125601444552150000155440ustar00rootroot00000000000000// SPDX-FileCopyrightText: 2023 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package stun import ( "bytes" "encoding/base64" "encoding/binary" "encoding/hex" "errors" "io" "net" "testing" ) func BenchmarkXORMappedAddress_AddTo(b *testing.B) { m := New() b.ReportAllocs() ip := net.ParseIP("192.168.1.32") for i := 0; i < b.N; i++ { addr := &XORMappedAddress{IP: ip, Port: 3654} addr.AddTo(m) //nolint:errcheck,gosec m.Reset() } } func BenchmarkXORMappedAddress_GetFrom(b *testing.B) { m := New() transactionID, err := base64.StdEncoding.DecodeString("jxhBARZwX+rsC6er") if err != nil { b.Error(err) } copy(m.TransactionID[:], transactionID) addrValue, err := hex.DecodeString("00019cd5f49f38ae") if err != nil { b.Error(err) } m.Add(AttrXORMappedAddress, addrValue) addr := new(XORMappedAddress) b.ReportAllocs() for i := 0; i < b.N; i++ { if err := addr.GetFrom(m); err != nil { b.Fatal(err) } } } func TestXORMappedAddress_GetFrom(t *testing.T) { m := New() transactionID, err := base64.StdEncoding.DecodeString("jxhBARZwX+rsC6er") if err != nil { t.Error(err) } copy(m.TransactionID[:], transactionID) addrValue, err := hex.DecodeString("00019cd5f49f38ae") if err != nil { t.Error(err) } m.Add(AttrXORMappedAddress, addrValue) addr := new(XORMappedAddress) if err = addr.GetFrom(m); err != nil { t.Error(err) } if !addr.IP.Equal(net.ParseIP("213.141.156.236")) { t.Error("bad IP", addr.IP, "!=", "213.141.156.236") } if addr.Port != 48583 { t.Error("bad Port", addr.Port, "!=", 48583) } t.Run("UnexpectedEOF", func(t *testing.T) { m := New() // {0, 1} is correct addr family. m.Add(AttrXORMappedAddress, []byte{0, 1, 3, 4}) addr := new(XORMappedAddress) if err = addr.GetFrom(m); !errors.Is(err, io.ErrUnexpectedEOF) { t.Errorf("len(v) = 4 should render <%s> error, got <%s>", io.ErrUnexpectedEOF, err, ) } }) t.Run("AttrOverflowErr", func(t *testing.T) { m := New() // {0, 1} is correct addr family. m.Add(AttrXORMappedAddress, []byte{0, 1, 3, 4, 5, 6, 7, 8, 9, 1, 1, 1, 1, 1, 2, 3, 4}) addr := new(XORMappedAddress) if err := addr.GetFrom(m); !IsAttrSizeOverflow(err) { t.Errorf("AddTo should return *AttrOverflowErr, got: %v", err) } }) } func TestXORMappedAddress_GetFrom_Invalid(t *testing.T) { m := New() transactionID, err := base64.StdEncoding.DecodeString("jxhBARZwX+rsC6er") if err != nil { t.Error(err) } copy(m.TransactionID[:], transactionID) expectedIP := net.ParseIP("213.141.156.236") expectedPort := 21254 addr := new(XORMappedAddress) if err = addr.GetFrom(m); err == nil { t.Fatal(err, "should be nil") } addr.IP = expectedIP addr.Port = expectedPort addr.AddTo(m) //nolint:errcheck,gosec m.WriteHeader() mRes := New() binary.BigEndian.PutUint16(m.Raw[20+4:20+4+2], 0x21) if _, err = mRes.ReadFrom(bytes.NewReader(m.Raw)); err != nil { t.Fatal(err) } if err = addr.GetFrom(m); err == nil { t.Fatal(err, "should not be nil") } } func TestXORMappedAddress_AddTo(t *testing.T) { m := New() transactionID, err := base64.StdEncoding.DecodeString("jxhBARZwX+rsC6er") if err != nil { t.Error(err) } copy(m.TransactionID[:], transactionID) expectedIP := net.ParseIP("213.141.156.236") expectedPort := 21254 addr := &XORMappedAddress{ IP: net.ParseIP("213.141.156.236"), Port: expectedPort, } if err = addr.AddTo(m); err != nil { t.Fatal(err) } m.WriteHeader() mRes := New() if _, err = mRes.Write(m.Raw); err != nil { t.Fatal(err) } if err = addr.GetFrom(mRes); err != nil { t.Fatal(err) } if !addr.IP.Equal(expectedIP) { t.Errorf("%s (got) != %s (expected)", addr.IP, expectedIP) } if addr.Port != expectedPort { t.Error("bad Port", addr.Port, "!=", expectedPort) } } func TestXORMappedAddress_AddTo_IPv6(t *testing.T) { m := New() transactionID, err := base64.StdEncoding.DecodeString("jxhBARZwX+rsC6er") if err != nil { t.Error(err) } copy(m.TransactionID[:], transactionID) expectedIP := net.ParseIP("fe80::dc2b:44ff:fe20:6009") expectedPort := 21254 addr := &XORMappedAddress{ IP: net.ParseIP("fe80::dc2b:44ff:fe20:6009"), Port: 21254, } addr.AddTo(m) //nolint:errcheck,gosec m.WriteHeader() mRes := New() if _, err = mRes.ReadFrom(m.reader()); err != nil { t.Fatal(err) } gotAddr := new(XORMappedAddress) if err = gotAddr.GetFrom(m); err != nil { t.Fatal(err) } if !gotAddr.IP.Equal(expectedIP) { t.Error("bad IP", gotAddr.IP, "!=", expectedIP) } if gotAddr.Port != expectedPort { t.Error("bad Port", gotAddr.Port, "!=", expectedPort) } } func TestXORMappedAddress_AddTo_Invalid(t *testing.T) { m := New() addr := &XORMappedAddress{ IP: []byte{1, 2, 3, 4, 5, 6, 7, 8}, Port: 21254, } if err := addr.AddTo(m); !errors.Is(err, ErrBadIPLength) { t.Errorf("AddTo should return %q, got: %v", ErrBadIPLength, err) } } func TestXORMappedAddress_String(t *testing.T) { tt := []struct { in XORMappedAddress out string }{ { // 0 XORMappedAddress{ IP: net.ParseIP("fe80::dc2b:44ff:fe20:6009"), Port: 124, }, "[fe80::dc2b:44ff:fe20:6009]:124", }, { // 1 XORMappedAddress{ IP: net.ParseIP("213.141.156.236"), Port: 8147, }, "213.141.156.236:8147", }, } for i, c := range tt { if got := c.in.String(); got != c.out { t.Errorf("[%d]: XORMappesAddres.String() %s (got) != %s (expected)", i, got, c.out, ) } } }