├── .github ├── .gitignore ├── workflows │ ├── lint.yaml │ ├── release.yml │ ├── generate-authors.yml │ ├── renovate-go-sum-fix.yaml │ ├── tidy-check.yaml │ ├── codeql-analysis.yml │ └── test.yaml ├── install-hooks.sh └── fetch-scripts.sh ├── .goreleaser.yml ├── go.mod ├── renovate.json ├── .gitignore ├── codecov.yml ├── AUTHORS.txt ├── pkg └── sync │ ├── waitgroup_test.go │ └── waitgroup.go ├── LICENSE ├── README.md ├── go.sum ├── .golangci.yml ├── conn.go └── conn_test.go /.github/.gitignore: -------------------------------------------------------------------------------- 1 | .goassets 2 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - skip: true 3 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pion/udp/v2 2 | 3 | go 1.14 4 | 5 | require github.com/pion/transport/v2 v2.0.2 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>pion/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### JetBrains IDE ### 2 | ##################### 3 | .idea/ 4 | 5 | ### Emacs Temporary Files ### 6 | ############################# 7 | *~ 8 | 9 | ### Folders ### 10 | ############### 11 | bin/ 12 | vendor/ 13 | node_modules/ 14 | 15 | ### Files ### 16 | ############# 17 | *.ivf 18 | *.ogg 19 | tags 20 | cover.out 21 | *.sw[poe] 22 | *.wasm 23 | examples/sfu-ws/cert.pem 24 | examples/sfu-ws/key.pem 25 | wasm_exec.js 26 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # 6 | 7 | coverage: 8 | status: 9 | project: 10 | default: 11 | # Allow decreasing 2% of total coverage to avoid noise. 12 | threshold: 2% 13 | patch: 14 | default: 15 | target: 70% 16 | only_pulls: true 17 | 18 | ignore: 19 | - "examples/*" 20 | - "examples/**/*" 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | 12 | name: Lint 13 | on: 14 | pull_request: 15 | 16 | jobs: 17 | lint: 18 | uses: pion/.goassets/.github/workflows/lint.reusable.yml@master 19 | -------------------------------------------------------------------------------- /.github/install-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # DO NOT EDIT THIS FILE 5 | # 6 | # It is automatically copied from https://github.com/pion/.goassets repository. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | 12 | SCRIPT_PATH="$(realpath "$(dirname "$0")")" 13 | 14 | . ${SCRIPT_PATH}/fetch-scripts.sh 15 | 16 | cp "${GOASSETS_PATH}/hooks/commit-msg.sh" "${SCRIPT_PATH}/../.git/hooks/commit-msg" 17 | cp "${GOASSETS_PATH}/hooks/pre-commit.sh" "${SCRIPT_PATH}/../.git/hooks/pre-commit" 18 | cp "${GOASSETS_PATH}/hooks/pre-push.sh" "${SCRIPT_PATH}/../.git/hooks/pre-push" 19 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | 12 | name: Release 13 | on: 14 | push: 15 | tags: 16 | - 'v*' 17 | 18 | jobs: 19 | release: 20 | uses: pion/.goassets/.github/workflows/release.reusable.yml@master 21 | with: 22 | go-version: '1.19' # auto-update/latest-go-version 23 | -------------------------------------------------------------------------------- /.github/workflows/generate-authors.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | 12 | name: Generate Authors 13 | 14 | on: 15 | pull_request: 16 | 17 | jobs: 18 | generate: 19 | uses: pion/.goassets/.github/workflows/generate-authors.reusable.yml@master 20 | secrets: 21 | token: ${{ secrets.PIONBOT_PRIVATE_KEY }} 22 | -------------------------------------------------------------------------------- /.github/workflows/renovate-go-sum-fix.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | 12 | name: Fix go.sum 13 | on: 14 | push: 15 | branches: 16 | - renovate/* 17 | 18 | jobs: 19 | fix: 20 | uses: pion/.goassets/.github/workflows/renovate-go-sum-fix.reusable.yml@master 21 | secrets: 22 | token: ${{ secrets.PIONBOT_PRIVATE_KEY }} 23 | -------------------------------------------------------------------------------- /.github/workflows/tidy-check.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | 12 | name: Go mod tidy 13 | on: 14 | pull_request: 15 | push: 16 | branches: 17 | - master 18 | 19 | jobs: 20 | tidy: 21 | uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master 22 | with: 23 | go-version: '1.19' # auto-update/latest-go-version 24 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | 12 | name: CodeQL 13 | 14 | on: 15 | workflow_dispatch: 16 | schedule: 17 | - cron: '23 5 * * 0' 18 | pull_request: 19 | branches: 20 | - master 21 | paths: 22 | - '**.go' 23 | 24 | jobs: 25 | analyze: 26 | uses: pion/.goassets/.github/workflows/codeql-analysis.reusable.yml@master 27 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | # Thank you to everyone that made Pion possible. If you are interested in contributing 2 | # we would love to have you https://github.com/pion/webrtc/wiki/Contributing 3 | # 4 | # This file is auto generated, using git to list all individuals contributors. 5 | # see https://github.com/pion/.goassets/blob/master/scripts/generate-authors.sh for the scripting 6 | Atsushi Watanabe 7 | Daniel Beseda 8 | Daniele Sluijters 9 | Jozef Kralik 10 | Mathias Fredriksson 11 | Michiel De Backker 12 | Sean DuBois 13 | Steffen Vogel 14 | sterling.deng 15 | ZHENK 16 | 17 | # List of contributors not appearing in Git history 18 | 19 | -------------------------------------------------------------------------------- /.github/fetch-scripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # DO NOT EDIT THIS FILE 5 | # 6 | # It is automatically copied from https://github.com/pion/.goassets repository. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | 12 | set -eu 13 | 14 | SCRIPT_PATH="$(realpath "$(dirname "$0")")" 15 | GOASSETS_PATH="${SCRIPT_PATH}/.goassets" 16 | 17 | GOASSETS_REF=${GOASSETS_REF:-master} 18 | 19 | if [ -d "${GOASSETS_PATH}" ]; then 20 | if ! git -C "${GOASSETS_PATH}" diff --exit-code; then 21 | echo "${GOASSETS_PATH} has uncommitted changes" >&2 22 | exit 1 23 | fi 24 | git -C "${GOASSETS_PATH}" fetch origin 25 | git -C "${GOASSETS_PATH}" checkout ${GOASSETS_REF} 26 | git -C "${GOASSETS_PATH}" reset --hard origin/${GOASSETS_REF} 27 | else 28 | git clone -b ${GOASSETS_REF} https://github.com/pion/.goassets.git "${GOASSETS_PATH}" 29 | fi 30 | -------------------------------------------------------------------------------- /pkg/sync/waitgroup_test.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package sync_test 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pion/udp/v2/pkg/sync" 10 | ) 11 | 12 | func testWaitGroup(t *testing.T, wg1 *sync.WaitGroup, wg2 *sync.WaitGroup) { 13 | n := 16 14 | wg1.Add(n) 15 | wg2.Add(n) 16 | exited := make(chan bool, n) 17 | for i := 0; i != n; i++ { 18 | go func() { 19 | wg1.Done() 20 | wg2.Wait() 21 | exited <- true 22 | }() 23 | } 24 | wg1.Wait() 25 | for i := 0; i != n; i++ { 26 | select { 27 | case <-exited: 28 | t.Fatal("WaitGroup released group too soon") 29 | default: 30 | } 31 | wg2.Done() 32 | } 33 | for i := 0; i != n; i++ { 34 | <-exited // Will block if barrier fails to unlock someone. 35 | } 36 | } 37 | 38 | func TestWaitGroup(t *testing.T) { 39 | wg1 := sync.NewWaitGroup() 40 | wg2 := sync.NewWaitGroup() 41 | 42 | // Run the same test a few times to ensure barrier is in a proper state. 43 | for i := 0; i != 8; i++ { 44 | testWaitGroup(t, wg1, wg2) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | 12 | name: Test 13 | on: 14 | push: 15 | branches: 16 | - master 17 | pull_request: 18 | 19 | jobs: 20 | test: 21 | uses: pion/.goassets/.github/workflows/test.reusable.yml@master 22 | strategy: 23 | matrix: 24 | go: ['1.19', '1.18'] # auto-update/supported-go-version-list 25 | fail-fast: false 26 | with: 27 | go-version: ${{ matrix.go }} 28 | 29 | test-i386: 30 | uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master 31 | strategy: 32 | matrix: 33 | go: ['1.19', '1.18'] # auto-update/supported-go-version-list 34 | fail-fast: false 35 | with: 36 | go-version: ${{ matrix.go }} 37 | 38 | test-wasm: 39 | uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master 40 | with: 41 | go-version: '1.19' # auto-update/latest-go-version 42 | -------------------------------------------------------------------------------- /pkg/sync/waitgroup.go: -------------------------------------------------------------------------------- 1 | // Package sync extends basic synchronization primitives. 2 | package sync 3 | 4 | import ( 5 | "sync" 6 | ) 7 | 8 | // A WaitGroup waits for a collection of goroutines to finish. 9 | // The main goroutine calls Add to set the number of 10 | // goroutines to wait for. Then each of the goroutines 11 | // runs and calls Done when finished. At the same time, 12 | // Wait can be used to block until all goroutines have finished. 13 | // 14 | // WaitGroups in the sync package do not allow adding or 15 | // subtracting from the counter while another goroutine is 16 | // waiting, while this one does. 17 | // 18 | // A WaitGroup must not be copied after first use. 19 | // 20 | // In the terminology of the Go memory model, a call to Done 21 | type WaitGroup struct { 22 | c int64 23 | mutex sync.Mutex 24 | cond *sync.Cond 25 | } 26 | 27 | // NewWaitGroup creates a new WaitGroup. 28 | func NewWaitGroup() *WaitGroup { 29 | wg := &WaitGroup{} 30 | wg.cond = sync.NewCond(&wg.mutex) 31 | return wg 32 | } 33 | 34 | // Add adds delta, which may be negative, to the WaitGroup counter. 35 | // If the counter becomes zero, all goroutines blocked on Wait are released. 36 | // If the counter goes negative, Add panics. 37 | func (wg *WaitGroup) Add(delta int) { 38 | wg.mutex.Lock() 39 | defer wg.mutex.Unlock() 40 | wg.c += int64(delta) 41 | if wg.c < 0 { 42 | panic("udp: negative WaitGroup counter") // nolint 43 | } 44 | wg.cond.Signal() 45 | } 46 | 47 | // Done decrements the WaitGroup counter by one. 48 | func (wg *WaitGroup) Done() { 49 | wg.Add(-1) 50 | } 51 | 52 | // Wait blocks until the WaitGroup counter is zero. 53 | func (wg *WaitGroup) Wait() { 54 | wg.mutex.Lock() 55 | defer wg.mutex.Unlock() 56 | for { 57 | c := wg.c 58 | switch { 59 | case c == 0: 60 | // wake another goroutine if there is one 61 | wg.cond.Signal() 62 | return 63 | case c < 0: 64 | panic("udp: negative WaitGroup counter") // nolint 65 | } 66 | wg.cond.Wait() 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Pion UDP 4 |
5 |

6 |

A connection-oriented listener over a UDP PacketConn

7 |

8 | Pion UDP 9 | Slack Widget 10 |
11 | GitHub Workflow Status 12 | Go Reference 13 | Coverage Status 14 | Go Report Card 15 | License: MIT 16 |

17 |
18 | 19 | ### Roadmap 20 | 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. 21 | 22 | ### Community 23 | Pion has an active community on the [Slack](https://pion.ly/slack). 24 | 25 | Follow the [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. 26 | 27 | We are always looking to support **your projects**. Please reach out if you have something to build! 28 | 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) 29 | 30 | ### Contributing 31 | 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) 32 | 33 | ### License 34 | MIT License - see [LICENSE](LICENSE) for full text 35 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 2 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 3 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 4 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 5 | github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg= 6 | github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0= 7 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 8 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 9 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 10 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 11 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 12 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 13 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 14 | github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= 15 | github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 16 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 17 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 18 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 19 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 20 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 21 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 22 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 23 | golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= 24 | golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 25 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 26 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 27 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 28 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 30 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 31 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 32 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 33 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 34 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 35 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 36 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 37 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 38 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 39 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 40 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 41 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 42 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 43 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 44 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 45 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 46 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 47 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 48 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 49 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters-settings: 2 | govet: 3 | check-shadowing: true 4 | misspell: 5 | locale: US 6 | exhaustive: 7 | default-signifies-exhaustive: true 8 | gomodguard: 9 | blocked: 10 | modules: 11 | - github.com/pkg/errors: 12 | recommendations: 13 | - errors 14 | 15 | linters: 16 | enable: 17 | - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers 18 | - bidichk # Checks for dangerous unicode character sequences 19 | - bodyclose # checks whether HTTP response body is closed successfully 20 | - contextcheck # check the function whether use a non-inherited context 21 | - decorder # check declaration order and count of types, constants, variables and functions 22 | - depguard # Go linter that checks if package imports are in a list of acceptable packages 23 | - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) 24 | - dupl # Tool for code clone detection 25 | - durationcheck # check for two durations multiplied together 26 | - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases 27 | - 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. 28 | - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. 29 | - 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. 30 | - exhaustive # check exhaustiveness of enum switch statements 31 | - exportloopref # checks for pointers to enclosing loop variables 32 | - forcetypeassert # finds forced type assertions 33 | - gci # Gci control golang package import order and make it always deterministic. 34 | - gochecknoglobals # Checks that no globals are present in Go code 35 | - gochecknoinits # Checks that no init functions are present in Go code 36 | - gocognit # Computes and checks the cognitive complexity of functions 37 | - goconst # Finds repeated strings that could be replaced by a constant 38 | - gocritic # The most opinionated Go source code linter 39 | - godox # Tool for detection of FIXME, TODO and other comment keywords 40 | - goerr113 # Golang linter to check the errors handling expressions 41 | - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification 42 | - gofumpt # Gofumpt checks whether code was gofumpt-ed. 43 | - goheader # Checks is file header matches to pattern 44 | - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports 45 | - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. 46 | - 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. 47 | - goprintffuncname # Checks that printf-like functions are named with `f` at the end 48 | - gosec # Inspects source code for security problems 49 | - gosimple # Linter for Go source code that specializes in simplifying a code 50 | - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 51 | - grouper # An analyzer to analyze expression groups. 52 | - importas # Enforces consistent import aliases 53 | - ineffassign # Detects when assignments to existing variables are not used 54 | - misspell # Finds commonly misspelled English words in comments 55 | - nakedret # Finds naked returns in functions greater than a specified function length 56 | - nilerr # Finds the code that returns nil even if it checks that the error is not nil. 57 | - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. 58 | - noctx # noctx finds sending http request without context.Context 59 | - predeclared # find code that shadows one of Go's predeclared identifiers 60 | - revive # golint replacement, finds style mistakes 61 | - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks 62 | - stylecheck # Stylecheck is a replacement for golint 63 | - tagliatelle # Checks the struct tags. 64 | - tenv # tenv is analyzer that detects using os.Setenv instead of t.Setenv since Go1.17 65 | - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes 66 | - typecheck # Like the front-end of a Go compiler, parses and type-checks Go code 67 | - unconvert # Remove unnecessary type conversions 68 | - unparam # Reports unused function parameters 69 | - unused # Checks Go code for unused constants, variables, functions and types 70 | - wastedassign # wastedassign finds wasted assignment statements 71 | - whitespace # Tool for detection of leading and trailing whitespace 72 | disable: 73 | - containedctx # containedctx is a linter that detects struct contained context.Context field 74 | - cyclop # checks function and package cyclomatic complexity 75 | - exhaustivestruct # Checks if all struct's fields are initialized 76 | - forbidigo # Forbids identifiers 77 | - funlen # Tool for detection of long functions 78 | - gocyclo # Computes and checks the cyclomatic complexity of functions 79 | - godot # Check if comments end in a period 80 | - gomnd # An analyzer to detect magic numbers. 81 | - ifshort # Checks that your code uses short syntax for if-statements whenever possible 82 | - ireturn # Accept Interfaces, Return Concrete Types 83 | - lll # Reports long lines 84 | - maintidx # maintidx measures the maintainability index of each function. 85 | - makezero # Finds slice declarations with non-zero initial length 86 | - maligned # Tool to detect Go structs that would take less memory if their fields were sorted 87 | - nestif # Reports deeply nested if statements 88 | - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity 89 | - nolintlint # Reports ill-formed or insufficient nolint directives 90 | - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test 91 | - prealloc # Finds slice declarations that could potentially be preallocated 92 | - promlinter # Check Prometheus metrics naming via promlint 93 | - rowserrcheck # checks whether Err of rows is checked successfully 94 | - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. 95 | - testpackage # linter that makes you use a separate _test package 96 | - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers 97 | - varnamelen # checks that the length of a variable's name matches its scope 98 | - wrapcheck # Checks that errors returned from external packages are wrapped 99 | - wsl # Whitespace Linter - Forces you to use empty lines! 100 | 101 | issues: 102 | exclude-use-default: false 103 | exclude-rules: 104 | # Allow complex tests, better to be self contained 105 | - path: _test\.go 106 | linters: 107 | - gocognit 108 | 109 | # Allow complex main function in examples 110 | - path: examples 111 | text: "of func `main` is high" 112 | linters: 113 | - gocognit 114 | 115 | run: 116 | skip-dirs-use-default: false 117 | -------------------------------------------------------------------------------- /conn.go: -------------------------------------------------------------------------------- 1 | // Package udp provides a connection-oriented listener over a UDP PacketConn 2 | package udp 3 | 4 | import ( 5 | "context" 6 | "errors" 7 | "net" 8 | "sync" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/pion/transport/v2/deadline" 13 | "github.com/pion/transport/v2/packetio" 14 | pkgSync "github.com/pion/udp/v2/pkg/sync" 15 | ) 16 | 17 | const ( 18 | receiveMTU = 8192 19 | defaultListenBacklog = 128 // same as Linux default 20 | ) 21 | 22 | // Typed errors 23 | var ( 24 | ErrClosedListener = errors.New("udp: listener closed") 25 | ErrListenQueueExceeded = errors.New("udp: listen queue exceeded") 26 | ) 27 | 28 | // listener augments a connection-oriented Listener over a UDP PacketConn 29 | type listener struct { 30 | pConn *net.UDPConn 31 | 32 | accepting atomic.Value // bool 33 | acceptCh chan *Conn 34 | doneCh chan struct{} 35 | doneOnce sync.Once 36 | acceptFilter func([]byte) bool 37 | readBufferPool *sync.Pool 38 | 39 | connLock sync.Mutex 40 | conns map[string]*Conn 41 | connWG *pkgSync.WaitGroup 42 | 43 | readWG sync.WaitGroup 44 | errClose atomic.Value // error 45 | } 46 | 47 | // Accept waits for and returns the next connection to the listener. 48 | func (l *listener) Accept() (net.Conn, error) { 49 | select { 50 | case c := <-l.acceptCh: 51 | l.connWG.Add(1) 52 | return c, nil 53 | 54 | case <-l.doneCh: 55 | return nil, ErrClosedListener 56 | } 57 | } 58 | 59 | // Close closes the listener. 60 | // Any blocked Accept operations will be unblocked and return errors. 61 | func (l *listener) Close() error { 62 | var err error 63 | l.doneOnce.Do(func() { 64 | l.accepting.Store(false) 65 | close(l.doneCh) 66 | 67 | l.connLock.Lock() 68 | // Close unaccepted connections 69 | L_CLOSE: 70 | for { 71 | select { 72 | case c := <-l.acceptCh: 73 | close(c.doneCh) 74 | delete(l.conns, c.rAddr.String()) 75 | 76 | default: 77 | break L_CLOSE 78 | } 79 | } 80 | nConns := len(l.conns) 81 | l.connLock.Unlock() 82 | 83 | l.connWG.Done() 84 | 85 | if nConns == 0 { 86 | // Wait if this is the final connection 87 | l.readWG.Wait() 88 | if errClose, ok := l.errClose.Load().(error); ok { 89 | err = errClose 90 | } 91 | } else { 92 | err = nil 93 | } 94 | }) 95 | 96 | return err 97 | } 98 | 99 | // Addr returns the listener's network address. 100 | func (l *listener) Addr() net.Addr { 101 | return l.pConn.LocalAddr() 102 | } 103 | 104 | // ListenConfig stores options for listening to an address. 105 | type ListenConfig struct { 106 | // Backlog defines the maximum length of the queue of pending 107 | // connections. It is equivalent of the backlog argument of 108 | // POSIX listen function. 109 | // If a connection request arrives when the queue is full, 110 | // the request will be silently discarded, unlike TCP. 111 | // Set zero to use default value 128 which is same as Linux default. 112 | Backlog int 113 | 114 | // AcceptFilter determines whether the new conn should be made for 115 | // the incoming packet. If not set, any packet creates new conn. 116 | AcceptFilter func([]byte) bool 117 | } 118 | 119 | // Listen creates a new listener based on the ListenConfig. 120 | func (lc *ListenConfig) Listen(network string, laddr *net.UDPAddr) (net.Listener, error) { 121 | if lc.Backlog == 0 { 122 | lc.Backlog = defaultListenBacklog 123 | } 124 | 125 | conn, err := net.ListenUDP(network, laddr) 126 | if err != nil { 127 | return nil, err 128 | } 129 | 130 | l := &listener{ 131 | pConn: conn, 132 | acceptCh: make(chan *Conn, lc.Backlog), 133 | conns: make(map[string]*Conn), 134 | doneCh: make(chan struct{}), 135 | acceptFilter: lc.AcceptFilter, 136 | readBufferPool: &sync.Pool{ 137 | New: func() interface{} { 138 | buf := make([]byte, receiveMTU) 139 | return &buf 140 | }, 141 | }, 142 | connWG: pkgSync.NewWaitGroup(), 143 | } 144 | 145 | l.accepting.Store(true) 146 | l.connWG.Add(1) 147 | l.readWG.Add(2) // wait readLoop and Close execution routine 148 | 149 | go l.readLoop() 150 | go func() { 151 | l.connWG.Wait() 152 | if err := l.pConn.Close(); err != nil { 153 | l.errClose.Store(err) 154 | } 155 | l.readWG.Done() 156 | }() 157 | 158 | return l, nil 159 | } 160 | 161 | // Listen creates a new listener using default ListenConfig. 162 | func Listen(network string, laddr *net.UDPAddr) (net.Listener, error) { 163 | return (&ListenConfig{}).Listen(network, laddr) 164 | } 165 | 166 | // readLoop has to tasks: 167 | // 1. Dispatching incoming packets to the correct Conn. 168 | // It can therefore not be ended until all Conns are closed. 169 | // 2. Creating a new Conn when receiving from a new remote. 170 | func (l *listener) readLoop() { 171 | defer l.readWG.Done() 172 | 173 | for { 174 | buf, ok := l.readBufferPool.Get().(*[]byte) 175 | if !ok { 176 | return 177 | } 178 | 179 | n, raddr, err := l.pConn.ReadFrom(*buf) 180 | if err != nil { 181 | return 182 | } 183 | conn, ok, err := l.getConn(raddr, (*buf)[:n]) 184 | if err != nil { 185 | continue 186 | } 187 | if ok { 188 | _, _ = conn.buffer.Write((*buf)[:n]) 189 | } 190 | } 191 | } 192 | 193 | func (l *listener) getConn(raddr net.Addr, buf []byte) (*Conn, bool, error) { 194 | l.connLock.Lock() 195 | defer l.connLock.Unlock() 196 | conn, ok := l.conns[raddr.String()] 197 | if !ok { 198 | if isAccepting, ok := l.accepting.Load().(bool); !isAccepting || !ok { 199 | return nil, false, ErrClosedListener 200 | } 201 | if l.acceptFilter != nil { 202 | if !l.acceptFilter(buf) { 203 | return nil, false, nil 204 | } 205 | } 206 | conn = l.newConn(raddr) 207 | select { 208 | case l.acceptCh <- conn: 209 | l.conns[raddr.String()] = conn 210 | default: 211 | return nil, false, ErrListenQueueExceeded 212 | } 213 | } 214 | return conn, true, nil 215 | } 216 | 217 | // Conn augments a connection-oriented connection over a UDP PacketConn 218 | type Conn struct { 219 | listener *listener 220 | 221 | rAddr net.Addr 222 | 223 | buffer *packetio.Buffer 224 | 225 | doneCh chan struct{} 226 | doneOnce sync.Once 227 | 228 | writeDeadline *deadline.Deadline 229 | } 230 | 231 | func (l *listener) newConn(rAddr net.Addr) *Conn { 232 | return &Conn{ 233 | listener: l, 234 | rAddr: rAddr, 235 | buffer: packetio.NewBuffer(), 236 | doneCh: make(chan struct{}), 237 | writeDeadline: deadline.New(), 238 | } 239 | } 240 | 241 | // Read reads from c into p 242 | func (c *Conn) Read(p []byte) (int, error) { 243 | return c.buffer.Read(p) 244 | } 245 | 246 | // Write writes len(p) bytes from p to the DTLS connection 247 | func (c *Conn) Write(p []byte) (n int, err error) { 248 | select { 249 | case <-c.writeDeadline.Done(): 250 | return 0, context.DeadlineExceeded 251 | default: 252 | } 253 | return c.listener.pConn.WriteTo(p, c.rAddr) 254 | } 255 | 256 | // Close closes the conn and releases any Read calls 257 | func (c *Conn) Close() error { 258 | var err error 259 | c.doneOnce.Do(func() { 260 | c.listener.connWG.Done() 261 | close(c.doneCh) 262 | c.listener.connLock.Lock() 263 | delete(c.listener.conns, c.rAddr.String()) 264 | nConns := len(c.listener.conns) 265 | c.listener.connLock.Unlock() 266 | 267 | if isAccepting, ok := c.listener.accepting.Load().(bool); nConns == 0 && !isAccepting && ok { 268 | // Wait if this is the final connection 269 | c.listener.readWG.Wait() 270 | if errClose, ok := c.listener.errClose.Load().(error); ok { 271 | err = errClose 272 | } 273 | } else { 274 | err = nil 275 | } 276 | 277 | if errBuf := c.buffer.Close(); errBuf != nil && err == nil { 278 | err = errBuf 279 | } 280 | }) 281 | 282 | return err 283 | } 284 | 285 | // LocalAddr implements net.Conn.LocalAddr 286 | func (c *Conn) LocalAddr() net.Addr { 287 | return c.listener.pConn.LocalAddr() 288 | } 289 | 290 | // RemoteAddr implements net.Conn.RemoteAddr 291 | func (c *Conn) RemoteAddr() net.Addr { 292 | return c.rAddr 293 | } 294 | 295 | // SetDeadline implements net.Conn.SetDeadline 296 | func (c *Conn) SetDeadline(t time.Time) error { 297 | c.writeDeadline.Set(t) 298 | return c.SetReadDeadline(t) 299 | } 300 | 301 | // SetReadDeadline implements net.Conn.SetDeadline 302 | func (c *Conn) SetReadDeadline(t time.Time) error { 303 | return c.buffer.SetReadDeadline(t) 304 | } 305 | 306 | // SetWriteDeadline implements net.Conn.SetDeadline 307 | func (c *Conn) SetWriteDeadline(t time.Time) error { 308 | c.writeDeadline.Set(t) 309 | // Write deadline of underlying connection should not be changed 310 | // since the connection can be shared. 311 | return nil 312 | } 313 | -------------------------------------------------------------------------------- /conn_test.go: -------------------------------------------------------------------------------- 1 | //go:build !js 2 | // +build !js 3 | 4 | package udp 5 | 6 | import ( 7 | "bytes" 8 | "errors" 9 | "fmt" 10 | "io" 11 | "net" 12 | "sync" 13 | "testing" 14 | "time" 15 | 16 | "github.com/pion/transport/v2/test" 17 | ) 18 | 19 | var errHandshakeFailed = errors.New("handshake failed") 20 | 21 | // Note: doesn't work since closing isn't propagated to the other side 22 | // func TestNetTest(t *testing.T) { 23 | // lim := test.TimeOut(time.Minute*1 + time.Second*10) 24 | // defer lim.Stop() 25 | // 26 | // nettest.TestConn(t, func() (c1, c2 net.Conn, stop func(), err error) { 27 | // listener, c1, c2, err = pipe() 28 | // if err != nil { 29 | // return nil, nil, nil, err 30 | // } 31 | // stop = func() { 32 | // c1.Close() 33 | // c2.Close() 34 | // listener.Close(1 * time.Second) 35 | // } 36 | // return 37 | // }) 38 | //} 39 | 40 | func TestStressDuplex(t *testing.T) { 41 | // Limit runtime in case of deadlocks 42 | lim := test.TimeOut(time.Second * 20) 43 | defer lim.Stop() 44 | 45 | // Check for leaking routines 46 | report := test.CheckRoutines(t) 47 | defer report() 48 | 49 | // Run the test 50 | stressDuplex(t) 51 | } 52 | 53 | func stressDuplex(t *testing.T) { 54 | listener, ca, cb, err := pipe() 55 | if err != nil { 56 | t.Fatal(err) 57 | } 58 | 59 | defer func() { 60 | err = ca.Close() 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | err = cb.Close() 65 | if err != nil { 66 | t.Fatal(err) 67 | } 68 | err = listener.Close() 69 | if err != nil { 70 | t.Fatal(err) 71 | } 72 | }() 73 | 74 | opt := test.Options{ 75 | MsgSize: 2048, 76 | MsgCount: 1, // Can't rely on UDP message order in CI 77 | } 78 | 79 | err = test.StressDuplex(ca, cb, opt) 80 | if err != nil { 81 | t.Fatal(err) 82 | } 83 | } 84 | 85 | func TestListenerCloseTimeout(t *testing.T) { 86 | // Limit runtime in case of deadlocks 87 | lim := test.TimeOut(time.Second * 20) 88 | defer lim.Stop() 89 | 90 | // Check for leaking routines 91 | report := test.CheckRoutines(t) 92 | defer report() 93 | 94 | listener, ca, _, err := pipe() 95 | if err != nil { 96 | t.Fatal(err) 97 | } 98 | 99 | err = listener.Close() 100 | if err != nil { 101 | t.Fatal(err) 102 | } 103 | 104 | // Close client after server closes to cleanup 105 | err = ca.Close() 106 | if err != nil { 107 | t.Fatal(err) 108 | } 109 | } 110 | 111 | func TestListenerCloseUnaccepted(t *testing.T) { 112 | // Limit runtime in case of deadlocks 113 | lim := test.TimeOut(time.Second * 20) 114 | defer lim.Stop() 115 | 116 | // Check for leaking routines 117 | report := test.CheckRoutines(t) 118 | defer report() 119 | 120 | const backlog = 2 121 | 122 | network, addr := getConfig() 123 | listener, err := (&ListenConfig{ 124 | Backlog: backlog, 125 | }).Listen(network, addr) 126 | if err != nil { 127 | t.Fatal(err) 128 | } 129 | 130 | for i := 0; i < backlog; i++ { 131 | conn, derr := net.DialUDP(network, nil, listener.Addr().(*net.UDPAddr)) 132 | if derr != nil { 133 | t.Error(derr) 134 | continue 135 | } 136 | if _, werr := conn.Write([]byte{byte(i)}); werr != nil { 137 | t.Error(werr) 138 | } 139 | if cerr := conn.Close(); cerr != nil { 140 | t.Error(cerr) 141 | } 142 | } 143 | 144 | time.Sleep(100 * time.Millisecond) // Wait all packets being processed by readLoop 145 | 146 | // Unaccepted connections must be closed by listener.Close() 147 | err = listener.Close() 148 | if err != nil { 149 | t.Fatal(err) 150 | } 151 | } 152 | 153 | func TestListenerAcceptFilter(t *testing.T) { 154 | // Limit runtime in case of deadlocks 155 | lim := test.TimeOut(time.Second * 20) 156 | defer lim.Stop() 157 | 158 | // Check for leaking routines 159 | report := test.CheckRoutines(t) 160 | defer report() 161 | 162 | testCases := map[string]struct { 163 | packet []byte 164 | accept bool 165 | }{ 166 | "CreateConn": { 167 | packet: []byte{0xAA}, 168 | accept: true, 169 | }, 170 | "Discarded": { 171 | packet: []byte{0x00}, 172 | accept: false, 173 | }, 174 | } 175 | 176 | for name, testCase := range testCases { 177 | testCase := testCase 178 | t.Run(name, func(t *testing.T) { 179 | network, addr := getConfig() 180 | listener, err := (&ListenConfig{ 181 | AcceptFilter: func(pkt []byte) bool { 182 | return pkt[0] == 0xAA 183 | }, 184 | }).Listen(network, addr) 185 | if err != nil { 186 | t.Fatal(err) 187 | } 188 | 189 | var wgAcceptLoop sync.WaitGroup 190 | wgAcceptLoop.Add(1) 191 | defer func() { 192 | cerr := listener.Close() 193 | if cerr != nil { 194 | t.Fatal(cerr) 195 | } 196 | wgAcceptLoop.Wait() 197 | }() 198 | 199 | conn, derr := net.DialUDP(network, nil, listener.Addr().(*net.UDPAddr)) 200 | if derr != nil { 201 | t.Fatal(derr) 202 | } 203 | if _, werr := conn.Write(testCase.packet); werr != nil { 204 | t.Fatal(werr) 205 | } 206 | defer func() { 207 | if cerr := conn.Close(); cerr != nil { 208 | t.Error(cerr) 209 | } 210 | }() 211 | 212 | chAccepted := make(chan struct{}) 213 | go func() { 214 | defer wgAcceptLoop.Done() 215 | 216 | conn, aerr := listener.Accept() 217 | if aerr != nil { 218 | if !errors.Is(aerr, ErrClosedListener) { 219 | t.Error(aerr) 220 | } 221 | return 222 | } 223 | close(chAccepted) 224 | if cerr := conn.Close(); cerr != nil { 225 | t.Error(cerr) 226 | } 227 | }() 228 | 229 | var accepted bool 230 | select { 231 | case <-chAccepted: 232 | accepted = true 233 | case <-time.After(10 * time.Millisecond): 234 | } 235 | 236 | if accepted != testCase.accept { 237 | if testCase.accept { 238 | t.Error("Packet should create new conn") 239 | } else { 240 | t.Error("Packet should not create new conn") 241 | } 242 | } 243 | }) 244 | } 245 | } 246 | 247 | func TestListenerConcurrent(t *testing.T) { 248 | // Limit runtime in case of deadlocks 249 | lim := test.TimeOut(time.Second * 20) 250 | defer lim.Stop() 251 | 252 | // Check for leaking routines 253 | report := test.CheckRoutines(t) 254 | defer report() 255 | 256 | const backlog = 2 257 | 258 | network, addr := getConfig() 259 | listener, err := (&ListenConfig{ 260 | Backlog: backlog, 261 | }).Listen(network, addr) 262 | if err != nil { 263 | t.Fatal(err) 264 | } 265 | 266 | for i := 0; i < backlog+1; i++ { 267 | conn, derr := net.DialUDP(network, nil, listener.Addr().(*net.UDPAddr)) 268 | if derr != nil { 269 | t.Error(derr) 270 | continue 271 | } 272 | if _, werr := conn.Write([]byte{byte(i)}); werr != nil { 273 | t.Error(werr) 274 | } 275 | if cerr := conn.Close(); cerr != nil { 276 | t.Error(cerr) 277 | } 278 | } 279 | 280 | time.Sleep(100 * time.Millisecond) // Wait all packets being processed by readLoop 281 | 282 | for i := 0; i < backlog; i++ { 283 | conn, aerr := listener.Accept() 284 | if aerr != nil { 285 | t.Error(aerr) 286 | continue 287 | } 288 | b := make([]byte, 1) 289 | n, rerr := conn.Read(b) 290 | if rerr != nil { 291 | t.Error(rerr) 292 | } else if !bytes.Equal([]byte{byte(i)}, b[:n]) { 293 | t.Errorf("Packet from connection %d is wrong, expected: [%d], got: %v", i, i, b[:n]) 294 | } 295 | if err = conn.Close(); err != nil { 296 | t.Error(err) 297 | } 298 | } 299 | 300 | var wg sync.WaitGroup 301 | wg.Add(1) 302 | go func() { 303 | defer wg.Done() 304 | if conn, aerr := listener.Accept(); !errors.Is(aerr, ErrClosedListener) { 305 | t.Errorf("Connection exceeding backlog limit must be discarded: %v", aerr) 306 | if aerr == nil { 307 | _ = conn.Close() 308 | } 309 | } 310 | }() 311 | 312 | time.Sleep(100 * time.Millisecond) // Last Accept should be discarded 313 | err = listener.Close() 314 | if err != nil { 315 | t.Fatal(err) 316 | } 317 | 318 | wg.Wait() 319 | } 320 | 321 | func pipe() (net.Listener, net.Conn, *net.UDPConn, error) { 322 | // Start listening 323 | network, addr := getConfig() 324 | listener, err := Listen(network, addr) 325 | if err != nil { 326 | return nil, nil, nil, fmt.Errorf("failed to listen: %w", err) 327 | } 328 | 329 | // Open a connection 330 | var dConn *net.UDPConn 331 | dConn, err = net.DialUDP(network, nil, listener.Addr().(*net.UDPAddr)) 332 | if err != nil { 333 | return nil, nil, nil, fmt.Errorf("failed to dial: %w", err) 334 | } 335 | 336 | // Write to the connection to initiate it 337 | handshake := "hello" 338 | _, err = dConn.Write([]byte(handshake)) 339 | if err != nil { 340 | return nil, nil, nil, fmt.Errorf("failed to write to dialed Conn: %w", err) 341 | } 342 | 343 | // Accept the connection 344 | var lConn net.Conn 345 | lConn, err = listener.Accept() 346 | if err != nil { 347 | return nil, nil, nil, fmt.Errorf("failed to accept Conn: %w", err) 348 | } 349 | 350 | var n int 351 | buf := make([]byte, len(handshake)) 352 | if n, err = lConn.Read(buf); err != nil { 353 | return nil, nil, nil, fmt.Errorf("failed to read handshake: %w", err) 354 | } 355 | 356 | result := string(buf[:n]) 357 | if handshake != result { 358 | return nil, nil, nil, fmt.Errorf("%w: %s != %s", errHandshakeFailed, handshake, result) 359 | } 360 | 361 | return listener, lConn, dConn, nil 362 | } 363 | 364 | func getConfig() (string, *net.UDPAddr) { 365 | return "udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: 0} 366 | } 367 | 368 | func TestConnClose(t *testing.T) { 369 | lim := test.TimeOut(time.Second * 5) 370 | defer lim.Stop() 371 | 372 | t.Run("Close", func(t *testing.T) { 373 | // Check for leaking routines 374 | report := test.CheckRoutines(t) 375 | defer report() 376 | 377 | l, ca, cb, errPipe := pipe() 378 | if errPipe != nil { 379 | t.Fatal(errPipe) 380 | } 381 | if err := ca.Close(); err != nil { 382 | t.Errorf("Failed to close A side: %v", err) 383 | } 384 | if err := cb.Close(); err != nil { 385 | t.Errorf("Failed to close B side: %v", err) 386 | } 387 | if err := l.Close(); err != nil { 388 | t.Errorf("Failed to close listener: %v", err) 389 | } 390 | }) 391 | t.Run("CloseError1", func(t *testing.T) { 392 | // Check for leaking routines 393 | report := test.CheckRoutines(t) 394 | defer report() 395 | 396 | l, ca, cb, errPipe := pipe() 397 | if errPipe != nil { 398 | t.Fatal(errPipe) 399 | } 400 | // Close l.pConn to inject error. 401 | if err := l.(*listener).pConn.Close(); err != nil { //nolint:forcetypeassert 402 | t.Error(err) 403 | } 404 | 405 | if err := cb.Close(); err != nil { 406 | t.Errorf("Failed to close A side: %v", err) 407 | } 408 | if err := ca.Close(); err != nil { 409 | t.Errorf("Failed to close B side: %v", err) 410 | } 411 | if err := l.Close(); err == nil { 412 | t.Errorf("Error is not propagated to Listener.Close") 413 | } 414 | }) 415 | t.Run("CloseError2", func(t *testing.T) { 416 | // Check for leaking routines 417 | report := test.CheckRoutines(t) 418 | defer report() 419 | 420 | l, ca, cb, errPipe := pipe() 421 | if errPipe != nil { 422 | t.Fatal(errPipe) 423 | } 424 | // Close l.pConn to inject error. 425 | if err := l.(*listener).pConn.Close(); err != nil { //nolint:forcetypeassert 426 | t.Error(err) 427 | } 428 | 429 | if err := cb.Close(); err != nil { 430 | t.Errorf("Failed to close A side: %v", err) 431 | } 432 | if err := l.Close(); err != nil { 433 | t.Errorf("Failed to close listener: %v", err) 434 | } 435 | if err := ca.Close(); err == nil { 436 | t.Errorf("Error is not propagated to Conn.Close") 437 | } 438 | }) 439 | t.Run("CancelRead", func(t *testing.T) { 440 | // Limit runtime in case of deadlocks 441 | lim := test.TimeOut(time.Second * 5) 442 | defer lim.Stop() 443 | 444 | // Check for leaking routines 445 | report := test.CheckRoutines(t) 446 | defer report() 447 | 448 | l, ca, cb, errPipe := pipe() 449 | if errPipe != nil { 450 | t.Fatal(errPipe) 451 | } 452 | 453 | errC := make(chan error, 1) 454 | go func() { 455 | buf := make([]byte, 1024) 456 | // This read will block because we don't write on the other side. 457 | // Calling Close must unblock the call. 458 | _, readErr := ca.Read(buf) 459 | errC <- readErr 460 | }() 461 | 462 | if err := ca.Close(); err != nil { // Trigger Read cancellation. 463 | t.Errorf("Failed to close B side: %v", err) 464 | } 465 | 466 | // Main test condition, Read should return 467 | // after ca.Close() by closing the buffer. 468 | if err := <-errC; !errors.Is(err, io.EOF) { 469 | t.Errorf("expected err to be io.EOF but got %v", err) 470 | } 471 | 472 | if err := cb.Close(); err != nil { 473 | t.Errorf("Failed to close A side: %v", err) 474 | } 475 | if err := l.Close(); err != nil { 476 | t.Errorf("Failed to close listener: %v", err) 477 | } 478 | }) 479 | } 480 | --------------------------------------------------------------------------------