From 426799a231076a8b741d975f15f3a01cb48dcfb9 Mon Sep 17 00:00:00 2001 From: Zoltan Papp Date: Wed, 27 Aug 2025 18:19:42 +0200 Subject: [PATCH] Update to the latest upstream version (#4) --- .github/workflows/api.yaml | 20 + .github/workflows/release.yml | 2 +- .github/workflows/test.yaml | 8 +- .github/workflows/tidy-check.yaml | 2 +- .golangci.yml | 157 +- .reuse/dep5 | 4 +- README.md | 8 +- active_tcp.go | 45 +- active_tcp_test.go | 208 ++- addr.go | 132 +- agent.go | 848 +++++---- agent_config.go | 132 +- ..._get_best_available_candidate_pair_test.go | 17 +- agent_get_best_valid_candidate_pair_test.go | 8 +- agent_handlers.go | 168 +- agent_handlers_test.go | 72 + ..._on_selected_candidate_pair_change_test.go | 14 +- agent_stats.go | 119 +- agent_test.go | 1640 ++++++++++------- agent_udpmux_test.go | 29 +- candidate.go | 29 +- candidate_base.go | 740 ++++++-- candidate_host.go | 46 +- candidate_peer_reflexive.go | 18 +- candidate_relay.go | 47 +- candidate_relay_test.go | 53 +- candidate_server_reflexive.go | 33 +- candidate_server_reflexive_test.go | 47 +- candidate_test.go | 1206 ++++++++++-- candidatepair.go | 180 +- candidatepair_state.go | 7 +- candidatepair_test.go | 12 +- candidaterelatedaddress.go | 3 +- candidatetype.go | 9 +- connectivity_vnet_test.go | 214 +-- context.go | 40 - errors.go | 65 +- examples/ping-pong/main.go | 6 +- external_ip_mapper.go | 25 +- external_ip_mapper_test.go | 278 +-- gather.go | 284 ++- gather_test.go | 502 ++--- gather_vnet_test.go | 396 ++-- go.mod | 31 +- go.sum | 99 +- ice.go | 35 +- ice_test.go | 6 +- icecontrol.go | 8 +- icecontrol_test.go | 134 +- internal/atomic/atomic.go | 7 +- internal/fakenet/mock_conn.go | 2 +- internal/fakenet/packet_conn.go | 7 +- internal/stun/stun.go | 4 +- internal/taskloop/taskloop.go | 121 ++ mdns.go | 107 +- mdns_test.go | 235 ++- net.go | 128 +- net_test.go | 70 +- networktype.go | 23 +- networktype_test.go | 36 +- priority.go | 4 +- priority_test.go | 33 +- rand_test.go | 37 +- role.go | 1 + selection.go | 152 +- selection_test.go | 160 ++ stats.go | 14 +- tcp_mux.go | 87 +- tcp_mux_multi.go | 11 +- tcp_mux_multi_test.go | 25 +- tcp_mux_test.go | 60 +- tcp_packet_conn.go | 28 +- tcptype_test.go | 22 +- transport.go | 46 +- transport_test.go | 235 +-- transport_vnet_test.go | 49 +- udp_mux.go | 130 +- udp_mux_multi.go | 48 +- udp_mux_multi_test.go | 23 +- udp_mux_test.go | 104 +- udp_mux_universal.go | 54 +- udp_mux_universal_test.go | 6 +- udp_muxed_conn.go | 256 +-- url.go | 38 +- usecandidate.go | 4 +- usecandidate_test.go | 21 +- test_utils.go => utils_test.go | 15 + 87 files changed, 6846 insertions(+), 3743 deletions(-) create mode 100644 .github/workflows/api.yaml create mode 100644 agent_handlers_test.go delete mode 100644 context.go create mode 100644 internal/taskloop/taskloop.go create mode 100644 selection_test.go rename test_utils.go => utils_test.go (96%) diff --git a/.github/workflows/api.yaml b/.github/workflows/api.yaml new file mode 100644 index 0000000..1032179 --- /dev/null +++ b/.github/workflows/api.yaml @@ -0,0 +1,20 @@ +# +# 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: API +on: + pull_request: + +jobs: + check: + uses: pion/.goassets/.github/workflows/api.reusable.yml@master diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 01227e2..b4967b2 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -21,4 +21,4 @@ jobs: release: uses: pion/.goassets/.github/workflows/release.reusable.yml@master with: - go-version: '1.20' # auto-update/latest-go-version + go-version: "1.24" # auto-update/latest-go-version diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index c8294ef..7713e93 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,16 +23,17 @@ jobs: uses: pion/.goassets/.github/workflows/test.reusable.yml@master strategy: matrix: - go: ['1.21', '1.20'] # auto-update/supported-go-version-list + go: ["1.24", "1.23"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} + secrets: inherit test-i386: uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master strategy: matrix: - go: ['1.21', '1.20'] # auto-update/supported-go-version-list + go: ["1.24", "1.23"] # auto-update/supported-go-version-list fail-fast: false with: go-version: ${{ matrix.go }} @@ -40,4 +41,5 @@ jobs: test-wasm: uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master with: - go-version: '1.20' # auto-update/latest-go-version + go-version: "1.24" # auto-update/latest-go-version + secrets: inherit diff --git a/.github/workflows/tidy-check.yaml b/.github/workflows/tidy-check.yaml index 33d6b50..710dbc9 100644 --- a/.github/workflows/tidy-check.yaml +++ b/.github/workflows/tidy-check.yaml @@ -22,4 +22,4 @@ jobs: tidy: uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master with: - go-version: '1.21' # auto-update/latest-go-version + go-version: "1.24" # auto-update/latest-go-version diff --git a/.golangci.yml b/.golangci.yml index 6dd80c8..6fddc53 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,101 +1,71 @@ # 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)?$ - +version: "2" 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 + - containedctx # containedctx is a linter that detects struct contained context.Context field - contextcheck # check the function whether use a non-inherited context + - cyclop # checks function and package cyclomatic complexity - decorder # check declaration order and count of types, constants, variables and functions - 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 + - err113 # Golang linter to check the errors handling expressions - 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 + - gocyclo # Computes and checks the cyclomatic complexity of functions + - godot # Check if comments end in a period - 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 + - lll # Reports long lines + - maintidx # maintidx measures the maintainability index of each function. + - makezero # Finds slice declarations with non-zero initial length - misspell # Finds commonly misspelled English words in comments + - nakedret # Finds naked returns in functions greater than a specified function length + - nestif # Reports deeply nested if statements - 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. + - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity - 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 + - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers - unconvert # Remove unnecessary type conversions - unparam # Reports unused function parameters - unused # Checks Go code for unused constants, variables, functions and types + - varnamelen # checks that the length of a variable's name matches its scope - wastedassign # wastedassign finds wasted assignment statements - whitespace # Tool for detection of leading and trailing whitespace disable: - depguard # Go linter that checks if package imports are in a list of acceptable packages - - 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 + - gochecknoinits # Checks that no init functions are present in Go code + - 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. + - interfacebloat # A linter that checks length of interface. - 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 - - nakedret # Finds naked returns in functions greater than a specified function length - - nestif # Reports deeply nested if statements - - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity + - mnd # An analyzer to detect magic numbers - 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 @@ -103,24 +73,77 @@ linters: - 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 + - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes - 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 and examples, better to be self contained - - path: (examples|main\.go|_test\.go) - linters: - - forbidigo - - gocognit - - # Allow forbidden identifiers in CLI commands - - path: cmd - linters: - - forbidigo - -run: - skip-dirs-use-default: false + settings: + staticcheck: + checks: + - all + # "could remove embedded field", to keep it explicit! + - -QF1008 + # "could use tagged switch on enum", Cases conflicts with exhaustive! + - -QF1003 + exhaustive: + default-signifies-exhaustive: true + forbidigo: + forbid: + - pattern: ^fmt.Print(f|ln)?$ + - pattern: ^log.(Panic|Fatal|Print)(f|ln)?$ + - pattern: ^os.Exit$ + - pattern: ^panic$ + - pattern: ^print(ln)?$ + - pattern: ^testing.T.(Error|Errorf|Fatal|Fatalf|Fail|FailNow)$ + pkg: ^testing$ + msg: use testify/assert instead + analyze-types: true + gomodguard: + blocked: + modules: + - github.com/pkg/errors: + recommendations: + - errors + govet: + enable: + - shadow + revive: + rules: + # Prefer 'any' type alias over 'interface{}' for Go 1.18+ compatibility + - name: use-any + severity: warning + disabled: false + misspell: + locale: US + varnamelen: + max-distance: 12 + min-name-length: 2 + ignore-type-assert-ok: true + ignore-map-index-ok: true + ignore-chan-recv-ok: true + ignore-decls: + - i int + - n int + - w io.Writer + - r io.Reader + - b []byte + exclusions: + generated: lax + rules: + - linters: + - forbidigo + - gocognit + path: (examples|main\.go) + - linters: + - gocognit + path: _test\.go + - linters: + - forbidigo + path: cmd +formatters: + enable: + - gci + - gofmt + - gofumpt + - goimports + exclusions: + generated: lax diff --git a/.reuse/dep5 b/.reuse/dep5 index 717f0c1..b26c56d 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -2,10 +2,10 @@ Format: 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 **/go.mod **/go.sum .eslintrc.json package.json examples/examples.json +Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json yarn.lock Copyright: 2023 The Pion community License: MIT -Files: testdata/fuzz/* **/testdata/fuzz/* api/*.txt +Files: testdata/seed/* testdata/fuzz/* **/testdata/fuzz/* api/*.txt Copyright: 2023 The Pion community License: CC0-1.0 diff --git a/README.md b/README.md index 3035594..5171a41 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

A Go implementation of ICE

Pion ICE - Slack Widget + join us on Discord Follow us on Bluesky
GitHub Workflow Status - Go Reference + Go Reference Coverage Status Go Report Card License: MIT @@ -20,9 +20,9 @@ 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). +Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt). -Follow the [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. +Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [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) diff --git a/active_tcp.go b/active_tcp.go index 4ffcb6e..2fd466b 100644 --- a/active_tcp.go +++ b/active_tcp.go @@ -7,6 +7,7 @@ import ( "context" "io" "net" + "net/netip" "sync/atomic" "time" @@ -17,10 +18,15 @@ import ( type activeTCPConn struct { readBuffer, writeBuffer *packetio.Buffer localAddr, remoteAddr atomic.Value - closed int32 + closed atomic.Bool } -func newActiveTCPConn(ctx context.Context, localAddress, remoteAddress string, log logging.LeveledLogger) (a *activeTCPConn) { +func newActiveTCPConn( + ctx context.Context, + localAddress string, + remoteAddress netip.AddrPort, + log logging.LeveledLogger, +) (a *activeTCPConn) { a = &activeTCPConn{ readBuffer: packetio.NewBuffer(), writeBuffer: packetio.NewBuffer(), @@ -28,40 +34,43 @@ func newActiveTCPConn(ctx context.Context, localAddress, remoteAddress string, l laddr, err := getTCPAddrOnInterface(localAddress) if err != nil { - atomic.StoreInt32(&a.closed, 1) + a.closed.Store(true) log.Infof("Failed to dial TCP address %s: %v", remoteAddress, err) - return + + return a } a.localAddr.Store(laddr) go func() { defer func() { - atomic.StoreInt32(&a.closed, 1) + a.closed.Store(true) }() dialer := &net.Dialer{ LocalAddr: laddr, } - conn, err := dialer.DialContext(ctx, "tcp", remoteAddress) + conn, err := dialer.DialContext(ctx, "tcp", remoteAddress.String()) if err != nil { log.Infof("Failed to dial TCP address %s: %v", remoteAddress, err) + return } - a.remoteAddr.Store(conn.RemoteAddr()) go func() { buff := make([]byte, receiveMTU) - for atomic.LoadInt32(&a.closed) == 0 { + for !a.closed.Load() { n, err := readStreamingPacket(conn, buff) if err != nil { log.Infof("Failed to read streaming packet: %s", err) + break } if _, err := a.readBuffer.Write(buff[:n]); err != nil { log.Infof("Failed to write to buffer: %s", err) + break } } @@ -69,15 +78,17 @@ func newActiveTCPConn(ctx context.Context, localAddress, remoteAddress string, l buff := make([]byte, receiveMTU) - for atomic.LoadInt32(&a.closed) == 0 { + for !a.closed.Load() { n, err := a.writeBuffer.Read(buff) if err != nil { log.Infof("Failed to read from buffer: %s", err) + break } if _, err = writeStreamingPacket(conn, buff[:n]); err != nil { log.Infof("Failed to write streaming packet: %s", err) + break } } @@ -91,17 +102,19 @@ func newActiveTCPConn(ctx context.Context, localAddress, remoteAddress string, l } func (a *activeTCPConn) ReadFrom(buff []byte) (n int, srcAddr net.Addr, err error) { - if atomic.LoadInt32(&a.closed) == 1 { + if a.closed.Load() { return 0, nil, io.ErrClosedPipe } - srcAddr = a.RemoteAddr() n, err = a.readBuffer.Read(buff) + // RemoteAddr is assuredly set *after* we can read from the buffer + srcAddr = a.RemoteAddr() + return } func (a *activeTCPConn) WriteTo(buff []byte, _ net.Addr) (n int, err error) { - if atomic.LoadInt32(&a.closed) == 1 { + if a.closed.Load() { return 0, io.ErrClosedPipe } @@ -109,9 +122,10 @@ func (a *activeTCPConn) WriteTo(buff []byte, _ net.Addr) (n int, err error) { } func (a *activeTCPConn) Close() error { - atomic.StoreInt32(&a.closed, 1) + a.closed.Store(true) _ = a.readBuffer.Close() _ = a.writeBuffer.Close() + return nil } @@ -123,6 +137,11 @@ func (a *activeTCPConn) LocalAddr() net.Addr { return &net.TCPAddr{} } +// RemoteAddr returns the remote address of the connection which is only +// set once a background goroutine has successfully dialed. That means +// this may return ":0" for the address prior to that happening. If this +// becomes an issue, we can introduce a synchronization point between Dial +// and these methods. func (a *activeTCPConn) RemoteAddr() net.Addr { if v, ok := a.remoteAddr.Load().(*net.TCPAddr); ok { return v diff --git a/active_tcp_test.go b/active_tcp_test.go index e2f2752..4959976 100644 --- a/active_tcp_test.go +++ b/active_tcp_test.go @@ -7,47 +7,54 @@ package ice import ( + "fmt" "net" + "net/netip" + "sync/atomic" "testing" "time" "github.com/pion/logging" "github.com/pion/transport/v3/stdnet" "github.com/pion/transport/v3/test" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) -func getLocalIPAddress(t *testing.T, networkType NetworkType) net.IP { +func getLocalIPAddress(t *testing.T, networkType NetworkType) netip.Addr { + t.Helper() + net, err := stdnet.NewNet() require.NoError(t, err) - localIPs, err := localInterfaces(net, nil, nil, []NetworkType{networkType}, false) + _, localAddrs, err := localInterfaces(net, problematicNetworkInterfaces, nil, []NetworkType{networkType}, false) require.NoError(t, err) - require.NotEmpty(t, localIPs) - return localIPs[0] + require.NotEmpty(t, localAddrs) + + return localAddrs[0] } func ipv6Available(t *testing.T) bool { + t.Helper() + net, err := stdnet.NewNet() require.NoError(t, err) - localIPs, err := localInterfaces(net, nil, nil, []NetworkType{NetworkTypeTCP6}, false) + _, localAddrs, err := localInterfaces(net, problematicNetworkInterfaces, nil, []NetworkType{NetworkTypeTCP6}, false) require.NoError(t, err) - return len(localIPs) > 0 + + return len(localAddrs) > 0 } func TestActiveTCP(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 5) - defer lim.Stop() + defer test.TimeOut(time.Second * 5).Stop() const listenPort = 7686 type testCase struct { name string networkTypes []NetworkType - listenIPAddress net.IP + listenIPAddress netip.Addr selectedPairNetworkType string + useMDNS bool } testCases := []testCase{ @@ -72,25 +79,30 @@ func TestActiveTCP(t *testing.T) { networkTypes: []NetworkType{NetworkTypeTCP6}, listenIPAddress: getLocalIPAddress(t, NetworkTypeTCP6), selectedPairNetworkType: tcp, + // if we don't use mDNS, we will very likely be filtering out location tracked ips. + useMDNS: true, }, testCase{ - name: "UDP is preferred over TCP6", // This fails some time + name: "UDP is preferred over TCP6", networkTypes: supportedNetworkTypes(), listenIPAddress: getLocalIPAddress(t, NetworkTypeTCP6), selectedPairNetworkType: udp, + // if we don't use mDNS, we will very likely be filtering out location tracked ips. + useMDNS: true, }, ) } for _, testCase := range testCases { t.Run(testCase.name, func(t *testing.T) { - r := require.New(t) + req := require.New(t) listener, err := net.ListenTCP("tcp", &net.TCPAddr{ - IP: testCase.listenIPAddress, + IP: testCase.listenIPAddress.AsSlice(), Port: listenPort, + Zone: testCase.listenIPAddress.Zone(), }) - r.NoError(err) + req.NoError(err) defer func() { _ = listener.Close() }() @@ -107,104 +119,174 @@ func TestActiveTCP(t *testing.T) { _ = tcpMux.Close() }() - r.NotNil(tcpMux.LocalAddr(), "tcpMux.LocalAddr() is nil") + req.NotNil(tcpMux.LocalAddr(), "tcpMux.LocalAddr() is nil") hostAcceptanceMinWait := 100 * time.Millisecond - passiveAgent, err := NewAgent(&AgentConfig{ + cfg := &AgentConfig{ TCPMux: tcpMux, CandidateTypes: []CandidateType{CandidateTypeHost}, NetworkTypes: testCase.networkTypes, LoggerFactory: loggerFactory, - IncludeLoopback: true, HostAcceptanceMinWait: &hostAcceptanceMinWait, - }) - r.NoError(err) - r.NotNil(passiveAgent) + InterfaceFilter: problematicNetworkInterfaces, + } + if testCase.useMDNS { + cfg.MulticastDNSMode = MulticastDNSModeQueryAndGather + } + passiveAgent, err := NewAgent(cfg) + req.NoError(err) + req.NotNil(passiveAgent) activeAgent, err := NewAgent(&AgentConfig{ CandidateTypes: []CandidateType{CandidateTypeHost}, NetworkTypes: testCase.networkTypes, LoggerFactory: loggerFactory, HostAcceptanceMinWait: &hostAcceptanceMinWait, + InterfaceFilter: problematicNetworkInterfaces, }) - r.NoError(err) - r.NotNil(activeAgent) + req.NoError(err) + req.NotNil(activeAgent) - passiveAgentConn, activeAgenConn := connect(passiveAgent, activeAgent) - r.NotNil(passiveAgentConn) - r.NotNil(activeAgenConn) + passiveAgentConn, activeAgenConn := connect(t, passiveAgent, activeAgent) + req.NotNil(passiveAgentConn) + req.NotNil(activeAgenConn) + + defer func() { + req.NoError(activeAgenConn.Close()) + req.NoError(passiveAgentConn.Close()) + }() pair := passiveAgent.getSelectedPair() - r.NotNil(pair) - r.Equal(testCase.selectedPairNetworkType, pair.Local.NetworkType().NetworkShort()) + req.NotNil(pair) + req.Equal(testCase.selectedPairNetworkType, pair.Local.NetworkType().NetworkShort()) foo := []byte("foo") _, err = passiveAgentConn.Write(foo) - r.NoError(err) + req.NoError(err) buffer := make([]byte, 1024) n, err := activeAgenConn.Read(buffer) - r.NoError(err) - r.Equal(foo, buffer[:n]) + req.NoError(err) + req.Equal(foo, buffer[:n]) bar := []byte("bar") _, err = activeAgenConn.Write(bar) - r.NoError(err) + req.NoError(err) n, err = passiveAgentConn.Read(buffer) - r.NoError(err) - r.Equal(bar, buffer[:n]) - - r.NoError(activeAgenConn.Close()) - r.NoError(passiveAgentConn.Close()) + req.NoError(err) + req.Equal(bar, buffer[:n]) }) } } -// Assert that Active TCP connectivity isn't established inside -// the main thread of the Agent +// Assert that Active TCP connectivity isn't established inside. +// the main thread of the Agent. func TestActiveTCP_NonBlocking(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 5) - defer lim.Stop() + defer test.TimeOut(time.Second * 5).Stop() cfg := &AgentConfig{ - NetworkTypes: supportedNetworkTypes(), + NetworkTypes: supportedNetworkTypes(), + InterfaceFilter: problematicNetworkInterfaces, } aAgent, err := NewAgent(cfg) - if err != nil { - t.Error(err) - } + require.NoError(t, err) + + defer func() { + require.NoError(t, aAgent.Close()) + }() bAgent, err := NewAgent(cfg) - if err != nil { - t.Error(err) - } + require.NoError(t, err) - isConnected := make(chan interface{}) + defer func() { + require.NoError(t, bAgent.Close()) + }() + + isConnected := make(chan any) err = aAgent.OnConnectionStateChange(func(c ConnectionState) { if c == ConnectionStateConnected { close(isConnected) } }) - if err != nil { - t.Error(err) - } + require.NoError(t, err) // Add a invalid ice-tcp candidate to each invalidCandidate, err := UnmarshalCandidate("1052353102 1 tcp 1675624447 192.0.2.1 8080 typ host tcptype passive") - if err != nil { - t.Fatal(err) - } - assert.NoError(t, aAgent.AddRemoteCandidate(invalidCandidate)) - assert.NoError(t, bAgent.AddRemoteCandidate(invalidCandidate)) + require.NoError(t, err) + require.NoError(t, aAgent.AddRemoteCandidate(invalidCandidate)) + require.NoError(t, bAgent.AddRemoteCandidate(invalidCandidate)) - connect(aAgent, bAgent) + connect(t, aAgent, bAgent) <-isConnected - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) +} + +// Assert that we ignore remote TCP candidates when running a UDP Only Agent. +func TestActiveTCP_Respect_NetworkTypes(t *testing.T) { + defer test.CheckRoutines(t)() + defer test.TimeOut(time.Second * 5).Stop() + + tcpListener, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + _, port, err := net.SplitHostPort(tcpListener.Addr().String()) + require.NoError(t, err) + + var incomingTCPCount uint64 + go func() { + for { + conn, listenErr := tcpListener.Accept() + if listenErr != nil { + return + } + + require.NoError(t, conn.Close()) + atomic.AddUint64(&incomingTCPCount, ^uint64(0)) + } + }() + + cfg := &AgentConfig{ + NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6, NetworkTypeTCP6}, + InterfaceFilter: problematicNetworkInterfaces, + IncludeLoopback: true, + } + + aAgent, err := NewAgent(cfg) + require.NoError(t, err) + + defer func() { + require.NoError(t, aAgent.Close()) + }() + + bAgent, err := NewAgent(cfg) + require.NoError(t, err) + + defer func() { + require.NoError(t, bAgent.Close()) + }() + + isConnected := make(chan any) + err = aAgent.OnConnectionStateChange(func(c ConnectionState) { + if c == ConnectionStateConnected { + close(isConnected) + } + }) + require.NoError(t, err) + + invalidCandidate, err := UnmarshalCandidate( + fmt.Sprintf("1052353102 1 tcp 1675624447 127.0.0.1 %s typ host tcptype passive", port), + ) + require.NoError(t, err) + require.NoError(t, aAgent.AddRemoteCandidate(invalidCandidate)) + require.NoError(t, bAgent.AddRemoteCandidate(invalidCandidate)) + + connect(t, aAgent, bAgent) + + <-isConnected + require.NoError(t, tcpListener.Close()) + require.Equal(t, uint64(0), atomic.LoadUint64(&incomingTCPCount)) } diff --git a/addr.go b/addr.go index 1d70025..fad58b9 100644 --- a/addr.go +++ b/addr.go @@ -4,52 +4,133 @@ package ice import ( + "fmt" "net" + "net/netip" ) -func parseMulticastAnswerAddr(in net.Addr) (net.IP, bool) { +func addrWithOptionalZone(addr netip.Addr, zone string) netip.Addr { + if zone == "" { + return addr + } + if addr.Is6() && (addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast()) { + return addr.WithZone(zone) + } + + return addr +} + +// parseAddrFromIface should only be used when it's known the address belongs to that interface. +// e.g. it's LocalAddress on a listener. +func parseAddrFromIface(in net.Addr, ifcName string) (netip.Addr, int, NetworkType, error) { + addr, port, nt, err := parseAddr(in) + if err != nil { + return netip.Addr{}, 0, 0, err + } + if _, ok := in.(*net.IPNet); ok { + // net.IPNet does not have a Zone but we provide it from the interface + addr = addrWithOptionalZone(addr, ifcName) + } + + return addr, port, nt, nil +} + +func parseAddr(in net.Addr) (netip.Addr, int, NetworkType, error) { //nolint:cyclop switch addr := in.(type) { + case *net.IPNet: + ipAddr, err := ipAddrToNetIP(addr.IP, "") + if err != nil { + return netip.Addr{}, 0, 0, err + } + + return ipAddr, 0, 0, nil case *net.IPAddr: - return addr.IP, true + ipAddr, err := ipAddrToNetIP(addr.IP, addr.Zone) + if err != nil { + return netip.Addr{}, 0, 0, err + } + + return ipAddr, 0, 0, nil case *net.UDPAddr: - return addr.IP, true + ipAddr, err := ipAddrToNetIP(addr.IP, addr.Zone) + if err != nil { + return netip.Addr{}, 0, 0, err + } + var nt NetworkType + if ipAddr.Is4() { + nt = NetworkTypeUDP4 + } else { + nt = NetworkTypeUDP6 + } + + return ipAddr, addr.Port, nt, nil case *net.TCPAddr: - return addr.IP, true + ipAddr, err := ipAddrToNetIP(addr.IP, addr.Zone) + if err != nil { + return netip.Addr{}, 0, 0, err + } + var nt NetworkType + if ipAddr.Is4() { + nt = NetworkTypeTCP4 + } else { + nt = NetworkTypeTCP6 + } + + return ipAddr, addr.Port, nt, nil + default: + return netip.Addr{}, 0, 0, addrParseError{in} } - return nil, false } -func parseAddr(in net.Addr) (net.IP, int, NetworkType, bool) { - switch addr := in.(type) { - case *net.UDPAddr: - return addr.IP, addr.Port, NetworkTypeUDP4, true - case *net.TCPAddr: - return addr.IP, addr.Port, NetworkTypeTCP4, true - } - return nil, 0, 0, false +type addrParseError struct { + addr net.Addr } -func createAddr(network NetworkType, ip net.IP, port int) net.Addr { +func (e addrParseError) Error() string { + return fmt.Sprintf("do not know how to parse address type %T", e.addr) +} + +type ipConvertError struct { + ip []byte +} + +func (e ipConvertError) Error() string { + return fmt.Sprintf("failed to convert IP '%s' to netip.Addr", e.ip) +} + +func ipAddrToNetIP(ip []byte, zone string) (netip.Addr, error) { + netIPAddr, ok := netip.AddrFromSlice(ip) + if !ok { + return netip.Addr{}, ipConvertError{ip} + } + // we'd rather have an IPv4-mapped IPv6 become IPv4 so that it is usable. + netIPAddr = netIPAddr.Unmap() + netIPAddr = addrWithOptionalZone(netIPAddr, zone) + + return netIPAddr, nil +} + +func createAddr(network NetworkType, ip netip.Addr, port int) net.Addr { switch { case network.IsTCP(): - return &net.TCPAddr{IP: ip, Port: port} + return &net.TCPAddr{IP: ip.AsSlice(), Port: port, Zone: ip.Zone()} default: - return &net.UDPAddr{IP: ip, Port: port} + return &net.UDPAddr{IP: ip.AsSlice(), Port: port, Zone: ip.Zone()} } } func addrEqual(a, b net.Addr) bool { - aIP, aPort, aType, aOk := parseAddr(a) - if !aOk { + aIP, aPort, aType, aErr := parseAddr(a) + if aErr != nil { return false } - bIP, bPort, bType, bOk := parseAddr(b) - if !bOk { + bIP, bPort, bType, bErr := parseAddr(b) + if bErr != nil { return false } - return aType == bType && aIP.Equal(bIP) && aPort == bPort + return aType == bType && aIP.Compare(bIP) == 0 && aPort == bPort } // AddrPort is an IP and a port number. @@ -60,12 +141,13 @@ func toAddrPort(addr net.Addr) AddrPort { switch addr := addr.(type) { case *net.UDPAddr: copy(ap[:16], addr.IP.To16()) - ap[16] = uint8(addr.Port >> 8) - ap[17] = uint8(addr.Port) + ap[16] = uint8(addr.Port >> 8) //nolint:gosec // G115 false positive + ap[17] = uint8(addr.Port) //nolint:gosec // G115 false positive case *net.TCPAddr: copy(ap[:16], addr.IP.To16()) - ap[16] = uint8(addr.Port >> 8) - ap[17] = uint8(addr.Port) + ap[16] = uint8(addr.Port >> 8) //nolint:gosec // G115 false positive + ap[17] = uint8(addr.Port) //nolint:gosec // G115 false positive } + return ap } diff --git a/agent.go b/agent.go index d30281f..5220c1d 100644 --- a/agent.go +++ b/agent.go @@ -8,24 +8,24 @@ package ice import ( "context" "fmt" + "math" "net" - "strconv" + "net/netip" "strings" "sync" "sync/atomic" "time" + stunx "github.com/pion/ice/v4/internal/stun" + "github.com/pion/ice/v4/internal/taskloop" "github.com/pion/logging" - "github.com/pion/mdns" - "github.com/pion/stun/v2" + "github.com/pion/mdns/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3" "github.com/pion/transport/v3/packetio" "github.com/pion/transport/v3/stdnet" "github.com/pion/transport/v3/vnet" "golang.org/x/net/proxy" - - atomicx "github.com/pion/ice/v3/internal/atomic" - stunx "github.com/pion/ice/v3/internal/stun" ) type bindingRequest struct { @@ -35,18 +35,14 @@ type bindingRequest struct { isUseCandidate bool } -// Agent represents the ICE agent +// Agent represents the ICE agent. type Agent struct { - chanTask chan task - afterRunFn []func(ctx context.Context) - muAfterRun sync.Mutex + loop *taskloop.Loop - onConnectionStateChangeHdlr atomic.Value // func(ConnectionState) - onSelectedCandidatePairChangeHdlr atomic.Value // func(Candidate, Candidate) - onCandidateHdlr atomic.Value // func(Candidate) - onSuccessfulSelectedPairBindingResponseHdlr atomic.Value // func(*Candidate) + onConnectionStateChangeHdlr atomic.Value // func(ConnectionState) + onSelectedCandidatePairChangeHdlr atomic.Value // func(Candidate, Candidate) + onCandidateHdlr atomic.Value // func(Candidate) - // State owned by the taskLoop onConnected chan struct{} onConnectedOnce sync.Once @@ -66,7 +62,7 @@ type Agent struct { muHaveStarted sync.Mutex startedCh <-chan struct{} startedFn func() - isControlling bool + isControlling atomic.Bool maxBindingRequests uint16 @@ -74,6 +70,7 @@ type Agent struct { srflxAcceptanceMinWait time.Duration prflxAcceptanceMinWait time.Duration relayAcceptanceMinWait time.Duration + stunGatherTimeout time.Duration tcpPriorityOffset uint16 disableActiveTCP bool @@ -107,7 +104,9 @@ type Agent struct { remoteCandidates map[NetworkType][]Candidate checklist []*CandidatePair - selector pairCandidateSelector + + selectorLock sync.RWMutex + selector pairCandidateSelector selectedPair atomic.Value // *CandidatePair @@ -122,17 +121,16 @@ type Agent struct { // 1:1 D-NAT IP address mapping extIPMapper *externalIPMapper - // State for closing - done chan struct{} - taskLoopDone chan struct{} - err atomicx.Error + // Callback that allows user to implement custom behavior + // for STUN Binding Requests + userBindingRequestHandler func(m *stun.Message, local, remote Candidate, pair *CandidatePair) bool gatherCandidateCancel func() gatherCandidateDone chan struct{} - chanCandidate chan Candidate - chanCandidatePair chan *CandidatePair - chanState chan ConnectionState + connectionStateNotifier *handlerNotifier + candidateNotifier *handlerNotifier + selectedCandidatePairNotifier *handlerNotifier loggerFactory logging.LoggerFactory log logging.LeveledLogger @@ -142,113 +140,19 @@ type Agent struct { udpMux UDPMux udpMuxSrflx UniversalUDPMux - interfaceFilter func(string) bool - ipFilter func(net.IP) bool + interfaceFilter func(string) (keep bool) + ipFilter func(net.IP) (keep bool) includeLoopback bool insecureSkipVerify bool proxyDialer proxy.Dialer + + enableUseCandidateCheckPriority bool } -type task struct { - fn func(context.Context, *Agent) - done chan struct{} -} - -// afterRun registers function to be run after the task. -func (a *Agent) afterRun(f func(context.Context)) { - a.muAfterRun.Lock() - a.afterRunFn = append(a.afterRunFn, f) - a.muAfterRun.Unlock() -} - -func (a *Agent) getAfterRunFn() []func(context.Context) { - a.muAfterRun.Lock() - defer a.muAfterRun.Unlock() - fns := a.afterRunFn - a.afterRunFn = nil - return fns -} - -func (a *Agent) ok() error { - select { - case <-a.done: - return a.getErr() - default: - } - return nil -} - -func (a *Agent) getErr() error { - if err := a.err.Load(); err != nil { - return err - } - return ErrClosed -} - -// Run task in serial. Blocking tasks must be cancelable by context. -func (a *Agent) run(ctx context.Context, t func(context.Context, *Agent)) error { - if err := a.ok(); err != nil { - return err - } - done := make(chan struct{}) - select { - case <-ctx.Done(): - return ctx.Err() - case a.chanTask <- task{t, done}: - <-done - return nil - } -} - -// taskLoop handles registered tasks and agent close. -func (a *Agent) taskLoop() { - after := func() { - for { - // Get and run func registered by afterRun(). - fns := a.getAfterRunFn() - if len(fns) == 0 { - break - } - for _, fn := range fns { - fn(a.context()) - } - } - } - defer func() { - a.deleteAllCandidates() - a.startedFn() - - if err := a.buf.Close(); err != nil { - a.log.Warnf("Failed to close buffer: %v", err) - } - - a.closeMulticastConn() - a.updateConnectionState(ConnectionStateClosed) - - after() - - close(a.chanState) - close(a.chanCandidate) - close(a.chanCandidatePair) - close(a.taskLoopDone) - }() - - for { - select { - case <-a.done: - return - case t := <-a.chanTask: - t.fn(a.context(), a) - close(t.done) - after() - } - } -} - -// NewAgent creates a new Agent -func NewAgent(config *AgentConfig) (*Agent, error) { //nolint:gocognit +// NewAgent creates a new Agent. +func NewAgent(config *AgentConfig) (*Agent, error) { //nolint:gocognit,cyclop var err error if config.PortMax < config.PortMin { return nil, ErrPort @@ -278,34 +182,28 @@ func NewAgent(config *AgentConfig) (*Agent, error) { //nolint:gocognit startedCtx, startedFn := context.WithCancel(context.Background()) - a := &Agent{ - chanTask: make(chan task), - chanState: make(chan ConnectionState), - chanCandidate: make(chan Candidate), - chanCandidatePair: make(chan *CandidatePair), - tieBreaker: globalMathRandomGenerator.Uint64(), - lite: config.Lite, - gatheringState: GatheringStateNew, - connectionState: ConnectionStateNew, - localCandidates: make(map[NetworkType][]Candidate), - remoteCandidates: make(map[NetworkType][]Candidate), - urls: config.Urls, - networkTypes: config.NetworkTypes, - onConnected: make(chan struct{}), - buf: packetio.NewBuffer(), - done: make(chan struct{}), - taskLoopDone: make(chan struct{}), - startedCh: startedCtx.Done(), - startedFn: startedFn, - portMin: config.PortMin, - portMax: config.PortMax, - loggerFactory: loggerFactory, - log: log, - net: config.Net, - proxyDialer: config.ProxyDialer, - tcpMux: config.TCPMux, - udpMux: config.UDPMux, - udpMuxSrflx: config.UDPMuxSrflx, + agent := &Agent{ + tieBreaker: globalMathRandomGenerator.Uint64(), + lite: config.Lite, + gatheringState: GatheringStateNew, + connectionState: ConnectionStateNew, + localCandidates: make(map[NetworkType][]Candidate), + remoteCandidates: make(map[NetworkType][]Candidate), + urls: config.Urls, + networkTypes: config.NetworkTypes, + onConnected: make(chan struct{}), + buf: packetio.NewBuffer(), + startedCh: startedCtx.Done(), + startedFn: startedFn, + portMin: config.PortMin, + portMax: config.PortMax, + loggerFactory: loggerFactory, + log: log, + net: config.Net, + proxyDialer: config.ProxyDialer, + tcpMux: config.TCPMux, + udpMux: config.UDPMux, + udpMuxSrflx: config.UDPMuxSrflx, mDNSMode: mDNSMode, mDNSName: mDNSName, @@ -323,65 +221,113 @@ func NewAgent(config *AgentConfig) (*Agent, error) { //nolint:gocognit includeLoopback: config.IncludeLoopback, disableActiveTCP: config.DisableActiveTCP, + + userBindingRequestHandler: config.BindingRequestHandler, + + enableUseCandidateCheckPriority: config.EnableUseCandidateCheckPriority, + } + agent.connectionStateNotifier = &handlerNotifier{ + connectionStateFunc: agent.onConnectionStateChange, + done: make(chan struct{}), + } + agent.candidateNotifier = &handlerNotifier{candidateFunc: agent.onCandidate, done: make(chan struct{})} + agent.selectedCandidatePairNotifier = &handlerNotifier{ + candidatePairFunc: agent.onSelectedCandidatePairChange, + done: make(chan struct{}), } - if a.net == nil { - a.net, err = stdnet.NewNet() + if agent.net == nil { + agent.net, err = stdnet.NewNet() if err != nil { return nil, fmt.Errorf("failed to create network: %w", err) } - } else if _, isVirtual := a.net.(*vnet.Net); isVirtual { - a.log.Warn("Virtual network is enabled") - if a.mDNSMode != MulticastDNSModeDisabled { - a.log.Warn("Virtual network does not support mDNS yet") + } else if _, isVirtual := agent.net.(*vnet.Net); isVirtual { + agent.log.Warn("Virtual network is enabled") + if agent.mDNSMode != MulticastDNSModeDisabled { + agent.log.Warn("Virtual network does not support mDNS yet") } } + localIfcs, _, err := localInterfaces( + agent.net, + agent.interfaceFilter, + agent.ipFilter, + agent.networkTypes, + agent.includeLoopback, + ) + if err != nil { + return nil, fmt.Errorf("error getting local interfaces: %w", err) + } + // Opportunistic mDNS: If we can't open the connection, that's ok: we // can continue without it. - if a.mDNSConn, a.mDNSMode, err = createMulticastDNS(a.net, mDNSMode, mDNSName, log); err != nil { + if agent.mDNSConn, agent.mDNSMode, err = createMulticastDNS( + agent.net, + agent.networkTypes, + localIfcs, + agent.includeLoopback, + mDNSMode, + mDNSName, + log, + loggerFactory, + ); err != nil { log.Warnf("Failed to initialize mDNS %s: %v", mDNSName, err) } - config.initWithDefaults(a) + config.initWithDefaults(agent) // Make sure the buffer doesn't grow indefinitely. // NOTE: We actually won't get anywhere close to this limit. // SRTP will constantly read from the endpoint and drop packets if it's full. - a.buf.SetLimitSize(maxBufferSize) + agent.buf.SetLimitSize(maxBufferSize) + + if agent.lite && (len(agent.candidateTypes) != 1 || agent.candidateTypes[0] != CandidateTypeHost) { + agent.closeMulticastConn() - if a.lite && (len(a.candidateTypes) != 1 || a.candidateTypes[0] != CandidateTypeHost) { - a.closeMulticastConn() return nil, ErrLiteUsingNonHostCandidates } - if config.Urls != nil && len(config.Urls) > 0 && !containsCandidateType(CandidateTypeServerReflexive, a.candidateTypes) && !containsCandidateType(CandidateTypeRelay, a.candidateTypes) { - a.closeMulticastConn() + if len(config.Urls) > 0 && + !containsCandidateType(CandidateTypeServerReflexive, agent.candidateTypes) && + !containsCandidateType(CandidateTypeRelay, agent.candidateTypes) { + agent.closeMulticastConn() + return nil, ErrUselessUrlsProvided } - if err = config.initExtIPMapping(a); err != nil { - a.closeMulticastConn() + if err = config.initExtIPMapping(agent); err != nil { + agent.closeMulticastConn() + return nil, err } - go a.taskLoop() + agent.loop = taskloop.New(func() { + agent.removeUfragFromMux() + agent.deleteAllCandidates() + agent.startedFn() - // CandidatePair and ConnectionState are usually changed at once. - // Blocking one by the other one causes deadlock. - // Hence, we call handlers from independent Goroutines. - go a.candidatePairRoutine() - go a.connectionStateRoutine() - go a.candidateRoutine() + if err := agent.buf.Close(); err != nil { + agent.log.Warnf("Failed to close buffer: %v", err) + } + + agent.closeMulticastConn() + agent.updateConnectionState(ConnectionStateClosed) + + agent.gatherCandidateCancel() + if agent.gatherCandidateDone != nil { + <-agent.gatherCandidateDone + } + }) // Restart is also used to initialize the agent for the first time - if err := a.Restart(config.LocalUfrag, config.LocalPwd); err != nil { - a.closeMulticastConn() - _ = a.Close() + if err := agent.Restart(config.LocalUfrag, config.LocalPwd); err != nil { + agent.closeMulticastConn() + _ = agent.Close() + return nil, err } - return a, nil + return agent, nil } func (a *Agent) startConnectivityChecks(isControlling bool, remoteUfrag, remotePwd string) error { @@ -398,37 +344,27 @@ func (a *Agent) startConnectivityChecks(isControlling bool, remoteUfrag, remoteP a.log.Debugf("Started agent: isControlling? %t, remoteUfrag: %q, remotePwd: %q", isControlling, remoteUfrag, remotePwd) - return a.run(a.context(), func(ctx context.Context, agent *Agent) { - agent.isControlling = isControlling - agent.remoteUfrag = remoteUfrag - agent.remotePwd = remotePwd + return a.loop.Run(a.loop, func(_ context.Context) { + a.isControlling.Store(isControlling) + a.remoteUfrag = remoteUfrag + a.remotePwd = remotePwd + a.setSelector() - if isControlling { - a.selector = &controllingSelector{agent: a, log: a.log} - } else { - a.selector = &controlledSelector{agent: a, log: a.log} - } - - if a.lite { - a.selector = &liteSelector{pairCandidateSelector: a.selector} - } - - a.selector.Start() a.startedFn() - agent.updateConnectionState(ConnectionStateChecking) + a.updateConnectionState(ConnectionStateChecking) a.requestConnectivityCheck() go a.connectivityChecks() //nolint:contextcheck }) } -func (a *Agent) connectivityChecks() { +func (a *Agent) connectivityChecks() { //nolint:cyclop lastConnectionState := ConnectionState(0) checkingDuration := time.Time{} contact := func() { - if err := a.run(a.context(), func(ctx context.Context, a *Agent) { + if err := a.loop.Run(a.loop, func(_ context.Context) { defer func() { lastConnectionState = a.connectionState }() @@ -447,17 +383,21 @@ func (a *Agent) connectivityChecks() { // We have been in checking longer then Disconnect+Failed timeout, set the connection to Failed if time.Since(checkingDuration) > a.disconnectedTimeout+a.failedTimeout { a.updateConnectionState(ConnectionStateFailed) + return } default: } - a.selector.ContactCandidates() + a.getSelector().ContactCandidates() }); err != nil { a.log.Warnf("Failed to start connectivity checks: %v", err) } } + timer := time.NewTimer(math.MaxInt64) + timer.Stop() + for { interval := defaultKeepaliveInterval @@ -478,15 +418,19 @@ func (a *Agent) connectivityChecks() { updateInterval(a.disconnectedTimeout) updateInterval(a.failedTimeout) - t := time.NewTimer(interval) + timer.Reset(interval) + select { case <-a.forceCandidateContact: - t.Stop() + if !timer.Stop() { + <-timer.C + } contact() - case <-t.C: + case <-timer.C: contact() - case <-a.done: - t.Stop() + case <-a.loop.Done(): + timer.Stop() + return } } @@ -505,36 +449,27 @@ func (a *Agent) updateConnectionState(newState ConnectionState) { a.log.Infof("Setting new connection state: %s", newState) a.connectionState = newState - - // Call handler after finishing current task since we may be holding the agent lock - // and the handler may also require it - a.afterRun(func(ctx context.Context) { - a.chanState <- newState - }) + a.connectionStateNotifier.EnqueueConnectionState(newState) } } -func (a *Agent) setSelectedPair(p *CandidatePair) { - if p == nil { +func (a *Agent) setSelectedPair(pair *CandidatePair) { + if pair == nil { var nilPair *CandidatePair a.selectedPair.Store(nilPair) a.log.Tracef("Unset selected candidate pair") + return } - p.nominated = true - a.selectedPair.Store(p) - a.log.Tracef("Set selected candidate pair: %s", p) + pair.nominated = true + a.selectedPair.Store(pair) + a.log.Tracef("Set selected candidate pair: %s", pair) a.updateConnectionState(ConnectionStateConnected) // Notify when the selected pair changes - a.afterRun(func(ctx context.Context) { - select { - case a.chanCandidatePair <- p: - case <-ctx.Done(): - } - }) + a.selectedCandidatePairNotifier.EnqueueSelectedCandidatePair(pair) // Signal connected a.onConnectedOnce.Do(func() { close(a.onConnected) }) @@ -558,7 +493,7 @@ func (a *Agent) pingAllCandidates() { a.log.Tracef("Maximum requests reached for pair %s, marking it as failed", p) p.state = CandidatePairStateFailed } else { - a.selector.PingCandidate(p.Local, p.Remote) + a.getSelector().PingCandidate(p.Local, p.Remote) p.bindingRequestCount++ } } @@ -577,6 +512,7 @@ func (a *Agent) getBestAvailableCandidatePair() *CandidatePair { best = p } } + return best } @@ -593,12 +529,14 @@ func (a *Agent) getBestValidCandidatePair() *CandidatePair { best = p } } + return best } func (a *Agent) addPair(local, remote Candidate) *CandidatePair { - p := newCandidatePair(local, remote, a.isControlling) + p := newCandidatePair(local, remote, a.isControlling.Load()) a.checklist = append(a.checklist, p) + return p } @@ -608,6 +546,7 @@ func (a *Agent) findPair(local, remote Candidate) *CandidatePair { return p } } + return nil } @@ -648,83 +587,83 @@ func (a *Agent) checkKeepalive() { return } - if (a.keepaliveInterval != 0) && - ((time.Since(selectedPair.Local.LastSent()) > a.keepaliveInterval) || - (time.Since(selectedPair.Remote.LastReceived()) > a.keepaliveInterval) || - (time.Since(selectedPair.lastBindingRequest) > a.keepaliveInterval)) { + if a.keepaliveInterval != 0 { // We use binding request instead of indication to support refresh consent schemas // see https://tools.ietf.org/html/rfc7675 - a.selector.PingCandidate(selectedPair.Local, selectedPair.Remote) + a.getSelector().PingCandidate(selectedPair.Local, selectedPair.Remote) } } -// AddRemoteCandidate adds a new remote candidate -func (a *Agent) AddRemoteCandidate(c Candidate) error { - if c == nil { +// AddRemoteCandidate adds a new remote candidate. +func (a *Agent) AddRemoteCandidate(cand Candidate) error { + if cand == nil { return nil } // TCP Candidates with TCP type active will probe server passive ones, so // no need to do anything with them. - if c.TCPType() == TCPTypeActive { - a.log.Infof("Ignoring remote candidate with tcpType active: %s", c) + if cand.TCPType() == TCPTypeActive { + a.log.Infof("Ignoring remote candidate with tcpType active: %s", cand) + return nil } // If we have a mDNS Candidate lets fully resolve it before adding it locally - if c.Type() == CandidateTypeHost && strings.HasSuffix(c.Address(), ".local") { + if cand.Type() == CandidateTypeHost && strings.HasSuffix(cand.Address(), ".local") { if a.mDNSMode == MulticastDNSModeDisabled { - a.log.Warnf("Remote mDNS candidate added, but mDNS is disabled: (%s)", c.Address()) + a.log.Warnf("Remote mDNS candidate added, but mDNS is disabled: (%s)", cand.Address()) + return nil } - hostCandidate, ok := c.(*CandidateHost) + hostCandidate, ok := cand.(*CandidateHost) if !ok { return ErrAddressParseFailed } go a.resolveAndAddMulticastCandidate(hostCandidate) + return nil } go func() { - if err := a.run(a.context(), func(ctx context.Context, agent *Agent) { + if err := a.loop.Run(a.loop, func(_ context.Context) { // nolint: contextcheck - agent.addRemoteCandidate(c) + a.addRemoteCandidate(cand) }); err != nil { - a.log.Warnf("Failed to add remote candidate %s: %v", c.Address(), err) + a.log.Warnf("Failed to add remote candidate %s: %v", cand.Address(), err) + return } }() + return nil } -func (a *Agent) resolveAndAddMulticastCandidate(c *CandidateHost) { +func (a *Agent) resolveAndAddMulticastCandidate(cand *CandidateHost) { if a.mDNSConn == nil { return } - _, src, err := a.mDNSConn.Query(c.context(), c.Address()) + + _, src, err := a.mDNSConn.QueryAddr(cand.context(), cand.Address()) if err != nil { - a.log.Warnf("Failed to discover mDNS candidate %s: %v", c.Address(), err) + a.log.Warnf("Failed to discover mDNS candidate %s: %v", cand.Address(), err) + return } - ip, ipOk := parseMulticastAnswerAddr(src) - if !ipOk { - a.log.Warnf("Failed to discover mDNS candidate %s: failed to parse IP", c.Address()) + if err = cand.setIPAddr(src); err != nil { + a.log.Warnf("Failed to discover mDNS candidate %s: %v", cand.Address(), err) + return } - if err = c.setIP(ip); err != nil { - a.log.Warnf("Failed to discover mDNS candidate %s: %v", c.Address(), err) - return - } - - if err = a.run(a.context(), func(ctx context.Context, agent *Agent) { + if err = a.loop.Run(a.loop, func(_ context.Context) { // nolint: contextcheck - agent.addRemoteCandidate(c) + a.addRemoteCandidate(cand) }); err != nil { - a.log.Warnf("Failed to add mDNS candidate %s: %v", c.Address(), err) + a.log.Warnf("Failed to add mDNS candidate %s: %v", cand.Address(), err) + return } } @@ -737,23 +676,38 @@ func (a *Agent) requestConnectivityCheck() { } func (a *Agent) addRemotePassiveTCPCandidate(remoteCandidate Candidate) { - localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{remoteCandidate.NetworkType()}, a.includeLoopback) + _, localIPs, err := localInterfaces( + a.net, + a.interfaceFilter, + a.ipFilter, + []NetworkType{remoteCandidate.NetworkType()}, + a.includeLoopback, + ) if err != nil { a.log.Warnf("Failed to iterate local interfaces, host candidates will not be gathered %s", err) + return } for i := range localIPs { + ip, _, _, err := parseAddr(remoteCandidate.addr()) + if err != nil { + a.log.Warnf("Failed to parse address: %s; error: %s", remoteCandidate.addr(), err) + + continue + } + conn := newActiveTCPConn( - a.context(), + a.loop, net.JoinHostPort(localIPs[i].String(), "0"), - net.JoinHostPort(remoteCandidate.Address(), strconv.Itoa(remoteCandidate.Port())), + netip.AddrPortFrom(ip, uint16(remoteCandidate.Port())), //nolint:gosec // G115, no overflow, a port a.log, ) tcpAddr, ok := conn.LocalAddr().(*net.TCPAddr) if !ok { closeConnAndLog(conn, a.log, "Failed to create Active ICE-TCP Candidate: %v", errInvalidAddress) + continue } @@ -766,45 +720,52 @@ func (a *Agent) addRemotePassiveTCPCandidate(remoteCandidate Candidate) { }) if err != nil { closeConnAndLog(conn, a.log, "Failed to create Active ICE-TCP Candidate: %v", err) + continue } localCandidate.start(a, conn, a.startedCh) - a.localCandidates[localCandidate.NetworkType()] = append(a.localCandidates[localCandidate.NetworkType()], localCandidate) - a.chanCandidate <- localCandidate + a.localCandidates[localCandidate.NetworkType()] = append( + a.localCandidates[localCandidate.NetworkType()], + localCandidate, + ) + a.candidateNotifier.EnqueueCandidate(localCandidate) a.addPair(localCandidate, remoteCandidate) } } -// addRemoteCandidate assumes you are holding the lock (must be execute using a.run) -func (a *Agent) addRemoteCandidate(c Candidate) { - set := a.remoteCandidates[c.NetworkType()] +// addRemoteCandidate assumes you are holding the lock (must be execute using a.run). +func (a *Agent) addRemoteCandidate(cand Candidate) { //nolint:cyclop + set := a.remoteCandidates[cand.NetworkType()] for _, candidate := range set { - if candidate.Equal(c) { + if candidate.Equal(cand) { return } } - tcpNetworkTypeFound := false - for _, networkType := range a.networkTypes { - if networkType.IsTCP() { - tcpNetworkTypeFound = true + acceptRemotePassiveTCPCandidate := false + // Assert that TCP4 or TCP6 is a enabled NetworkType locally + if !a.disableActiveTCP && cand.TCPType() == TCPTypePassive { + for _, networkType := range a.networkTypes { + if cand.NetworkType() == networkType { + acceptRemotePassiveTCPCandidate = true + } } } - if !a.disableActiveTCP && tcpNetworkTypeFound && c.TCPType() == TCPTypePassive { - a.addRemotePassiveTCPCandidate(c) + if acceptRemotePassiveTCPCandidate { + a.addRemotePassiveTCPCandidate(cand) } - set = append(set, c) - a.remoteCandidates[c.NetworkType()] = set + set = append(set, cand) + a.remoteCandidates[cand.NetworkType()] = set - if c.TCPType() != TCPTypePassive { - if localCandidates, ok := a.localCandidates[c.NetworkType()]; ok { + if cand.TCPType() != TCPTypePassive { + if localCandidates, ok := a.localCandidates[cand.NetworkType()]; ok { for _, localCandidate := range localCandidates { - a.addPair(localCandidate, c) + a.addPair(localCandidate, cand) } } } @@ -812,46 +773,60 @@ func (a *Agent) addRemoteCandidate(c Candidate) { a.requestConnectivityCheck() } -func (a *Agent) addCandidate(ctx context.Context, c Candidate, candidateConn net.PacketConn) error { - return a.run(ctx, func(ctx context.Context, agent *Agent) { - set := a.localCandidates[c.NetworkType()] +func (a *Agent) addCandidate(ctx context.Context, cand Candidate, candidateConn net.PacketConn) error { + return a.loop.Run(ctx, func(context.Context) { + set := a.localCandidates[cand.NetworkType()] for _, candidate := range set { - if candidate.Equal(c) { - a.log.Debugf("Ignore duplicate candidate: %s", c.String()) - if err := c.close(); err != nil { + if candidate.Equal(cand) { + a.log.Debugf("Ignore duplicate candidate: %s", cand) + if err := cand.close(); err != nil { a.log.Warnf("Failed to close duplicate candidate: %v", err) } if err := candidateConn.Close(); err != nil { a.log.Warnf("Failed to close duplicate candidate connection: %v", err) } + return } } - c.start(a, candidateConn, a.startedCh) + a.setCandidateExtensions(cand) + cand.start(a, candidateConn, a.startedCh) - set = append(set, c) - a.localCandidates[c.NetworkType()] = set + set = append(set, cand) + a.localCandidates[cand.NetworkType()] = set - if remoteCandidates, ok := a.remoteCandidates[c.NetworkType()]; ok { + if remoteCandidates, ok := a.remoteCandidates[cand.NetworkType()]; ok { for _, remoteCandidate := range remoteCandidates { - a.addPair(c, remoteCandidate) + a.addPair(cand, remoteCandidate) } } a.requestConnectivityCheck() - a.chanCandidate <- c + if !cand.filterForLocationTracking() { + a.candidateNotifier.EnqueueCandidate(cand) + } }) } -// GetRemoteCandidates returns the remote candidates +func (a *Agent) setCandidateExtensions(cand Candidate) { + err := cand.AddExtension(CandidateExtension{ + Key: "ufrag", + Value: a.localUfrag, + }) + if err != nil { + a.log.Errorf("Failed to add ufrag extension to candidate: %v", err) + } +} + +// GetRemoteCandidates returns the remote candidates. func (a *Agent) GetRemoteCandidates() ([]Candidate, error) { var res []Candidate - err := a.run(a.context(), func(ctx context.Context, agent *Agent) { + err := a.loop.Run(a.loop, func(_ context.Context) { var candidates []Candidate - for _, set := range agent.remoteCandidates { + for _, set := range a.remoteCandidates { candidates = append(candidates, set...) } res = candidates @@ -863,14 +838,19 @@ func (a *Agent) GetRemoteCandidates() ([]Candidate, error) { return res, nil } -// GetLocalCandidates returns the local candidates +// GetLocalCandidates returns the local candidates. func (a *Agent) GetLocalCandidates() ([]Candidate, error) { var res []Candidate - err := a.run(a.context(), func(ctx context.Context, agent *Agent) { + err := a.loop.Run(a.loop, func(_ context.Context) { var candidates []Candidate - for _, set := range agent.localCandidates { - candidates = append(candidates, set...) + for _, set := range a.localCandidates { + for _, c := range set { + if c.filterForLocationTracking() { + continue + } + candidates = append(candidates, c) + } } res = candidates }) @@ -881,33 +861,35 @@ func (a *Agent) GetLocalCandidates() ([]Candidate, error) { return res, nil } -// GetLocalUserCredentials returns the local user credentials +// GetLocalUserCredentials returns the local user credentials. func (a *Agent) GetLocalUserCredentials() (frag string, pwd string, err error) { valSet := make(chan struct{}) - err = a.run(a.context(), func(ctx context.Context, agent *Agent) { - frag = agent.localUfrag - pwd = agent.localPwd + err = a.loop.Run(a.loop, func(_ context.Context) { + frag = a.localUfrag + pwd = a.localPwd close(valSet) }) if err == nil { <-valSet } + return } -// GetRemoteUserCredentials returns the remote user credentials +// GetRemoteUserCredentials returns the remote user credentials. func (a *Agent) GetRemoteUserCredentials() (frag string, pwd string, err error) { valSet := make(chan struct{}) - err = a.run(a.context(), func(ctx context.Context, agent *Agent) { - frag = agent.remoteUfrag - pwd = agent.remotePwd + err = a.loop.Run(a.loop, func(_ context.Context) { + frag = a.remoteUfrag + pwd = a.remotePwd close(valSet) }) if err == nil { <-valSet } + return } @@ -923,31 +905,35 @@ func (a *Agent) removeUfragFromMux() { } } -// Close cleans up the Agent +// Close cleans up the Agent. func (a *Agent) Close() error { - if err := a.ok(); err != nil { - return err - } + return a.close(false) +} - a.afterRun(func(context.Context) { - a.gatherCandidateCancel() - if a.gatherCandidateDone != nil { - <-a.gatherCandidateDone - } - }) - a.err.Store(ErrClosed) +// GracefulClose cleans up the Agent and waits for any goroutines it started +// to complete. This is only safe to call outside of Agent callbacks or if in a callback, +// in its own goroutine. +func (a *Agent) GracefulClose() error { + return a.close(true) +} - a.removeUfragFromMux() +func (a *Agent) close(graceful bool) error { + // the loop is safe to wait on no matter what + a.loop.Close() + + // but we are in less control of the notifiers, so we will + // pass through `graceful`. + a.connectionStateNotifier.Close(graceful) + a.candidateNotifier.Close(graceful) + a.selectedCandidatePairNotifier.Close(graceful) - close(a.done) - <-a.taskLoopDone return nil } // Remove all candidates. This closes any listening sockets // and removes both the local and remote candidate lists. // -// This is used for restarts, failures and on close +// This is used for restarts, failures and on close. func (a *Agent) deleteAllCandidates() { for net, cs := range a.localCandidates { for _, c := range cs { @@ -968,9 +954,10 @@ func (a *Agent) deleteAllCandidates() { } func (a *Agent) findRemoteCandidate(networkType NetworkType, addr net.Addr) Candidate { - ip, port, _, ok := parseAddr(addr) - if !ok { - a.log.Warnf("Failed to parse address: %s", addr) + ip, port, _, err := parseAddr(addr) + if err != nil { + a.log.Warnf("Failed to parse address: %s; error: %s", addr, err) + return nil } @@ -980,38 +967,42 @@ func (a *Agent) findRemoteCandidate(networkType NetworkType, addr net.Addr) Cand return c } } + return nil } -func (a *Agent) sendBindingRequest(m *stun.Message, local, remote Candidate) { - a.log.Tracef("Ping STUN from %s to %s", local.String(), remote.String()) +func (a *Agent) sendBindingRequest(msg *stun.Message, local, remote Candidate) { + a.log.Tracef("Ping STUN from %s to %s", local, remote) a.invalidatePendingBindingRequests(time.Now()) a.pendingBindingRequests = append(a.pendingBindingRequests, bindingRequest{ timestamp: time.Now(), - transactionID: m.TransactionID, + transactionID: msg.TransactionID, destination: remote.addr(), - isUseCandidate: m.Contains(stun.AttrUseCandidate), + isUseCandidate: msg.Contains(stun.AttrUseCandidate), }) - p := a.findPair(local, remote) - p.markBindingRequest(m.TransactionID) - - a.sendSTUN(m, local, remote) + if pair := a.findPair(local, remote); pair != nil { + pair.UpdateRequestSent() + } else { + a.log.Warnf("Failed to find pair for add binding request from %s to %s", local, remote) + } + a.sendSTUN(msg, local, remote) } func (a *Agent) sendBindingSuccess(m *stun.Message, local, remote Candidate) { base := remote - ip, port, _, ok := parseAddr(base.addr()) - if !ok { - a.log.Warnf("Failed to parse address: %s", base.addr()) + ip, port, _, err := parseAddr(base.addr()) + if err != nil { + a.log.Warnf("Failed to parse address: %s; error: %s", base.addr(), err) + return } if out, err := stun.Build(m, stun.BindingSuccess, &stun.XORMappedAddress{ - IP: ip, + IP: ip.AsSlice(), Port: port, }, stun.NewShortTermIntegrity(a.localPwd), @@ -1019,6 +1010,11 @@ func (a *Agent) sendBindingSuccess(m *stun.Message, local, remote Candidate) { ); err != nil { a.log.Warnf("Failed to handle inbound ICE from: %s to: %s error: %s", local, remote, err) } else { + if pair := a.findPair(local, remote); pair != nil { + pair.UpdateResponseSent() + } else { + a.log.Warnf("Failed to find pair for add binding response from %s to %s", local, remote) + } a.sendSTUN(out, local, remote) } } @@ -1045,77 +1041,106 @@ func (a *Agent) invalidatePendingBindingRequests(filterTime time.Time) { } // Assert that the passed TransactionID is in our pendingBindingRequests and returns the destination -// If the bindingRequest was valid remove it from our pending cache -func (a *Agent) handleInboundBindingSuccess(id [stun.TransactionIDSize]byte) (bool, *bindingRequest) { +// If the bindingRequest was valid remove it from our pending cache. +func (a *Agent) handleInboundBindingSuccess(id [stun.TransactionIDSize]byte) (bool, *bindingRequest, time.Duration) { a.invalidatePendingBindingRequests(time.Now()) for i := range a.pendingBindingRequests { if a.pendingBindingRequests[i].transactionID == id { validBindingRequest := a.pendingBindingRequests[i] a.pendingBindingRequests = append(a.pendingBindingRequests[:i], a.pendingBindingRequests[i+1:]...) - return true, &validBindingRequest + + return true, &validBindingRequest, time.Since(validBindingRequest.timestamp) } } - return false, nil + + return false, nil, 0 } -// handleInbound processes STUN traffic from a remote candidate -func (a *Agent) handleInbound(m *stun.Message, local Candidate, remote net.Addr) { //nolint:gocognit - var err error - if m == nil || local == nil { - return - } +func (a *Agent) handleRoleConflict(msg *stun.Message, local, remote Candidate, remoteTieBreaker *AttrControl) { + localIsGreaterOrEqual := a.tieBreaker >= remoteTieBreaker.Tiebreaker + a.log.Warnf("Role conflict local and remote same role(%s), localIsGreaterOrEqual(%t)", a.role(), localIsGreaterOrEqual) - if m.Type.Method != stun.MethodBinding || - !(m.Type.Class == stun.ClassSuccessResponse || - m.Type.Class == stun.ClassRequest || - m.Type.Class == stun.ClassIndication) { - a.log.Tracef("Unhandled STUN from %s to %s class(%s) method(%s)", remote, local, m.Type.Class, m.Type.Method) - return - } + // https://datatracker.ietf.org/doc/html/rfc8445#section-7.3.1.1 + // An agent MUST examine the Binding request for either the ICE- + // CONTROLLING or ICE-CONTROLLED attribute. It MUST follow these + // procedures: - if a.isControlling { - if m.Contains(stun.AttrICEControlling) { - a.log.Debug("Inbound STUN message: isControlling && a.isControlling == true") - return - } else if m.Contains(stun.AttrUseCandidate) { - a.log.Debug("Inbound STUN message: useCandidate && a.isControlling == true") - return + // If the agent's tiebreaker value is larger than or equal to the contents of the ICE-CONTROLLING attribute + // If the agent's tiebreaker value is less than the contents of the ICE-CONTROLLED attribute + // the agent generates a Binding error response + if (a.isControlling.Load() && localIsGreaterOrEqual) || (!a.isControlling.Load() && !localIsGreaterOrEqual) { + if roleConflictMsg, err := stun.Build(msg, stun.BindingError, + stun.ErrorCodeAttribute{ + Code: stun.CodeRoleConflict, + Reason: []byte("Role Conflict"), + }, + stun.NewShortTermIntegrity(a.localPwd), + stun.Fingerprint, + ); err != nil { + a.log.Warnf("Failed to generate Role Conflict message from: %s to: %s error: %s", local, remote, err) + } else { + a.sendSTUN(roleConflictMsg, local, remote) } } else { - if m.Contains(stun.AttrICEControlled) { - a.log.Debug("Inbound STUN message: isControlled && a.isControlling == false") - return - } + a.isControlling.Store(!a.isControlling.Load()) + a.setSelector() + } +} + +// handleInbound processes STUN traffic from a remote candidate. +func (a *Agent) handleInbound(msg *stun.Message, local Candidate, remote net.Addr) { //nolint:gocognit,cyclop + if msg == nil || local == nil { + return + } + + if msg.Type.Method != stun.MethodBinding || + (msg.Type.Class != stun.ClassSuccessResponse && + msg.Type.Class != stun.ClassRequest && + msg.Type.Class != stun.ClassIndication) { + a.log.Tracef("Unhandled STUN from %s to %s class(%s) method(%s)", remote, local, msg.Type.Class, msg.Type.Method) + + return } remoteCandidate := a.findRemoteCandidate(local.NetworkType(), remote) - if m.Type.Class == stun.ClassSuccessResponse { - if err = stun.MessageIntegrity([]byte(a.remotePwd)).Check(m); err != nil { + + if msg.Type.Class == stun.ClassSuccessResponse { //nolint:nestif + if err := stun.MessageIntegrity([]byte(a.remotePwd)).Check(msg); err != nil { a.log.Warnf("Discard message from (%s), %v", remote, err) + return } if remoteCandidate == nil { a.log.Warnf("Discard success message from (%s), no such remote", remote) + return } - a.selector.HandleSuccessResponse(m, local, remoteCandidate, remote) - } else if m.Type.Class == stun.ClassRequest { - a.log.Tracef("Inbound STUN (Request) from %s to %s, useCandidate: %v", remote.String(), local.String(), m.Contains(stun.AttrUseCandidate)) + a.getSelector().HandleSuccessResponse(msg, local, remoteCandidate, remote) + } else if msg.Type.Class == stun.ClassRequest { + a.log.Tracef( + "Inbound STUN (Request) from %s to %s, useCandidate: %v", + remote, + local, + msg.Contains(stun.AttrUseCandidate), + ) - if err = stunx.AssertUsername(m, a.localUfrag+":"+a.remoteUfrag); err != nil { + if err := stunx.AssertUsername(msg, a.localUfrag+":"+a.remoteUfrag); err != nil { a.log.Warnf("Discard message from (%s), %v", remote, err) + return - } else if err = stun.MessageIntegrity([]byte(a.localPwd)).Check(m); err != nil { + } else if err := stun.MessageIntegrity([]byte(a.localPwd)).Check(msg); err != nil { a.log.Warnf("Discard message from (%s), %v", remote, err) + return } if remoteCandidate == nil { - ip, port, networkType, ok := parseAddr(remote) - if !ok { - a.log.Errorf("Failed to create parse remote net.Addr when creating remote prflx candidate") + ip, port, networkType, err := parseAddr(remote) + if err != nil { + a.log.Errorf("Failed to create parse remote net.Addr when creating remote prflx candidate: %s", err) + return } @@ -1131,6 +1156,7 @@ func (a *Agent) handleInbound(m *stun.Message, local Candidate, remote net.Addr) prflxCandidate, err := NewCandidatePeerReflexive(&prflxCandidateConfig) if err != nil { a.log.Errorf("Failed to create new remote prflx candidate (%s)", err) + return } remoteCandidate = prflxCandidate @@ -1139,7 +1165,16 @@ func (a *Agent) handleInbound(m *stun.Message, local Candidate, remote net.Addr) a.addRemoteCandidate(remoteCandidate) } - a.selector.HandleBindingRequest(m, local, remoteCandidate) + // Support Remotes that don't set a TIE-BREAKER. Not standards compliant, but + // keeping to maintain backwards compat + remoteTieBreaker := &AttrControl{} + if err := remoteTieBreaker.GetFrom(msg); err == nil && remoteTieBreaker.Role == a.role() { + a.handleRoleConflict(msg, local, remoteCandidate, remoteTieBreaker) + + return + } + + a.getSelector().HandleBindingRequest(msg, local, remoteCandidate) } if remoteCandidate != nil { @@ -1148,10 +1183,10 @@ func (a *Agent) handleInbound(m *stun.Message, local Candidate, remote net.Addr) } // validateNonSTUNTraffic processes non STUN traffic from a remote candidate, -// and returns true if it is an actual remote candidate +// and returns true if it is an actual remote candidate. func (a *Agent) validateNonSTUNTraffic(local Candidate, remote net.Addr) (Candidate, bool) { var remoteCandidate Candidate - if err := a.run(local.context(), func(ctx context.Context, agent *Agent) { + if err := a.loop.Run(local.context(), func(context.Context) { remoteCandidate = a.findRemoteCandidate(local.NetworkType(), remote) if remoteCandidate != nil { remoteCandidate.seen(false) @@ -1163,7 +1198,7 @@ func (a *Agent) validateNonSTUNTraffic(local Candidate, remote net.Addr) (Candid return remoteCandidate, remoteCandidate != nil } -// GetSelectedCandidatePair returns the selected pair or nil if there is none +// GetSelectedCandidatePair returns the selected pair or nil if there is none. func (a *Agent) GetSelectedCandidatePair() (*CandidatePair, error) { selectedPair := a.getSelectedPair() if selectedPair == nil { @@ -1180,7 +1215,7 @@ func (a *Agent) GetSelectedCandidatePair() (*CandidatePair, error) { return nil, err } - return &CandidatePair{Local: local, Remote: remote, latency: selectedPair.Latency()}, nil + return &CandidatePair{Local: local, Remote: remote}, nil } func (a *Agent) getSelectedPair() *CandidatePair { @@ -1199,7 +1234,7 @@ func (a *Agent) closeMulticastConn() { } } -// SetRemoteCredentials sets the credentials of the remote agent +// SetRemoteCredentials sets the credentials of the remote agent. func (a *Agent) SetRemoteCredentials(remoteUfrag, remotePwd string) error { switch { case remoteUfrag == "": @@ -1208,9 +1243,9 @@ func (a *Agent) SetRemoteCredentials(remoteUfrag, remotePwd string) error { return ErrRemotePwdEmpty } - return a.run(a.context(), func(ctx context.Context, agent *Agent) { - agent.remoteUfrag = remoteUfrag - agent.remotePwd = remotePwd + return a.loop.Run(a.loop, func(_ context.Context) { + a.remoteUfrag = remoteUfrag + a.remotePwd = remotePwd }) } @@ -1221,7 +1256,7 @@ func (a *Agent) SetRemoteCredentials(remoteUfrag, remotePwd string) error { // cancel it. // After a Restart, the user must then call GatherCandidates explicitly // to start generating new ones. -func (a *Agent) Restart(ufrag, pwd string) error { +func (a *Agent) Restart(ufrag, pwd string) error { //nolint:cyclop if ufrag == "" { var err error ufrag, err = generateUFrag() @@ -1245,25 +1280,23 @@ func (a *Agent) Restart(ufrag, pwd string) error { } var err error - if runErr := a.run(a.context(), func(ctx context.Context, agent *Agent) { - if agent.gatheringState == GatheringStateGathering { - agent.gatherCandidateCancel() + if runErr := a.loop.Run(a.loop, func(_ context.Context) { + if a.gatheringState == GatheringStateGathering { + a.gatherCandidateCancel() } // Clear all agent needed to take back to fresh state a.removeUfragFromMux() - agent.localUfrag = ufrag - agent.localPwd = pwd - agent.remoteUfrag = "" - agent.remotePwd = "" + a.localUfrag = ufrag + a.localPwd = pwd + a.remoteUfrag = "" + a.remotePwd = "" a.gatheringState = GatheringStateNew a.checklist = make([]*CandidatePair, 0) a.pendingBindingRequests = make([]bindingRequest, 0) a.setSelectedPair(nil) a.deleteAllCandidates() - if a.selector != nil { - a.selector.Start() - } + a.setSelector() // Restart is used by NewAgent. Accept/Connect should be used to move to checking // for new Agents @@ -1273,14 +1306,15 @@ func (a *Agent) Restart(ufrag, pwd string) error { }); runErr != nil { return runErr } + return err } func (a *Agent) setGatheringState(newState GatheringState) error { done := make(chan struct{}) - if err := a.run(a.context(), func(ctx context.Context, agent *Agent) { + if err := a.loop.Run(a.loop, func(context.Context) { if a.gatheringState != newState && newState == GatheringStateComplete { - a.chanCandidate <- nil + a.candidateNotifier.EnqueueCandidate(nil) } a.gatheringState = newState @@ -1290,5 +1324,43 @@ func (a *Agent) setGatheringState(newState GatheringState) error { } <-done + return nil } + +func (a *Agent) needsToCheckPriorityOnNominated() bool { + return !a.lite || a.enableUseCandidateCheckPriority +} + +func (a *Agent) role() Role { + if a.isControlling.Load() { + return Controlling + } + + return Controlled +} + +func (a *Agent) setSelector() { + a.selectorLock.Lock() + defer a.selectorLock.Unlock() + + var s pairCandidateSelector + if a.isControlling.Load() { + s = &controllingSelector{agent: a, log: a.log} + } else { + s = &controlledSelector{agent: a, log: a.log} + } + if a.lite { + s = &liteSelector{pairCandidateSelector: s} + } + + s.Start() + a.selector = s +} + +func (a *Agent) getSelector() pairCandidateSelector { + a.selectorLock.Lock() + defer a.selectorLock.Unlock() + + return a.selector +} diff --git a/agent_config.go b/agent_config.go index 6877313..708aab5 100644 --- a/agent_config.go +++ b/agent_config.go @@ -8,47 +8,50 @@ import ( "time" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3" "golang.org/x/net/proxy" ) const ( - // defaultCheckInterval is the interval at which the agent performs candidate checks in the connecting phase + // defaultCheckInterval is the interval at which the agent performs candidate checks in the connecting phase. defaultCheckInterval = 200 * time.Millisecond - // keepaliveInterval used to keep candidates alive + // keepaliveInterval used to keep candidates alive. defaultKeepaliveInterval = 2 * time.Second - // defaultDisconnectedTimeout is the default time till an Agent transitions disconnected + // defaultDisconnectedTimeout is the default time till an Agent transitions disconnected. defaultDisconnectedTimeout = 5 * time.Second - // defaultFailedTimeout is the default time till an Agent transitions to failed after disconnected + // defaultFailedTimeout is the default time till an Agent transitions to failed after disconnected. defaultFailedTimeout = 25 * time.Second - // defaultHostAcceptanceMinWait is the wait time before nominating a host candidate + // defaultHostAcceptanceMinWait is the wait time before nominating a host candidate. defaultHostAcceptanceMinWait = 0 - // defaultSrflxAcceptanceMinWait is the wait time before nominating a srflx candidate + // defaultSrflxAcceptanceMinWait is the wait time before nominating a srflx candidate. defaultSrflxAcceptanceMinWait = 500 * time.Millisecond - // defaultPrflxAcceptanceMinWait is the wait time before nominating a prflx candidate + // defaultPrflxAcceptanceMinWait is the wait time before nominating a prflx candidate. defaultPrflxAcceptanceMinWait = 1000 * time.Millisecond - // defaultRelayAcceptanceMinWait is the wait time before nominating a relay candidate + // defaultRelayAcceptanceMinWait is the wait time before nominating a relay candidate. defaultRelayAcceptanceMinWait = 2000 * time.Millisecond - // defaultMaxBindingRequests is the maximum number of binding requests before considering a pair failed + // defaultSTUNGatherTimeout is the wait time for STUN responses. + defaultSTUNGatherTimeout = 5 * time.Second + + // defaultMaxBindingRequests is the maximum number of binding requests before considering a pair failed. defaultMaxBindingRequests = 7 // TCPPriorityOffset is a number which is subtracted from the default (UDP) candidate type preference // for host, srflx and prfx candidate types. defaultTCPPriorityOffset = 27 - // maxBufferSize is the number of bytes that can be buffered before we start to error + // maxBufferSize is the number of bytes that can be buffered before we start to error. maxBufferSize = 1000 * 1000 // 1MB - // maxBindingRequestTimeout is the wait time before binding requests can be deleted + // maxBindingRequestTimeout is the wait time before binding requests can be deleted. maxBindingRequestTimeout = 4000 * time.Millisecond ) @@ -57,7 +60,7 @@ func defaultCandidateTypes() []CandidateType { } // AgentConfig collects the arguments to ice.Agent construction into -// a single structure, for future-proofness of the interface +// a single structure, for future-proofness of the interface. type AgentConfig struct { Urls []*stun.URI @@ -88,7 +91,7 @@ type AgentConfig struct { // KeepaliveInterval determines how often should we send ICE // keepalives (should be less then connectiontimeout above) - // when this is nil, it defaults to 10 seconds. + // when this is nil, it defaults to 2 seconds. // A keepalive interval of 0 means we never send keepalive packets KeepaliveInterval *time.Duration @@ -130,12 +133,14 @@ type AgentConfig struct { // HostAcceptanceMinWait specify a minimum wait time before selecting host candidates HostAcceptanceMinWait *time.Duration - // HostAcceptanceMinWait specify a minimum wait time before selecting srflx candidates + // SrflxAcceptanceMinWait specify a minimum wait time before selecting srflx candidates SrflxAcceptanceMinWait *time.Duration - // HostAcceptanceMinWait specify a minimum wait time before selecting prflx candidates + // PrflxAcceptanceMinWait specify a minimum wait time before selecting prflx candidates PrflxAcceptanceMinWait *time.Duration - // HostAcceptanceMinWait specify a minimum wait time before selecting relay candidates + // RelayAcceptanceMinWait specify a minimum wait time before selecting relay candidates RelayAcceptanceMinWait *time.Duration + // STUNGatherTimeout specify a minimum wait time for STUN responses + STUNGatherTimeout *time.Duration // Net is the our abstracted network interface for internal development purpose only // (see https://github.com/pion/transport) @@ -143,11 +148,11 @@ type AgentConfig struct { // InterfaceFilter is a function that you can use in order to whitelist or blacklist // the interfaces which are used to gather ICE candidates. - InterfaceFilter func(string) bool + InterfaceFilter func(string) (keep bool) // IPFilter is a function that you can use in order to whitelist or blacklist // the ips which are used to gather ICE candidates. - IPFilter func(net.IP) bool + IPFilter func(net.IP) (keep bool) // InsecureSkipVerify controls if self-signed certificates are accepted when connecting // to TURN servers via TLS or DTLS @@ -188,105 +193,127 @@ type AgentConfig struct { // DisableActiveTCP can be used to disable Active TCP candidates. Otherwise when TCP is enabled // Active TCP candidates will be created when a new passive TCP remote candidate is added. DisableActiveTCP bool + + // BindingRequestHandler allows applications to perform logic on incoming STUN Binding Requests + // This was implemented to allow users to + // * Log incoming Binding Requests for debugging + // * Implement draft-thatcher-ice-renomination + // * Implement custom CandidatePair switching logic + BindingRequestHandler func(m *stun.Message, local, remote Candidate, pair *CandidatePair) bool + + // EnableUseCandidateCheckPriority can be used to enable checking for equal or higher priority to + // switch selected candidate pair if the peer requests USE-CANDIDATE and agent is a lite agent. + // This is disabled by default, i. e. when peer requests USE-CANDIDATE, the selected pair will be + // switched to that irrespective of relative priority between current selected pair + // and priority of the pair being switched to. + EnableUseCandidateCheckPriority bool } -// initWithDefaults populates an agent and falls back to defaults if fields are unset -func (config *AgentConfig) initWithDefaults(a *Agent) { +// initWithDefaults populates an agent and falls back to defaults if fields are unset. +func (config *AgentConfig) initWithDefaults(agent *Agent) { //nolint:cyclop if config.MaxBindingRequests == nil { - a.maxBindingRequests = defaultMaxBindingRequests + agent.maxBindingRequests = defaultMaxBindingRequests } else { - a.maxBindingRequests = *config.MaxBindingRequests + agent.maxBindingRequests = *config.MaxBindingRequests } if config.HostAcceptanceMinWait == nil { - a.hostAcceptanceMinWait = defaultHostAcceptanceMinWait + agent.hostAcceptanceMinWait = defaultHostAcceptanceMinWait } else { - a.hostAcceptanceMinWait = *config.HostAcceptanceMinWait + agent.hostAcceptanceMinWait = *config.HostAcceptanceMinWait } if config.SrflxAcceptanceMinWait == nil { - a.srflxAcceptanceMinWait = defaultSrflxAcceptanceMinWait + agent.srflxAcceptanceMinWait = defaultSrflxAcceptanceMinWait } else { - a.srflxAcceptanceMinWait = *config.SrflxAcceptanceMinWait + agent.srflxAcceptanceMinWait = *config.SrflxAcceptanceMinWait } if config.PrflxAcceptanceMinWait == nil { - a.prflxAcceptanceMinWait = defaultPrflxAcceptanceMinWait + agent.prflxAcceptanceMinWait = defaultPrflxAcceptanceMinWait } else { - a.prflxAcceptanceMinWait = *config.PrflxAcceptanceMinWait + agent.prflxAcceptanceMinWait = *config.PrflxAcceptanceMinWait } if config.RelayAcceptanceMinWait == nil { - a.relayAcceptanceMinWait = defaultRelayAcceptanceMinWait + agent.relayAcceptanceMinWait = defaultRelayAcceptanceMinWait } else { - a.relayAcceptanceMinWait = *config.RelayAcceptanceMinWait + agent.relayAcceptanceMinWait = *config.RelayAcceptanceMinWait + } + + if config.STUNGatherTimeout == nil { + agent.stunGatherTimeout = defaultSTUNGatherTimeout + } else { + agent.stunGatherTimeout = *config.STUNGatherTimeout } if config.TCPPriorityOffset == nil { - a.tcpPriorityOffset = defaultTCPPriorityOffset + agent.tcpPriorityOffset = defaultTCPPriorityOffset } else { - a.tcpPriorityOffset = *config.TCPPriorityOffset + agent.tcpPriorityOffset = *config.TCPPriorityOffset } if config.DisconnectedTimeout == nil { - a.disconnectedTimeout = defaultDisconnectedTimeout + agent.disconnectedTimeout = defaultDisconnectedTimeout } else { - a.disconnectedTimeout = *config.DisconnectedTimeout + agent.disconnectedTimeout = *config.DisconnectedTimeout } if config.FailedTimeout == nil { - a.failedTimeout = defaultFailedTimeout + agent.failedTimeout = defaultFailedTimeout } else { - a.failedTimeout = *config.FailedTimeout + agent.failedTimeout = *config.FailedTimeout } if config.KeepaliveInterval == nil { - a.keepaliveInterval = defaultKeepaliveInterval + agent.keepaliveInterval = defaultKeepaliveInterval } else { - a.keepaliveInterval = *config.KeepaliveInterval + agent.keepaliveInterval = *config.KeepaliveInterval } if config.CheckInterval == nil { - a.checkInterval = defaultCheckInterval + agent.checkInterval = defaultCheckInterval } else { - a.checkInterval = *config.CheckInterval + agent.checkInterval = *config.CheckInterval } - if config.CandidateTypes == nil || len(config.CandidateTypes) == 0 { - a.candidateTypes = defaultCandidateTypes() + if len(config.CandidateTypes) == 0 { + agent.candidateTypes = defaultCandidateTypes() } else { - a.candidateTypes = config.CandidateTypes + agent.candidateTypes = config.CandidateTypes } } -func (config *AgentConfig) initExtIPMapping(a *Agent) error { +func (config *AgentConfig) initExtIPMapping(agent *Agent) error { //nolint:cyclop var err error - a.extIPMapper, err = newExternalIPMapper(config.NAT1To1IPCandidateType, config.NAT1To1IPs) + agent.extIPMapper, err = newExternalIPMapper(config.NAT1To1IPCandidateType, config.NAT1To1IPs) if err != nil { return err } - if a.extIPMapper == nil { + if agent.extIPMapper == nil { return nil // This may happen when config.NAT1To1IPs is an empty array } - if a.extIPMapper.candidateType == CandidateTypeHost { - if a.mDNSMode == MulticastDNSModeQueryAndGather { + if agent.extIPMapper.candidateType == CandidateTypeHost { //nolint:nestif + if agent.mDNSMode == MulticastDNSModeQueryAndGather { return ErrMulticastDNSWithNAT1To1IPMapping } candiHostEnabled := false - for _, candiType := range a.candidateTypes { + for _, candiType := range agent.candidateTypes { if candiType == CandidateTypeHost { candiHostEnabled = true + break } } if !candiHostEnabled { return ErrIneffectiveNAT1To1IPMappingHost } - } else if a.extIPMapper.candidateType == CandidateTypeServerReflexive { + } else if agent.extIPMapper.candidateType == CandidateTypeServerReflexive { candiSrflxEnabled := false - for _, candiType := range a.candidateTypes { + for _, candiType := range agent.candidateTypes { if candiType == CandidateTypeServerReflexive { candiSrflxEnabled = true + break } } @@ -294,5 +321,6 @@ func (config *AgentConfig) initExtIPMapping(a *Agent) error { return ErrIneffectiveNAT1To1IPMappingSrflx } } + return nil } diff --git a/agent_get_best_available_candidate_pair_test.go b/agent_get_best_available_candidate_pair_test.go index 44c6a78..c7bb2e4 100644 --- a/agent_get_best_available_candidate_pair_test.go +++ b/agent_get_best_available_candidate_pair_test.go @@ -13,19 +13,12 @@ import ( ) func TestNoBestAvailableCandidatePairAfterAgentConstruction(t *testing.T) { - agent := setupTest(t) - - require.Nil(t, agent.getBestAvailableCandidatePair()) - - tearDownTest(t, agent) -} - -func setupTest(t *testing.T) *Agent { agent, err := NewAgent(&AgentConfig{}) require.NoError(t, err) - return agent -} -func tearDownTest(t *testing.T, agent *Agent) { - require.NoError(t, agent.Close()) + defer func() { + require.NoError(t, agent.Close()) + }() + + require.Nil(t, agent.getBestAvailableCandidatePair()) } diff --git a/agent_get_best_valid_candidate_pair_test.go b/agent_get_best_valid_candidate_pair_test.go index a3c551a..f9f94af 100644 --- a/agent_get_best_valid_candidate_pair_test.go +++ b/agent_get_best_valid_candidate_pair_test.go @@ -9,12 +9,14 @@ package ice import ( "testing" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) func TestAgentGetBestValidCandidatePair(t *testing.T) { f := setupTestAgentGetBestValidCandidatePair(t) + defer func() { + require.NoError(t, f.sut.Close()) + }() remoteCandidatesFromLowestPriorityToHighest := []Candidate{f.relayRemote, f.srflxRemote, f.prflxRemote, f.hostRemote} @@ -27,11 +29,11 @@ func TestAgentGetBestValidCandidatePair(t *testing.T) { require.Equal(t, actualBestPair.String(), expectedBestPair.String()) } - - assert.NoError(t, f.sut.Close()) } func setupTestAgentGetBestValidCandidatePair(t *testing.T) *TestAgentGetBestValidCandidatePairFixture { + t.Helper() + fixture := new(TestAgentGetBestValidCandidatePairFixture) fixture.hostLocal = newHostLocal(t) fixture.relayRemote = newRelayRemote(t) diff --git a/agent_handlers.go b/agent_handlers.go index 7ceb6e7..823514e 100644 --- a/agent_handlers.go +++ b/agent_handlers.go @@ -3,16 +3,20 @@ package ice -// OnConnectionStateChange sets a handler that is fired when the connection state changes +import "sync" + +// OnConnectionStateChange sets a handler that is fired when the connection state changes. func (a *Agent) OnConnectionStateChange(f func(ConnectionState)) error { a.onConnectionStateChangeHdlr.Store(f) + return nil } -// OnSelectedCandidatePairChange sets a handler that is fired when the final candidate -// pair is selected +// OnSelectedCandidatePairChange sets a handler that is fired when the final candidate. +// pair is selected. func (a *Agent) OnSelectedCandidatePairChange(f func(Candidate, Candidate)) error { a.onSelectedCandidatePairChangeHdlr.Store(f) + return nil } @@ -20,53 +24,167 @@ func (a *Agent) OnSelectedCandidatePairChange(f func(Candidate, Candidate)) erro // the gathering process complete the last candidate is nil. func (a *Agent) OnCandidate(f func(Candidate)) error { a.onCandidateHdlr.Store(f) - return nil -} -// OnSuccessfulSelectedPairBindingResponse sets a handler that is fired when a successful binding response is received for the selected candidate pair -func (a *Agent) OnSuccessfulSelectedPairBindingResponse(f func(*CandidatePair)) error { - a.onSuccessfulSelectedPairBindingResponseHdlr.Store(f) return nil } func (a *Agent) onSelectedCandidatePairChange(p *CandidatePair) { - if h, ok := a.onSelectedCandidatePairChangeHdlr.Load().(func(Candidate, Candidate)); ok { + if h, ok := a.onSelectedCandidatePairChangeHdlr.Load().(func(Candidate, Candidate)); ok && h != nil { h(p.Local, p.Remote) } } func (a *Agent) onCandidate(c Candidate) { - if onCandidateHdlr, ok := a.onCandidateHdlr.Load().(func(Candidate)); ok { + if onCandidateHdlr, ok := a.onCandidateHdlr.Load().(func(Candidate)); ok && onCandidateHdlr != nil { onCandidateHdlr(c) } } func (a *Agent) onConnectionStateChange(s ConnectionState) { - if hdlr, ok := a.onConnectionStateChangeHdlr.Load().(func(ConnectionState)); ok { + if hdlr, ok := a.onConnectionStateChangeHdlr.Load().(func(ConnectionState)); ok && hdlr != nil { hdlr(s) } } -func (a *Agent) onSuccessfulSelectedPairBindingResponse(p *CandidatePair) { - if h, ok := a.onSuccessfulSelectedPairBindingResponseHdlr.Load().(func(*CandidatePair)); ok { - h(p) +type handlerNotifier struct { + sync.Mutex + running bool + notifiers sync.WaitGroup + + connectionStates []ConnectionState + connectionStateFunc func(ConnectionState) + + candidates []Candidate + candidateFunc func(Candidate) + + selectedCandidatePairs []*CandidatePair + candidatePairFunc func(*CandidatePair) + + // State for closing + done chan struct{} +} + +func (h *handlerNotifier) Close(graceful bool) { + if graceful { + // if we were closed ungracefully before, we now + // want ot wait. + defer h.notifiers.Wait() + } + + h.Lock() + + select { + case <-h.done: + h.Unlock() + + return + default: + } + close(h.done) + h.Unlock() +} + +func (h *handlerNotifier) EnqueueConnectionState(state ConnectionState) { + h.Lock() + defer h.Unlock() + + select { + case <-h.done: + return + default: + } + + notify := func() { + defer h.notifiers.Done() + for { + h.Lock() + if len(h.connectionStates) == 0 { + h.running = false + h.Unlock() + + return + } + notification := h.connectionStates[0] + h.connectionStates = h.connectionStates[1:] + h.Unlock() + h.connectionStateFunc(notification) + } + } + + h.connectionStates = append(h.connectionStates, state) + if !h.running { + h.running = true + h.notifiers.Add(1) + go notify() } } -func (a *Agent) candidatePairRoutine() { - for p := range a.chanCandidatePair { - a.onSelectedCandidatePairChange(p) +func (h *handlerNotifier) EnqueueCandidate(cand Candidate) { + h.Lock() + defer h.Unlock() + + select { + case <-h.done: + return + default: + } + + notify := func() { + defer h.notifiers.Done() + for { + h.Lock() + if len(h.candidates) == 0 { + h.running = false + h.Unlock() + + return + } + notification := h.candidates[0] + h.candidates = h.candidates[1:] + h.Unlock() + h.candidateFunc(notification) + } + } + + h.candidates = append(h.candidates, cand) + if !h.running { + h.running = true + h.notifiers.Add(1) + go notify() } } -func (a *Agent) connectionStateRoutine() { - for s := range a.chanState { - go a.onConnectionStateChange(s) - } -} +func (h *handlerNotifier) EnqueueSelectedCandidatePair(pair *CandidatePair) { + h.Lock() + defer h.Unlock() -func (a *Agent) candidateRoutine() { - for c := range a.chanCandidate { - a.onCandidate(c) + select { + case <-h.done: + return + default: + } + + notify := func() { + defer h.notifiers.Done() + for { + h.Lock() + if len(h.selectedCandidatePairs) == 0 { + h.running = false + h.Unlock() + + return + } + notification := h.selectedCandidatePairs[0] + h.selectedCandidatePairs = h.selectedCandidatePairs[1:] + h.Unlock() + h.candidatePairFunc(notification) + } + } + + h.selectedCandidatePairs = append(h.selectedCandidatePairs, pair) + if !h.running { + h.running = true + h.notifiers.Add(1) + go notify() } } diff --git a/agent_handlers_test.go b/agent_handlers_test.go new file mode 100644 index 0000000..0c980f6 --- /dev/null +++ b/agent_handlers_test.go @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +package ice + +import ( + "testing" + "time" + + "github.com/pion/transport/v3/test" + "github.com/stretchr/testify/assert" +) + +func TestConnectionStateNotifier(t *testing.T) { + t.Run("TestManyUpdates", func(t *testing.T) { + defer test.CheckRoutines(t)() + + updates := make(chan struct{}, 1) + notifier := &handlerNotifier{ + connectionStateFunc: func(_ ConnectionState) { + updates <- struct{}{} + }, + done: make(chan struct{}), + } + // Enqueue all updates upfront to ensure that it + // doesn't block + for i := 0; i < 10000; i++ { + notifier.EnqueueConnectionState(ConnectionStateNew) + } + done := make(chan struct{}) + go func() { + for i := 0; i < 10000; i++ { + <-updates + } + select { + case <-updates: + t.Errorf("received more updates than expected") // nolint + case <-time.After(1 * time.Second): + } + close(done) + }() + <-done + notifier.Close(true) + }) + t.Run("TestUpdateOrdering", func(t *testing.T) { + defer test.CheckRoutines(t)() + updates := make(chan ConnectionState) + notifer := &handlerNotifier{ + connectionStateFunc: func(cs ConnectionState) { + updates <- cs + }, + done: make(chan struct{}), + } + done := make(chan struct{}) + go func() { + for i := 0; i < 10000; i++ { + assert.Equal(t, ConnectionState(i), <-updates) + } + select { + case <-updates: + t.Errorf("received more updates than expected") // nolint + case <-time.After(1 * time.Second): + } + close(done) + }() + for i := 0; i < 10000; i++ { + notifer.EnqueueConnectionState(ConnectionState(i)) + } + <-done + notifer.Close(true) + }) +} diff --git a/agent_on_selected_candidate_pair_change_test.go b/agent_on_selected_candidate_pair_change_test.go index 78cb4d5..4816db7 100644 --- a/agent_on_selected_candidate_pair_change_test.go +++ b/agent_on_selected_candidate_pair_change_test.go @@ -15,34 +15,42 @@ import ( func TestOnSelectedCandidatePairChange(t *testing.T) { agent, candidatePair := fixtureTestOnSelectedCandidatePairChange(t) + defer func() { + require.NoError(t, agent.Close()) + }() callbackCalled := make(chan struct{}, 1) - err := agent.OnSelectedCandidatePairChange(func(local, remote Candidate) { + err := agent.OnSelectedCandidatePairChange(func(_, _ Candidate) { close(callbackCalled) }) require.NoError(t, err) - err = agent.run(context.Background(), func(ctx context.Context, agent *Agent) { + err = agent.loop.Run(context.Background(), func(_ context.Context) { agent.setSelectedPair(candidatePair) }) require.NoError(t, err) <-callbackCalled - require.NoError(t, agent.Close()) } func fixtureTestOnSelectedCandidatePairChange(t *testing.T) (*Agent, *CandidatePair) { + t.Helper() + agent, err := NewAgent(&AgentConfig{}) require.NoError(t, err) candidatePair := makeCandidatePair(t) + return agent, candidatePair } func makeCandidatePair(t *testing.T) *CandidatePair { + t.Helper() + hostLocal := newHostLocal(t) relayRemote := newRelayRemote(t) candidatePair := newCandidatePair(hostLocal, relayRemote, false) + return candidatePair } diff --git a/agent_stats.go b/agent_stats.go index b9ad718..c6b21f5 100644 --- a/agent_stats.go +++ b/agent_stats.go @@ -8,12 +8,12 @@ import ( "time" ) -// GetCandidatePairsStats returns a list of candidate pair stats +// GetCandidatePairsStats returns a list of candidate pair stats. func (a *Agent) GetCandidatePairsStats() []CandidatePairStats { var res []CandidatePairStats - err := a.run(a.context(), func(ctx context.Context, agent *Agent) { - result := make([]CandidatePairStats, 0, len(agent.checklist)) - for _, cp := range agent.checklist { + err := a.loop.Run(a.loop, func(_ context.Context) { + result := make([]CandidatePairStats, 0, len(a.checklist)) + for _, cp := range a.checklist { stat := CandidatePairStats{ Timestamp: time.Now(), LocalCandidateID: cp.Local.ID(), @@ -26,18 +26,22 @@ func (a *Agent) GetCandidatePairsStats() []CandidatePairStats { // BytesReceived uint64 // LastPacketSentTimestamp time.Time // LastPacketReceivedTimestamp time.Time - // FirstRequestTimestamp time.Time - // LastRequestTimestamp time.Time - // LastResponseTimestamp time.Time - // TotalRoundTripTime float64 - // CurrentRoundTripTime float64 + FirstRequestTimestamp: cp.FirstRequestSentAt(), + LastRequestTimestamp: cp.LastRequestSentAt(), + FirstResponseTimestamp: cp.FirstReponseReceivedAt(), + LastResponseTimestamp: cp.LastResponseReceivedAt(), + FirstRequestReceivedTimestamp: cp.FirstRequestReceivedAt(), + LastRequestReceivedTimestamp: cp.LastRequestReceivedAt(), + + TotalRoundTripTime: cp.TotalRoundTripTime(), + CurrentRoundTripTime: cp.CurrentRoundTripTime(), // AvailableOutgoingBitrate float64 // AvailableIncomingBitrate float64 // CircuitBreakerTriggerCount uint32 - // RequestsReceived uint64 - // RequestsSent uint64 - // ResponsesReceived uint64 - // ResponsesSent uint64 + RequestsReceived: cp.RequestsReceived(), + RequestsSent: cp.RequestsSent(), + ResponsesReceived: cp.ResponsesReceived(), + ResponsesSent: cp.ResponsesSent(), // RetransmissionsReceived uint64 // RetransmissionsSent uint64 // ConsentRequestsSent uint64 @@ -49,32 +53,85 @@ func (a *Agent) GetCandidatePairsStats() []CandidatePairStats { }) if err != nil { a.log.Errorf("Failed to get candidate pairs stats: %v", err) + return []CandidatePairStats{} } + return res } -// GetLocalCandidatesStats returns a list of local candidates stats +// GetSelectedCandidatePairStats returns a candidate pair stats for selected candidate pair. +// Returns false if there is no selected pair. +func (a *Agent) GetSelectedCandidatePairStats() (CandidatePairStats, bool) { + isAvailable := false + var res CandidatePairStats + err := a.loop.Run(a.loop, func(_ context.Context) { + sp := a.getSelectedPair() + if sp == nil { + return + } + + isAvailable = true + res = CandidatePairStats{ + Timestamp: time.Now(), + LocalCandidateID: sp.Local.ID(), + RemoteCandidateID: sp.Remote.ID(), + State: sp.state, + Nominated: sp.nominated, + // PacketsSent uint32 + // PacketsReceived uint32 + // BytesSent uint64 + // BytesReceived uint64 + // LastPacketSentTimestamp time.Time + // LastPacketReceivedTimestamp time.Time + // FirstRequestTimestamp time.Time + // LastRequestTimestamp time.Time + // LastResponseTimestamp time.Time + TotalRoundTripTime: sp.TotalRoundTripTime(), + CurrentRoundTripTime: sp.CurrentRoundTripTime(), + // AvailableOutgoingBitrate float64 + // AvailableIncomingBitrate float64 + // CircuitBreakerTriggerCount uint32 + // RequestsReceived uint64 + // RequestsSent uint64 + ResponsesReceived: sp.ResponsesReceived(), + // ResponsesSent uint64 + // RetransmissionsReceived uint64 + // RetransmissionsSent uint64 + // ConsentRequestsSent uint64 + // ConsentExpiredTimestamp time.Time + } + }) + if err != nil { + a.log.Errorf("Failed to get selected candidate pair stats: %v", err) + + return CandidatePairStats{}, false + } + + return res, isAvailable +} + +// GetLocalCandidatesStats returns a list of local candidates stats. func (a *Agent) GetLocalCandidatesStats() []CandidateStats { var res []CandidateStats - err := a.run(a.context(), func(ctx context.Context, agent *Agent) { - result := make([]CandidateStats, 0, len(agent.localCandidates)) - for networkType, localCandidates := range agent.localCandidates { - for _, c := range localCandidates { + err := a.loop.Run(a.loop, func(_ context.Context) { + result := make([]CandidateStats, 0, len(a.localCandidates)) + for networkType, localCandidates := range a.localCandidates { + for _, cand := range localCandidates { relayProtocol := "" - if c.Type() == CandidateTypeRelay { - if cRelay, ok := c.(*CandidateRelay); ok { + if cand.Type() == CandidateTypeRelay { + if cRelay, ok := cand.(*CandidateRelay); ok { relayProtocol = cRelay.RelayProtocol() } } stat := CandidateStats{ Timestamp: time.Now(), - ID: c.ID(), + ID: cand.ID(), NetworkType: networkType, - IP: c.Address(), - Port: c.Port(), - CandidateType: c.Type(), - Priority: c.Priority(), + IP: cand.Address(), + Port: cand.Port(), + CandidateType: cand.Type(), + Priority: cand.Priority(), // URL string RelayProtocol: relayProtocol, // Deleted bool @@ -86,17 +143,19 @@ func (a *Agent) GetLocalCandidatesStats() []CandidateStats { }) if err != nil { a.log.Errorf("Failed to get candidate pair stats: %v", err) + return []CandidateStats{} } + return res } -// GetRemoteCandidatesStats returns a list of remote candidates stats +// GetRemoteCandidatesStats returns a list of remote candidates stats. func (a *Agent) GetRemoteCandidatesStats() []CandidateStats { var res []CandidateStats - err := a.run(a.context(), func(ctx context.Context, agent *Agent) { - result := make([]CandidateStats, 0, len(agent.remoteCandidates)) - for networkType, remoteCandidates := range agent.remoteCandidates { + err := a.loop.Run(a.loop, func(_ context.Context) { + result := make([]CandidateStats, 0, len(a.remoteCandidates)) + for networkType, remoteCandidates := range a.remoteCandidates { for _, c := range remoteCandidates { stat := CandidateStats{ Timestamp: time.Now(), @@ -116,7 +175,9 @@ func (a *Agent) GetRemoteCandidatesStats() []CandidateStats { }) if err != nil { a.log.Errorf("Failed to get candidate pair stats: %v", err) + return []CandidateStats{} } + return res } diff --git a/agent_test.go b/agent_test.go index 291df4d..29f99d2 100644 --- a/agent_test.go +++ b/agent_test.go @@ -8,19 +8,17 @@ package ice import ( "context" - "errors" "net" "strconv" "sync" "testing" "time" - "github.com/pion/ice/v3/internal/fakenet" + "github.com/pion/ice/v4/internal/fakenet" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" "github.com/pion/transport/v3/vnet" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -34,31 +32,21 @@ func (ba *BadAddr) String() string { return "yyy" } -func runAgentTest(t *testing.T, config *AgentConfig, task func(ctx context.Context, a *Agent)) { - a, err := NewAgent(config) - if err != nil { - t.Fatalf("Error constructing ice.Agent") - } - - if err := a.run(context.Background(), task); err != nil { - t.Fatalf("Agent run failure: %v", err) - } - - assert.NoError(t, a.Close()) -} - -func TestHandlePeerReflexive(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +func TestHandlePeerReflexive(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 2) - defer lim.Stop() + defer test.TimeOut(time.Second * 2).Stop() t.Run("UDP prflx candidate from handleInbound()", func(t *testing.T) { - var config AgentConfig - runAgentTest(t, &config, func(ctx context.Context, a *Agent) { - a.selector = &controllingSelector{agent: a, log: a.log} + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() + + require.NoError(t, agent.loop.Run(agent.loop, func(_ context.Context) { + agent.selector = &controllingSelector{agent: agent, log: agent.log} hostConfig := CandidateHostConfig{ Network: "udp", @@ -68,58 +56,47 @@ func TestHandlePeerReflexive(t *testing.T) { } local, err := NewCandidateHost(&hostConfig) local.conn = &fakenet.MockPacketConn{} - if err != nil { - t.Fatalf("failed to create a new candidate: %v", err) - } + require.NoError(t, err) remote := &net.UDPAddr{IP: net.ParseIP("172.17.0.3"), Port: 999} msg, err := stun.Build(stun.BindingRequest, stun.TransactionID, - stun.NewUsername(a.localUfrag+":"+a.remoteUfrag), + stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag), UseCandidate(), - AttrControlling(a.tieBreaker), + AttrControlling(agent.tieBreaker), PriorityAttr(local.Priority()), - stun.NewShortTermIntegrity(a.localPwd), + stun.NewShortTermIntegrity(agent.localPwd), stun.Fingerprint, ) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // nolint: contextcheck - a.handleInbound(msg, local, remote) + agent.handleInbound(msg, local, remote) // Length of remote candidate list must be one now - if len(a.remoteCandidates) != 1 { - t.Fatal("failed to add a network type to the remote candidate list") - } + require.Len(t, agent.remoteCandidates, 1) // Length of remote candidate list for a network type must be 1 - set := a.remoteCandidates[local.NetworkType()] - if len(set) != 1 { - t.Fatal("failed to add prflx candidate to remote candidate list") - } + set := agent.remoteCandidates[local.NetworkType()] + require.Len(t, set, 1) c := set[0] - if c.Type() != CandidateTypePeerReflexive { - t.Fatal("candidate type must be prflx") - } - - if c.Address() != "172.17.0.3" { - t.Fatal("IP address mismatch") - } - - if c.Port() != 999 { - t.Fatal("Port number mismatch") - } - }) + require.Equal(t, CandidateTypePeerReflexive, c.Type()) + require.Equal(t, "172.17.0.3", c.Address()) + require.Equal(t, 999, c.Port()) + })) }) t.Run("Bad network type with handleInbound()", func(t *testing.T) { - var config AgentConfig - runAgentTest(t, &config, func(ctx context.Context, a *Agent) { - a.selector = &controllingSelector{agent: a, log: a.log} + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() + + require.NoError(t, agent.loop.Run(agent.loop, func(_ context.Context) { + agent.selector = &controllingSelector{agent: agent, log: agent.log} hostConfig := CandidateHostConfig{ Network: "tcp", @@ -128,28 +105,28 @@ func TestHandlePeerReflexive(t *testing.T) { Component: 1, } local, err := NewCandidateHost(&hostConfig) - if err != nil { - t.Fatalf("failed to create a new candidate: %v", err) - } + require.NoError(t, err) remote := &BadAddr{} // nolint: contextcheck - a.handleInbound(nil, local, remote) - - if len(a.remoteCandidates) != 0 { - t.Fatal("bad address should not be added to the remote candidate list") - } - }) + agent.handleInbound(nil, local, remote) + require.Len(t, agent.remoteCandidates, 0) + })) }) t.Run("Success from unknown remote, prflx candidate MUST only be created via Binding Request", func(t *testing.T) { - var config AgentConfig - runAgentTest(t, &config, func(ctx context.Context, a *Agent) { - a.selector = &controllingSelector{agent: a, log: a.log} + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() + + require.NoError(t, agent.loop.Run(agent.loop, func(_ context.Context) { + agent.selector = &controllingSelector{agent: agent, log: agent.log} tID := [stun.TransactionIDSize]byte{} copy(tID[:], "ABC") - a.pendingBindingRequests = []bindingRequest{ + agent.pendingBindingRequests = []bindingRequest{ {time.Now(), tID, &net.UDPAddr{}, false}, } @@ -161,57 +138,49 @@ func TestHandlePeerReflexive(t *testing.T) { } local, err := NewCandidateHost(&hostConfig) local.conn = &fakenet.MockPacketConn{} - if err != nil { - t.Fatalf("failed to create a new candidate: %v", err) - } + require.NoError(t, err) remote := &net.UDPAddr{IP: net.ParseIP("172.17.0.3"), Port: 999} msg, err := stun.Build(stun.BindingSuccess, stun.NewTransactionIDSetter(tID), - stun.NewShortTermIntegrity(a.remotePwd), + stun.NewShortTermIntegrity(agent.remotePwd), stun.Fingerprint, ) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // nolint: contextcheck - a.handleInbound(msg, local, remote) - if len(a.remoteCandidates) != 0 { - t.Fatal("unknown remote was able to create a candidate") - } - }) + agent.handleInbound(msg, local, remote) + require.Len(t, agent.remoteCandidates, 0) + })) }) } // Assert that Agent on startup sends message, and doesn't wait for connectivityTicker to fire // https://github.com/pion/ice/issues/15 func TestConnectivityOnStartup(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() // Create a network with two interfaces wan, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: "0.0.0.0/0", LoggerFactory: logging.NewDefaultLoggerFactory(), }) - assert.NoError(t, err) + require.NoError(t, err) net0, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"192.168.0.1"}, }) - assert.NoError(t, err) - assert.NoError(t, wan.AddNet(net0)) + require.NoError(t, err) + require.NoError(t, wan.AddNet(net0)) net1, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"192.168.0.2"}, }) - assert.NoError(t, err) - assert.NoError(t, wan.AddNet(net1)) + require.NoError(t, err) + require.NoError(t, wan.AddNet(net1)) - assert.NoError(t, wan.Start()) + require.NoError(t, wan.Start()) aNotifier, aConnected := onConnected() bNotifier, bConnected := onConnected() @@ -227,6 +196,9 @@ func TestConnectivityOnStartup(t *testing.T) { aAgent, err := NewAgent(cfg0) require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) cfg1 := &AgentConfig{ @@ -239,17 +211,20 @@ func TestConnectivityOnStartup(t *testing.T) { bAgent, err := NewAgent(cfg1) require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) - aConn, bConn := func(aAgent, bAgent *Agent) (*Conn, *Conn) { + func(aAgent, bAgent *Agent) (*Conn, *Conn) { // Manual signaling aUfrag, aPwd, err := aAgent.GetLocalUserCredentials() - assert.NoError(t, err) + require.NoError(t, err) bUfrag, bPwd, err := bAgent.GetLocalUserCredentials() - assert.NoError(t, err) + require.NoError(t, err) - gatherAndExchangeCandidates(aAgent, bAgent) + gatherAndExchangeCandidates(t, aAgent, bAgent) accepted := make(chan struct{}) accepting := make(chan struct{}) @@ -257,9 +232,9 @@ func TestConnectivityOnStartup(t *testing.T) { origHdlr := aAgent.onConnectionStateChangeHdlr.Load() if origHdlr != nil { - defer check(aAgent.OnConnectionStateChange(origHdlr.(func(ConnectionState)))) //nolint:forcetypeassert + defer require.NoError(t, aAgent.OnConnectionStateChange(origHdlr.(func(ConnectionState)))) //nolint:forcetypeassert } - check(aAgent.OnConnectionStateChange(func(s ConnectionState) { + require.NoError(t, aAgent.OnConnectionStateChange(func(s ConnectionState) { if s == ConnectionStateChecking { close(accepting) } @@ -271,17 +246,18 @@ func TestConnectivityOnStartup(t *testing.T) { go func() { var acceptErr error aConn, acceptErr = aAgent.Accept(context.TODO(), bUfrag, bPwd) - check(acceptErr) + require.NoError(t, acceptErr) close(accepted) }() <-accepting bConn, err := bAgent.Dial(context.TODO(), aUfrag, aPwd) - check(err) + require.NoError(t, err) // Ensure accepted <-accepted + return aConn, bConn }(aAgent, bAgent) @@ -290,18 +266,13 @@ func TestConnectivityOnStartup(t *testing.T) { <-aConnected <-bConnected - assert.NoError(t, wan.Stop()) - if !closePipe(t, aConn, bConn) { - return - } + require.NoError(t, wan.Stop()) } func TestConnectivityLite(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() stunServerURL := &stun.URI{ Scheme: SchemeTypeSTUN, @@ -314,9 +285,9 @@ func TestConnectivityLite(t *testing.T) { MappingBehavior: vnet.EndpointIndependent, FilteringBehavior: vnet.EndpointIndependent, } - v, err := buildVNet(natType, natType) + vent, err := buildVNet(natType, natType) require.NoError(t, err, "should succeed") - defer v.close() + defer vent.close() aNotifier, aConnected := onConnected() bNotifier, bConnected := onConnected() @@ -325,11 +296,14 @@ func TestConnectivityLite(t *testing.T) { Urls: []*stun.URI{stunServerURL}, NetworkTypes: supportedNetworkTypes(), MulticastDNSMode: MulticastDNSModeDisabled, - Net: v.net0, + Net: vent.net0, } aAgent, err := NewAgent(cfg0) require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) cfg1 := &AgentConfig{ @@ -338,28 +312,26 @@ func TestConnectivityLite(t *testing.T) { CandidateTypes: []CandidateType{CandidateTypeHost}, NetworkTypes: supportedNetworkTypes(), MulticastDNSMode: MulticastDNSModeDisabled, - Net: v.net1, + Net: vent.net1, } bAgent, err := NewAgent(cfg1) require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) - aConn, bConn := connectWithVNet(aAgent, bAgent) + connectWithVNet(t, aAgent, bAgent) // Ensure pair selected // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair <-aConnected <-bConnected - - if !closePipe(t, aConn, bConn) { - return - } } -func TestInboundValidity(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +func TestInboundValidity(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() buildMsg := func(class stun.MessageClass, username, key string) *stun.Message { msg, err := stun.Build(stun.NewType(stun.MethodBinding, class), stun.TransactionID, @@ -367,9 +339,7 @@ func TestInboundValidity(t *testing.T) { stun.NewShortTermIntegrity(key), stun.Fingerprint, ) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) return msg } @@ -383,101 +353,88 @@ func TestInboundValidity(t *testing.T) { } local, err := NewCandidateHost(&hostConfig) local.conn = &fakenet.MockPacketConn{} - if err != nil { - t.Fatalf("failed to create a new candidate: %v", err) - } + require.NoError(t, err) t.Run("Invalid Binding requests should be discarded", func(t *testing.T) { - a, err := NewAgent(&AgentConfig{}) - if err != nil { - t.Fatalf("Error constructing ice.Agent") - } + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() - a.handleInbound(buildMsg(stun.ClassRequest, "invalid", a.localPwd), local, remote) - if len(a.remoteCandidates) == 1 { - t.Fatal("Binding with invalid Username was able to create prflx candidate") - } + agent.handleInbound(buildMsg(stun.ClassRequest, "invalid", agent.localPwd), local, remote) + require.Len(t, agent.remoteCandidates, 0) - a.handleInbound(buildMsg(stun.ClassRequest, a.localUfrag+":"+a.remoteUfrag, "Invalid"), local, remote) - if len(a.remoteCandidates) == 1 { - t.Fatal("Binding with invalid MessageIntegrity was able to create prflx candidate") - } - - assert.NoError(t, a.Close()) + agent.handleInbound(buildMsg(stun.ClassRequest, agent.localUfrag+":"+agent.remoteUfrag, "Invalid"), local, remote) + require.Len(t, agent.remoteCandidates, 0) }) t.Run("Invalid Binding success responses should be discarded", func(t *testing.T) { a, err := NewAgent(&AgentConfig{}) - if err != nil { - t.Fatalf("Error constructing ice.Agent") - } + require.NoError(t, err) + defer func() { + require.NoError(t, a.Close()) + }() a.handleInbound(buildMsg(stun.ClassSuccessResponse, a.localUfrag+":"+a.remoteUfrag, "Invalid"), local, remote) - if len(a.remoteCandidates) == 1 { - t.Fatal("Binding with invalid MessageIntegrity was able to create prflx candidate") - } - - assert.NoError(t, a.Close()) + require.Len(t, a.remoteCandidates, 0) }) t.Run("Discard non-binding messages", func(t *testing.T) { a, err := NewAgent(&AgentConfig{}) - if err != nil { - t.Fatalf("Error constructing ice.Agent") - } + require.NoError(t, err) + defer func() { + require.NoError(t, a.Close()) + }() a.handleInbound(buildMsg(stun.ClassErrorResponse, a.localUfrag+":"+a.remoteUfrag, "Invalid"), local, remote) - if len(a.remoteCandidates) == 1 { - t.Fatal("non-binding message was able to create prflxRemote") - } - - assert.NoError(t, a.Close()) + require.Len(t, a.remoteCandidates, 0) }) t.Run("Valid bind request", func(t *testing.T) { a, err := NewAgent(&AgentConfig{}) - if err != nil { - t.Fatalf("Error constructing ice.Agent") - } + require.NoError(t, err) + defer func() { + require.NoError(t, a.Close()) + }() - err = a.run(context.Background(), func(ctx context.Context, a *Agent) { + err = a.loop.Run(a.loop, func(_ context.Context) { a.selector = &controllingSelector{agent: a, log: a.log} // nolint: contextcheck a.handleInbound(buildMsg(stun.ClassRequest, a.localUfrag+":"+a.remoteUfrag, a.localPwd), local, remote) - if len(a.remoteCandidates) != 1 { - t.Fatal("Binding with valid values was unable to create prflx candidate") - } + require.Len(t, a.remoteCandidates, 1) }) - assert.NoError(t, err) - assert.NoError(t, a.Close()) + require.NoError(t, err) }) t.Run("Valid bind without fingerprint", func(t *testing.T) { - var config AgentConfig - runAgentTest(t, &config, func(ctx context.Context, a *Agent) { - a.selector = &controllingSelector{agent: a, log: a.log} + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() + + require.NoError(t, agent.loop.Run(agent.loop, func(_ context.Context) { + agent.selector = &controllingSelector{agent: agent, log: agent.log} msg, err := stun.Build(stun.BindingRequest, stun.TransactionID, - stun.NewUsername(a.localUfrag+":"+a.remoteUfrag), - stun.NewShortTermIntegrity(a.localPwd), + stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag), + stun.NewShortTermIntegrity(agent.localPwd), ) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) // nolint: contextcheck - a.handleInbound(msg, local, remote) - if len(a.remoteCandidates) != 1 { - t.Fatal("Binding with valid values (but no fingerprint) was unable to create prflx candidate") - } - }) + agent.handleInbound(msg, local, remote) + require.Len(t, agent.remoteCandidates, 1) + })) }) t.Run("Success with invalid TransactionID", func(t *testing.T) { - a, err := NewAgent(&AgentConfig{}) - if err != nil { - t.Fatalf("Error constructing ice.Agent") - } + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() hostConfig := CandidateHostConfig{ Network: "udp", @@ -487,65 +444,53 @@ func TestInboundValidity(t *testing.T) { } local, err := NewCandidateHost(&hostConfig) local.conn = &fakenet.MockPacketConn{} - if err != nil { - t.Fatalf("failed to create a new candidate: %v", err) - } + require.NoError(t, err) remote := &net.UDPAddr{IP: net.ParseIP("172.17.0.3"), Port: 999} tID := [stun.TransactionIDSize]byte{} copy(tID[:], "ABC") msg, err := stun.Build(stun.BindingSuccess, stun.NewTransactionIDSetter(tID), - stun.NewShortTermIntegrity(a.remotePwd), + stun.NewShortTermIntegrity(agent.remotePwd), stun.Fingerprint, ) - assert.NoError(t, err) + require.NoError(t, err) - a.handleInbound(msg, local, remote) - if len(a.remoteCandidates) != 0 { - t.Fatal("unknown remote was able to create a candidate") - } - - assert.NoError(t, a.Close()) + agent.handleInbound(msg, local, remote) + require.Len(t, agent.remoteCandidates, 0) }) } func TestInvalidAgentStarts(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - a, err := NewAgent(&AgentConfig{}) - assert.NoError(t, err) + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() - if _, err = a.Dial(ctx, "", "bar"); err != nil && !errors.Is(err, ErrRemoteUfragEmpty) { - t.Fatal(err) - } + _, err = agent.Dial(ctx, "", "bar") + require.ErrorIs(t, ErrRemoteUfragEmpty, err) - if _, err = a.Dial(ctx, "foo", ""); err != nil && !errors.Is(err, ErrRemotePwdEmpty) { - t.Fatal(err) - } + _, err = agent.Dial(ctx, "foo", "") + require.ErrorIs(t, ErrRemotePwdEmpty, err) - if _, err = a.Dial(ctx, "foo", "bar"); err != nil && !errors.Is(err, ErrCanceledByCaller) { - t.Fatal(err) - } + _, err = agent.Dial(ctx, "foo", "bar") + require.ErrorIs(t, ErrCanceledByCaller, err) - if _, err = a.Dial(context.TODO(), "foo", "bar"); err != nil && !errors.Is(err, ErrMultipleStart) { - t.Fatal(err) - } - - assert.NoError(t, a.Close()) + _, err = agent.Dial(ctx, "foo", "bar") + require.ErrorIs(t, ErrMultipleStart, err) } -// Assert that Agent emits Connecting/Connected/Disconnected/Failed/Closed messages -func TestConnectionStateCallback(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +// Assert that Agent emits Connecting/Connected/Disconnected/Failed/Closed messages. +func TestConnectionStateCallback(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 5) - defer lim.Stop() + defer test.TimeOut(time.Second * 5).Stop() disconnectedDuration := time.Second failedDuration := time.Second @@ -557,23 +502,37 @@ func TestConnectionStateCallback(t *testing.T) { DisconnectedTimeout: &disconnectedDuration, FailedTimeout: &failedDuration, KeepaliveInterval: &KeepaliveInterval, + InterfaceFilter: problematicNetworkInterfaces, } + isClosed := make(chan any) + aAgent, err := NewAgent(cfg) - if err != nil { - t.Error(err) - } + require.NoError(t, err) + defer func() { + select { + case <-isClosed: + return + default: + } + require.NoError(t, aAgent.Close()) + }() bAgent, err := NewAgent(cfg) - if err != nil { - t.Error(err) - } + require.NoError(t, err) + defer func() { + select { + case <-isClosed: + return + default: + } + require.NoError(t, bAgent.Close()) + }() - isChecking := make(chan interface{}) - isConnected := make(chan interface{}) - isDisconnected := make(chan interface{}) - isFailed := make(chan interface{}) - isClosed := make(chan interface{}) + isChecking := make(chan any) + isConnected := make(chan any) + isDisconnected := make(chan any) + isFailed := make(chan any) err = aAgent.OnConnectionStateChange(func(c ConnectionState) { switch c { case ConnectionStateChecking: @@ -589,19 +548,17 @@ func TestConnectionStateCallback(t *testing.T) { default: } }) - if err != nil { - t.Error(err) - } + require.NoError(t, err) - connect(aAgent, bAgent) + connect(t, aAgent, bAgent) <-isChecking <-isConnected <-isDisconnected <-isFailed - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) + require.NoError(t, aAgent.Close()) + require.NoError(t, bAgent.Close()) <-isClosed } @@ -609,29 +566,27 @@ func TestConnectionStateCallback(t *testing.T) { func TestInvalidGather(t *testing.T) { t.Run("Gather with no OnCandidate should error", func(t *testing.T) { a, err := NewAgent(&AgentConfig{}) - if err != nil { - t.Fatalf("Error constructing ice.Agent") - } + require.NoError(t, err) + defer func() { + require.NoError(t, a.Close()) + }() err = a.GatherCandidates() - if !errors.Is(err, ErrNoOnCandidateHandler) { - t.Fatal("trickle GatherCandidates succeeded without OnCandidate") - } - assert.NoError(t, a.Close()) + require.ErrorIs(t, ErrNoOnCandidateHandler, err) }) } -func TestCandidatePairStats(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +func TestCandidatePairsStats(t *testing.T) { //nolint:cyclop,gocyclo + defer test.CheckRoutines(t)() // Avoid deadlocks? defer test.TimeOut(1 * time.Second).Stop() - a, err := NewAgent(&AgentConfig{}) - if err != nil { - t.Fatalf("Failed to create agent: %s", err) - } + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() hostConfig := &CandidateHostConfig{ Network: "udp", @@ -640,9 +595,7 @@ func TestCandidatePairStats(t *testing.T) { Component: 1, } hostLocal, err := NewCandidateHost(hostConfig) - if err != nil { - t.Fatalf("Failed to construct local host candidate: %s", err) - } + require.NoError(t, err) relayConfig := &CandidateRelayConfig{ Network: "udp", @@ -653,9 +606,7 @@ func TestCandidatePairStats(t *testing.T) { RelPort: 43210, } relayRemote, err := NewCandidateRelay(relayConfig) - if err != nil { - t.Fatalf("Failed to construct remote relay candidate: %s", err) - } + require.NoError(t, err) srflxConfig := &CandidateServerReflexiveConfig{ Network: "udp", @@ -666,9 +617,7 @@ func TestCandidatePairStats(t *testing.T) { RelPort: 43212, } srflxRemote, err := NewCandidateServerReflexive(srflxConfig) - if err != nil { - t.Fatalf("Failed to construct remote srflx candidate: %s", err) - } + require.NoError(t, err) prflxConfig := &CandidatePeerReflexiveConfig{ Network: "udp", @@ -679,9 +628,7 @@ func TestCandidatePairStats(t *testing.T) { RelPort: 43211, } prflxRemote, err := NewCandidatePeerReflexive(prflxConfig) - if err != nil { - t.Fatalf("Failed to construct remote prflx candidate: %s", err) - } + require.NoError(t, err) hostConfig = &CandidateHostConfig{ Network: "udp", @@ -690,32 +637,34 @@ func TestCandidatePairStats(t *testing.T) { Component: 1, } hostRemote, err := NewCandidateHost(hostConfig) - if err != nil { - t.Fatalf("Failed to construct remote host candidate: %s", err) - } + require.NoError(t, err) for _, remote := range []Candidate{relayRemote, srflxRemote, prflxRemote, hostRemote} { - p := a.findPair(hostLocal, remote) + p := agent.findPair(hostLocal, remote) if p == nil { - a.addPair(hostLocal, remote) + p = agent.addPair(hostLocal, remote) } + p.UpdateRequestReceived() + p.UpdateRequestSent() + p.UpdateResponseSent() + p.UpdateRoundTripTime(time.Second) } - p := a.findPair(hostLocal, prflxRemote) + p := agent.findPair(hostLocal, prflxRemote) p.state = CandidatePairStateFailed - stats := a.GetCandidatePairsStats() - if len(stats) != 4 { - t.Fatal("expected 4 candidate pairs stats") + for i := 1; i < 10; i++ { + p.UpdateRoundTripTime(time.Duration(i+1) * time.Second) } + stats := agent.GetCandidatePairsStats() + require.Len(t, stats, 4) + var relayPairStat, srflxPairStat, prflxPairStat, hostPairStat CandidatePairStats for _, cps := range stats { - if cps.LocalCandidateID != hostLocal.ID() { - t.Fatal("invalid local candidate id") - } + require.Equal(t, cps.LocalCandidateID, hostLocal.ID()) switch cps.RemoteCandidateID { case relayRemote.ID(): relayPairStat = cps @@ -726,45 +675,43 @@ func TestCandidatePairStats(t *testing.T) { case hostRemote.ID(): hostPairStat = cps default: - t.Fatal("invalid remote candidate ID") + t.Fatal("invalid remote candidate ID") //nolint } + + require.False(t, cps.FirstRequestTimestamp.IsZero()) + require.False(t, cps.LastRequestTimestamp.IsZero()) + require.False(t, cps.FirstResponseTimestamp.IsZero()) + require.False(t, cps.LastResponseTimestamp.IsZero()) + require.False(t, cps.FirstRequestReceivedTimestamp.IsZero()) + require.False(t, cps.LastRequestReceivedTimestamp.IsZero()) + require.NotZero(t, cps.RequestsReceived) + require.NotZero(t, cps.RequestsSent) + require.NotZero(t, cps.ResponsesSent) + require.NotZero(t, cps.ResponsesReceived) } - if relayPairStat.RemoteCandidateID != relayRemote.ID() { - t.Fatal("missing host-relay pair stat") - } + require.Equal(t, relayPairStat.RemoteCandidateID, relayRemote.ID()) + require.Equal(t, srflxPairStat.RemoteCandidateID, srflxRemote.ID()) + require.Equal(t, prflxPairStat.RemoteCandidateID, prflxRemote.ID()) + require.Equal(t, hostPairStat.RemoteCandidateID, hostRemote.ID()) + require.Equal(t, prflxPairStat.State, CandidatePairStateFailed) - if srflxPairStat.RemoteCandidateID != srflxRemote.ID() { - t.Fatal("missing host-srflx pair stat") - } - - if prflxPairStat.RemoteCandidateID != prflxRemote.ID() { - t.Fatal("missing host-prflx pair stat") - } - - if hostPairStat.RemoteCandidateID != hostRemote.ID() { - t.Fatal("missing host-host pair stat") - } - - if prflxPairStat.State != CandidatePairStateFailed { - t.Fatalf("expected host-prflx pair to have state failed, it has state %s instead", - prflxPairStat.State.String()) - } - - assert.NoError(t, a.Close()) + require.Equal(t, float64(10), prflxPairStat.CurrentRoundTripTime) + require.Equal(t, float64(55), prflxPairStat.TotalRoundTripTime) + require.Equal(t, uint64(10), prflxPairStat.ResponsesReceived) } -func TestLocalCandidateStats(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +func TestSelectedCandidatePairStats(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() // Avoid deadlocks? defer test.TimeOut(1 * time.Second).Stop() - a, err := NewAgent(&AgentConfig{}) - if err != nil { - t.Fatalf("Failed to create agent: %s", err) - } + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() hostConfig := &CandidateHostConfig{ Network: "udp", @@ -773,9 +720,66 @@ func TestLocalCandidateStats(t *testing.T) { Component: 1, } hostLocal, err := NewCandidateHost(hostConfig) - if err != nil { - t.Fatalf("Failed to construct local host candidate: %s", err) + require.NoError(t, err) + + srflxConfig := &CandidateServerReflexiveConfig{ + Network: "udp", + Address: "10.10.10.2", + Port: 19218, + Component: 1, + RelAddr: "4.3.2.1", + RelPort: 43212, } + srflxRemote, err := NewCandidateServerReflexive(srflxConfig) + require.NoError(t, err) + + // no selected pair, should return not available + _, ok := agent.GetSelectedCandidatePairStats() + require.False(t, ok) + + // add pair and populate some RTT stats + p := agent.findPair(hostLocal, srflxRemote) + if p == nil { + agent.addPair(hostLocal, srflxRemote) + p = agent.findPair(hostLocal, srflxRemote) + } + for i := 0; i < 10; i++ { + p.UpdateRoundTripTime(time.Duration(i+1) * time.Second) + } + + // set the pair as selected + agent.setSelectedPair(p) + + stats, ok := agent.GetSelectedCandidatePairStats() + require.True(t, ok) + + require.Equal(t, stats.LocalCandidateID, hostLocal.ID()) + require.Equal(t, stats.RemoteCandidateID, srflxRemote.ID()) + require.Equal(t, float64(10), stats.CurrentRoundTripTime) + require.Equal(t, float64(55), stats.TotalRoundTripTime) + require.Equal(t, uint64(10), stats.ResponsesReceived) +} + +func TestLocalCandidateStats(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() + + // Avoid deadlocks? + defer test.TimeOut(1 * time.Second).Stop() + + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() + + hostConfig := &CandidateHostConfig{ + Network: "udp", + Address: "192.168.1.1", + Port: 19216, + Component: 1, + } + hostLocal, err := NewCandidateHost(hostConfig) + require.NoError(t, err) srflxConfig := &CandidateServerReflexiveConfig{ Network: "udp", @@ -786,16 +790,12 @@ func TestLocalCandidateStats(t *testing.T) { RelPort: 43212, } srflxLocal, err := NewCandidateServerReflexive(srflxConfig) - if err != nil { - t.Fatalf("Failed to construct local srflx candidate: %s", err) - } + require.NoError(t, err) - a.localCandidates[NetworkTypeUDP4] = []Candidate{hostLocal, srflxLocal} + agent.localCandidates[NetworkTypeUDP4] = []Candidate{hostLocal, srflxLocal} - localStats := a.GetLocalCandidatesStats() - if len(localStats) != 2 { - t.Fatalf("expected 2 local candidates stats, got %d instead", len(localStats)) - } + localStats := agent.GetLocalCandidatesStats() + require.Len(t, localStats, 2) var hostLocalStat, srflxLocalStat CandidateStats for _, stats := range localStats { @@ -808,44 +808,29 @@ func TestLocalCandidateStats(t *testing.T) { srflxLocalStat = stats candidate = srflxLocal default: - t.Fatal("invalid local candidate ID") + t.Fatal("invalid local candidate ID") // nolint } - if stats.CandidateType != candidate.Type() { - t.Fatal("invalid stats CandidateType") - } - - if stats.Priority != candidate.Priority() { - t.Fatal("invalid stats CandidateType") - } - - if stats.IP != candidate.Address() { - t.Fatal("invalid stats IP") - } + require.Equal(t, stats.CandidateType, candidate.Type()) + require.Equal(t, stats.Priority, candidate.Priority()) + require.Equal(t, stats.IP, candidate.Address()) } - if hostLocalStat.ID != hostLocal.ID() { - t.Fatal("missing host local stat") - } - - if srflxLocalStat.ID != srflxLocal.ID() { - t.Fatal("missing srflx local stat") - } - - assert.NoError(t, a.Close()) + require.Equal(t, hostLocalStat.ID, hostLocal.ID()) + require.Equal(t, srflxLocalStat.ID, srflxLocal.ID()) } -func TestRemoteCandidateStats(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +func TestRemoteCandidateStats(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() // Avoid deadlocks? defer test.TimeOut(1 * time.Second).Stop() - a, err := NewAgent(&AgentConfig{}) - if err != nil { - t.Fatalf("Failed to create agent: %s", err) - } + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() relayConfig := &CandidateRelayConfig{ Network: "udp", @@ -856,9 +841,7 @@ func TestRemoteCandidateStats(t *testing.T) { RelPort: 43210, } relayRemote, err := NewCandidateRelay(relayConfig) - if err != nil { - t.Fatalf("Failed to construct remote relay candidate: %s", err) - } + require.NoError(t, err) srflxConfig := &CandidateServerReflexiveConfig{ Network: "udp", @@ -869,9 +852,7 @@ func TestRemoteCandidateStats(t *testing.T) { RelPort: 43212, } srflxRemote, err := NewCandidateServerReflexive(srflxConfig) - if err != nil { - t.Fatalf("Failed to construct remote srflx candidate: %s", err) - } + require.NoError(t, err) prflxConfig := &CandidatePeerReflexiveConfig{ Network: "udp", @@ -882,9 +863,7 @@ func TestRemoteCandidateStats(t *testing.T) { RelPort: 43211, } prflxRemote, err := NewCandidatePeerReflexive(prflxConfig) - if err != nil { - t.Fatalf("Failed to construct remote prflx candidate: %s", err) - } + require.NoError(t, err) hostConfig := &CandidateHostConfig{ Network: "udp", @@ -893,16 +872,12 @@ func TestRemoteCandidateStats(t *testing.T) { Component: 1, } hostRemote, err := NewCandidateHost(hostConfig) - if err != nil { - t.Fatalf("Failed to construct remote host candidate: %s", err) - } + require.NoError(t, err) - a.remoteCandidates[NetworkTypeUDP4] = []Candidate{relayRemote, srflxRemote, prflxRemote, hostRemote} + agent.remoteCandidates[NetworkTypeUDP4] = []Candidate{relayRemote, srflxRemote, prflxRemote, hostRemote} - remoteStats := a.GetRemoteCandidatesStats() - if len(remoteStats) != 4 { - t.Fatalf("expected 4 remote candidates stats, got %d instead", len(remoteStats)) - } + remoteStats := agent.GetRemoteCandidatesStats() + require.Len(t, remoteStats, 4) var relayRemoteStat, srflxRemoteStat, prflxRemoteStat, hostRemoteStat CandidateStats for _, stats := range remoteStats { var candidate Candidate @@ -920,67 +895,37 @@ func TestRemoteCandidateStats(t *testing.T) { hostRemoteStat = stats candidate = hostRemote default: - t.Fatal("invalid remote candidate ID") + t.Fatal("invalid remote candidate ID") // nolint } - if stats.CandidateType != candidate.Type() { - t.Fatal("invalid stats CandidateType") - } - - if stats.Priority != candidate.Priority() { - t.Fatal("invalid stats CandidateType") - } - - if stats.IP != candidate.Address() { - t.Fatal("invalid stats IP") - } + require.Equal(t, stats.CandidateType, candidate.Type()) + require.Equal(t, stats.Priority, candidate.Priority()) + require.Equal(t, stats.IP, candidate.Address()) } - if relayRemoteStat.ID != relayRemote.ID() { - t.Fatal("missing relay remote stat") - } - - if srflxRemoteStat.ID != srflxRemote.ID() { - t.Fatal("missing srflx remote stat") - } - - if prflxRemoteStat.ID != prflxRemote.ID() { - t.Fatal("missing prflx remote stat") - } - - if hostRemoteStat.ID != hostRemote.ID() { - t.Fatal("missing host remote stat") - } - - assert.NoError(t, a.Close()) + require.Equal(t, relayRemoteStat.ID, relayRemote.ID()) + require.Equal(t, srflxRemoteStat.ID, srflxRemote.ID()) + require.Equal(t, prflxRemoteStat.ID, prflxRemote.ID()) + require.Equal(t, hostRemoteStat.ID, hostRemote.ID()) } func TestInitExtIPMapping(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - // a.extIPMapper should be nil by default - a, err := NewAgent(&AgentConfig{}) - if err != nil { - t.Fatalf("Failed to create agent: %v", err) - } - if a.extIPMapper != nil { - t.Fatal("a.extIPMapper should be nil by default") - } - assert.NoError(t, a.Close()) + // agent.extIPMapper should be nil by default + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + require.Nil(t, agent.extIPMapper) + require.NoError(t, agent.Close()) // a.extIPMapper should be nil when NAT1To1IPs is a non-nil empty array - a, err = NewAgent(&AgentConfig{ + agent, err = NewAgent(&AgentConfig{ NAT1To1IPs: []string{}, NAT1To1IPCandidateType: CandidateTypeHost, }) - if err != nil { - t.Fatalf("Failed to create agent: %v", err) - } - if a.extIPMapper != nil { - t.Fatal("a.extIPMapper should be nil by default") - } - assert.NoError(t, a.Close()) + require.NoError(t, err) + require.Nil(t, agent.extIPMapper) + require.NoError(t, agent.Close()) // NewAgent should return an error when 1:1 NAT for host candidate is enabled // but the candidate type does not appear in the CandidateTypes. @@ -989,9 +934,7 @@ func TestInitExtIPMapping(t *testing.T) { NAT1To1IPCandidateType: CandidateTypeHost, CandidateTypes: []CandidateType{CandidateTypeRelay}, }) - if !errors.Is(err, ErrIneffectiveNAT1To1IPMappingHost) { - t.Fatalf("Unexpected error: %v", err) - } + require.ErrorIs(t, ErrIneffectiveNAT1To1IPMappingHost, err) // NewAgent should return an error when 1:1 NAT for srflx candidate is enabled // but the candidate type does not appear in the CandidateTypes. @@ -1000,9 +943,7 @@ func TestInitExtIPMapping(t *testing.T) { NAT1To1IPCandidateType: CandidateTypeServerReflexive, CandidateTypes: []CandidateType{CandidateTypeRelay}, }) - if !errors.Is(err, ErrIneffectiveNAT1To1IPMappingSrflx) { - t.Fatalf("Unexpected error: %v", err) - } + require.ErrorIs(t, ErrIneffectiveNAT1To1IPMappingSrflx, err) // NewAgent should return an error when 1:1 NAT for host candidate is enabled // along with mDNS with MulticastDNSModeQueryAndGather @@ -1011,53 +952,55 @@ func TestInitExtIPMapping(t *testing.T) { NAT1To1IPCandidateType: CandidateTypeHost, MulticastDNSMode: MulticastDNSModeQueryAndGather, }) - if !errors.Is(err, ErrMulticastDNSWithNAT1To1IPMapping) { - t.Fatalf("Unexpected error: %v", err) - } + require.ErrorIs(t, ErrMulticastDNSWithNAT1To1IPMapping, err) // NewAgent should return if newExternalIPMapper() returns an error. _, err = NewAgent(&AgentConfig{ NAT1To1IPs: []string{"bad.2.3.4"}, // Bad IP NAT1To1IPCandidateType: CandidateTypeHost, }) - if !errors.Is(err, ErrInvalidNAT1To1IPMapping) { - t.Fatalf("Unexpected error: %v", err) - } + require.ErrorIs(t, ErrInvalidNAT1To1IPMapping, err) } func TestBindingRequestTimeout(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() const expectedRemovalCount = 2 - a, err := NewAgent(&AgentConfig{}) - assert.NoError(t, err) + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() now := time.Now() - a.pendingBindingRequests = append(a.pendingBindingRequests, bindingRequest{ + agent.pendingBindingRequests = append(agent.pendingBindingRequests, bindingRequest{ timestamp: now, // Valid }) - a.pendingBindingRequests = append(a.pendingBindingRequests, bindingRequest{ + agent.pendingBindingRequests = append(agent.pendingBindingRequests, bindingRequest{ timestamp: now.Add(-3900 * time.Millisecond), // Valid }) - a.pendingBindingRequests = append(a.pendingBindingRequests, bindingRequest{ + agent.pendingBindingRequests = append(agent.pendingBindingRequests, bindingRequest{ timestamp: now.Add(-4100 * time.Millisecond), // Invalid }) - a.pendingBindingRequests = append(a.pendingBindingRequests, bindingRequest{ + agent.pendingBindingRequests = append(agent.pendingBindingRequests, bindingRequest{ timestamp: now.Add(-75 * time.Hour), // Invalid }) - a.invalidatePendingBindingRequests(now) - assert.Equal(t, expectedRemovalCount, len(a.pendingBindingRequests), "Binding invalidation due to timeout did not remove the correct number of binding requests") - assert.NoError(t, a.Close()) + agent.invalidatePendingBindingRequests(now) + + require.Equal( + t, + expectedRemovalCount, + len(agent.pendingBindingRequests), + "Binding invalidation due to timeout did not remove the correct number of binding requests", + ) } // TestAgentCredentials checks if local username fragments and passwords (if set) meet RFC standard -// and ensure it's backwards compatible with previous versions of the pion/ice +// and ensure it's backwards compatible with previous versions of the pion/ice. func TestAgentCredentials(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() // Make sure to pass Travis check by disabling the logs log := logging.NewDefaultLoggerFactory() @@ -1067,10 +1010,12 @@ func TestAgentCredentials(t *testing.T) { // If set, they should follow the default 16/128 bits random number generator strategy agent, err := NewAgent(&AgentConfig{LoggerFactory: log}) - assert.NoError(t, err) - assert.GreaterOrEqual(t, len([]rune(agent.localUfrag))*8, 24) - assert.GreaterOrEqual(t, len([]rune(agent.localPwd))*8, 128) - assert.NoError(t, agent.Close()) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() + require.GreaterOrEqual(t, len([]rune(agent.localUfrag))*8, 24) + require.GreaterOrEqual(t, len([]rune(agent.localPwd))*8, 128) // Should honor RFC standards // Local values MUST be unguessable, with at least 128 bits of @@ -1078,20 +1023,18 @@ func TestAgentCredentials(t *testing.T) { // at least 24 bits of output to generate the username fragment. _, err = NewAgent(&AgentConfig{LocalUfrag: "xx", LoggerFactory: log}) - assert.EqualError(t, err, ErrLocalUfragInsufficientBits.Error()) + require.EqualError(t, err, ErrLocalUfragInsufficientBits.Error()) _, err = NewAgent(&AgentConfig{LocalPwd: "xxxxxx", LoggerFactory: log}) - assert.EqualError(t, err, ErrLocalPwdInsufficientBits.Error()) + require.EqualError(t, err, ErrLocalPwdInsufficientBits.Error()) } // Assert that Agent on Failure deletes all existing candidates -// User can then do an ICE Restart to bring agent back +// User can then do an ICE Restart to bring agent back. func TestConnectionStateFailedDeleteAllCandidates(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 5) - defer lim.Stop() + defer test.TimeOut(time.Second * 5).Stop() oneSecond := time.Second KeepaliveInterval := time.Duration(0) @@ -1104,40 +1047,41 @@ func TestConnectionStateFailedDeleteAllCandidates(t *testing.T) { } aAgent, err := NewAgent(cfg) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() bAgent, err := NewAgent(cfg) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() - isFailed := make(chan interface{}) - assert.NoError(t, aAgent.OnConnectionStateChange(func(c ConnectionState) { + isFailed := make(chan any) + require.NoError(t, aAgent.OnConnectionStateChange(func(c ConnectionState) { if c == ConnectionStateFailed { close(isFailed) } })) - connect(aAgent, bAgent) + connect(t, aAgent, bAgent) <-isFailed done := make(chan struct{}) - assert.NoError(t, aAgent.run(context.Background(), func(ctx context.Context, agent *Agent) { - assert.Equal(t, len(aAgent.remoteCandidates), 0) - assert.Equal(t, len(aAgent.localCandidates), 0) + require.NoError(t, aAgent.loop.Run(context.Background(), func(context.Context) { + require.Equal(t, len(aAgent.remoteCandidates), 0) + require.Equal(t, len(aAgent.localCandidates), 0) close(done) })) <-done - - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) } -// Assert that the ICE Agent can go directly from Connecting -> Failed on both sides +// Assert that the ICE Agent can go directly from Connecting -> Failed on both sides. func TestConnectionStateConnectingToFailed(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 5) - defer lim.Stop() + defer test.TimeOut(time.Second * 5).Stop() oneSecond := time.Second KeepaliveInterval := time.Duration(0) @@ -1149,10 +1093,16 @@ func TestConnectionStateConnectingToFailed(t *testing.T) { } aAgent, err := NewAgent(cfg) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() bAgent, err := NewAgent(cfg) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() var isFailed sync.WaitGroup var isChecking sync.WaitGroup @@ -1167,168 +1117,160 @@ func TestConnectionStateConnectingToFailed(t *testing.T) { case ConnectionStateChecking: isChecking.Done() case ConnectionStateCompleted: - t.Errorf("Unexpected ConnectionState: %v", c) + t.Errorf("Unexpected ConnectionState: %v", c) //nolint default: } } - assert.NoError(t, aAgent.OnConnectionStateChange(connectionStateCheck)) - assert.NoError(t, bAgent.OnConnectionStateChange(connectionStateCheck)) + require.NoError(t, aAgent.OnConnectionStateChange(connectionStateCheck)) + require.NoError(t, bAgent.OnConnectionStateChange(connectionStateCheck)) go func() { _, err := aAgent.Accept(context.TODO(), "InvalidFrag", "InvalidPwd") - assert.Error(t, err) + require.Error(t, err) }() go func() { _, err := bAgent.Dial(context.TODO(), "InvalidFrag", "InvalidPwd") - assert.Error(t, err) + require.Error(t, err) }() isChecking.Wait() isFailed.Wait() - - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) } func TestAgentRestart(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() oneSecond := time.Second t.Run("Restart During Gather", func(t *testing.T) { - connA, connB := pipe(&AgentConfig{ + connA, connB := pipe(t, &AgentConfig{ DisconnectedTimeout: &oneSecond, FailedTimeout: &oneSecond, }) + defer closePipe(t, connA, connB) ctx, cancel := context.WithCancel(context.Background()) - assert.NoError(t, connB.agent.OnConnectionStateChange(func(c ConnectionState) { + require.NoError(t, connB.agent.OnConnectionStateChange(func(c ConnectionState) { if c == ConnectionStateFailed || c == ConnectionStateDisconnected { cancel() } })) connA.agent.gatheringState = GatheringStateGathering - assert.NoError(t, connA.agent.Restart("", "")) + require.NoError(t, connA.agent.Restart("", "")) <-ctx.Done() - assert.NoError(t, connA.agent.Close()) - assert.NoError(t, connB.agent.Close()) }) t.Run("Restart When Closed", func(t *testing.T) { agent, err := NewAgent(&AgentConfig{}) - assert.NoError(t, err) - assert.NoError(t, agent.Close()) + require.NoError(t, err) + require.NoError(t, agent.Close()) - assert.Equal(t, ErrClosed, agent.Restart("", "")) + require.Equal(t, ErrClosed, agent.Restart("", "")) }) t.Run("Restart One Side", func(t *testing.T) { - connA, connB := pipe(&AgentConfig{ + connA, connB := pipe(t, &AgentConfig{ DisconnectedTimeout: &oneSecond, FailedTimeout: &oneSecond, }) + defer closePipe(t, connA, connB) ctx, cancel := context.WithCancel(context.Background()) - assert.NoError(t, connB.agent.OnConnectionStateChange(func(c ConnectionState) { + require.NoError(t, connB.agent.OnConnectionStateChange(func(c ConnectionState) { if c == ConnectionStateFailed || c == ConnectionStateDisconnected { cancel() } })) - assert.NoError(t, connA.agent.Restart("", "")) + require.NoError(t, connA.agent.Restart("", "")) <-ctx.Done() - assert.NoError(t, connA.agent.Close()) - assert.NoError(t, connB.agent.Close()) }) t.Run("Restart Both Sides", func(t *testing.T) { // Get all addresses of candidates concatenated generateCandidateAddressStrings := func(candidates []Candidate, err error) (out string) { - assert.NoError(t, err) + require.NoError(t, err) for _, c := range candidates { out += c.Address() + ":" out += strconv.Itoa(c.Port()) } + return } // Store the original candidates, confirm that after we reconnect we have new pairs - connA, connB := pipe(&AgentConfig{ + connA, connB := pipe(t, &AgentConfig{ DisconnectedTimeout: &oneSecond, FailedTimeout: &oneSecond, }) + defer closePipe(t, connA, connB) connAFirstCandidates := generateCandidateAddressStrings(connA.agent.GetLocalCandidates()) connBFirstCandidates := generateCandidateAddressStrings(connB.agent.GetLocalCandidates()) aNotifier, aConnected := onConnected() - assert.NoError(t, connA.agent.OnConnectionStateChange(aNotifier)) + require.NoError(t, connA.agent.OnConnectionStateChange(aNotifier)) bNotifier, bConnected := onConnected() - assert.NoError(t, connB.agent.OnConnectionStateChange(bNotifier)) + require.NoError(t, connB.agent.OnConnectionStateChange(bNotifier)) // Restart and Re-Signal - assert.NoError(t, connA.agent.Restart("", "")) - assert.NoError(t, connB.agent.Restart("", "")) + require.NoError(t, connA.agent.Restart("", "")) + require.NoError(t, connB.agent.Restart("", "")) // Exchange Candidates and Credentials ufrag, pwd, err := connB.agent.GetLocalUserCredentials() - assert.NoError(t, err) - assert.NoError(t, connA.agent.SetRemoteCredentials(ufrag, pwd)) + require.NoError(t, err) + require.NoError(t, connA.agent.SetRemoteCredentials(ufrag, pwd)) ufrag, pwd, err = connA.agent.GetLocalUserCredentials() - assert.NoError(t, err) - assert.NoError(t, connB.agent.SetRemoteCredentials(ufrag, pwd)) + require.NoError(t, err) + require.NoError(t, connB.agent.SetRemoteCredentials(ufrag, pwd)) - gatherAndExchangeCandidates(connA.agent, connB.agent) + gatherAndExchangeCandidates(t, connA.agent, connB.agent) // Wait until both have gone back to connected <-aConnected <-bConnected // Assert that we have new candidates each time - assert.NotEqual(t, connAFirstCandidates, generateCandidateAddressStrings(connA.agent.GetLocalCandidates())) - assert.NotEqual(t, connBFirstCandidates, generateCandidateAddressStrings(connB.agent.GetLocalCandidates())) - - assert.NoError(t, connA.agent.Close()) - assert.NoError(t, connB.agent.Close()) + require.NotEqual(t, connAFirstCandidates, generateCandidateAddressStrings(connA.agent.GetLocalCandidates())) + require.NotEqual(t, connBFirstCandidates, generateCandidateAddressStrings(connB.agent.GetLocalCandidates())) }) } func TestGetRemoteCredentials(t *testing.T) { var config AgentConfig - a, err := NewAgent(&config) - if err != nil { - t.Fatalf("Error constructing ice.Agent: %v", err) - } + agent, err := NewAgent(&config) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() - a.remoteUfrag = "remoteUfrag" - a.remotePwd = "remotePwd" + agent.remoteUfrag = "remoteUfrag" + agent.remotePwd = "remotePwd" - actualUfrag, actualPwd, err := a.GetRemoteUserCredentials() - assert.NoError(t, err) + actualUfrag, actualPwd, err := agent.GetRemoteUserCredentials() + require.NoError(t, err) - assert.Equal(t, actualUfrag, a.remoteUfrag) - assert.Equal(t, actualPwd, a.remotePwd) - - assert.NoError(t, a.Close()) + require.Equal(t, actualUfrag, agent.remoteUfrag) + require.Equal(t, actualPwd, agent.remotePwd) } func TestGetRemoteCandidates(t *testing.T) { var config AgentConfig - a, err := NewAgent(&config) - if err != nil { - t.Fatalf("Error constructing ice.Agent: %v", err) - } + agent, err := NewAgent(&config) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() expectedCandidates := []Candidate{} @@ -1341,27 +1283,26 @@ func TestGetRemoteCandidates(t *testing.T) { } cand, errCand := NewCandidateHost(&cfg) - assert.NoError(t, errCand) + require.NoError(t, errCand) expectedCandidates = append(expectedCandidates, cand) - a.addRemoteCandidate(cand) + agent.addRemoteCandidate(cand) } - actualCandidates, err := a.GetRemoteCandidates() - assert.NoError(t, err) - assert.ElementsMatch(t, expectedCandidates, actualCandidates) - - assert.NoError(t, a.Close()) + actualCandidates, err := agent.GetRemoteCandidates() + require.NoError(t, err) + require.ElementsMatch(t, expectedCandidates, actualCandidates) } func TestGetLocalCandidates(t *testing.T) { var config AgentConfig - a, err := NewAgent(&config) - if err != nil { - t.Fatalf("Error constructing ice.Agent: %v", err) - } + agent, err := NewAgent(&config) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() dummyConn := &net.UDPConn{} expectedCandidates := []Candidate{} @@ -1375,27 +1316,23 @@ func TestGetLocalCandidates(t *testing.T) { } cand, errCand := NewCandidateHost(&cfg) - assert.NoError(t, errCand) + require.NoError(t, errCand) expectedCandidates = append(expectedCandidates, cand) - err = a.addCandidate(context.Background(), cand, dummyConn) - assert.NoError(t, err) + err = agent.addCandidate(context.Background(), cand, dummyConn) + require.NoError(t, err) } - actualCandidates, err := a.GetLocalCandidates() - assert.NoError(t, err) - assert.ElementsMatch(t, expectedCandidates, actualCandidates) - - assert.NoError(t, a.Close()) + actualCandidates, err := agent.GetLocalCandidates() + require.NoError(t, err) + require.ElementsMatch(t, expectedCandidates, actualCandidates) } func TestCloseInConnectionStateCallback(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 5) - defer lim.Stop() + defer test.TimeOut(time.Second * 5).Stop() disconnectedDuration := time.Second failedDuration := time.Second @@ -1412,44 +1349,46 @@ func TestCloseInConnectionStateCallback(t *testing.T) { } aAgent, err := NewAgent(cfg) - if err != nil { - t.Error(err) - } + require.NoError(t, err) + var aAgentClosed bool + defer func() { + if aAgentClosed { + return + } + require.NoError(t, aAgent.Close()) + }() bAgent, err := NewAgent(cfg) - if err != nil { - t.Error(err) - } + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() - isClosed := make(chan interface{}) - isConnected := make(chan interface{}) + isClosed := make(chan any) + isConnected := make(chan any) err = aAgent.OnConnectionStateChange(func(c ConnectionState) { switch c { case ConnectionStateConnected: <-isConnected - assert.NoError(t, aAgent.Close()) + require.NoError(t, aAgent.Close()) + aAgentClosed = true case ConnectionStateClosed: close(isClosed) default: } }) - if err != nil { - t.Error(err) - } + require.NoError(t, err) - connect(aAgent, bAgent) + connect(t, aAgent, bAgent) close(isConnected) <-isClosed - assert.NoError(t, bAgent.Close()) } func TestRunTaskInConnectionStateCallback(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 5) - defer lim.Stop() + defer test.TimeOut(time.Second * 5).Stop() oneSecond := time.Second KeepaliveInterval := time.Duration(0) @@ -1465,36 +1404,36 @@ func TestRunTaskInConnectionStateCallback(t *testing.T) { } aAgent, err := NewAgent(cfg) - check(err) + require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() bAgent, err := NewAgent(cfg) - check(err) + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() - isComplete := make(chan interface{}) + isComplete := make(chan any) err = aAgent.OnConnectionStateChange(func(c ConnectionState) { if c == ConnectionStateConnected { _, _, errCred := aAgent.GetLocalUserCredentials() - assert.NoError(t, errCred) - assert.NoError(t, aAgent.Restart("", "")) + require.NoError(t, errCred) + require.NoError(t, aAgent.Restart("", "")) close(isComplete) } }) - if err != nil { - t.Error(err) - } + require.NoError(t, err) - connect(aAgent, bAgent) + connect(t, aAgent, bAgent) <-isComplete - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) } func TestRunTaskInSelectedCandidatePairChangeCallback(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 5) - defer lim.Stop() + defer test.TimeOut(time.Second * 5).Stop() oneSecond := time.Second KeepaliveInterval := time.Duration(0) @@ -1510,44 +1449,45 @@ func TestRunTaskInSelectedCandidatePairChangeCallback(t *testing.T) { } aAgent, err := NewAgent(cfg) - check(err) + require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() bAgent, err := NewAgent(cfg) - check(err) + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() - isComplete := make(chan interface{}) - isTested := make(chan interface{}) - if err = aAgent.OnSelectedCandidatePairChange(func(Candidate, Candidate) { + isComplete := make(chan any) + isTested := make(chan any) + err = aAgent.OnSelectedCandidatePairChange(func(Candidate, Candidate) { go func() { _, _, errCred := aAgent.GetLocalUserCredentials() - assert.NoError(t, errCred) + require.NoError(t, errCred) close(isTested) }() - }); err != nil { - t.Error(err) - } - if err = aAgent.OnConnectionStateChange(func(c ConnectionState) { + }) + require.NoError(t, err) + + err = aAgent.OnConnectionStateChange(func(c ConnectionState) { if c == ConnectionStateConnected { close(isComplete) } - }); err != nil { - t.Error(err) - } + }) + require.NoError(t, err) - connect(aAgent, bAgent) + connect(t, aAgent, bAgent) <-isComplete <-isTested - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) } -// Assert that a Lite agent goes to disconnected and failed +// Assert that a Lite agent goes to disconnected and failed. func TestLiteLifecycle(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() aNotifier, aConnected := onConnected() @@ -1556,6 +1496,13 @@ func TestLiteLifecycle(t *testing.T) { MulticastDNSMode: MulticastDNSModeDisabled, }) require.NoError(t, err) + var aClosed bool + defer func() { + if aClosed { + return + } + require.NoError(t, aAgent.Close()) + }() require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) disconnectedDuration := time.Second @@ -1573,10 +1520,17 @@ func TestLiteLifecycle(t *testing.T) { CheckInterval: &CheckInterval, }) require.NoError(t, err) + var bClosed bool + defer func() { + if bClosed { + return + } + require.NoError(t, bAgent.Close()) + }() - bConnected := make(chan interface{}) - bDisconnected := make(chan interface{}) - bFailed := make(chan interface{}) + bConnected := make(chan any) + bDisconnected := make(chan any) + bFailed := make(chan any) require.NoError(t, bAgent.OnConnectionStateChange(func(c ConnectionState) { switch c { @@ -1590,53 +1544,55 @@ func TestLiteLifecycle(t *testing.T) { } })) - connectWithVNet(bAgent, aAgent) + connectWithVNet(t, bAgent, aAgent) <-aConnected <-bConnected - assert.NoError(t, aAgent.Close()) + require.NoError(t, aAgent.Close()) + aClosed = true <-bDisconnected <-bFailed - assert.NoError(t, bAgent.Close()) + require.NoError(t, bAgent.Close()) + bClosed = true } func TestNilCandidate(t *testing.T) { a, err := NewAgent(&AgentConfig{}) - assert.NoError(t, err) + require.NoError(t, err) - assert.NoError(t, a.AddRemoteCandidate(nil)) - assert.NoError(t, a.Close()) + require.NoError(t, a.AddRemoteCandidate(nil)) + require.NoError(t, a.Close()) } func TestNilCandidatePair(t *testing.T) { a, err := NewAgent(&AgentConfig{}) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, a.Close()) + }() a.setSelectedPair(nil) - assert.NoError(t, a.Close()) } func TestGetSelectedCandidatePair(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() wan, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: "0.0.0.0/0", LoggerFactory: logging.NewDefaultLoggerFactory(), }) - assert.NoError(t, err) + require.NoError(t, err) net, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"192.168.0.1"}, }) - assert.NoError(t, err) - assert.NoError(t, wan.AddNet(net)) + require.NoError(t, err) + require.NoError(t, wan.AddNet(net)) - assert.NoError(t, wan.Start()) + require.NoError(t, wan.Start()) cfg := &AgentConfig{ NetworkTypes: supportedNetworkTypes(), @@ -1644,158 +1600,414 @@ func TestGetSelectedCandidatePair(t *testing.T) { } aAgent, err := NewAgent(cfg) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() bAgent, err := NewAgent(cfg) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() aAgentPair, err := aAgent.GetSelectedCandidatePair() - assert.NoError(t, err) - assert.Nil(t, aAgentPair) + require.NoError(t, err) + require.Nil(t, aAgentPair) bAgentPair, err := bAgent.GetSelectedCandidatePair() - assert.NoError(t, err) - assert.Nil(t, bAgentPair) + require.NoError(t, err) + require.Nil(t, bAgentPair) - connect(aAgent, bAgent) + connect(t, aAgent, bAgent) aAgentPair, err = aAgent.GetSelectedCandidatePair() - assert.NoError(t, err) - assert.NotNil(t, aAgentPair) + require.NoError(t, err) + require.NotNil(t, aAgentPair) bAgentPair, err = bAgent.GetSelectedCandidatePair() - assert.NoError(t, err) - assert.NotNil(t, bAgentPair) + require.NoError(t, err) + require.NotNil(t, bAgentPair) - assert.True(t, bAgentPair.Local.Equal(aAgentPair.Remote)) - assert.True(t, bAgentPair.Remote.Equal(aAgentPair.Local)) + require.True(t, bAgentPair.Local.Equal(aAgentPair.Remote)) + require.True(t, bAgentPair.Remote.Equal(aAgentPair.Local)) - assert.NoError(t, wan.Stop()) - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) + require.NoError(t, wan.Stop()) } -func TestAcceptAggressiveNomination(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +func TestAcceptAggressiveNomination(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() // Create a network with two interfaces wan, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: "0.0.0.0/0", LoggerFactory: logging.NewDefaultLoggerFactory(), }) - assert.NoError(t, err) + require.NoError(t, err) net0, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"192.168.0.1"}, }) - assert.NoError(t, err) - assert.NoError(t, wan.AddNet(net0)) + require.NoError(t, err) + require.NoError(t, wan.AddNet(net0)) net1, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"192.168.0.2", "192.168.0.3", "192.168.0.4"}, }) - assert.NoError(t, err) - assert.NoError(t, wan.AddNet(net1)) + require.NoError(t, err) + require.NoError(t, wan.AddNet(net1)) - assert.NoError(t, wan.Start()) + require.NoError(t, wan.Start()) - aNotifier, aConnected := onConnected() - bNotifier, bConnected := onConnected() - - KeepaliveInterval := time.Hour - cfg0 := &AgentConfig{ - NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, - MulticastDNSMode: MulticastDNSModeDisabled, - Net: net0, - - KeepaliveInterval: &KeepaliveInterval, - CheckInterval: &KeepaliveInterval, - AcceptAggressiveNomination: true, + testCases := []struct { + name string + isLite bool + enableUseCandidateCheckPriority bool + useHigherPriority bool + isExpectedToSwitch bool + }{ + {"should accept higher priority - full agent", false, false, true, true}, + {"should not accept lower priority - full agent", false, false, false, false}, + {"should accept higher priority - no use-candidate priority check - lite agent", true, false, true, true}, + {"should accept lower priority - no use-candidate priority check - lite agent", true, false, false, true}, + {"should accept higher priority - use-candidate priority check - lite agent", true, true, true, true}, + {"should not accept lower priority - use-candidate priority check - lite agent", true, true, false, false}, } - var aAgent, bAgent *Agent - aAgent, err = NewAgent(cfg0) - require.NoError(t, err) - require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + aNotifier, aConnected := onConnected() + bNotifier, bConnected := onConnected() - cfg1 := &AgentConfig{ - NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, - MulticastDNSMode: MulticastDNSModeDisabled, - Net: net1, - KeepaliveInterval: &KeepaliveInterval, - CheckInterval: &KeepaliveInterval, - } + KeepaliveInterval := time.Hour + cfg0 := &AgentConfig{ + NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, + MulticastDNSMode: MulticastDNSModeDisabled, + Net: net0, + KeepaliveInterval: &KeepaliveInterval, + CheckInterval: &KeepaliveInterval, + Lite: tc.isLite, + EnableUseCandidateCheckPriority: tc.enableUseCandidateCheckPriority, + } + if tc.isLite { + cfg0.CandidateTypes = []CandidateType{CandidateTypeHost} + } - bAgent, err = NewAgent(cfg1) - require.NoError(t, err) - require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) + var aAgent, bAgent *Agent + aAgent, err = NewAgent(cfg0) + require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() + require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) - aConn, bConn := connect(aAgent, bAgent) + cfg1 := &AgentConfig{ + NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, + MulticastDNSMode: MulticastDNSModeDisabled, + Net: net1, + KeepaliveInterval: &KeepaliveInterval, + CheckInterval: &KeepaliveInterval, + } - // Ensure pair selected - // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair - <-aConnected - <-bConnected + bAgent, err = NewAgent(cfg1) + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() + require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) - // Send new USE-CANDIDATE message with higher priority to update the selected pair - buildMsg := func(class stun.MessageClass, username, key string, priority uint32) *stun.Message { - msg, err1 := stun.Build(stun.NewType(stun.MethodBinding, class), stun.TransactionID, - stun.NewUsername(username), - stun.NewShortTermIntegrity(key), - UseCandidate(), - PriorityAttr(priority), - stun.Fingerprint, - ) - if err1 != nil { - t.Fatal(err1) - } + connect(t, aAgent, bAgent) - return msg - } + // Ensure pair selected + // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair + <-aConnected + <-bConnected - selectedCh := make(chan Candidate, 1) - var expectNewSelectedCandidate Candidate - err = aAgent.OnSelectedCandidatePairChange(func(_, remote Candidate) { - selectedCh <- remote - }) - require.NoError(t, err) - var bcandidates []Candidate - bcandidates, err = bAgent.GetLocalCandidates() - require.NoError(t, err) + // Send new USE-CANDIDATE message with priority to update the selected pair + buildMsg := func(class stun.MessageClass, username, key string, priority uint32) *stun.Message { + msg, err1 := stun.Build(stun.NewType(stun.MethodBinding, class), stun.TransactionID, + stun.NewUsername(username), + stun.NewShortTermIntegrity(key), + UseCandidate(), + PriorityAttr(priority), + stun.Fingerprint, + ) + require.NoError(t, err1) - for _, c := range bcandidates { - if c != bAgent.getSelectedPair().Local { - if expectNewSelectedCandidate == nil { - incr_priority: - for _, candidates := range aAgent.remoteCandidates { - for _, candidate := range candidates { - if candidate.Equal(c) { - candidate.(*CandidateHost).priorityOverride += 1000 //nolint:forcetypeassert - break incr_priority + return msg + } + + selectedCh := make(chan Candidate, 1) + var expectNewSelectedCandidate Candidate + err = aAgent.OnSelectedCandidatePairChange(func(_, remote Candidate) { + selectedCh <- remote + }) + require.NoError(t, err) + var bcandidates []Candidate + bcandidates, err = bAgent.GetLocalCandidates() + require.NoError(t, err) + + for _, cand := range bcandidates { + if cand != bAgent.getSelectedPair().Local { //nolint:nestif + if expectNewSelectedCandidate == nil { + expected_change_priority: + for _, candidates := range aAgent.remoteCandidates { + for _, candidate := range candidates { + if candidate.Equal(cand) { + if tc.useHigherPriority { + candidate.(*CandidateHost).priorityOverride += 1000 //nolint:forcetypeassert + } else { + candidate.(*CandidateHost).priorityOverride -= 1000 //nolint:forcetypeassert + } + + break expected_change_priority + } + } + } + if tc.isExpectedToSwitch { + expectNewSelectedCandidate = cand + } else { + expectNewSelectedCandidate = aAgent.getSelectedPair().Remote + } + } else { + // a smaller change for other candidates other the new expected one + change_priority: + for _, candidates := range aAgent.remoteCandidates { + for _, candidate := range candidates { + if candidate.Equal(cand) { + if tc.useHigherPriority { + candidate.(*CandidateHost).priorityOverride += 500 //nolint:forcetypeassert + } else { + candidate.(*CandidateHost).priorityOverride -= 500 //nolint:forcetypeassert + } + + break change_priority + } + } } } + _, err = cand.writeTo( + buildMsg( + stun.ClassRequest, + aAgent.localUfrag+":"+aAgent.remoteUfrag, + aAgent.localPwd, + cand.Priority(), + ).Raw, + bAgent.getSelectedPair().Remote, + ) + require.NoError(t, err) } - expectNewSelectedCandidate = c } - _, err = c.writeTo(buildMsg(stun.ClassRequest, aAgent.localUfrag+":"+aAgent.remoteUfrag, aAgent.localPwd, c.Priority()).Raw, bAgent.getSelectedPair().Remote) - require.NoError(t, err) + + time.Sleep(1 * time.Second) + select { + case selected := <-selectedCh: + require.True(t, selected.Equal(expectNewSelectedCandidate)) + default: + require.False(t, tc.isExpectedToSwitch) + require.True(t, aAgent.getSelectedPair().Remote.Equal(expectNewSelectedCandidate)) + } + }) + } + + require.NoError(t, wan.Stop()) +} + +// Close can deadlock but GracefulClose must not. +func TestAgentGracefulCloseDeadlock(t *testing.T) { + defer test.CheckRoutinesStrict(t)() + defer test.TimeOut(time.Second * 5).Stop() + + config := &AgentConfig{ + NetworkTypes: supportedNetworkTypes(), + } + aAgent, err := NewAgent(config) + require.NoError(t, err) + var aAgentClosed bool + defer func() { + if aAgentClosed { + return } + require.NoError(t, aAgent.Close()) + }() + + bAgent, err := NewAgent(config) + require.NoError(t, err) + var bAgentClosed bool + defer func() { + if bAgentClosed { + return + } + require.NoError(t, bAgent.Close()) + }() + + var connected, closeNow, closed sync.WaitGroup + connected.Add(2) + closeNow.Add(1) + closed.Add(2) + closeHdlr := func(agent *Agent, agentClosed *bool) { + require.NoError(t, agent.OnConnectionStateChange(func(cs ConnectionState) { + if cs == ConnectionStateConnected { + connected.Done() + closeNow.Wait() + + go func() { + require.NoError(t, agent.GracefulClose()) + *agentClosed = true + closed.Done() + }() + } + })) } - time.Sleep(1 * time.Second) - select { - case selected := <-selectedCh: - assert.True(t, selected.Equal(expectNewSelectedCandidate)) - default: - t.Fatal("No selected candidate pair") + closeHdlr(aAgent, &aAgentClosed) + closeHdlr(bAgent, &bAgentClosed) + + t.Log("connecting agents") + _, _ = connect(t, aAgent, bAgent) + + t.Log("waiting for them to confirm connection in callback") + connected.Wait() + + t.Log("tell them to close themselves in the same callback and wait") + closeNow.Done() + closed.Wait() +} + +func TestSetCandidatesUfrag(t *testing.T) { + var config AgentConfig + + agent, err := NewAgent(&config) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() + + dummyConn := &net.UDPConn{} + + for i := 0; i < 5; i++ { + cfg := CandidateHostConfig{ + Network: "udp", + Address: "192.168.0.2", + Port: 1000 + i, + Component: 1, + } + + cand, errCand := NewCandidateHost(&cfg) + require.NoError(t, errCand) + + err = agent.addCandidate(context.Background(), cand, dummyConn) + require.NoError(t, err) } - assert.NoError(t, wan.Stop()) - if !closePipe(t, aConn, bConn) { - return + actualCandidates, err := agent.GetLocalCandidates() + require.NoError(t, err) + + for _, candidate := range actualCandidates { + ext, ok := candidate.GetExtension("ufrag") + + require.True(t, ok) + require.Equal(t, agent.localUfrag, ext.Value) } } + +func TestAlwaysSentKeepAlive(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() + + // Avoid deadlocks? + defer test.TimeOut(1 * time.Second).Stop() + + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() + + log := logging.NewDefaultLoggerFactory().NewLogger("agent") + agent.selector = &controllingSelector{agent: agent, log: log} + pair := makeCandidatePair(t) + s, ok := pair.Local.(*CandidateHost) + require.True(t, ok) + s.conn = &fakenet.MockPacketConn{} + agent.setSelectedPair(pair) + + pair.Remote.seen(false) + + lastSent := pair.Local.LastSent() + agent.checkKeepalive() + newLastSent := pair.Local.LastSent() + require.NotEqual(t, lastSent, newLastSent) + lastSent = newLastSent + + // sleep, so there is difference in sent time of local candidate + time.Sleep(10 * time.Millisecond) + agent.checkKeepalive() + newLastSent = pair.Local.LastSent() + require.NotEqual(t, lastSent, newLastSent) +} + +func TestRoleConflict(t *testing.T) { + defer test.CheckRoutines(t)() + defer test.TimeOut(time.Second * 30).Stop() + + runTest := func(doDial bool) { + cfg := &AgentConfig{ + NetworkTypes: supportedNetworkTypes(), + MulticastDNSMode: MulticastDNSModeDisabled, + InterfaceFilter: problematicNetworkInterfaces, + } + + aAgent, err := NewAgent(cfg) + require.NoError(t, err) + + bAgent, err := NewAgent(cfg) + require.NoError(t, err) + + isConnected := make(chan any) + err = aAgent.OnConnectionStateChange(func(c ConnectionState) { + if c == ConnectionStateConnected { + close(isConnected) + } + }) + require.NoError(t, err) + + gatherAndExchangeCandidates(t, aAgent, bAgent) + + go func() { + ufrag, pwd, routineErr := bAgent.GetLocalUserCredentials() + require.NoError(t, routineErr) + + if doDial { + _, routineErr = aAgent.Dial(context.TODO(), ufrag, pwd) + } else { + _, routineErr = aAgent.Accept(context.TODO(), ufrag, pwd) + } + require.NoError(t, routineErr) + }() + + ufrag, pwd, err := aAgent.GetLocalUserCredentials() + require.NoError(t, err) + + if doDial { + _, err = bAgent.Dial(context.TODO(), ufrag, pwd) + } else { + _, err = bAgent.Accept(context.TODO(), ufrag, pwd) + } + require.NoError(t, err) + + <-isConnected + + require.NoError(t, aAgent.Close()) + require.NoError(t, bAgent.Close()) + } + + t.Run("Controlling", func(t *testing.T) { + runTest(true) + }) + + t.Run("Controlled", func(t *testing.T) { + runTest(false) + }) +} diff --git a/agent_udpmux_test.go b/agent_udpmux_test.go index 6050ccd..ec71bee 100644 --- a/agent_udpmux_test.go +++ b/agent_udpmux_test.go @@ -16,13 +16,11 @@ import ( "github.com/stretchr/testify/require" ) -// TestMuxAgent is an end to end test over UDP mux, ensuring two agents could connect over mux +// TestMuxAgent is an end to end test over UDP mux, ensuring two agents could connect over mux. func TestMuxAgent(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() const muxPort = 7686 @@ -49,16 +47,31 @@ func TestMuxAgent(t *testing.T) { NetworkTypes: []NetworkType{ NetworkTypeUDP4, }, + IncludeLoopback: addr.IP.IsLoopback(), }) require.NoError(t, err) + var muxedAClosed bool + defer func() { + if muxedAClosed { + return + } + require.NoError(t, muxedA.Close()) + }() - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ CandidateTypes: []CandidateType{CandidateTypeHost}, NetworkTypes: supportedNetworkTypes(), }) require.NoError(t, err) + var aClosed bool + defer func() { + if aClosed { + return + } + require.NoError(t, agent.Close()) + }() - conn, muxedConn := connect(a, muxedA) + conn, muxedConn := connect(t, agent, muxedA) pair := muxedA.getSelectedPair() require.NotNil(t, pair) @@ -84,7 +97,9 @@ func TestMuxAgent(t *testing.T) { // Close it down require.NoError(t, conn.Close()) + aClosed = true require.NoError(t, muxedConn.Close()) + muxedAClosed = true require.NoError(t, udpMux.Close()) // Expect error when reading from closed mux diff --git a/candidate.go b/candidate.go index 92a0076..89082f9 100644 --- a/candidate.go +++ b/candidate.go @@ -13,13 +13,13 @@ const ( receiveMTU = 8192 defaultLocalPreference = 65535 - // ComponentRTP indicates that the candidate is used for RTP + // ComponentRTP indicates that the candidate is used for RTP. ComponentRTP uint16 = 1 - // ComponentRTCP indicates that the candidate is used for RTCP + // ComponentRTCP indicates that the candidate is used for RTCP. ComponentRTCP ) -// Candidate represents an ICE candidate +// Candidate represents an ICE candidate. type Candidate interface { // An arbitrary string used in the freezing algorithm to // group similar candidates. It is the same for two candidates that @@ -52,15 +52,38 @@ type Candidate interface { // candidate, which is useful for diagnostics and other purposes RelatedAddress() *CandidateRelatedAddress + // Extensions returns a copy of all extension attributes associated with the ICECandidate. + // In the order of insertion, *(key value). + // Extension attributes are defined in RFC 5245, Section 15.1: + // https://datatracker.ietf.org/doc/html/rfc5245#section-15.1 + //. + Extensions() []CandidateExtension + // GetExtension returns the value of the extension attribute associated with the ICECandidate. + // Extension attributes are defined in RFC 5245, Section 15.1: + // https://datatracker.ietf.org/doc/html/rfc5245#section-15.1 + //. + GetExtension(key string) (value CandidateExtension, ok bool) + // AddExtension adds an extension attribute to the ICECandidate. + // If an extension with the same key already exists, it will be overwritten. + // Extension attributes are defined in RFC 5245, Section 15.1: + AddExtension(extension CandidateExtension) error + // RemoveExtension removes an extension attribute from the ICECandidate. + // Extension attributes are defined in RFC 5245, Section 15.1: + RemoveExtension(key string) (ok bool) + String() string Type() CandidateType TCPType() TCPType Equal(other Candidate) bool + // DeepEqual same as Equal, But it also compares the candidate extensions. + DeepEqual(other Candidate) bool + Marshal() string addr() net.Addr + filterForLocationTracking() bool agent() *Agent context() context.Context diff --git a/candidate_base.go b/candidate_base.go index dad95d2..45c090e 100644 --- a/candidate_base.go +++ b/candidate_base.go @@ -12,10 +12,11 @@ import ( "net" "strconv" "strings" + "sync" "sync/atomic" "time" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" ) type candidateBase struct { @@ -43,14 +44,16 @@ type candidateBase struct { priorityOverride uint32 remoteCandidateCaches map[AddrPort]Candidate + isLocationTracked bool + extensions []CandidateExtension } -// Done implements context.Context +// Done implements context.Context. func (c *candidateBase) Done() <-chan struct{} { return c.closeCh } -// Err implements context.Context +// Err implements context.Context. func (c *candidateBase) Err() error { select { case <-c.closedCh: @@ -60,17 +63,17 @@ func (c *candidateBase) Err() error { } } -// Deadline implements context.Context +// Deadline implements context.Context. func (c *candidateBase) Deadline() (deadline time.Time, ok bool) { return time.Time{}, false } -// Value implements context.Context -func (c *candidateBase) Value(interface{}) interface{} { +// Value implements context.Context. +func (c *candidateBase) Value(any) any { return nil } -// ID returns Candidate ID +// ID returns Candidate ID. func (c *candidateBase) ID() string { return c.id } @@ -83,27 +86,27 @@ func (c *candidateBase) Foundation() string { return fmt.Sprintf("%d", crc32.ChecksumIEEE([]byte(c.Type().String()+c.address+c.networkType.String()))) } -// Address returns Candidate Address +// Address returns Candidate Address. func (c *candidateBase) Address() string { return c.address } -// Port returns Candidate Port +// Port returns Candidate Port. func (c *candidateBase) Port() int { return c.port } -// Type returns candidate type +// Type returns candidate type. func (c *candidateBase) Type() CandidateType { return c.candidateType } -// NetworkType returns candidate NetworkType +// NetworkType returns candidate NetworkType. func (c *candidateBase) NetworkType() NetworkType { return c.networkType } -// Component returns candidate component +// Component returns candidate component. func (c *candidateBase) Component() uint16 { return c.component } @@ -112,8 +115,8 @@ func (c *candidateBase) SetComponent(component uint16) { c.component = component } -// LocalPreference returns the local preference for this candidate -func (c *candidateBase) LocalPreference() uint16 { +// LocalPreference returns the local preference for this candidate. +func (c *candidateBase) LocalPreference() uint16 { //nolint:cyclop if c.NetworkType().IsTCP() { // RFC 6544, section 4.2 // @@ -179,6 +182,7 @@ func (c *candidateBase) LocalPreference() uint16 { case CandidateTypeUnspecified: return 0 } + return 0 }() @@ -188,7 +192,7 @@ func (c *candidateBase) LocalPreference() uint16 { return defaultLocalPreference } -// RelatedAddress returns *CandidateRelatedAddress +// RelatedAddress returns *CandidateRelatedAddress. func (c *candidateBase) RelatedAddress() *CandidateRelatedAddress { return c.relatedAddress } @@ -197,10 +201,11 @@ func (c *candidateBase) TCPType() TCPType { return c.tcpType } -// start runs the candidate using the provided connection +// start runs the candidate using the provided connection. func (c *candidateBase) start(a *Agent, conn net.PacketConn, initializedCh <-chan struct{}) { if c.conn != nil { c.agent().log.Warn("Can't start already started candidateBase") + return } c.currAgent = a @@ -211,8 +216,14 @@ func (c *candidateBase) start(a *Agent, conn net.PacketConn, initializedCh <-cha go c.recvLoop(initializedCh) } +var bufferPool = sync.Pool{ // nolint:gochecknoglobals + New: func() any { + return make([]byte, receiveMTU) + }, +} + func (c *candidateBase) recvLoop(initializedCh <-chan struct{}) { - a := c.agent() + agent := c.agent() defer close(c.closedCh) @@ -222,13 +233,20 @@ func (c *candidateBase) recvLoop(initializedCh <-chan struct{}) { return } - buf := make([]byte, receiveMTU) + bufferPoolBuffer := bufferPool.Get() + defer bufferPool.Put(bufferPoolBuffer) + buf, ok := bufferPoolBuffer.([]byte) + if !ok { + return + } + for { n, srcAddr, err := c.conn.ReadFrom(buf) if err != nil { - if !(errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed)) { - a.log.Warnf("Failed to read from candidate %s: %v", c, err) + if !errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed) { + agent.log.Warnf("Failed to read from candidate %s: %v", c, err) } + return } @@ -239,8 +257,10 @@ func (c *candidateBase) recvLoop(initializedCh <-chan struct{}) { func (c *candidateBase) validateSTUNTrafficCache(addr net.Addr) bool { if candidate, ok := c.remoteCandidateCaches[toAddrPort(addr)]; ok { candidate.seen(false) + return true } + return false } @@ -252,48 +272,51 @@ func (c *candidateBase) addRemoteCandidateCache(candidate Candidate, srcAddr net } func (c *candidateBase) handleInboundPacket(buf []byte, srcAddr net.Addr) { - a := c.agent() + agent := c.agent() if stun.IsMessage(buf) { - m := &stun.Message{ + msg := &stun.Message{ Raw: make([]byte, len(buf)), } // Explicitly copy raw buffer so Message can own the memory. - copy(m.Raw, buf) + copy(msg.Raw, buf) + + if err := msg.Decode(); err != nil { + agent.log.Warnf("Failed to handle decode ICE from %s to %s: %v", c.addr(), srcAddr, err) - if err := m.Decode(); err != nil { - a.log.Warnf("Failed to handle decode ICE from %s to %s: %v", c.addr(), srcAddr, err) return } - if err := a.run(c, func(ctx context.Context, a *Agent) { + if err := agent.loop.Run(c, func(_ context.Context) { // nolint: contextcheck - a.handleInbound(m, c, srcAddr) + agent.handleInbound(msg, c, srcAddr) }); err != nil { - a.log.Warnf("Failed to handle message: %v", err) + agent.log.Warnf("Failed to handle message: %v", err) } return } if !c.validateSTUNTrafficCache(srcAddr) { - remoteCandidate, valid := a.validateNonSTUNTraffic(c, srcAddr) //nolint:contextcheck + remoteCandidate, valid := agent.validateNonSTUNTraffic(c, srcAddr) //nolint:contextcheck if !valid { - a.log.Warnf("Discarded message from %s, not a valid remote candidate", c.addr()) + agent.log.Warnf("Discarded message from %s, not a valid remote candidate", c.addr()) + return } c.addRemoteCandidateCache(remoteCandidate, srcAddr) } // Note: This will return packetio.ErrFull if the buffer ever manages to fill up. - if _, err := a.buf.Write(buf); err != nil { - a.log.Warnf("Failed to write packet: %s", err) + if _, err := agent.buf.Write(buf); err != nil { + agent.log.Warnf("Failed to write packet: %s", err) + return } } -// close stops the recvLoop +// close stops the recvLoop. func (c *candidateBase) close() error { // If conn has never been started will be nil if c.Done() == nil { @@ -338,13 +361,15 @@ func (c *candidateBase) writeTo(raw []byte, dst Candidate) (int, error) { return n, err } c.agent().log.Infof("Failed to send packet: %v", err) + return n, nil } c.seen(true) + return n, nil } -// TypePreference returns the type preference for this candidate +// TypePreference returns the type preference for this candidate. func (c *candidateBase) TypePreference() uint16 { pref := c.Type().Preference() if pref == 0 { @@ -382,8 +407,17 @@ func (c *candidateBase) Priority() uint32 { (1<<0)*uint32(256-c.Component()) } -// Equal is used to compare two candidateBases +// Equal is used to compare two candidateBases. func (c *candidateBase) Equal(other Candidate) bool { + if c.addr() != other.addr() { + if c.addr() == nil || other.addr() == nil { + return false + } + if !addrEqual(c.addr(), other.addr()) { + return false + } + } + return c.NetworkType() == other.NetworkType() && c.Type() == other.Type() && c.Address() == other.Address() && @@ -392,17 +426,30 @@ func (c *candidateBase) Equal(other Candidate) bool { c.RelatedAddress().Equal(other.RelatedAddress()) } -// String makes the candidateBase printable +// DeepEqual is same as Equal but also compares the extensions. +func (c *candidateBase) DeepEqual(other Candidate) bool { + return c.Equal(other) && c.extensionsEqual(other.Extensions()) +} + +// String makes the candidateBase printable. func (c *candidateBase) String() string { - return fmt.Sprintf("%s %s %s%s", c.NetworkType(), c.Type(), net.JoinHostPort(c.Address(), strconv.Itoa(c.Port())), c.relatedAddress) + return fmt.Sprintf( + "%s %s %s%s (resolved: %v)", + c.NetworkType(), + c.Type(), + net.JoinHostPort(c.Address(), strconv.Itoa(c.Port())), + c.relatedAddress, + c.resolvedAddr, + ) } // LastReceived returns a time.Time indicating the last time -// this candidate was received +// this candidate was received. func (c *candidateBase) LastReceived() time.Time { if lastReceived, ok := c.lastReceived.Load().(time.Time); ok { return lastReceived } + return time.Time{} } @@ -411,11 +458,12 @@ func (c *candidateBase) setLastReceived(t time.Time) { } // LastSent returns a time.Time indicating the last time -// this candidate was sent +// this candidate was sent. func (c *candidateBase) LastSent() time.Time { if lastSent, ok := c.lastSent.Load().(time.Time); ok { return lastSent } + return time.Time{} } @@ -435,6 +483,10 @@ func (c *candidateBase) addr() net.Addr { return c.resolvedAddr } +func (c *candidateBase) filterForLocationTracking() bool { + return c.isLocationTracked +} + func (c *candidateBase) agent() *Agent { return c.currAgent } @@ -447,7 +499,15 @@ func (c *candidateBase) copy() (Candidate, error) { return UnmarshalCandidate(c.Marshal()) } -// Marshal returns the string representation of the ICECandidate +func removeZoneIDFromAddress(addr string) string { + if i := strings.Index(addr, "%"); i != -1 { + return addr[:i] + } + + return addr +} + +// Marshal returns the string representation of the ICECandidate. func (c *candidateBase) Marshal() string { val := c.Foundation() if val == " " { @@ -459,14 +519,10 @@ func (c *candidateBase) Marshal() string { c.Component(), c.NetworkType().NetworkShort(), c.Priority(), - c.Address(), + removeZoneIDFromAddress(c.Address()), c.Port(), c.Type()) - if c.tcpType != TCPTypeUnspecified { - val += fmt.Sprintf(" tcptype %s", c.tcpType.String()) - } - if r := c.RelatedAddress(); r != nil && r.Address != "" && r.Port != 0 { val = fmt.Sprintf("%s raddr %s rport %d", val, @@ -474,92 +530,530 @@ func (c *candidateBase) Marshal() string { r.Port) } + extensions := c.marshalExtensions() + + if extensions != "" { + val = fmt.Sprintf("%s %s", val, extensions) + } + return val } -// UnmarshalCandidate creates a Candidate from its string representation -func UnmarshalCandidate(raw string) (Candidate, error) { - split := strings.Fields(raw) - // Foundation not specified: not RFC 8445 compliant but seen in the wild - if len(raw) != 0 && raw[0] == ' ' { - split = append([]string{" "}, split...) - } - if len(split) < 8 { - return nil, fmt.Errorf("%w (%d)", errAttributeTooShortICECandidate, len(split)) +// CandidateExtension represents a single candidate extension +// as defined in https://tools.ietf.org/html/rfc5245#section-15.1 +// . +type CandidateExtension struct { + Key string + Value string +} + +func (c *candidateBase) Extensions() []CandidateExtension { + tcpType := c.TCPType() + hasTCPType := 0 + if tcpType != TCPTypeUnspecified { + hasTCPType = 1 } - // Foundation - foundation := split[0] - - // Component - rawComponent, err := strconv.ParseUint(split[1], 10, 16) - if err != nil { - return nil, fmt.Errorf("%w: %v", errParseComponent, err) //nolint:errorlint - } - component := uint16(rawComponent) - - // Protocol - protocol := split[2] - - // Priority - priorityRaw, err := strconv.ParseUint(split[3], 10, 32) - if err != nil { - return nil, fmt.Errorf("%w: %v", errParsePriority, err) //nolint:errorlint - } - priority := uint32(priorityRaw) - - // Address - address := split[4] - - // Port - rawPort, err := strconv.ParseUint(split[5], 10, 16) - if err != nil { - return nil, fmt.Errorf("%w: %v", errParsePort, err) //nolint:errorlint - } - port := int(rawPort) - typ := split[7] - - relatedAddress := "" - relatedPort := 0 - tcpType := TCPTypeUnspecified - - if len(split) > 8 { - split = split[8:] - - if split[0] == "raddr" { - if len(split) < 4 { - return nil, fmt.Errorf("%w: incorrect length", errParseRelatedAddr) - } - - // RelatedAddress - relatedAddress = split[1] - - // RelatedPort - rawRelatedPort, parseErr := strconv.ParseUint(split[3], 10, 16) - if parseErr != nil { - return nil, fmt.Errorf("%w: %v", errParsePort, parseErr) //nolint:errorlint - } - relatedPort = int(rawRelatedPort) - } else if split[0] == "tcptype" { - if len(split) < 2 { - return nil, fmt.Errorf("%w: incorrect length", errParseTCPType) - } - - tcpType = NewTCPType(split[1]) + extensions := make([]CandidateExtension, len(c.extensions)+hasTCPType) + // We store the TCPType in c.tcpType, but we need to return it as an extension. + if hasTCPType == 1 { + extensions[0] = CandidateExtension{ + Key: "tcptype", + Value: tcpType.String(), } } - switch typ { - case "host": - return NewCandidateHost(&CandidateHostConfig{"", protocol, address, port, component, priority, foundation, tcpType}) - case "srflx": - return NewCandidateServerReflexive(&CandidateServerReflexiveConfig{"", protocol, address, port, component, priority, foundation, relatedAddress, relatedPort}) - case "prflx": - return NewCandidatePeerReflexive(&CandidatePeerReflexiveConfig{"", protocol, address, port, component, priority, foundation, relatedAddress, relatedPort}) - case "relay": - return NewCandidateRelay(&CandidateRelayConfig{"", protocol, address, port, component, priority, foundation, relatedAddress, relatedPort, "", nil}) - default: + copy(extensions[hasTCPType:], c.extensions) + + return extensions +} + +// Get returns the value of the given key if it exists. +func (c *candidateBase) GetExtension(key string) (CandidateExtension, bool) { + extension := CandidateExtension{Key: key} + + for i := range c.extensions { + if c.extensions[i].Key == key { + extension.Value = c.extensions[i].Value + + return extension, true + } } - return nil, fmt.Errorf("%w (%s)", ErrUnknownCandidateTyp, typ) + // TCPType was manually set. + if key == "tcptype" && c.TCPType() != TCPTypeUnspecified { //nolint:goconst + extension.Value = c.TCPType().String() + + return extension, true + } + + return extension, false +} + +func (c *candidateBase) AddExtension(ext CandidateExtension) error { + if ext.Key == "tcptype" { + tcpType := NewTCPType(ext.Value) + if tcpType == TCPTypeUnspecified { + return fmt.Errorf("%w: invalid or unsupported TCPtype %s", errParseTCPType, ext.Value) + } + + c.tcpType = tcpType + + return nil + } + + if ext.Key == "" { + return fmt.Errorf("%w: key is empty", errParseExtension) + } + + // per spec, Extensions aren't explicitly unique, we only set the first one. + // If the exteion is set multiple times. + for i := range c.extensions { + if c.extensions[i].Key == ext.Key { + c.extensions[i] = ext + + return nil + } + } + + c.extensions = append(c.extensions, ext) + + return nil +} + +func (c *candidateBase) RemoveExtension(key string) (ok bool) { + if key == "tcptype" { + c.tcpType = TCPTypeUnspecified + ok = true + } + + for i := range c.extensions { + if c.extensions[i].Key == key { + c.extensions = append(c.extensions[:i], c.extensions[i+1:]...) + ok = true + + break + } + } + + return ok +} + +// marshalExtensions returns the string representation of the candidate extensions. +func (c *candidateBase) marshalExtensions() string { + value := "" + exts := c.Extensions() + + for i := range exts { + if value != "" { + value += " " + } + + value += exts[i].Key + " " + exts[i].Value + } + + return value +} + +// Equal returns true if the candidate extensions are equal. +func (c *candidateBase) extensionsEqual(other []CandidateExtension) bool { + freq1 := make(map[CandidateExtension]int) + freq2 := make(map[CandidateExtension]int) + + if len(c.extensions) != len(other) { + return false + } + + if len(c.extensions) == 0 { + return true + } + + if len(c.extensions) == 1 { + return c.extensions[0] == other[0] + } + + for i := range c.extensions { + freq1[c.extensions[i]]++ + freq2[other[i]]++ + } + + for k, v := range freq1 { + if freq2[k] != v { + return false + } + } + + return true +} + +func (c *candidateBase) setExtensions(extensions []CandidateExtension) { + c.extensions = extensions +} + +// UnmarshalCandidate Parses a candidate from a string +// https://datatracker.ietf.org/doc/html/rfc5245#section-15.1 +func UnmarshalCandidate(raw string) (Candidate, error) { //nolint:cyclop + // Handle candidates with the "candidate:" prefix as defined in RFC 5245 section 15.1. + raw = strings.TrimPrefix(raw, "candidate:") + + pos := 0 + // foundation ( 1*32ice-char ) But we allow for empty foundation, + foundation, pos, err := readCandidateCharToken(raw, pos, 32) + if err != nil { + return nil, fmt.Errorf("%w: %v in %s", errParseFoundation, err, raw) //nolint:errorlint // we wrap the error + } + + // Empty foundation, not RFC 8445 compliant but seen in the wild + if foundation == "" { + foundation = " " + } + + if pos >= len(raw) { + return nil, fmt.Errorf("%w: expected component in %s", errAttributeTooShortICECandidate, raw) + } + + // component-id ( 1*5DIGIT ) + component, pos, err := readCandidateDigitToken(raw, pos, 5) + if err != nil { + return nil, fmt.Errorf("%w: %v in %s", errParseComponent, err, raw) //nolint:errorlint // we wrap the error + } + + if pos >= len(raw) { + return nil, fmt.Errorf("%w: expected transport in %s", errAttributeTooShortICECandidate, raw) + } + + // transport ( "UDP" / transport-extension ; from RFC 3261 ) SP + protocol, pos := readCandidateStringToken(raw, pos) + + if pos >= len(raw) { + return nil, fmt.Errorf("%w: expected priority in %s", errAttributeTooShortICECandidate, raw) + } + + // priority ( 1*10DIGIT ) SP + priority, pos, err := readCandidateDigitToken(raw, pos, 10) + if err != nil { + return nil, fmt.Errorf("%w: %v in %s", errParsePriority, err, raw) //nolint:errorlint // we wrap the error + } + + if pos >= len(raw) { + return nil, fmt.Errorf("%w: expected address in %s", errAttributeTooShortICECandidate, raw) + } + + // connection-address SP ;from RFC 4566 + address, pos := readCandidateStringToken(raw, pos) + + // Remove IPv6 ZoneID: https://github.com/pion/ice/pull/704 + address = removeZoneIDFromAddress(address) + + if pos >= len(raw) { + return nil, fmt.Errorf("%w: expected port in %s", errAttributeTooShortICECandidate, raw) + } + + // port from RFC 4566 + port, pos, err := readCandidatePort(raw, pos) + if err != nil { + return nil, fmt.Errorf("%w: %v in %s", errParsePort, err, raw) //nolint:errorlint // we wrap the error + } + + // "typ" SP + typeKey, pos := readCandidateStringToken(raw, pos) + if typeKey != "typ" { + return nil, fmt.Errorf("%w (%s)", ErrUnknownCandidateTyp, typeKey) + } + + if pos >= len(raw) { + return nil, fmt.Errorf("%w: expected candidate type in %s", errAttributeTooShortICECandidate, raw) + } + + // SP cand-type ("host" / "srflx" / "prflx" / "relay") + typ, pos := readCandidateStringToken(raw, pos) + + raddr, rport, pos, err := tryReadRelativeAddrs(raw, pos) + if err != nil { + return nil, err + } + + tcpType := TCPTypeUnspecified + var extensions []CandidateExtension + var tcpTypeRaw string + + if pos < len(raw) { + extensions, tcpTypeRaw, err = unmarshalCandidateExtensions(raw[pos:]) + if err != nil { + return nil, fmt.Errorf("%w: %v", errParseExtension, err) //nolint:errorlint // we wrap the error + } + + if tcpTypeRaw != "" { + tcpType = NewTCPType(tcpTypeRaw) + if tcpType == TCPTypeUnspecified { + return nil, fmt.Errorf("%w: invalid or unsupported TCPtype %s", errParseTCPType, tcpTypeRaw) + } + } + } + + // this code is ugly because we can't break backwards compatibility + // with the old way of parsing candidates + switch typ { + case "host": + candidate, err := NewCandidateHost(&CandidateHostConfig{ + "", + protocol, + address, + port, + uint16(component), //nolint:gosec // G115 no overflow we read 5 digits + uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits + foundation, + tcpType, + false, + }) + if err != nil { + return nil, err + } + + candidate.setExtensions(extensions) + + return candidate, nil + case "srflx": + candidate, err := NewCandidateServerReflexive(&CandidateServerReflexiveConfig{ + "", + protocol, + address, + port, + uint16(component), //nolint:gosec // G115 no overflow we read 5 digits + uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits + foundation, + raddr, + rport, + }) + if err != nil { + return nil, err + } + + candidate.setExtensions(extensions) + + return candidate, nil + case "prflx": + candidate, err := NewCandidatePeerReflexive(&CandidatePeerReflexiveConfig{ + "", + protocol, + address, + port, + uint16(component), //nolint:gosec // G115 no overflow we read 5 digits + uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits + foundation, + raddr, + rport, + }) + if err != nil { + return nil, err + } + + candidate.setExtensions(extensions) + + return candidate, nil + case "relay": + candidate, err := NewCandidateRelay(&CandidateRelayConfig{ + "", + protocol, + address, + port, + uint16(component), //nolint:gosec // G115 no overflow we read 5 digits + uint32(priority), //nolint:gosec // G115 no overflow we read 5 digits + foundation, + raddr, + rport, + "", + nil, + }) + if err != nil { + return nil, err + } + + candidate.setExtensions(extensions) + + return candidate, nil + default: + return nil, fmt.Errorf("%w (%s)", ErrUnknownCandidateTyp, typ) + } +} + +// Read an ice-char token from the raw string +// ice-char = ALPHA / DIGIT / "+" / "/" +// stop reading when a space is encountered or the end of the string. +func readCandidateCharToken(raw string, start int, limit int) (string, int, error) { //nolint:cyclop + for i, char := range raw[start:] { + if char == 0x20 { // SP + return raw[start : start+i], start + i + 1, nil + } + + if i == limit { + //nolint: err113 // handled by caller + return "", 0, fmt.Errorf("token too long: %s expected 1x%d", raw[start:start+i], limit) + } + + if (char < 'A' || char > 'Z') && + (char < 'a' || char > 'z') && + (char < '0' || char > '9') && + char != '+' && char != '/' { + return "", 0, fmt.Errorf("invalid ice-char token: %c", char) //nolint: err113 // handled by caller + } + } + + return raw[start:], len(raw), nil +} + +// Read an ice string token from the raw string until a space is encountered +// Or the end of the string, we imply that ice string are UTF-8 encoded. +func readCandidateStringToken(raw string, start int) (string, int) { + for i, char := range raw[start:] { + if char == 0x20 { // SP + return raw[start : start+i], start + i + 1 + } + } + + return raw[start:], len(raw) +} + +// Read a digit token from the raw string +// stop reading when a space is encountered or the end of the string. +func readCandidateDigitToken(raw string, start, limit int) (int, int, error) { + var val int + for i, char := range raw[start:] { + if char == 0x20 { // SP + return val, start + i + 1, nil + } + + if i == limit { + //nolint: err113 // handled by caller + return 0, 0, fmt.Errorf("token too long: %s expected 1x%d", raw[start:start+i], limit) + } + + if char < '0' || char > '9' { + return 0, 0, fmt.Errorf("invalid digit token: %c", char) //nolint: err113 // handled by caller + } + + val = val*10 + int(char-'0') + } + + return val, len(raw), nil +} + +// Read and validate RFC 4566 port from the raw string. +func readCandidatePort(raw string, start int) (int, int, error) { + port, pos, err := readCandidateDigitToken(raw, start, 5) + if err != nil { + return 0, 0, err + } + + if port > 65535 { + return 0, 0, fmt.Errorf("invalid RFC 4566 port %d", port) //nolint: err113 // handled by caller + } + + return port, pos, nil +} + +// Read a byte-string token from the raw string +// As defined in RFC 4566 1*(%x01-09/%x0B-0C/%x0E-FF) ;any byte except NUL, CR, or LF +// we imply that extensions byte-string are UTF-8 encoded. +func readCandidateByteString(raw string, start int) (string, int, error) { + for i, char := range raw[start:] { + if char == 0x20 { // SP + return raw[start : start+i], start + i + 1, nil + } + + // 1*(%x01-09/%x0B-0C/%x0E-FF) + if (char < 0x01 || char > 0x09) && + (char < 0x0B || char > 0x0C) && + (char < 0x0E || char > 0xFF) { + return "", 0, fmt.Errorf("invalid byte-string character: %c", char) //nolint: err113 // handled by caller + } + } + + return raw[start:], len(raw), nil +} + +// Read and validate raddr and rport from the raw string +// [SP rel-addr] [SP rel-port] +// defined in https://datatracker.ietf.org/doc/html/rfc5245#section-15.1 +// . +func tryReadRelativeAddrs(raw string, start int) (raddr string, rport, pos int, err error) { + key, pos := readCandidateStringToken(raw, start) + + if key != "raddr" { + return "", 0, start, nil + } + + if pos >= len(raw) { + return "", 0, 0, fmt.Errorf("%w: expected raddr value in %s", errParseRelatedAddr, raw) + } + + raddr, pos = readCandidateStringToken(raw, pos) + + if pos >= len(raw) { + return "", 0, 0, fmt.Errorf("%w: expected rport in %s", errParseRelatedAddr, raw) + } + + key, pos = readCandidateStringToken(raw, pos) + if key != "rport" { + return "", 0, 0, fmt.Errorf("%w: expected rport in %s", errParseRelatedAddr, raw) + } + + if pos >= len(raw) { + return "", 0, 0, fmt.Errorf("%w: expected rport value in %s", errParseRelatedAddr, raw) + } + + rport, pos, err = readCandidatePort(raw, pos) + if err != nil { + return "", 0, 0, fmt.Errorf("%w: %v", errParseRelatedAddr, err) //nolint:errorlint // we wrap the error + } + + return raddr, rport, pos, nil +} + +// UnmarshalCandidateExtensions parses the candidate extensions from the raw string. +// *(SP extension-att-name SP extension-att-value) +// Where extension-att-name, and extension-att-value are byte-strings +// as defined in https://tools.ietf.org/html/rfc5245#section-15.1 +func unmarshalCandidateExtensions(raw string) (extensions []CandidateExtension, rawTCPTypeRaw string, err error) { + extensions = make([]CandidateExtension, 0) + + if raw == "" { + return extensions, "", nil + } + + if raw[0] == 0x20 { // SP + return extensions, "", fmt.Errorf("%w: unexpected space %s", errParseExtension, raw) + } + + for i := 0; i < len(raw); { + key, next, err := readCandidateByteString(raw, i) + if err != nil { + return extensions, "", fmt.Errorf( + "%w: failed to read key %v", errParseExtension, err, //nolint: errorlint // we wrap the error + ) + } + i = next + + // while not spec-compliant, we allow for empty values, as seen in the wild + var value string + if i < len(raw) { + value, next, err = readCandidateByteString(raw, i) + if err != nil { + return extensions, "", fmt.Errorf( + "%w: failed to read value %v", errParseExtension, err, //nolint: errorlint // we are wrapping the error + ) + } + i = next + } + + if key == "tcptype" { + rawTCPTypeRaw = value + + continue + } + + extensions = append(extensions, CandidateExtension{key, value}) + } + + return extensions, rawTCPTypeRaw, nil } diff --git a/candidate_host.go b/candidate_host.go index 5d207dd..ac33ca3 100644 --- a/candidate_host.go +++ b/candidate_host.go @@ -4,30 +4,31 @@ package ice import ( - "net" + "net/netip" "strings" ) -// CandidateHost is a candidate of type host +// CandidateHost is a candidate of type host. type CandidateHost struct { candidateBase network string } -// CandidateHostConfig is the config required to create a new CandidateHost +// CandidateHostConfig is the config required to create a new CandidateHost. type CandidateHostConfig struct { - CandidateID string - Network string - Address string - Port int - Component uint16 - Priority uint32 - Foundation string - TCPType TCPType + CandidateID string + Network string + Address string + Port int + Component uint16 + Priority uint32 + Foundation string + TCPType TCPType + IsLocationTracked bool } -// NewCandidateHost creates a new host candidate +// NewCandidateHost creates a new host candidate. func NewCandidateHost(config *CandidateHostConfig) (*CandidateHost, error) { candidateID := config.CandidateID @@ -35,7 +36,7 @@ func NewCandidateHost(config *CandidateHostConfig) (*CandidateHost, error) { candidateID = globalCandidateIDGenerator.Generate() } - c := &CandidateHost{ + candidateHost := &CandidateHost{ candidateBase: candidateBase{ id: candidateID, address: config.Address, @@ -46,35 +47,36 @@ func NewCandidateHost(config *CandidateHostConfig) (*CandidateHost, error) { foundationOverride: config.Foundation, priorityOverride: config.Priority, remoteCandidateCaches: map[AddrPort]Candidate{}, + isLocationTracked: config.IsLocationTracked, }, network: config.Network, } if !strings.HasSuffix(config.Address, ".local") { - ip := net.ParseIP(config.Address) - if ip == nil { - return nil, ErrAddressParseFailed + ipAddr, err := netip.ParseAddr(config.Address) + if err != nil { + return nil, err } - if err := c.setIP(ip); err != nil { + if err := candidateHost.setIPAddr(ipAddr); err != nil { return nil, err } } else { // Until mDNS candidate is resolved assume it is UDPv4 - c.candidateBase.networkType = NetworkTypeUDP4 + candidateHost.candidateBase.networkType = NetworkTypeUDP4 } - return c, nil + return candidateHost, nil } -func (c *CandidateHost) setIP(ip net.IP) error { - networkType, err := determineNetworkType(c.network, ip) +func (c *CandidateHost) setIPAddr(addr netip.Addr) error { + networkType, err := determineNetworkType(c.network, addr) if err != nil { return err } c.candidateBase.networkType = networkType - c.candidateBase.resolvedAddr = createAddr(networkType, ip, c.port) + c.candidateBase.resolvedAddr = createAddr(networkType, addr, c.port) return nil } diff --git a/candidate_peer_reflexive.go b/candidate_peer_reflexive.go index bbcfe33..9bf435c 100644 --- a/candidate_peer_reflexive.go +++ b/candidate_peer_reflexive.go @@ -6,14 +6,16 @@ //nolint:dupl package ice -import "net" +import ( + "net/netip" +) // CandidatePeerReflexive ... type CandidatePeerReflexive struct { candidateBase } -// CandidatePeerReflexiveConfig is the config required to create a new CandidatePeerReflexive +// CandidatePeerReflexiveConfig is the config required to create a new CandidatePeerReflexive. type CandidatePeerReflexiveConfig struct { CandidateID string Network string @@ -26,14 +28,14 @@ type CandidatePeerReflexiveConfig struct { RelPort int } -// NewCandidatePeerReflexive creates a new peer reflective candidate +// NewCandidatePeerReflexive creates a new peer reflective candidate. func NewCandidatePeerReflexive(config *CandidatePeerReflexiveConfig) (*CandidatePeerReflexive, error) { - ip := net.ParseIP(config.Address) - if ip == nil { - return nil, ErrAddressParseFailed + ipAddr, err := netip.ParseAddr(config.Address) + if err != nil { + return nil, err } - networkType, err := determineNetworkType(config.Network, ip) + networkType, err := determineNetworkType(config.Network, ipAddr) if err != nil { return nil, err } @@ -50,7 +52,7 @@ func NewCandidatePeerReflexive(config *CandidatePeerReflexiveConfig) (*Candidate candidateType: CandidateTypePeerReflexive, address: config.Address, port: config.Port, - resolvedAddr: createAddr(networkType, ip, config.Port), + resolvedAddr: createAddr(networkType, ipAddr, config.Port), component: config.Component, foundationOverride: config.Foundation, priorityOverride: config.Priority, diff --git a/candidate_relay.go b/candidate_relay.go index 449d077..9e88b1a 100644 --- a/candidate_relay.go +++ b/candidate_relay.go @@ -5,6 +5,7 @@ package ice import ( "net" + "net/netip" ) // CandidateRelay ... @@ -15,7 +16,7 @@ type CandidateRelay struct { onClose func() error } -// CandidateRelayConfig is the config required to create a new CandidateRelay +// CandidateRelayConfig is the config required to create a new CandidateRelay. type CandidateRelayConfig struct { CandidateID string Network string @@ -30,7 +31,7 @@ type CandidateRelayConfig struct { OnClose func() error } -// NewCandidateRelay creates a new relay candidate +// NewCandidateRelay creates a new relay candidate. func NewCandidateRelay(config *CandidateRelayConfig) (*CandidateRelay, error) { candidateID := config.CandidateID @@ -38,24 +39,28 @@ func NewCandidateRelay(config *CandidateRelayConfig) (*CandidateRelay, error) { candidateID = globalCandidateIDGenerator.Generate() } - ip := net.ParseIP(config.Address) - if ip == nil { - return nil, ErrAddressParseFailed + ipAddr, err := netip.ParseAddr(config.Address) + if err != nil { + return nil, err } - networkType, err := determineNetworkType(config.Network, ip) + networkType, err := determineNetworkType(config.Network, ipAddr) if err != nil { return nil, err } return &CandidateRelay{ candidateBase: candidateBase{ - id: candidateID, - networkType: networkType, - candidateType: CandidateTypeRelay, - address: config.Address, - port: config.Port, - resolvedAddr: &net.UDPAddr{IP: ip, Port: config.Port}, + id: candidateID, + networkType: networkType, + candidateType: CandidateTypeRelay, + address: config.Address, + port: config.Port, + resolvedAddr: &net.UDPAddr{ + IP: ipAddr.AsSlice(), + Port: config.Port, + Zone: ipAddr.Zone(), + }, component: config.Component, foundationOverride: config.Foundation, priorityOverride: config.Priority, @@ -70,6 +75,23 @@ func NewCandidateRelay(config *CandidateRelayConfig) (*CandidateRelay, error) { }, nil } +// LocalPreference returns the local preference for this candidate. +func (c *CandidateRelay) LocalPreference() uint16 { + // These preference values come from libwebrtc + // https://github.com/mozilla/libwebrtc/blob/1389c76d9c79839a2ca069df1db48aa3f2e6a1ac/p2p/base/turn_port.cc#L61 + var relayPreference uint16 + switch c.relayProtocol { + case relayProtocolTLS, relayProtocolDTLS: + relayPreference = 2 + case tcp: + relayPreference = 1 + default: + relayPreference = 0 + } + + return c.candidateBase.LocalPreference() + relayPreference +} + // RelayProtocol returns the protocol used between the endpoint and the relay server. func (c *CandidateRelay) RelayProtocol() string { return c.relayProtocol @@ -81,6 +103,7 @@ func (c *CandidateRelay) close() error { err = c.onClose() c.onClose = nil } + return err } diff --git a/candidate_relay_test.go b/candidate_relay_test.go index b022671..1a04506 100644 --- a/candidate_relay_test.go +++ b/candidate_relay_test.go @@ -12,10 +12,10 @@ import ( "testing" "time" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" - "github.com/pion/turn/v3" - "github.com/stretchr/testify/assert" + "github.com/pion/turn/v4" + "github.com/stretchr/testify/require" ) func optimisticAuthHandler(string, string, net.Addr) (key []byte, ok bool) { @@ -24,15 +24,13 @@ func optimisticAuthHandler(string, string, net.Addr) (key []byte, ok bool) { func TestRelayOnlyConnection(t *testing.T) { // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() serverPort := randomPort(t) - serverListener, err := net.ListenPacket("udp", "127.0.0.1:"+strconv.Itoa(serverPort)) - assert.NoError(t, err) + serverListener, err := net.ListenPacket("udp", localhostIPStr+":"+strconv.Itoa(serverPort)) + require.NoError(t, err) server, err := turn.NewServer(turn.ServerConfig{ Realm: "pion.ly", @@ -40,18 +38,21 @@ func TestRelayOnlyConnection(t *testing.T) { PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: serverListener, - RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"}, + RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: localhostIPStr + ""}, }, }, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, server.Close()) + }() cfg := &AgentConfig{ NetworkTypes: supportedNetworkTypes(), Urls: []*stun.URI{ { Scheme: stun.SchemeTypeTURN, - Host: "127.0.0.1", + Host: localhostIPStr + "", Username: "username", Password: "password", Port: serverPort, @@ -62,30 +63,24 @@ func TestRelayOnlyConnection(t *testing.T) { } aAgent, err := NewAgent(cfg) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() aNotifier, aConnected := onConnected() - if err = aAgent.OnConnectionStateChange(aNotifier); err != nil { - t.Fatal(err) - } + require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) bAgent, err := NewAgent(cfg) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() bNotifier, bConnected := onConnected() - if err = bAgent.OnConnectionStateChange(bNotifier); err != nil { - t.Fatal(err) - } + require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) - connect(aAgent, bAgent) + connect(t, aAgent, bAgent) <-aConnected <-bConnected - - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) - assert.NoError(t, server.Close()) } diff --git a/candidate_server_reflexive.go b/candidate_server_reflexive.go index 3a8ac0f..3612edb 100644 --- a/candidate_server_reflexive.go +++ b/candidate_server_reflexive.go @@ -3,14 +3,17 @@ package ice -import "net" +import ( + "net" + "net/netip" +) // CandidateServerReflexive ... type CandidateServerReflexive struct { candidateBase } -// CandidateServerReflexiveConfig is the config required to create a new CandidateServerReflexive +// CandidateServerReflexiveConfig is the config required to create a new CandidateServerReflexive. type CandidateServerReflexiveConfig struct { CandidateID string Network string @@ -23,14 +26,14 @@ type CandidateServerReflexiveConfig struct { RelPort int } -// NewCandidateServerReflexive creates a new server reflective candidate +// NewCandidateServerReflexive creates a new server reflective candidate. func NewCandidateServerReflexive(config *CandidateServerReflexiveConfig) (*CandidateServerReflexive, error) { - ip := net.ParseIP(config.Address) - if ip == nil { - return nil, ErrAddressParseFailed + ipAddr, err := netip.ParseAddr(config.Address) + if err != nil { + return nil, err } - networkType, err := determineNetworkType(config.Network, ip) + networkType, err := determineNetworkType(config.Network, ipAddr) if err != nil { return nil, err } @@ -42,12 +45,16 @@ func NewCandidateServerReflexive(config *CandidateServerReflexiveConfig) (*Candi return &CandidateServerReflexive{ candidateBase: candidateBase{ - id: candidateID, - networkType: networkType, - candidateType: CandidateTypeServerReflexive, - address: config.Address, - port: config.Port, - resolvedAddr: &net.UDPAddr{IP: ip, Port: config.Port}, + id: candidateID, + networkType: networkType, + candidateType: CandidateTypeServerReflexive, + address: config.Address, + port: config.Port, + resolvedAddr: &net.UDPAddr{ + IP: ipAddr.AsSlice(), + Port: config.Port, + Zone: ipAddr.Zone(), + }, component: config.Component, foundationOverride: config.Foundation, priorityOverride: config.Priority, diff --git a/candidate_server_reflexive_test.go b/candidate_server_reflexive_test.go index 236ab03..c3e91aa 100644 --- a/candidate_server_reflexive_test.go +++ b/candidate_server_reflexive_test.go @@ -12,23 +12,21 @@ import ( "testing" "time" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" - "github.com/pion/turn/v3" - "github.com/stretchr/testify/assert" + "github.com/pion/turn/v4" + "github.com/stretchr/testify/require" ) func TestServerReflexiveOnlyConnection(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() serverPort := randomPort(t) serverListener, err := net.ListenPacket("udp4", "127.0.0.1:"+strconv.Itoa(serverPort)) - assert.NoError(t, err) + require.NoError(t, err) server, err := turn.NewServer(turn.ServerConfig{ Realm: "pion.ly", @@ -40,7 +38,10 @@ func TestServerReflexiveOnlyConnection(t *testing.T) { }, }, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, server.Close()) + }() cfg := &AgentConfig{ NetworkTypes: []NetworkType{NetworkTypeUDP4}, @@ -55,30 +56,24 @@ func TestServerReflexiveOnlyConnection(t *testing.T) { } aAgent, err := NewAgent(cfg) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() aNotifier, aConnected := onConnected() - if err = aAgent.OnConnectionStateChange(aNotifier); err != nil { - t.Fatal(err) - } + require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) bAgent, err := NewAgent(cfg) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() bNotifier, bConnected := onConnected() - if err = bAgent.OnConnectionStateChange(bNotifier); err != nil { - t.Fatal(err) - } + require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) - connect(aAgent, bAgent) + connect(t, aAgent, bAgent) <-aConnected <-bConnected - - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) - assert.NoError(t, server.Close()) } diff --git a/candidate_test.go b/candidate_test.go index 9fb7ebc..a89632f 100644 --- a/candidate_test.go +++ b/candidate_test.go @@ -5,16 +5,19 @@ package ice import ( "net" + "strconv" + "strings" "testing" "time" "github.com/pion/logging" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +const localhostIPStr = "127.0.0.1" + func TestCandidateTypePreference(t *testing.T) { - r := require.New(t) + req := require.New(t) hostDefaultPreference := uint16(126) prflxDefaultPreference := uint16(110) @@ -51,16 +54,16 @@ func TestCandidateTypePreference(t *testing.T) { } if networkType.IsTCP() { - r.Equal(hostDefaultPreference-tcpOffset, hostCandidate.TypePreference()) - r.Equal(prflxDefaultPreference-tcpOffset, prflxCandidate.TypePreference()) - r.Equal(srflxDefaultPreference-tcpOffset, srflxCandidate.TypePreference()) + req.Equal(hostDefaultPreference-tcpOffset, hostCandidate.TypePreference()) + req.Equal(prflxDefaultPreference-tcpOffset, prflxCandidate.TypePreference()) + req.Equal(srflxDefaultPreference-tcpOffset, srflxCandidate.TypePreference()) } else { - r.Equal(hostDefaultPreference, hostCandidate.TypePreference()) - r.Equal(prflxDefaultPreference, prflxCandidate.TypePreference()) - r.Equal(srflxDefaultPreference, srflxCandidate.TypePreference()) + req.Equal(hostDefaultPreference, hostCandidate.TypePreference()) + req.Equal(prflxDefaultPreference, prflxCandidate.TypePreference()) + req.Equal(srflxDefaultPreference, srflxCandidate.TypePreference()) } - r.Equal(relayDefaultPreference, relayCandidate.TypePreference()) + req.Equal(relayDefaultPreference, relayCandidate.TypePreference()) } } } @@ -173,31 +176,29 @@ func TestCandidatePriority(t *testing.T) { WantPriority: 16777215, }, } { - if got, want := test.Candidate.Priority(), test.WantPriority; got != want { - t.Fatalf("Candidate(%v).Priority() = %d, want %d", test.Candidate, got, want) - } + require.Equal(t, test.Candidate.Priority(), test.WantPriority) } } func TestCandidateLastSent(t *testing.T) { candidate := candidateBase{} - assert.Equal(t, candidate.LastSent(), time.Time{}) + require.Equal(t, candidate.LastSent(), time.Time{}) now := time.Now() candidate.setLastSent(now) - assert.Equal(t, candidate.LastSent(), now) + require.Equal(t, candidate.LastSent(), now) } func TestCandidateLastReceived(t *testing.T) { candidate := candidateBase{} - assert.Equal(t, candidate.LastReceived(), time.Time{}) + require.Equal(t, candidate.LastReceived(), time.Time{}) now := time.Now() candidate.setLastReceived(now) - assert.Equal(t, candidate.LastReceived(), now) + require.Equal(t, candidate.LastReceived(), now) } func TestCandidateFoundation(t *testing.T) { // All fields are the same - assert.Equal(t, + require.Equal(t, (&candidateBase{ candidateType: CandidateTypeHost, networkType: NetworkTypeUDP4, @@ -210,7 +211,7 @@ func TestCandidateFoundation(t *testing.T) { }).Foundation()) // Different Address - assert.NotEqual(t, + require.NotEqual(t, (&candidateBase{ candidateType: CandidateTypeHost, networkType: NetworkTypeUDP4, @@ -223,7 +224,7 @@ func TestCandidateFoundation(t *testing.T) { }).Foundation()) // Different networkType - assert.NotEqual(t, + require.NotEqual(t, (&candidateBase{ candidateType: CandidateTypeHost, networkType: NetworkTypeUDP4, @@ -236,7 +237,7 @@ func TestCandidateFoundation(t *testing.T) { }).Foundation()) // Different candidateType - assert.NotEqual(t, + require.NotEqual(t, (&candidateBase{ candidateType: CandidateTypeHost, networkType: NetworkTypeUDP4, @@ -249,7 +250,7 @@ func TestCandidateFoundation(t *testing.T) { }).Foundation()) // Port has no effect - assert.Equal(t, + require.Equal(t, (&candidateBase{ candidateType: CandidateTypeHost, networkType: NetworkTypeUDP4, @@ -264,108 +265,234 @@ func TestCandidateFoundation(t *testing.T) { }).Foundation()) } +func mustCandidateHost(t *testing.T, conf *CandidateHostConfig) Candidate { + t.Helper() + + cand, err := NewCandidateHost(conf) + require.NoError(t, err) + + return cand +} + +func mustCandidateHostWithExtensions( + t *testing.T, + conf *CandidateHostConfig, + extensions []CandidateExtension, +) Candidate { + t.Helper() + + cand, err := NewCandidateHost(conf) + require.NoError(t, err) + + cand.setExtensions(extensions) + + return cand +} + +func mustCandidateRelay(t *testing.T, conf *CandidateRelayConfig) Candidate { + t.Helper() + + cand, err := NewCandidateRelay(conf) + require.NoError(t, err) + + return cand +} + +func mustCandidateRelayWithExtensions( + t *testing.T, + conf *CandidateRelayConfig, + extensions []CandidateExtension, +) Candidate { + t.Helper() + + cand, err := NewCandidateRelay(conf) + require.NoError(t, err) + + cand.setExtensions(extensions) + + return cand +} + +func mustCandidateServerReflexive(t *testing.T, conf *CandidateServerReflexiveConfig) Candidate { + t.Helper() + + cand, err := NewCandidateServerReflexive(conf) + require.NoError(t, err) + + return cand +} + +func mustCandidateServerReflexiveWithExtensions( + t *testing.T, + conf *CandidateServerReflexiveConfig, + extensions []CandidateExtension, +) Candidate { + t.Helper() + + cand, err := NewCandidateServerReflexive(conf) + require.NoError(t, err) + + cand.setExtensions(extensions) + + return cand +} + +func mustCandidatePeerReflexiveWithExtensions( + t *testing.T, + conf *CandidatePeerReflexiveConfig, + extensions []CandidateExtension, +) Candidate { + t.Helper() + + cand, err := NewCandidatePeerReflexive(conf) + require.NoError(t, err) + + cand.setExtensions(extensions) + + return cand +} + func TestCandidateMarshal(t *testing.T) { - for _, test := range []struct { + for idx, test := range []struct { candidate Candidate marshaled string expectError bool }{ { - &CandidateHost{ - candidateBase{ - networkType: NetworkTypeUDP6, - candidateType: CandidateTypeHost, - address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", - port: 53987, - priorityOverride: 500, - foundationOverride: "750", - }, - "", - }, + mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeUDP6.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }), "750 1 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a 53987 typ host", false, }, { - &CandidateHost{ - candidateBase{ - networkType: NetworkTypeUDP4, - candidateType: CandidateTypeHost, - address: "10.0.75.1", - port: 53634, - }, - "", - }, + mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "10.0.75.1", + Port: 53634, + }), "4273957277 1 udp 2130706431 10.0.75.1 53634 typ host", false, }, { - &CandidateServerReflexive{ - candidateBase{ - networkType: NetworkTypeUDP4, - candidateType: CandidateTypeServerReflexive, - address: "191.228.238.68", - port: 53991, - relatedAddress: &CandidateRelatedAddress{"192.168.0.274", 53991}, - }, - }, + mustCandidateServerReflexive(t, &CandidateServerReflexiveConfig{ + Network: NetworkTypeUDP4.String(), + Address: "191.228.238.68", + Port: 53991, + RelAddr: "192.168.0.274", + RelPort: 53991, + }), "647372371 1 udp 1694498815 191.228.238.68 53991 typ srflx raddr 192.168.0.274 rport 53991", false, }, { - &CandidateRelay{ - candidateBase{ - networkType: NetworkTypeUDP4, - candidateType: CandidateTypeRelay, - address: "50.0.0.1", - port: 5000, - relatedAddress: &CandidateRelatedAddress{"192.168.0.1", 5001}, + mustCandidatePeerReflexiveWithExtensions( + t, + &CandidatePeerReflexiveConfig{ + Network: NetworkTypeTCP4.String(), + Address: "192.0.2.15", + Port: 50000, + RelAddr: "10.0.0.1", + RelPort: 12345, }, - "", - nil, - }, + []CandidateExtension{ + {"generation", "0"}, + {"network-id", "2"}, + {"network-cost", "10"}, + }, + ), + //nolint: lll + "4207374052 1 tcp 1685790463 192.0.2.15 50000 typ prflx raddr 10.0.0.1 rport 12345 generation 0 network-id 2 network-cost 10", + false, + }, + { + mustCandidateRelay(t, &CandidateRelayConfig{ + Network: NetworkTypeUDP4.String(), + Address: "50.0.0.1", + Port: 5000, + RelAddr: "192.168.0.1", + RelPort: 5001, + }), "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 5001", false, }, { - &CandidateHost{ - candidateBase{ - networkType: NetworkTypeTCP4, - candidateType: CandidateTypeHost, - address: "192.168.0.196", - port: 0, - tcpType: TCPTypeActive, - }, - "", - }, + mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeTCP4.String(), + Address: "192.168.0.196", + Port: 0, + TCPType: TCPTypeActive, + }), "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active", false, }, { - &CandidateHost{ - candidateBase{ - networkType: NetworkTypeUDP4, - candidateType: CandidateTypeHost, - address: "e2494022-4d9a-4c1e-a750-cc48d4f8d6ee.local", - port: 60542, - }, - "", - }, + mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "e2494022-4d9a-4c1e-a750-cc48d4f8d6ee.local", + Port: 60542, + }), "1380287402 1 udp 2130706431 e2494022-4d9a-4c1e-a750-cc48d4f8d6ee.local 60542 typ host", false, }, // Missing Foundation { - &CandidateHost{ - candidateBase{ - networkType: NetworkTypeUDP4, - candidateType: CandidateTypeHost, - address: "127.0.0.1", - port: 80, - priorityOverride: 500, - foundationOverride: " ", - }, - "", - }, - " 1 udp 500 127.0.0.1 80 typ host", + mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: localhostIPStr, + Port: 80, + Priority: 500, + Foundation: " ", + }), + " 1 udp 500 " + localhostIPStr + " 80 typ host", + false, + }, + // Missing Foundation + { + mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: localhostIPStr, + Port: 80, + Priority: 500, + Foundation: " ", + }), + "candidate: 1 udp 500 " + localhostIPStr + " 80 typ host", + false, + }, + { + mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: localhostIPStr, + Port: 80, + Priority: 500, + Foundation: "+/3713fhi", + }), + "+/3713fhi 1 udp 500 " + localhostIPStr + " 80 typ host", + false, + }, + { + mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeTCP4.String(), + Address: "172.28.142.173", + Port: 7686, + Priority: 1671430143, + Foundation: "+/3713fhi", + }), + "3359356140 1 tcp 1671430143 172.28.142.173 7686 typ host", + false, + }, + { + mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeTCP4.String(), + Address: "172.28.142.173", + Port: 7686, + Priority: 1671430143, + Foundation: "+/3713fhi", + }), + "candidate:3359356140 1 tcp 1671430143 172.28.142.173 7686 typ host", false, }, @@ -374,25 +501,82 @@ func TestCandidateMarshal(t *testing.T) { {nil, "1938809241", true}, {nil, "1986380506 99999999 udp 2122063615 10.0.75.1 53634 typ host generation 0 network-id 2", true}, {nil, "1986380506 1 udp 99999999999 10.0.75.1 53634 typ host", true}, + //nolint: lll {nil, "4207374051 1 udp 1685790463 191.228.238.68 99999999 typ srflx raddr 192.168.0.278 rport 53991 generation 0 network-id 3", true}, {nil, "4207374051 1 udp 1685790463 191.228.238.68 53991 typ srflx raddr", true}, + //nolint: lll {nil, "4207374051 1 udp 1685790463 191.228.238.68 53991 typ srflx raddr 192.168.0.278 rport 99999999 generation 0 network-id 3", true}, {nil, "4207374051 INVALID udp 2130706431 10.0.75.1 53634 typ host", true}, {nil, "4207374051 1 udp INVALID 10.0.75.1 53634 typ host", true}, {nil, "4207374051 INVALID udp 2130706431 10.0.75.1 INVALID typ host", true}, {nil, "4207374051 1 udp 2130706431 10.0.75.1 53634 typ INVALID", true}, {nil, "4207374051 1 INVALID 2130706431 10.0.75.1 53634 typ host", true}, + {nil, "4207374051 1 INVALID 2130706431 10.0.75.1 53634 typ", true}, + {nil, "4207374051 1 INVALID 2130706431 10.0.75.1 53634", true}, + {nil, "848194626 1 udp 16777215 50.0.0.^^1 5000 typ relay raddr 192.168.0.1 rport 5001", true}, + {nil, "4207374052 1 tcp 1685790463 192.0#.2.15 50000 typ prflx raddr 10.0.0.1 rport 12345 rport 5001", true}, + {nil, "647372371 1 udp 1694498815 191.228.2@338.68 53991 typ srflx raddr 192.168.0.274 rport 53991", true}, + // invalid foundion; longer than 32 characters + {nil, "111111111111111111111111111111111 1 udp 500 " + localhostIPStr + " 80 typ host", true}, + // Invalid ice-char + {nil, "3$3 1 udp 500 " + localhostIPStr + " 80 typ host", true}, + // invalid component; longer than 5 digits + {nil, "4207374051 123456 udp 500 " + localhostIPStr + " 0 typ host", true}, + // invalid priority; longer than 10 digits + {nil, "4207374051 99999 udp 12345678910 " + localhostIPStr + " 99999 typ host", true}, + // invalid port; + {nil, "4207374051 99999 udp 500 " + localhostIPStr + " 65536 typ host", true}, + {nil, "4207374051 99999 udp 500 " + localhostIPStr + " 999999 typ host", true}, + {nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 999999", true}, + + // bad byte-string in extension value + {nil, "750 1 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a 53987 typ host ext valu\nu", true}, + {nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 654 ext valu\nu", true}, + {nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 654 ext valu\000e", true}, + + // bad byte-string in extension key + {nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 654 ext\r value", true}, + + // invalid tcptype + {nil, "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype INVALID", true}, + + // expect rport after raddr + {nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 extension 322", true}, + {nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport", true}, + {nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1", true}, + {nil, "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr", true}, + {nil, "4207374051 99999 udp 500 " + localhostIPStr + " 80 typ", true}, + {nil, "4207374051 99999 udp 500 " + localhostIPStr + " 80", true}, + {nil, "4207374051 99999 udp 500 " + localhostIPStr, true}, + {nil, "4207374051 99999 udp 500 ", true}, + {nil, "4207374051 99999 udp", true}, + {nil, "4207374051 99999", true}, + {nil, "4207374051", true}, } { - actualCandidate, err := UnmarshalCandidate(test.marshaled) - if test.expectError { - assert.Error(t, err) - continue - } + t.Run(strconv.Itoa(idx), func(t *testing.T) { + actualCandidate, err := UnmarshalCandidate(test.marshaled) + if test.expectError { + require.Error(t, err, "expected error", test.marshaled) - assert.NoError(t, err) + return + } - assert.True(t, test.candidate.Equal(actualCandidate)) - assert.Equal(t, test.marshaled, actualCandidate.Marshal()) + require.NoError(t, err) + + require.Truef( + t, + test.candidate.Equal(actualCandidate), + "%s != %s", + test.candidate.String(), + actualCandidate.String(), + ) + + if strings.HasPrefix(test.marshaled, "candidate:") { + require.Equal(t, test.marshaled[len("candidate:"):], actualCandidate.Marshal()) + } else { + require.Equal(t, test.marshaled, actualCandidate.Marshal()) + } + }) } } @@ -403,7 +587,7 @@ func TestCandidateWriteTo(t *testing.T) { }) require.NoError(t, err, "error creating test TCP listener") - conn, err := net.DialTCP("tcp", nil, listener.Addr().(*net.TCPAddr)) + conn, err := net.DialTCP("tcp", nil, listener.Addr().(*net.TCPAddr)) // nolint require.NoError(t, err, "error dialing test TCP connection") loggerFactory := logging.NewDefaultLoggerFactory() @@ -427,11 +611,827 @@ func TestCandidateWriteTo(t *testing.T) { } _, err = c1.writeTo([]byte("test"), c2) - assert.NoError(t, err, "writing to open conn") + require.NoError(t, err, "writing to open conn") err = packetConn.Close() require.NoError(t, err, "error closing test TCP connection") _, err = c1.writeTo([]byte("test"), c2) - assert.Error(t, err, "writing to closed conn") + require.Error(t, err, "writing to closed conn") +} + +func TestMarshalUnmarshalCandidateWithZoneID(t *testing.T) { + candidateWithZoneID := mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeUDP6.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a%Local Connection", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + candidateStr := "750 0 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a 53987 typ host" + require.Equal(t, candidateStr, candidateWithZoneID.Marshal()) + + candidate := mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeUDP6.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + candidateWithZoneIDStr := "750 0 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a%eth0 53987 typ host" + candidate2, err := UnmarshalCandidate(candidateWithZoneIDStr) + require.NoError(t, err) + require.Truef(t, candidate.Equal(candidate2), "%s != %s", candidate.String(), candidate2.String()) + + candidateWithZoneIDStr2 := "750 0 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a%eth0%eth1 53987 typ host" + candidate2, err = UnmarshalCandidate(candidateWithZoneIDStr2) + require.NoError(t, err) + require.Truef(t, candidate.Equal(candidate2), "%s != %s", candidate.String(), candidate2.String()) +} + +func TestCandidateExtensionsMarshal(t *testing.T) { + testCases := []struct { + Extensions []CandidateExtension + candidate string + }{ + { + []CandidateExtension{ + {"generation", "0"}, + {"ufrag", "QNvE"}, + {"network-id", "4"}, + }, + //nolint: lll + "1299692247 1 udp 2122134271 fdc8:cc8:c835:e400:343c:feb:32c8:17b9 58240 typ host generation 0 ufrag QNvE network-id 4", + }, + { + []CandidateExtension{ + {"generation", "1"}, + {"network-id", "2"}, + {"network-cost", "50"}, + }, + //nolint:lll + "647372371 1 udp 1694498815 191.228.238.68 53991 typ srflx raddr 192.168.0.274 rport 53991 generation 1 network-id 2 network-cost 50", + }, + { + []CandidateExtension{ + {"generation", "0"}, + {"network-id", "2"}, + {"network-cost", "10"}, + }, + //nolint:lll + "4207374052 1 tcp 1685790463 192.0.2.15 50000 typ prflx raddr 10.0.0.1 rport 12345 generation 0 network-id 2 network-cost 10", + }, + { + []CandidateExtension{ + {"generation", "0"}, + {"network-id", "1"}, + {"network-cost", "20"}, + {"ufrag", "frag42abcdef"}, + {"password", "abc123exp123"}, + }, + //nolint: lll + "848194626 1 udp 16777215 50.0.0.1 5000 typ relay raddr 192.168.0.1 rport 5001 generation 0 network-id 1 network-cost 20 ufrag frag42abcdef password abc123exp123", + }, + { + []CandidateExtension{ + {"tcptype", "active"}, + {"generation", "0"}, + }, + "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active generation 0", + }, + { + []CandidateExtension{ + {"tcptype", "active"}, + {"generation", "0"}, + }, + "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active generation 0", + }, + { + []CandidateExtension{}, + "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host", + }, + { + []CandidateExtension{ + {"tcptype", "active"}, + {"empty-value-1", ""}, + {"empty-value-2", ""}, + }, + "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active empty-value-1 empty-value-2", + }, + { + []CandidateExtension{ + {"tcptype", "active"}, + {"empty-value-1", ""}, + {"empty-value-2", ""}, + }, + "1052353102 1 tcp 2128609279 192.168.0.196 0 typ host tcptype active empty-value-1 empty-value-2 ", + }, + } + + for _, tc := range testCases { + candidate, err := UnmarshalCandidate(tc.candidate) + require.NoError(t, err) + require.Equal(t, tc.Extensions, candidate.Extensions(), "Extensions should be equal", tc.candidate) + + valueStr := candidate.Marshal() + candidate2, err := UnmarshalCandidate(valueStr) + + require.NoError(t, err) + require.Equal(t, tc.Extensions, candidate2.Extensions(), "Marshal() should preserve extensions") + } +} + +func TestCandidateExtensionsDeepEqual(t *testing.T) { + noExt, err := UnmarshalCandidate("750 0 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a 53987 typ host") + require.NoError(t, err) + + generation := "0" + ufrag := "QNvE" + networkID := "4" + + extensions := []CandidateExtension{ + {"generation", generation}, + {"ufrag", ufrag}, + {"network-id", networkID}, + } + + candidate, err := UnmarshalCandidate( + "750 0 udp 500 fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a 53987 typ host generation " + + generation + " ufrag " + ufrag + " network-id " + networkID, + ) + require.NoError(t, err) + + testCases := []struct { + a Candidate + b Candidate + equal bool + }{ + { + mustCandidateHost(t, &CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }), + noExt, + true, + }, + { + mustCandidateHostWithExtensions( + t, + &CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }, + []CandidateExtension{}, + ), + noExt, + true, + }, + { + mustCandidateHostWithExtensions( + t, + &CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }, + extensions, + ), + candidate, + true, + }, + { + mustCandidateRelayWithExtensions( + t, + &CandidateRelayConfig{ + Network: NetworkTypeUDP4.String(), + Address: "10.0.0.10", + Port: 5000, + RelAddr: "10.0.0.2", + RelPort: 5001, + }, + []CandidateExtension{ + {"generation", "0"}, + {"network-id", "1"}, + }, + ), + mustCandidateRelayWithExtensions( + t, + &CandidateRelayConfig{ + Network: NetworkTypeUDP4.String(), + Address: "10.0.0.10", + Port: 5000, + RelAddr: "10.0.0.2", + RelPort: 5001, + }, + []CandidateExtension{ + {"network-id", "1"}, + {"generation", "0"}, + }, + ), + true, + }, + { + mustCandidatePeerReflexiveWithExtensions( + t, + &CandidatePeerReflexiveConfig{ + Network: NetworkTypeTCP4.String(), + Address: "192.0.2.15", + Port: 50000, + RelAddr: "10.0.0.1", + RelPort: 12345, + }, + []CandidateExtension{ + {"generation", "0"}, + {"network-id", "2"}, + {"network-cost", "10"}, + }, + ), + mustCandidatePeerReflexiveWithExtensions( + t, + &CandidatePeerReflexiveConfig{ + Network: NetworkTypeTCP4.String(), + Address: "192.0.2.15", + Port: 50000, + RelAddr: "10.0.0.1", + RelPort: 12345, + }, + []CandidateExtension{ + {"generation", "0"}, + {"network-id", "2"}, + {"network-cost", "10"}, + }, + ), + true, + }, + { + mustCandidateServerReflexiveWithExtensions( + t, + &CandidateServerReflexiveConfig{ + Network: NetworkTypeUDP4.String(), + Address: "191.228.238.68", + Port: 53991, + RelAddr: "192.168.0.274", + RelPort: 53991, + }, + []CandidateExtension{ + {"generation", "0"}, + {"network-id", "2"}, + {"network-cost", "10"}, + }, + ), + mustCandidateServerReflexiveWithExtensions( + t, + &CandidateServerReflexiveConfig{ + Network: NetworkTypeUDP4.String(), + Address: "191.228.238.68", + Port: 53991, + RelAddr: "192.168.0.274", + RelPort: 53991, + }, + []CandidateExtension{ + {"generation", "0"}, + {"network-id", "2"}, + {"network-cost", "10"}, + }, + ), + true, + }, + { + mustCandidateHostWithExtensions( + t, + &CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }, + []CandidateExtension{ + {"generation", "5"}, + {"ufrag", ufrag}, + {"network-id", networkID}, + }, + ), + candidate, + false, + }, + { + mustCandidateHostWithExtensions( + t, + &CandidateHostConfig{ + Network: NetworkTypeTCP4.String(), + Address: "192.168.0.196", + Port: 0, + Priority: 2128609279, + Foundation: "1052353102", + TCPType: TCPTypeActive, + }, + []CandidateExtension{ + {"tcptype", TCPTypeActive.String()}, + {"generation", "0"}, + }, + ), + mustCandidateHostWithExtensions( + t, + &CandidateHostConfig{ + Network: NetworkTypeTCP4.String(), + Address: "192.168.0.197", + Port: 0, + Priority: 2128609279, + Foundation: "1052353102", + TCPType: TCPTypeActive, + }, + []CandidateExtension{ + {"tcptype", TCPTypeActive.String()}, + {"generation", "0"}, + }, + ), + false, + }, + } + + for _, tc := range testCases { + require.Equal(t, tc.a.DeepEqual(tc.b), tc.equal, "a: %s, b: %s", tc.a.Marshal(), tc.b.Marshal()) + } +} + +func TestUnmarshalCandidateExtensions(t *testing.T) { + testCases := []struct { + name string + value string + expected []CandidateExtension + fail bool + }{ + { + name: "empty string", + value: "", + expected: []CandidateExtension{}, + fail: false, + }, + { + name: "valid extension string", + value: "a b c d", + expected: []CandidateExtension{{"a", "b"}, {"c", "d"}}, + fail: false, + }, + { + name: "valid extension string", + value: "a b empty c d", + expected: []CandidateExtension{ + {"a", "b"}, + {"empty", ""}, + {"c", "d"}, + }, + fail: false, + }, + { + name: "invalid extension", + value: " a b d", + expected: []CandidateExtension{{"", "a"}, {"b", "d"}}, + fail: true, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + req := require.New(t) + + actual, _, err := unmarshalCandidateExtensions(testCase.value) + if testCase.fail { + req.Error(err) + } else { + req.NoError(err) + req.EqualValuesf( + testCase.expected, + actual, + "UnmarshalCandidateExtensions() did not return the expected value %v", + testCase.value, + ) + } + }) + } +} + +func TestCandidateGetExtension(t *testing.T) { + t.Run("Get extension", func(t *testing.T) { + extensions := []CandidateExtension{ + {"a", "b"}, + {"c", "d"}, + } + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + candidate.setExtensions(extensions) + + value, ok := candidate.GetExtension("c") + require.True(t, ok) + require.Equal(t, "c", value.Key) + require.Equal(t, "d", value.Value) + + value, ok = candidate.GetExtension("a") + require.True(t, ok) + require.Equal(t, "a", value.Key) + require.Equal(t, "b", value.Value) + + value, ok = candidate.GetExtension("b") + require.False(t, ok) + require.Equal(t, "b", value.Key) + require.Equal(t, "", value.Value) + }) + + // This is undefined behavior in the spec; extension-att-name is not unique + // but it implied that it's unique in the implementation + t.Run("Extension with multiple values", func(t *testing.T) { + extensions := []CandidateExtension{ + {"a", "1"}, + {"a", "2"}, + } + + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + candidate.setExtensions(extensions) + + value, ok := candidate.GetExtension("a") + require.True(t, ok) + require.Equal(t, "a", value.Key) + require.Equal(t, "1", value.Value) + }) + + t.Run("TCPType extension", func(t *testing.T) { + extensions := []CandidateExtension{ + {"tcptype", "passive"}, + } + + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeTCP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + TCPType: TCPTypeActive, + }) + require.NoError(t, err) + + tcpType, ok := candidate.GetExtension("tcptype") + + require.True(t, ok) + require.Equal(t, "tcptype", tcpType.Key) + require.Equal(t, TCPTypeActive.String(), tcpType.Value) + + candidate.setExtensions(extensions) + + tcpType, ok = candidate.GetExtension("tcptype") + + require.True(t, ok) + require.Equal(t, "tcptype", tcpType.Key) + require.Equal(t, "passive", tcpType.Value) + + candidate2, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeTCP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + tcpType, ok = candidate2.GetExtension("tcptype") + + require.False(t, ok) + require.Equal(t, "tcptype", tcpType.Key) + require.Equal(t, "", tcpType.Value) + }) +} + +func TestBaseCandidateMarshalExtensions(t *testing.T) { + t.Run("Marshal extension", func(t *testing.T) { + extensions := []CandidateExtension{ + {"generation", "0"}, + {"ValuE", "KeE"}, + {"empty", ""}, + {"another", "value"}, + } + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + candidate.setExtensions(extensions) + + value := candidate.marshalExtensions() + require.Equal(t, "generation 0 ValuE KeE empty another value", value) + }) + + t.Run("Marshal Empty", func(t *testing.T) { + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + value := candidate.marshalExtensions() + require.Equal(t, "", value) + }) + + t.Run("Marshal TCPType no extension", func(t *testing.T) { + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + TCPType: TCPTypeActive, + }) + require.NoError(t, err) + + value := candidate.marshalExtensions() + require.Equal(t, "tcptype active", value) + }) +} + +func TestBaseCandidateExtensionsEqual(t *testing.T) { + testCases := []struct { + name string + extensions1 []CandidateExtension + extensions2 []CandidateExtension + expected bool + }{ + { + name: "Empty extensions", + extensions1: []CandidateExtension{}, + extensions2: []CandidateExtension{}, + expected: true, + }, + { + name: "Single value extensions", + extensions1: []CandidateExtension{{"a", "b"}}, + extensions2: []CandidateExtension{{"a", "b"}}, + expected: true, + }, + { + name: "multiple value extensions", + extensions1: []CandidateExtension{ + {"a", "b"}, + {"c", "d"}, + }, + extensions2: []CandidateExtension{ + {"a", "b"}, + {"c", "d"}, + }, + expected: true, + }, + { + name: "unsorted extensions", + extensions1: []CandidateExtension{ + {"c", "d"}, + {"a", "b"}, + }, + extensions2: []CandidateExtension{ + {"a", "b"}, + {"c", "d"}, + }, + expected: true, + }, + { + name: "different values", + extensions1: []CandidateExtension{ + {"a", "b"}, + {"c", "d"}, + }, + extensions2: []CandidateExtension{ + {"a", "b"}, + {"c", "e"}, + }, + expected: false, + }, + { + name: "different size", + extensions1: []CandidateExtension{ + {"a", "b"}, + {"c", "d"}, + }, + extensions2: []CandidateExtension{ + {"a", "b"}, + }, + expected: false, + }, + { + name: "different keys", + extensions1: []CandidateExtension{ + {"a", "b"}, + {"c", "d"}, + }, + extensions2: []CandidateExtension{ + {"a", "b"}, + {"e", "d"}, + }, + expected: false, + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + cand, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + cand.setExtensions(testCase.extensions1) + + require.Equal(t, testCase.expected, cand.extensionsEqual(testCase.extensions2)) + }) + } +} + +func TestCandidateAddExtension(t *testing.T) { + t.Run("Add extension", func(t *testing.T) { + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + require.NoError(t, candidate.AddExtension(CandidateExtension{"a", "b"})) + require.NoError(t, candidate.AddExtension(CandidateExtension{"c", "d"})) + + extensions := candidate.Extensions() + require.Equal(t, []CandidateExtension{{"a", "b"}, {"c", "d"}}, extensions) + }) + + t.Run("Add extension with existing key", func(t *testing.T) { + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + require.NoError(t, candidate.AddExtension(CandidateExtension{"a", "b"})) + require.NoError(t, candidate.AddExtension(CandidateExtension{"a", "d"})) + + extensions := candidate.Extensions() + require.Equal(t, []CandidateExtension{{"a", "d"}}, extensions) + }) + + t.Run("Keep tcptype extension", func(t *testing.T) { + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeTCP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + TCPType: TCPTypeActive, + }) + require.NoError(t, err) + + ext, ok := candidate.GetExtension("tcptype") + require.True(t, ok) + require.Equal(t, ext, CandidateExtension{"tcptype", "active"}) + require.Equal(t, candidate.Extensions(), []CandidateExtension{{"tcptype", "active"}}) + + require.NoError(t, candidate.AddExtension(CandidateExtension{"a", "b"})) + + ext, ok = candidate.GetExtension("tcptype") + require.True(t, ok) + require.Equal(t, ext, CandidateExtension{"tcptype", "active"}) + require.Equal(t, candidate.Extensions(), []CandidateExtension{{"tcptype", "active"}, {"a", "b"}}) + }) + + t.Run("TcpType change extension", func(t *testing.T) { + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeTCP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + require.NoError(t, candidate.AddExtension(CandidateExtension{"tcptype", "active"})) + + extensions := candidate.Extensions() + require.Equal(t, []CandidateExtension{{"tcptype", "active"}}, extensions) + require.Equal(t, TCPTypeActive, candidate.TCPType()) + + require.Error(t, candidate.AddExtension(CandidateExtension{"tcptype", "INVALID"})) + }) + + t.Run("Add empty extension", func(t *testing.T) { + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + require.Error(t, candidate.AddExtension(CandidateExtension{"", ""})) + + require.NoError(t, candidate.AddExtension(CandidateExtension{"a", ""})) + + extensions := candidate.Extensions() + + require.Equal(t, []CandidateExtension{{"a", ""}}, extensions) + }) +} + +func TestCandidateRemoveExtension(t *testing.T) { + t.Run("Remove extension", func(t *testing.T) { + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + require.NoError(t, candidate.AddExtension(CandidateExtension{"a", "b"})) + require.NoError(t, candidate.AddExtension(CandidateExtension{"c", "d"})) + + require.True(t, candidate.RemoveExtension("a")) + + extensions := candidate.Extensions() + require.Equal(t, []CandidateExtension{{"c", "d"}}, extensions) + }) + + t.Run("Remove extension that does not exist", func(t *testing.T) { + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeUDP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + }) + require.NoError(t, err) + + require.NoError(t, candidate.AddExtension(CandidateExtension{"a", "b"})) + require.NoError(t, candidate.AddExtension(CandidateExtension{"c", "d"})) + + require.False(t, candidate.RemoveExtension("b")) + + extensions := candidate.Extensions() + require.Equal(t, []CandidateExtension{{"a", "b"}, {"c", "d"}}, extensions) + }) + + t.Run("Remove tcptype extension", func(t *testing.T) { + candidate, err := NewCandidateHost(&CandidateHostConfig{ + Network: NetworkTypeTCP4.String(), + Address: "fcd9:e3b8:12ce:9fc5:74a5:c6bb:d8b:e08a", + Port: 53987, + Priority: 500, + Foundation: "750", + TCPType: TCPTypeActive, + }) + require.NoError(t, err) + + // tcptype extension should be removed, even if it's not in the extensions list (Not Parsed) + require.True(t, candidate.RemoveExtension("tcptype")) + require.Equal(t, TCPTypeUnspecified, candidate.TCPType()) + require.Empty(t, candidate.Extensions()) + + require.NoError(t, candidate.AddExtension(CandidateExtension{"tcptype", "passive"})) + + require.True(t, candidate.RemoveExtension("tcptype")) + require.Equal(t, TCPTypeUnspecified, candidate.TCPType()) + require.Empty(t, candidate.Extensions()) + }) } diff --git a/candidatepair.go b/candidatepair.go index 7dc3885..82655aa 100644 --- a/candidatepair.go +++ b/candidatepair.go @@ -5,9 +5,10 @@ package ice import ( "fmt" + "sync/atomic" "time" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" ) func newCandidatePair(local, remote Candidate, controlling bool) *CandidatePair { @@ -19,21 +20,31 @@ func newCandidatePair(local, remote Candidate, controlling bool) *CandidatePair } } -type TransactionID [stun.TransactionIDSize]byte - -// CandidatePair is a combination of a -// local and remote candidate +// CandidatePair is a combination of a local and remote candidate. type CandidatePair struct { iceRoleControlling bool Remote Candidate Local Candidate - latency time.Duration - lastBindingRequest time.Time - lastBindingTransactionID TransactionID bindingRequestCount uint16 state CandidatePairState nominated bool nominateOnBindingSuccess bool + + // stats + currentRoundTripTime int64 // in ns + totalRoundTripTime int64 // in ns + + requestsReceived uint64 + requestsSent uint64 + responsesReceived uint64 + responsesSent uint64 + + firstRequestSentAt atomic.Value // time.Time + lastRequestSentAt atomic.Value // time.Time + firstReponseReceivedAt atomic.Value // time.Time + lastResponseReceivedAt atomic.Value // time.Time + firstRequestReceivedAt atomic.Value // time.Time + lastRequestReceivedAt atomic.Value // time.Time } func (p *CandidatePair) String() string { @@ -41,8 +52,17 @@ func (p *CandidatePair) String() string { return "" } - return fmt.Sprintf("prio %d (local, prio %d) %s <-> %s (remote, prio %d), state: %s, nominated: %v, nominateOnBindingSuccess: %v", - p.priority(), p.Local.Priority(), p.Local, p.Remote, p.Remote.Priority(), p.state, p.nominated, p.nominateOnBindingSuccess) + return fmt.Sprintf( + "prio %d (local, prio %d) %s <-> %s (remote, prio %d), state: %s, nominated: %v, nominateOnBindingSuccess: %v", + p.priority(), + p.Local.Priority(), + p.Local, + p.Remote, + p.Remote.Priority(), + p.state, + p.nominated, + p.nominateOnBindingSuccess, + ) } func (p *CandidatePair) equal(other *CandidatePair) bool { @@ -52,6 +72,7 @@ func (p *CandidatePair) equal(other *CandidatePair) bool { if p == nil || other == nil { return false } + return p.Local.Equal(other.Local) && p.Remote.Equal(other.Remote) } @@ -59,9 +80,9 @@ func (p *CandidatePair) equal(other *CandidatePair) bool { // Let G be the priority for the candidate provided by the controlling // agent. Let D be the priority for the candidate provided by the // controlled agent. -// pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0) +// pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0). func (p *CandidatePair) priority() uint64 { - var g, d uint32 + var g, d uint32 //nolint:varnamelen // clearer to use g and d here if p.iceRoleControlling { g = p.Local.Priority() d = p.Remote.Priority() @@ -72,28 +93,31 @@ func (p *CandidatePair) priority() uint64 { // Just implement these here rather // than fooling around with the math package - min := func(x, y uint32) uint64 { + localMin := func(x, y uint32) uint64 { if x < y { return uint64(x) } + return uint64(y) } - max := func(x, y uint32) uint64 { + localMax := func(x, y uint32) uint64 { if x > y { return uint64(x) } + return uint64(y) } cmp := func(x, y uint32) uint64 { if x > y { return uint64(1) } + return uint64(0) } // 1<<32 overflows uint32; and if both g && d are // maxUint32, this result would overflow uint64 - return (1<<32-1)*min(g, d) + 2*max(g, d) + cmp(g, d) + return (1<<32-1)*localMin(g, d) + 2*localMax(g, d) + cmp(g, d) } func (p *CandidatePair) Write(b []byte) (int, error) { @@ -107,20 +131,126 @@ func (a *Agent) sendSTUN(msg *stun.Message, local, remote Candidate) { } } -func (p *CandidatePair) markBindingRequest(transactionID TransactionID) { - p.lastBindingRequest = time.Now() - p.lastBindingTransactionID = transactionID +// UpdateRoundTripTime sets the current round time of this pair and +// accumulates total round trip time and responses received. +func (p *CandidatePair) UpdateRoundTripTime(rtt time.Duration) { + rttNs := rtt.Nanoseconds() + atomic.StoreInt64(&p.currentRoundTripTime, rttNs) + atomic.AddInt64(&p.totalRoundTripTime, rttNs) + atomic.AddUint64(&p.responsesReceived, 1) + + now := time.Now() + p.firstReponseReceivedAt.CompareAndSwap(nil, now) + p.lastResponseReceivedAt.Store(now) } -func (p *CandidatePair) markBindingResponse(transactionID TransactionID) bool { - if p.lastBindingRequest.IsZero() || transactionID != p.lastBindingTransactionID { - return false +// CurrentRoundTripTime returns the current round trip time in seconds +// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-currentroundtriptime +func (p *CandidatePair) CurrentRoundTripTime() float64 { + return time.Duration(atomic.LoadInt64(&p.currentRoundTripTime)).Seconds() +} + +// TotalRoundTripTime returns the current round trip time in seconds +// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-totalroundtriptime +func (p *CandidatePair) TotalRoundTripTime() float64 { + return time.Duration(atomic.LoadInt64(&p.totalRoundTripTime)).Seconds() +} + +// RequestsReceived returns the total number of connectivity checks received +// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-requestsreceived +func (p *CandidatePair) RequestsReceived() uint64 { + return atomic.LoadUint64(&p.requestsReceived) +} + +// RequestsSent returns the total number of connectivity checks sent +// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-requestssent +func (p *CandidatePair) RequestsSent() uint64 { + return atomic.LoadUint64(&p.requestsSent) +} + +// ResponsesReceived returns the total number of connectivity responses received +// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-responsesreceived +func (p *CandidatePair) ResponsesReceived() uint64 { + return atomic.LoadUint64(&p.responsesReceived) +} + +// ResponsesSent returns the total number of connectivity responses sent +// https://www.w3.org/TR/webrtc-stats/#dom-rtcicecandidatepairstats-responsessent +func (p *CandidatePair) ResponsesSent() uint64 { + return atomic.LoadUint64(&p.responsesSent) +} + +// FirstRequestSentAt returns the timestamp of the first connectivity check sent. +func (p *CandidatePair) FirstRequestSentAt() time.Time { + if v, ok := p.firstRequestSentAt.Load().(time.Time); ok { + return v } - p.latency = time.Since(p.lastBindingRequest) - return true + return time.Time{} } -func (p *CandidatePair) Latency() time.Duration { - return p.latency +// LastRequestSentAt returns the timestamp of the last connectivity check sent. +func (p *CandidatePair) LastRequestSentAt() time.Time { + if v, ok := p.lastRequestSentAt.Load().(time.Time); ok { + return v + } + + return time.Time{} +} + +// FirstReponseReceivedAt returns the timestamp of the first connectivity response received. +func (p *CandidatePair) FirstReponseReceivedAt() time.Time { + if v, ok := p.firstReponseReceivedAt.Load().(time.Time); ok { + return v + } + + return time.Time{} +} + +// LastResponseReceivedAt returns the timestamp of the last connectivity response received. +func (p *CandidatePair) LastResponseReceivedAt() time.Time { + if v, ok := p.lastResponseReceivedAt.Load().(time.Time); ok { + return v + } + + return time.Time{} +} + +// FirstRequestReceivedAt returns the timestamp of the first connectivity check received. +func (p *CandidatePair) FirstRequestReceivedAt() time.Time { + if v, ok := p.firstRequestReceivedAt.Load().(time.Time); ok { + return v + } + + return time.Time{} +} + +// LastRequestReceivedAt returns the timestamp of the last connectivity check received. +func (p *CandidatePair) LastRequestReceivedAt() time.Time { + if v, ok := p.lastRequestReceivedAt.Load().(time.Time); ok { + return v + } + + return time.Time{} +} + +// UpdateRequestSent increments the number of requests sent and updates the timestamp. +func (p *CandidatePair) UpdateRequestSent() { + atomic.AddUint64(&p.requestsSent, 1) + now := time.Now() + p.firstRequestSentAt.CompareAndSwap(nil, now) + p.lastRequestSentAt.Store(now) +} + +// UpdateResponseSent increments the number of responses sent. +func (p *CandidatePair) UpdateResponseSent() { + atomic.AddUint64(&p.responsesSent, 1) +} + +// UpdateRequestReceived increments the number of requests received and updates the timestamp. +func (p *CandidatePair) UpdateRequestReceived() { + atomic.AddUint64(&p.requestsReceived, 1) + now := time.Now() + p.firstRequestReceivedAt.CompareAndSwap(nil, now) + p.lastRequestReceivedAt.Store(now) } diff --git a/candidatepair_state.go b/candidatepair_state.go index 1a1e827..e1efd39 100644 --- a/candidatepair_state.go +++ b/candidatepair_state.go @@ -3,13 +3,13 @@ package ice -// CandidatePairState represent the ICE candidate pair state +// CandidatePairState represent the ICE candidate pair state. type CandidatePairState int const ( // CandidatePairStateWaiting means a check has not been performed for - // this pair - CandidatePairStateWaiting = iota + 1 + // this pair. + CandidatePairStateWaiting CandidatePairState = iota + 1 // CandidatePairStateInProgress means a check has been sent for this pair, // but the transaction is in progress. @@ -36,5 +36,6 @@ func (c CandidatePairState) String() string { case CandidatePairStateSucceeded: return "succeeded" } + return "Unknown candidate pair state" } diff --git a/candidatepair_test.go b/candidatepair_test.go index 58653bd..f6e8284 100644 --- a/candidatepair_test.go +++ b/candidatepair_test.go @@ -6,7 +6,7 @@ package ice import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func hostCandidate() *CandidateHost { @@ -115,9 +115,7 @@ func TestCandidatePairPriority(t *testing.T) { WantPriority: 72057593987596287, }, } { - if got, want := test.Pair.priority(), test.WantPriority; got != want { - t.Fatalf("CandidatePair(%v).Priority() = %d, want %d", test.Pair, got, want) - } + require.Equal(t, test.Pair.priority(), test.WantPriority) } } @@ -125,12 +123,10 @@ func TestCandidatePairEquality(t *testing.T) { pairA := newCandidatePair(hostCandidate(), srflxCandidate(), true) pairB := newCandidatePair(hostCandidate(), srflxCandidate(), false) - if !pairA.equal(pairB) { - t.Fatalf("Expected %v to equal %v", pairA, pairB) - } + require.True(t, pairA.equal(pairB)) } func TestNilCandidatePairString(t *testing.T) { var nilCandidatePair *CandidatePair - assert.Equal(t, nilCandidatePair.String(), "") + require.Equal(t, nilCandidatePair.String(), "") } diff --git a/candidaterelatedaddress.go b/candidaterelatedaddress.go index e87c705..161adf8 100644 --- a/candidaterelatedaddress.go +++ b/candidaterelatedaddress.go @@ -12,7 +12,7 @@ type CandidateRelatedAddress struct { Port int } -// String makes CandidateRelatedAddress printable +// String makes CandidateRelatedAddress printable. func (c *CandidateRelatedAddress) String() string { if c == nil { return "" @@ -27,6 +27,7 @@ func (c *CandidateRelatedAddress) Equal(other *CandidateRelatedAddress) bool { if c == nil && other == nil { return true } + return c != nil && other != nil && c.Address == other.Address && c.Port == other.Port diff --git a/candidatetype.go b/candidatetype.go index 3972934..fef798b 100644 --- a/candidatetype.go +++ b/candidatetype.go @@ -3,10 +3,10 @@ package ice -// CandidateType represents the type of candidate +// CandidateType represents the type of candidate. type CandidateType byte -// CandidateType enum +// CandidateType enum. const ( CandidateTypeUnspecified CandidateType = iota CandidateTypeHost @@ -15,7 +15,7 @@ const ( CandidateTypeRelay ) -// String makes CandidateType printable +// String makes CandidateType printable. func (c CandidateType) String() string { switch c { case CandidateTypeHost: @@ -29,6 +29,7 @@ func (c CandidateType) String() string { case CandidateTypeUnspecified: return "Unknown candidate type" } + return "Unknown candidate type" } @@ -49,6 +50,7 @@ func (c CandidateType) Preference() uint16 { case CandidateTypeRelay, CandidateTypeUnspecified: return 0 } + return 0 } @@ -61,5 +63,6 @@ func containsCandidateType(candidateType CandidateType, candidateTypeList []Cand return true } } + return false } diff --git a/connectivity_vnet_test.go b/connectivity_vnet_test.go index b70acee..2064c46 100644 --- a/connectivity_vnet_test.go +++ b/connectivity_vnet_test.go @@ -15,11 +15,11 @@ import ( "time" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" "github.com/pion/transport/v3/vnet" - "github.com/pion/turn/v3" - "github.com/stretchr/testify/assert" + "github.com/pion/turn/v4" + "github.com/stretchr/testify/require" ) const ( @@ -45,7 +45,7 @@ func (v *virtualNet) close() { v.wan.Stop() //nolint:errcheck,gosec } -func buildVNet(natType0, natType1 *vnet.NATType) (*virtualNet, error) { +func buildVNet(natType0, natType1 *vnet.NATType) (*virtualNet, error) { //nolint:cyclop loggerFactory := logging.NewDefaultLoggerFactory() // WAN @@ -77,6 +77,7 @@ func buildVNet(natType0, natType1 *vnet.NATType) (*virtualNet, error) { vnetGlobalIPA + "/" + vnetLocalIPA, } } + return []string{ vnetGlobalIPA, } @@ -114,6 +115,7 @@ func buildVNet(natType0, natType1 *vnet.NATType) (*virtualNet, error) { vnetGlobalIPB + "/" + vnetLocalIPB, } } + return []string{ vnetGlobalIPB, } @@ -171,10 +173,11 @@ func addVNetSTUN(wanNet *vnet.Net, loggerFactory logging.LoggerFactory) (*turn.S return nil, err } server, err := turn.NewServer(turn.ServerConfig{ - AuthHandler: func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) { + AuthHandler: func(username, realm string, _ net.Addr) (key []byte, ok bool) { if pw, ok := credMap[username]; ok { return turn.GenerateAuthKey(username, realm, pw), true } + return nil, false }, PacketConnConfigs: []turn.PacketConnConfig{ @@ -197,15 +200,16 @@ func addVNetSTUN(wanNet *vnet.Net, loggerFactory logging.LoggerFactory) (*turn.S return server, err } -func connectWithVNet(aAgent, bAgent *Agent) (*Conn, *Conn) { +func connectWithVNet(t *testing.T, aAgent, bAgent *Agent) (*Conn, *Conn) { + t.Helper() // Manual signaling aUfrag, aPwd, err := aAgent.GetLocalUserCredentials() - check(err) + require.NoError(t, err) bUfrag, bPwd, err := bAgent.GetLocalUserCredentials() - check(err) + require.NoError(t, err) - gatherAndExchangeCandidates(aAgent, bAgent) + gatherAndExchangeCandidates(t, aAgent, bAgent) accepted := make(chan struct{}) var aConn *Conn @@ -213,15 +217,16 @@ func connectWithVNet(aAgent, bAgent *Agent) (*Conn, *Conn) { go func() { var acceptErr error aConn, acceptErr = aAgent.Accept(context.TODO(), bUfrag, bPwd) - check(acceptErr) + require.NoError(t, acceptErr) close(accepted) }() bConn, err := bAgent.Dial(context.TODO(), aUfrag, aPwd) - check(err) + require.NoError(t, err) // Ensure accepted <-accepted + return aConn, bConn } @@ -230,7 +235,8 @@ type agentTestConfig struct { nat1To1IPCandidateType CandidateType } -func pipeWithVNet(v *virtualNet, a0TestConfig, a1TestConfig *agentTestConfig) (*Conn, *Conn) { +func pipeWithVNet(t *testing.T, vnet *virtualNet, a0TestConfig, a1TestConfig *agentTestConfig) (*Conn, *Conn) { + t.Helper() aNotifier, aConnected := onConnected() bNotifier, bConnected := onConnected() @@ -247,17 +253,12 @@ func pipeWithVNet(v *virtualNet, a0TestConfig, a1TestConfig *agentTestConfig) (* MulticastDNSMode: MulticastDNSModeDisabled, NAT1To1IPs: nat1To1IPs, NAT1To1IPCandidateType: a0TestConfig.nat1To1IPCandidateType, - Net: v.net0, + Net: vnet.net0, } aAgent, err := NewAgent(cfg0) - if err != nil { - panic(err) - } - err = aAgent.OnConnectionStateChange(aNotifier) - if err != nil { - panic(err) - } + require.NoError(t, err) + require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) if a1TestConfig.nat1To1IPCandidateType != CandidateTypeUnspecified { nat1To1IPs = []string{ @@ -270,19 +271,14 @@ func pipeWithVNet(v *virtualNet, a0TestConfig, a1TestConfig *agentTestConfig) (* MulticastDNSMode: MulticastDNSModeDisabled, NAT1To1IPs: nat1To1IPs, NAT1To1IPCandidateType: a1TestConfig.nat1To1IPCandidateType, - Net: v.net1, + Net: vnet.net1, } bAgent, err := NewAgent(cfg1) - if err != nil { - panic(err) - } - err = bAgent.OnConnectionStateChange(bNotifier) - if err != nil { - panic(err) - } + require.NoError(t, err) + require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) - aConn, bConn := connectWithVNet(aAgent, bAgent) + aConn, bConn := connectWithVNet(t, aAgent, bAgent) // Ensure pair selected // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair @@ -292,18 +288,15 @@ func pipeWithVNet(v *virtualNet, a0TestConfig, a1TestConfig *agentTestConfig) (* return aConn, bConn } -func closePipe(t *testing.T, ca *Conn, cb *Conn) bool { - err := ca.Close() - if !assert.NoError(t, err, "should succeed") { - return false - } - err = cb.Close() - return assert.NoError(t, err, "should succeed") +func closePipe(t *testing.T, ca *Conn, cb *Conn) { + t.Helper() + + require.NoError(t, ca.Close()) + require.NoError(t, cb.Close()) } func TestConnectivityVNet(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() stunServerURL := &stun.URI{ Scheme: stun.SchemeTypeSTUN, @@ -330,12 +323,10 @@ func TestConnectivityVNet(t *testing.T) { MappingBehavior: vnet.EndpointIndependent, FilteringBehavior: vnet.EndpointIndependent, } - v, err := buildVNet(natType, natType) + vnet, err := buildVNet(natType, natType) - if !assert.NoError(t, err, "should succeed") { - return - } - defer v.close() + require.NoError(t, err, "should succeed") + defer vnet.close() log.Debug("Connecting...") a0TestConfig := &agentTestConfig{ @@ -348,14 +339,12 @@ func TestConnectivityVNet(t *testing.T) { stunServerURL, }, } - ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig) + ca, cb := pipeWithVNet(t, vnet, a0TestConfig, a1TestConfig) time.Sleep(1 * time.Second) log.Debug("Closing...") - if !closePipe(t, ca, cb) { - return - } + closePipe(t, ca, cb) }) t.Run("Symmetric NATs on both ends", func(t *testing.T) { @@ -367,12 +356,10 @@ func TestConnectivityVNet(t *testing.T) { MappingBehavior: vnet.EndpointAddrPortDependent, FilteringBehavior: vnet.EndpointAddrPortDependent, } - v, err := buildVNet(natType, natType) + vnet, err := buildVNet(natType, natType) - if !assert.NoError(t, err, "should succeed") { - return - } - defer v.close() + require.NoError(t, err, "should succeed") + defer vnet.close() log.Debug("Connecting...") a0TestConfig := &agentTestConfig{ @@ -386,12 +373,10 @@ func TestConnectivityVNet(t *testing.T) { stunServerURL, }, } - ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig) + ca, cb := pipeWithVNet(t, vnet, a0TestConfig, a1TestConfig) log.Debug("Closing...") - if !closePipe(t, ca, cb) { - return - } + closePipe(t, ca, cb) }) t.Run("1:1 NAT with host candidate vs Symmetric NATs", func(t *testing.T) { @@ -407,12 +392,10 @@ func TestConnectivityVNet(t *testing.T) { MappingBehavior: vnet.EndpointAddrPortDependent, FilteringBehavior: vnet.EndpointAddrPortDependent, } - v, err := buildVNet(natType0, natType1) + vnet, err := buildVNet(natType0, natType1) - if !assert.NoError(t, err, "should succeed") { - return - } - defer v.close() + require.NoError(t, err, "should succeed") + defer vnet.close() log.Debug("Connecting...") a0TestConfig := &agentTestConfig{ @@ -422,12 +405,10 @@ func TestConnectivityVNet(t *testing.T) { a1TestConfig := &agentTestConfig{ urls: []*stun.URI{}, } - ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig) + ca, cb := pipeWithVNet(t, vnet, a0TestConfig, a1TestConfig) log.Debug("Closing...") - if !closePipe(t, ca, cb) { - return - } + closePipe(t, ca, cb) }) t.Run("1:1 NAT with srflx candidate vs Symmetric NATs", func(t *testing.T) { @@ -443,12 +424,10 @@ func TestConnectivityVNet(t *testing.T) { MappingBehavior: vnet.EndpointAddrPortDependent, FilteringBehavior: vnet.EndpointAddrPortDependent, } - v, err := buildVNet(natType0, natType1) + vnet, err := buildVNet(natType0, natType1) - if !assert.NoError(t, err, "should succeed") { - return - } - defer v.close() + require.NoError(t, err, "should succeed") + defer vnet.close() log.Debug("Connecting...") a0TestConfig := &agentTestConfig{ @@ -458,22 +437,19 @@ func TestConnectivityVNet(t *testing.T) { a1TestConfig := &agentTestConfig{ urls: []*stun.URI{}, } - ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig) + ca, cb := pipeWithVNet(t, vnet, a0TestConfig, a1TestConfig) log.Debug("Closing...") - if !closePipe(t, ca, cb) { - return - } + closePipe(t, ca, cb) }) } -// TestDisconnectedToConnected asserts that an agent can go to disconnected, and then return to connected successfully +// TestDisconnectedToConnected requires that an agent can go to disconnected, +// and then return to connected successfully. func TestDisconnectedToConnected(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 10) - defer lim.Stop() + defer test.TimeOut(time.Second * 10).Stop() loggerFactory := logging.NewDefaultLoggerFactory() @@ -482,7 +458,7 @@ func TestDisconnectedToConnected(t *testing.T) { CIDR: "0.0.0.0/0", LoggerFactory: loggerFactory, }) - assert.NoError(t, err) + require.NoError(t, err) var dropAllData uint64 wan.AddChunkFilter(func(vnet.Chunk) bool { @@ -492,16 +468,16 @@ func TestDisconnectedToConnected(t *testing.T) { net0, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"192.168.0.1"}, }) - assert.NoError(t, err) - assert.NoError(t, wan.AddNet(net0)) + require.NoError(t, err) + require.NoError(t, wan.AddNet(net0)) net1, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"192.168.0.2"}, }) - assert.NoError(t, err) - assert.NoError(t, wan.AddNet(net1)) + require.NoError(t, err) + require.NoError(t, wan.AddNet(net1)) - assert.NoError(t, wan.Start()) + require.NoError(t, wan.Start()) disconnectTimeout := time.Second keepaliveInterval := time.Millisecond * 20 @@ -515,7 +491,10 @@ func TestDisconnectedToConnected(t *testing.T) { KeepaliveInterval: &keepaliveInterval, CheckInterval: &keepaliveInterval, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, controllingAgent.Close()) + }() controlledAgent, err := NewAgent(&AgentConfig{ NetworkTypes: supportedNetworkTypes(), @@ -525,19 +504,22 @@ func TestDisconnectedToConnected(t *testing.T) { KeepaliveInterval: &keepaliveInterval, CheckInterval: &keepaliveInterval, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, controlledAgent.Close()) + }() controllingStateChanges := make(chan ConnectionState, 100) - assert.NoError(t, controllingAgent.OnConnectionStateChange(func(c ConnectionState) { + require.NoError(t, controllingAgent.OnConnectionStateChange(func(c ConnectionState) { controllingStateChanges <- c })) controlledStateChanges := make(chan ConnectionState, 100) - assert.NoError(t, controlledAgent.OnConnectionStateChange(func(c ConnectionState) { + require.NoError(t, controlledAgent.OnConnectionStateChange(func(c ConnectionState) { controlledStateChanges <- c })) - connectWithVNet(controllingAgent, controlledAgent) + connectWithVNet(t, controllingAgent, controlledAgent) blockUntilStateSeen := func(expectedState ConnectionState, stateQueue chan ConnectionState) { for s := range stateQueue { if s == expectedState { @@ -560,18 +542,14 @@ func TestDisconnectedToConnected(t *testing.T) { blockUntilStateSeen(ConnectionStateConnected, controllingStateChanges) blockUntilStateSeen(ConnectionStateConnected, controlledStateChanges) - assert.NoError(t, wan.Stop()) - assert.NoError(t, controllingAgent.Close()) - assert.NoError(t, controlledAgent.Close()) + require.NoError(t, wan.Stop()) } -// Agent.Write should use the best valid pair if a selected pair is not yet available +// Agent.Write should use the best valid pair if a selected pair is not yet available. func TestWriteUseValidPair(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 10) - defer lim.Stop() + defer test.TimeOut(time.Second * 10).Stop() loggerFactory := logging.NewDefaultLoggerFactory() @@ -580,7 +558,7 @@ func TestWriteUseValidPair(t *testing.T) { CIDR: "0.0.0.0/0", LoggerFactory: loggerFactory, }) - assert.NoError(t, err) + require.NoError(t, err) wan.AddChunkFilter(func(c vnet.Chunk) bool { if stun.IsMessage(c.UserData()) { @@ -600,16 +578,16 @@ func TestWriteUseValidPair(t *testing.T) { net0, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"192.168.0.1"}, }) - assert.NoError(t, err) - assert.NoError(t, wan.AddNet(net0)) + require.NoError(t, err) + require.NoError(t, wan.AddNet(net0)) net1, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{"192.168.0.2"}, }) - assert.NoError(t, err) - assert.NoError(t, wan.AddNet(net1)) + require.NoError(t, err) + require.NoError(t, wan.AddNet(net1)) - assert.NoError(t, wan.Start()) + require.NoError(t, wan.Start()) // Create two agents and connect them controllingAgent, err := NewAgent(&AgentConfig{ @@ -617,25 +595,31 @@ func TestWriteUseValidPair(t *testing.T) { MulticastDNSMode: MulticastDNSModeDisabled, Net: net0, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, controllingAgent.Close()) + }() controlledAgent, err := NewAgent(&AgentConfig{ NetworkTypes: supportedNetworkTypes(), MulticastDNSMode: MulticastDNSModeDisabled, Net: net1, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, controlledAgent.Close()) + }() - gatherAndExchangeCandidates(controllingAgent, controlledAgent) + gatherAndExchangeCandidates(t, controllingAgent, controlledAgent) controllingUfrag, controllingPwd, err := controllingAgent.GetLocalUserCredentials() - assert.NoError(t, err) + require.NoError(t, err) controlledUfrag, controlledPwd, err := controlledAgent.GetLocalUserCredentials() - assert.NoError(t, err) + require.NoError(t, err) - assert.NoError(t, controllingAgent.startConnectivityChecks(true, controlledUfrag, controlledPwd)) - assert.NoError(t, controlledAgent.startConnectivityChecks(false, controllingUfrag, controllingPwd)) + require.NoError(t, controllingAgent.startConnectivityChecks(true, controlledUfrag, controlledPwd)) + require.NoError(t, controlledAgent.startConnectivityChecks(false, controllingUfrag, controllingPwd)) testMessage := []byte("Test Message") go func() { @@ -650,11 +634,9 @@ func TestWriteUseValidPair(t *testing.T) { readBuf := make([]byte, len(testMessage)) _, err = (&Conn{agent: controlledAgent}).Read(readBuf) - assert.NoError(t, err) + require.NoError(t, err) - assert.Equal(t, readBuf, testMessage) + require.Equal(t, readBuf, testMessage) - assert.NoError(t, wan.Stop()) - assert.NoError(t, controllingAgent.Close()) - assert.NoError(t, controlledAgent.Close()) + require.NoError(t, wan.Stop()) } diff --git a/context.go b/context.go deleted file mode 100644 index 3645445..0000000 --- a/context.go +++ /dev/null @@ -1,40 +0,0 @@ -// SPDX-FileCopyrightText: 2023 The Pion community -// SPDX-License-Identifier: MIT - -package ice - -import ( - "context" - "time" -) - -func (a *Agent) context() context.Context { - return agentContext(a.done) -} - -type agentContext chan struct{} - -// Done implements context.Context -func (a agentContext) Done() <-chan struct{} { - return (chan struct{})(a) -} - -// Err implements context.Context -func (a agentContext) Err() error { - select { - case <-(chan struct{})(a): - return ErrRunCanceled - default: - return nil - } -} - -// Deadline implements context.Context -func (a agentContext) Deadline() (deadline time.Time, ok bool) { - return time.Time{}, false -} - -// Value implements context.Context -func (a agentContext) Value(interface{}) interface{} { - return nil -} diff --git a/errors.go b/errors.go index 46785ed..a803665 100644 --- a/errors.go +++ b/errors.go @@ -3,7 +3,11 @@ package ice -import "errors" +import ( + "errors" + + "github.com/pion/ice/v4/internal/taskloop" +) var ( // ErrUnknownType indicates an error with Unknown info. @@ -25,69 +29,71 @@ var ( ErrPort = errors.New("invalid port") // ErrLocalUfragInsufficientBits indicates local username fragment insufficient bits are provided. - // Have to be at least 24 bits long + // Have to be at least 24 bits long. ErrLocalUfragInsufficientBits = errors.New("local username fragment is less than 24 bits long") // ErrLocalPwdInsufficientBits indicates local password insufficient bits are provided. - // Have to be at least 128 bits long + // Have to be at least 128 bits long. ErrLocalPwdInsufficientBits = errors.New("local password is less than 128 bits long") // ErrProtoType indicates an unsupported transport type was provided. ErrProtoType = errors.New("invalid transport protocol type") - // ErrClosed indicates the agent is closed - ErrClosed = errors.New("the agent is closed") + // ErrClosed indicates the agent is closed. + ErrClosed = taskloop.ErrClosed - // ErrNoCandidatePairs indicates agent does not have a valid candidate pair + // ErrNoCandidatePairs indicates agent does not have a valid candidate pair. ErrNoCandidatePairs = errors.New("no candidate pairs available") - // ErrCanceledByCaller indicates agent connection was canceled by the caller + // ErrCanceledByCaller indicates agent connection was canceled by the caller. ErrCanceledByCaller = errors.New("connecting canceled by caller") - // ErrMultipleStart indicates agent was started twice + // ErrMultipleStart indicates agent was started twice. ErrMultipleStart = errors.New("attempted to start agent twice") - // ErrRemoteUfragEmpty indicates agent was started with an empty remote ufrag + // ErrRemoteUfragEmpty indicates agent was started with an empty remote ufrag. ErrRemoteUfragEmpty = errors.New("remote ufrag is empty") - // ErrRemotePwdEmpty indicates agent was started with an empty remote pwd + // ErrRemotePwdEmpty indicates agent was started with an empty remote pwd. ErrRemotePwdEmpty = errors.New("remote pwd is empty") - // ErrNoOnCandidateHandler indicates agent was started without OnCandidate + // ErrNoOnCandidateHandler indicates agent was started without OnCandidate. ErrNoOnCandidateHandler = errors.New("no OnCandidate provided") - // ErrMultipleGatherAttempted indicates GatherCandidates has been called multiple times + // ErrMultipleGatherAttempted indicates GatherCandidates has been called multiple times. ErrMultipleGatherAttempted = errors.New("attempting to gather candidates during gathering state") - // ErrUsernameEmpty indicates agent was give TURN URL with an empty Username + // ErrUsernameEmpty indicates agent was give TURN URL with an empty Username. ErrUsernameEmpty = errors.New("username is empty") - // ErrPasswordEmpty indicates agent was give TURN URL with an empty Password + // ErrPasswordEmpty indicates agent was give TURN URL with an empty Password. ErrPasswordEmpty = errors.New("password is empty") - // ErrAddressParseFailed indicates we were unable to parse a candidate address + // ErrAddressParseFailed indicates we were unable to parse a candidate address. ErrAddressParseFailed = errors.New("failed to parse address") - // ErrLiteUsingNonHostCandidates indicates non host candidates were selected for a lite agent + // ErrLiteUsingNonHostCandidates indicates non host candidates were selected for a lite agent. ErrLiteUsingNonHostCandidates = errors.New("lite agents must only use host candidates") // ErrUselessUrlsProvided indicates that one or more URL was provided to the agent but no host - // candidate required them + // candidate required them. ErrUselessUrlsProvided = errors.New("agent does not need URL with selected candidate types") // ErrUnsupportedNAT1To1IPCandidateType indicates that the specified NAT1To1IPCandidateType is - // unsupported + // unsupported. ErrUnsupportedNAT1To1IPCandidateType = errors.New("unsupported 1:1 NAT IP candidate type") - // ErrInvalidNAT1To1IPMapping indicates that the given 1:1 NAT IP mapping is invalid + // ErrInvalidNAT1To1IPMapping indicates that the given 1:1 NAT IP mapping is invalid. ErrInvalidNAT1To1IPMapping = errors.New("invalid 1:1 NAT IP mapping") - // ErrExternalMappedIPNotFound in NAT1To1IPMapping + // ErrExternalMappedIPNotFound in NAT1To1IPMapping. ErrExternalMappedIPNotFound = errors.New("external mapped IP not found") // ErrMulticastDNSWithNAT1To1IPMapping indicates that the mDNS gathering cannot be used along // with 1:1 NAT IP mapping for host candidate. - ErrMulticastDNSWithNAT1To1IPMapping = errors.New("mDNS gathering cannot be used with 1:1 NAT IP mapping for host candidate") + ErrMulticastDNSWithNAT1To1IPMapping = errors.New( + "mDNS gathering cannot be used with 1:1 NAT IP mapping for host candidate", + ) // ErrIneffectiveNAT1To1IPMappingHost indicates that 1:1 NAT IP mapping for host candidate is // requested, but the host candidate type is disabled. @@ -97,10 +103,12 @@ var ( // requested, but the srflx candidate type is disabled. ErrIneffectiveNAT1To1IPMappingSrflx = errors.New("1:1 NAT IP mapping for srflx candidate ineffective") - // ErrInvalidMulticastDNSHostName indicates an invalid MulticastDNSHostName - ErrInvalidMulticastDNSHostName = errors.New("invalid mDNS HostName, must end with .local and can only contain a single '.'") + // ErrInvalidMulticastDNSHostName indicates an invalid MulticastDNSHostName. + ErrInvalidMulticastDNSHostName = errors.New( + "invalid mDNS HostName, must end with .local and can only contain a single '.'", + ) - // ErrRunCanceled indicates a run operation was canceled by its individual done + // ErrRunCanceled indicates a run operation was canceled by its individual done. ErrRunCanceled = errors.New("run was canceled by done") // ErrTCPRemoteAddrAlreadyExists indicates we already have the connection with same remote addr. @@ -109,7 +117,7 @@ var ( // ErrUnknownCandidateTyp indicates that a candidate had a unknown type value. ErrUnknownCandidateTyp = errors.New("unknown candidate typ") - // ErrDetermineNetworkType indicates that the NetworkType was not able to be parsed + // ErrDetermineNetworkType indicates that the NetworkType was not able to be parsed. ErrDetermineNetworkType = errors.New("unable to determine networkType") errAttributeTooShortICECandidate = errors.New("attribute not long enough to be ICE candidate") @@ -121,20 +129,23 @@ var ( errNotImplemented = errors.New("not implemented yet") errNoUDPMuxAvailable = errors.New("no UDP mux is available") errNoXorAddrMapping = errors.New("no address mapping") + errParseFoundation = errors.New("failed to parse foundation") errParseComponent = errors.New("failed to parse component") errParsePort = errors.New("failed to parse port") errParsePriority = errors.New("failed to parse priority") errParseRelatedAddr = errors.New("failed to parse related addresses") + errParseExtension = errors.New("failed to parse extension") errParseTCPType = errors.New("failed to parse TCP type") - errRead = errors.New("failed to read") errUDPMuxDisabled = errors.New("UDPMux is not enabled") errUnknownRole = errors.New("unknown role") errWrite = errors.New("failed to write") errWriteSTUNMessage = errors.New("failed to send STUN message") errWriteSTUNMessageToIceConn = errors.New("failed to write STUN message to ICE connection") errXORMappedAddrTimeout = errors.New("timeout while waiting for XORMappedAddr") + errFailedToCastUDPAddr = errors.New("failed to cast net.Addr to net.UDPAddr") + errInvalidIPAddress = errors.New("invalid ip address") // UDPMuxDefault should not listen on unspecified address, but to keep backward compatibility, don't return error now. // will be used in the future. - // errListenUnspecified = errors.New("can't listen on unspecified address") + // errListenUnspecified = errors.New("can't listen on unspecified address"). ) diff --git a/examples/ping-pong/main.go b/examples/ping-pong/main.go index 1d9574d..5bb5ae4 100644 --- a/examples/ping-pong/main.go +++ b/examples/ping-pong/main.go @@ -14,7 +14,7 @@ import ( "os" "time" - "github.com/pion/ice/v3" + "github.com/pion/ice/v4" "github.com/pion/randutil" ) @@ -26,7 +26,7 @@ var ( localHTTPPort, remoteHTTPPort int ) -// HTTP Listener to get ICE Credentials from remote Peer +// HTTP Listener to get ICE Credentials from remote Peer. func remoteAuth(_ http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { panic(err) @@ -36,7 +36,7 @@ func remoteAuth(_ http.ResponseWriter, r *http.Request) { remoteAuthChannel <- r.PostForm["pwd"][0] } -// HTTP Listener to get ICE Candidate from remote Peer +// HTTP Listener to get ICE Candidate from remote Peer. func remoteCandidate(_ http.ResponseWriter, r *http.Request) { if err := r.ParseForm(); err != nil { panic(err) diff --git a/external_ip_mapper.go b/external_ip_mapper.go index 3d542fb..2d483ff 100644 --- a/external_ip_mapper.go +++ b/external_ip_mapper.go @@ -13,10 +13,13 @@ func validateIPString(ipStr string) (net.IP, bool, error) { if ip == nil { return nil, false, ErrInvalidNAT1To1IPMapping } + return ip, (ip.To4() != nil), nil } -// ipMapping holds the mapping of local and external IP address for a particular IP family +// ipMapping holds the mapping of local and external IP address +// +// for a particular IP family. type ipMapping struct { ipSole net.IP // When non-nil, this is the sole external IP for one local IP assumed ipMap map[string]net.IP // Local-to-external IP mapping (k: local, v: external) @@ -75,7 +78,11 @@ type externalIPMapper struct { candidateType CandidateType } -func newExternalIPMapper(candidateType CandidateType, ips []string) (*externalIPMapper, error) { //nolint:gocognit +//nolint:gocognit,cyclop +func newExternalIPMapper( + candidateType CandidateType, + ips []string, +) (*externalIPMapper, error) { if len(ips) == 0 { return nil, nil //nolint:nilnil } @@ -85,7 +92,7 @@ func newExternalIPMapper(candidateType CandidateType, ips []string) (*externalIP return nil, ErrUnsupportedNAT1To1IPCandidateType } - m := &externalIPMapper{ + mapper := &externalIPMapper{ ipv4Mapping: ipMapping{ipMap: map[string]net.IP{}}, ipv6Mapping: ipMapping{ipMap: map[string]net.IP{}}, candidateType: candidateType, @@ -101,13 +108,13 @@ func newExternalIPMapper(candidateType CandidateType, ips []string) (*externalIP if err != nil { return nil, err } - if len(ipPair) == 1 { + if len(ipPair) == 1 { //nolint:nestif if isExtIPv4 { - if err := m.ipv4Mapping.setSoleIP(extIP); err != nil { + if err := mapper.ipv4Mapping.setSoleIP(extIP); err != nil { return nil, err } } else { - if err := m.ipv6Mapping.setSoleIP(extIP); err != nil { + if err := mapper.ipv6Mapping.setSoleIP(extIP); err != nil { return nil, err } } @@ -121,7 +128,7 @@ func newExternalIPMapper(candidateType CandidateType, ips []string) (*externalIP return nil, ErrInvalidNAT1To1IPMapping } - if err := m.ipv4Mapping.addIPMapping(locIP, extIP); err != nil { + if err := mapper.ipv4Mapping.addIPMapping(locIP, extIP); err != nil { return nil, err } } else { @@ -129,14 +136,14 @@ func newExternalIPMapper(candidateType CandidateType, ips []string) (*externalIP return nil, ErrInvalidNAT1To1IPMapping } - if err := m.ipv6Mapping.addIPMapping(locIP, extIP); err != nil { + if err := mapper.ipv6Mapping.addIPMapping(locIP, extIP); err != nil { return nil, err } } } } - return m, nil + return mapper, nil } func (m *externalIPMapper) findExternalIP(localIPStr string) (net.IP, error) { diff --git a/external_ip_mapper_test.go b/external_ip_mapper_test.go index dbe39ad..bedf484 100644 --- a/external_ip_mapper_test.go +++ b/external_ip_mapper_test.go @@ -7,189 +7,189 @@ import ( "net" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestExternalIPMapper(t *testing.T) { +func TestExternalIPMapper(t *testing.T) { //nolint:maintidx t.Run("validateIPString", func(t *testing.T) { var ip net.IP var isIPv4 bool var err error ip, isIPv4, err = validateIPString("1.2.3.4") - assert.NoError(t, err, "should succeed") - assert.True(t, isIPv4, "should be true") - assert.Equal(t, "1.2.3.4", ip.String(), "should be true") + require.NoError(t, err, "should succeed") + require.True(t, isIPv4, "should be true") + require.Equal(t, "1.2.3.4", ip.String(), "should be true") ip, isIPv4, err = validateIPString("2601:4567::5678") - assert.NoError(t, err, "should succeed") - assert.False(t, isIPv4, "should be false") - assert.Equal(t, "2601:4567::5678", ip.String(), "should be true") + require.NoError(t, err, "should succeed") + require.False(t, isIPv4, "should be false") + require.Equal(t, "2601:4567::5678", ip.String(), "should be true") _, _, err = validateIPString("bad.6.6.6") - assert.Error(t, err, "should fail") + require.Error(t, err, "should fail") }) t.Run("newExternalIPMapper", func(t *testing.T) { - var m *externalIPMapper + var mapper *externalIPMapper var err error // ips being nil should succeed but mapper will be nil also - m, err = newExternalIPMapper(CandidateTypeUnspecified, nil) - assert.NoError(t, err, "should succeed") - assert.Nil(t, m, "should be nil") + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, nil) + require.NoError(t, err, "should succeed") + require.Nil(t, mapper, "should be nil") // ips being empty should succeed but mapper will still be nil - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{}) - assert.NoError(t, err, "should succeed") - assert.Nil(t, m, "should be nil") + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{}) + require.NoError(t, err, "should succeed") + require.Nil(t, mapper, "should be nil") // IPv4 with no explicit local IP, defaults to CandidateTypeHost - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "1.2.3.4", }) - assert.NoError(t, err, "should succeed") - assert.NotNil(t, m, "should not be nil") - assert.Equal(t, CandidateTypeHost, m.candidateType, "should match") - assert.NotNil(t, m.ipv4Mapping.ipSole) - assert.Nil(t, m.ipv6Mapping.ipSole) - assert.Equal(t, 0, len(m.ipv4Mapping.ipMap), "should match") - assert.Equal(t, 0, len(m.ipv6Mapping.ipMap), "should match") + require.NoError(t, err, "should succeed") + require.NotNil(t, mapper, "should not be nil") + require.Equal(t, CandidateTypeHost, mapper.candidateType, "should match") + require.NotNil(t, mapper.ipv4Mapping.ipSole) + require.Nil(t, mapper.ipv6Mapping.ipSole) + require.Equal(t, 0, len(mapper.ipv4Mapping.ipMap), "should match") + require.Equal(t, 0, len(mapper.ipv6Mapping.ipMap), "should match") // IPv4 with no explicit local IP, using CandidateTypeServerReflexive - m, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ + mapper, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ "1.2.3.4", }) - assert.NoError(t, err, "should succeed") - assert.NotNil(t, m, "should not be nil") - assert.Equal(t, CandidateTypeServerReflexive, m.candidateType, "should match") - assert.NotNil(t, m.ipv4Mapping.ipSole) - assert.Nil(t, m.ipv6Mapping.ipSole) - assert.Equal(t, 0, len(m.ipv4Mapping.ipMap), "should match") - assert.Equal(t, 0, len(m.ipv6Mapping.ipMap), "should match") + require.NoError(t, err, "should succeed") + require.NotNil(t, mapper, "should not be nil") + require.Equal(t, CandidateTypeServerReflexive, mapper.candidateType, "should match") + require.NotNil(t, mapper.ipv4Mapping.ipSole) + require.Nil(t, mapper.ipv6Mapping.ipSole) + require.Equal(t, 0, len(mapper.ipv4Mapping.ipMap), "should match") + require.Equal(t, 0, len(mapper.ipv6Mapping.ipMap), "should match") // IPv4 with no explicit local IP, defaults to CandidateTypeHost - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "2601:4567::5678", }) - assert.NoError(t, err, "should succeed") - assert.NotNil(t, m, "should not be nil") - assert.Equal(t, CandidateTypeHost, m.candidateType, "should match") - assert.Nil(t, m.ipv4Mapping.ipSole) - assert.NotNil(t, m.ipv6Mapping.ipSole) - assert.Equal(t, 0, len(m.ipv4Mapping.ipMap), "should match") - assert.Equal(t, 0, len(m.ipv6Mapping.ipMap), "should match") + require.NoError(t, err, "should succeed") + require.NotNil(t, mapper, "should not be nil") + require.Equal(t, CandidateTypeHost, mapper.candidateType, "should match") + require.Nil(t, mapper.ipv4Mapping.ipSole) + require.NotNil(t, mapper.ipv6Mapping.ipSole) + require.Equal(t, 0, len(mapper.ipv4Mapping.ipMap), "should match") + require.Equal(t, 0, len(mapper.ipv6Mapping.ipMap), "should match") // IPv4 and IPv6 in the mix - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "1.2.3.4", "2601:4567::5678", }) - assert.NoError(t, err, "should succeed") - assert.NotNil(t, m, "should not be nil") - assert.Equal(t, CandidateTypeHost, m.candidateType, "should match") - assert.NotNil(t, m.ipv4Mapping.ipSole) - assert.NotNil(t, m.ipv6Mapping.ipSole) - assert.Equal(t, 0, len(m.ipv4Mapping.ipMap), "should match") - assert.Equal(t, 0, len(m.ipv6Mapping.ipMap), "should match") + require.NoError(t, err, "should succeed") + require.NotNil(t, mapper, "should not be nil") + require.Equal(t, CandidateTypeHost, mapper.candidateType, "should match") + require.NotNil(t, mapper.ipv4Mapping.ipSole) + require.NotNil(t, mapper.ipv6Mapping.ipSole) + require.Equal(t, 0, len(mapper.ipv4Mapping.ipMap), "should match") + require.Equal(t, 0, len(mapper.ipv6Mapping.ipMap), "should match") // Unsupported candidate type - CandidateTypePeerReflexive - m, err = newExternalIPMapper(CandidateTypePeerReflexive, []string{ + mapper, err = newExternalIPMapper(CandidateTypePeerReflexive, []string{ "1.2.3.4", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") // Unsupported candidate type - CandidateTypeRelay - m, err = newExternalIPMapper(CandidateTypePeerReflexive, []string{ + mapper, err = newExternalIPMapper(CandidateTypePeerReflexive, []string{ "1.2.3.4", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") // Cannot duplicate mapping IPv4 family - m, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ + mapper, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ "1.2.3.4", "5.6.7.8", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") // Cannot duplicate mapping IPv6 family - m, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ + mapper, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ "2201::1", "2201::0002", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") // Invalide external IP string - m, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ + mapper, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ "bad.2.3.4", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") // Invalide local IP string - m, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ + mapper, err = newExternalIPMapper(CandidateTypeServerReflexive, []string{ "1.2.3.4/10.0.0.bad", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") }) t.Run("newExternalIPMapper with explicit local IP", func(t *testing.T) { - var m *externalIPMapper + var mapper *externalIPMapper var err error // IPv4 with explicit local IP, defaults to CandidateTypeHost - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "1.2.3.4/10.0.0.1", }) - assert.NoError(t, err, "should succeed") - assert.NotNil(t, m, "should not be nil") - assert.Equal(t, CandidateTypeHost, m.candidateType, "should match") - assert.Nil(t, m.ipv4Mapping.ipSole) - assert.Nil(t, m.ipv6Mapping.ipSole) - assert.Equal(t, 1, len(m.ipv4Mapping.ipMap), "should match") - assert.Equal(t, 0, len(m.ipv6Mapping.ipMap), "should match") + require.NoError(t, err, "should succeed") + require.NotNil(t, mapper, "should not be nil") + require.Equal(t, CandidateTypeHost, mapper.candidateType, "should match") + require.Nil(t, mapper.ipv4Mapping.ipSole) + require.Nil(t, mapper.ipv6Mapping.ipSole) + require.Equal(t, 1, len(mapper.ipv4Mapping.ipMap), "should match") + require.Equal(t, 0, len(mapper.ipv6Mapping.ipMap), "should match") // Cannot assign two ext IPs for one local IPv4 - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "1.2.3.4/10.0.0.1", "1.2.3.5/10.0.0.1", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") // Cannot assign two ext IPs for one local IPv6 - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "2200::1/fe80::1", "2200::0002/fe80::1", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") // Cannot mix different IP family in a pair (1) - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "2200::1/10.0.0.1", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") // Cannot mix different IP family in a pair (2) - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "1.2.3.4/fe80::1", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") // Invalid pair - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "1.2.3.4/192.168.0.2/10.0.0.1", }) - assert.Error(t, err, "should fail") - assert.Nil(t, m, "should be nil") + require.Error(t, err, "should fail") + require.Nil(t, mapper, "should be nil") }) t.Run("newExternalIPMapper with implicit and explicit local IP", func(t *testing.T) { @@ -198,112 +198,112 @@ func TestExternalIPMapper(t *testing.T) { "1.2.3.4", "1.2.3.5/10.0.0.1", }) - assert.Error(t, err, "should fail") + require.Error(t, err, "should fail") // Mixing implicit and explicit local IPs not allowed _, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "1.2.3.5/10.0.0.1", "1.2.3.4", }) - assert.Error(t, err, "should fail") + require.Error(t, err, "should fail") }) t.Run("findExternalIP without explicit local IP", func(t *testing.T) { - var m *externalIPMapper + var mapper *externalIPMapper var err error var extIP net.IP // IPv4 with explicit local IP, defaults to CandidateTypeHost - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "1.2.3.4", "2200::1", }) - assert.NoError(t, err, "should succeed") - assert.NotNil(t, m, "should not be nil") - assert.NotNil(t, m.ipv4Mapping.ipSole) - assert.NotNil(t, m.ipv6Mapping.ipSole) + require.NoError(t, err, "should succeed") + require.NotNil(t, mapper, "should not be nil") + require.NotNil(t, mapper.ipv4Mapping.ipSole) + require.NotNil(t, mapper.ipv6Mapping.ipSole) // Find external IPv4 - extIP, err = m.findExternalIP("10.0.0.1") - assert.NoError(t, err, "should succeed") - assert.Equal(t, "1.2.3.4", extIP.String(), "should match") + extIP, err = mapper.findExternalIP("10.0.0.1") + require.NoError(t, err, "should succeed") + require.Equal(t, "1.2.3.4", extIP.String(), "should match") // Find external IPv6 - extIP, err = m.findExternalIP("fe80::0001") // Use '0001' instead of '1' on purpose - assert.NoError(t, err, "should succeed") - assert.Equal(t, "2200::1", extIP.String(), "should match") + extIP, err = mapper.findExternalIP("fe80::0001") // Use '0001' instead of '1' on purpose + require.NoError(t, err, "should succeed") + require.Equal(t, "2200::1", extIP.String(), "should match") // Bad local IP string - _, err = m.findExternalIP("really.bad") - assert.Error(t, err, "should fail") + _, err = mapper.findExternalIP("really.bad") + require.Error(t, err, "should fail") }) t.Run("findExternalIP with explicit local IP", func(t *testing.T) { - var m *externalIPMapper + var mapper *externalIPMapper var err error var extIP net.IP // IPv4 with explicit local IP, defaults to CandidateTypeHost - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "1.2.3.4/10.0.0.1", "1.2.3.5/10.0.0.2", "2200::1/fe80::1", "2200::2/fe80::2", }) - assert.NoError(t, err, "should succeed") - assert.NotNil(t, m, "should not be nil") + require.NoError(t, err, "should succeed") + require.NotNil(t, mapper, "should not be nil") // Find external IPv4 - extIP, err = m.findExternalIP("10.0.0.1") - assert.NoError(t, err, "should succeed") - assert.Equal(t, "1.2.3.4", extIP.String(), "should match") + extIP, err = mapper.findExternalIP("10.0.0.1") + require.NoError(t, err, "should succeed") + require.Equal(t, "1.2.3.4", extIP.String(), "should match") - extIP, err = m.findExternalIP("10.0.0.2") - assert.NoError(t, err, "should succeed") - assert.Equal(t, "1.2.3.5", extIP.String(), "should match") + extIP, err = mapper.findExternalIP("10.0.0.2") + require.NoError(t, err, "should succeed") + require.Equal(t, "1.2.3.5", extIP.String(), "should match") - _, err = m.findExternalIP("10.0.0.3") - assert.Error(t, err, "should fail") + _, err = mapper.findExternalIP("10.0.0.3") + require.Error(t, err, "should fail") // Find external IPv6 - extIP, err = m.findExternalIP("fe80::0001") // Use '0001' instead of '1' on purpose - assert.NoError(t, err, "should succeed") - assert.Equal(t, "2200::1", extIP.String(), "should match") + extIP, err = mapper.findExternalIP("fe80::0001") // Use '0001' instead of '1' on purpose + require.NoError(t, err, "should succeed") + require.Equal(t, "2200::1", extIP.String(), "should match") - extIP, err = m.findExternalIP("fe80::0002") // Use '0002' instead of '2' on purpose - assert.NoError(t, err, "should succeed") - assert.Equal(t, "2200::2", extIP.String(), "should match") + extIP, err = mapper.findExternalIP("fe80::0002") // Use '0002' instead of '2' on purpose + require.NoError(t, err, "should succeed") + require.Equal(t, "2200::2", extIP.String(), "should match") - _, err = m.findExternalIP("fe80::3") - assert.Error(t, err, "should fail") + _, err = mapper.findExternalIP("fe80::3") + require.Error(t, err, "should fail") // Bad local IP string - _, err = m.findExternalIP("really.bad") - assert.Error(t, err, "should fail") + _, err = mapper.findExternalIP("really.bad") + require.Error(t, err, "should fail") }) t.Run("findExternalIP with empty map", func(t *testing.T) { - var m *externalIPMapper + var mapper *externalIPMapper var err error - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "1.2.3.4", }) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") // Attempt to find IPv6 that does not exist in the map - extIP, err := m.findExternalIP("fe80::1") - assert.NoError(t, err, "should succeed") - assert.Equal(t, "fe80::1", extIP.String(), "should match") + extIP, err := mapper.findExternalIP("fe80::1") + require.NoError(t, err, "should succeed") + require.Equal(t, "fe80::1", extIP.String(), "should match") - m, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ + mapper, err = newExternalIPMapper(CandidateTypeUnspecified, []string{ "2200::1", }) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") // Attempt to find IPv4 that does not exist in the map - extIP, err = m.findExternalIP("10.0.0.1") - assert.NoError(t, err, "should succeed") - assert.Equal(t, "10.0.0.1", extIP.String(), "should match") + extIP, err = mapper.findExternalIP("10.0.0.1") + require.NoError(t, err, "should succeed") + require.Equal(t, "10.0.0.1", extIP.String(), "should match") }) } diff --git a/gather.go b/gather.go index 8d2ce62..ebf2999 100644 --- a/gather.go +++ b/gather.go @@ -9,26 +9,23 @@ import ( "fmt" "io" "net" + "net/netip" "reflect" "sync" - "time" - "github.com/pion/dtls/v2" - "github.com/pion/ice/v3/internal/fakenet" - stunx "github.com/pion/ice/v3/internal/stun" + "github.com/pion/dtls/v3" + "github.com/pion/ice/v4/internal/fakenet" + stunx "github.com/pion/ice/v4/internal/stun" "github.com/pion/logging" - "github.com/pion/stun/v2" - "github.com/pion/turn/v3" + "github.com/pion/stun/v3" + "github.com/pion/turn/v4" ) -const ( - stunGatherTimeout = time.Second * 5 -) - -// Close a net.Conn and log if we have a failure -func closeConnAndLog(c io.Closer, log logging.LeveledLogger, msg string, args ...interface{}) { +// Close a net.Conn and log if we have a failure. +func closeConnAndLog(c io.Closer, log logging.LeveledLogger, msg string, args ...any) { if c == nil || (reflect.ValueOf(c).Kind() == reflect.Ptr && reflect.ValueOf(c).IsNil()) { log.Warnf("Connection is not allocated: "+msg, args...) + return } @@ -42,12 +39,14 @@ func closeConnAndLog(c io.Closer, log logging.LeveledLogger, msg string, args .. func (a *Agent) GatherCandidates() error { var gatherErr error - if runErr := a.run(a.context(), func(ctx context.Context, agent *Agent) { + if runErr := a.loop.Run(a.loop, func(ctx context.Context) { if a.gatheringState != GatheringStateNew { gatherErr = ErrMultipleGatherAttempted + return } else if a.onCandidateHdlr.Load() == nil { gatherErr = ErrNoOnCandidateHandler + return } @@ -61,13 +60,15 @@ func (a *Agent) GatherCandidates() error { }); runErr != nil { return runErr } + return gatherErr } -func (a *Agent) gatherCandidates(ctx context.Context, done chan struct{}) { +func (a *Agent) gatherCandidates(ctx context.Context, done chan struct{}) { //nolint:cyclop defer close(done) if err := a.setGatheringState(GatheringStateGathering); err != nil { //nolint:contextcheck a.log.Warnf("Failed to set gatheringState to GatheringStateGathering: %v", err) + return } @@ -115,7 +116,8 @@ func (a *Agent) gatherCandidates(ctx context.Context, done chan struct{}) { } } -func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []NetworkType) { //nolint:gocognit +//nolint:gocognit,gocyclo,cyclop +func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []NetworkType) { networks := map[string]struct{}{} for _, networkType := range networkTypes { if networkType.IsTCP() { @@ -133,25 +135,40 @@ func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []Networ delete(networks, udp) } - localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, networkTypes, a.includeLoopback) + _, localAddrs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, networkTypes, a.includeLoopback) if err != nil { a.log.Warnf("Failed to iterate local interfaces, host candidates will not be gathered %s", err) + return } - for _, ip := range localIPs { - mappedIP := ip - if a.mDNSMode != MulticastDNSModeQueryAndGather && a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeHost { - if _mappedIP, innerErr := a.extIPMapper.findExternalIP(ip.String()); innerErr == nil { - mappedIP = _mappedIP + for _, addr := range localAddrs { + mappedIP := addr + if a.mDNSMode != MulticastDNSModeQueryAndGather && + a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeHost { + if _mappedIP, innerErr := a.extIPMapper.findExternalIP(addr.String()); innerErr == nil { + conv, ok := netip.AddrFromSlice(_mappedIP) + if !ok { + a.log.Warnf("failed to convert mapped external IP to netip.Addr'%s'", addr.String()) + + continue + } + // we'd rather have an IPv4-mapped IPv6 become IPv4 so that it is usable + mappedIP = conv.Unmap() } else { - a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s", ip.String()) + a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s", addr.String()) } } address := mappedIP.String() + var isLocationTracked bool if a.mDNSMode == MulticastDNSModeQueryAndGather { address = a.mDNSName + } else { + // Here, we are not doing multicast gathering, so we will need to skip this address so + // that we don't accidentally reveal location tracking information. Otherwise, the + // case above hides the IP behind an mDNS address. + isLocationTracked = shouldFilterLocationTrackedIP(mappedIP) } for network := range networks { @@ -174,16 +191,20 @@ func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []Networ var muxConns []net.PacketConn if multi, ok := a.tcpMux.(AllConnsGetter); ok { a.log.Debugf("GetAllConns by ufrag: %s", a.localUfrag) - muxConns, err = multi.GetAllConns(a.localUfrag, mappedIP.To4() == nil, ip) + // Note: this is missing zone for IPv6 by just grabbing the IP slice + muxConns, err = multi.GetAllConns(a.localUfrag, mappedIP.Is6(), addr.AsSlice()) if err != nil { - a.log.Warnf("Failed to get all TCP connections by ufrag: %s %s %s", network, ip, a.localUfrag) + a.log.Warnf("Failed to get all TCP connections by ufrag: %s %s %s", network, addr, a.localUfrag) + continue } } else { a.log.Debugf("GetConn by ufrag: %s", a.localUfrag) - conn, err := a.tcpMux.GetConnByUfrag(a.localUfrag, mappedIP.To4() == nil, ip) + // Note: this is missing zone for IPv6 by just grabbing the IP slice + conn, err := a.tcpMux.GetConnByUfrag(a.localUfrag, mappedIP.Is6(), addr.AsSlice()) if err != nil { - a.log.Warnf("Failed to get TCP connections by ufrag: %s %s %s", network, ip, a.localUfrag) + a.log.Warnf("Failed to get TCP connections by ufrag: %s %s %s", network, addr, a.localUfrag) + continue } muxConns = []net.PacketConn{conn} @@ -194,7 +215,7 @@ func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []Networ if tcpConn, ok := conn.LocalAddr().(*net.TCPAddr); ok { conns = append(conns, connAndPort{conn, tcpConn.Port}) } else { - a.log.Warnf("Failed to get port of connection from TCPMux: %s %s %s", network, ip, a.localUfrag) + a.log.Warnf("Failed to get port of connection from TCPMux: %s %s %s", network, addr, a.localUfrag) } } if len(conns) == 0 { @@ -205,16 +226,22 @@ func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []Networ // Is there a way to verify that the listen address is even // accessible from the current interface. case udp: - conn, err := listenUDPInPortRange(a.net, a.log, int(a.portMax), int(a.portMin), network, &net.UDPAddr{IP: ip, Port: 0}) + conn, err := listenUDPInPortRange(a.net, a.log, int(a.portMax), int(a.portMin), network, &net.UDPAddr{ + IP: addr.AsSlice(), + Port: 0, + Zone: addr.Zone(), + }) if err != nil { - a.log.Warnf("Failed to listen %s %s", network, ip) + a.log.Warnf("Failed to listen %s %s", network, addr) + continue } if udpConn, ok := conn.LocalAddr().(*net.UDPAddr); ok { conns = append(conns, connAndPort{conn, udpConn.Port}) } else { - a.log.Warnf("Failed to get port of UDPAddr from ListenUDPInPortRange: %s %s %s", network, ip, a.localUfrag) + a.log.Warnf("Failed to get port of UDPAddr from ListenUDPInPortRange: %s %s %s", network, addr, a.localUfrag) + continue } } @@ -226,23 +253,43 @@ func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []Networ Port: connAndPort.port, Component: ComponentRTP, TCPType: tcpType, + // we will still process this candidate so that we start up the right + // listeners. + IsLocationTracked: isLocationTracked, } - c, err := NewCandidateHost(&hostConfig) + candidateHost, err := NewCandidateHost(&hostConfig) if err != nil { - closeConnAndLog(connAndPort.conn, a.log, "failed to create host candidate: %s %s %d: %v", network, mappedIP, connAndPort.port, err) + closeConnAndLog( + connAndPort.conn, + a.log, + "failed to create host candidate: %s %s %d: %v", + network, mappedIP, + connAndPort.port, + err, + ) + continue } if a.mDNSMode == MulticastDNSModeQueryAndGather { - if err = c.setIP(ip); err != nil { - closeConnAndLog(connAndPort.conn, a.log, "failed to create host candidate: %s %s %d: %v", network, mappedIP, connAndPort.port, err) + if err = candidateHost.setIPAddr(addr); err != nil { + closeConnAndLog( + connAndPort.conn, + a.log, + "failed to create host candidate: %s %s %d: %v", + network, + mappedIP, + connAndPort.port, + err, + ) + continue } } - if err := a.addCandidate(ctx, c, connAndPort.conn); err != nil { - if closeErr := c.close(); closeErr != nil { + if err := a.addCandidate(ctx, candidateHost, connAndPort.conn); err != nil { + if closeErr := candidateHost.close(); closeErr != nil { a.log.Warnf("Failed to close candidate: %v", closeErr) } a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v", err) @@ -252,7 +299,29 @@ func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []Networ } } -func (a *Agent) gatherCandidatesLocalUDPMux(ctx context.Context) error { //nolint:gocognit +// shouldFilterLocationTrackedIP returns if this candidate IP should be filtered out from +// any candidate publishing/notification for location tracking reasons. +func shouldFilterLocationTrackedIP(candidateIP netip.Addr) bool { + // https://tools.ietf.org/html/rfc8445#section-5.1.1.1 + // Similarly, when host candidates corresponding to + // an IPv6 address generated using a mechanism that prevents location + // tracking are gathered, then host candidates corresponding to IPv6 + // link-local addresses [RFC4291] MUST NOT be gathered. + return candidateIP.Is6() && (candidateIP.IsLinkLocalUnicast() || candidateIP.IsLinkLocalMulticast()) +} + +// shouldFilterLocationTracked returns if this candidate IP should be filtered out from +// any candidate publishing/notification for location tracking reasons. +func shouldFilterLocationTracked(candidateIP net.IP) bool { + addr, ok := netip.AddrFromSlice(candidateIP) + if !ok { + return false + } + + return shouldFilterLocationTrackedIP(addr) +} + +func (a *Agent) gatherCandidatesLocalUDPMux(ctx context.Context) error { //nolint:gocognit,cyclop if a.udpMux == nil { return errUDPMuxDisabled } @@ -266,21 +335,44 @@ func (a *Agent) gatherCandidatesLocalUDPMux(ctx context.Context) error { //nolin return errInvalidAddress } candidateIP := udpAddr.IP - if a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeHost { + + if _, ok := a.udpMux.(*UDPMuxDefault); ok && !a.includeLoopback && candidateIP.IsLoopback() { + // Unlike MultiUDPMux Default, UDPMuxDefault doesn't have + // a separate param to include loopback, so we respect agent config + continue + } + + if a.mDNSMode != MulticastDNSModeQueryAndGather && + a.extIPMapper != nil && + a.extIPMapper.candidateType == CandidateTypeHost { mappedIP, err := a.extIPMapper.findExternalIP(candidateIP.String()) if err != nil { a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s", candidateIP.String()) + continue } candidateIP = mappedIP } + var address string + var isLocationTracked bool + if a.mDNSMode == MulticastDNSModeQueryAndGather { + address = a.mDNSName + } else { + address = candidateIP.String() + // Here, we are not doing multicast gathering, so we will need to skip this address so + // that we don't accidentally reveal location tracking information. Otherwise, the + // case above hides the IP behind an mDNS address. + isLocationTracked = shouldFilterLocationTracked(candidateIP) + } + hostConfig := CandidateHostConfig{ - Network: udp, - Address: candidateIP.String(), - Port: udpAddr.Port, - Component: ComponentRTP, + Network: udp, + Address: address, + Port: udpAddr.Port, + Component: ComponentRTP, + IsLocationTracked: isLocationTracked, } // Detect a duplicate candidate before calling addCandidate(). @@ -299,6 +391,7 @@ func (a *Agent) gatherCandidatesLocalUDPMux(ctx context.Context) error { //nolin c, err := NewCandidateHost(&hostConfig) if err != nil { closeConnAndLog(conn, a.log, "failed to create host mux candidate: %s %d: %v", candidateIP, udpAddr.Port, err) + continue } @@ -308,6 +401,7 @@ func (a *Agent) gatherCandidatesLocalUDPMux(ctx context.Context) error { //nolin } closeConnAndLog(conn, a.log, "failed to add candidate: %s %d: %v", candidateIP, udpAddr.Port, err) + continue } @@ -331,21 +425,37 @@ func (a *Agent) gatherCandidatesSrflxMapped(ctx context.Context, networkTypes [] go func() { defer wg.Done() - conn, err := listenUDPInPortRange(a.net, a.log, int(a.portMax), int(a.portMin), network, &net.UDPAddr{IP: nil, Port: 0}) + conn, err := listenUDPInPortRange( + a.net, + a.log, + int(a.portMax), + int(a.portMin), + network, + &net.UDPAddr{IP: nil, Port: 0}, + ) if err != nil { a.log.Warnf("Failed to listen %s: %v", network, err) + return } lAddr, ok := conn.LocalAddr().(*net.UDPAddr) if !ok { closeConnAndLog(conn, a.log, "1:1 NAT mapping is enabled but LocalAddr is not a UDPAddr") + return } mappedIP, err := a.extIPMapper.findExternalIP(lAddr.IP.String()) if err != nil { closeConnAndLog(conn, a.log, "1:1 NAT mapping is enabled but no external IP is found for %s", lAddr.IP.String()) + + return + } + + if shouldFilterLocationTracked(mappedIP) { + closeConnAndLog(conn, a.log, "external IP is somehow filtered for location tracking reasons %s", mappedIP) + return } @@ -364,6 +474,7 @@ func (a *Agent) gatherCandidatesSrflxMapped(ctx context.Context, networkTypes [] mappedIP.String(), lAddr.Port, err) + return } @@ -377,7 +488,8 @@ func (a *Agent) gatherCandidatesSrflxMapped(ctx context.Context, networkTypes [] } } -func (a *Agent) gatherCandidatesSrflxUDPMux(ctx context.Context, urls []*stun.URI, networkTypes []NetworkType) { //nolint:gocognit +//nolint:gocognit,cyclop +func (a *Agent) gatherCandidatesSrflxUDPMux(ctx context.Context, urls []*stun.URI, networkTypes []NetworkType) { var wg sync.WaitGroup defer wg.Wait() @@ -391,6 +503,7 @@ func (a *Agent) gatherCandidatesSrflxUDPMux(ctx context.Context, urls []*stun.UR udpAddr, ok := listenAddr.(*net.UDPAddr) if !ok { a.log.Warn("Failed to cast udpMuxSrflx listen address to UDPAddr") + continue } wg.Add(1) @@ -400,19 +513,28 @@ func (a *Agent) gatherCandidatesSrflxUDPMux(ctx context.Context, urls []*stun.UR hostPort := fmt.Sprintf("%s:%d", url.Host, url.Port) serverAddr, err := a.net.ResolveUDPAddr(network, hostPort) if err != nil { - a.log.Debugf("Failed to resolve STUN host: %s: %v", hostPort, err) + a.log.Debugf("Failed to resolve STUN host: %s %s: %v", network, hostPort, err) + return } - xorAddr, err := a.udpMuxSrflx.GetXORMappedAddr(serverAddr, stunGatherTimeout) + if shouldFilterLocationTracked(serverAddr.IP) { + a.log.Warnf("STUN host %s is somehow filtered for location tracking reasons", hostPort) + + return + } + + xorAddr, err := a.udpMuxSrflx.GetXORMappedAddr(serverAddr, a.stunGatherTimeout) if err != nil { a.log.Warnf("Failed get server reflexive address %s %s: %v", network, url, err) + return } conn, err := a.udpMuxSrflx.GetConnForURL(a.localUfrag, url.String(), localAddr) if err != nil { a.log.Warnf("Failed to find connection in UDPMuxSrflx %s %s: %v", network, url, err) + return } @@ -430,6 +552,7 @@ func (a *Agent) gatherCandidatesSrflxUDPMux(ctx context.Context, urls []*stun.UR c, err := NewCandidateServerReflexive(&srflxConfig) if err != nil { closeConnAndLog(conn, a.log, "failed to create server reflexive candidate: %s %s %d: %v", network, ip, port, err) + return } @@ -445,7 +568,8 @@ func (a *Agent) gatherCandidatesSrflxUDPMux(ctx context.Context, urls []*stun.UR } } -func (a *Agent) gatherCandidatesSrflx(ctx context.Context, urls []*stun.URI, networkTypes []NetworkType) { //nolint:gocognit +//nolint:cyclop,gocognit +func (a *Agent) gatherCandidatesSrflx(ctx context.Context, urls []*stun.URI, networkTypes []NetworkType) { var wg sync.WaitGroup defer wg.Wait() @@ -462,13 +586,28 @@ func (a *Agent) gatherCandidatesSrflx(ctx context.Context, urls []*stun.URI, net hostPort := fmt.Sprintf("%s:%d", url.Host, url.Port) serverAddr, err := a.net.ResolveUDPAddr(network, hostPort) if err != nil { - a.log.Debugf("Failed to resolve STUN host: %s: %v", hostPort, err) + a.log.Debugf("Failed to resolve STUN host: %s %s: %v", network, hostPort, err) + return } - conn, err := listenUDPInPortRange(a.net, a.log, int(a.portMax), int(a.portMin), network, &net.UDPAddr{IP: nil, Port: 0}) + if shouldFilterLocationTracked(serverAddr.IP) { + a.log.Warnf("STUN host %s is somehow filtered for location tracking reasons", hostPort) + + return + } + + conn, err := listenUDPInPortRange( + a.net, + a.log, + int(a.portMax), + int(a.portMin), + network, + &net.UDPAddr{IP: nil, Port: 0}, + ) if err != nil { closeConnAndLog(conn, a.log, "failed to listen for %s: %v", serverAddr.String(), err) + return } // If the agent closes midway through the connection @@ -479,14 +618,15 @@ func (a *Agent) gatherCandidatesSrflx(ctx context.Context, urls []*stun.URI, net select { case <-cancelCtx.Done(): return - case <-a.done: + case <-a.loop.Done(): _ = conn.Close() } }() - xorAddr, err := stunx.GetXORMappedAddr(conn, serverAddr, stunGatherTimeout) + xorAddr, err := stunx.GetXORMappedAddr(conn, serverAddr, a.stunGatherTimeout) if err != nil { closeConnAndLog(conn, a.log, "failed to get server reflexive address %s %s: %v", network, url, err) + return } @@ -505,6 +645,7 @@ func (a *Agent) gatherCandidatesSrflx(ctx context.Context, urls []*stun.URI, net c, err := NewCandidateServerReflexive(&srflxConfig) if err != nil { closeConnAndLog(conn, a.log, "failed to create server reflexive candidate: %s %s %d: %v", network, ip, port, err) + return } @@ -519,7 +660,8 @@ func (a *Agent) gatherCandidatesSrflx(ctx context.Context, urls []*stun.URI, net } } -func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { //nolint:gocognit +//nolint:maintidx,gocognit,gocyclo,cyclop +func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { var wg sync.WaitGroup defer wg.Wait() @@ -530,9 +672,11 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { / continue case urls[i].Username == "": a.log.Errorf("Failed to gather relay candidates: %v", ErrUsernameEmpty) + return case urls[i].Password == "": a.log.Errorf("Failed to gather relay candidates: %v", ErrPasswordEmpty) + return } @@ -552,6 +696,7 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { / case url.Proto == stun.ProtoTypeUDP && url.Scheme == stun.SchemeTypeTURN: if locConn, err = a.net.ListenPacket(network, "0.0.0.0:0"); err != nil { a.log.Warnf("Failed to listen %s: %v", network, err) + return } @@ -563,6 +708,7 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { / conn, connectErr := a.proxyDialer.Dial(NetworkTypeTCP4.String(), turnServerAddr) if connectErr != nil { a.log.Warnf("Failed to dial TCP address %s via proxy dialer: %v", turnServerAddr, connectErr) + return } @@ -579,12 +725,14 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { / tcpAddr, connectErr := a.net.ResolveTCPAddr(NetworkTypeTCP4.String(), turnServerAddr) if connectErr != nil { a.log.Warnf("Failed to resolve TCP address %s: %v", turnServerAddr, connectErr) + return } conn, connectErr := a.net.DialTCP(NetworkTypeTCP4.String(), nil, tcpAddr) if connectErr != nil { a.log.Warnf("Failed to dial TCP address %s: %v", turnServerAddr, connectErr) + return } @@ -596,38 +744,50 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { / udpAddr, connectErr := a.net.ResolveUDPAddr(network, turnServerAddr) if connectErr != nil { a.log.Warnf("Failed to resolve UDP address %s: %v", turnServerAddr, connectErr) + return } udpConn, dialErr := a.net.DialUDP("udp", nil, udpAddr) if dialErr != nil { a.log.Warnf("Failed to dial DTLS address %s: %v", turnServerAddr, dialErr) + return } - conn, connectErr := dtls.ClientWithContext(ctx, udpConn, &dtls.Config{ + conn, connectErr := dtls.Client(&fakenet.PacketConn{Conn: udpConn}, udpConn.RemoteAddr(), &dtls.Config{ ServerName: url.Host, InsecureSkipVerify: a.insecureSkipVerify, //nolint:gosec + LoggerFactory: a.loggerFactory, }) if connectErr != nil { a.log.Warnf("Failed to create DTLS client: %v", turnServerAddr, connectErr) + + return + } + + if connectErr = conn.HandshakeContext(ctx); connectErr != nil { + a.log.Warnf("Failed to create DTLS client: %v", turnServerAddr, connectErr) + return } relAddr = conn.LocalAddr().(*net.UDPAddr).IP.String() //nolint:forcetypeassert relPort = conn.LocalAddr().(*net.UDPAddr).Port //nolint:forcetypeassert - relayProtocol = "dtls" + relayProtocol = relayProtocolDTLS locConn = &fakenet.PacketConn{Conn: conn} case url.Proto == stun.ProtoTypeTCP && url.Scheme == stun.SchemeTypeTURNS: tcpAddr, resolvErr := a.net.ResolveTCPAddr(NetworkTypeTCP4.String(), turnServerAddr) if resolvErr != nil { a.log.Warnf("Failed to resolve relay address %s: %v", turnServerAddr, resolvErr) + return } tcpConn, dialErr := a.net.DialTCP(NetworkTypeTCP4.String(), nil, tcpAddr) if dialErr != nil { a.log.Warnf("Failed to connect to relay: %v", dialErr) + return } @@ -641,15 +801,17 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { / a.log.Errorf("Failed to close relay connection: %v", closeErr) } a.log.Warnf("Failed to connect to relay: %v", hsErr) + return } relAddr = conn.LocalAddr().(*net.TCPAddr).IP.String() //nolint:forcetypeassert relPort = conn.LocalAddr().(*net.TCPAddr).Port //nolint:forcetypeassert - relayProtocol = "tls" + relayProtocol = relayProtocolTLS locConn = turn.NewSTUNConn(conn) default: a.log.Warnf("Unable to handle URL in gatherCandidatesRelay %v", url) + return } @@ -663,12 +825,14 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { / }) if err != nil { closeConnAndLog(locConn, a.log, "failed to create new TURN client %s %s", turnServerAddr, err) + return } if err = client.Listen(); err != nil { client.Close() closeConnAndLog(locConn, a.log, "failed to listen on TURN client %s %s", turnServerAddr, err) + return } @@ -676,10 +840,18 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { / if err != nil { client.Close() closeConnAndLog(locConn, a.log, "failed to allocate on TURN client %s %s", turnServerAddr, err) + return } rAddr := relayConn.LocalAddr().(*net.UDPAddr) //nolint:forcetypeassert + + if shouldFilterLocationTracked(rAddr.IP) { + a.log.Warnf("TURN address %s is somehow filtered for location tracking reasons", rAddr.IP) + + return + } + relayConfig := CandidateRelayConfig{ Network: network, Component: ComponentRTP, @@ -690,6 +862,7 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { / RelayProtocol: relayProtocol, OnClose: func() error { client.Close() + return locConn.Close() }, } @@ -704,6 +877,7 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) { / client.Close() closeConnAndLog(locConn, a.log, "failed to create relay candidate: %s %s: %v", network, rAddr.String(), err) + return } diff --git a/gather_test.go b/gather_test.go index 07e31c9..6ef39e1 100644 --- a/gather_test.go +++ b/gather_test.go @@ -12,7 +12,6 @@ import ( "io" "net" "net/url" - "reflect" "sort" "strconv" "sync" @@ -20,41 +19,49 @@ import ( "testing" "time" - "github.com/pion/dtls/v2" - "github.com/pion/dtls/v2/pkg/crypto/selfsign" + "github.com/pion/dtls/v3" + "github.com/pion/dtls/v3/pkg/crypto/selfsign" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" - "github.com/pion/turn/v3" - "github.com/stretchr/testify/assert" + "github.com/pion/turn/v4" "github.com/stretchr/testify/require" "golang.org/x/net/proxy" ) func TestListenUDP(t *testing.T) { - a, err := NewAgent(&AgentConfig{}) - assert.NoError(t, err) + agent, err := NewAgent(&AgentConfig{}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() - localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{NetworkTypeUDP4}, false) - assert.NotEqual(t, len(localIPs), 0, "localInterfaces found no interfaces, unable to test") - assert.NoError(t, err) + _, localAddrs, err := localInterfaces( + agent.net, + agent.interfaceFilter, + agent.ipFilter, + []NetworkType{NetworkTypeUDP4}, + false, + ) + require.NotEqual(t, len(localAddrs), 0, "localInterfaces found no interfaces, unable to test") + require.NoError(t, err) - ip := localIPs[0] + ip := localAddrs[0].AsSlice() - conn, err := listenUDPInPortRange(a.net, a.log, 0, 0, udp, &net.UDPAddr{IP: ip, Port: 0}) - assert.NoError(t, err, "listenUDP error with no port restriction") - assert.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn") + conn, err := listenUDPInPortRange(agent.net, agent.log, 0, 0, udp, &net.UDPAddr{IP: ip, Port: 0}) + require.NoError(t, err, "listenUDP error with no port restriction") + require.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn") - _, err = listenUDPInPortRange(a.net, a.log, 4999, 5000, udp, &net.UDPAddr{IP: ip, Port: 0}) - assert.Equal(t, err, ErrPort, "listenUDP with invalid port range did not return ErrPort") + _, err = listenUDPInPortRange(agent.net, agent.log, 4999, 5000, udp, &net.UDPAddr{IP: ip, Port: 0}) + require.Equal(t, err, ErrPort, "listenUDP with invalid port range did not return ErrPort") - conn, err = listenUDPInPortRange(a.net, a.log, 5000, 5000, udp, &net.UDPAddr{IP: ip, Port: 0}) - assert.NoError(t, err, "listenUDP error with no port restriction") - assert.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn") + conn, err = listenUDPInPortRange(agent.net, agent.log, 5000, 5000, udp, &net.UDPAddr{IP: ip, Port: 0}) + require.NoError(t, err, "listenUDP error with no port restriction") + require.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn") _, port, err := net.SplitHostPort(conn.LocalAddr().String()) - assert.NoError(t, err) - assert.Equal(t, port, "5000", "listenUDP with port restriction of 5000 listened on incorrect port") + require.NoError(t, err) + require.Equal(t, port, "5000", "listenUDP with port restriction of 5000 listened on incorrect port") portMin := 5100 portMax := 5109 @@ -62,77 +69,75 @@ func TestListenUDP(t *testing.T) { result := make([]int, 0, total) portRange := make([]int, 0, total) for i := 0; i < total; i++ { - conn, err = listenUDPInPortRange(a.net, a.log, portMax, portMin, udp, &net.UDPAddr{IP: ip, Port: 0}) - assert.NoError(t, err, "listenUDP error with no port restriction") - assert.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn") + conn, err = listenUDPInPortRange(agent.net, agent.log, portMax, portMin, udp, &net.UDPAddr{IP: ip, Port: 0}) + require.NoError(t, err, "listenUDP error with no port restriction") + require.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn") _, port, err = net.SplitHostPort(conn.LocalAddr().String()) - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + p, _ := strconv.Atoi(port) - if p < portMin || p > portMax { - t.Fatalf("listenUDP with port restriction [%d, %d] listened on incorrect port (%s)", portMin, portMax, port) - } + require.False(t, p < portMin || p > portMax) result = append(result, p) portRange = append(portRange, portMin+i) } - if sort.IntsAreSorted(result) { - t.Fatalf("listenUDP with port restriction [%d, %d], ports result should be random", portMin, portMax) - } + require.False(t, sort.IntsAreSorted(result)) sort.Ints(result) - if !reflect.DeepEqual(result, portRange) { - t.Fatalf("listenUDP with port restriction [%d, %d], got:%v, want:%v", portMin, portMax, result, portRange) - } - _, err = listenUDPInPortRange(a.net, a.log, portMax, portMin, udp, &net.UDPAddr{IP: ip, Port: 0}) - assert.Equal(t, err, ErrPort, "listenUDP with port restriction [%d, %d], did not return ErrPort", portMin, portMax) - - assert.NoError(t, a.Close()) + require.Equal(t, result, portRange) + _, err = listenUDPInPortRange(agent.net, agent.log, portMax, portMin, udp, &net.UDPAddr{IP: ip, Port: 0}) + require.Equal(t, err, ErrPort, "listenUDP with port restriction [%d, %d], did not return ErrPort", portMin, portMax) } func TestGatherConcurrency(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, IncludeLoopback: true, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background()) - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(Candidate) { candidateGatheredFunc() })) // Testing for panic for i := 0; i < 10; i++ { - _ = a.GatherCandidates() + _ = agent.GatherCandidates() } <-candidateGathered.Done() - - assert.NoError(t, a.Close()) } func TestLoopbackCandidate(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() type testCase struct { name string agentConfig *AgentConfig loExpected bool } mux, err := NewMultiUDPMuxFromPort(12500) - assert.NoError(t, err) + require.NoError(t, err) muxWithLo, errlo := NewMultiUDPMuxFromPort(12501, UDPMuxFromPortWithLoopback()) - assert.NoError(t, errlo) + require.NoError(t, errlo) + + unspecConn, errconn := net.ListenPacket("udp", ":0") + require.NoError(t, errconn) + defer func() { + _ = unspecConn.Close() + }() + muxUnspecDefault := NewUDPMuxDefault(UDPMuxParams{ + UDPConn: unspecConn, + }) + testCases := []testCase{ { name: "mux should not have loopback candidate", @@ -150,6 +155,23 @@ func TestLoopbackCandidate(t *testing.T) { }, loExpected: true, }, + { + name: "UDPMuxDefault with unspecified IP should not have loopback candidate", + agentConfig: &AgentConfig{ + NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, + UDPMux: muxUnspecDefault, + }, + loExpected: false, + }, + { + name: "UDPMuxDefault with unspecified IP should respect agent includeloopback", + agentConfig: &AgentConfig{ + NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, + UDPMux: muxUnspecDefault, + IncludeLoopback: true, + }, + loExpected: true, + }, { name: "includeloopback enabled", agentConfig: &AgentConfig{ @@ -171,46 +193,48 @@ func TestLoopbackCandidate(t *testing.T) { for _, tc := range testCases { tcase := tc t.Run(tcase.name, func(t *testing.T) { - a, err := NewAgent(tc.agentConfig) - assert.NoError(t, err) + agent, err := NewAgent(tc.agentConfig) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background()) var loopback int32 - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(c Candidate) { if c != nil { if net.ParseIP(c.Address()).IsLoopback() { atomic.StoreInt32(&loopback, 1) } } else { candidateGatheredFunc() + return } t.Log(c.NetworkType(), c.Priority(), c) })) - assert.NoError(t, a.GatherCandidates()) + require.NoError(t, agent.GatherCandidates()) <-candidateGathered.Done() - assert.NoError(t, a.Close()) - assert.Equal(t, tcase.loExpected, atomic.LoadInt32(&loopback) == 1) + require.Equal(t, tcase.loExpected, atomic.LoadInt32(&loopback) == 1) }) } - assert.NoError(t, mux.Close()) - assert.NoError(t, muxWithLo.Close()) + require.NoError(t, mux.Close()) + require.NoError(t, muxWithLo.Close()) + require.NoError(t, muxUnspecDefault.Close()) } -// Assert that STUN gathering is done concurrently +// Assert that STUN gathering is done concurrently. func TestSTUNConcurrency(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() serverPort := randomPort(t) - serverListener, err := net.ListenPacket("udp4", "127.0.0.1:"+strconv.Itoa(serverPort)) - assert.NoError(t, err) + serverListener, err := net.ListenPacket("udp4", localhostIPStr+":"+strconv.Itoa(serverPort)) + require.NoError(t, err) server, err := turn.NewServer(turn.ServerConfig{ Realm: "pion.ly", @@ -218,23 +242,26 @@ func TestSTUNConcurrency(t *testing.T) { PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: serverListener, - RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"}, + RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: localhostIPStr}, }, }, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, server.Close()) + }() urls := []*stun.URI{} for i := 0; i <= 10; i++ { urls = append(urls, &stun.URI{ Scheme: stun.SchemeTypeSTUN, - Host: "127.0.0.1", + Host: localhostIPStr, Port: serverPort + 1, }) } urls = append(urls, &stun.URI{ Scheme: stun.SchemeTypeSTUN, - Host: "127.0.0.1", + Host: localhostIPStr, Port: serverPort, }) @@ -246,7 +273,7 @@ func TestSTUNConcurrency(t *testing.T) { _ = listener.Close() }() - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ NetworkTypes: supportedNetworkTypes(), Urls: urls, CandidateTypes: []CandidateType{CandidateTypeHost, CandidateTypeServerReflexive}, @@ -258,38 +285,43 @@ func TestSTUNConcurrency(t *testing.T) { }, ), }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background()) - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(c Candidate) { if c == nil { candidateGatheredFunc() + return } t.Log(c.NetworkType(), c.Priority(), c) })) - assert.NoError(t, a.GatherCandidates()) + require.NoError(t, agent.GatherCandidates()) <-candidateGathered.Done() - - assert.NoError(t, a.Close()) - assert.NoError(t, server.Close()) } -// Assert that TURN gathering is done concurrently +// Assert that TURN gathering is done concurrently. func TestTURNConcurrency(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() - runTest := func(protocol stun.ProtoType, scheme stun.SchemeType, packetConn net.PacketConn, listener net.Listener, serverPort int) { + runTest := func( + protocol stun.ProtoType, + scheme stun.SchemeType, + packetConn net.PacketConn, + listener net.Listener, + serverPort int, + ) { packetConnConfigs := []turn.PacketConnConfig{} if packetConn != nil { packetConnConfigs = append(packetConnConfigs, turn.PacketConnConfig{ PacketConn: packetConn, - RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"}, + RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: localhostIPStr}, }) } @@ -297,7 +329,7 @@ func TestTURNConcurrency(t *testing.T) { if listener != nil { listenerConfigs = append(listenerConfigs, turn.ListenerConfig{ Listener: listener, - RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"}, + RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: localhostIPStr}, }) } @@ -307,13 +339,16 @@ func TestTURNConcurrency(t *testing.T) { PacketConnConfigs: packetConnConfigs, ListenerConfigs: listenerConfigs, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, server.Close()) + }() urls := []*stun.URI{} for i := 0; i <= 10; i++ { urls = append(urls, &stun.URI{ Scheme: scheme, - Host: "127.0.0.1", + Host: localhostIPStr, Username: "username", Password: "password", Proto: protocol, @@ -322,89 +357,91 @@ func TestTURNConcurrency(t *testing.T) { } urls = append(urls, &stun.URI{ Scheme: scheme, - Host: "127.0.0.1", + Host: localhostIPStr, Username: "username", Password: "password", Proto: protocol, Port: serverPort, }) - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ CandidateTypes: []CandidateType{CandidateTypeRelay}, InsecureSkipVerify: true, NetworkTypes: supportedNetworkTypes(), Urls: urls, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background()) - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(c Candidate) { if c != nil { candidateGatheredFunc() } })) - assert.NoError(t, a.GatherCandidates()) + require.NoError(t, agent.GatherCandidates()) <-candidateGathered.Done() - - assert.NoError(t, a.Close()) - assert.NoError(t, server.Close()) } t.Run("UDP Relay", func(t *testing.T) { serverPort := randomPort(t) - serverListener, err := net.ListenPacket("udp", "127.0.0.1:"+strconv.Itoa(serverPort)) - assert.NoError(t, err) + serverListener, err := net.ListenPacket("udp", localhostIPStr+":"+strconv.Itoa(serverPort)) + require.NoError(t, err) runTest(stun.ProtoTypeUDP, stun.SchemeTypeTURN, serverListener, nil, serverPort) }) t.Run("TCP Relay", func(t *testing.T) { serverPort := randomPort(t) - serverListener, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(serverPort)) - assert.NoError(t, err) + serverListener, err := net.Listen("tcp", localhostIPStr+":"+strconv.Itoa(serverPort)) + require.NoError(t, err) runTest(stun.ProtoTypeTCP, stun.SchemeTypeTURN, nil, serverListener, serverPort) }) t.Run("TLS Relay", func(t *testing.T) { certificate, genErr := selfsign.GenerateSelfSigned() - assert.NoError(t, genErr) + require.NoError(t, genErr) serverPort := randomPort(t) - serverListener, err := tls.Listen("tcp", "127.0.0.1:"+strconv.Itoa(serverPort), &tls.Config{ //nolint:gosec + serverListener, err := tls.Listen("tcp", localhostIPStr+":"+strconv.Itoa(serverPort), &tls.Config{ //nolint:gosec Certificates: []tls.Certificate{certificate}, }) - assert.NoError(t, err) + require.NoError(t, err) runTest(stun.ProtoTypeTCP, stun.SchemeTypeTURNS, nil, serverListener, serverPort) }) t.Run("DTLS Relay", func(t *testing.T) { certificate, genErr := selfsign.GenerateSelfSigned() - assert.NoError(t, genErr) + require.NoError(t, genErr) serverPort := randomPort(t) - serverListener, err := dtls.Listen("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: serverPort}, &dtls.Config{ - Certificates: []tls.Certificate{certificate}, - }) - assert.NoError(t, err) + serverListener, err := dtls.Listen( + "udp", + &net.UDPAddr{IP: net.ParseIP(localhostIPStr), Port: serverPort}, + &dtls.Config{ + Certificates: []tls.Certificate{certificate}, + }, + ) + require.NoError(t, err) runTest(stun.ProtoTypeUDP, stun.SchemeTypeTURNS, nil, serverListener, serverPort) }) } -// Assert that STUN and TURN gathering are done concurrently +// Assert that STUN and TURN gathering are done concurrently. func TestSTUNTURNConcurrency(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 8) - defer lim.Stop() + defer test.TimeOut(time.Second * 8).Stop() serverPort := randomPort(t) - serverListener, err := net.ListenPacket("udp4", "127.0.0.1:"+strconv.Itoa(serverPort)) - assert.NoError(t, err) + serverListener, err := net.ListenPacket("udp4", localhostIPStr+":"+strconv.Itoa(serverPort)) + require.NoError(t, err) server, err := turn.NewServer(turn.ServerConfig{ Realm: "pion.ly", @@ -412,53 +449,56 @@ func TestSTUNTURNConcurrency(t *testing.T) { PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: serverListener, - RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"}, + RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: localhostIPStr}, }, }, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, server.Close()) + }() urls := []*stun.URI{} for i := 0; i <= 10; i++ { urls = append(urls, &stun.URI{ Scheme: stun.SchemeTypeSTUN, - Host: "127.0.0.1", + Host: localhostIPStr, Port: serverPort + 1, }) } urls = append(urls, &stun.URI{ Scheme: stun.SchemeTypeTURN, Proto: stun.ProtoTypeUDP, - Host: "127.0.0.1", + Host: localhostIPStr, Port: serverPort, Username: "username", Password: "password", }) - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ NetworkTypes: supportedNetworkTypes(), Urls: urls, CandidateTypes: []CandidateType{CandidateTypeServerReflexive, CandidateTypeRelay}, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() { - gatherLim := test.TimeOut(time.Second * 3) // As TURN and STUN should be checked in parallel, this should complete before the default STUN timeout (5s) + // As TURN and STUN should be checked in parallel, this should complete before the default STUN timeout (5s) + gatherLim := test.TimeOut(time.Second * 3) candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background()) - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(c Candidate) { if c != nil { candidateGatheredFunc() } })) - assert.NoError(t, a.GatherCandidates()) + require.NoError(t, agent.GatherCandidates()) <-candidateGathered.Done() - gatherLim.Stop() } - - assert.NoError(t, a.Close()) - assert.NoError(t, server.Close()) } // Assert that srflx candidates can be gathered from TURN servers @@ -468,15 +508,13 @@ func TestSTUNTURNConcurrency(t *testing.T) { // // https://tools.ietf.org/html/rfc5245#section-2.1 func TestTURNSrflx(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() serverPort := randomPort(t) - serverListener, err := net.ListenPacket("udp4", "127.0.0.1:"+strconv.Itoa(serverPort)) - assert.NoError(t, err) + serverListener, err := net.ListenPacket("udp4", localhostIPStr+":"+strconv.Itoa(serverPort)) + require.NoError(t, err) server, err := turn.NewServer(turn.ServerConfig{ Realm: "pion.ly", @@ -484,53 +522,57 @@ func TestTURNSrflx(t *testing.T) { PacketConnConfigs: []turn.PacketConnConfig{ { PacketConn: serverListener, - RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"}, + RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: localhostIPStr}, }, }, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, server.Close()) + }() urls := []*stun.URI{{ Scheme: stun.SchemeTypeTURN, Proto: stun.ProtoTypeUDP, - Host: "127.0.0.1", + Host: localhostIPStr, Port: serverPort, Username: "username", Password: "password", }} - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ NetworkTypes: supportedNetworkTypes(), Urls: urls, CandidateTypes: []CandidateType{CandidateTypeServerReflexive, CandidateTypeRelay}, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background()) - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(c Candidate) { if c != nil && c.Type() == CandidateTypeServerReflexive { candidateGatheredFunc() } })) - assert.NoError(t, a.GatherCandidates()) + require.NoError(t, agent.GatherCandidates()) <-candidateGathered.Done() - - assert.NoError(t, a.Close()) - assert.NoError(t, server.Close()) } func TestCloseConnLog(t *testing.T) { a, err := NewAgent(&AgentConfig{}) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, a.Close()) + }() closeConnAndLog(nil, a.log, "normal nil") var nc *net.UDPConn closeConnAndLog(nc, a.log, "nil ptr") - - assert.NoError(t, a.Close()) } type mockProxy struct { @@ -550,15 +592,14 @@ func (m *mockConn) SetWriteDeadline(time.Time) error { return io.EOF } func (m *mockProxy) Dial(string, string) (net.Conn, error) { m.proxyWasDialed() + return &mockConn{}, nil } func TestTURNProxyDialer(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() proxyWasDialed, proxyWasDialedFunc := context.WithCancel(context.Background()) proxy.RegisterDialerType("tcp", func(*url.URL, proxy.Dialer) (proxy.Dialer, error) { @@ -566,18 +607,18 @@ func TestTURNProxyDialer(t *testing.T) { }) tcpProxyURI, err := url.Parse("tcp://fakeproxy:3128") - assert.NoError(t, err) + require.NoError(t, err) proxyDialer, err := proxy.FromURL(tcpProxyURI, proxy.Direct) - assert.NoError(t, err) + require.NoError(t, err) - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ CandidateTypes: []CandidateType{CandidateTypeRelay}, NetworkTypes: supportedNetworkTypes(), Urls: []*stun.URI{ { Scheme: stun.SchemeTypeTURN, - Host: "127.0.0.1", + Host: localhostIPStr, Username: "username", Password: "password", Proto: stun.ProtoTypeTCP, @@ -586,33 +627,32 @@ func TestTURNProxyDialer(t *testing.T) { }, ProxyDialer: proxyDialer, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() candidateGatherFinish, candidateGatherFinishFunc := context.WithCancel(context.Background()) - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(c Candidate) { if c == nil { candidateGatherFinishFunc() } })) - assert.NoError(t, a.GatherCandidates()) + require.NoError(t, agent.GatherCandidates()) <-candidateGatherFinish.Done() <-proxyWasDialed.Done() - - assert.NoError(t, a.Close()) } -// TestUDPMuxDefaultWithNAT1To1IPsUsage asserts that candidates +// TestUDPMuxDefaultWithNAT1To1IPsUsage requires that candidates // are given and connections are valid when using UDPMuxDefault and NAT1To1IPs. func TestUDPMuxDefaultWithNAT1To1IPsUsage(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() conn, err := net.ListenPacket("udp4", ":0") - assert.NoError(t, err) + require.NoError(t, err) defer func() { _ = conn.Close() }() @@ -624,43 +664,42 @@ func TestUDPMuxDefaultWithNAT1To1IPsUsage(t *testing.T) { _ = mux.Close() }() - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ NAT1To1IPs: []string{"1.2.3.4"}, NAT1To1IPCandidateType: CandidateTypeHost, UDPMux: mux, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() gatherCandidateDone := make(chan struct{}) - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(c Candidate) { if c == nil { close(gatherCandidateDone) } else { - assert.Equal(t, "1.2.3.4", c.Address()) + require.Equal(t, "1.2.3.4", c.Address()) } })) - assert.NoError(t, a.GatherCandidates()) + require.NoError(t, agent.GatherCandidates()) <-gatherCandidateDone - assert.NotEqual(t, 0, len(mux.connsIPv4)) - - assert.NoError(t, a.Close()) + require.NotEqual(t, 0, len(mux.connsIPv4)) } -// Assert that candidates are given for each mux in a MultiUDPMux +// Assert that candidates are given for each mux in a MultiUDPMux. func TestMultiUDPMuxUsage(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() var expectedPorts []int var udpMuxInstances []UDPMux for i := 0; i < 3; i++ { port := randomPort(t) conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: port}) - assert.NoError(t, err) + require.NoError(t, err) defer func() { _ = conn.Close() }() @@ -674,43 +713,43 @@ func TestMultiUDPMuxUsage(t *testing.T) { }() } - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, CandidateTypes: []CandidateType{CandidateTypeHost}, UDPMux: NewMultiUDPMuxDefault(udpMuxInstances...), }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() candidateCh := make(chan Candidate) - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(c Candidate) { if c == nil { close(candidateCh) + return } candidateCh <- c })) - assert.NoError(t, a.GatherCandidates()) + require.NoError(t, agent.GatherCandidates()) portFound := make(map[int]bool) for c := range candidateCh { portFound[c.Port()] = true - assert.True(t, c.NetworkType().IsUDP(), "All candidates should be UDP") + require.True(t, c.NetworkType().IsUDP(), "All candidates should be UDP") } - assert.Len(t, portFound, len(expectedPorts)) + require.Len(t, portFound, len(expectedPorts)) for _, port := range expectedPorts { - assert.True(t, portFound[port], "There should be a candidate for each UDP mux port") + require.True(t, portFound[port], "There should be a candidate for each UDP mux port") } - - assert.NoError(t, a.Close()) } -// Assert that candidates are given for each mux in a MultiTCPMux +// Assert that candidates are given for each mux in a MultiTCPMux. func TestMultiTCPMuxUsage(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() var expectedPorts []int var tcpMuxInstances []TCPMux @@ -720,7 +759,7 @@ func TestMultiTCPMuxUsage(t *testing.T) { IP: net.IP{127, 0, 0, 1}, Port: port, }) - assert.NoError(t, err) + require.NoError(t, err) defer func() { _ = listener.Close() }() @@ -732,22 +771,26 @@ func TestMultiTCPMuxUsage(t *testing.T) { })) } - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ NetworkTypes: supportedNetworkTypes(), CandidateTypes: []CandidateType{CandidateTypeHost}, TCPMux: NewMultiTCPMuxDefault(tcpMuxInstances...), }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() candidateCh := make(chan Candidate) - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(c Candidate) { if c == nil { close(candidateCh) + return } candidateCh <- c })) - assert.NoError(t, a.GatherCandidates()) + require.NoError(t, agent.GatherCandidates()) portFound := make(map[int]bool) for c := range candidateCh { @@ -756,24 +799,20 @@ func TestMultiTCPMuxUsage(t *testing.T) { portFound[c.Port()] = true } } - assert.Len(t, portFound, len(expectedPorts)) + require.Len(t, portFound, len(expectedPorts)) for _, port := range expectedPorts { - assert.True(t, portFound[port], "There should be a candidate for each TCP mux port") + require.True(t, portFound[port], "There should be a candidate for each TCP mux port") } - - assert.NoError(t, a.Close()) } -// Assert that UniversalUDPMux is used while gathering when configured in the Agent +// Assert that UniversalUDPMux is used while gathering when configured in the Agent. func TestUniversalUDPMuxUsage(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: randomPort(t)}) - assert.NoError(t, err) + require.NoError(t, err) defer func() { _ = conn.Close() }() @@ -787,38 +826,53 @@ func TestUniversalUDPMuxUsage(t *testing.T) { for i := 0; i < numSTUNS; i++ { urls = append(urls, &stun.URI{ Scheme: SchemeTypeSTUN, - Host: "127.0.0.1", + Host: localhostIPStr, Port: 3478 + i, }) } - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ NetworkTypes: supportedNetworkTypes(), Urls: urls, CandidateTypes: []CandidateType{CandidateTypeServerReflexive}, UDPMuxSrflx: udpMuxSrflx, }) - assert.NoError(t, err) + require.NoError(t, err) + var aClosed bool + defer func() { + if aClosed { + return + } + require.NoError(t, agent.Close()) + }() candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background()) - assert.NoError(t, a.OnCandidate(func(c Candidate) { + require.NoError(t, agent.OnCandidate(func(c Candidate) { if c == nil { candidateGatheredFunc() + return } t.Log(c.NetworkType(), c.Priority(), c) })) - assert.NoError(t, a.GatherCandidates()) + require.NoError(t, agent.GatherCandidates()) <-candidateGathered.Done() - assert.NoError(t, a.Close()) + require.NoError(t, agent.Close()) + aClosed = true + // Twice because of 2 STUN servers configured - assert.Equal(t, numSTUNS, udpMuxSrflx.getXORMappedAddrUsedTimes, "expected times that GetXORMappedAddr should be called") + require.Equal( + t, + numSTUNS, + udpMuxSrflx.getXORMappedAddrUsedTimes, + "expected times that GetXORMappedAddr should be called", + ) // One for Restart() when agent has been initialized and one time when Close() the agent - assert.Equal(t, 2, udpMuxSrflx.removeConnByUfragTimes, "expected times that RemoveConnByUfrag should be called") + require.Equal(t, 2, udpMuxSrflx.removeConnByUfragTimes, "expected times that RemoveConnByUfrag should be called") // Twice because of 2 STUN servers configured - assert.Equal(t, numSTUNS, udpMuxSrflx.getConnForURLTimes, "expected times that GetConnForURL should be called") + require.Equal(t, numSTUNS, udpMuxSrflx.getConnForURLTimes, "expected times that GetConnForURL should be called") } type universalUDPMuxMock struct { @@ -838,6 +892,7 @@ func (m *universalUDPMuxMock) GetConnForURL(string, string, net.Addr) (net.Packe m.mu.Lock() defer m.mu.Unlock() m.getConnForURLTimes++ + return m.conn, nil } @@ -845,6 +900,7 @@ func (m *universalUDPMuxMock) GetXORMappedAddr(net.Addr, time.Duration) (*stun.X m.mu.Lock() defer m.mu.Unlock() m.getXORMappedAddrUsedTimes++ + return &stun.XORMappedAddress{IP: net.IP{100, 64, 0, 1}, Port: 77878}, nil } diff --git a/gather_vnet_test.go b/gather_vnet_test.go index 5410bc2..3020367 100644 --- a/gather_vnet_test.go +++ b/gather_vnet_test.go @@ -8,163 +8,127 @@ package ice import ( "context" - "errors" "fmt" "net" "testing" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" "github.com/pion/transport/v3/vnet" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestVNetGather(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +func TestVNetGather(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() loggerFactory := logging.NewDefaultLoggerFactory() t.Run("No local IP address", func(t *testing.T) { n, err := vnet.NewNet(&vnet.NetConfig{}) - assert.NoError(t, err) + require.NoError(t, err) a, err := NewAgent(&AgentConfig{ Net: n, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, a.Close()) + }() - localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{NetworkTypeUDP4}, false) - if len(localIPs) > 0 { - t.Fatal("should return no local IP") - } else if err != nil { - t.Fatal(err) - } - - assert.NoError(t, a.Close()) + _, localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{NetworkTypeUDP4}, false) + require.Len(t, localIPs, 0) + require.NoError(t, err) }) t.Run("Gather a dynamic IP address", func(t *testing.T) { cider := "1.2.3.0/24" _, ipNet, err := net.ParseCIDR(cider) - if err != nil { - t.Fatalf("Failed to parse CIDR: %s", err) - } + require.NoError(t, err) - r, err := vnet.NewRouter(&vnet.RouterConfig{ + router, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: cider, LoggerFactory: loggerFactory, }) - if err != nil { - t.Fatalf("Failed to create a router: %s", err) - } + require.NoError(t, err) nw, err := vnet.NewNet(&vnet.NetConfig{}) - if err != nil { - t.Fatalf("Failed to create a Net: %s", err) - } + require.NoError(t, err) - err = r.AddNet(nw) - if err != nil { - t.Fatalf("Failed to add a Net to the router: %s", err) - } + require.NoError(t, router.AddNet(nw)) a, err := NewAgent(&AgentConfig{ Net: nw, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, a.Close()) + }() - localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{NetworkTypeUDP4}, false) - if len(localIPs) == 0 { - t.Fatal("should have one local IP") - } else if err != nil { - t.Fatal(err) + _, localAddrs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{NetworkTypeUDP4}, false) + require.Len(t, localAddrs, 1) + require.NoError(t, err) + + for _, addr := range localAddrs { + require.False(t, addr.IsLoopback()) + require.True(t, ipNet.Contains(addr.AsSlice())) } - - for _, ip := range localIPs { - if ip.IsLoopback() { - t.Fatal("should not return loopback IP") - } - if !ipNet.Contains(ip) { - t.Fatal("should be contained in the CIDR") - } - } - - assert.NoError(t, a.Close()) }) t.Run("listenUDP", func(t *testing.T) { - r, err := vnet.NewRouter(&vnet.RouterConfig{ + router, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: "1.2.3.0/24", LoggerFactory: loggerFactory, }) - if err != nil { - t.Fatalf("Failed to create a router: %s", err) - } + require.NoError(t, err) nw, err := vnet.NewNet(&vnet.NetConfig{}) - if err != nil { - t.Fatalf("Failed to create a Net: %s", err) - } + require.NoError(t, err) - err = r.AddNet(nw) - if err != nil { - t.Fatalf("Failed to add a Net to the router: %s", err) - } + require.NoError(t, router.AddNet(nw)) - a, err := NewAgent(&AgentConfig{Net: nw}) - if err != nil { - t.Fatalf("Failed to create agent: %s", err) - } + agent, err := NewAgent(&AgentConfig{Net: nw}) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() - localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{NetworkTypeUDP4}, false) - if len(localIPs) == 0 { - t.Fatal("localInterfaces found no interfaces, unable to test") - } else if err != nil { - t.Fatal(err) - } + _, localAddrs, err := localInterfaces( + agent.net, + agent.interfaceFilter, + agent.ipFilter, + []NetworkType{NetworkTypeUDP4}, + false, + ) + require.NotEqual(t, 0, len(localAddrs)) + require.NoError(t, err) - ip := localIPs[0] + ip := localAddrs[0].AsSlice() - conn, err := listenUDPInPortRange(a.net, a.log, 0, 0, udp, &net.UDPAddr{IP: ip, Port: 0}) - if err != nil { - t.Fatalf("listenUDP error with no port restriction %v", err) - } else if conn == nil { - t.Fatalf("listenUDP error with no port restriction return a nil conn") - } - err = conn.Close() - if err != nil { - t.Fatalf("failed to close conn") - } + conn, err := listenUDPInPortRange(agent.net, agent.log, 0, 0, udp, &net.UDPAddr{IP: ip, Port: 0}) + require.NoError(t, err) + require.NotNil(t, conn) + require.NoError(t, conn.Close()) - _, err = listenUDPInPortRange(a.net, a.log, 4999, 5000, udp, &net.UDPAddr{IP: ip, Port: 0}) - if !errors.Is(err, ErrPort) { - t.Fatal("listenUDP with invalid port range did not return ErrPort") - } + _, err = listenUDPInPortRange(agent.net, agent.log, 4999, 5000, udp, &net.UDPAddr{IP: ip, Port: 0}) + require.ErrorIs(t, ErrPort, err) - conn, err = listenUDPInPortRange(a.net, a.log, 5000, 5000, udp, &net.UDPAddr{IP: ip, Port: 0}) - if err != nil { - t.Fatalf("listenUDP error with no port restriction %v", err) - } else if conn == nil { - t.Fatalf("listenUDP error with no port restriction return a nil conn") - } + conn, err = listenUDPInPortRange(agent.net, agent.log, 5000, 5000, udp, &net.UDPAddr{IP: ip, Port: 0}) + require.NoError(t, err) + require.NotNil(t, conn) + defer func() { + require.NoError(t, conn.Close()) + }() _, port, err := net.SplitHostPort(conn.LocalAddr().String()) - if err != nil { - t.Fatal(err) - } else if port != "5000" { - t.Fatalf("listenUDP with port restriction of 5000 listened on incorrect port (%s)", port) - } - assert.NoError(t, conn.Close()) - assert.NoError(t, a.Close()) + require.NoError(t, err) + require.Equal(t, "5000", port) }) } -func TestVNetGatherWithNAT1To1(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +func TestVNetGatherWithNAT1To1(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() loggerFactory := logging.NewDefaultLoggerFactory() log := loggerFactory.NewLogger("test") @@ -181,7 +145,7 @@ func TestVNetGatherWithNAT1To1(t *testing.T) { CIDR: "1.2.3.0/24", LoggerFactory: loggerFactory, }) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") lan, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: "10.0.0.0/24", @@ -191,81 +155,65 @@ func TestVNetGatherWithNAT1To1(t *testing.T) { }, LoggerFactory: loggerFactory, }) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") err = wan.AddRouter(lan) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") nw, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{localIP0, localIP1}, }) - if err != nil { - t.Fatalf("Failed to create a Net: %s", err) - } + require.NoError(t, err) err = lan.AddNet(nw) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ NetworkTypes: []NetworkType{ NetworkTypeUDP4, }, NAT1To1IPs: []string{map0, map1}, Net: nw, }) - assert.NoError(t, err, "should succeed") - defer a.Close() //nolint:errcheck + require.NoError(t, err, "should succeed") + defer func() { + require.NoError(t, agent.Close()) + }() done := make(chan struct{}) - err = a.OnCandidate(func(c Candidate) { + err = agent.OnCandidate(func(c Candidate) { if c == nil { close(done) } }) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") - err = a.GatherCandidates() - assert.NoError(t, err, "should succeed") + err = agent.GatherCandidates() + require.NoError(t, err, "should succeed") log.Debug("Wait until gathering is complete...") <-done log.Debug("Gathering is done") - candidates, err := a.GetLocalCandidates() - assert.NoError(t, err, "should succeed") + candidates, err := agent.GetLocalCandidates() + require.NoError(t, err, "should succeed") - if len(candidates) != 2 { - t.Fatal("There must be two candidates") - } + require.Len(t, candidates, 2) lAddr := [2]*net.UDPAddr{nil, nil} for i, candi := range candidates { lAddr[i] = candi.(*CandidateHost).conn.LocalAddr().(*net.UDPAddr) //nolint:forcetypeassert - if candi.Port() != lAddr[i].Port { - t.Fatalf("Unexpected candidate port: %d", candi.Port()) - } + require.Equal(t, candi.Port(), lAddr[i].Port) } - if candidates[0].Address() == externalIP0 { - if candidates[1].Address() != externalIP1 { - t.Fatalf("Unexpected candidate IP: %s", candidates[1].Address()) - } - if lAddr[0].IP.String() != localIP0 { - t.Fatalf("Unexpected listen IP: %s", lAddr[0].IP.String()) - } - if lAddr[1].IP.String() != localIP1 { - t.Fatalf("Unexpected listen IP: %s", lAddr[1].IP.String()) - } + if candidates[0].Address() == externalIP0 { //nolint:nestif + require.Equal(t, candidates[1].Address(), externalIP1) + require.Equal(t, lAddr[0].IP.String(), localIP0) + require.Equal(t, lAddr[1].IP.String(), localIP1) } else if candidates[0].Address() == externalIP1 { - if candidates[1].Address() != externalIP0 { - t.Fatalf("Unexpected candidate IP: %s", candidates[1].Address()) - } - if lAddr[0].IP.String() != localIP1 { - t.Fatalf("Unexpected listen IP: %s", lAddr[0].IP.String()) - } - if lAddr[1].IP.String() != localIP0 { - t.Fatalf("Unexpected listen IP: %s", lAddr[1].IP.String()) - } + require.Equal(t, candidates[1].Address(), externalIP0) + require.Equal(t, lAddr[0].IP.String(), localIP1) + require.Equal(t, lAddr[1].IP.String(), localIP0) } }) @@ -274,7 +222,7 @@ func TestVNetGatherWithNAT1To1(t *testing.T) { CIDR: "1.2.3.0/24", LoggerFactory: loggerFactory, }) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") lan, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: "10.0.0.0/24", @@ -286,24 +234,22 @@ func TestVNetGatherWithNAT1To1(t *testing.T) { }, LoggerFactory: loggerFactory, }) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") err = wan.AddRouter(lan) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") nw, err := vnet.NewNet(&vnet.NetConfig{ StaticIPs: []string{ "10.0.0.1", }, }) - if err != nil { - t.Fatalf("Failed to create a Net: %s", err) - } + require.NoError(t, err) err = lan.AddNet(nw) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ NetworkTypes: []NetworkType{ NetworkTypeUDP4, }, @@ -313,30 +259,30 @@ func TestVNetGatherWithNAT1To1(t *testing.T) { NAT1To1IPCandidateType: CandidateTypeServerReflexive, Net: nw, }) - assert.NoError(t, err, "should succeed") - defer a.Close() //nolint:errcheck + require.NoError(t, err, "should succeed") + defer func() { + require.NoError(t, agent.Close()) + }() done := make(chan struct{}) - err = a.OnCandidate(func(c Candidate) { + err = agent.OnCandidate(func(c Candidate) { if c == nil { close(done) } }) - assert.NoError(t, err, "should succeed") + require.NoError(t, err, "should succeed") - err = a.GatherCandidates() - assert.NoError(t, err, "should succeed") + err = agent.GatherCandidates() + require.NoError(t, err, "should succeed") log.Debug("Wait until gathering is complete...") <-done log.Debug("Gathering is done") - candidates, err := a.GetLocalCandidates() - assert.NoError(t, err, "should succeed") + candidates, err := agent.GetLocalCandidates() + require.NoError(t, err, "should succeed") - if len(candidates) != 2 { - t.Fatalf("Expected two candidates. actually %d", len(candidates)) - } + require.Len(t, candidates, 2) var candiHost *CandidateHost var candiSrflx *CandidateServerReflexive @@ -348,103 +294,109 @@ func TestVNetGatherWithNAT1To1(t *testing.T) { case *CandidateServerReflexive: candiSrflx = candi default: - t.Fatal("Unexpected candidate type") + t.Fatal("Unexpected candidate type") // nolint } } - assert.NotNil(t, candiHost, "should not be nil") - assert.Equal(t, "10.0.0.1", candiHost.Address(), "should match") - assert.NotNil(t, candiSrflx, "should not be nil") - assert.Equal(t, "1.2.3.4", candiSrflx.Address(), "should match") + require.NotNil(t, candiHost, "should not be nil") + require.Equal(t, "10.0.0.1", candiHost.Address(), "should match") + require.NotNil(t, candiSrflx, "should not be nil") + require.Equal(t, "1.2.3.4", candiSrflx.Address(), "should match") }) } func TestVNetGatherWithInterfaceFilter(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() loggerFactory := logging.NewDefaultLoggerFactory() - r, err := vnet.NewRouter(&vnet.RouterConfig{ + router, err := vnet.NewRouter(&vnet.RouterConfig{ CIDR: "1.2.3.0/24", LoggerFactory: loggerFactory, }) - if err != nil { - t.Fatalf("Failed to create a router: %s", err) - } + require.NoError(t, err) nw, err := vnet.NewNet(&vnet.NetConfig{}) - if err != nil { - t.Fatalf("Failed to create a Net: %s", err) - } - - if err = r.AddNet(nw); err != nil { - t.Fatalf("Failed to add a Net to the router: %s", err) - } + require.NoError(t, err) + require.NoError(t, router.AddNet(nw)) t.Run("InterfaceFilter should exclude the interface", func(t *testing.T) { - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ Net: nw, - InterfaceFilter: func(interfaceName string) bool { - assert.Equal(t, "eth0", interfaceName) + InterfaceFilter: func(interfaceName string) (keep bool) { + require.Equal(t, "eth0", interfaceName) + return false }, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() - localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{NetworkTypeUDP4}, false) - if err != nil { - t.Fatal(err) - } else if len(localIPs) != 0 { - t.Fatal("InterfaceFilter should have excluded everything") - } - - assert.NoError(t, a.Close()) + _, localIPs, err := localInterfaces( + agent.net, + agent.interfaceFilter, + agent.ipFilter, + []NetworkType{NetworkTypeUDP4}, + false, + ) + require.NoError(t, err) + require.Len(t, localIPs, 0) }) t.Run("IPFilter should exclude the IP", func(t *testing.T) { - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ Net: nw, - IPFilter: func(ip net.IP) bool { - assert.Equal(t, net.IP{1, 2, 3, 1}, ip) + IPFilter: func(ip net.IP) (keep bool) { + require.Equal(t, net.IP{1, 2, 3, 1}, ip) + return false }, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() - localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{NetworkTypeUDP4}, false) - if err != nil { - t.Fatal(err) - } else if len(localIPs) != 0 { - t.Fatal("IPFilter should have excluded everything") - } - - assert.NoError(t, a.Close()) + _, localIPs, err := localInterfaces( + agent.net, + agent.interfaceFilter, + agent.ipFilter, + []NetworkType{NetworkTypeUDP4}, + false, + ) + require.NoError(t, err) + require.Len(t, localIPs, 0) }) t.Run("InterfaceFilter should not exclude the interface", func(t *testing.T) { - a, err := NewAgent(&AgentConfig{ + agent, err := NewAgent(&AgentConfig{ Net: nw, - InterfaceFilter: func(interfaceName string) bool { - assert.Equal(t, "eth0", interfaceName) + InterfaceFilter: func(interfaceName string) (keep bool) { + require.Equal(t, "eth0", interfaceName) + return true }, }) - assert.NoError(t, err) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() - localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{NetworkTypeUDP4}, false) - if err != nil { - t.Fatal(err) - } else if len(localIPs) == 0 { - t.Fatal("InterfaceFilter should not have excluded anything") - } - - assert.NoError(t, a.Close()) + _, localIPs, err := localInterfaces( + agent.net, + agent.interfaceFilter, + agent.ipFilter, + []NetworkType{NetworkTypeUDP4}, + false, + ) + require.NoError(t, err) + require.Len(t, localIPs, 1) }) } func TestVNetGather_TURNConnectionLeak(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() turnServerURL := &stun.URI{ Scheme: stun.SchemeTypeTURN, @@ -462,9 +414,7 @@ func TestVNetGather_TURNConnectionLeak(t *testing.T) { } v, err := buildVNet(natType, natType) - if !assert.NoError(t, err, "should succeed") { - return - } + require.NoError(t, err, "should succeed") defer v.close() cfg0 := &AgentConfig{ @@ -477,11 +427,11 @@ func TestVNetGather_TURNConnectionLeak(t *testing.T) { Net: v.net0, } aAgent, err := NewAgent(cfg0) - if !assert.NoError(t, err, "should succeed") { - return - } + require.NoError(t, err, "should succeed") + defer func() { + // Assert relay conn leak on close. + require.NoError(t, aAgent.Close()) + }() aAgent.gatherCandidatesRelay(context.Background(), []*stun.URI{turnServerURL}) - // Assert relay conn leak on close. - assert.NoError(t, aAgent.Close()) } diff --git a/go.mod b/go.mod index 4f87ca6..7b30346 100644 --- a/go.mod +++ b/go.mod @@ -1,18 +1,27 @@ -module github.com/pion/ice/v3 +module github.com/pion/ice/v4 -go 1.13 +go 1.20 require ( github.com/google/uuid v1.6.0 - github.com/kr/pretty v0.1.0 // indirect - github.com/pion/dtls/v2 v2.2.10 - github.com/pion/logging v0.2.2 - github.com/pion/mdns v0.0.12 + github.com/pion/dtls/v3 v3.0.7 + github.com/pion/logging v0.2.4 + github.com/pion/mdns/v2 v2.0.7 github.com/pion/randutil v0.1.0 - github.com/pion/stun/v2 v2.0.0 - github.com/pion/transport/v3 v3.0.1 - github.com/pion/turn/v3 v3.0.1 - github.com/stretchr/testify v1.8.4 - golang.org/x/net v0.20.0 + github.com/pion/stun/v3 v3.0.0 + github.com/pion/transport/v3 v3.0.7 + github.com/pion/turn/v4 v4.1.1 + github.com/stretchr/testify v1.10.0 + golang.org/x/net v0.34.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/kr/pretty v0.1.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/wlynxg/anet v0.0.3 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/sys v0.30.0 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index ede3fb2..5d020c8 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,3 @@ -github.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/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -8,88 +7,34 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/dtls/v2 v2.2.10 h1:u2Axk+FyIR1VFTPurktB+1zoEPGIW3bmyj3LEFrXjAA= -github.com/pion/dtls/v2 v2.2.10/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE= -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/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= -github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= +github.com/pion/dtls/v3 v3.0.7 h1:bItXtTYYhZwkPFk4t1n3Kkf5TDrfj6+4wG+CZR8uI9Q= +github.com/pion/dtls/v3 v3.0.7/go.mod h1:uDlH5VPrgOQIw59irKYkMudSFprY9IEFCqz/eTz16f8= +github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= +github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= +github.com/pion/mdns/v2 v2.0.7 h1:c9kM8ewCgjslaAmicYMFQIde2H9/lrZpjBkN8VwoVtM= +github.com/pion/mdns/v2 v2.0.7/go.mod h1:vAdSYNAT0Jy3Ru0zl2YiW3Rm/fJCwIeM0nToenfOJKA= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= -github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= -github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= -github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= -github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= -github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= -github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= -github.com/pion/turn/v3 v3.0.1 h1:wLi7BTQr6/Q20R0vt/lHbjv6y4GChFtC33nkYbasoT8= -github.com/pion/turn/v3 v3.0.1/go.mod h1:MrJDKgqryDyWy1/4NT9TWfXWGMC7UHT6pJIv1+gMeNE= +github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw= +github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU= +github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0= +github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo= +github.com/pion/turn/v4 v4.1.1 h1:9UnY2HB99tpDyz3cVVZguSxcqkJ1DsTSZ+8TGruh4fc= +github.com/pion/turn/v4 v4.1.1/go.mod h1:2123tHk1O++vmjI5VSD0awT50NywDAq5A2NNNU4Jjs8= 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/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= -golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -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/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= -golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -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/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -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/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -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/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -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= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg= +github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/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= diff --git a/ice.go b/ice.go index bd55120..bd53702 100644 --- a/ice.go +++ b/ice.go @@ -3,33 +3,33 @@ package ice -// ConnectionState is an enum showing the state of a ICE Connection +// ConnectionState is an enum showing the state of a ICE Connection. type ConnectionState int -// List of supported States +// List of supported States. const ( - // ConnectionStateUnknown represents an unknown state + // ConnectionStateUnknown represents an unknown state. ConnectionStateUnknown ConnectionState = iota - // ConnectionStateNew ICE agent is gathering addresses + // ConnectionStateNew ICE agent is gathering addresses. ConnectionStateNew - // ConnectionStateChecking ICE agent has been given local and remote candidates, and is attempting to find a match + // ConnectionStateChecking ICE agent has been given local and remote candidates, and is attempting to find a match. ConnectionStateChecking - // ConnectionStateConnected ICE agent has a pairing, but is still checking other pairs + // ConnectionStateConnected ICE agent has a pairing, but is still checking other pairs. ConnectionStateConnected - // ConnectionStateCompleted ICE agent has finished + // ConnectionStateCompleted ICE agent has finished. ConnectionStateCompleted - // ConnectionStateFailed ICE agent never could successfully connect + // ConnectionStateFailed ICE agent never could successfully connect. ConnectionStateFailed - // ConnectionStateDisconnected ICE agent connected successfully, but has entered a failed state + // ConnectionStateDisconnected ICE agent connected successfully, but has entered a failed state. ConnectionStateDisconnected - // ConnectionStateClosed ICE agent has finished and is no longer handling requests + // ConnectionStateClosed ICE agent has finished and is no longer handling requests. ConnectionStateClosed ) @@ -54,20 +54,20 @@ func (c ConnectionState) String() string { } } -// GatheringState describes the state of the candidate gathering process +// GatheringState describes the state of the candidate gathering process. type GatheringState int const ( - // GatheringStateUnknown represents an unknown state + // GatheringStateUnknown represents an unknown state. GatheringStateUnknown GatheringState = iota - // GatheringStateNew indicates candidate gathering is not yet started + // GatheringStateNew indicates candidate gathering is not yet started. GatheringStateNew - // GatheringStateGathering indicates candidate gathering is ongoing + // GatheringStateGathering indicates candidate gathering is ongoing. GatheringStateGathering - // GatheringStateComplete indicates candidate gathering has been completed + // GatheringStateComplete indicates candidate gathering has been completed. GatheringStateComplete ) @@ -83,3 +83,8 @@ func (t GatheringState) String() string { return ErrUnknownType.Error() } } + +const ( + relayProtocolDTLS = "dtls" + relayProtocolTLS = "tls" +) diff --git a/ice_test.go b/ice_test.go index 5740cd1..a88f309 100644 --- a/ice_test.go +++ b/ice_test.go @@ -6,7 +6,7 @@ package ice import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestConnectedState_String(t *testing.T) { @@ -25,7 +25,7 @@ func TestConnectedState_String(t *testing.T) { } for i, testCase := range testCases { - assert.Equal(t, + require.Equal(t, testCase.expectedString, testCase.connectionState.String(), "testCase: %d %v", i, testCase, @@ -45,7 +45,7 @@ func TestGatheringState_String(t *testing.T) { } for i, testCase := range testCases { - assert.Equal(t, + require.Equal(t, testCase.expectedString, testCase.gatheringState.String(), "testCase: %d %v", i, testCase, diff --git a/icecontrol.go b/icecontrol.go index 82ed098..fcf08a0 100644 --- a/icecontrol.go +++ b/icecontrol.go @@ -6,7 +6,7 @@ package ice import ( "encoding/binary" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" ) // tiebreaker is common helper for ICE-{CONTROLLED,CONTROLLING} @@ -20,6 +20,7 @@ func (a tiebreaker) AddToAs(m *stun.Message, t stun.AttrType) error { v := make([]byte, tiebreakerSize) binary.BigEndian.PutUint64(v, uint64(a)) m.Add(t, v) + return nil } @@ -33,6 +34,7 @@ func (a *tiebreaker) GetFromAs(m *stun.Message, t stun.AttrType) error { return err } *a = tiebreaker(binary.BigEndian.Uint64(v)) + return nil } @@ -73,6 +75,7 @@ func (c AttrControl) AddTo(m *stun.Message) error { if c.Role == Controlling { return tiebreaker(c.Tiebreaker).AddToAs(m, stun.AttrICEControlling) } + return tiebreaker(c.Tiebreaker).AddToAs(m, stun.AttrICEControlled) } @@ -80,11 +83,14 @@ func (c AttrControl) AddTo(m *stun.Message) error { func (c *AttrControl) GetFrom(m *stun.Message) error { if m.Contains(stun.AttrICEControlling) { c.Role = Controlling + return (*tiebreaker)(&c.Tiebreaker).GetFromAs(m, stun.AttrICEControlling) } if m.Contains(stun.AttrICEControlled) { c.Role = Controlled + return (*tiebreaker)(&c.Tiebreaker).GetFromAs(m, stun.AttrICEControlled) } + return stun.ErrAttributeNotFound } diff --git a/icecontrol_test.go b/icecontrol_test.go index 80a22de..1ea25a4 100644 --- a/icecontrol_test.go +++ b/icecontrol_test.go @@ -4,140 +4,102 @@ package ice import ( - "errors" "testing" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" + "github.com/stretchr/testify/require" ) func TestControlled_GetFrom(t *testing.T) { //nolint:dupl m := new(stun.Message) - var c AttrControlled - if err := c.GetFrom(m); !errors.Is(err, stun.ErrAttributeNotFound) { - t.Error("unexpected error") - } - if err := m.Build(stun.BindingRequest, &c); err != nil { - t.Error(err) - } + var attrCtr AttrControlled + require.ErrorIs(t, stun.ErrAttributeNotFound, attrCtr.GetFrom(m)) + require.NoError(t, m.Build(stun.BindingRequest, &attrCtr)) + m1 := new(stun.Message) - if _, err := m1.Write(m.Raw); err != nil { - t.Error(err) - } + _, err := m1.Write(m.Raw) + require.NoError(t, err) + var c1 AttrControlled - if err := c1.GetFrom(m1); err != nil { - t.Error(err) - } - if c1 != c { - t.Error("not equal") - } + require.NoError(t, c1.GetFrom(m1)) + require.Equal(t, c1, attrCtr) + t.Run("IncorrectSize", func(t *testing.T) { m3 := new(stun.Message) m3.Add(stun.AttrICEControlled, make([]byte, 100)) var c2 AttrControlled - if err := c2.GetFrom(m3); !stun.IsAttrSizeInvalid(err) { - t.Error("should error") - } + require.True(t, stun.IsAttrSizeInvalid(c2.GetFrom(m3))) }) } func TestControlling_GetFrom(t *testing.T) { //nolint:dupl m := new(stun.Message) - var c AttrControlling - if err := c.GetFrom(m); !errors.Is(err, stun.ErrAttributeNotFound) { - t.Error("unexpected error") - } - if err := m.Build(stun.BindingRequest, &c); err != nil { - t.Error(err) - } + var attrCtr AttrControlling + require.ErrorIs(t, stun.ErrAttributeNotFound, attrCtr.GetFrom(m)) + require.NoError(t, m.Build(stun.BindingRequest, &attrCtr)) + m1 := new(stun.Message) - if _, err := m1.Write(m.Raw); err != nil { - t.Error(err) - } + _, err := m1.Write(m.Raw) + require.NoError(t, err) + var c1 AttrControlling - if err := c1.GetFrom(m1); err != nil { - t.Error(err) - } - if c1 != c { - t.Error("not equal") - } + require.NoError(t, c1.GetFrom(m1)) + require.Equal(t, c1, attrCtr) t.Run("IncorrectSize", func(t *testing.T) { m3 := new(stun.Message) m3.Add(stun.AttrICEControlling, make([]byte, 100)) var c2 AttrControlling - if err := c2.GetFrom(m3); !stun.IsAttrSizeInvalid(err) { - t.Error("should error") - } + require.True(t, stun.IsAttrSizeInvalid(c2.GetFrom(m3))) }) } -func TestControl_GetFrom(t *testing.T) { +func TestControl_GetFrom(t *testing.T) { //nolint:cyclop t.Run("Blank", func(t *testing.T) { m := new(stun.Message) var c AttrControl - if err := c.GetFrom(m); !errors.Is(err, stun.ErrAttributeNotFound) { - t.Error("unexpected error") - } + require.ErrorIs(t, stun.ErrAttributeNotFound, c.GetFrom(m)) }) t.Run("Controlling", func(t *testing.T) { //nolint:dupl m := new(stun.Message) - var c AttrControl - if err := c.GetFrom(m); !errors.Is(err, stun.ErrAttributeNotFound) { - t.Error("unexpected error") - } - c.Role = Controlling - c.Tiebreaker = 4321 - if err := m.Build(stun.BindingRequest, &c); err != nil { - t.Error(err) - } + var attCtr AttrControl + require.ErrorIs(t, stun.ErrAttributeNotFound, attCtr.GetFrom(m)) + attCtr.Role = Controlling + attCtr.Tiebreaker = 4321 + require.NoError(t, m.Build(stun.BindingRequest, &attCtr)) m1 := new(stun.Message) - if _, err := m1.Write(m.Raw); err != nil { - t.Error(err) - } + _, err := m1.Write(m.Raw) + require.NoError(t, err) var c1 AttrControl - if err := c1.GetFrom(m1); err != nil { - t.Error(err) - } - if c1 != c { - t.Error("not equal") - } + require.NoError(t, c1.GetFrom(m1)) + require.Equal(t, c1, attCtr) t.Run("IncorrectSize", func(t *testing.T) { m3 := new(stun.Message) m3.Add(stun.AttrICEControlling, make([]byte, 100)) var c2 AttrControl - if err := c2.GetFrom(m3); !stun.IsAttrSizeInvalid(err) { - t.Error("should error") - } + err := c2.GetFrom(m3) + require.True(t, stun.IsAttrSizeInvalid(err)) }) }) t.Run("Controlled", func(t *testing.T) { //nolint:dupl m := new(stun.Message) - var c AttrControl - if err := c.GetFrom(m); !errors.Is(err, stun.ErrAttributeNotFound) { - t.Error("unexpected error") - } - c.Role = Controlled - c.Tiebreaker = 1234 - if err := m.Build(stun.BindingRequest, &c); err != nil { - t.Error(err) - } + var attrCtrl AttrControl + require.ErrorIs(t, stun.ErrAttributeNotFound, attrCtrl.GetFrom(m)) + attrCtrl.Role = Controlled + attrCtrl.Tiebreaker = 1234 + require.NoError(t, m.Build(stun.BindingRequest, &attrCtrl)) m1 := new(stun.Message) - if _, err := m1.Write(m.Raw); err != nil { - t.Error(err) - } + _, err := m1.Write(m.Raw) + require.NoError(t, err) + var c1 AttrControl - if err := c1.GetFrom(m1); err != nil { - t.Error(err) - } - if c1 != c { - t.Error("not equal") - } + require.NoError(t, c1.GetFrom(m1)) + require.Equal(t, c1, attrCtrl) t.Run("IncorrectSize", func(t *testing.T) { m3 := new(stun.Message) m3.Add(stun.AttrICEControlling, make([]byte, 100)) var c2 AttrControl - if err := c2.GetFrom(m3); !stun.IsAttrSizeInvalid(err) { - t.Error("should error") - } + err := c2.GetFrom(m3) + require.True(t, stun.IsAttrSizeInvalid(err)) }) }) } diff --git a/internal/atomic/atomic.go b/internal/atomic/atomic.go index f8caf5a..f170133 100644 --- a/internal/atomic/atomic.go +++ b/internal/atomic/atomic.go @@ -6,18 +6,19 @@ package atomic import "sync/atomic" -// Error is an atomic error +// Error is an atomic error. type Error struct { v atomic.Value } -// Store updates the value of the atomic variable +// Store updates the value of the atomic variable. func (a *Error) Store(err error) { a.v.Store(struct{ error }{err}) } -// Load retrieves the current value of the atomic variable +// Load retrieves the current value of the atomic variable. func (a *Error) Load() error { err, _ := a.v.Load().(struct{ error }) + return err.error } diff --git a/internal/fakenet/mock_conn.go b/internal/fakenet/mock_conn.go index cc98849..baf012a 100644 --- a/internal/fakenet/mock_conn.go +++ b/internal/fakenet/mock_conn.go @@ -11,7 +11,7 @@ import ( "time" ) -// MockPacketConn for tests +// MockPacketConn for tests. type MockPacketConn struct{} func (m *MockPacketConn) ReadFrom([]byte) (n int, addr net.Addr, err error) { return 0, nil, nil } //nolint:revive diff --git a/internal/fakenet/packet_conn.go b/internal/fakenet/packet_conn.go index 0b9faaa..f9cb66f 100644 --- a/internal/fakenet/packet_conn.go +++ b/internal/fakenet/packet_conn.go @@ -8,18 +8,19 @@ import ( "net" ) -// Compile-time assertion +// Compile-time assertion. var _ net.PacketConn = (*PacketConn)(nil) -// PacketConn wraps a net.Conn and emulates net.PacketConn +// PacketConn wraps a net.Conn and emulates net.PacketConn. type PacketConn struct { net.Conn } -// ReadFrom reads a packet from the connection, +// ReadFrom reads a packet from the connection. func (f *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { n, err = f.Conn.Read(p) addr = f.Conn.RemoteAddr() + return } diff --git a/internal/stun/stun.go b/internal/stun/stun.go index 2b05f50..55ccb9b 100644 --- a/internal/stun/stun.go +++ b/internal/stun/stun.go @@ -10,7 +10,7 @@ import ( "net" "time" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" ) var ( @@ -59,7 +59,7 @@ func GetXORMappedAddr(conn net.PacketConn, serverAddr net.Addr, timeout time.Dur return &addr, nil } -// AssertUsername checks that the given STUN message m has a USERNAME attribute with a given value +// AssertUsername checks that the given STUN message m has a USERNAME attribute with a given value. func AssertUsername(m *stun.Message, expectedUsername string) error { var username stun.Username if err := username.GetFrom(m); err != nil { diff --git a/internal/taskloop/taskloop.go b/internal/taskloop/taskloop.go new file mode 100644 index 0000000..63e780f --- /dev/null +++ b/internal/taskloop/taskloop.go @@ -0,0 +1,121 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +// Package taskloop implements a task loop to run +// tasks sequentially in a separate Goroutine. +package taskloop + +import ( + "context" + "errors" + "time" + + atomicx "github.com/pion/ice/v4/internal/atomic" +) + +// ErrClosed indicates that the loop has been stopped. +var ErrClosed = errors.New("the agent is closed") + +type task struct { + fn func(context.Context) + done chan struct{} +} + +// Loop runs submitted task serially in a dedicated Goroutine. +type Loop struct { + tasks chan task + + // State for closing + done chan struct{} + taskLoopDone chan struct{} + err atomicx.Error +} + +// New creates and starts a new task loop. +func New(onClose func()) *Loop { + l := &Loop{ + tasks: make(chan task), + done: make(chan struct{}), + taskLoopDone: make(chan struct{}), + } + + go l.runLoop(onClose) + + return l +} + +// runLoop handles registered tasks and agent close. +func (l *Loop) runLoop(onClose func()) { + defer func() { + onClose() + close(l.taskLoopDone) + }() + + for { + select { + case <-l.done: + return + case t := <-l.tasks: + t.fn(l) + close(t.done) + } + } +} + +// Close stops the loop after finishing the execution of the current task. +// Other pending tasks will not be executed. +func (l *Loop) Close() { + if err := l.Err(); err != nil { + return + } + + l.err.Store(ErrClosed) + + close(l.done) + <-l.taskLoopDone +} + +// Run serially executes the submitted callback. +// Blocking tasks must be cancelable by context. +func (l *Loop) Run(ctx context.Context, t func(context.Context)) error { + if err := l.Err(); err != nil { + return err + } + done := make(chan struct{}) + select { + case <-ctx.Done(): + return ctx.Err() + case l.tasks <- task{t, done}: + <-done + + return nil + } +} + +// The following methods implement context.Context for TaskLoop + +// Done returns a channel that's closed when the task loop has been stopped. +func (l *Loop) Done() <-chan struct{} { + return l.done +} + +// Err returns nil if the task loop is still running. +// Otherwise it return errClosed if the loop has been closed/stopped. +func (l *Loop) Err() error { + select { + case <-l.done: + return ErrClosed + default: + return nil + } +} + +// Deadline returns the no valid time as task loops have no deadline. +func (l *Loop) Deadline() (deadline time.Time, ok bool) { + return time.Time{}, false +} + +// Value is not supported for task loops. +func (l *Loop) Value(any) any { + return nil +} diff --git a/mdns.go b/mdns.go index 1fa52a7..b88909d 100644 --- a/mdns.go +++ b/mdns.go @@ -4,25 +4,29 @@ package ice import ( + "net" + "github.com/google/uuid" "github.com/pion/logging" - "github.com/pion/mdns" + "github.com/pion/mdns/v2" "github.com/pion/transport/v3" "golang.org/x/net/ipv4" + "golang.org/x/net/ipv6" ) -// MulticastDNSMode represents the different Multicast modes ICE can run in +// MulticastDNSMode represents the different Multicast modes ICE can run in. type MulticastDNSMode byte -// MulticastDNSMode enum +// MulticastDNSMode enum. const ( - // MulticastDNSModeDisabled means remote mDNS candidates will be discarded, and local host candidates will use IPs + // MulticastDNSModeDisabled means remote mDNS candidates will be discarded, and local host candidates will use IPs. MulticastDNSModeDisabled MulticastDNSMode = iota + 1 - // MulticastDNSModeQueryOnly means remote mDNS candidates will be accepted, and local host candidates will use IPs + // MulticastDNSModeQueryOnly means remote mDNS candidates will be accepted, and local host candidates will use IPs. MulticastDNSModeQueryOnly - // MulticastDNSModeQueryAndGather means remote mDNS candidates will be accepted, and local host candidates will use mDNS + // MulticastDNSModeQueryAndGather means remote mDNS candidates will be accepted, + // and local host candidates will use mDNS. MulticastDNSModeQueryAndGather ) @@ -30,34 +34,109 @@ func generateMulticastDNSName() (string, error) { // https://tools.ietf.org/id/draft-ietf-rtcweb-mdns-ice-candidates-02.html#gathering // The unique name MUST consist of a version 4 UUID as defined in [RFC4122], followed by “.local”. u, err := uuid.NewRandom() + return u.String() + ".local", err } -func createMulticastDNS(n transport.Net, mDNSMode MulticastDNSMode, mDNSName string, log logging.LeveledLogger) (*mdns.Conn, MulticastDNSMode, error) { +//nolint:cyclop +func createMulticastDNS( + netTransport transport.Net, + networkTypes []NetworkType, + interfaces []*transport.Interface, + includeLoopback bool, + mDNSMode MulticastDNSMode, + mDNSName string, + log logging.LeveledLogger, + loggerFactory logging.LoggerFactory, +) (*mdns.Conn, MulticastDNSMode, error) { if mDNSMode == MulticastDNSModeDisabled { return nil, mDNSMode, nil } - addr, mdnsErr := n.ResolveUDPAddr("udp4", mdns.DefaultAddress) + var useV4, useV6 bool + if len(networkTypes) == 0 { + useV4 = true + useV6 = true + } else { + for _, nt := range networkTypes { + if nt.IsIPv4() { + useV4 = true + + continue + } + if nt.IsIPv6() { + useV6 = true + } + } + } + + addr4, mdnsErr := netTransport.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) + if mdnsErr != nil { + return nil, mDNSMode, mdnsErr + } + addr6, mdnsErr := netTransport.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) if mdnsErr != nil { return nil, mDNSMode, mdnsErr } - l, mdnsErr := n.ListenUDP("udp4", addr) - if mdnsErr != nil { + var pktConnV4 *ipv4.PacketConn + var mdns4Err error + if useV4 { + var l transport.UDPConn + l, mdns4Err = netTransport.ListenUDP("udp4", addr4) + if mdns4Err != nil { + // If ICE fails to start MulticastDNS server just warn the user and continue + log.Errorf("Failed to enable mDNS over IPv4: (%s)", mdns4Err) + + return nil, MulticastDNSModeDisabled, nil + } + pktConnV4 = ipv4.NewPacketConn(l) + } + + var pktConnV6 *ipv6.PacketConn + var mdns6Err error + if useV6 { + var l transport.UDPConn + l, mdns6Err = netTransport.ListenUDP("udp6", addr6) + if mdns6Err != nil { + log.Errorf("Failed to enable mDNS over IPv6: (%s)", mdns6Err) + + return nil, MulticastDNSModeDisabled, nil + } + pktConnV6 = ipv6.NewPacketConn(l) + } + + if mdns4Err != nil && mdns6Err != nil { // If ICE fails to start MulticastDNS server just warn the user and continue - log.Errorf("Failed to enable mDNS, continuing in mDNS disabled mode: (%s)", mdnsErr) + log.Errorf("Failed to enable mDNS, continuing in mDNS disabled mode") + //nolint:nilerr return nil, MulticastDNSModeDisabled, nil } + var ifcs []net.Interface + if interfaces != nil { + ifcs = make([]net.Interface, 0, len(ifcs)) + for _, ifc := range interfaces { + ifcs = append(ifcs, ifc.Interface) + } + } switch mDNSMode { case MulticastDNSModeQueryOnly: - conn, err := mdns.Server(ipv4.NewPacketConn(l), &mdns.Config{}) + conn, err := mdns.Server(pktConnV4, pktConnV6, &mdns.Config{ + Interfaces: ifcs, + IncludeLoopback: includeLoopback, + LoggerFactory: loggerFactory, + }) + return conn, mDNSMode, err case MulticastDNSModeQueryAndGather: - conn, err := mdns.Server(ipv4.NewPacketConn(l), &mdns.Config{ - LocalNames: []string{mDNSName}, + conn, err := mdns.Server(pktConnV4, pktConnV6, &mdns.Config{ + Interfaces: ifcs, + IncludeLoopback: includeLoopback, + LocalNames: []string{mDNSName}, + LoggerFactory: loggerFactory, }) + return conn, mDNSMode, err default: return nil, mDNSMode, nil diff --git a/mdns_test.go b/mdns_test.go index 7cab077..3dea4e4 100644 --- a/mdns_test.go +++ b/mdns_test.go @@ -13,140 +13,187 @@ import ( "time" "github.com/pion/transport/v3/test" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestMulticastDNSOnlyConnection(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() - cfg := &AgentConfig{ - NetworkTypes: []NetworkType{NetworkTypeUDP4}, - CandidateTypes: []CandidateType{CandidateTypeHost}, - MulticastDNSMode: MulticastDNSModeQueryAndGather, + type testCase struct { + Name string + NetworkTypes []NetworkType } - aAgent, err := NewAgent(cfg) - if err != nil { - t.Fatal(err) + testCases := []testCase{ + {Name: "UDP4", NetworkTypes: []NetworkType{NetworkTypeUDP4}}, } - aNotifier, aConnected := onConnected() - if err = aAgent.OnConnectionStateChange(aNotifier); err != nil { - t.Fatal(err) + if ipv6Available(t) { + testCases = append(testCases, + testCase{Name: "UDP6", NetworkTypes: []NetworkType{NetworkTypeUDP6}}, + testCase{Name: "UDP46", NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}}, + ) } - bAgent, err := NewAgent(cfg) - if err != nil { - t.Fatal(err) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + cfg := &AgentConfig{ + NetworkTypes: tc.NetworkTypes, + CandidateTypes: []CandidateType{CandidateTypeHost}, + MulticastDNSMode: MulticastDNSModeQueryAndGather, + InterfaceFilter: problematicNetworkInterfaces, + } + + aAgent, err := NewAgent(cfg) + require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() + + aNotifier, aConnected := onConnected() + require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) + + bAgent, err := NewAgent(cfg) + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() + + bNotifier, bConnected := onConnected() + require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) + + connect(t, aAgent, bAgent) + <-aConnected + <-bConnected + }) } - - bNotifier, bConnected := onConnected() - if err = bAgent.OnConnectionStateChange(bNotifier); err != nil { - t.Fatal(err) - } - - connect(aAgent, bAgent) - <-aConnected - <-bConnected - - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) } func TestMulticastDNSMixedConnection(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() - aAgent, err := NewAgent(&AgentConfig{ - NetworkTypes: []NetworkType{NetworkTypeUDP4}, - CandidateTypes: []CandidateType{CandidateTypeHost}, - MulticastDNSMode: MulticastDNSModeQueryAndGather, - }) - if err != nil { - t.Fatal(err) + type testCase struct { + Name string + NetworkTypes []NetworkType } - aNotifier, aConnected := onConnected() - if err = aAgent.OnConnectionStateChange(aNotifier); err != nil { - t.Fatal(err) + testCases := []testCase{ + {Name: "UDP4", NetworkTypes: []NetworkType{NetworkTypeUDP4}}, } - bAgent, err := NewAgent(&AgentConfig{ - NetworkTypes: []NetworkType{NetworkTypeUDP4}, - CandidateTypes: []CandidateType{CandidateTypeHost}, - MulticastDNSMode: MulticastDNSModeQueryOnly, - }) - if err != nil { - t.Fatal(err) + if ipv6Available(t) { + testCases = append(testCases, + testCase{Name: "UDP6", NetworkTypes: []NetworkType{NetworkTypeUDP6}}, + testCase{Name: "UDP46", NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}}, + ) } - bNotifier, bConnected := onConnected() - if err = bAgent.OnConnectionStateChange(bNotifier); err != nil { - t.Fatal(err) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + aAgent, err := NewAgent(&AgentConfig{ + NetworkTypes: tc.NetworkTypes, + CandidateTypes: []CandidateType{CandidateTypeHost}, + MulticastDNSMode: MulticastDNSModeQueryAndGather, + InterfaceFilter: problematicNetworkInterfaces, + }) + require.NoError(t, err) + defer func() { + require.NoError(t, aAgent.Close()) + }() + + aNotifier, aConnected := onConnected() + require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) + + bAgent, err := NewAgent(&AgentConfig{ + NetworkTypes: tc.NetworkTypes, + CandidateTypes: []CandidateType{CandidateTypeHost}, + MulticastDNSMode: MulticastDNSModeQueryOnly, + InterfaceFilter: problematicNetworkInterfaces, + }) + require.NoError(t, err) + defer func() { + require.NoError(t, bAgent.Close()) + }() + + bNotifier, bConnected := onConnected() + require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) + + connect(t, aAgent, bAgent) + <-aConnected + <-bConnected + }) } - - connect(aAgent, bAgent) - <-aConnected - <-bConnected - - assert.NoError(t, aAgent.Close()) - assert.NoError(t, bAgent.Close()) } func TestMulticastDNSStaticHostName(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() - _, err := NewAgent(&AgentConfig{ - NetworkTypes: []NetworkType{NetworkTypeUDP4}, - CandidateTypes: []CandidateType{CandidateTypeHost}, - MulticastDNSMode: MulticastDNSModeQueryAndGather, - MulticastDNSHostName: "invalidHostName", - }) - assert.Equal(t, err, ErrInvalidMulticastDNSHostName) + type testCase struct { + Name string + NetworkTypes []NetworkType + } - agent, err := NewAgent(&AgentConfig{ - NetworkTypes: []NetworkType{NetworkTypeUDP4}, - CandidateTypes: []CandidateType{CandidateTypeHost}, - MulticastDNSMode: MulticastDNSModeQueryAndGather, - MulticastDNSHostName: "validName.local", - }) - assert.NoError(t, err) + testCases := []testCase{ + {Name: "UDP4", NetworkTypes: []NetworkType{NetworkTypeUDP4}}, + } - correctHostName, resolveFunc := context.WithCancel(context.Background()) - assert.NoError(t, agent.OnCandidate(func(c Candidate) { - if c != nil && c.Address() == "validName.local" { - resolveFunc() - } - })) + if ipv6Available(t) { + testCases = append(testCases, + testCase{Name: "UDP6", NetworkTypes: []NetworkType{NetworkTypeUDP6}}, + testCase{Name: "UDP46", NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}}, + ) + } - assert.NoError(t, agent.GatherCandidates()) - <-correctHostName.Done() - assert.NoError(t, agent.Close()) + for _, tc := range testCases { + t.Run(tc.Name, func(t *testing.T) { + _, err := NewAgent(&AgentConfig{ + NetworkTypes: tc.NetworkTypes, + CandidateTypes: []CandidateType{CandidateTypeHost}, + MulticastDNSMode: MulticastDNSModeQueryAndGather, + MulticastDNSHostName: "invalidHostName", + InterfaceFilter: problematicNetworkInterfaces, + }) + require.Equal(t, err, ErrInvalidMulticastDNSHostName) + + agent, err := NewAgent(&AgentConfig{ + NetworkTypes: tc.NetworkTypes, + CandidateTypes: []CandidateType{CandidateTypeHost}, + MulticastDNSMode: MulticastDNSModeQueryAndGather, + MulticastDNSHostName: "validName.local", + InterfaceFilter: problematicNetworkInterfaces, + }) + require.NoError(t, err) + defer func() { + require.NoError(t, agent.Close()) + }() + + correctHostName, resolveFunc := context.WithCancel(context.Background()) + require.NoError(t, agent.OnCandidate(func(c Candidate) { + if c != nil && c.Address() == "validName.local" { + resolveFunc() + } + })) + + require.NoError(t, agent.GatherCandidates()) + <-correctHostName.Done() + }) + } } func TestGenerateMulticastDNSName(t *testing.T) { name, err := generateMulticastDNSName() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) isMDNSName := regexp.MustCompile( `^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}.local+$`, ).MatchString - if !isMDNSName(name) { - t.Fatalf("mDNS name must be UUID v4 + \".local\" suffix, got %s", name) - } + require.True(t, isMDNSName(name)) } diff --git a/net.go b/net.go index 365745d..54ec4ca 100644 --- a/net.go +++ b/net.go @@ -5,6 +5,7 @@ package ice import ( "net" + "net/netip" "github.com/pion/logging" "github.com/pion/transport/v3" @@ -12,14 +13,18 @@ import ( // The conditions of invalidation written below are defined in // https://tools.ietf.org/html/rfc8445#section-5.1.1.1 -func isSupportedIPv6(ip net.IP) bool { +// It is partial because the link-local check is done later in various gather local +// candidate methods which conditionally accept IPv6 based on usage of mDNS or not. +func isSupportedIPv6Partial(ip net.IP) bool { if len(ip) != net.IPv6len || + // Deprecated IPv4-compatible IPv6 addresses [RFC4291] and IPv6 site- + // local unicast addresses [RFC3879] MUST NOT be included in the + // address candidates. isZeros(ip[0:12]) || // !(IPv4-compatible IPv6) - ip[0] == 0xfe && ip[1]&0xc0 == 0xc0 || // !(IPv6 site-local unicast) - ip.IsLinkLocalUnicast() || - ip.IsLinkLocalMulticast() { + ip[0] == 0xfe && ip[1]&0xc0 == 0xc0 { // !(IPv6 site-local unicast) return false } + return true } @@ -29,24 +34,39 @@ func isZeros(ip net.IP) bool { return false } } + return true } -func localInterfaces(n transport.Net, interfaceFilter func(string) bool, ipFilter func(net.IP) bool, networkTypes []NetworkType, includeLoopback bool) ([]net.IP, error) { //nolint:gocognit - ips := []net.IP{} +//nolint:gocognit,cyclop +func localInterfaces( + n transport.Net, + interfaceFilter func(string) (keep bool), + ipFilter func(net.IP) (keep bool), + networkTypes []NetworkType, + includeLoopback bool, +) ([]*transport.Interface, []netip.Addr, error) { + ipAddrs := []netip.Addr{} ifaces, err := n.Interfaces() if err != nil { - return ips, err + return nil, ipAddrs, err } - var IPv4Requested, IPv6Requested bool - for _, typ := range networkTypes { - if typ.IsIPv4() { - IPv4Requested = true - } + filteredIfaces := make([]*transport.Interface, 0, len(ifaces)) - if typ.IsIPv6() { - IPv6Requested = true + var ipV4Requested, ipv6Requested bool + if len(networkTypes) == 0 { + ipV4Requested = true + ipv6Requested = true + } else { + for _, typ := range networkTypes { + if typ.IsIPv4() { + ipV4Requested = true + } + + if typ.IsIPv6() { + ipv6Requested = true + } } } @@ -62,76 +82,90 @@ func localInterfaces(n transport.Net, interfaceFilter func(string) bool, ipFilte continue } - addrs, err := iface.Addrs() + ifaceAddrs, err := iface.Addrs() if err != nil { continue } - for _, addr := range addrs { - var ip net.IP - switch addr := addr.(type) { - case *net.IPNet: - ip = addr.IP - case *net.IPAddr: - ip = addr.IP - } - if ip == nil || (ip.IsLoopback() && !includeLoopback) { + atLeastOneAddr := false + for _, addr := range ifaceAddrs { + ipAddr, _, _, err := parseAddrFromIface(addr, iface.Name) + if err != nil || (ipAddr.IsLoopback() && !includeLoopback) { continue } - - if ipv4 := ip.To4(); ipv4 == nil { - if !IPv6Requested { + if ipAddr.Is6() { + if !ipv6Requested { continue - } else if !isSupportedIPv6(ip) { + } else if !isSupportedIPv6Partial(ipAddr.AsSlice()) { continue } - } else if !IPv4Requested { + } else if !ipV4Requested { continue } - if ipFilter != nil && !ipFilter(ip) { + if ipFilter != nil && !ipFilter(ipAddr.AsSlice()) { continue } - ips = append(ips, ip) + atLeastOneAddr = true + ipAddrs = append(ipAddrs, ipAddr) + } + + if atLeastOneAddr { + ifaceCopy := iface + filteredIfaces = append(filteredIfaces, ifaceCopy) } } - return ips, nil + + return filteredIfaces, ipAddrs, nil } -func listenUDPInPortRange(n transport.Net, log logging.LeveledLogger, portMax, portMin int, network string, lAddr *net.UDPAddr) (transport.UDPConn, error) { +//nolint:cyclop +func listenUDPInPortRange( + netTransport transport.Net, + log logging.LeveledLogger, + portMax, portMin int, + network string, + lAddr *net.UDPAddr, +) (transport.UDPConn, error) { if (lAddr.Port != 0) || ((portMin == 0) && (portMax == 0)) { - return n.ListenUDP(network, lAddr) + return netTransport.ListenUDP(network, lAddr) } - var i, j int - i = portMin - if i == 0 { - i = 1 + + if portMin == 0 { + portMin = 1024 // Start at 1024 which is non-privileged } - j = portMax - if j == 0 { - j = 0xFFFF + + if portMax == 0 { + portMax = 0xFFFF } - if i > j { + + if portMin > portMax { return nil, ErrPort } - portStart := globalMathRandomGenerator.Intn(j-i+1) + i + portStart := globalMathRandomGenerator.Intn(portMax-portMin+1) + portMin portCurrent := portStart for { - lAddr = &net.UDPAddr{IP: lAddr.IP, Port: portCurrent} - c, e := n.ListenUDP(network, lAddr) + addr := &net.UDPAddr{ + IP: lAddr.IP, + Zone: lAddr.Zone, + Port: portCurrent, + } + + c, e := netTransport.ListenUDP(network, addr) if e == nil { return c, e //nolint:nilerr } log.Debugf("Failed to listen %s: %v", lAddr.String(), e) portCurrent++ - if portCurrent > j { - portCurrent = i + if portCurrent > portMax { + portCurrent = portMin } if portCurrent == portStart { break } } + return nil, ErrPort } diff --git a/net_test.go b/net_test.go index 5b32fbe..8ebdf1f 100644 --- a/net_test.go +++ b/net_test.go @@ -5,40 +5,56 @@ package ice import ( "net" + "net/netip" + "strings" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) -func TestIsSupportedIPv6(t *testing.T) { - if isSupportedIPv6(net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1}) { - t.Errorf("isSupportedIPv6 return true with IPv4-compatible IPv6 address") - } - - if isSupportedIPv6(net.ParseIP("fec0::2333")) { - t.Errorf("isSupportedIPv6 return true with IPv6 site-local unicast address") - } - - if isSupportedIPv6(net.ParseIP("fe80::2333")) { - t.Errorf("isSupportedIPv6 return true with IPv6 link-local address") - } - - if isSupportedIPv6(net.ParseIP("ff02::2333")) { - t.Errorf("isSupportedIPv6 return true with IPv6 link-local multicast address") - } - - if !isSupportedIPv6(net.ParseIP("2001::1")) { - t.Errorf("isSupportedIPv6 return false with IPv6 global unicast address") - } +func TestIsSupportedIPv6Partial(t *testing.T) { + require.False(t, isSupportedIPv6Partial(net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1})) + require.False(t, isSupportedIPv6Partial(net.ParseIP("fec0::2333"))) + require.True(t, isSupportedIPv6Partial(net.ParseIP("fe80::2333"))) + require.True(t, isSupportedIPv6Partial(net.ParseIP("ff02::2333"))) + require.True(t, isSupportedIPv6Partial(net.ParseIP("2001::1"))) } func TestCreateAddr(t *testing.T) { - ipv4 := net.IP{127, 0, 0, 1} - ipv6 := net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1} + ipv4 := mustAddr(t, net.IP{127, 0, 0, 1}) + ipv6 := mustAddr(t, net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) port := 9000 - assert.Equal(t, &net.UDPAddr{IP: ipv4, Port: port}, createAddr(NetworkTypeUDP4, ipv4, port)) - assert.Equal(t, &net.UDPAddr{IP: ipv6, Port: port}, createAddr(NetworkTypeUDP6, ipv6, port)) - assert.Equal(t, &net.TCPAddr{IP: ipv4, Port: port}, createAddr(NetworkTypeTCP4, ipv4, port)) - assert.Equal(t, &net.TCPAddr{IP: ipv6, Port: port}, createAddr(NetworkTypeTCP6, ipv6, port)) + require.Equal(t, &net.UDPAddr{IP: ipv4.AsSlice(), Port: port}, createAddr(NetworkTypeUDP4, ipv4, port)) + require.Equal(t, &net.UDPAddr{IP: ipv6.AsSlice(), Port: port}, createAddr(NetworkTypeUDP6, ipv6, port)) + require.Equal(t, &net.TCPAddr{IP: ipv4.AsSlice(), Port: port}, createAddr(NetworkTypeTCP4, ipv4, port)) + require.Equal(t, &net.TCPAddr{IP: ipv6.AsSlice(), Port: port}, createAddr(NetworkTypeTCP6, ipv6, port)) +} + +func problematicNetworkInterfaces(s string) (keep bool) { + defaultDockerBridgeNetwork := strings.Contains(s, "docker") + customDockerBridgeNetwork := strings.Contains(s, "br-") + + // Apple filters + accessPoint := strings.Contains(s, "ap") + appleWirelessDirectLink := strings.Contains(s, "awdl") + appleLowLatencyWLANInterface := strings.Contains(s, "llw") + appleTunnelingInterface := strings.Contains(s, "utun") + + return !defaultDockerBridgeNetwork && + !customDockerBridgeNetwork && + !accessPoint && + !appleWirelessDirectLink && + !appleLowLatencyWLANInterface && + !appleTunnelingInterface +} + +func mustAddr(t *testing.T, ip net.IP) netip.Addr { + t.Helper() + addr, ok := netip.AddrFromSlice(ip) + if !ok { + t.Fatal(ipConvertError{ip}) // nolint + } + + return addr } diff --git a/networktype.go b/networktype.go index 57df186..af055c5 100644 --- a/networktype.go +++ b/networktype.go @@ -5,7 +5,7 @@ package ice import ( "fmt" - "net" + "net/netip" "strings" ) @@ -27,7 +27,7 @@ func supportedNetworkTypes() []NetworkType { } } -// NetworkType represents the type of network +// NetworkType represents the type of network. type NetworkType int const ( @@ -69,7 +69,7 @@ func (t NetworkType) IsTCP() bool { return t == NetworkTypeTCP4 || t == NetworkTypeTCP6 } -// NetworkShort returns the short network description +// NetworkShort returns the short network description. func (t NetworkType) NetworkShort() string { switch t { case NetworkTypeUDP4, NetworkTypeUDP6: @@ -81,7 +81,7 @@ func (t NetworkType) NetworkShort() string { } } -// IsReliable returns true if the network is reliable +// IsReliable returns true if the network is reliable. func (t NetworkType) IsReliable() bool { switch t { case NetworkTypeUDP4, NetworkTypeUDP6: @@ -89,6 +89,7 @@ func (t NetworkType) IsReliable() bool { case NetworkTypeTCP4, NetworkTypeTCP6: return true } + return false } @@ -100,6 +101,7 @@ func (t NetworkType) IsIPv4() bool { case NetworkTypeUDP6, NetworkTypeTCP6: return false } + return false } @@ -111,25 +113,28 @@ func (t NetworkType) IsIPv6() bool { case NetworkTypeUDP6, NetworkTypeTCP6: return true } + return false } // determineNetworkType determines the type of network based on // the short network string and an IP address. -func determineNetworkType(network string, ip net.IP) (NetworkType, error) { - ipv4 := ip.To4() != nil - +func determineNetworkType(network string, ip netip.Addr) (NetworkType, error) { + // we'd rather have an IPv4-mapped IPv6 become IPv4 so that it is usable. + ip = ip.Unmap() switch { case strings.HasPrefix(strings.ToLower(network), udp): - if ipv4 { + if ip.Is4() { return NetworkTypeUDP4, nil } + return NetworkTypeUDP6, nil case strings.HasPrefix(strings.ToLower(network), tcp): - if ipv4 { + if ip.Is4() { return NetworkTypeTCP4, nil } + return NetworkTypeTCP6, nil } diff --git a/networktype_test.go b/networktype_test.go index 201aadd..31aba30 100644 --- a/networktype_test.go +++ b/networktype_test.go @@ -7,7 +7,7 @@ import ( "net" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestNetworkTypeParsing_Success(t *testing.T) { @@ -45,14 +45,9 @@ func TestNetworkTypeParsing_Success(t *testing.T) { NetworkTypeUDP6, }, } { - actual, err := determineNetworkType(test.inNetwork, test.inIP) - if err != nil { - t.Errorf("NetworkTypeParsing failed: %v", err) - } - if actual != test.expected { - t.Errorf("NetworkTypeParsing: '%s' -- input:%s expected:%s actual:%s", - test.name, test.inNetwork, test.expected, actual) - } + actual, err := determineNetworkType(test.inNetwork, mustAddr(t, test.inIP)) + require.NoError(t, err) + require.Equal(t, test.expected, actual) } } @@ -70,24 +65,21 @@ func TestNetworkTypeParsing_Failure(t *testing.T) { ipv6, }, } { - actual, err := determineNetworkType(test.inNetwork, test.inIP) - if err == nil { - t.Errorf("NetworkTypeParsing should fail: '%s' -- input:%s actual:%s", - test.name, test.inNetwork, actual) - } + _, err := determineNetworkType(test.inNetwork, mustAddr(t, test.inIP)) + require.Error(t, err) } } func TestNetworkTypeIsUDP(t *testing.T) { - assert.True(t, NetworkTypeUDP4.IsUDP()) - assert.True(t, NetworkTypeUDP6.IsUDP()) - assert.False(t, NetworkTypeUDP4.IsTCP()) - assert.False(t, NetworkTypeUDP6.IsTCP()) + require.True(t, NetworkTypeUDP4.IsUDP()) + require.True(t, NetworkTypeUDP6.IsUDP()) + require.False(t, NetworkTypeUDP4.IsTCP()) + require.False(t, NetworkTypeUDP6.IsTCP()) } func TestNetworkTypeIsTCP(t *testing.T) { - assert.True(t, NetworkTypeTCP4.IsTCP()) - assert.True(t, NetworkTypeTCP6.IsTCP()) - assert.False(t, NetworkTypeTCP4.IsUDP()) - assert.False(t, NetworkTypeTCP6.IsUDP()) + require.True(t, NetworkTypeTCP4.IsTCP()) + require.True(t, NetworkTypeTCP6.IsTCP()) + require.False(t, NetworkTypeTCP4.IsUDP()) + require.False(t, NetworkTypeTCP6.IsUDP()) } diff --git a/priority.go b/priority.go index 13689fb..f8c8740 100644 --- a/priority.go +++ b/priority.go @@ -6,7 +6,7 @@ package ice import ( "encoding/binary" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" ) // PriorityAttr represents PRIORITY attribute. @@ -19,6 +19,7 @@ func (p PriorityAttr) AddTo(m *stun.Message) error { v := make([]byte, prioritySize) binary.BigEndian.PutUint32(v, uint32(p)) m.Add(stun.AttrPriority, v) + return nil } @@ -32,5 +33,6 @@ func (p *PriorityAttr) GetFrom(m *stun.Message) error { return err } *p = PriorityAttr(binary.BigEndian.Uint32(v)) + return nil } diff --git a/priority_test.go b/priority_test.go index ea76272..eee418d 100644 --- a/priority_test.go +++ b/priority_test.go @@ -4,38 +4,29 @@ package ice import ( - "errors" "testing" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" + "github.com/stretchr/testify/require" ) func TestPriority_GetFrom(t *testing.T) { //nolint:dupl m := new(stun.Message) - var p PriorityAttr - if err := p.GetFrom(m); !errors.Is(err, stun.ErrAttributeNotFound) { - t.Error("unexpected error") - } - if err := m.Build(stun.BindingRequest, &p); err != nil { - t.Error(err) - } + var priority PriorityAttr + require.ErrorIs(t, stun.ErrAttributeNotFound, priority.GetFrom(m)) + require.NoError(t, m.Build(stun.BindingRequest, &priority)) + m1 := new(stun.Message) - if _, err := m1.Write(m.Raw); err != nil { - t.Error(err) - } + _, err := m1.Write(m.Raw) + require.NoError(t, err) + var p1 PriorityAttr - if err := p1.GetFrom(m1); err != nil { - t.Error(err) - } - if p1 != p { - t.Error("not equal") - } + require.NoError(t, p1.GetFrom(m1)) + require.Equal(t, p1, priority) t.Run("IncorrectSize", func(t *testing.T) { m3 := new(stun.Message) m3.Add(stun.AttrPriority, make([]byte, 100)) var p2 PriorityAttr - if err := p2.GetFrom(m3); !stun.IsAttrSizeInvalid(err) { - t.Error("should error") - } + require.True(t, stun.IsAttrSizeInvalid(p2.GetFrom(m3))) }) } diff --git a/rand_test.go b/rand_test.go index e7e8569..420858c 100644 --- a/rand_test.go +++ b/rand_test.go @@ -6,6 +6,8 @@ package ice import ( "sync" "testing" + + "github.com/stretchr/testify/require" ) func TestRandomGeneratorCollision(t *testing.T) { @@ -15,31 +17,33 @@ func TestRandomGeneratorCollision(t *testing.T) { gen func(t *testing.T) string }{ "CandidateID": { - gen: func(t *testing.T) string { + gen: func(*testing.T) string { return candidateIDGen.Generate() }, }, "PWD": { gen: func(t *testing.T) string { + t.Helper() + s, err := generatePwd() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + return s }, }, "Ufrag": { gen: func(t *testing.T) string { + t.Helper() + s, err := generateUFrag() - if err != nil { - t.Fatal(err) - } + require.NoError(t, err) + return s }, }, } - const N = 100 + const num = 100 const iteration = 100 for name, testCase := range testCases { @@ -49,9 +53,9 @@ func TestRandomGeneratorCollision(t *testing.T) { var wg sync.WaitGroup var mu sync.Mutex - rands := make([]string, 0, N) + rands := make([]string, 0, num) - for i := 0; i < N; i++ { + for i := 0; i < num; i++ { wg.Add(1) go func() { r := testCase.gen(t) @@ -63,15 +67,10 @@ func TestRandomGeneratorCollision(t *testing.T) { } wg.Wait() - if len(rands) != N { - t.Fatal("Failed to generate randoms") - } - - for i := 0; i < N; i++ { - for j := i + 1; j < N; j++ { - if rands[i] == rands[j] { - t.Fatalf("generateRandString caused collision: %s == %s", rands[i], rands[j]) - } + require.Len(t, rands, num) + for i := 0; i < num; i++ { + for j := i + 1; j < num; j++ { + require.NotEqual(t, rands[i], rands[j]) } } } diff --git a/role.go b/role.go index e9a7bda..49f6896 100644 --- a/role.go +++ b/role.go @@ -26,6 +26,7 @@ func (r *Role) UnmarshalText(text []byte) error { default: return fmt.Errorf("%w %q", errUnknownRole, text) } + return nil } diff --git a/selection.go b/selection.go index bfbd5a9..b67898d 100644 --- a/selection.go +++ b/selection.go @@ -8,7 +8,7 @@ import ( "time" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" ) type pairCandidateSelector interface { @@ -44,6 +44,7 @@ func (s *controllingSelector) isNominatable(c Candidate) bool { } s.log.Errorf("Invalid candidate type: %s", c.Type()) + return false } @@ -59,10 +60,11 @@ func (s *controllingSelector) ContactCandidates() { default: p := s.agent.getBestValidCandidatePair() if p != nil && s.isNominatable(p.Local) && s.isNominatable(p.Remote) { - s.log.Tracef("Nominatable pair found, nominating (%s, %s)", p.Local.String(), p.Remote.String()) + s.log.Tracef("Nominatable pair found, nominating (%s, %s)", p.Local, p.Remote) p.nominated = true s.nominatedPair = p s.nominatePair(p) + return } s.agent.pingAllCandidates() @@ -84,40 +86,54 @@ func (s *controllingSelector) nominatePair(pair *CandidatePair) { ) if err != nil { s.log.Error(err.Error()) + return } - s.log.Tracef("Ping STUN (nominate candidate pair) from %s to %s", pair.Local.String(), pair.Remote.String()) + s.log.Tracef("Ping STUN (nominate candidate pair) from %s to %s", pair.Local, pair.Remote) s.agent.sendBindingRequest(msg, pair.Local, pair.Remote) } -func (s *controllingSelector) HandleBindingRequest(m *stun.Message, local, remote Candidate) { - s.agent.sendBindingSuccess(m, local, remote) +func (s *controllingSelector) HandleBindingRequest(message *stun.Message, local, remote Candidate) { //nolint:cyclop + s.agent.sendBindingSuccess(message, local, remote) - p := s.agent.findPair(local, remote) + pair := s.agent.findPair(local, remote) + + if pair == nil { + pair = s.agent.addPair(local, remote) + pair.UpdateRequestReceived() - if p == nil { - s.agent.addPair(local, remote) return } + pair.UpdateRequestReceived() - if p.state == CandidatePairStateSucceeded && s.nominatedPair == nil && s.agent.getSelectedPair() == nil { + if pair.state == CandidatePairStateSucceeded && s.nominatedPair == nil && s.agent.getSelectedPair() == nil { bestPair := s.agent.getBestAvailableCandidatePair() if bestPair == nil { s.log.Tracef("No best pair available") - } else if bestPair.equal(p) && s.isNominatable(p.Local) && s.isNominatable(p.Remote) { - s.log.Tracef("The candidate (%s, %s) is the best candidate available, marking it as nominated", - p.Local.String(), p.Remote.String()) - s.nominatedPair = p - s.nominatePair(p) + } else if bestPair.equal(pair) && s.isNominatable(pair.Local) && s.isNominatable(pair.Remote) { + s.log.Tracef( + "The candidate (%s, %s) is the best candidate available, marking it as nominated", + pair.Local, + pair.Remote, + ) + s.nominatedPair = pair + s.nominatePair(pair) + } + } + + if s.agent.userBindingRequestHandler != nil { + if shouldSwitch := s.agent.userBindingRequestHandler(message, local, remote, pair); shouldSwitch { + s.agent.setSelectedPair(pair) } } } func (s *controllingSelector) HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr) { - ok, pendingRequest := s.agent.handleInboundBindingSuccess(m.TransactionID) + ok, pendingRequest, rtt := s.agent.handleInboundBindingSuccess(m.TransactionID) if !ok { s.log.Warnf("Discard message from (%s), unknown TransactionID 0x%x", remote, m.TransactionID) + return } @@ -126,29 +142,32 @@ func (s *controllingSelector) HandleSuccessResponse(m *stun.Message, local, remo // Assert that NAT is not symmetric // https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1 if !addrEqual(transactionAddr, remoteAddr) { - s.log.Debugf("Discard message: transaction source and destination does not match expected(%s), actual(%s)", transactionAddr, remote) + s.log.Debugf( + "Discard message: transaction source and destination does not match expected(%s), actual(%s)", + transactionAddr, + remote, + ) + return } - s.log.Tracef("Inbound STUN (SuccessResponse) from %s to %s", remote.String(), local.String()) - p := s.agent.findPair(local, remote) + s.log.Tracef("Inbound STUN (SuccessResponse) from %s to %s", remote, local) + pair := s.agent.findPair(local, remote) - if p == nil { + if pair == nil { // This shouldn't happen s.log.Error("Success response from invalid candidate pair") + return } - p.state = CandidatePairStateSucceeded - s.log.Tracef("Found valid candidate pair: %s", p) + pair.state = CandidatePairStateSucceeded + s.log.Tracef("Found valid candidate pair: %s", pair) if pendingRequest.isUseCandidate && s.agent.getSelectedPair() == nil { - s.agent.setSelectedPair(p) + s.agent.setSelectedPair(pair) } - ok = p.markBindingResponse(m.TransactionID) - if ok && s.agent.getSelectedPair() == p { - s.agent.onSuccessfulSelectedPairBindingResponse(p) - } + pair.UpdateRoundTripTime(rtt) } func (s *controllingSelector) PingCandidate(local, remote Candidate) { @@ -161,6 +180,7 @@ func (s *controllingSelector) PingCandidate(local, remote Candidate) { ) if err != nil { s.log.Error(err.Error()) + return } @@ -196,6 +216,7 @@ func (s *controlledSelector) PingCandidate(local, remote Candidate) { ) if err != nil { s.log.Error(err.Error()) + return } @@ -211,9 +232,10 @@ func (s *controlledSelector) HandleSuccessResponse(m *stun.Message, local, remot // request with an appropriate error code response (e.g., 400) // [RFC5389]. - ok, pendingRequest := s.agent.handleInboundBindingSuccess(m.TransactionID) + ok, pendingRequest, rtt := s.agent.handleInboundBindingSuccess(m.TransactionID) if !ok { s.log.Warnf("Discard message from (%s), unknown TransactionID 0x%x", remote, m.TransactionID) + return } @@ -222,57 +244,63 @@ func (s *controlledSelector) HandleSuccessResponse(m *stun.Message, local, remot // Assert that NAT is not symmetric // https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1 if !addrEqual(transactionAddr, remoteAddr) { - s.log.Debugf("Discard message: transaction source and destination does not match expected(%s), actual(%s)", transactionAddr, remote) + s.log.Debugf( + "Discard message: transaction source and destination does not match expected(%s), actual(%s)", + transactionAddr, + remote, + ) + return } - s.log.Tracef("Inbound STUN (SuccessResponse) from %s to %s", remote.String(), local.String()) + s.log.Tracef("Inbound STUN (SuccessResponse) from %s to %s", remote, local) - p := s.agent.findPair(local, remote) - if p == nil { + pair := s.agent.findPair(local, remote) + if pair == nil { // This shouldn't happen s.log.Error("Success response from invalid candidate pair") + return } - ok = p.markBindingResponse(m.TransactionID) - if ok { - s.agent.onSuccessfulSelectedPairBindingResponse(p) - } - - p.state = CandidatePairStateSucceeded - s.log.Tracef("Found valid candidate pair: %s", p) - if p.nominateOnBindingSuccess { + pair.state = CandidatePairStateSucceeded + s.log.Tracef("Found valid candidate pair: %s", pair) + if pair.nominateOnBindingSuccess { if selectedPair := s.agent.getSelectedPair(); selectedPair == nil || - (selectedPair != p && selectedPair.priority() <= p.priority()) { - s.agent.setSelectedPair(p) - } else if selectedPair != p { - s.log.Tracef("Ignore nominate new pair %s, already nominated pair %s", p, selectedPair) + (selectedPair != pair && + (!s.agent.needsToCheckPriorityOnNominated() || selectedPair.priority() <= pair.priority())) { + s.agent.setSelectedPair(pair) + } else if selectedPair != pair { + s.log.Tracef("Ignore nominate new pair %s, already nominated pair %s", pair, selectedPair) } } + + pair.UpdateRoundTripTime(rtt) } -func (s *controlledSelector) HandleBindingRequest(m *stun.Message, local, remote Candidate) { - useCandidate := m.Contains(stun.AttrUseCandidate) - - p := s.agent.findPair(local, remote) - if p == nil { - p = s.agent.addPair(local, remote) +func (s *controlledSelector) HandleBindingRequest(message *stun.Message, local, remote Candidate) { //nolint:cyclop + pair := s.agent.findPair(local, remote) + if pair == nil { + pair = s.agent.addPair(local, remote) } + pair.UpdateRequestReceived() - if useCandidate { + if message.Contains(stun.AttrUseCandidate) { //nolint:nestif // https://tools.ietf.org/html/rfc8445#section-7.3.1.5 - if p.state == CandidatePairStateSucceeded { + if pair.state == CandidatePairStateSucceeded { // If the state of this pair is Succeeded, it means that the check // previously sent by this pair produced a successful response and // generated a valid pair (Section 7.2.5.3.2). The agent sets the // nominated flag value of the valid pair to true. - if selectedPair := s.agent.getSelectedPair(); selectedPair == nil || - (selectedPair != p && selectedPair.priority() <= p.priority()) { - s.agent.setSelectedPair(p) - } else if selectedPair != p { - s.log.Tracef("Ignore nominate new pair %s, already nominated pair %s", p, selectedPair) + selectedPair := s.agent.getSelectedPair() + if selectedPair == nil || + (selectedPair != pair && + (!s.agent.needsToCheckPriorityOnNominated() || + selectedPair.priority() <= pair.priority())) { + s.agent.setSelectedPair(pair) + } else if selectedPair != pair { + s.log.Tracef("Ignore nominate new pair %s, already nominated pair %s", pair, selectedPair) } } else { // If the received Binding request triggered a new check to be @@ -283,19 +311,25 @@ func (s *controlledSelector) HandleBindingRequest(m *stun.Message, local, remote // MUST remove the candidate pair from the valid list, set the // candidate pair state to Failed, and set the checklist state to // Failed. - p.nominateOnBindingSuccess = true + pair.nominateOnBindingSuccess = true } } - s.agent.sendBindingSuccess(m, local, remote) + s.agent.sendBindingSuccess(message, local, remote) s.PingCandidate(local, remote) + + if s.agent.userBindingRequestHandler != nil { + if shouldSwitch := s.agent.userBindingRequestHandler(message, local, remote, pair); shouldSwitch { + s.agent.setSelectedPair(pair) + } + } } type liteSelector struct { pairCandidateSelector } -// A lite selector should not contact candidates +// A lite selector should not contact candidates. func (s *liteSelector) ContactCandidates() { if _, ok := s.pairCandidateSelector.(*controllingSelector); ok { //nolint:godox diff --git a/selection_test.go b/selection_test.go new file mode 100644 index 0000000..810df8e --- /dev/null +++ b/selection_test.go @@ -0,0 +1,160 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// SPDX-License-Identifier: MIT + +//go:build !js +// +build !js + +package ice + +import ( + "bytes" + "context" + "errors" + "io" + "net" + "sync/atomic" + "testing" + "time" + + "github.com/pion/stun/v3" + "github.com/pion/transport/v3/test" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func sendUntilDone(t *testing.T, writingConn, readingConn net.Conn, maxAttempts int) bool { + t.Helper() + + testMessage := []byte("Hello World") + testBuffer := make([]byte, len(testMessage)) + + readDone, readDoneCancel := context.WithCancel(context.Background()) + go func() { + _, err := readingConn.Read(testBuffer) + if errors.Is(err, io.EOF) { + return + } + + require.NoError(t, err) + require.True(t, bytes.Equal(testMessage, testBuffer)) + + readDoneCancel() + }() + + attempts := 0 + for { + select { + case <-time.After(5 * time.Millisecond): + if attempts > maxAttempts { + return false + } + + _, err := writingConn.Write(testMessage) + require.NoError(t, err) + attempts++ + case <-readDone.Done(): + return true + } + } +} + +func TestBindingRequestHandler(t *testing.T) { + defer test.CheckRoutines(t)() + defer test.TimeOut(time.Second * 30).Stop() + + var switchToNewCandidatePair, controlledLoggingFired atomic.Value + oneHour := time.Hour + keepaliveInterval := time.Millisecond * 20 + + aNotifier, aConnected := onConnected() + bNotifier, bConnected := onConnected() + controllingAgent, err := NewAgent(&AgentConfig{ + NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, + MulticastDNSMode: MulticastDNSModeDisabled, + KeepaliveInterval: &keepaliveInterval, + CheckInterval: &oneHour, + BindingRequestHandler: func(_ *stun.Message, _, _ Candidate, _ *CandidatePair) bool { + controlledLoggingFired.Store(true) + + return false + }, + }) + require.NoError(t, err) + require.NoError(t, controllingAgent.OnConnectionStateChange(aNotifier)) + + controlledAgent, err := NewAgent(&AgentConfig{ + NetworkTypes: []NetworkType{NetworkTypeUDP4}, + MulticastDNSMode: MulticastDNSModeDisabled, + KeepaliveInterval: &keepaliveInterval, + CheckInterval: &oneHour, + BindingRequestHandler: func(_ *stun.Message, _, _ Candidate, _ *CandidatePair) bool { + // Don't switch candidate pair until we are ready + val, ok := switchToNewCandidatePair.Load().(bool) + + return ok && val + }, + }) + require.NoError(t, err) + require.NoError(t, controlledAgent.OnConnectionStateChange(bNotifier)) + + controlledConn, controllingConn := connect(t, controlledAgent, controllingAgent) + <-aConnected + <-bConnected + + // Assert we have connected and can send data + require.True(t, sendUntilDone(t, controlledConn, controllingConn, 100)) + + // Take the lock on the controlling Agent and unset state + assert.NoError(t, controlledAgent.loop.Run(controlledAgent.loop, func(_ context.Context) { + for net, cs := range controlledAgent.remoteCandidates { + for _, c := range cs { + require.NoError(t, c.close()) + } + delete(controlledAgent.remoteCandidates, net) + } + + for _, c := range controlledAgent.localCandidates[NetworkTypeUDP4] { + cast, ok := c.(*CandidateHost) + require.True(t, ok) + cast.remoteCandidateCaches = map[AddrPort]Candidate{} + } + + controlledAgent.setSelectedPair(nil) + controlledAgent.checklist = make([]*CandidatePair, 0) + })) + + // Assert that Selected Candidate pair has only been unset on Controlled side + candidatePair, err := controlledAgent.GetSelectedCandidatePair() + assert.Nil(t, candidatePair) + assert.NoError(t, err) + + candidatePair, err = controllingAgent.GetSelectedCandidatePair() + assert.NotNil(t, candidatePair) + assert.NoError(t, err) + + // Sending will fail, we no longer have a selected candidate pair + require.False(t, sendUntilDone(t, controlledConn, controllingConn, 20)) + + // Send STUN Binding requests until a new Selected Candidate Pair has been set by BindingRequestHandler + switchToNewCandidatePair.Store(true) + for { + controllingAgent.requestConnectivityCheck() + + candidatePair, err = controlledAgent.GetSelectedCandidatePair() + require.NoError(t, err) + if candidatePair != nil { + break + } + + time.Sleep(time.Millisecond * 5) + } + + // We have a new selected candidate pair because of BindingRequestHandler, test that it works + require.True(t, sendUntilDone(t, controllingConn, controlledConn, 100)) + + fired, ok := controlledLoggingFired.Load().(bool) + require.True(t, ok) + require.True(t, fired) + + closePipe(t, controllingConn, controlledConn) +} diff --git a/stats.go b/stats.go index 9b83bea..30a0c02 100644 --- a/stats.go +++ b/stats.go @@ -7,7 +7,7 @@ import ( "time" ) -// CandidatePairStats contains ICE candidate pair statistics +// CandidatePairStats contains ICE candidate pair statistics. type CandidatePairStats struct { // Timestamp is the timestamp associated with this object. Timestamp time.Time @@ -58,10 +58,22 @@ type CandidatePairStats struct { // (LastRequestTimestamp - FirstRequestTimestamp) / RequestsSent. LastRequestTimestamp time.Time + // FirstResponseTimestamp represents the timestamp at which the first STUN response + // was received on this particular candidate pair. + FirstResponseTimestamp time.Time + // LastResponseTimestamp represents the timestamp at which the last STUN response // was received on this particular candidate pair. LastResponseTimestamp time.Time + // FirstRequestReceivedTimestamp represents the timestamp at which the first + // connectivity check request was received. + FirstRequestReceivedTimestamp time.Time + + // LastRequestReceivedTimestamp represents the timestamp at which the last + // connectivity check request was received. + LastRequestReceivedTimestamp time.Time + // TotalRoundTripTime represents the sum of all round trip time measurements // in seconds since the beginning of the session, based on STUN connectivity // check responses (ResponsesReceived), including those that reply to requests diff --git a/tcp_mux.go b/tcp_mux.go index c5608b3..829eac1 100644 --- a/tcp_mux.go +++ b/tcp_mux.go @@ -13,7 +13,7 @@ import ( "time" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" ) // ErrGetTransportAddress can't convert net.Addr to underlying type (UDPAddr or TCPAddr). @@ -79,20 +79,20 @@ func NewTCPMuxDefault(params TCPMuxParams) *TCPMuxDefault { params.AliveDurationForConnFromStun = 30 * time.Second } - m := &TCPMuxDefault{ + mux := &TCPMuxDefault{ params: ¶ms, connsIPv4: map[string]map[ipAddr]*tcpPacketConn{}, connsIPv6: map[string]map[ipAddr]*tcpPacketConn{}, } - m.wg.Add(1) + mux.wg.Add(1) go func() { - defer m.wg.Done() - m.start() + defer mux.wg.Done() + mux.start() }() - return m + return mux } func (m *TCPMuxDefault) start() { @@ -101,6 +101,7 @@ func (m *TCPMuxDefault) start() { conn, err := m.params.Listener.Accept() if err != nil { m.params.Logger.Infof("Error accepting connection: %s", err) + return } @@ -130,6 +131,7 @@ func (m *TCPMuxDefault) GetConnByUfrag(ufrag string, isIPv6 bool, local net.IP) if conn, ok := m.getConn(ufrag, isIPv6, local); ok { conn.ClearAliveTimer() + return conn, nil } @@ -142,6 +144,7 @@ func (m *TCPMuxDefault) createConn(ufrag string, isIPv6 bool, local net.IP, from return nil, ErrGetTransportAddress } localAddr := *addr + // Note: this is missing zone for IPv6 localAddr.IP = local var alive time.Duration @@ -169,13 +172,15 @@ func (m *TCPMuxDefault) createConn(ufrag string, isIPv6 bool, local net.IP, from m.connsIPv4[ufrag] = conns } } - conns[ipAddr(local.String())] = conn + // Note: this is missing zone for IPv6 + connKey := ipAddr(local.String()) + conns[connKey] = conn m.wg.Add(1) go func() { defer m.wg.Done() <-conn.CloseChannel() - m.removeConnByUfragAndLocalHost(ufrag, local) + m.removeConnByUfragAndLocalHost(ufrag, connKey) }() return conn, nil @@ -188,12 +193,17 @@ func (m *TCPMuxDefault) closeAndLogError(closer io.Closer) { } } -func (m *TCPMuxDefault) handleConn(conn net.Conn) { +func (m *TCPMuxDefault) handleConn(conn net.Conn) { //nolint:cyclop buf := make([]byte, 512) if m.params.FirstStunBindTimeout > 0 { if err := conn.SetReadDeadline(time.Now().Add(m.params.FirstStunBindTimeout)); err != nil { - m.params.Logger.Warnf("Failed to set read deadline for first STUN message: %s to %s , err: %s", conn.RemoteAddr(), conn.LocalAddr(), err) + m.params.Logger.Warnf( + "Failed to set read deadline for first STUN message: %s to %s , err: %s", + conn.RemoteAddr(), + conn.LocalAddr(), + err, + ) } } n, err := readStreamingPacket(conn, buf) @@ -204,6 +214,7 @@ func (m *TCPMuxDefault) handleConn(conn net.Conn) { m.params.Logger.Warnf("Error reading first packet from %s: %s", conn.RemoteAddr(), err) } m.closeAndLogError(conn) + return } if err = conn.SetReadDeadline(time.Time{}); err != nil { @@ -220,12 +231,14 @@ func (m *TCPMuxDefault) handleConn(conn net.Conn) { if err = msg.Decode(); err != nil { m.closeAndLogError(conn) m.params.Logger.Warnf("Failed to handle decode ICE from %s to %s: %v", conn.RemoteAddr(), conn.LocalAddr(), err) + return } if m == nil || msg.Type.Method != stun.MethodBinding { // Not a STUN m.closeAndLogError(conn) m.params.Logger.Warnf("Not a STUN message from %s to %s", conn.RemoteAddr(), conn.LocalAddr()) + return } @@ -236,7 +249,12 @@ func (m *TCPMuxDefault) handleConn(conn net.Conn) { attr, err := msg.Get(stun.AttrUsername) if err != nil { m.closeAndLogError(conn) - m.params.Logger.Warnf("No Username attribute in STUN message from %s to %s", conn.RemoteAddr(), conn.LocalAddr()) + m.params.Logger.Warnf( + "No Username attribute in STUN message from %s to %s", + conn.RemoteAddr(), + conn.LocalAddr(), + ) + return } @@ -246,7 +264,12 @@ func (m *TCPMuxDefault) handleConn(conn net.Conn) { host, _, err := net.SplitHostPort(conn.RemoteAddr().String()) if err != nil { m.closeAndLogError(conn) - m.params.Logger.Warnf("Failed to get host in STUN message from %s to %s", conn.RemoteAddr(), conn.LocalAddr()) + m.params.Logger.Warnf( + "Failed to get host in STUN message from %s to %s", + conn.RemoteAddr(), + conn.LocalAddr(), + ) + return } @@ -255,17 +278,28 @@ func (m *TCPMuxDefault) handleConn(conn net.Conn) { localAddr, ok := conn.LocalAddr().(*net.TCPAddr) if !ok { m.closeAndLogError(conn) - m.params.Logger.Warnf("Failed to get local tcp address in STUN message from %s to %s", conn.RemoteAddr(), conn.LocalAddr()) + m.params.Logger.Warnf( + "Failed to get local tcp address in STUN message from %s to %s", + conn.RemoteAddr(), + conn.LocalAddr(), + ) + return } m.mu.Lock() + packetConn, ok := m.getConn(ufrag, isIPv6, localAddr.IP) if !ok { packetConn, err = m.createConn(ufrag, isIPv6, localAddr.IP, true) if err != nil { m.mu.Unlock() m.closeAndLogError(conn) - m.params.Logger.Warnf("Failed to create packetConn for STUN message from %s to %s", conn.RemoteAddr(), conn.LocalAddr()) + m.params.Logger.Warnf( + "Failed to create packetConn for STUN message from %s to %s", + conn.RemoteAddr(), + conn.LocalAddr(), + ) + return } } @@ -273,7 +307,13 @@ func (m *TCPMuxDefault) handleConn(conn net.Conn) { if err := packetConn.AddConn(conn, buf); err != nil { m.closeAndLogError(conn) - m.params.Logger.Warnf("Error adding conn to tcpPacketConn from %s to %s: %s", conn.RemoteAddr(), conn.LocalAddr(), err) + m.params.Logger.Warnf( + "Error adding conn to tcpPacketConn from %s to %s: %s", + conn.RemoteAddr(), + conn.LocalAddr(), + err, + ) + return } } @@ -334,15 +374,14 @@ func (m *TCPMuxDefault) RemoveConnByUfrag(ufrag string) { } } -func (m *TCPMuxDefault) removeConnByUfragAndLocalHost(ufrag string, local net.IP) { +func (m *TCPMuxDefault) removeConnByUfragAndLocalHost(ufrag string, localIPAddr ipAddr) { removedConns := make([]*tcpPacketConn, 0, 4) - localIP := ipAddr(local.String()) // Keep lock section small to avoid deadlock with conn lock m.mu.Lock() if conns, ok := m.connsIPv4[ufrag]; ok { - if conn, ok := conns[localIP]; ok { - delete(conns, localIP) + if conn, ok := conns[localIPAddr]; ok { + delete(conns, localIPAddr) if len(conns) == 0 { delete(m.connsIPv4, ufrag) } @@ -350,8 +389,8 @@ func (m *TCPMuxDefault) removeConnByUfragAndLocalHost(ufrag string, local net.IP } } if conns, ok := m.connsIPv6[ufrag]; ok { - if conn, ok := conns[localIP]; ok { - delete(conns, localIP) + if conn, ok := conns[localIPAddr]; ok { + delete(conns, localIPAddr) if len(conns) == 0 { delete(m.connsIPv6, ufrag) } @@ -375,7 +414,9 @@ func (m *TCPMuxDefault) getConn(ufrag string, isIPv6 bool, local net.IP) (val *t conns, ok = m.connsIPv4[ufrag] } if conns != nil { - val, ok = conns[ipAddr(local.String())] + // Note: this is missing zone for IPv6 + connKey := ipAddr(local.String()) + val, ok = conns[connKey] } return @@ -423,7 +464,7 @@ func readStreamingPacket(conn net.Conn, buf []byte) (int, error) { func writeStreamingPacket(conn net.Conn, buf []byte) (int, error) { bufCopy := make([]byte, streamingPacketHeaderLen+len(buf)) - binary.BigEndian.PutUint16(bufCopy, uint16(len(buf))) + binary.BigEndian.PutUint16(bufCopy, uint16(len(buf))) //nolint:gosec // G115 copy(bufCopy[2:], buf) n, err := conn.Write(bufCopy) diff --git a/tcp_mux_multi.go b/tcp_mux_multi.go index e32acbf..225cefe 100644 --- a/tcp_mux_multi.go +++ b/tcp_mux_multi.go @@ -3,7 +3,9 @@ package ice -import "net" +import ( + "net" +) // AllConnsGetter allows multiple fixed TCP ports to be used, // each of which is multiplexed like TCPMux. AllConnsGetter also acts as @@ -38,6 +40,7 @@ func (m *MultiTCPMuxDefault) GetConnByUfrag(ufrag string, isIPv6 bool, local net if len(m.muxes) == 0 { return nil, errNoTCPMuxAvailable } + return m.muxes[0].GetConnByUfrag(ufrag, isIPv6, local) } @@ -49,7 +52,7 @@ func (m *MultiTCPMuxDefault) RemoveConnByUfrag(ufrag string) { } } -// GetAllConns returns a PacketConn for each underlying TCPMux +// GetAllConns returns a PacketConn for each underlying TCPMux. func (m *MultiTCPMuxDefault) GetAllConns(ufrag string, isIPv6 bool, local net.IP) ([]net.PacketConn, error) { if len(m.muxes) == 0 { // Make sure that we either return at least one connection or an error. @@ -66,10 +69,11 @@ func (m *MultiTCPMuxDefault) GetAllConns(ufrag string, isIPv6 bool, local net.IP conns = append(conns, conn) } } + return conns, nil } -// Close the multi mux, no further connections could be created +// Close the multi mux, no further connections could be created. func (m *MultiTCPMuxDefault) Close() error { var err error for _, mux := range m.muxes { @@ -77,5 +81,6 @@ func (m *MultiTCPMuxDefault) Close() error { err = e } } + return err } diff --git a/tcp_mux_multi_test.go b/tcp_mux_multi_test.go index 291a29d..a73252c 100644 --- a/tcp_mux_multi_test.go +++ b/tcp_mux_multi_test.go @@ -12,9 +12,8 @@ import ( "testing" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -25,8 +24,7 @@ func TestMultiTCPMux_Recv(t *testing.T) { } { bufSize := bufSize t.Run(name, func(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() loggerFactory := logging.NewDefaultLoggerFactory() @@ -63,7 +61,7 @@ func TestMultiTCPMux_Recv(t *testing.T) { defer func() { _ = pktConn.Close() }() - conn, err := net.DialTCP("tcp", nil, pktConn.LocalAddr().(*net.TCPAddr)) + conn, err := net.DialTCP("tcp", nil, pktConn.LocalAddr().(*net.TCPAddr)) // nolint require.NoError(t, err, "error dialing test TCP connection") msg := stun.New() @@ -77,9 +75,9 @@ func TestMultiTCPMux_Recv(t *testing.T) { recv := make([]byte, n) n2, rAddr, err := pktConn.ReadFrom(recv) require.NoError(t, err, "error receiving data") - assert.Equal(t, conn.LocalAddr(), rAddr, "remote TCP address mismatch") - assert.Equal(t, n, n2, "received byte size mismatch") - assert.Equal(t, msg.Raw, recv, "received bytes mismatch") + require.Equal(t, conn.LocalAddr(), rAddr, "remote TCP address mismatch") + require.Equal(t, n, n2, "received byte size mismatch") + require.Equal(t, msg.Raw, recv, "received bytes mismatch") // Check echo response n, err = pktConn.WriteTo(recv, conn.LocalAddr()) @@ -87,16 +85,15 @@ func TestMultiTCPMux_Recv(t *testing.T) { recvEcho := make([]byte, n) n3, err := readStreamingPacket(conn, recvEcho) require.NoError(t, err, "error receiving echo data") - assert.Equal(t, n2, n3, "received byte size mismatch") - assert.Equal(t, msg.Raw, recvEcho, "received bytes mismatch") + require.Equal(t, n2, n3, "received byte size mismatch") + require.Equal(t, msg.Raw, recvEcho, "received bytes mismatch") } }) } } func TestMultiTCPMux_NoDeadlockWhenClosingUnusedPacketConn(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() loggerFactory := logging.NewDefaultLoggerFactory() @@ -126,6 +123,6 @@ func TestMultiTCPMux_NoDeadlockWhenClosingUnusedPacketConn(t *testing.T) { require.NoError(t, muxMulti.Close(), "error closing tcpMux") conn, err := muxMulti.GetAllConns("test", false, net.IP{127, 0, 0, 1}) - assert.Nil(t, conn, "should receive nil because mux is closed") - assert.Equal(t, io.ErrClosedPipe, err, "should receive error because mux is closed") + require.Nil(t, conn, "should receive nil because mux is closed") + require.Equal(t, io.ErrClosedPipe, err, "should receive error because mux is closed") } diff --git a/tcp_mux_test.go b/tcp_mux_test.go index 0175b77..b33e399 100644 --- a/tcp_mux_test.go +++ b/tcp_mux_test.go @@ -11,9 +11,8 @@ import ( "time" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -26,8 +25,7 @@ func TestTCPMux_Recv(t *testing.T) { } { bufSize := bufSize t.Run(name, func(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() loggerFactory := logging.NewDefaultLoggerFactory() @@ -53,7 +51,7 @@ func TestTCPMux_Recv(t *testing.T) { require.NotNil(t, tcpMux.LocalAddr(), "tcpMux.LocalAddr() is nil") - conn, err := net.DialTCP("tcp", nil, tcpMux.LocalAddr().(*net.TCPAddr)) + conn, err := net.DialTCP("tcp", nil, tcpMux.LocalAddr().(*net.TCPAddr)) // nolint require.NoError(t, err, "error dialing test TCP connection") msg := stun.New() @@ -64,7 +62,10 @@ func TestTCPMux_Recv(t *testing.T) { n, err := writeStreamingPacket(conn, msg.Raw) require.NoError(t, err, "error writing TCP STUN packet") - pktConn, err := tcpMux.GetConnByUfrag("myufrag", false, listener.Addr().(*net.TCPAddr).IP) + listenerAddr, ok := listener.Addr().(*net.TCPAddr) + require.True(t, ok) + + pktConn, err := tcpMux.GetConnByUfrag("myufrag", false, listenerAddr.IP) require.NoError(t, err, "error retrieving muxed connection for ufrag") defer func() { _ = pktConn.Close() @@ -73,9 +74,9 @@ func TestTCPMux_Recv(t *testing.T) { recv := make([]byte, n) n2, rAddr, err := pktConn.ReadFrom(recv) require.NoError(t, err, "error receiving data") - assert.Equal(t, conn.LocalAddr(), rAddr, "remote tcp address mismatch") - assert.Equal(t, n, n2, "received byte size mismatch") - assert.Equal(t, msg.Raw, recv, "received bytes mismatch") + require.Equal(t, conn.LocalAddr(), rAddr, "remote tcp address mismatch") + require.Equal(t, n, n2, "received byte size mismatch") + require.Equal(t, msg.Raw, recv, "received bytes mismatch") // Check echo response n, err = pktConn.WriteTo(recv, conn.LocalAddr()) @@ -83,15 +84,14 @@ func TestTCPMux_Recv(t *testing.T) { recvEcho := make([]byte, n) n3, err := readStreamingPacket(conn, recvEcho) require.NoError(t, err, "error receiving echo data") - assert.Equal(t, n2, n3, "received byte size mismatch") - assert.Equal(t, msg.Raw, recvEcho, "received bytes mismatch") + require.Equal(t, n2, n3, "received byte size mismatch") + require.Equal(t, msg.Raw, recvEcho, "received bytes mismatch") }) } } func TestTCPMux_NoDeadlockWhenClosingUnusedPacketConn(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() loggerFactory := logging.NewDefaultLoggerFactory() @@ -114,19 +114,21 @@ func TestTCPMux_NoDeadlockWhenClosingUnusedPacketConn(t *testing.T) { _ = tcpMux.Close() }() - _, err = tcpMux.GetConnByUfrag("test", false, listener.Addr().(*net.TCPAddr).IP) + listenerAddr, ok := listener.Addr().(*net.TCPAddr) + require.True(t, ok) + + _, err = tcpMux.GetConnByUfrag("test", false, listenerAddr.IP) require.NoError(t, err, "error getting conn by ufrag") require.NoError(t, tcpMux.Close(), "error closing tcpMux") - conn, err := tcpMux.GetConnByUfrag("test", false, listener.Addr().(*net.TCPAddr).IP) - assert.Nil(t, conn, "should receive nil because mux is closed") - assert.Equal(t, io.ErrClosedPipe, err, "should receive error because mux is closed") + conn, err := tcpMux.GetConnByUfrag("test", false, listenerAddr.IP) + require.Nil(t, conn, "should receive nil because mux is closed") + require.Equal(t, io.ErrClosedPipe, err, "should receive error because mux is closed") } func TestTCPMux_FirstPacketTimeout(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() loggerFactory := logging.NewDefaultLoggerFactory() @@ -148,7 +150,7 @@ func TestTCPMux_FirstPacketTimeout(t *testing.T) { require.NotNil(t, tcpMux.LocalAddr(), "tcpMux.LocalAddr() is nil") - conn, err := net.DialTCP("tcp", nil, tcpMux.LocalAddr().(*net.TCPAddr)) + conn, err := net.DialTCP("tcp", nil, tcpMux.LocalAddr().(*net.TCPAddr)) // nolint require.NoError(t, err, "error dialing test TCP connection") defer func() { _ = conn.Close() @@ -163,8 +165,7 @@ func TestTCPMux_FirstPacketTimeout(t *testing.T) { } func TestTCPMux_NoLeakForConnectionFromStun(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() loggerFactory := logging.NewDefaultLoggerFactory() @@ -191,7 +192,7 @@ func TestTCPMux_NoLeakForConnectionFromStun(t *testing.T) { require.NotNil(t, tcpMux.LocalAddr(), "tcpMux.LocalAddr() is nil") t.Run("close connection from stun msg after timeout", func(t *testing.T) { - conn, err := net.DialTCP("tcp", nil, tcpMux.LocalAddr().(*net.TCPAddr)) + conn, err := net.DialTCP("tcp", nil, tcpMux.LocalAddr().(*net.TCPAddr)) // nolint require.NoError(t, err, "error dialing test TCP connection") defer func() { _ = conn.Close() @@ -216,7 +217,7 @@ func TestTCPMux_NoLeakForConnectionFromStun(t *testing.T) { }) t.Run("connection keep alive if access by user", func(t *testing.T) { - conn, err := net.DialTCP("tcp", nil, tcpMux.LocalAddr().(*net.TCPAddr)) + conn, err := net.DialTCP("tcp", nil, tcpMux.LocalAddr().(*net.TCPAddr)) // nolint require.NoError(t, err, "error dialing test TCP connection") defer func() { _ = conn.Close() @@ -236,7 +237,10 @@ func TestTCPMux_NoLeakForConnectionFromStun(t *testing.T) { // wait for the connection to be created time.Sleep(100 * time.Millisecond) - pktConn, err := tcpMux.GetConnByUfrag("myufrag2", false, listener.Addr().(*net.TCPAddr).IP) + listenerAddr, ok := listener.Addr().(*net.TCPAddr) + require.True(t, ok) + + pktConn, err := tcpMux.GetConnByUfrag("myufrag2", false, listenerAddr.IP) require.NoError(t, err, "error retrieving muxed connection for ufrag") defer func() { _ = pktConn.Close() @@ -253,8 +257,8 @@ func TestTCPMux_NoLeakForConnectionFromStun(t *testing.T) { recv := make([]byte, n) n2, rAddr, err := pktConn.ReadFrom(recv) require.NoError(t, err, "error receiving data") - assert.Equal(t, conn.LocalAddr(), rAddr, "remote tcp address mismatch") - assert.Equal(t, n, n2, "received byte size mismatch") - assert.Equal(t, msg.Raw, recv, "received bytes mismatch") + require.Equal(t, conn.LocalAddr(), rAddr, "remote tcp address mismatch") + require.Equal(t, n, n2, "received byte size mismatch") + require.Equal(t, msg.Raw, recv, "received bytes mismatch") }) } diff --git a/tcp_packet_conn.go b/tcp_packet_conn.go index 283f1da..b7e7162 100644 --- a/tcp_packet_conn.go +++ b/tcp_packet_conn.go @@ -36,6 +36,7 @@ func newBufferedConn(conn net.Conn, bufSize int, logger logging.LeveledLogger) n } go bc.writeProcess() + return bc } @@ -44,6 +45,7 @@ func (bc *bufferedConn) Write(b []byte) (int, error) { if err != nil { return n, err } + return n, nil } @@ -57,11 +59,13 @@ func (bc *bufferedConn) writeProcess() { if err != nil { bc.logger.Warnf("Failed to read from buffer: %s", err) + continue } if _, err := bc.Conn.Write(pktBuf[:n]); err != nil { bc.logger.Warnf("Failed to write: %s", err) + continue } } @@ -70,6 +74,7 @@ func (bc *bufferedConn) writeProcess() { func (bc *bufferedConn) Close() error { atomic.StoreInt32(&bc.closed, 1) _ = bc.buf.Close() + return bc.Conn.Close() } @@ -103,7 +108,7 @@ type tcpPacketParams struct { } func newTCPPacketConn(params tcpPacketParams) *tcpPacketConn { - p := &tcpPacketConn{ + packet := &tcpPacketConn{ params: ¶ms, conns: map[string]net.Conn{}, @@ -113,13 +118,13 @@ func newTCPPacketConn(params tcpPacketParams) *tcpPacketConn { } if params.AliveDuration > 0 { - p.aliveTimer = time.AfterFunc(params.AliveDuration, func() { - p.params.Logger.Warn("close tcp packet conn by alive timeout") - _ = p.Close() + packet.aliveTimer = time.AfterFunc(params.AliveDuration, func() { + packet.params.Logger.Warn("close tcp packet conn by alive timeout") + _ = packet.Close() }) } - return p + return packet } func (t *tcpPacketConn) ClearAliveTimer() { @@ -131,7 +136,12 @@ func (t *tcpPacketConn) ClearAliveTimer() { } func (t *tcpPacketConn) AddConn(conn net.Conn, firstPacketData []byte) error { - t.params.Logger.Infof("Added connection: %s remote %s to local %s", conn.RemoteAddr().Network(), conn.RemoteAddr(), conn.LocalAddr()) + t.params.Logger.Infof( + "Added connection: %s remote %s to local %s", + conn.RemoteAddr().Network(), + conn.RemoteAddr(), + conn.LocalAddr(), + ) t.mu.Lock() defer t.mu.Unlock() @@ -180,9 +190,10 @@ func (t *tcpPacketConn) startReading(conn net.Conn) { t.params.Logger.Warnf("Failed to read streaming packet: %s", err) last := t.removeConn(conn) // Only propagate connection closure errors if no other open connection exists. - if last || !(errors.Is(err, io.EOF) || errors.Is(err, net.ErrClosed)) { + if last || (!errors.Is(err, io.EOF) && !errors.Is(err, net.ErrClosed)) { t.handleRecv(streamingPacket{nil, conn.RemoteAddr(), err}) } + return } @@ -236,6 +247,7 @@ func (t *tcpPacketConn) ReadFrom(b []byte) (n int, rAddr net.Addr, err error) { n = len(pkt.Data) copy(b, pkt.Data[:n]) + return n, pkt.RAddr, err } @@ -252,6 +264,7 @@ func (t *tcpPacketConn) WriteTo(buf []byte, rAddr net.Addr) (n int, err error) { n, err = writeStreamingPacket(conn, buf) if err != nil { t.params.Logger.Tracef("%w %s", errWrite, rAddr) + return n, err } @@ -272,6 +285,7 @@ func (t *tcpPacketConn) removeConn(conn net.Conn) bool { t.closeAndLogError(conn) delete(t.conns, conn.RemoteAddr().String()) + return len(t.conns) == 0 } diff --git a/tcptype_test.go b/tcptype_test.go index 8a35bf1..af075d5 100644 --- a/tcptype_test.go +++ b/tcptype_test.go @@ -6,21 +6,21 @@ package ice import ( "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestTCPType(t *testing.T) { var tcpType TCPType - assert.Equal(t, TCPTypeUnspecified, tcpType) - assert.Equal(t, TCPTypeActive, NewTCPType("active")) - assert.Equal(t, TCPTypePassive, NewTCPType("passive")) - assert.Equal(t, TCPTypeSimultaneousOpen, NewTCPType("so")) - assert.Equal(t, TCPTypeUnspecified, NewTCPType("something else")) + require.Equal(t, TCPTypeUnspecified, tcpType) + require.Equal(t, TCPTypeActive, NewTCPType("active")) + require.Equal(t, TCPTypePassive, NewTCPType("passive")) + require.Equal(t, TCPTypeSimultaneousOpen, NewTCPType("so")) + require.Equal(t, TCPTypeUnspecified, NewTCPType("something else")) - assert.Equal(t, "", TCPTypeUnspecified.String()) - assert.Equal(t, "active", TCPTypeActive.String()) - assert.Equal(t, "passive", TCPTypePassive.String()) - assert.Equal(t, "so", TCPTypeSimultaneousOpen.String()) - assert.Equal(t, "Unknown", TCPType(-1).String()) + require.Equal(t, "", TCPTypeUnspecified.String()) + require.Equal(t, "active", TCPTypeActive.String()) + require.Equal(t, "passive", TCPTypePassive.String()) + require.Equal(t, "so", TCPTypeSimultaneousOpen.String()) + require.Equal(t, "Unknown", TCPType(-1).String()) } diff --git a/transport.go b/transport.go index d8b1a6e..81c2a0a 100644 --- a/transport.go +++ b/transport.go @@ -9,7 +9,7 @@ import ( "sync/atomic" "time" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" ) // Dial connects to the remote agent, acting as the controlling ice agent. @@ -27,23 +27,23 @@ func (a *Agent) Accept(ctx context.Context, remoteUfrag, remotePwd string) (*Con // Conn represents the ICE connection. // At the moment the lifetime of the Conn is equal to the Agent. type Conn struct { - bytesReceived uint64 - bytesSent uint64 + bytesReceived atomic.Uint64 + bytesSent atomic.Uint64 agent *Agent } -// BytesSent returns the number of bytes sent +// BytesSent returns the number of bytes sent. func (c *Conn) BytesSent() uint64 { - return atomic.LoadUint64(&c.bytesSent) + return c.bytesSent.Load() } -// BytesReceived returns the number of bytes received +// BytesReceived returns the number of bytes received. func (c *Conn) BytesReceived() uint64 { - return atomic.LoadUint64(&c.bytesReceived) + return c.bytesReceived.Load() } func (a *Agent) connect(ctx context.Context, isControlling bool, remoteUfrag, remotePwd string) (*Conn, error) { - err := a.ok() + err := a.loop.Err() if err != nil { return nil, err } @@ -54,8 +54,8 @@ func (a *Agent) connect(ctx context.Context, isControlling bool, remoteUfrag, re // Block until pair selected select { - case <-a.done: - return nil, a.getErr() + case <-a.loop.Done(): + return nil, a.loop.Err() case <-ctx.Done(): return nil, ErrCanceledByCaller case <-a.onConnected: @@ -68,31 +68,32 @@ func (a *Agent) connect(ctx context.Context, isControlling bool, remoteUfrag, re // Read implements the Conn Read method. func (c *Conn) Read(p []byte) (int, error) { - err := c.agent.ok() + err := c.agent.loop.Err() if err != nil { return 0, err } n, err := c.agent.buf.Read(p) - atomic.AddUint64(&c.bytesReceived, uint64(n)) + c.bytesReceived.Add(uint64(n)) //nolint:gosec // G115 + return n, err } // Write implements the Conn Write method. -func (c *Conn) Write(p []byte) (int, error) { - err := c.agent.ok() +func (c *Conn) Write(packet []byte) (int, error) { + err := c.agent.loop.Err() if err != nil { return 0, err } - if stun.IsMessage(p) { + if stun.IsMessage(packet) { return 0, errWriteSTUNMessageToIceConn } pair := c.agent.getSelectedPair() if pair == nil { - if err = c.agent.run(c.agent.context(), func(ctx context.Context, a *Agent) { - pair = a.getBestValidCandidatePair() + if err = c.agent.loop.Run(c.agent.loop, func(_ context.Context) { + pair = c.agent.getBestValidCandidatePair() }); err != nil { return 0, err } @@ -102,8 +103,9 @@ func (c *Conn) Write(p []byte) (int, error) { } } - atomic.AddUint64(&c.bytesSent, uint64(len(p))) - return pair.Write(p) + c.bytesSent.Add(uint64(len(packet))) + + return pair.Write(packet) } // Close implements the Conn Close method. It is used to close @@ -132,17 +134,17 @@ func (c *Conn) RemoteAddr() net.Addr { return pair.Remote.addr() } -// SetDeadline is a stub +// SetDeadline is a stub. func (c *Conn) SetDeadline(time.Time) error { return nil } -// SetReadDeadline is a stub +// SetReadDeadline is a stub. func (c *Conn) SetReadDeadline(time.Time) error { return nil } -// SetWriteDeadline is a stub +// SetWriteDeadline is a stub. func (c *Conn) SetWriteDeadline(time.Time) error { return nil } diff --git a/transport_test.go b/transport_test.go index e11ba37..0377f81 100644 --- a/transport_test.go +++ b/transport_test.go @@ -9,37 +9,36 @@ package ice import ( "context" "net" + "net/netip" "sync" "testing" "time" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" + "github.com/stretchr/testify/require" ) func TestStressDuplex(t *testing.T) { // Check for leaking routines - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 20) - defer lim.Stop() + defer test.TimeOut(time.Second * 20).Stop() // Run the test stressDuplex(t) } -func testTimeout(t *testing.T, c *Conn, timeout time.Duration) { +func testTimeout(t *testing.T, conn *Conn, timeout time.Duration) { + t.Helper() + const pollRate = 100 * time.Millisecond const margin = 20 * time.Millisecond // Allow 20msec error in time ticker := time.NewTicker(pollRate) defer func() { ticker.Stop() - err := c.Close() - if err != nil { - t.Error(err) - } + require.NoError(t, conn.Close()) }() startedAt := time.Now() @@ -49,25 +48,18 @@ func testTimeout(t *testing.T, c *Conn, timeout time.Duration) { var cs ConnectionState - err := c.agent.run(context.Background(), func(ctx context.Context, agent *Agent) { - cs = agent.connectionState - }) - if err != nil { - // We should never get here. - panic(err) - } + require.NoError(t, conn.agent.loop.Run(context.Background(), func(_ context.Context) { + cs = conn.agent.connectionState + })) if cs != ConnectionStateConnected { elapsed := time.Since(startedAt) - if elapsed+margin < timeout { - t.Fatalf("Connection timed out %f msec early", elapsed.Seconds()*1000) - } else { - t.Logf("Connection timed out in %f msec", elapsed.Seconds()*1000) - return - } + require.Less(t, timeout, elapsed+margin) + + return } } - t.Fatalf("Connection failed to time out in time. (expected timeout: %v)", timeout) + t.Fatalf("Connection failed to time out in time. (expected timeout: %v)", timeout) //nolint } func TestTimeout(t *testing.T) { @@ -76,78 +68,48 @@ func TestTimeout(t *testing.T) { } // Check for leaking routines - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 20) - defer lim.Stop() + defer test.TimeOut(time.Second * 20).Stop() t.Run("WithoutDisconnectTimeout", func(t *testing.T) { - ca, cb := pipe(nil) - err := cb.Close() - if err != nil { - // We should never get here. - panic(err) - } - + ca, cb := pipe(t, nil) + require.NoError(t, cb.Close()) testTimeout(t, ca, defaultDisconnectedTimeout) }) t.Run("WithDisconnectTimeout", func(t *testing.T) { - ca, cb := pipeWithTimeout(5*time.Second, 3*time.Second) - err := cb.Close() - if err != nil { - // We should never get here. - panic(err) - } - + ca, cb := pipeWithTimeout(t, 5*time.Second, 3*time.Second) + require.NoError(t, cb.Close()) testTimeout(t, ca, 5*time.Second) }) } func TestReadClosed(t *testing.T) { // Check for leaking routines - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 20) - defer lim.Stop() + defer test.TimeOut(time.Second * 20).Stop() - ca, cb := pipe(nil) - - err := ca.Close() - if err != nil { - // We should never get here. - panic(err) - } - - err = cb.Close() - if err != nil { - // We should never get here. - panic(err) - } + ca, cb := pipe(t, nil) + require.NoError(t, ca.Close()) + require.NoError(t, cb.Close()) empty := make([]byte, 10) - _, err = ca.Read(empty) - if err == nil { - t.Fatalf("Reading from a closed channel should return an error") - } + _, err := ca.Read(empty) + require.Error(t, err) } func stressDuplex(t *testing.T) { - ca, cb := pipe(nil) + t.Helper() + + ca, cb := pipe(t, nil) defer func() { - err := ca.Close() - if err != nil { - t.Fatal(err) - } - err = cb.Close() - if err != nil { - t.Fatal(err) - } + require.NoError(t, ca.Close()) + require.NoError(t, cb.Close()) }() opt := test.Options{ @@ -155,57 +117,55 @@ func stressDuplex(t *testing.T) { MsgCount: 1, // Order not reliable due to UDP & potentially multiple candidate pairs. } - err := test.StressDuplex(ca, cb, opt) - if err != nil { - t.Fatal(err) - } + require.NoError(t, test.StressDuplex(ca, cb, opt)) } -func check(err error) { - if err != nil { - panic(err) - } -} - -func gatherAndExchangeCandidates(aAgent, bAgent *Agent) { +func gatherAndExchangeCandidates(t *testing.T, aAgent, bAgent *Agent) { + t.Helper() var wg sync.WaitGroup wg.Add(2) - check(aAgent.OnCandidate(func(candidate Candidate) { + require.NoError(t, aAgent.OnCandidate(func(candidate Candidate) { if candidate == nil { wg.Done() } })) - check(aAgent.GatherCandidates()) + require.NoError(t, aAgent.GatherCandidates()) - check(bAgent.OnCandidate(func(candidate Candidate) { + require.NoError(t, bAgent.OnCandidate(func(candidate Candidate) { if candidate == nil { wg.Done() } })) - check(bAgent.GatherCandidates()) + require.NoError(t, bAgent.GatherCandidates()) wg.Wait() candidates, err := aAgent.GetLocalCandidates() - check(err) + require.NoError(t, err) + for _, c := range candidates { + if addr, parseErr := netip.ParseAddr(c.Address()); parseErr == nil { + require.False(t, shouldFilterLocationTrackedIP(addr)) + } candidateCopy, copyErr := c.copy() - check(copyErr) - check(bAgent.AddRemoteCandidate(candidateCopy)) + require.NoError(t, copyErr) + require.NoError(t, bAgent.AddRemoteCandidate(candidateCopy)) } candidates, err = bAgent.GetLocalCandidates() - check(err) + + require.NoError(t, err) for _, c := range candidates { candidateCopy, copyErr := c.copy() - check(copyErr) - check(aAgent.AddRemoteCandidate(candidateCopy)) + require.NoError(t, copyErr) + require.NoError(t, aAgent.AddRemoteCandidate(candidateCopy)) } } -func connect(aAgent, bAgent *Agent) (*Conn, *Conn) { - gatherAndExchangeCandidates(aAgent, bAgent) +func connect(t *testing.T, aAgent, bAgent *Agent) (*Conn, *Conn) { + t.Helper() + gatherAndExchangeCandidates(t, aAgent, bAgent) accepted := make(chan struct{}) var aConn *Conn @@ -213,22 +173,24 @@ func connect(aAgent, bAgent *Agent) (*Conn, *Conn) { go func() { var acceptErr error bUfrag, bPwd, acceptErr := bAgent.GetLocalUserCredentials() - check(acceptErr) + require.NoError(t, acceptErr) aConn, acceptErr = aAgent.Accept(context.TODO(), bUfrag, bPwd) - check(acceptErr) + require.NoError(t, acceptErr) close(accepted) }() aUfrag, aPwd, err := aAgent.GetLocalUserCredentials() - check(err) + require.NoError(t, err) bConn, err := bAgent.Dial(context.TODO(), aUfrag, aPwd) - check(err) + require.NoError(t, err) // Ensure accepted <-accepted + return aConn, bConn } -func pipe(defaultConfig *AgentConfig) (*Conn, *Conn) { +func pipe(t *testing.T, defaultConfig *AgentConfig) (*Conn, *Conn) { + t.Helper() var urls []*stun.URI aNotifier, aConnected := onConnected() @@ -243,15 +205,15 @@ func pipe(defaultConfig *AgentConfig) (*Conn, *Conn) { cfg.NetworkTypes = supportedNetworkTypes() aAgent, err := NewAgent(cfg) - check(err) - check(aAgent.OnConnectionStateChange(aNotifier)) + require.NoError(t, err) + require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) bAgent, err := NewAgent(cfg) - check(err) + require.NoError(t, err) - check(bAgent.OnConnectionStateChange(bNotifier)) + require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) - aConn, bConn := connect(aAgent, bAgent) + aConn, bConn := connect(t, aAgent, bAgent) // Ensure pair selected // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair @@ -261,7 +223,8 @@ func pipe(defaultConfig *AgentConfig) (*Conn, *Conn) { return aConn, bConn } -func pipeWithTimeout(disconnectTimeout time.Duration, iceKeepalive time.Duration) (*Conn, *Conn) { +func pipeWithTimeout(t *testing.T, disconnectTimeout time.Duration, iceKeepalive time.Duration) (*Conn, *Conn) { + t.Helper() var urls []*stun.URI aNotifier, aConnected := onConnected() @@ -275,14 +238,14 @@ func pipeWithTimeout(disconnectTimeout time.Duration, iceKeepalive time.Duration } aAgent, err := NewAgent(cfg) - check(err) - check(aAgent.OnConnectionStateChange(aNotifier)) + require.NoError(t, err) + require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) bAgent, err := NewAgent(cfg) - check(err) - check(bAgent.OnConnectionStateChange(bNotifier)) + require.NoError(t, err) + require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) - aConn, bConn := connect(aAgent, bAgent) + aConn, bConn := connect(t, aAgent, bAgent) // Ensure pair selected // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair @@ -294,6 +257,7 @@ func pipeWithTimeout(disconnectTimeout time.Duration, iceKeepalive time.Duration func onConnected() (func(ConnectionState), chan struct{}) { done := make(chan struct{}) + return func(state ConnectionState) { if state == ConnectionStateConnected { close(done) @@ -301,11 +265,11 @@ func onConnected() (func(ConnectionState), chan struct{}) { }, done } -func randomPort(t testing.TB) int { - t.Helper() +func randomPort(tb testing.TB) int { + tb.Helper() conn, err := net.ListenPacket("udp4", "127.0.0.1:0") if err != nil { - t.Fatalf("failed to pickPort: %v", err) + tb.Fatalf("failed to pickPort: %v", err) } defer func() { _ = conn.Close() @@ -314,54 +278,35 @@ func randomPort(t testing.TB) int { case *net.UDPAddr: return addr.Port default: - t.Fatalf("unknown addr type %T", addr) + tb.Fatalf("unknown addr type %T", addr) + return 0 } } func TestConnStats(t *testing.T) { // Check for leaking routines - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 20) - defer lim.Stop() + defer test.TimeOut(time.Second * 20).Stop() - ca, cb := pipe(nil) - if _, err := ca.Write(make([]byte, 10)); err != nil { - t.Fatal("unexpected error trying to write") - } + ca, cb := pipe(t, nil) + _, err := ca.Write(make([]byte, 10)) + require.NoError(t, err) + defer closePipe(t, ca, cb) var wg sync.WaitGroup wg.Add(1) go func() { buf := make([]byte, 10) - if _, err := cb.Read(buf); err != nil { - panic(errRead) - } + _, err := cb.Read(buf) + require.NoError(t, err) wg.Done() }() wg.Wait() - if ca.BytesSent() != 10 { - t.Fatal("bytes sent don't match") - } - - if cb.BytesReceived() != 10 { - t.Fatal("bytes received don't match") - } - - err := ca.Close() - if err != nil { - // We should never get here. - panic(err) - } - - err = cb.Close() - if err != nil { - // We should never get here. - panic(err) - } + require.Equal(t, uint64(10), ca.BytesSent()) + require.Equal(t, uint64(10), cb.BytesReceived()) } diff --git a/transport_vnet_test.go b/transport_vnet_test.go index e9e0ffc..07e5940 100644 --- a/transport_vnet_test.go +++ b/transport_vnet_test.go @@ -12,31 +12,27 @@ import ( "testing" "time" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" "github.com/pion/transport/v3/vnet" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestRemoteLocalAddr(t *testing.T) { // Check for leaking routines - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() // Limit runtime in case of deadlocks - lim := test.TimeOut(time.Second * 20) - defer lim.Stop() + defer test.TimeOut(time.Second * 20).Stop() // Agent0 is behind 1:1 NAT natType0 := &vnet.NATType{Mode: vnet.NATModeNAT1To1} // Agent1 is behind 1:1 NAT natType1 := &vnet.NATType{Mode: vnet.NATModeNAT1To1} - v, errVnet := buildVNet(natType0, natType1) - if !assert.NoError(t, errVnet, "should succeed") { - return - } - defer v.close() + builtVnet, errVnet := buildVNet(natType0, natType1) + require.NoError(t, errVnet, "should succeed") + defer builtVnet.close() stunServerURL := &stun.URI{ Scheme: stun.SchemeTypeSTUN, @@ -47,17 +43,17 @@ func TestRemoteLocalAddr(t *testing.T) { t.Run("Disconnected Returns nil", func(t *testing.T) { disconnectedAgent, err := NewAgent(&AgentConfig{}) - assert.NoError(t, err) + require.NoError(t, err) disconnectedConn := Conn{agent: disconnectedAgent} - assert.Nil(t, disconnectedConn.RemoteAddr()) - assert.Nil(t, disconnectedConn.LocalAddr()) + require.Nil(t, disconnectedConn.RemoteAddr()) + require.Nil(t, disconnectedConn.LocalAddr()) - assert.NoError(t, disconnectedConn.Close()) + require.NoError(t, disconnectedConn.Close()) }) t.Run("Remote/Local Pair Match between Agents", func(t *testing.T) { - ca, cb := pipeWithVNet(v, + ca, cb := pipeWithVNet(t, builtVnet, &agentTestConfig{ urls: []*stun.URI{stunServerURL}, }, @@ -65,6 +61,7 @@ func TestRemoteLocalAddr(t *testing.T) { urls: []*stun.URI{stunServerURL}, }, ) + defer closePipe(t, ca, cb) aRAddr := ca.RemoteAddr() aLAddr := ca.LocalAddr() @@ -72,27 +69,23 @@ func TestRemoteLocalAddr(t *testing.T) { bLAddr := cb.LocalAddr() // Assert that nothing is nil - assert.NotNil(t, aRAddr) - assert.NotNil(t, aLAddr) - assert.NotNil(t, bRAddr) - assert.NotNil(t, bLAddr) + require.NotNil(t, aRAddr) + require.NotNil(t, aLAddr) + require.NotNil(t, bRAddr) + require.NotNil(t, bLAddr) // Assert addresses - assert.Equal(t, aLAddr.String(), + require.Equal(t, aLAddr.String(), fmt.Sprintf("%s:%d", vnetLocalIPA, bRAddr.(*net.UDPAddr).Port), //nolint:forcetypeassert ) - assert.Equal(t, bLAddr.String(), + require.Equal(t, bLAddr.String(), fmt.Sprintf("%s:%d", vnetLocalIPB, aRAddr.(*net.UDPAddr).Port), //nolint:forcetypeassert ) - assert.Equal(t, aRAddr.String(), + require.Equal(t, aRAddr.String(), fmt.Sprintf("%s:%d", vnetGlobalIPB, bLAddr.(*net.UDPAddr).Port), //nolint:forcetypeassert ) - assert.Equal(t, bRAddr.String(), + require.Equal(t, bRAddr.String(), fmt.Sprintf("%s:%d", vnetGlobalIPA, aLAddr.(*net.UDPAddr).Port), //nolint:forcetypeassert ) - - // Close - assert.NoError(t, ca.Close()) - assert.NoError(t, cb.Close()) }) } diff --git a/udp_mux.go b/udp_mux.go index cf01537..257ef5f 100644 --- a/udp_mux.go +++ b/udp_mux.go @@ -7,17 +7,18 @@ import ( "errors" "io" "net" + "net/netip" "os" "strings" "sync" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3" "github.com/pion/transport/v3/stdnet" ) -// UDPMux allows multiple connections to go over a single UDP port +// UDPMux allows multiple connections to go over a single UDP port. type UDPMux interface { io.Closer GetConn(ufrag string, addr net.Addr) (net.PacketConn, error) @@ -25,7 +26,7 @@ type UDPMux interface { GetListenAddresses() []net.Addr } -// UDPMuxDefault is an implementation of the interface +// UDPMuxDefault is an implementation of the interface. type UDPMuxDefault struct { params UDPMuxParams @@ -36,7 +37,7 @@ type UDPMuxDefault struct { connsIPv4, connsIPv6 map[string]*udpMuxedConn addressMapMu sync.RWMutex - addressMap map[string]*udpMuxedConn + addressMap map[ipPort]*udpMuxedConn // Buffer pool to recycle buffers for net.UDPAddr encodes/decodes pool *sync.Pool @@ -47,12 +48,11 @@ type UDPMuxDefault struct { localAddrsForUnspecified []net.Addr } -const maxAddrSize = 512 - // UDPMuxParams are parameters for UDPMux. type UDPMuxParams struct { - Logger logging.LeveledLogger - UDPConn net.PacketConn + Logger logging.LeveledLogger + UDPConn net.PacketConn + UDPConnString string // Required for gathering local addresses // in case a un UDPConn is passed which does not @@ -60,26 +60,26 @@ type UDPMuxParams struct { Net transport.Net } -// NewUDPMuxDefault creates an implementation of UDPMux -func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault { +// NewUDPMuxDefault creates an implementation of UDPMux. +func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault { //nolint:cyclop if params.Logger == nil { params.Logger = logging.NewDefaultLoggerFactory().NewLogger("ice") } var localAddrsForUnspecified []net.Addr - if addr, ok := params.UDPConn.LocalAddr().(*net.UDPAddr); !ok { + if udpAddr, ok := params.UDPConn.LocalAddr().(*net.UDPAddr); !ok { //nolint:nestif params.Logger.Errorf("LocalAddr is not a net.UDPAddr, got %T", params.UDPConn.LocalAddr()) - } else if ok && addr.IP.IsUnspecified() { + } else if ok && udpAddr.IP.IsUnspecified() { // For unspecified addresses, the correct behavior is to return errListenUnspecified, but // it will break the applications that are already using unspecified UDP connection // with UDPMuxDefault, so print a warn log and create a local address list for mux. params.Logger.Warn("UDPMuxDefault should not listening on unspecified address, use NewMultiUDPMuxFromPort instead") var networks []NetworkType switch { - case addr.IP.To4() != nil: + case udpAddr.IP.To4() != nil: networks = []NetworkType{NetworkTypeUDP4} - case addr.IP.To16() != nil: + case udpAddr.IP.To16() != nil: networks = []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6} default: @@ -93,43 +93,49 @@ func NewUDPMuxDefault(params UDPMuxParams) *UDPMuxDefault { } } - ips, err := localInterfaces(params.Net, nil, nil, networks, true) + _, addrs, err := localInterfaces(params.Net, nil, nil, networks, true) if err == nil { - for _, ip := range ips { - localAddrsForUnspecified = append(localAddrsForUnspecified, &net.UDPAddr{IP: ip, Port: addr.Port}) + localAddrsForUnspecified = make([]net.Addr, len(addrs)) + for i, addr := range addrs { + localAddrsForUnspecified[i] = &net.UDPAddr{ + IP: addr.AsSlice(), + Port: udpAddr.Port, + Zone: addr.Zone(), + } } } else { params.Logger.Errorf("Failed to get local interfaces for unspecified addr: %v", err) } } } + params.UDPConnString = params.UDPConn.LocalAddr().String() - m := &UDPMuxDefault{ - addressMap: map[string]*udpMuxedConn{}, + mux := &UDPMuxDefault{ + addressMap: map[ipPort]*udpMuxedConn{}, params: params, connsIPv4: make(map[string]*udpMuxedConn), connsIPv6: make(map[string]*udpMuxedConn), closedChan: make(chan struct{}, 1), pool: &sync.Pool{ - New: func() interface{} { + New: func() any { // Big enough buffer to fit both packet and address - return newBufferHolder(receiveMTU + maxAddrSize) + return newBufferHolder(receiveMTU) }, }, localAddrsForUnspecified: localAddrsForUnspecified, } - go m.connWorker() + go mux.connWorker() - return m + return mux } -// LocalAddr returns the listening address of this UDPMuxDefault +// LocalAddr returns the listening address of this UDPMuxDefault. func (m *UDPMuxDefault) LocalAddr() net.Addr { return m.params.UDPConn.LocalAddr() } -// GetListenAddresses returns the list of addresses that this mux is listening on +// GetListenAddresses returns the list of addresses that this mux is listening on. func (m *UDPMuxDefault) GetListenAddresses() []net.Addr { if len(m.localAddrsForUnspecified) > 0 { return m.localAddrsForUnspecified @@ -138,11 +144,11 @@ func (m *UDPMuxDefault) GetListenAddresses() []net.Addr { return []net.Addr{m.LocalAddr()} } -// GetConn returns a PacketConn given the connection's ufrag and network address -// creates the connection if an existing one can't be found +// GetConn returns a PacketConn given the connection's ufrag and network address. +// creates the connection if an existing one can't be found. func (m *UDPMuxDefault) GetConn(ufrag string, addr net.Addr) (net.PacketConn, error) { // don't check addr for mux using unspecified address - if len(m.localAddrsForUnspecified) == 0 && m.params.UDPConn.LocalAddr().String() != addr.String() { + if len(m.localAddrsForUnspecified) == 0 && m.params.UDPConnString != addr.String() { return nil, errInvalidAddress } @@ -176,11 +182,11 @@ func (m *UDPMuxDefault) GetConn(ufrag string, addr net.Addr) (net.PacketConn, er return c, nil } -// RemoveConnByUfrag stops and removes the muxed packet connection +// RemoveConnByUfrag stops and removes the muxed packet connection. func (m *UDPMuxDefault) RemoveConnByUfrag(ufrag string) { removedConns := make([]*udpMuxedConn, 0, 2) - // Keep lock section small to avoid deadlock with conn lock + // Keep lock section small to avoid deadlock with conn lock. m.mu.Lock() if c, ok := m.connsIPv4[ufrag]; ok { delete(m.connsIPv4, ufrag) @@ -193,7 +199,7 @@ func (m *UDPMuxDefault) RemoveConnByUfrag(ufrag string) { m.mu.Unlock() if len(removedConns) == 0 { - // No need to lock if no connection was found + // No need to lock if no connection was found. return } @@ -208,7 +214,7 @@ func (m *UDPMuxDefault) RemoveConnByUfrag(ufrag string) { } } -// IsClosed returns true if the mux had been closed +// IsClosed returns true if the mux had been closed. func (m *UDPMuxDefault) IsClosed() bool { select { case <-m.closedChan: @@ -218,7 +224,7 @@ func (m *UDPMuxDefault) IsClosed() bool { } } -// Close the mux, no further connections could be created +// Close the mux, no further connections could be created. func (m *UDPMuxDefault) Close() error { var err error m.closeOnce.Do(func() { @@ -239,6 +245,7 @@ func (m *UDPMuxDefault) Close() error { _ = m.params.UDPConn.Close() }) + return err } @@ -246,7 +253,7 @@ func (m *UDPMuxDefault) writeTo(buf []byte, rAddr net.Addr) (n int, err error) { return m.params.UDPConn.WriteTo(buf, rAddr) } -func (m *UDPMuxDefault) registerConnForAddress(conn *udpMuxedConn, addr string) { +func (m *UDPMuxDefault) registerConnForAddress(conn *udpMuxedConn, addr ipPort) { if m.IsClosed() { return } @@ -260,7 +267,7 @@ func (m *UDPMuxDefault) registerConnForAddress(conn *udpMuxedConn, addr string) } m.addressMap[addr] = conn - m.params.Logger.Debugf("Registered %s for %s", addr, conn.params.Key) + m.params.Logger.Debugf("Registered %s for %s", addr.addr.String(), conn.params.Key) } func (m *UDPMuxDefault) createMuxedConn(key string) *udpMuxedConn { @@ -271,10 +278,11 @@ func (m *UDPMuxDefault) createMuxedConn(key string) *udpMuxedConn { LocalAddr: m.LocalAddr(), Logger: m.params.Logger, }) + return c } -func (m *UDPMuxDefault) connWorker() { +func (m *UDPMuxDefault) connWorker() { //nolint:cyclop logger := m.params.Logger defer func() { @@ -296,15 +304,22 @@ func (m *UDPMuxDefault) connWorker() { return } - udpAddr, ok := addr.(*net.UDPAddr) + netUDPAddr, ok := addr.(*net.UDPAddr) if !ok { logger.Errorf("Underlying PacketConn did not return a UDPAddr") + + return + } + udpAddr, err := newIPPort(netUDPAddr.IP, netUDPAddr.Zone, uint16(netUDPAddr.Port)) //nolint:gosec + if err != nil { + logger.Errorf("Failed to create a new IP/Port host pair") + return } // If we have already seen this address dispatch to the appropriate destination m.addressMapMu.Lock() - destinationConn := m.addressMap[addr.String()] + destinationConn := m.addressMap[udpAddr] m.addressMapMu.Unlock() // If we haven't seen this address before but is a STUN packet lookup by ufrag @@ -315,17 +330,19 @@ func (m *UDPMuxDefault) connWorker() { if err = msg.Decode(); err != nil { m.params.Logger.Warnf("Failed to handle decode ICE from %s: %v", addr.String(), err) + continue } attr, stunAttrErr := msg.Get(stun.AttrUsername) if stunAttrErr != nil { m.params.Logger.Warnf("No Username attribute in STUN message from %s", addr.String()) + continue } ufrag := strings.Split(string(attr), ":")[0] - isIPv6 := udpAddr.IP.To4() == nil + isIPv6 := netUDPAddr.IP.To4() == nil m.mu.Lock() destinationConn, _ = m.getConn(ufrag, isIPv6) @@ -333,11 +350,12 @@ func (m *UDPMuxDefault) connWorker() { } if destinationConn == nil { - m.params.Logger.Tracef("Dropping packet from %s, addr: %s", udpAddr.String(), addr.String()) + m.params.Logger.Tracef("Dropping packet from %s, addr: %s", udpAddr.addr, addr) + continue } - if err = destinationConn.writePacket(buf[:n], udpAddr); err != nil { + if err = destinationConn.writePacket(buf[:n], netUDPAddr); err != nil { m.params.Logger.Errorf("Failed to write packet: %v", err) } } @@ -349,11 +367,14 @@ func (m *UDPMuxDefault) getConn(ufrag string, isIPv6 bool) (val *udpMuxedConn, o } else { val, ok = m.connsIPv4[ufrag] } + return } type bufferHolder struct { - buf []byte + next *bufferHolder + buf []byte + addr *net.UDPAddr } func newBufferHolder(size int) *bufferHolder { @@ -361,3 +382,28 @@ func newBufferHolder(size int) *bufferHolder { buf: make([]byte, size), } } + +func (b *bufferHolder) reset() { + b.next = nil + b.addr = nil +} + +type ipPort struct { + addr netip.Addr + port uint16 +} + +// newIPPort create a custom type of address based on netip.Addr and +// port. The underlying ip address passed is converted to IPv6 format +// to simplify ip address handling. +func newIPPort(ip net.IP, zone string, port uint16) (ipPort, error) { + n, ok := netip.AddrFromSlice(ip.To16()) + if !ok { + return ipPort{}, errInvalidIPAddress + } + + return ipPort{ + addr: n.WithZone(zone), + port: port, + }, nil +} diff --git a/udp_mux_multi.go b/udp_mux_multi.go index c46db9b..46c88bb 100644 --- a/udp_mux_multi.go +++ b/udp_mux_multi.go @@ -29,6 +29,7 @@ func NewMultiUDPMuxDefault(muxes ...UDPMux) *MultiUDPMuxDefault { addrToMux[addr.String()] = mux } } + return &MultiUDPMuxDefault{ muxes: muxes, localAddrToMux: addrToMux, @@ -42,6 +43,7 @@ func (m *MultiUDPMuxDefault) GetConn(ufrag string, addr net.Addr) (net.PacketCon if !ok { return nil, errNoUDPMuxAvailable } + return mux.GetConn(ufrag, addr) } @@ -53,7 +55,7 @@ func (m *MultiUDPMuxDefault) RemoveConnByUfrag(ufrag string) { } } -// Close the multi mux, no further connections could be created +// Close the multi mux, no further connections could be created. func (m *MultiUDPMuxDefault) Close() error { var err error for _, mux := range m.muxes { @@ -61,21 +63,23 @@ func (m *MultiUDPMuxDefault) Close() error { err = e } } + return err } -// GetListenAddresses returns the list of addresses that this mux is listening on +// GetListenAddresses returns the list of addresses that this mux is listening on. func (m *MultiUDPMuxDefault) GetListenAddresses() []net.Addr { addrs := make([]net.Addr, 0, len(m.localAddrToMux)) for _, mux := range m.muxes { addrs = append(addrs, mux.GetListenAddresses()...) } + return addrs } // NewMultiUDPMuxFromPort creates an instance of MultiUDPMuxDefault that // listen all interfaces on the provided port. -func NewMultiUDPMuxFromPort(port int, opts ...UDPMuxFromPortOption) (*MultiUDPMuxDefault, error) { +func NewMultiUDPMuxFromPort(port int, opts ...UDPMuxFromPortOption) (*MultiUDPMuxDefault, error) { //nolint:cyclop params := multiUDPMuxFromPortParam{ networks: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, } @@ -90,16 +94,21 @@ func NewMultiUDPMuxFromPort(port int, opts ...UDPMuxFromPortOption) (*MultiUDPMu } } - ips, err := localInterfaces(params.net, params.ifFilter, params.ipFilter, params.networks, params.includeLoopback) + _, addrs, err := localInterfaces(params.net, params.ifFilter, params.ipFilter, params.networks, params.includeLoopback) if err != nil { return nil, err } - conns := make([]net.PacketConn, 0, len(ips)) - for _, ip := range ips { - conn, listenErr := params.net.ListenUDP("udp", &net.UDPAddr{IP: ip, Port: port}) + conns := make([]net.PacketConn, 0, len(addrs)) + for _, addr := range addrs { + conn, listenErr := params.net.ListenUDP("udp", &net.UDPAddr{ + IP: addr.AsSlice(), + Port: port, + Zone: addr.Zone(), + }) if listenErr != nil { err = listenErr + break } if params.readBufferSize > 0 { @@ -115,6 +124,7 @@ func NewMultiUDPMuxFromPort(port int, opts ...UDPMuxFromPortOption) (*MultiUDPMu for _, conn := range conns { _ = conn.Close() } + return nil, err } @@ -131,14 +141,14 @@ func NewMultiUDPMuxFromPort(port int, opts ...UDPMuxFromPortOption) (*MultiUDPMu return NewMultiUDPMuxDefault(muxes...), nil } -// UDPMuxFromPortOption provide options for NewMultiUDPMuxFromPort +// UDPMuxFromPortOption provide options for NewMultiUDPMuxFromPort. type UDPMuxFromPortOption interface { apply(*multiUDPMuxFromPortParam) } type multiUDPMuxFromPortParam struct { - ifFilter func(string) bool - ipFilter func(ip net.IP) bool + ifFilter func(string) (keep bool) + ipFilter func(ip net.IP) (keep bool) networks []NetworkType readBufferSize int writeBufferSize int @@ -155,8 +165,8 @@ func (o *udpMuxFromPortOption) apply(p *multiUDPMuxFromPortParam) { o.f(p) } -// UDPMuxFromPortWithInterfaceFilter set the filter to filter out interfaces that should not be used -func UDPMuxFromPortWithInterfaceFilter(f func(string) bool) UDPMuxFromPortOption { +// UDPMuxFromPortWithInterfaceFilter set the filter to filter out interfaces that should not be used. +func UDPMuxFromPortWithInterfaceFilter(f func(string) (keep bool)) UDPMuxFromPortOption { return &udpMuxFromPortOption{ f: func(p *multiUDPMuxFromPortParam) { p.ifFilter = f @@ -164,8 +174,8 @@ func UDPMuxFromPortWithInterfaceFilter(f func(string) bool) UDPMuxFromPortOption } } -// UDPMuxFromPortWithIPFilter set the filter to filter out IP addresses that should not be used -func UDPMuxFromPortWithIPFilter(f func(ip net.IP) bool) UDPMuxFromPortOption { +// UDPMuxFromPortWithIPFilter set the filter to filter out IP addresses that should not be used. +func UDPMuxFromPortWithIPFilter(f func(ip net.IP) (keep bool)) UDPMuxFromPortOption { return &udpMuxFromPortOption{ f: func(p *multiUDPMuxFromPortParam) { p.ipFilter = f @@ -173,7 +183,7 @@ func UDPMuxFromPortWithIPFilter(f func(ip net.IP) bool) UDPMuxFromPortOption { } } -// UDPMuxFromPortWithNetworks set the networks that should be used. default is both IPv4 and IPv6 +// UDPMuxFromPortWithNetworks set the networks that should be used. default is both IPv4 and IPv6. func UDPMuxFromPortWithNetworks(networks ...NetworkType) UDPMuxFromPortOption { return &udpMuxFromPortOption{ f: func(p *multiUDPMuxFromPortParam) { @@ -182,7 +192,7 @@ func UDPMuxFromPortWithNetworks(networks ...NetworkType) UDPMuxFromPortOption { } } -// UDPMuxFromPortWithReadBufferSize set the UDP connection read buffer size +// UDPMuxFromPortWithReadBufferSize set the UDP connection read buffer size. func UDPMuxFromPortWithReadBufferSize(size int) UDPMuxFromPortOption { return &udpMuxFromPortOption{ f: func(p *multiUDPMuxFromPortParam) { @@ -191,7 +201,7 @@ func UDPMuxFromPortWithReadBufferSize(size int) UDPMuxFromPortOption { } } -// UDPMuxFromPortWithWriteBufferSize set the UDP connection write buffer size +// UDPMuxFromPortWithWriteBufferSize set the UDP connection write buffer size. func UDPMuxFromPortWithWriteBufferSize(size int) UDPMuxFromPortOption { return &udpMuxFromPortOption{ f: func(p *multiUDPMuxFromPortParam) { @@ -200,7 +210,7 @@ func UDPMuxFromPortWithWriteBufferSize(size int) UDPMuxFromPortOption { } } -// UDPMuxFromPortWithLogger set the logger for the created UDPMux +// UDPMuxFromPortWithLogger set the logger for the created UDPMux. func UDPMuxFromPortWithLogger(logger logging.LeveledLogger) UDPMuxFromPortOption { return &udpMuxFromPortOption{ f: func(p *multiUDPMuxFromPortParam) { @@ -209,7 +219,7 @@ func UDPMuxFromPortWithLogger(logger logging.LeveledLogger) UDPMuxFromPortOption } } -// UDPMuxFromPortWithLoopback set loopback interface should be included +// UDPMuxFromPortWithLoopback set loopback interface should be included. func UDPMuxFromPortWithLoopback() UDPMuxFromPortOption { return &udpMuxFromPortOption{ f: func(p *multiUDPMuxFromPortParam) { diff --git a/udp_mux_multi_test.go b/udp_mux_multi_test.go index a38702c..4fc64de 100644 --- a/udp_mux_multi_test.go +++ b/udp_mux_multi_test.go @@ -8,7 +8,6 @@ package ice import ( "net" - "strings" "sync" "testing" "time" @@ -18,11 +17,9 @@ import ( ) func TestMultiUDPMux(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() conn1, err := net.ListenUDP(udp, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) require.NoError(t, err) @@ -82,6 +79,8 @@ func TestMultiUDPMux(t *testing.T) { } func testMultiUDPMuxConnections(t *testing.T, udpMuxMulti *MultiUDPMuxDefault, ufrag string, network string) { + t.Helper() + addrs := udpMuxMulti.GetListenAddresses() pktConns := make([]net.PacketConn, 0, len(addrs)) for _, addr := range addrs { @@ -104,25 +103,19 @@ func testMultiUDPMuxConnections(t *testing.T, udpMuxMulti *MultiUDPMuxDefault, u // Try talking with each PacketConn for _, pktConn := range pktConns { - remoteConn, err := net.DialUDP(network, nil, pktConn.LocalAddr().(*net.UDPAddr)) + remoteConn, err := net.DialUDP(network, nil, pktConn.LocalAddr().(*net.UDPAddr)) // nolint require.NoError(t, err, "error dialing test UDP connection") testMuxConnectionPair(t, pktConn, remoteConn, ufrag) } } func TestUnspecifiedUDPMux(t *testing.T) { - report := test.CheckRoutines(t) - defer report() + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() muxPort := 7778 - udpMuxMulti, err := NewMultiUDPMuxFromPort(muxPort, UDPMuxFromPortWithInterfaceFilter(func(s string) bool { - defaultDockerBridgeNetwork := strings.Contains(s, "docker") - customDockerBridgeNetwork := strings.Contains(s, "br-") - return !defaultDockerBridgeNetwork && !customDockerBridgeNetwork - })) + udpMuxMulti, err := NewMultiUDPMuxFromPort(muxPort, UDPMuxFromPortWithInterfaceFilter(problematicNetworkInterfaces)) require.NoError(t, err) require.GreaterOrEqual(t, len(udpMuxMulti.muxes), 1, "at least have 1 muxes") diff --git a/udp_mux_test.go b/udp_mux_test.go index 01f52c5..0aa7344 100644 --- a/udp_mux_test.go +++ b/udp_mux_test.go @@ -8,24 +8,22 @@ package ice import ( "crypto/rand" - "crypto/sha1" //nolint:gosec + "crypto/sha256" "encoding/binary" "net" "sync" "testing" "time" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3/test" "github.com/stretchr/testify/require" ) -func TestUDPMux(t *testing.T) { - report := test.CheckRoutines(t) - defer report() +func TestUDPMux(t *testing.T) { //nolint:cyclop + defer test.CheckRoutines(t)() - lim := test.TimeOut(time.Second * 30) - defer lim.Stop() + defer test.TimeOut(time.Second * 30).Stop() conn4, err := net.ListenUDP(udp, &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1)}) require.NoError(t, err) @@ -52,13 +50,31 @@ func TestUDPMux(t *testing.T) { network string } - for _, subTest := range []testCase{ + testCases := []testCase{ {name: "IPv4loopback", conn: conn4, network: udp4}, {name: "IPv6loopback", conn: conn6, network: udp6}, {name: "Unspecified", conn: connUnspecified, network: udp}, {name: "IPv4Unspecified", conn: conn4Unspecified, network: udp4}, {name: "IPv6Unspecified", conn: conn6Unspecified, network: udp6}, - } { + } + + if ipv6Available(t) { + addr6 := getLocalIPAddress(t, NetworkTypeUDP6) + + conn6Unspecified, listenEerr := net.ListenUDP(udp, &net.UDPAddr{ + IP: addr6.AsSlice(), + Zone: addr6.Zone(), + }) + if listenEerr != nil { + t.Log("IPv6 is not supported on this machine") + } + + testCases = append(testCases, + testCase{name: "IPv6Specified", conn: conn6Unspecified, network: udp6}, + ) + } + + for _, subTest := range testCases { network, conn := subTest.network, subTest.conn if udpConn, ok := conn.(*net.UDPConn); !ok || udpConn == nil { continue @@ -110,47 +126,9 @@ func TestUDPMux(t *testing.T) { } } -func TestAddressEncoding(t *testing.T) { - cases := []struct { - name string - addr net.UDPAddr - }{ - { - name: "empty address", - }, - { - name: "ipv4", - addr: net.UDPAddr{ - IP: net.IPv4(244, 120, 0, 5), - Port: 6000, - Zone: "", - }, - }, - { - name: "ipv6", - addr: net.UDPAddr{ - IP: net.IPv6loopback, - Port: 2500, - Zone: "zone", - }, - }, - } - - for _, c := range cases { - addr := c.addr - t.Run(c.name, func(t *testing.T) { - buf := make([]byte, maxAddrSize) - n, err := encodeUDPAddr(&addr, buf) - require.NoError(t, err) - - parsedAddr, err := decodeUDPAddr(buf[:n]) - require.NoError(t, err) - require.EqualValues(t, &addr, parsedAddr) - }) - } -} - func testMuxConnection(t *testing.T, udpMux *UDPMuxDefault, ufrag string, network string) { + t.Helper() + pktConn, err := udpMux.GetConn(ufrag, udpMux.LocalAddr()) require.NoError(t, err, "error retrieving muxed connection for ufrag") defer func() { @@ -169,6 +147,8 @@ func testMuxConnection(t *testing.T, udpMux *UDPMuxDefault, ufrag string, networ } func testMuxConnectionPair(t *testing.T, pktConn net.PacketConn, remoteConn *net.UDPConn, ufrag string) { + t.Helper() + // Initial messages are dropped _, err := remoteConn.Write([]byte("dropped bytes")) require.NoError(t, err) @@ -240,13 +220,13 @@ func testMuxConnectionPair(t *testing.T, pktConn net.PacketConn, remoteConn *net for written := 0; written < targetSize; { buf := make([]byte, receiveMTU) // Byte 0-4: sequence - // Bytes 4-24: sha1 checksum - // Bytes2 4-mtu: random data - _, err := rand.Read(buf[24:]) + // Bytes 4-36: sha256 checksum + // Bytes2 36-mtu: random data + _, err := rand.Read(buf[36:]) require.NoError(t, err) - h := sha1.Sum(buf[24:]) //nolint:gosec - copy(buf[4:24], h[:]) - binary.LittleEndian.PutUint32(buf[0:4], uint32(sequence)) + h := sha256.Sum256(buf[36:]) + copy(buf[4:36], h[:]) + binary.LittleEndian.PutUint32(buf[0:4], uint32(sequence)) //nolint:gosec // G115 _, err = remoteConn.Write(buf) require.NoError(t, err) @@ -262,18 +242,21 @@ func testMuxConnectionPair(t *testing.T, pktConn net.PacketConn, remoteConn *net } func verifyPacket(t *testing.T, b []byte, nextSeq uint32) { + t.Helper() + readSeq := binary.LittleEndian.Uint32(b[0:4]) require.Equal(t, nextSeq, readSeq) - h := sha1.Sum(b[24:]) //nolint:gosec - require.Equal(t, h[:], b[4:24]) + h := sha256.Sum256(b[36:]) + require.Equal(t, h[:], b[4:36]) } func TestUDPMux_Agent_Restart(t *testing.T) { oneSecond := time.Second - connA, connB := pipe(&AgentConfig{ + connA, connB := pipe(t, &AgentConfig{ DisconnectedTimeout: &oneSecond, FailedTimeout: &oneSecond, }) + defer closePipe(t, connA, connB) aNotifier, aConnected := onConnected() require.NoError(t, connA.agent.OnConnectionStateChange(aNotifier)) @@ -296,12 +279,9 @@ func TestUDPMux_Agent_Restart(t *testing.T) { require.NoError(t, connA.agent.SetRemoteCredentials(ufragB, pwdB)) require.NoError(t, connB.agent.SetRemoteCredentials(ufragA, pwdA)) - gatherAndExchangeCandidates(connA.agent, connB.agent) + gatherAndExchangeCandidates(t, connA.agent, connB.agent) // Wait until both have gone back to connected <-aConnected <-bConnected - - require.NoError(t, connA.agent.Close()) - require.NoError(t, connB.agent.Close()) } diff --git a/udp_mux_universal.go b/udp_mux_universal.go index e7d9004..50c80af 100644 --- a/udp_mux_universal.go +++ b/udp_mux_universal.go @@ -9,7 +9,7 @@ import ( "time" "github.com/pion/logging" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/pion/transport/v3" ) @@ -29,7 +29,8 @@ type UniversalUDPMuxDefault struct { *UDPMuxDefault params UniversalUDPMuxParams - // Since we have a shared socket, for srflx candidates it makes sense to have a shared mapped address across all the agents + // Since we have a shared socket, for srflx candidates it makes sense + // to have a shared mapped address across all the agents // stun.XORMappedAddress indexed by the STUN server addr xorMappedMap map[string]*xorMapped } @@ -42,7 +43,7 @@ type UniversalUDPMuxParams struct { Net transport.Net } -// NewUniversalUDPMuxDefault creates an implementation of UniversalUDPMux embedding UDPMux +// NewUniversalUDPMuxDefault creates an implementation of UniversalUDPMux embedding UDPMux. func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDefault { if params.Logger == nil { params.Logger = logging.NewDefaultLoggerFactory().NewLogger("ice") @@ -51,31 +52,31 @@ func NewUniversalUDPMuxDefault(params UniversalUDPMuxParams) *UniversalUDPMuxDef params.XORMappedAddrCacheTTL = time.Second * 25 } - m := &UniversalUDPMuxDefault{ + mux := &UniversalUDPMuxDefault{ params: params, xorMappedMap: make(map[string]*xorMapped), } // Wrap UDP connection, process server reflexive messages // before they are passed to the UDPMux connection handler (connWorker) - m.params.UDPConn = &udpConn{ + mux.params.UDPConn = &udpConn{ PacketConn: params.UDPConn, - mux: m, + mux: mux, logger: params.Logger, } // Embed UDPMux udpMuxParams := UDPMuxParams{ Logger: params.Logger, - UDPConn: m.params.UDPConn, - Net: m.params.Net, + UDPConn: mux.params.UDPConn, + Net: mux.params.Net, } - m.UDPMuxDefault = NewUDPMuxDefault(udpMuxParams) + mux.UDPMuxDefault = NewUDPMuxDefault(udpMuxParams) - return m + return mux } -// udpConn is a wrapper around UDPMux conn that overrides ReadFrom and handles STUN/TURN packets +// udpConn is a wrapper around UDPMux conn that overrides ReadFrom and handles STUN/TURN packets. type udpConn struct { net.PacketConn mux *UniversalUDPMuxDefault @@ -88,7 +89,8 @@ func (m *UniversalUDPMuxDefault) GetRelayedAddr(net.Addr, time.Duration) (*net.A return nil, errNotImplemented } -// GetConnForURL add uniques to the muxed connection by concatenating ufrag and URL (e.g. STUN URL) to be able to support multiple STUN/TURN servers +// GetConnForURL add uniques to the muxed connection by concatenating ufrag and URL +// (e.g. STUN URL) to be able to support multiple STUN/TURN servers // and return a unique connection per server. func (m *UniversalUDPMuxDefault) GetConnForURL(ufrag string, url string, addr net.Addr) (net.PacketConn, error) { return m.UDPMuxDefault.GetConn(fmt.Sprintf("%s%s", ufrag, url), addr) @@ -99,24 +101,24 @@ func (m *UniversalUDPMuxDefault) GetConnForURL(ufrag string, url string, addr ne func (c *udpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { n, addr, err = c.PacketConn.ReadFrom(p) if err != nil { - return + return n, addr, err } - if stun.IsMessage(p[:n]) { + if stun.IsMessage(p[:n]) { //nolint:nestif msg := &stun.Message{ Raw: append([]byte{}, p[:n]...), } if err = msg.Decode(); err != nil { c.logger.Warnf("Failed to handle decode ICE from %s: %v", addr.String(), err) - err = nil - return + + return n, addr, nil } udpAddr, ok := addr.(*net.UDPAddr) if !ok { // Message about this err will be logged in the UDPMux - return + return n, addr, err } if c.mux.isXORMappedResponse(msg, udpAddr.String()) { @@ -125,9 +127,11 @@ func (c *udpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { c.logger.Debugf("%w: %v", errGetXorMappedAddrResponse, err) err = nil } - return + + return n, addr, err } } + return n, addr, err } @@ -135,14 +139,16 @@ func (c *udpConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { func (m *UniversalUDPMuxDefault) isXORMappedResponse(msg *stun.Message, stunAddr string) bool { m.mu.Lock() defer m.mu.Unlock() - // Check first if it is a STUN server address because remote peer can also send similar messages but as a BindingSuccess + // Check first if it is a STUN server address, + // because remote peer can also send similar messages but as a BindingSuccess. _, ok := m.xorMappedMap[stunAddr] _, err := msg.Get(stun.AttrXORMappedAddress) + return err == nil && ok } -// handleXORMappedResponse parses response from the STUN server, extracts XORMappedAddress attribute -// and set the mapped address for the server +// handleXORMappedResponse parses response from the STUN server, extracts XORMappedAddress attribute. +// and set the mapped address for the server. func (m *UniversalUDPMuxDefault) handleXORMappedResponse(stunAddr *net.UDPAddr, msg *stun.Message) error { m.mu.Lock() defer m.mu.Unlock() @@ -167,7 +173,10 @@ func (m *UniversalUDPMuxDefault) handleXORMappedResponse(stunAddr *net.UDPAddr, // Makes a STUN binding request to discover mapped address otherwise. // Blocks until the stun.XORMappedAddress has been discovered or deadline. // Method is safe for concurrent use. -func (m *UniversalUDPMuxDefault) GetXORMappedAddr(serverAddr net.Addr, deadline time.Duration) (*stun.XORMappedAddress, error) { +func (m *UniversalUDPMuxDefault) GetXORMappedAddr( + serverAddr net.Addr, + deadline time.Duration, +) (*stun.XORMappedAddress, error) { m.mu.Lock() mappedAddr, ok := m.xorMappedMap[serverAddr.String()] // If we already have a mapping for this STUN server (address already received) @@ -203,6 +212,7 @@ func (m *UniversalUDPMuxDefault) GetXORMappedAddr(serverAddr net.Addr, deadline if mappedAddr.addr == nil { return nil, errNoXorAddrMapping } + return mappedAddr.addr, nil case <-time.After(deadline): return nil, errXORMappedAddrTimeout diff --git a/udp_mux_universal_test.go b/udp_mux_universal_test.go index ec8de2e..1f648e6 100644 --- a/udp_mux_universal_test.go +++ b/udp_mux_universal_test.go @@ -12,7 +12,7 @@ import ( "testing" "time" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" "github.com/stretchr/testify/require" ) @@ -43,13 +43,15 @@ func TestUniversalUDPMux(t *testing.T) { } func testMuxSrflxConnection(t *testing.T, udpMux *UniversalUDPMuxDefault, ufrag string, network string) { + t.Helper() + pktConn, err := udpMux.GetConn(ufrag, udpMux.LocalAddr()) require.NoError(t, err, "error retrieving muxed connection for ufrag") defer func() { _ = pktConn.Close() }() - remoteConn, err := net.DialUDP(network, nil, &net.UDPAddr{ + remoteConn, err := net.DialUDP(network, nil, &net.UDPAddr{ // nolint Port: udpMux.LocalAddr().(*net.UDPAddr).Port, }) require.NoError(t, err, "error dialing test UDP connection") diff --git a/udp_muxed_conn.go b/udp_muxed_conn.go index e69c307..e32cb30 100644 --- a/udp_muxed_conn.go +++ b/udp_muxed_conn.go @@ -4,14 +4,20 @@ package ice import ( - "encoding/binary" "io" "net" "sync" "time" "github.com/pion/logging" - "github.com/pion/transport/v3/packetio" +) + +type udpMuxedConnState int + +const ( + udpMuxedConnOpen udpMuxedConnState = iota + udpMuxedConnWaiting + udpMuxedConnClosed ) type udpMuxedConnParams struct { @@ -22,58 +28,68 @@ type udpMuxedConnParams struct { Logger logging.LeveledLogger } -// udpMuxedConn represents a logical packet conn for a single remote as identified by ufrag +// udpMuxedConn represents a logical packet conn for a single remote as identified by ufrag. type udpMuxedConn struct { params *udpMuxedConnParams // Remote addresses that we have sent to on this conn - addresses []string + addresses []ipPort - // Channel holding incoming packets - buf *packetio.Buffer - closedChan chan struct{} - closeOnce sync.Once - mu sync.Mutex + // FIFO queue holding incoming packets + bufHead, bufTail *bufferHolder + notify chan struct{} + closedChan chan struct{} + state udpMuxedConnState + mu sync.Mutex } func newUDPMuxedConn(params *udpMuxedConnParams) *udpMuxedConn { - p := &udpMuxedConn{ + return &udpMuxedConn{ params: params, - buf: packetio.NewBuffer(), + notify: make(chan struct{}, 1), closedChan: make(chan struct{}), } - - return p } func (c *udpMuxedConn) ReadFrom(b []byte) (n int, rAddr net.Addr, err error) { - buf := c.params.AddrPool.Get().(*bufferHolder) //nolint:forcetypeassert - defer c.params.AddrPool.Put(buf) + for { + c.mu.Lock() + if c.bufTail != nil { + pkt := c.bufTail + c.bufTail = pkt.next - // Read address - total, err := c.buf.Read(buf.buf) - if err != nil { - return 0, nil, err + if pkt == c.bufHead { + c.bufHead = nil + } + c.mu.Unlock() + + if len(b) < len(pkt.buf) { + err = io.ErrShortBuffer + } else { + n = copy(b, pkt.buf) + rAddr = pkt.addr + } + + pkt.reset() + c.params.AddrPool.Put(pkt) + + return n, rAddr, err + } + + if c.state == udpMuxedConnClosed { + c.mu.Unlock() + + return 0, nil, io.EOF + } + + c.state = udpMuxedConnWaiting + c.mu.Unlock() + + select { + case <-c.notify: + case <-c.closedChan: + return 0, nil, io.EOF + } } - - dataLen := int(binary.LittleEndian.Uint16(buf.buf[:2])) - if dataLen > total || dataLen > len(b) { - return 0, nil, io.ErrShortBuffer - } - - // Read data and then address - offset := 2 - copy(b, buf.buf[offset:offset+dataLen]) - offset += dataLen - - // Read address len & decode address - addrLen := int(binary.LittleEndian.Uint16(buf.buf[offset : offset+2])) - offset += 2 - - if rAddr, err = decodeUDPAddr(buf.buf[offset : offset+addrLen]); err != nil { - return 0, nil, err - } - - return dataLen, rAddr, nil } func (c *udpMuxedConn) WriteTo(buf []byte, rAddr net.Addr) (n int, err error) { @@ -81,9 +97,18 @@ func (c *udpMuxedConn) WriteTo(buf []byte, rAddr net.Addr) (n int, err error) { return 0, io.ErrClosedPipe } // Each time we write to a new address, we'll register it with the mux - addr := rAddr.String() - if !c.containsAddress(addr) { - c.addAddress(addr) + netUDPAddr, ok := rAddr.(*net.UDPAddr) + if !ok { + return 0, errFailedToCastUDPAddr + } + + //nolint:gosec // TODO add port validation G115 + ipAndPort, err := newIPPort(netUDPAddr.IP, netUDPAddr.Zone, uint16(netUDPAddr.Port)) + if err != nil { + return 0, err + } + if !c.containsAddress(ipAndPort) { + c.addAddress(ipAndPort) } return c.params.Mux.writeTo(buf, rAddr) @@ -110,32 +135,44 @@ func (c *udpMuxedConn) CloseChannel() <-chan struct{} { } func (c *udpMuxedConn) Close() error { - var err error - c.closeOnce.Do(func() { - err = c.buf.Close() + c.mu.Lock() + defer c.mu.Unlock() + if c.state != udpMuxedConnClosed { + for pkt := c.bufTail; pkt != nil; { + next := pkt.next + + pkt.reset() + c.params.AddrPool.Put(pkt) + + pkt = next + } + c.bufHead = nil + c.bufTail = nil + + c.state = udpMuxedConnClosed close(c.closedChan) - }) - return err + } + + return nil } func (c *udpMuxedConn) isClosed() bool { - select { - case <-c.closedChan: - return true - default: - return false - } -} - -func (c *udpMuxedConn) getAddresses() []string { c.mu.Lock() defer c.mu.Unlock() - addresses := make([]string, len(c.addresses)) + + return c.state == udpMuxedConnClosed +} + +func (c *udpMuxedConn) getAddresses() []ipPort { + c.mu.Lock() + defer c.mu.Unlock() + addresses := make([]ipPort, len(c.addresses)) copy(addresses, c.addresses) + return addresses } -func (c *udpMuxedConn) addAddress(addr string) { +func (c *udpMuxedConn) addAddress(addr ipPort) { c.mu.Lock() c.addresses = append(c.addresses, addr) c.mu.Unlock() @@ -144,11 +181,11 @@ func (c *udpMuxedConn) addAddress(addr string) { c.params.Mux.registerConnForAddress(c, addr) } -func (c *udpMuxedConn) removeAddress(addr string) { +func (c *udpMuxedConn) removeAddress(addr ipPort) { c.mu.Lock() defer c.mu.Unlock() - newAddresses := make([]string, 0, len(c.addresses)) + newAddresses := make([]ipPort, 0, len(c.addresses)) for _, a := range c.addresses { if a != addr { newAddresses = append(newAddresses, a) @@ -158,7 +195,7 @@ func (c *udpMuxedConn) removeAddress(addr string) { c.addresses = newAddresses } -func (c *udpMuxedConn) containsAddress(addr string) bool { +func (c *udpMuxedConn) containsAddress(addr ipPort) bool { c.mu.Lock() defer c.mu.Unlock() for _, a := range c.addresses { @@ -166,81 +203,50 @@ func (c *udpMuxedConn) containsAddress(addr string) bool { return true } } + return false } func (c *udpMuxedConn) writePacket(data []byte, addr *net.UDPAddr) error { - // Write two packets, address and data - buf := c.params.AddrPool.Get().(*bufferHolder) //nolint:forcetypeassert - defer c.params.AddrPool.Put(buf) + pkt := c.params.AddrPool.Get().(*bufferHolder) //nolint:forcetypeassert + if cap(pkt.buf) < len(data) { + c.params.AddrPool.Put(pkt) - // Format of buffer | data len | data bytes | addr len | addr bytes | - if len(buf.buf) < len(data)+maxAddrSize { return io.ErrShortBuffer } - // Data length - binary.LittleEndian.PutUint16(buf.buf, uint16(len(data))) - offset := 2 - // Data - copy(buf.buf[offset:], data) - offset += len(data) + pkt.buf = append(pkt.buf[:0], data...) + pkt.addr = addr - // Write address first, leaving room for its length - n, err := encodeUDPAddr(addr, buf.buf[offset+2:]) - if err != nil { - return err + c.mu.Lock() + if c.state == udpMuxedConnClosed { + c.mu.Unlock() + + pkt.reset() + c.params.AddrPool.Put(pkt) + + return io.ErrClosedPipe } - total := offset + n + 2 - // Address len - binary.LittleEndian.PutUint16(buf.buf[offset:], uint16(n)) - - if _, err := c.buf.Write(buf.buf[:total]); err != nil { - return err + if c.bufHead != nil { + c.bufHead.next = pkt } + c.bufHead = pkt + + if c.bufTail == nil { + c.bufTail = pkt + } + + state := c.state + c.state = udpMuxedConnOpen + c.mu.Unlock() + + if state == udpMuxedConnWaiting { + select { + case c.notify <- struct{}{}: + default: + } + } + return nil } - -func encodeUDPAddr(addr *net.UDPAddr, buf []byte) (int, error) { - ipData, err := addr.IP.MarshalText() - if err != nil { - return 0, err - } - total := 2 + len(ipData) + 2 + len(addr.Zone) - if total > len(buf) { - return 0, io.ErrShortBuffer - } - - binary.LittleEndian.PutUint16(buf, uint16(len(ipData))) - offset := 2 - n := copy(buf[offset:], ipData) - offset += n - binary.LittleEndian.PutUint16(buf[offset:], uint16(addr.Port)) - offset += 2 - copy(buf[offset:], addr.Zone) - return total, nil -} - -func decodeUDPAddr(buf []byte) (*net.UDPAddr, error) { - addr := net.UDPAddr{} - - offset := 0 - ipLen := int(binary.LittleEndian.Uint16(buf[:2])) - offset += 2 - // Basic bounds checking - if ipLen+offset > len(buf) { - return nil, io.ErrShortBuffer - } - if err := addr.IP.UnmarshalText(buf[offset : offset+ipLen]); err != nil { - return nil, err - } - offset += ipLen - addr.Port = int(binary.LittleEndian.Uint16(buf[offset : offset+2])) - offset += 2 - zone := make([]byte, len(buf[offset:])) - copy(zone, buf[offset:]) - addr.Zone = string(zone) - - return &addr, nil -} diff --git a/url.go b/url.go index 50d354a..f18d787 100644 --- a/url.go +++ b/url.go @@ -3,80 +3,80 @@ package ice -import "github.com/pion/stun/v2" +import "github.com/pion/stun/v3" type ( - // URL represents a STUN (rfc7064) or TURN (rfc7065) URI + // URL represents a STUN (rfc7064) or TURN (rfc7065) URI. // - // Deprecated: Please use pion/stun.URI + // Deprecated: Please use pion/stun.URI. URL = stun.URI // ProtoType indicates the transport protocol type that is used in the ice.URL // structure. // - // Deprecated: TPlease use pion/stun.ProtoType + // Deprecated: TPlease use pion/stun.ProtoType. ProtoType = stun.ProtoType // SchemeType indicates the type of server used in the ice.URL structure. // - // Deprecated: Please use pion/stun.SchemeType + // Deprecated: Please use pion/stun.SchemeType. SchemeType = stun.SchemeType ) const ( // SchemeTypeSTUN indicates the URL represents a STUN server. // - // Deprecated: Please use pion/stun.SchemeTypeSTUN + // Deprecated: Please use pion/stun.SchemeTypeSTUN. SchemeTypeSTUN = stun.SchemeTypeSTUN // SchemeTypeSTUNS indicates the URL represents a STUNS (secure) server. // - // Deprecated: Please use pion/stun.SchemeTypeSTUNS + // Deprecated: Please use pion/stun.SchemeTypeSTUNS. SchemeTypeSTUNS = stun.SchemeTypeSTUNS // SchemeTypeTURN indicates the URL represents a TURN server. // - // Deprecated: Please use pion/stun.SchemeTypeTURN + // Deprecated: Please use pion/stun.SchemeTypeTURN. SchemeTypeTURN = stun.SchemeTypeTURN // SchemeTypeTURNS indicates the URL represents a TURNS (secure) server. // - // Deprecated: Please use pion/stun.SchemeTypeTURNS + // Deprecated: Please use pion/stun.SchemeTypeTURNS. SchemeTypeTURNS = stun.SchemeTypeTURNS ) const ( // ProtoTypeUDP indicates the URL uses a UDP transport. // - // Deprecated: Please use pion/stun.ProtoTypeUDP + // Deprecated: Please use pion/stun.ProtoTypeUDP. ProtoTypeUDP = stun.ProtoTypeUDP // ProtoTypeTCP indicates the URL uses a TCP transport. // - // Deprecated: Please use pion/stun.ProtoTypeTCP + // Deprecated: Please use pion/stun.ProtoTypeTCP. ProtoTypeTCP = stun.ProtoTypeTCP ) -// Unknown represents and unknown ProtoType or SchemeType +// Unknown represents and unknown ProtoType or SchemeType. // -// Deprecated: Please use pion/stun.SchemeTypeUnknown or pion/stun.ProtoTypeUnknown +// Deprecated: Please use pion/stun.SchemeTypeUnknown or pion/stun.ProtoTypeUnknown. const Unknown = 0 -// ParseURL parses a STUN or TURN urls following the ABNF syntax described in +// ParseURL 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. // -// Deprecated: Please use pion/stun.ParseURI +// Deprecated: Please use pion/stun.ParseURI. var ParseURL = stun.ParseURI //nolint:gochecknoglobals -// NewSchemeType defines a procedure for creating a new SchemeType from a raw +// NewSchemeType defines a procedure for creating a new SchemeType from a raw. // string naming the scheme type. // -// Deprecated: Please use pion/stun.NewSchemeType +// Deprecated: Please use pion/stun.NewSchemeType. var NewSchemeType = stun.NewSchemeType //nolint:gochecknoglobals -// NewProtoType defines a procedure for creating a new ProtoType from a raw +// NewProtoType defines a procedure for creating a new ProtoType from a raw. // string naming the transport protocol type. // -// Deprecated: Please use pion/stun.NewProtoType +// Deprecated: Please use pion/stun.NewProtoType. var NewProtoType = stun.NewProtoType //nolint:gochecknoglobals diff --git a/usecandidate.go b/usecandidate.go index b5c489a..512f504 100644 --- a/usecandidate.go +++ b/usecandidate.go @@ -3,7 +3,7 @@ package ice -import "github.com/pion/stun/v2" +import "github.com/pion/stun/v3" // UseCandidateAttr represents USE-CANDIDATE attribute. type UseCandidateAttr struct{} @@ -11,12 +11,14 @@ type UseCandidateAttr struct{} // AddTo adds USE-CANDIDATE attribute to message. func (UseCandidateAttr) AddTo(m *stun.Message) error { m.Add(stun.AttrUseCandidate, nil) + return nil } // IsSet returns true if USE-CANDIDATE attribute is set. func (UseCandidateAttr) IsSet(m *stun.Message) bool { _, err := m.Get(stun.AttrUseCandidate) + return err == nil } diff --git a/usecandidate_test.go b/usecandidate_test.go index 1dc48ee..d50315a 100644 --- a/usecandidate_test.go +++ b/usecandidate_test.go @@ -6,22 +6,17 @@ package ice import ( "testing" - "github.com/pion/stun/v2" + "github.com/pion/stun/v3" + "github.com/stretchr/testify/require" ) func TestUseCandidateAttr_AddTo(t *testing.T) { m := new(stun.Message) - if UseCandidate().IsSet(m) { - t.Error("should not be set") - } - if err := m.Build(stun.BindingRequest, UseCandidate()); err != nil { - t.Error(err) - } + require.False(t, UseCandidate().IsSet(m)) + require.NoError(t, m.Build(stun.BindingRequest, UseCandidate())) + m1 := new(stun.Message) - if _, err := m1.Write(m.Raw); err != nil { - t.Error(err) - } - if !UseCandidate().IsSet(m1) { - t.Error("should be set") - } + _, err := m1.Write(m.Raw) + require.NoError(t, err) + require.True(t, UseCandidate().IsSet(m1)) } diff --git a/test_utils.go b/utils_test.go similarity index 96% rename from test_utils.go rename to utils_test.go index 235fda3..b16def2 100644 --- a/test_utils.go +++ b/utils_test.go @@ -13,6 +13,8 @@ import ( ) func newHostRemote(t *testing.T) *CandidateHost { + t.Helper() + remoteHostConfig := &CandidateHostConfig{ Network: "udp", Address: "1.2.3.5", @@ -21,10 +23,13 @@ func newHostRemote(t *testing.T) *CandidateHost { } hostRemote, err := NewCandidateHost(remoteHostConfig) require.NoError(t, err) + return hostRemote } func newPrflxRemote(t *testing.T) *CandidatePeerReflexive { + t.Helper() + prflxConfig := &CandidatePeerReflexiveConfig{ Network: "udp", Address: "10.10.10.2", @@ -35,10 +40,13 @@ func newPrflxRemote(t *testing.T) *CandidatePeerReflexive { } prflxRemote, err := NewCandidatePeerReflexive(prflxConfig) require.NoError(t, err) + return prflxRemote } func newSrflxRemote(t *testing.T) *CandidateServerReflexive { + t.Helper() + srflxConfig := &CandidateServerReflexiveConfig{ Network: "udp", Address: "10.10.10.2", @@ -49,10 +57,13 @@ func newSrflxRemote(t *testing.T) *CandidateServerReflexive { } srflxRemote, err := NewCandidateServerReflexive(srflxConfig) require.NoError(t, err) + return srflxRemote } func newRelayRemote(t *testing.T) *CandidateRelay { + t.Helper() + relayConfig := &CandidateRelayConfig{ Network: "udp", Address: "1.2.3.4", @@ -63,10 +74,13 @@ func newRelayRemote(t *testing.T) *CandidateRelay { } relayRemote, err := NewCandidateRelay(relayConfig) require.NoError(t, err) + return relayRemote } func newHostLocal(t *testing.T) *CandidateHost { + t.Helper() + localHostConfig := &CandidateHostConfig{ Network: "udp", Address: "192.168.1.1", @@ -75,5 +89,6 @@ func newHostLocal(t *testing.T) *CandidateHost { } hostLocal, err := NewCandidateHost(localHostConfig) require.NoError(t, err) + return hostLocal }