Merge upstream/master

This commit is contained in:
Zoltán Papp
2025-08-21 12:35:30 +02:00
87 changed files with 6846 additions and 3743 deletions
+20
View File
@@ -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 <https://pion.ly>
# SPDX-License-Identifier: MIT
name: API
on:
pull_request:
jobs:
check:
uses: pion/.goassets/.github/workflows/api.reusable.yml@master
+1 -1
View File
@@ -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
+5 -3
View File
@@ -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
+1 -1
View File
@@ -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
+90 -67
View File
@@ -1,101 +1,71 @@
# SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
# 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
+2 -2
View File
@@ -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 <https://pion.ly>
License: MIT
Files: testdata/fuzz/* **/testdata/fuzz/* api/*.txt
Files: testdata/seed/* testdata/fuzz/* **/testdata/fuzz/* api/*.txt
Copyright: 2023 The Pion community <https://pion.ly>
License: CC0-1.0
+4 -4
View File
@@ -6,10 +6,10 @@
<h4 align="center">A Go implementation of ICE</h4>
<p align="center">
<a href="https://pion.ly"><img src="https://img.shields.io/badge/pion-ice-gray.svg?longCache=true&colorB=brightgreen" alt="Pion ICE"></a>
<a href="http://gophers.slack.com/messages/pion"><img src="https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen" alt="Slack Widget"></a>
<a href="https://discord.gg/PngbdqpFbt"><img src="https://img.shields.io/badge/join-us%20on%20discord-gray.svg?longCache=true&logo=discord&colorB=brightblue" alt="join us on Discord"></a> <a href="https://bsky.app/profile/pion.ly"><img src="https://img.shields.io/badge/follow-us%20on%20bluesky-gray.svg?longCache=true&logo=bluesky&colorB=brightblue" alt="Follow us on Bluesky"></a>
<br>
<img alt="GitHub Workflow Status" src="https://img.shields.io/github/actions/workflow/status/pion/ice/test.yaml">
<a href="https://pkg.go.dev/github.com/pion/ice/v3"><img src="https://pkg.go.dev/badge/github.com/pion/ice/v3.svg" alt="Go Reference"></a>
<a href="https://pkg.go.dev/github.com/pion/ice/v4"><img src="https://pkg.go.dev/badge/github.com/pion/ice/v4.svg" alt="Go Reference"></a>
<a href="https://codecov.io/gh/pion/ice"><img src="https://codecov.io/gh/pion/ice/branch/master/graph/badge.svg" alt="Coverage Status"></a>
<a href="https://goreportcard.com/report/github.com/pion/ice"><img src="https://goreportcard.com/badge/github.com/pion/ice" alt="Go Report Card"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
@@ -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)
+32 -13
View File
@@ -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
+145 -63
View File
@@ -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))
}
+107 -25
View File
@@ -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
}
+460 -388
View File
File diff suppressed because it is too large Load Diff
+80 -52
View File
@@ -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
}
@@ -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())
}
+5 -3
View File
@@ -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)
+143 -25
View File
@@ -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()
}
}
+72
View File
@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// 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)
})
}
@@ -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
}
+90 -29
View File
@@ -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
}
+926 -714
View File
File diff suppressed because it is too large Load Diff
+22 -7
View File
@@ -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

Some files were not shown because too many files have changed in this diff Show More