├── .github ├── .gitignore ├── install-hooks.sh ├── workflows │ ├── api.yaml │ ├── lint.yaml │ ├── reuse.yml │ ├── release.yml │ ├── renovate-go-sum-fix.yaml │ ├── tidy-check.yaml │ ├── codeql-analysis.yml │ ├── fuzz.yaml │ └── test.yaml └── fetch-scripts.sh ├── .goreleaser.yml ├── renovate.json ├── internal ├── internal.go ├── atomic │ └── atomic.go ├── fakenet │ ├── packet_conn.go │ └── mock_conn.go ├── taskloop │ ├── taskloop_test.go │ └── taskloop.go └── stun │ └── stun.go ├── .gitignore ├── codecov.yml ├── agent_get_best_available_candidate_pair_test.go ├── .reuse └── dep5 ├── examples ├── nat-rules │ ├── Dockerfile │ ├── docker-compose.yml │ └── README.md ├── ping-pong │ ├── README.md │ └── main.go └── continual-gathering │ ├── README.md │ └── main.go ├── usecandidate_test.go ├── usecandidate.go ├── go.mod ├── tcptype_test.go ├── priority_test.go ├── priority.go ├── candidaterelatedaddress.go ├── role.go ├── LICENSES └── MIT.txt ├── LICENSE ├── candidatetype_test.go ├── candidatepair_state.go ├── tcptype.go ├── agent_on_selected_candidate_pair_change_test.go ├── ice_test.go ├── role_test.go ├── rand_test.go ├── candidatetype.go ├── agent_get_best_valid_candidate_pair_test.go ├── candidate_peer_reflexive.go ├── candidate_server_reflexive.go ├── rand.go ├── candidate_server_reflexive_test.go ├── utils_test.go ├── README.md ├── candidate_host.go ├── candidate_relay_test.go ├── transport_vnet_test.go ├── url.go ├── renomination.go ├── tcp_mux_multi.go ├── agent_config_test.go ├── icecontrol.go ├── candidate.go ├── ice.go ├── go.sum ├── candidate_relay.go ├── icecontrol_test.go ├── networktype.go ├── networktype_test.go ├── agent_udpmux_test.go ├── addr.go ├── addr_test.go ├── mdns.go ├── net.go ├── agent_handlers.go ├── transport.go ├── candidatepair_test.go ├── active_tcp.go ├── agent_handlers_test.go ├── udp_muxed_conn.go ├── mdns_test.go ├── agent_stats.go ├── udp_mux_multi.go ├── net_test.go ├── stats.go └── .golangci.yml /.github/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | .goassets 5 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | builds: 5 | - skip: true 6 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "github>pion/renovate-config" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /internal/internal.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package internal implements internal functionality for Pions ICE module 5 | package internal 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | ### JetBrains IDE ### 5 | ##################### 6 | .idea/ 7 | 8 | ### Emacs Temporary Files ### 9 | ############################# 10 | *~ 11 | 12 | ### Folders ### 13 | ############### 14 | bin/ 15 | vendor/ 16 | node_modules/ 17 | 18 | ### Files ### 19 | ############# 20 | *.ivf 21 | *.ogg 22 | tags 23 | cover.out 24 | *.sw[poe] 25 | *.wasm 26 | examples/sfu-ws/cert.pem 27 | examples/sfu-ws/key.pem 28 | wasm_exec.js 29 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # 6 | # SPDX-FileCopyrightText: 2023 The Pion community 7 | # SPDX-License-Identifier: MIT 8 | 9 | coverage: 10 | status: 11 | project: 12 | default: 13 | # Allow decreasing 2% of total coverage to avoid noise. 14 | threshold: 2% 15 | patch: 16 | default: 17 | target: 70% 18 | only_pulls: true 19 | 20 | ignore: 21 | - "examples/*" 22 | - "examples/**/*" 23 | -------------------------------------------------------------------------------- /agent_get_best_available_candidate_pair_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestNoBestAvailableCandidatePairAfterAgentConstruction(t *testing.T) { 16 | agent, err := NewAgent(&AgentConfig{}) 17 | require.NoError(t, err) 18 | 19 | defer func() { 20 | require.NoError(t, agent.Close()) 21 | }() 22 | 23 | require.Nil(t, agent.getBestAvailableCandidatePair()) 24 | } 25 | -------------------------------------------------------------------------------- /.reuse/dep5: -------------------------------------------------------------------------------- 1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ 2 | Upstream-Name: Pion 3 | Source: https://github.com/pion/ 4 | 5 | Files: README.md DESIGN.md **/README.md AUTHORS.txt renovate.json go.mod go.sum **/go.mod **/go.sum .eslintrc.json package.json examples.json sfu-ws/flutter/.gitignore sfu-ws/flutter/pubspec.yaml c-data-channels/webrtc.h examples/examples.json yarn.lock 6 | Copyright: 2023 The Pion community 7 | License: MIT 8 | 9 | Files: testdata/seed/* testdata/fuzz/* **/testdata/fuzz/* api/*.txt 10 | Copyright: 2023 The Pion community 11 | License: CC0-1.0 12 | -------------------------------------------------------------------------------- /internal/atomic/atomic.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package atomic contains custom atomic types 5 | package atomic 6 | 7 | import "sync/atomic" 8 | 9 | // Error is an atomic error. 10 | type Error struct { 11 | v atomic.Value 12 | } 13 | 14 | // Store updates the value of the atomic variable. 15 | func (a *Error) Store(err error) { 16 | a.v.Store(struct{ error }{err}) 17 | } 18 | 19 | // Load retrieves the current value of the atomic variable. 20 | func (a *Error) Load() error { 21 | err, _ := a.v.Load().(struct{ error }) 22 | 23 | return err.error 24 | } 25 | -------------------------------------------------------------------------------- /.github/install-hooks.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # DO NOT EDIT THIS FILE 5 | # 6 | # It is automatically copied from https://github.com/pion/.goassets repository. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | SCRIPT_PATH="$(realpath "$(dirname "$0")")" 15 | 16 | . ${SCRIPT_PATH}/fetch-scripts.sh 17 | 18 | cp "${GOASSETS_PATH}/hooks/commit-msg.sh" "${SCRIPT_PATH}/../.git/hooks/commit-msg" 19 | cp "${GOASSETS_PATH}/hooks/pre-commit.sh" "${SCRIPT_PATH}/../.git/hooks/pre-commit" 20 | -------------------------------------------------------------------------------- /.github/workflows/api.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: API 15 | on: 16 | pull_request: 17 | 18 | jobs: 19 | check: 20 | uses: pion/.goassets/.github/workflows/api.reusable.yml@master 21 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Lint 15 | on: 16 | pull_request: 17 | 18 | jobs: 19 | lint: 20 | uses: pion/.goassets/.github/workflows/lint.reusable.yml@master 21 | -------------------------------------------------------------------------------- /examples/nat-rules/Dockerfile: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | FROM golang:1.25-bookworm AS builder 5 | WORKDIR /src 6 | 7 | # Copy the entire repo so local changes to pion/ice are included. 8 | COPY . . 9 | 10 | WORKDIR /src/examples/nat-rules 11 | RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -o /tmp/nat-rules-demo . 12 | 13 | FROM debian:bookworm-slim 14 | RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates && \ 15 | rm -rf /var/lib/apt/lists/* 16 | 17 | COPY --from=builder /tmp/nat-rules-demo /usr/local/bin/nat-rules-demo 18 | 19 | ENTRYPOINT ["nat-rules-demo"] 20 | -------------------------------------------------------------------------------- /usecandidate_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pion/stun/v3" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestUseCandidateAttr_AddTo(t *testing.T) { 14 | msg := stun.New() 15 | msg.Type = stun.MessageType{Method: stun.MethodBinding, Class: stun.ClassRequest} 16 | require.False(t, UseCandidate().IsSet(msg)) 17 | 18 | require.NoError(t, UseCandidate().AddTo(msg)) 19 | msg.Encode() 20 | 21 | msg2 := &stun.Message{Raw: append([]byte{}, msg.Raw...)} 22 | require.NoError(t, msg2.Decode()) 23 | require.True(t, UseCandidate().IsSet(msg2)) 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/reuse.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: REUSE Compliance Check 15 | 16 | on: 17 | push: 18 | pull_request: 19 | 20 | jobs: 21 | lint: 22 | uses: pion/.goassets/.github/workflows/reuse.reusable.yml@master 23 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Release 15 | on: 16 | push: 17 | tags: 18 | - 'v*' 19 | 20 | jobs: 21 | release: 22 | uses: pion/.goassets/.github/workflows/release.reusable.yml@master 23 | with: 24 | go-version: "1.25" # auto-update/latest-go-version 25 | -------------------------------------------------------------------------------- /usecandidate.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import "github.com/pion/stun/v3" 7 | 8 | // UseCandidateAttr represents USE-CANDIDATE attribute. 9 | type UseCandidateAttr struct{} 10 | 11 | // AddTo adds USE-CANDIDATE attribute to message. 12 | func (UseCandidateAttr) AddTo(m *stun.Message) error { 13 | m.Add(stun.AttrUseCandidate, nil) 14 | 15 | return nil 16 | } 17 | 18 | // IsSet returns true if USE-CANDIDATE attribute is set. 19 | func (UseCandidateAttr) IsSet(m *stun.Message) bool { 20 | _, err := m.Get(stun.AttrUseCandidate) 21 | 22 | return err == nil 23 | } 24 | 25 | // UseCandidate is shorthand for UseCandidateAttr. 26 | func UseCandidate() UseCandidateAttr { 27 | return UseCandidateAttr{} 28 | } 29 | -------------------------------------------------------------------------------- /.github/workflows/renovate-go-sum-fix.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Fix go.sum 15 | on: 16 | push: 17 | branches: 18 | - renovate/* 19 | 20 | jobs: 21 | fix: 22 | uses: pion/.goassets/.github/workflows/renovate-go-sum-fix.reusable.yml@master 23 | secrets: 24 | token: ${{ secrets.PIONBOT_PRIVATE_KEY }} 25 | -------------------------------------------------------------------------------- /.github/workflows/tidy-check.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Go mod tidy 15 | on: 16 | pull_request: 17 | push: 18 | branches: 19 | - master 20 | 21 | jobs: 22 | tidy: 23 | uses: pion/.goassets/.github/workflows/tidy-check.reusable.yml@master 24 | with: 25 | go-version: "1.25" # auto-update/latest-go-version 26 | -------------------------------------------------------------------------------- /internal/fakenet/packet_conn.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package fakenet contains fake network abstractions 5 | package fakenet 6 | 7 | import ( 8 | "net" 9 | ) 10 | 11 | // Compile-time assertion. 12 | var _ net.PacketConn = (*PacketConn)(nil) 13 | 14 | // PacketConn wraps a net.Conn and emulates net.PacketConn. 15 | type PacketConn struct { 16 | net.Conn 17 | } 18 | 19 | // ReadFrom reads a packet from the connection. 20 | func (f *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { 21 | n, err = f.Conn.Read(p) 22 | addr = f.Conn.RemoteAddr() 23 | 24 | return 25 | } 26 | 27 | // WriteTo writes a packet with payload p to addr. 28 | func (f *PacketConn) WriteTo(p []byte, _ net.Addr) (int, error) { 29 | return f.Conn.Write(p) 30 | } 31 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: CodeQL 15 | 16 | on: 17 | workflow_dispatch: 18 | schedule: 19 | - cron: '23 5 * * 0' 20 | pull_request: 21 | branches: 22 | - master 23 | paths: 24 | - '**.go' 25 | 26 | jobs: 27 | analyze: 28 | uses: pion/.goassets/.github/workflows/codeql-analysis.reusable.yml@master 29 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/pion/ice/v4 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/pion/dtls/v3 v3.0.9 8 | github.com/pion/logging v0.2.4 9 | github.com/pion/mdns/v2 v2.1.0 10 | github.com/pion/randutil v0.1.0 11 | github.com/pion/stun/v3 v3.0.2 12 | github.com/pion/transport/v3 v3.1.1 13 | github.com/pion/turn/v4 v4.1.3 14 | github.com/stretchr/testify v1.11.1 15 | golang.org/x/net v0.35.0 16 | ) 17 | 18 | require ( 19 | github.com/davecgh/go-spew v1.1.1 // indirect 20 | github.com/kr/pretty v0.1.0 // indirect 21 | github.com/pmezard/go-difflib v1.0.0 // indirect 22 | github.com/wlynxg/anet v0.0.5 // indirect 23 | golang.org/x/crypto v0.33.0 // indirect 24 | golang.org/x/sys v0.30.0 // indirect 25 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect 26 | gopkg.in/yaml.v3 v3.0.1 // indirect 27 | ) 28 | -------------------------------------------------------------------------------- /.github/workflows/fuzz.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Fuzz 15 | on: 16 | push: 17 | branches: 18 | - master 19 | schedule: 20 | - cron: "0 */8 * * *" 21 | 22 | jobs: 23 | fuzz: 24 | uses: pion/.goassets/.github/workflows/fuzz.reusable.yml@master 25 | with: 26 | go-version: "1.25" # auto-update/latest-go-version 27 | fuzz-time: "60s" 28 | -------------------------------------------------------------------------------- /tcptype_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestTCPType(t *testing.T) { 13 | var tcpType TCPType 14 | 15 | require.Equal(t, TCPTypeUnspecified, tcpType) 16 | require.Equal(t, TCPTypeActive, NewTCPType("active")) 17 | require.Equal(t, TCPTypePassive, NewTCPType("passive")) 18 | require.Equal(t, TCPTypeSimultaneousOpen, NewTCPType("so")) 19 | require.Equal(t, TCPTypeUnspecified, NewTCPType("something else")) 20 | 21 | require.Equal(t, "", TCPTypeUnspecified.String()) 22 | require.Equal(t, "active", TCPTypeActive.String()) 23 | require.Equal(t, "passive", TCPTypePassive.String()) 24 | require.Equal(t, "so", TCPTypeSimultaneousOpen.String()) 25 | require.Equal(t, "Unknown", TCPType(-1).String()) 26 | } 27 | -------------------------------------------------------------------------------- /priority_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pion/stun/v3" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestPriority_GetFrom(t *testing.T) { //nolint:dupl 14 | m := new(stun.Message) 15 | var priority PriorityAttr 16 | require.ErrorIs(t, stun.ErrAttributeNotFound, priority.GetFrom(m)) 17 | require.NoError(t, m.Build(stun.BindingRequest, &priority)) 18 | 19 | m1 := new(stun.Message) 20 | _, err := m1.Write(m.Raw) 21 | require.NoError(t, err) 22 | 23 | var p1 PriorityAttr 24 | require.NoError(t, p1.GetFrom(m1)) 25 | require.Equal(t, p1, priority) 26 | t.Run("IncorrectSize", func(t *testing.T) { 27 | m3 := new(stun.Message) 28 | m3.Add(stun.AttrPriority, make([]byte, 100)) 29 | var p2 PriorityAttr 30 | require.True(t, stun.IsAttrSizeInvalid(p2.GetFrom(m3))) 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /priority.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "encoding/binary" 8 | 9 | "github.com/pion/stun/v3" 10 | ) 11 | 12 | // PriorityAttr represents PRIORITY attribute. 13 | type PriorityAttr uint32 14 | 15 | const prioritySize = 4 // 32 bit 16 | 17 | // AddTo adds PRIORITY attribute to message. 18 | func (p PriorityAttr) AddTo(m *stun.Message) error { 19 | v := make([]byte, prioritySize) 20 | binary.BigEndian.PutUint32(v, uint32(p)) 21 | m.Add(stun.AttrPriority, v) 22 | 23 | return nil 24 | } 25 | 26 | // GetFrom decodes PRIORITY attribute from message. 27 | func (p *PriorityAttr) GetFrom(m *stun.Message) error { 28 | v, err := m.Get(stun.AttrPriority) 29 | if err != nil { 30 | return err 31 | } 32 | if err = stun.CheckSize(stun.AttrPriority, len(v), prioritySize); err != nil { 33 | return err 34 | } 35 | *p = PriorityAttr(binary.BigEndian.Uint32(v)) 36 | 37 | return nil 38 | } 39 | -------------------------------------------------------------------------------- /examples/ping-pong/README.md: -------------------------------------------------------------------------------- 1 | # ping-pong 2 | 3 | This example demonstrates how to connect two peers via ICE. Once started they send the current time between each other. 4 | 5 | Currently this example exchanges candidates over a HTTP server running on localhost. In a real world setup `pion/ice` will typically 6 | exchange auth and candidates via a signaling server. 7 | 8 | ## Instruction 9 | ### Run controlling 10 | 11 | ```sh 12 | go run main.go -controlling 13 | ``` 14 | 15 | 16 | ### Run controlled 17 | 18 | ```sh 19 | go run main.go 20 | ``` 21 | 22 | ### Press enter in both to start the connection! 23 | 24 | You will see terminal output showing the messages being sent back and forth 25 | ``` 26 | Local Agent is controlled 27 | Press 'Enter' when both processes have started 28 | ICE Connection State has changed: Checking 29 | ICE Connection State has changed: Connected 30 | Sent: 'fCFXXlnGmXdYjOy' 31 | Received: 'EpqTQYLQMUCjBDX' 32 | Sent: 'yhgOtrufSfVmvrR' 33 | Received: 'xYSTPxBPZKfgnFr' 34 | ``` 35 | -------------------------------------------------------------------------------- /candidaterelatedaddress.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import "fmt" 7 | 8 | // CandidateRelatedAddress convey transport addresses related to the 9 | // candidate, useful for diagnostics and other purposes. 10 | type CandidateRelatedAddress struct { 11 | Address string 12 | Port int 13 | } 14 | 15 | // String makes CandidateRelatedAddress printable. 16 | func (c *CandidateRelatedAddress) String() string { 17 | if c == nil { 18 | return "" 19 | } 20 | 21 | return fmt.Sprintf(" related %s:%d", c.Address, c.Port) 22 | } 23 | 24 | // Equal allows comparing two CandidateRelatedAddresses. 25 | // The CandidateRelatedAddress are allowed to be nil. 26 | func (c *CandidateRelatedAddress) Equal(other *CandidateRelatedAddress) bool { 27 | if c == nil && other == nil { 28 | return true 29 | } 30 | 31 | return c != nil && other != nil && 32 | c.Address == other.Address && 33 | c.Port == other.Port 34 | } 35 | -------------------------------------------------------------------------------- /.github/fetch-scripts.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # DO NOT EDIT THIS FILE 5 | # 6 | # It is automatically copied from https://github.com/pion/.goassets repository. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | set -eu 15 | 16 | SCRIPT_PATH="$(realpath "$(dirname "$0")")" 17 | GOASSETS_PATH="${SCRIPT_PATH}/.goassets" 18 | 19 | GOASSETS_REF=${GOASSETS_REF:-master} 20 | 21 | if [ -d "${GOASSETS_PATH}" ]; then 22 | if ! git -C "${GOASSETS_PATH}" diff --exit-code; then 23 | echo "${GOASSETS_PATH} has uncommitted changes" >&2 24 | exit 1 25 | fi 26 | git -C "${GOASSETS_PATH}" fetch origin 27 | git -C "${GOASSETS_PATH}" checkout ${GOASSETS_REF} 28 | git -C "${GOASSETS_PATH}" reset --hard origin/${GOASSETS_REF} 29 | else 30 | git clone -b ${GOASSETS_REF} https://github.com/pion/.goassets.git "${GOASSETS_PATH}" 31 | fi 32 | -------------------------------------------------------------------------------- /role.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "fmt" 8 | ) 9 | 10 | // Role represents ICE agent role, which can be controlling or controlled. 11 | type Role byte 12 | 13 | // Possible ICE agent roles. 14 | const ( 15 | Controlling Role = iota 16 | Controlled 17 | ) 18 | 19 | // UnmarshalText implements TextUnmarshaler. 20 | func (r *Role) UnmarshalText(text []byte) error { 21 | switch string(text) { 22 | case "controlling": 23 | *r = Controlling 24 | case "controlled": 25 | *r = Controlled 26 | default: 27 | return fmt.Errorf("%w %q", errUnknownRole, text) 28 | } 29 | 30 | return nil 31 | } 32 | 33 | // MarshalText implements TextMarshaler. 34 | func (r Role) MarshalText() (text []byte, err error) { 35 | return []byte(r.String()), nil 36 | } 37 | 38 | func (r Role) String() string { 39 | switch r { 40 | case Controlling: 41 | return "controlling" 42 | case Controlled: 43 | return "controlled" 44 | default: 45 | return "unknown" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /LICENSES/MIT.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 The Pion community 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /internal/fakenet/mock_conn.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package fakenet 8 | 9 | import ( 10 | "net" 11 | "time" 12 | ) 13 | 14 | // MockPacketConn for tests. 15 | type MockPacketConn struct{} 16 | 17 | func (m *MockPacketConn) ReadFrom([]byte) (n int, addr net.Addr, err error) { return 0, nil, nil } //nolint:revive 18 | func (m *MockPacketConn) WriteTo([]byte, net.Addr) (n int, err error) { return 0, nil } //nolint:revive 19 | func (m *MockPacketConn) Close() error { return nil } //nolint:revive 20 | func (m *MockPacketConn) LocalAddr() net.Addr { return nil } //nolint:revive 21 | func (m *MockPacketConn) SetDeadline(time.Time) error { return nil } //nolint:revive 22 | func (m *MockPacketConn) SetReadDeadline(time.Time) error { return nil } //nolint:revive 23 | func (m *MockPacketConn) SetWriteDeadline(time.Time) error { return nil } //nolint:revive 24 | -------------------------------------------------------------------------------- /candidatetype_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestCandidateType_String_KnownCases(t *testing.T) { 13 | cases := map[CandidateType]string{ 14 | CandidateTypeHost: "host", 15 | CandidateTypeServerReflexive: "srflx", 16 | CandidateTypePeerReflexive: "prflx", 17 | CandidateTypeRelay: "relay", 18 | CandidateTypeUnspecified: "Unknown candidate type", 19 | } 20 | 21 | for ct, want := range cases { 22 | require.Equal(t, want, ct.String(), "unexpected string for %v", ct) 23 | } 24 | } 25 | 26 | func TestCandidateType_String_Default(t *testing.T) { 27 | const outOfBounds CandidateType = 255 28 | require.Equal(t, "Unknown candidate type", outOfBounds.String()) 29 | } 30 | 31 | func TestCandidateType_Preference_DefaultCase(t *testing.T) { 32 | const outOfBounds CandidateType = 255 33 | require.Equal(t, uint16(0), outOfBounds.Preference()) 34 | } 35 | 36 | func TestContainsCandidateType_NilSlice(t *testing.T) { 37 | var list []CandidateType // nil slice 38 | require.False(t, containsCandidateType(CandidateTypeHost, list)) 39 | } 40 | -------------------------------------------------------------------------------- /candidatepair_state.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | // CandidatePairState represent the ICE candidate pair state. 7 | type CandidatePairState int 8 | 9 | const ( 10 | // CandidatePairStateWaiting means a check has not been performed for 11 | // this pair. 12 | CandidatePairStateWaiting CandidatePairState = iota + 1 13 | 14 | // CandidatePairStateInProgress means a check has been sent for this pair, 15 | // but the transaction is in progress. 16 | CandidatePairStateInProgress 17 | 18 | // CandidatePairStateFailed means a check for this pair was already done 19 | // and failed, either never producing any response or producing an unrecoverable 20 | // failure response. 21 | CandidatePairStateFailed 22 | 23 | // CandidatePairStateSucceeded means a check for this pair was already 24 | // done and produced a successful result. 25 | CandidatePairStateSucceeded 26 | ) 27 | 28 | func (c CandidatePairState) String() string { 29 | switch c { 30 | case CandidatePairStateWaiting: 31 | return "waiting" 32 | case CandidatePairStateInProgress: 33 | return "in-progress" 34 | case CandidatePairStateFailed: 35 | return "failed" 36 | case CandidatePairStateSucceeded: 37 | return "succeeded" 38 | } 39 | 40 | return "Unknown candidate pair state" 41 | } 42 | -------------------------------------------------------------------------------- /examples/nat-rules/docker-compose.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2025 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | services: 5 | nat-demo: 6 | build: 7 | context: ../.. 8 | dockerfile: examples/nat-rules/Dockerfile 9 | command: ["-scenario", "all"] 10 | tty: true 11 | environment: 12 | NAT_DEMO_BLUE_LOCAL: 10.10.0.20 13 | NAT_DEMO_BLUE_PUBLIC: 203.0.113.10 14 | NAT_DEMO_GREEN_LOCAL: 10.20.0.20 15 | NAT_DEMO_GREEN_PUBLIC: 203.0.113.20 16 | NAT_DEMO_GLOBAL_HOST_FALLBACK: 198.51.100.200 17 | NAT_DEMO_SERVICE_LOCAL: 10.30.0.20 18 | NAT_DEMO_SERVICE_HOST_PUBLIC: 203.0.113.30 19 | NAT_DEMO_SCOPED_PUBLIC: 203.0.113.40 20 | NAT_DEMO_SCOPED_CIDR: 10.30.0.0/24 21 | NAT_DEMO_SRFLX_LOCAL: 0.0.0.0 22 | NAT_DEMO_SRFLX_PRIMARY: 198.51.100.50 23 | NAT_DEMO_SRFLX_SECONDARY: 198.51.100.60 24 | networks: 25 | lan_blue: 26 | ipv4_address: 10.10.0.20 27 | lan_green: 28 | ipv4_address: 10.20.0.20 29 | lan_service: 30 | ipv4_address: 10.30.0.20 31 | 32 | networks: 33 | lan_blue: 34 | driver: bridge 35 | ipam: 36 | config: 37 | - subnet: 10.10.0.0/24 38 | 39 | lan_green: 40 | driver: bridge 41 | ipam: 42 | config: 43 | - subnet: 10.20.0.0/24 44 | 45 | lan_service: 46 | driver: bridge 47 | ipam: 48 | config: 49 | - subnet: 10.30.0.0/24 50 | -------------------------------------------------------------------------------- /tcptype.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import "strings" 7 | 8 | // TCPType is the type of ICE TCP candidate as described in 9 | // https://tools.ietf.org/html/rfc6544#section-4.5 10 | type TCPType int 11 | 12 | const ( 13 | // TCPTypeUnspecified is the default value. For example UDP candidates do not 14 | // need this field. 15 | TCPTypeUnspecified TCPType = iota 16 | // TCPTypeActive is active TCP candidate, which initiates TCP connections. 17 | TCPTypeActive 18 | // TCPTypePassive is passive TCP candidate, only accepts TCP connections. 19 | TCPTypePassive 20 | // TCPTypeSimultaneousOpen is like active and passive at the same time. 21 | TCPTypeSimultaneousOpen 22 | ) 23 | 24 | // NewTCPType creates a new TCPType from string. 25 | func NewTCPType(value string) TCPType { 26 | switch strings.ToLower(value) { 27 | case "active": 28 | return TCPTypeActive 29 | case "passive": 30 | return TCPTypePassive 31 | case "so": 32 | return TCPTypeSimultaneousOpen 33 | default: 34 | return TCPTypeUnspecified 35 | } 36 | } 37 | 38 | func (t TCPType) String() string { 39 | switch t { 40 | case TCPTypeUnspecified: 41 | return "" 42 | case TCPTypeActive: 43 | return "active" 44 | case TCPTypePassive: 45 | return "passive" 46 | case TCPTypeSimultaneousOpen: 47 | return "so" 48 | default: 49 | return ErrUnknownType.Error() 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /agent_on_selected_candidate_pair_change_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "context" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func TestOnSelectedCandidatePairChange(t *testing.T) { 17 | agent, candidatePair := fixtureTestOnSelectedCandidatePairChange(t) 18 | defer func() { 19 | require.NoError(t, agent.Close()) 20 | }() 21 | 22 | callbackCalled := make(chan struct{}, 1) 23 | err := agent.OnSelectedCandidatePairChange(func(_, _ Candidate) { 24 | close(callbackCalled) 25 | }) 26 | require.NoError(t, err) 27 | 28 | err = agent.loop.Run(context.Background(), func(_ context.Context) { 29 | agent.setSelectedPair(candidatePair) 30 | }) 31 | require.NoError(t, err) 32 | 33 | <-callbackCalled 34 | } 35 | 36 | func fixtureTestOnSelectedCandidatePairChange(t *testing.T) (*Agent, *CandidatePair) { 37 | t.Helper() 38 | 39 | agent, err := NewAgent(&AgentConfig{}) 40 | require.NoError(t, err) 41 | 42 | candidatePair := makeCandidatePair(t) 43 | 44 | return agent, candidatePair 45 | } 46 | 47 | func makeCandidatePair(t *testing.T) *CandidatePair { 48 | t.Helper() 49 | 50 | hostLocal := newHostLocal(t) 51 | relayRemote := newRelayRemote(t) 52 | 53 | candidatePair := newCandidatePair(hostLocal, relayRemote, false) 54 | 55 | return candidatePair 56 | } 57 | -------------------------------------------------------------------------------- /internal/taskloop/taskloop_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package taskloop 5 | 6 | import ( 7 | "context" 8 | "sync/atomic" 9 | "testing" 10 | "time" 11 | 12 | "github.com/stretchr/testify/assert" 13 | ) 14 | 15 | func TestRunReturnsErrClosedWhenLoopClosing(t *testing.T) { 16 | loop := New(func() {}) 17 | 18 | blockStarted := make(chan struct{}) 19 | releaseBlock := make(chan struct{}) 20 | go func() { 21 | _ = loop.Run(context.Background(), func(context.Context) { 22 | close(blockStarted) 23 | <-releaseBlock 24 | }) 25 | }() 26 | <-blockStarted 27 | 28 | var secondRan atomic.Bool 29 | errCh := make(chan error, 1) 30 | go func() { 31 | errCh <- loop.Run(context.Background(), func(context.Context) { 32 | secondRan.Store(true) 33 | }) 34 | }() 35 | 36 | time.Sleep(10 * time.Millisecond) 37 | 38 | closeDone := make(chan struct{}) 39 | go func() { 40 | loop.Close() 41 | close(closeDone) 42 | }() 43 | 44 | select { 45 | case err := <-errCh: 46 | assert.ErrorIs(t, err, ErrClosed) 47 | case <-time.After(time.Second): 48 | assert.Fail(t, "Run did not return after loop close") 49 | } 50 | 51 | close(releaseBlock) 52 | 53 | select { 54 | case <-closeDone: 55 | case <-time.After(time.Second): 56 | assert.Fail(t, "Close did not return") 57 | } 58 | 59 | assert.False(t, secondRan.Load(), "second task should not excute after loop is closed") 60 | } 61 | -------------------------------------------------------------------------------- /ice_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func TestConnectedState_String(t *testing.T) { 13 | testCases := []struct { 14 | connectionState ConnectionState 15 | expectedString string 16 | }{ 17 | {ConnectionStateUnknown, "Invalid"}, 18 | {ConnectionStateNew, "New"}, 19 | {ConnectionStateChecking, "Checking"}, 20 | {ConnectionStateConnected, "Connected"}, 21 | {ConnectionStateCompleted, "Completed"}, 22 | {ConnectionStateFailed, "Failed"}, 23 | {ConnectionStateDisconnected, "Disconnected"}, 24 | {ConnectionStateClosed, "Closed"}, 25 | } 26 | 27 | for i, testCase := range testCases { 28 | require.Equal(t, 29 | testCase.expectedString, 30 | testCase.connectionState.String(), 31 | "testCase: %d %v", i, testCase, 32 | ) 33 | } 34 | } 35 | 36 | func TestGatheringState_String(t *testing.T) { 37 | testCases := []struct { 38 | gatheringState GatheringState 39 | expectedString string 40 | }{ 41 | {GatheringStateUnknown, ErrUnknownType.Error()}, 42 | {GatheringStateNew, "new"}, 43 | {GatheringStateGathering, "gathering"}, 44 | {GatheringStateComplete, "complete"}, 45 | } 46 | 47 | for i, testCase := range testCases { 48 | require.Equal(t, 49 | testCase.expectedString, 50 | testCase.gatheringState.String(), 51 | "testCase: %d %v", i, testCase, 52 | ) 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /role_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestUnmarshalText_Success(t *testing.T) { 16 | tests := []struct { 17 | name string 18 | in string 19 | want Role 20 | }{ 21 | {"controlling", "controlling", Controlling}, 22 | {"controlled", "controlled", Controlled}, 23 | } 24 | for _, tt := range tests { 25 | t.Run(tt.name, func(t *testing.T) { 26 | var r Role 27 | err := r.UnmarshalText([]byte(tt.in)) 28 | require.NoError(t, err) 29 | require.Equal(t, tt.want, r) 30 | }) 31 | } 32 | } 33 | 34 | func TestUnmarshalText_UnknownKeepsValueAndErrors(t *testing.T) { 35 | r := Controlled 36 | err := r.UnmarshalText([]byte("neither")) 37 | require.ErrorIs(t, err, errUnknownRole) 38 | require.Equal(t, Controlled, r, "role should remain unchanged on error") 39 | } 40 | 41 | func TestMarshalText(t *testing.T) { 42 | tests := []struct { 43 | name string 44 | in Role 45 | want string 46 | }{ 47 | {"controlling", Controlling, "controlling"}, 48 | {"controlled", Controlled, "controlled"}, 49 | {"unknown", Role(99), "unknown"}, 50 | } 51 | for _, tt := range tests { 52 | t.Run(tt.name, func(t *testing.T) { 53 | b, err := tt.in.MarshalText() 54 | require.NoError(t, err) 55 | require.Equal(t, tt.want, string(b)) 56 | }) 57 | } 58 | } 59 | 60 | func TestString(t *testing.T) { 61 | require.Equal(t, "controlling", Controlling.String()) 62 | require.Equal(t, "controlled", Controlled.String()) 63 | require.Equal(t, "unknown", Role(255).String()) 64 | } 65 | -------------------------------------------------------------------------------- /rand_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "sync" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestRandomGeneratorCollision(t *testing.T) { 14 | candidateIDGen := newCandidateIDGenerator() 15 | 16 | testCases := map[string]struct { 17 | gen func(t *testing.T) string 18 | }{ 19 | "CandidateID": { 20 | gen: func(*testing.T) string { 21 | return candidateIDGen.Generate() 22 | }, 23 | }, 24 | "PWD": { 25 | gen: func(t *testing.T) string { 26 | t.Helper() 27 | 28 | s, err := generatePwd() 29 | require.NoError(t, err) 30 | 31 | return s 32 | }, 33 | }, 34 | "Ufrag": { 35 | gen: func(t *testing.T) string { 36 | t.Helper() 37 | 38 | s, err := generateUFrag() 39 | require.NoError(t, err) 40 | 41 | return s 42 | }, 43 | }, 44 | } 45 | 46 | const num = 100 47 | const iteration = 100 48 | 49 | for name, testCase := range testCases { 50 | testCase := testCase 51 | t.Run(name, func(t *testing.T) { 52 | for iter := 0; iter < iteration; iter++ { 53 | var wg sync.WaitGroup 54 | var mu sync.Mutex 55 | 56 | rands := make([]string, 0, num) 57 | 58 | for i := 0; i < num; i++ { 59 | wg.Add(1) 60 | go func() { 61 | r := testCase.gen(t) 62 | mu.Lock() 63 | rands = append(rands, r) 64 | mu.Unlock() 65 | wg.Done() 66 | }() 67 | } 68 | wg.Wait() 69 | 70 | require.Len(t, rands, num) 71 | for i := 0; i < num; i++ { 72 | for j := i + 1; j < num; j++ { 73 | require.NotEqual(t, rands[i], rands[j]) 74 | } 75 | } 76 | } 77 | }) 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /candidatetype.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import "slices" 7 | 8 | // CandidateType represents the type of candidate. 9 | type CandidateType byte 10 | 11 | // CandidateType enum. 12 | const ( 13 | CandidateTypeUnspecified CandidateType = iota 14 | CandidateTypeHost 15 | CandidateTypeServerReflexive 16 | CandidateTypePeerReflexive 17 | CandidateTypeRelay 18 | ) 19 | 20 | // String makes CandidateType printable. 21 | func (c CandidateType) String() string { 22 | switch c { 23 | case CandidateTypeHost: 24 | return "host" 25 | case CandidateTypeServerReflexive: 26 | return "srflx" 27 | case CandidateTypePeerReflexive: 28 | return "prflx" 29 | case CandidateTypeRelay: 30 | return "relay" 31 | case CandidateTypeUnspecified: 32 | return "Unknown candidate type" 33 | } 34 | 35 | return "Unknown candidate type" 36 | } 37 | 38 | // Preference returns the preference weight of a CandidateType 39 | // 40 | // 4.1.2.2. Guidelines for Choosing Type and Local Preferences 41 | // The RECOMMENDED values are 126 for host candidates, 100 42 | // for server reflexive candidates, 110 for peer reflexive candidates, 43 | // and 0 for relayed candidates. 44 | func (c CandidateType) Preference() uint16 { 45 | switch c { 46 | case CandidateTypeHost: 47 | return 126 48 | case CandidateTypePeerReflexive: 49 | return 110 50 | case CandidateTypeServerReflexive: 51 | return 100 52 | case CandidateTypeRelay, CandidateTypeUnspecified: 53 | return 0 54 | } 55 | 56 | return 0 57 | } 58 | 59 | func containsCandidateType(candidateType CandidateType, candidateTypeList []CandidateType) bool { 60 | if candidateTypeList == nil { 61 | return false 62 | } 63 | 64 | return slices.Contains(candidateTypeList, candidateType) 65 | } 66 | -------------------------------------------------------------------------------- /agent_get_best_valid_candidate_pair_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestAgentGetBestValidCandidatePair(t *testing.T) { 16 | f := setupTestAgentGetBestValidCandidatePair(t) 17 | defer func() { 18 | require.NoError(t, f.sut.Close()) 19 | }() 20 | 21 | remoteCandidatesFromLowestPriorityToHighest := []Candidate{f.relayRemote, f.srflxRemote, f.prflxRemote, f.hostRemote} 22 | 23 | for _, remoteCandidate := range remoteCandidatesFromLowestPriorityToHighest { 24 | candidatePair := f.sut.addPair(f.hostLocal, remoteCandidate) 25 | candidatePair.state = CandidatePairStateSucceeded 26 | 27 | actualBestPair := f.sut.getBestValidCandidatePair() 28 | expectedBestPair := &CandidatePair{Remote: remoteCandidate, Local: f.hostLocal, state: CandidatePairStateSucceeded} 29 | 30 | require.Equal(t, actualBestPair.String(), expectedBestPair.String()) 31 | } 32 | } 33 | 34 | func setupTestAgentGetBestValidCandidatePair(t *testing.T) *TestAgentGetBestValidCandidatePairFixture { 35 | t.Helper() 36 | 37 | fixture := new(TestAgentGetBestValidCandidatePairFixture) 38 | fixture.hostLocal = newHostLocal(t) 39 | fixture.relayRemote = newRelayRemote(t) 40 | fixture.srflxRemote = newSrflxRemote(t) 41 | fixture.prflxRemote = newPrflxRemote(t) 42 | fixture.hostRemote = newHostRemote(t) 43 | 44 | agent, err := NewAgent(&AgentConfig{}) 45 | require.NoError(t, err) 46 | fixture.sut = agent 47 | 48 | return fixture 49 | } 50 | 51 | type TestAgentGetBestValidCandidatePairFixture struct { 52 | sut *Agent 53 | 54 | hostLocal Candidate 55 | relayRemote Candidate 56 | srflxRemote Candidate 57 | prflxRemote Candidate 58 | hostRemote Candidate 59 | } 60 | -------------------------------------------------------------------------------- /candidate_peer_reflexive.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package ice ... 5 | // 6 | //nolint:dupl 7 | package ice 8 | 9 | import ( 10 | "net/netip" 11 | ) 12 | 13 | // CandidatePeerReflexive ... 14 | type CandidatePeerReflexive struct { 15 | candidateBase 16 | } 17 | 18 | // CandidatePeerReflexiveConfig is the config required to create a new CandidatePeerReflexive. 19 | type CandidatePeerReflexiveConfig struct { 20 | CandidateID string 21 | Network string 22 | Address string 23 | Port int 24 | Component uint16 25 | Priority uint32 26 | Foundation string 27 | RelAddr string 28 | RelPort int 29 | } 30 | 31 | // NewCandidatePeerReflexive creates a new peer reflective candidate. 32 | func NewCandidatePeerReflexive(config *CandidatePeerReflexiveConfig) (*CandidatePeerReflexive, error) { 33 | ipAddr, err := netip.ParseAddr(config.Address) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | networkType, err := determineNetworkType(config.Network, ipAddr) 39 | if err != nil { 40 | return nil, err 41 | } 42 | 43 | candidateID := config.CandidateID 44 | if candidateID == "" { 45 | candidateID = globalCandidateIDGenerator.Generate() 46 | } 47 | 48 | return &CandidatePeerReflexive{ 49 | candidateBase: candidateBase{ 50 | id: candidateID, 51 | networkType: networkType, 52 | candidateType: CandidateTypePeerReflexive, 53 | address: config.Address, 54 | port: config.Port, 55 | resolvedAddr: createAddr(networkType, ipAddr, config.Port), 56 | component: config.Component, 57 | foundationOverride: config.Foundation, 58 | priorityOverride: config.Priority, 59 | relatedAddress: &CandidateRelatedAddress{ 60 | Address: config.RelAddr, 61 | Port: config.RelPort, 62 | }, 63 | remoteCandidateCaches: map[AddrPort]Candidate{}, 64 | }, 65 | }, nil 66 | } 67 | -------------------------------------------------------------------------------- /candidate_server_reflexive.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "net" 8 | "net/netip" 9 | ) 10 | 11 | // CandidateServerReflexive ... 12 | type CandidateServerReflexive struct { 13 | candidateBase 14 | } 15 | 16 | // CandidateServerReflexiveConfig is the config required to create a new CandidateServerReflexive. 17 | type CandidateServerReflexiveConfig struct { 18 | CandidateID string 19 | Network string 20 | Address string 21 | Port int 22 | Component uint16 23 | Priority uint32 24 | Foundation string 25 | RelAddr string 26 | RelPort int 27 | } 28 | 29 | // NewCandidateServerReflexive creates a new server reflective candidate. 30 | func NewCandidateServerReflexive(config *CandidateServerReflexiveConfig) (*CandidateServerReflexive, error) { 31 | ipAddr, err := netip.ParseAddr(config.Address) 32 | if err != nil { 33 | return nil, err 34 | } 35 | 36 | networkType, err := determineNetworkType(config.Network, ipAddr) 37 | if err != nil { 38 | return nil, err 39 | } 40 | 41 | candidateID := config.CandidateID 42 | if candidateID == "" { 43 | candidateID = globalCandidateIDGenerator.Generate() 44 | } 45 | 46 | return &CandidateServerReflexive{ 47 | candidateBase: candidateBase{ 48 | id: candidateID, 49 | networkType: networkType, 50 | candidateType: CandidateTypeServerReflexive, 51 | address: config.Address, 52 | port: config.Port, 53 | resolvedAddr: &net.UDPAddr{ 54 | IP: ipAddr.AsSlice(), 55 | Port: config.Port, 56 | Zone: ipAddr.Zone(), 57 | }, 58 | component: config.Component, 59 | foundationOverride: config.Foundation, 60 | priorityOverride: config.Priority, 61 | relatedAddress: &CandidateRelatedAddress{ 62 | Address: config.RelAddr, 63 | Port: config.RelPort, 64 | }, 65 | remoteCandidateCaches: map[AddrPort]Candidate{}, 66 | }, 67 | }, nil 68 | } 69 | -------------------------------------------------------------------------------- /.github/workflows/test.yaml: -------------------------------------------------------------------------------- 1 | # 2 | # DO NOT EDIT THIS FILE 3 | # 4 | # It is automatically copied from https://github.com/pion/.goassets repository. 5 | # If this repository should have package specific CI config, 6 | # remove the repository name from .goassets/.github/workflows/assets-sync.yml. 7 | # 8 | # If you want to update the shared CI config, send a PR to 9 | # https://github.com/pion/.goassets instead of this repository. 10 | # 11 | # SPDX-FileCopyrightText: 2023 The Pion community 12 | # SPDX-License-Identifier: MIT 13 | 14 | name: Test 15 | on: 16 | push: 17 | branches: 18 | - master 19 | pull_request: 20 | 21 | jobs: 22 | test: 23 | uses: pion/.goassets/.github/workflows/test.reusable.yml@master 24 | strategy: 25 | matrix: 26 | go: ["1.25", "1.24"] # auto-update/supported-go-version-list 27 | fail-fast: false 28 | with: 29 | go-version: ${{ matrix.go }} 30 | secrets: inherit 31 | 32 | test-i386: 33 | uses: pion/.goassets/.github/workflows/test-i386.reusable.yml@master 34 | strategy: 35 | matrix: 36 | go: ["1.25", "1.24"] # auto-update/supported-go-version-list 37 | fail-fast: false 38 | with: 39 | go-version: ${{ matrix.go }} 40 | 41 | test-windows: 42 | uses: pion/.goassets/.github/workflows/test-windows.reusable.yml@master 43 | strategy: 44 | matrix: 45 | go: ["1.25", "1.24"] # auto-update/supported-go-version-list 46 | fail-fast: false 47 | with: 48 | go-version: ${{ matrix.go }} 49 | 50 | test-macos: 51 | uses: pion/.goassets/.github/workflows/test-macos.reusable.yml@master 52 | strategy: 53 | matrix: 54 | go: ["1.25", "1.24"] # auto-update/supported-go-version-list 55 | fail-fast: false 56 | with: 57 | go-version: ${{ matrix.go }} 58 | 59 | test-wasm: 60 | uses: pion/.goassets/.github/workflows/test-wasm.reusable.yml@master 61 | with: 62 | go-version: "1.25" # auto-update/latest-go-version 63 | secrets: inherit 64 | -------------------------------------------------------------------------------- /rand.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import "github.com/pion/randutil" 7 | 8 | const ( 9 | runesAlpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" 10 | runesDigit = "0123456789" 11 | runesCandidateIDFoundation = runesAlpha + runesDigit + "+/" 12 | 13 | lenUFrag = 16 14 | lenPwd = 32 15 | ) 16 | 17 | // Seeding random generator each time limits number of generated sequence to 31-bits, 18 | // and causes collision on low time accuracy environments. 19 | // Use global random generator seeded by crypto grade random. 20 | var ( 21 | globalMathRandomGenerator = randutil.NewMathRandomGenerator() //nolint:gochecknoglobals 22 | globalCandidateIDGenerator = candidateIDGenerator{globalMathRandomGenerator} //nolint:gochecknoglobals 23 | ) 24 | 25 | // candidateIDGenerator is a random candidate ID generator. 26 | // Candidate ID is used in SDP and always shared to the other peer. 27 | // It doesn't require cryptographic random. 28 | type candidateIDGenerator struct { 29 | randutil.MathRandomGenerator 30 | } 31 | 32 | func newCandidateIDGenerator() *candidateIDGenerator { 33 | return &candidateIDGenerator{ 34 | randutil.NewMathRandomGenerator(), 35 | } 36 | } 37 | 38 | func (g *candidateIDGenerator) Generate() string { 39 | // https://tools.ietf.org/html/rfc5245#section-15.1 40 | // candidate-id = "candidate" ":" foundation 41 | // foundation = 1*32ice-char 42 | // ice-char = ALPHA / DIGIT / "+" / "/" 43 | return "candidate:" + g.MathRandomGenerator.GenerateString(32, runesCandidateIDFoundation) 44 | } 45 | 46 | // generatePwd generates ICE pwd. 47 | // This internally uses generateCryptoRandomString. 48 | func generatePwd() (string, error) { 49 | return randutil.GenerateCryptoRandomString(lenPwd, runesAlpha) 50 | } 51 | 52 | // generateUFrag generates ICE user fragment. 53 | // This internally uses generateCryptoRandomString. 54 | func generateUFrag() (string, error) { 55 | return randutil.GenerateCryptoRandomString(lenUFrag, runesAlpha) 56 | } 57 | -------------------------------------------------------------------------------- /candidate_server_reflexive_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "net" 11 | "strconv" 12 | "testing" 13 | "time" 14 | 15 | "github.com/pion/stun/v3" 16 | "github.com/pion/transport/v3/test" 17 | "github.com/pion/turn/v4" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func TestServerReflexiveOnlyConnection(t *testing.T) { 22 | defer test.CheckRoutines(t)() 23 | 24 | // Limit runtime in case of deadlocks 25 | defer test.TimeOut(time.Second * 30).Stop() 26 | 27 | serverPort := randomPort(t) 28 | serverListener, err := net.ListenPacket("udp4", "127.0.0.1:"+strconv.Itoa(serverPort)) // nolint: noctx 29 | require.NoError(t, err) 30 | 31 | server, err := turn.NewServer(turn.ServerConfig{ 32 | Realm: "pion.ly", 33 | AuthHandler: optimisticAuthHandler, 34 | PacketConnConfigs: []turn.PacketConnConfig{ 35 | { 36 | PacketConn: serverListener, 37 | RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"}, 38 | }, 39 | }, 40 | }) 41 | require.NoError(t, err) 42 | defer func() { 43 | require.NoError(t, server.Close()) 44 | }() 45 | 46 | cfg := &AgentConfig{ 47 | NetworkTypes: []NetworkType{NetworkTypeUDP4}, 48 | Urls: []*stun.URI{ 49 | { 50 | Scheme: SchemeTypeSTUN, 51 | Host: "127.0.0.1", 52 | Port: serverPort, 53 | }, 54 | }, 55 | CandidateTypes: []CandidateType{CandidateTypeServerReflexive}, 56 | } 57 | 58 | aAgent, err := NewAgent(cfg) 59 | require.NoError(t, err) 60 | defer func() { 61 | require.NoError(t, aAgent.Close()) 62 | }() 63 | 64 | aNotifier, aConnected := onConnected() 65 | require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) 66 | 67 | bAgent, err := NewAgent(cfg) 68 | require.NoError(t, err) 69 | defer func() { 70 | require.NoError(t, bAgent.Close()) 71 | }() 72 | 73 | bNotifier, bConnected := onConnected() 74 | require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) 75 | 76 | connect(t, aAgent, bAgent) 77 | <-aConnected 78 | <-bConnected 79 | } 80 | -------------------------------------------------------------------------------- /internal/stun/stun.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package stun contains ICE specific STUN code 5 | package stun 6 | 7 | import ( 8 | "errors" 9 | "fmt" 10 | "net" 11 | "time" 12 | 13 | "github.com/pion/stun/v3" 14 | ) 15 | 16 | var ( 17 | errGetXorMappedAddrResponse = errors.New("failed to get XOR-MAPPED-ADDRESS response") 18 | errMismatchUsername = errors.New("username mismatch") 19 | ) 20 | 21 | // GetXORMappedAddr initiates a STUN requests to serverAddr using conn, reads the response and returns 22 | // the XORMappedAddress returned by the STUN server. 23 | func GetXORMappedAddr(conn net.PacketConn, serverAddr net.Addr, timeout time.Duration) (*stun.XORMappedAddress, error) { 24 | if timeout > 0 { 25 | if err := conn.SetReadDeadline(time.Now().Add(timeout)); err != nil { 26 | return nil, err 27 | } 28 | 29 | // Reset timeout after completion 30 | defer conn.SetReadDeadline(time.Time{}) //nolint:errcheck 31 | } 32 | 33 | req, err := stun.Build(stun.BindingRequest, stun.TransactionID) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | if _, err = conn.WriteTo(req.Raw, serverAddr); err != nil { 39 | return nil, err 40 | } 41 | 42 | const maxMessageSize = 1280 43 | buf := make([]byte, maxMessageSize) 44 | n, _, err := conn.ReadFrom(buf) 45 | if err != nil { 46 | return nil, err 47 | } 48 | 49 | res := &stun.Message{Raw: buf[:n]} 50 | if err = res.Decode(); err != nil { 51 | return nil, err 52 | } 53 | 54 | var addr stun.XORMappedAddress 55 | if err = addr.GetFrom(res); err != nil { 56 | return nil, fmt.Errorf("%w: %v", errGetXorMappedAddrResponse, err) //nolint:errorlint 57 | } 58 | 59 | return &addr, nil 60 | } 61 | 62 | // AssertUsername checks that the given STUN message m has a USERNAME attribute with a given value. 63 | func AssertUsername(m *stun.Message, expectedUsername string) error { 64 | var username stun.Username 65 | if err := username.GetFrom(m); err != nil { 66 | return err 67 | } else if string(username) != expectedUsername { 68 | return fmt.Errorf("%w expected(%x) actual(%x)", errMismatchUsername, expectedUsername, string(username)) 69 | } 70 | 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /utils_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func newHostRemote(t *testing.T) *CandidateHost { 16 | t.Helper() 17 | 18 | remoteHostConfig := &CandidateHostConfig{ 19 | Network: "udp", 20 | Address: "1.2.3.5", 21 | Port: 12350, 22 | Component: 1, 23 | } 24 | hostRemote, err := NewCandidateHost(remoteHostConfig) 25 | require.NoError(t, err) 26 | 27 | return hostRemote 28 | } 29 | 30 | func newPrflxRemote(t *testing.T) *CandidatePeerReflexive { 31 | t.Helper() 32 | 33 | prflxConfig := &CandidatePeerReflexiveConfig{ 34 | Network: "udp", 35 | Address: "10.10.10.2", 36 | Port: 19217, 37 | Component: 1, 38 | RelAddr: "4.3.2.1", 39 | RelPort: 43211, 40 | } 41 | prflxRemote, err := NewCandidatePeerReflexive(prflxConfig) 42 | require.NoError(t, err) 43 | 44 | return prflxRemote 45 | } 46 | 47 | func newSrflxRemote(t *testing.T) *CandidateServerReflexive { 48 | t.Helper() 49 | 50 | srflxConfig := &CandidateServerReflexiveConfig{ 51 | Network: "udp", 52 | Address: "10.10.10.2", 53 | Port: 19218, 54 | Component: 1, 55 | RelAddr: "4.3.2.1", 56 | RelPort: 43212, 57 | } 58 | srflxRemote, err := NewCandidateServerReflexive(srflxConfig) 59 | require.NoError(t, err) 60 | 61 | return srflxRemote 62 | } 63 | 64 | func newRelayRemote(t *testing.T) *CandidateRelay { 65 | t.Helper() 66 | 67 | relayConfig := &CandidateRelayConfig{ 68 | Network: "udp", 69 | Address: "1.2.3.4", 70 | Port: 12340, 71 | Component: 1, 72 | RelAddr: "4.3.2.1", 73 | RelPort: 43210, 74 | } 75 | relayRemote, err := NewCandidateRelay(relayConfig) 76 | require.NoError(t, err) 77 | 78 | return relayRemote 79 | } 80 | 81 | func newHostLocal(t *testing.T) *CandidateHost { 82 | t.Helper() 83 | 84 | localHostConfig := &CandidateHostConfig{ 85 | Network: "udp", 86 | Address: "192.168.1.1", 87 | Port: 19216, 88 | Component: 1, 89 | } 90 | hostLocal, err := NewCandidateHost(localHostConfig) 91 | require.NoError(t, err) 92 | 93 | return hostLocal 94 | } 95 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | Pion ICE 4 |
5 |

6 |

A Go implementation of ICE

7 |

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

17 |
18 | 19 | ### Roadmap 20 | The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones. 21 | 22 | ### Community 23 | Pion has an active community on the [Discord](https://discord.gg/PngbdqpFbt). 24 | 25 | Follow the [Pion Bluesky](https://bsky.app/profile/pion.ly) or [Pion Twitter](https://twitter.com/_pion) for project updates and important WebRTC news. 26 | 27 | We are always looking to support **your projects**. Please reach out if you have something to build! 28 | If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly) 29 | 30 | ### Contributing 31 | Check out the [contributing wiki](https://github.com/pion/webrtc/wiki/Contributing) to join the group of amazing people making this project possible 32 | 33 | ### License 34 | MIT License - see [LICENSE](LICENSE) for full text 35 | -------------------------------------------------------------------------------- /examples/continual-gathering/README.md: -------------------------------------------------------------------------------- 1 | # Continual Gathering Example 2 | 3 | This example demonstrates the `ContinualGatheringPolicy` feature in Pion ICE, which allows agents to continuously discover network candidates throughout a connection's lifetime. 4 | 5 | ## Overview 6 | 7 | Traditional ICE gathering (`GatherOnce`) collects candidates once at startup and stops. This can be problematic when: 8 | 9 | - Users switch between WiFi and cellular networks 10 | - Network interfaces are added/removed 11 | - Moving between access points ("walk-out-the-door" problem) 12 | 13 | With `GatherContinually`, the agent monitors for network changes and automatically discovers new candidates, enabling seamless connectivity transitions. 14 | 15 | ## Usage 16 | 17 | ```bash 18 | # Traditional gathering (stops after initial collection) 19 | go run main.go -mode once 20 | 21 | # Continual gathering (monitors for network changes) 22 | go run main.go -mode continually -interval 2s 23 | ``` 24 | 25 | ## Testing 26 | 27 | While running in continual mode, try: 28 | 29 | - Connecting/disconnecting WiFi 30 | - Enabling/disabling network adapters 31 | - Switching between networks 32 | 33 | New candidates will be discovered and reported automatically! 34 | 35 | ### Testing with Virtual Network Adapters (Linux) 36 | 37 | You can easily test the continual gathering by creating/removing virtual network adapters: 38 | 39 | ```bash 40 | # Create a virtual network adapter with an IP address 41 | sudo ip link add veth0 type veth peer name veth1 42 | sudo ip addr add 10.0.0.1/24 dev veth0 43 | sudo ip link set veth0 up 44 | 45 | # Wait a few seconds, then check the example output 46 | # You should see new candidates for 10.0.0.1 47 | 48 | # Remove the virtual adapter 49 | sudo ip link delete veth0 50 | 51 | # The removed interface will be detected and logged 52 | ``` 53 | 54 | Alternative using dummy interface: 55 | ```bash 56 | # Create a dummy interface (simpler, no peer needed) 57 | sudo ip link add dummy0 type dummy 58 | sudo ip addr add 192.168.100.1/24 dev dummy0 59 | sudo ip link set dummy0 up 60 | 61 | # Remove it 62 | sudo ip link delete dummy0 63 | ``` 64 | 65 | You can also change IP addresses on existing interfaces: 66 | ```bash 67 | # Add a secondary IP to an existing interface 68 | sudo ip addr add 172.16.0.1/24 dev eth0 69 | 70 | # Remove it 71 | sudo ip addr del 172.16.0.1/24 dev eth0 72 | ``` 73 | -------------------------------------------------------------------------------- /candidate_host.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "net/netip" 8 | "strings" 9 | ) 10 | 11 | // CandidateHost is a candidate of type host. 12 | type CandidateHost struct { 13 | candidateBase 14 | 15 | network string 16 | } 17 | 18 | // CandidateHostConfig is the config required to create a new CandidateHost. 19 | type CandidateHostConfig struct { 20 | CandidateID string 21 | Network string 22 | Address string 23 | Port int 24 | Component uint16 25 | Priority uint32 26 | Foundation string 27 | TCPType TCPType 28 | IsLocationTracked bool 29 | } 30 | 31 | // NewCandidateHost creates a new host candidate. 32 | func NewCandidateHost(config *CandidateHostConfig) (*CandidateHost, error) { 33 | candidateID := config.CandidateID 34 | 35 | if candidateID == "" { 36 | candidateID = globalCandidateIDGenerator.Generate() 37 | } 38 | 39 | candidateHost := &CandidateHost{ 40 | candidateBase: candidateBase{ 41 | id: candidateID, 42 | address: config.Address, 43 | candidateType: CandidateTypeHost, 44 | component: config.Component, 45 | port: config.Port, 46 | tcpType: config.TCPType, 47 | foundationOverride: config.Foundation, 48 | priorityOverride: config.Priority, 49 | remoteCandidateCaches: map[AddrPort]Candidate{}, 50 | isLocationTracked: config.IsLocationTracked, 51 | }, 52 | network: config.Network, 53 | } 54 | 55 | if !strings.HasSuffix(config.Address, ".local") { 56 | ipAddr, err := netip.ParseAddr(config.Address) 57 | if err != nil { 58 | return nil, err 59 | } 60 | 61 | if err := candidateHost.setIPAddr(ipAddr); err != nil { 62 | return nil, err 63 | } 64 | } else { 65 | // Until mDNS candidate is resolved assume it is UDPv4 66 | candidateHost.candidateBase.networkType = NetworkTypeUDP4 67 | } 68 | 69 | return candidateHost, nil 70 | } 71 | 72 | func (c *CandidateHost) setIPAddr(addr netip.Addr) error { 73 | networkType, err := determineNetworkType(c.network, addr) 74 | if err != nil { 75 | return err 76 | } 77 | 78 | c.candidateBase.networkType = networkType 79 | c.candidateBase.resolvedAddr = createAddr(networkType, addr, c.port) 80 | 81 | return nil 82 | } 83 | -------------------------------------------------------------------------------- /candidate_relay_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "net" 11 | "strconv" 12 | "testing" 13 | "time" 14 | 15 | "github.com/pion/stun/v3" 16 | "github.com/pion/transport/v3/test" 17 | "github.com/pion/turn/v4" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func optimisticAuthHandler(string, string, net.Addr) (key []byte, ok bool) { 22 | return turn.GenerateAuthKey("username", "pion.ly", "password"), true 23 | } 24 | 25 | func TestRelayOnlyConnection(t *testing.T) { 26 | // Limit runtime in case of deadlocks 27 | defer test.TimeOut(time.Second * 30).Stop() 28 | 29 | defer test.CheckRoutines(t)() 30 | 31 | serverPort := randomPort(t) 32 | serverListener, err := net.ListenPacket("udp", localhostIPStr+":"+strconv.Itoa(serverPort)) // nolint: noctx 33 | require.NoError(t, err) 34 | 35 | server, err := turn.NewServer(turn.ServerConfig{ 36 | Realm: "pion.ly", 37 | AuthHandler: optimisticAuthHandler, 38 | PacketConnConfigs: []turn.PacketConnConfig{ 39 | { 40 | PacketConn: serverListener, 41 | RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: localhostIPStr + ""}, 42 | }, 43 | }, 44 | }) 45 | require.NoError(t, err) 46 | defer func() { 47 | require.NoError(t, server.Close()) 48 | }() 49 | 50 | cfg := &AgentConfig{ 51 | NetworkTypes: supportedNetworkTypes(), 52 | Urls: []*stun.URI{ 53 | { 54 | Scheme: stun.SchemeTypeTURN, 55 | Host: localhostIPStr + "", 56 | Username: "username", 57 | Password: "password", 58 | Port: serverPort, 59 | Proto: stun.ProtoTypeUDP, 60 | }, 61 | }, 62 | CandidateTypes: []CandidateType{CandidateTypeRelay}, 63 | } 64 | 65 | aAgent, err := NewAgent(cfg) 66 | require.NoError(t, err) 67 | defer func() { 68 | require.NoError(t, aAgent.Close()) 69 | }() 70 | 71 | aNotifier, aConnected := onConnected() 72 | require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) 73 | 74 | bAgent, err := NewAgent(cfg) 75 | require.NoError(t, err) 76 | defer func() { 77 | require.NoError(t, bAgent.Close()) 78 | }() 79 | 80 | bNotifier, bConnected := onConnected() 81 | require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) 82 | 83 | connect(t, aAgent, bAgent) 84 | <-aConnected 85 | <-bConnected 86 | } 87 | -------------------------------------------------------------------------------- /transport_vnet_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "fmt" 11 | "net" 12 | "testing" 13 | "time" 14 | 15 | "github.com/pion/stun/v3" 16 | "github.com/pion/transport/v3/test" 17 | "github.com/pion/transport/v3/vnet" 18 | "github.com/stretchr/testify/require" 19 | ) 20 | 21 | func TestRemoteLocalAddr(t *testing.T) { 22 | // Check for leaking routines 23 | defer test.CheckRoutines(t)() 24 | 25 | // Limit runtime in case of deadlocks 26 | defer test.TimeOut(time.Second * 20).Stop() 27 | 28 | // Agent0 is behind 1:1 NAT 29 | natType0 := &vnet.NATType{Mode: vnet.NATModeNAT1To1} 30 | // Agent1 is behind 1:1 NAT 31 | natType1 := &vnet.NATType{Mode: vnet.NATModeNAT1To1} 32 | 33 | builtVnet, errVnet := buildVNet(natType0, natType1) 34 | require.NoError(t, errVnet, "should succeed") 35 | defer builtVnet.close() 36 | 37 | stunServerURL := &stun.URI{ 38 | Scheme: stun.SchemeTypeSTUN, 39 | Host: vnetSTUNServerIP, 40 | Port: vnetSTUNServerPort, 41 | Proto: stun.ProtoTypeUDP, 42 | } 43 | 44 | t.Run("Disconnected Returns nil", func(t *testing.T) { 45 | disconnectedAgent, err := NewAgent(&AgentConfig{}) 46 | require.NoError(t, err) 47 | 48 | disconnectedConn := Conn{agent: disconnectedAgent} 49 | require.Nil(t, disconnectedConn.RemoteAddr()) 50 | require.Nil(t, disconnectedConn.LocalAddr()) 51 | 52 | require.NoError(t, disconnectedConn.Close()) 53 | }) 54 | 55 | t.Run("Remote/Local Pair Match between Agents", func(t *testing.T) { 56 | ca, cb := pipeWithVNet(t, builtVnet, 57 | &agentTestConfig{ 58 | urls: []*stun.URI{stunServerURL}, 59 | }, 60 | &agentTestConfig{ 61 | urls: []*stun.URI{stunServerURL}, 62 | }, 63 | ) 64 | defer closePipe(t, ca, cb) 65 | 66 | aRAddr := ca.RemoteAddr() 67 | aLAddr := ca.LocalAddr() 68 | bRAddr := cb.RemoteAddr() 69 | bLAddr := cb.LocalAddr() 70 | 71 | // Assert that nothing is nil 72 | require.NotNil(t, aRAddr) 73 | require.NotNil(t, aLAddr) 74 | require.NotNil(t, bRAddr) 75 | require.NotNil(t, bLAddr) 76 | 77 | // Assert addresses 78 | require.Equal(t, aLAddr.String(), 79 | fmt.Sprintf("%s:%d", vnetLocalIPA, bRAddr.(*net.UDPAddr).Port), //nolint:forcetypeassert 80 | ) 81 | require.Equal(t, bLAddr.String(), 82 | fmt.Sprintf("%s:%d", vnetLocalIPB, aRAddr.(*net.UDPAddr).Port), //nolint:forcetypeassert 83 | ) 84 | require.Equal(t, aRAddr.String(), 85 | fmt.Sprintf("%s:%d", vnetGlobalIPB, bLAddr.(*net.UDPAddr).Port), //nolint:forcetypeassert 86 | ) 87 | require.Equal(t, bRAddr.String(), 88 | fmt.Sprintf("%s:%d", vnetGlobalIPA, aLAddr.(*net.UDPAddr).Port), //nolint:forcetypeassert 89 | ) 90 | }) 91 | } 92 | -------------------------------------------------------------------------------- /url.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import "github.com/pion/stun/v3" 7 | 8 | type ( 9 | // URL represents a STUN (rfc7064) or TURN (rfc7065) URI. 10 | // 11 | // Deprecated: Please use pion/stun.URI. 12 | URL = stun.URI 13 | 14 | // ProtoType indicates the transport protocol type that is used in the ice.URL 15 | // structure. 16 | // 17 | // Deprecated: TPlease use pion/stun.ProtoType. 18 | ProtoType = stun.ProtoType 19 | 20 | // SchemeType indicates the type of server used in the ice.URL structure. 21 | // 22 | // Deprecated: Please use pion/stun.SchemeType. 23 | SchemeType = stun.SchemeType 24 | ) 25 | 26 | const ( 27 | // SchemeTypeSTUN indicates the URL represents a STUN server. 28 | // 29 | // Deprecated: Please use pion/stun.SchemeTypeSTUN. 30 | SchemeTypeSTUN = stun.SchemeTypeSTUN 31 | 32 | // SchemeTypeSTUNS indicates the URL represents a STUNS (secure) server. 33 | // 34 | // Deprecated: Please use pion/stun.SchemeTypeSTUNS. 35 | SchemeTypeSTUNS = stun.SchemeTypeSTUNS 36 | 37 | // SchemeTypeTURN indicates the URL represents a TURN server. 38 | // 39 | // Deprecated: Please use pion/stun.SchemeTypeTURN. 40 | SchemeTypeTURN = stun.SchemeTypeTURN 41 | 42 | // SchemeTypeTURNS indicates the URL represents a TURNS (secure) server. 43 | // 44 | // Deprecated: Please use pion/stun.SchemeTypeTURNS. 45 | SchemeTypeTURNS = stun.SchemeTypeTURNS 46 | ) 47 | 48 | const ( 49 | // ProtoTypeUDP indicates the URL uses a UDP transport. 50 | // 51 | // Deprecated: Please use pion/stun.ProtoTypeUDP. 52 | ProtoTypeUDP = stun.ProtoTypeUDP 53 | 54 | // ProtoTypeTCP indicates the URL uses a TCP transport. 55 | // 56 | // Deprecated: Please use pion/stun.ProtoTypeTCP. 57 | ProtoTypeTCP = stun.ProtoTypeTCP 58 | ) 59 | 60 | // Unknown represents and unknown ProtoType or SchemeType. 61 | // 62 | // Deprecated: Please use pion/stun.SchemeTypeUnknown or pion/stun.ProtoTypeUnknown. 63 | const Unknown = 0 64 | 65 | // ParseURL parses a STUN or TURN urls following the ABNF syntax described in. 66 | // https://tools.ietf.org/html/rfc7064 and https://tools.ietf.org/html/rfc7065 67 | // respectively. 68 | // 69 | // Deprecated: Please use pion/stun.ParseURI. 70 | var ParseURL = stun.ParseURI //nolint:gochecknoglobals 71 | 72 | // NewSchemeType defines a procedure for creating a new SchemeType from a raw. 73 | // string naming the scheme type. 74 | // 75 | // Deprecated: Please use pion/stun.NewSchemeType. 76 | var NewSchemeType = stun.NewSchemeType //nolint:gochecknoglobals 77 | 78 | // NewProtoType defines a procedure for creating a new ProtoType from a raw. 79 | // string naming the transport protocol type. 80 | // 81 | // Deprecated: Please use pion/stun.NewProtoType. 82 | var NewProtoType = stun.NewProtoType //nolint:gochecknoglobals 83 | -------------------------------------------------------------------------------- /examples/nat-rules/README.md: -------------------------------------------------------------------------------- 1 | # Address Rewrite Rules Demo 2 | 3 | This demo shows how the extended `AddressRewriteRule` feature in `pion/ice` rewrites addresses for a multi-homed host. It runs a small client that gathers host, srflx, UDP, and TCP candidates so you can see exactly what each rule produces. 4 | 5 | The included `docker-compose.yml` places one container on multiple host networks, similar to setups like the Glimesh broadcast box. The demo covers: 6 | 7 | 1. Multiple host networks with fixed public IPs for each interface. 8 | 2. Host and server-reflexive addresses from deterministic srflx pools. 9 | 3. Scoped rules that only rewrite specific CIDRs, with a global fallback for others. 10 | 4. TCP candidates generated by an automatic TCP mux. 11 | 5. Zero-length External handling: replace+empty drops a candidate, append+empty keeps it (useful for deny/allow layering). 12 | 13 | ## Scenarios 14 | 15 | | Key | Description | 16 | | ----------- | --------------------------------------------------------------------------- | 17 | | `multi-net` | Two host networks with distinct public IPs plus a global fallback. | 18 | | `srflx` | A srflx pool (two addresses) plus a host mapping for the service interface. | 19 | | `scoped` | A CIDR-scoped rule that overrides the global mapping only for matching IPs. | 20 | | `iface` | Interface-scoped host rewrite; only matching NICs are rewritten. | 21 | 22 | The client prints local interfaces and each gathered candidate. A `nil` candidate marks the end of the scenario. 23 | 24 | ## Docker Compose Topology 25 | 26 | The service `nat-demo` attaches to three bridge networks: 27 | 28 | | Network | Subnet | Purpose | 29 | | ------------- | ------------ | --------------------- | 30 | | `lan_blue` | 10.10.0.0/24 | First host interface | 31 | | `lan_green` | 10.20.0.0/24 | Second host interface | 32 | | `lan_service` | 10.30.0.0/24 | Shared service leg | 33 | 34 | Run everything with: 35 | 36 | ```sh 37 | docker compose up --build nat-demo 38 | ``` 39 | 40 | ## Configuration Variables 41 | 42 | All mappings come from environment variables so you can match your own network. The important ones are defined in `docker-compose.yml`: 43 | 44 | * `NAT_DEMO_BLUE_LOCAL` / `NAT_DEMO_BLUE_PUBLIC` 45 | * `NAT_DEMO_BLUE_IFACE` (default `eth0`) 46 | * `NAT_DEMO_GREEN_LOCAL` / `NAT_DEMO_GREEN_PUBLIC` 47 | * `NAT_DEMO_GREEN_IFACE` (default `eth1`) 48 | * `NAT_DEMO_GLOBAL_HOST_FALLBACK` 49 | * `NAT_DEMO_SERVICE_LOCAL` / `NAT_DEMO_SERVICE_HOST_PUBLIC` 50 | * `NAT_DEMO_SCOPED_PUBLIC` / `NAT_DEMO_SCOPED_CIDR` 51 | * `NAT_DEMO_SRFLX_PRIMARY` / `NAT_DEMO_SRFLX_SECONDARY` 52 | * `NAT_DEMO_DROP_LAN` (optional) — set to `1` to drop LAN host candidates via a replace+empty rule. 53 | 54 | Override any value with `docker compose run -e VAR=... nat-demo`. 55 | -------------------------------------------------------------------------------- /renomination.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2025 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "fmt" 8 | 9 | "github.com/pion/stun/v3" 10 | ) 11 | 12 | // Default STUN Nomination attribute type for ICE renomination. 13 | // Following the specification draft-thatcher-ice-renomination-01. 14 | const ( 15 | // DefaultNominationAttribute represents the default STUN Nomination attribute. 16 | // This is a custom attribute for ICE renomination support. 17 | // This value can be overridden via AgentConfig.NominationAttribute. 18 | DefaultNominationAttribute stun.AttrType = 0x0030 // Using a value in the reserved range 19 | ) 20 | 21 | // NominationAttribute represents a STUN Nomination attribute. 22 | type NominationAttribute struct { 23 | Value uint32 24 | } 25 | 26 | // GetFrom decodes a Nomination attribute from a STUN message. 27 | func (a *NominationAttribute) GetFrom(m *stun.Message) error { 28 | return a.GetFromWithType(m, DefaultNominationAttribute) 29 | } 30 | 31 | // GetFromWithType decodes a Nomination attribute from a STUN message using a specific attribute type. 32 | func (a *NominationAttribute) GetFromWithType(m *stun.Message, attrType stun.AttrType) error { 33 | v, err := m.Get(attrType) 34 | if err != nil { 35 | return err 36 | } 37 | if len(v) < 4 { 38 | return stun.ErrAttributeSizeInvalid 39 | } 40 | 41 | // Extract 24-bit value from the last 3 bytes 42 | a.Value = uint32(v[1])<<16 | uint32(v[2])<<8 | uint32(v[3]) 43 | 44 | return nil 45 | } 46 | 47 | // AddTo adds a Nomination attribute to a STUN message. 48 | func (a NominationAttribute) AddTo(m *stun.Message) error { 49 | return a.AddToWithType(m, DefaultNominationAttribute) 50 | } 51 | 52 | // AddToWithType adds a Nomination attribute to a STUN message using a specific attribute type. 53 | func (a NominationAttribute) AddToWithType(m *stun.Message, attrType stun.AttrType) error { 54 | // Store as 4 bytes with first byte as 0 55 | v := make([]byte, 4) 56 | v[1] = byte(a.Value >> 16) 57 | v[2] = byte(a.Value >> 8) 58 | v[3] = byte(a.Value) 59 | 60 | m.Add(attrType, v) 61 | 62 | return nil 63 | } 64 | 65 | // String returns string representation of the nomination attribute. 66 | func (a NominationAttribute) String() string { 67 | return fmt.Sprintf("NOMINATION: %d", a.Value) 68 | } 69 | 70 | // Nomination creates a new STUN nomination attribute. 71 | func Nomination(value uint32) NominationAttribute { 72 | return NominationAttribute{Value: value} 73 | } 74 | 75 | // NominationSetter is a STUN setter for nomination attribute with configurable type. 76 | type NominationSetter struct { 77 | Value uint32 78 | AttrType stun.AttrType 79 | } 80 | 81 | // AddTo adds a Nomination attribute to a STUN message using the configured attribute type. 82 | func (n NominationSetter) AddTo(m *stun.Message) error { 83 | attr := NominationAttribute{Value: n.Value} 84 | 85 | return attr.AddToWithType(m, n.AttrType) 86 | } 87 | -------------------------------------------------------------------------------- /tcp_mux_multi.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "net" 8 | ) 9 | 10 | // AllConnsGetter allows multiple fixed TCP ports to be used, 11 | // each of which is multiplexed like TCPMux. AllConnsGetter also acts as 12 | // a TCPMux, in which case it will return a single connection for one 13 | // of the ports. 14 | type AllConnsGetter interface { 15 | GetAllConns(ufrag string, isIPv6 bool, localIP net.IP) ([]net.PacketConn, error) 16 | } 17 | 18 | // MultiTCPMuxDefault implements both TCPMux and AllConnsGetter, 19 | // allowing users to pass multiple TCPMux instances to the ICE agent 20 | // configuration. 21 | type MultiTCPMuxDefault struct { 22 | muxes []TCPMux 23 | } 24 | 25 | // NewMultiTCPMuxDefault creates an instance of MultiTCPMuxDefault that 26 | // uses the provided TCPMux instances. 27 | func NewMultiTCPMuxDefault(muxes ...TCPMux) *MultiTCPMuxDefault { 28 | return &MultiTCPMuxDefault{ 29 | muxes: muxes, 30 | } 31 | } 32 | 33 | // GetConnByUfrag returns a PacketConn given the connection's ufrag, network and local address 34 | // creates the connection if an existing one can't be found. This, unlike 35 | // GetAllConns, will only return a single PacketConn from the first mux that was 36 | // passed in to NewMultiTCPMuxDefault. 37 | func (m *MultiTCPMuxDefault) GetConnByUfrag(ufrag string, isIPv6 bool, local net.IP) (net.PacketConn, error) { 38 | // NOTE: We always use the first element here in order to maintain the 39 | // behavior of using an existing connection if one exists. 40 | if len(m.muxes) == 0 { 41 | return nil, errNoTCPMuxAvailable 42 | } 43 | 44 | return m.muxes[0].GetConnByUfrag(ufrag, isIPv6, local) 45 | } 46 | 47 | // RemoveConnByUfrag stops and removes the muxed packet connection 48 | // from all underlying TCPMux instances. 49 | func (m *MultiTCPMuxDefault) RemoveConnByUfrag(ufrag string) { 50 | for _, mux := range m.muxes { 51 | mux.RemoveConnByUfrag(ufrag) 52 | } 53 | } 54 | 55 | // GetAllConns returns a PacketConn for each underlying TCPMux. 56 | func (m *MultiTCPMuxDefault) GetAllConns(ufrag string, isIPv6 bool, local net.IP) ([]net.PacketConn, error) { 57 | if len(m.muxes) == 0 { 58 | // Make sure that we either return at least one connection or an error. 59 | return nil, errNoTCPMuxAvailable 60 | } 61 | var conns []net.PacketConn 62 | for _, mux := range m.muxes { 63 | conn, err := mux.GetConnByUfrag(ufrag, isIPv6, local) 64 | if err != nil { 65 | // For now, this implementation is all or none. 66 | return nil, err 67 | } 68 | if conn != nil { 69 | conns = append(conns, conn) 70 | } 71 | } 72 | 73 | return conns, nil 74 | } 75 | 76 | // Close the multi mux, no further connections could be created. 77 | func (m *MultiTCPMuxDefault) Close() error { 78 | var err error 79 | for _, mux := range m.muxes { 80 | if e := mux.Close(); e != nil { 81 | err = e 82 | } 83 | } 84 | 85 | return err 86 | } 87 | -------------------------------------------------------------------------------- /agent_config_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestAgentConfig_initWithDefaults(t *testing.T) { 14 | relayAcceptanceMinWait := 5 * time.Second 15 | tests := []struct { 16 | name string 17 | config *AgentConfig 18 | fn func(*testing.T, *Agent) 19 | }{ 20 | { 21 | "default config", 22 | &AgentConfig{}, 23 | func(t *testing.T, result *Agent) { 24 | t.Helper() 25 | assert.Equal(t, result.relayAcceptanceMinWait, defaultRelayAcceptanceMinWait) 26 | }, 27 | }, 28 | { 29 | "multiple relay candidate types", 30 | &AgentConfig{CandidateTypes: []CandidateType{CandidateTypeHost, CandidateTypeServerReflexive}}, 31 | func(t *testing.T, result *Agent) { 32 | t.Helper() 33 | assert.Equal(t, result.relayAcceptanceMinWait, defaultRelayAcceptanceMinWait) 34 | }, 35 | }, 36 | { 37 | "host only candidate type", 38 | &AgentConfig{CandidateTypes: []CandidateType{CandidateTypeHost}}, 39 | func(t *testing.T, result *Agent) { 40 | t.Helper() 41 | assert.Equal(t, result.relayAcceptanceMinWait, defaultRelayAcceptanceMinWait) 42 | }, 43 | }, 44 | { 45 | "relay only candidate type", 46 | &AgentConfig{CandidateTypes: []CandidateType{CandidateTypeRelay}}, 47 | func(t *testing.T, result *Agent) { 48 | t.Helper() 49 | assert.Equal(t, result.relayAcceptanceMinWait, defaultRelayOnlyAcceptanceMinWait) 50 | }, 51 | }, 52 | { 53 | "relay only with relayAcceptanceMinWait set", 54 | &AgentConfig{CandidateTypes: []CandidateType{CandidateTypeRelay}, RelayAcceptanceMinWait: &relayAcceptanceMinWait}, 55 | func(t *testing.T, result *Agent) { 56 | t.Helper() 57 | assert.Equal(t, result.relayAcceptanceMinWait, relayAcceptanceMinWait) 58 | }, 59 | }, 60 | } 61 | 62 | for _, test := range tests { 63 | t.Run(test.name, func(t *testing.T) { 64 | agent, err := NewAgent(test.config) 65 | if !assert.NoError(t, err) { 66 | return 67 | } 68 | defer func() { _ = agent.Close() }() 69 | test.fn(t, agent) 70 | }) 71 | } 72 | } 73 | 74 | func TestDefaultRelayAcceptanceMinWaitForCandidates(t *testing.T) { 75 | tests := []struct { 76 | name string 77 | candidateType []CandidateType 78 | expectedWait time.Duration 79 | }{ 80 | { 81 | name: "relay only", 82 | candidateType: []CandidateType{CandidateTypeRelay}, 83 | expectedWait: defaultRelayOnlyAcceptanceMinWait, 84 | }, 85 | { 86 | name: "mixed types", 87 | candidateType: []CandidateType{CandidateTypeHost, CandidateTypeRelay}, 88 | expectedWait: defaultRelayAcceptanceMinWait, 89 | }, 90 | } 91 | 92 | for _, tc := range tests { 93 | t.Run(tc.name, func(t *testing.T) { 94 | assert.Equal(t, tc.expectedWait, defaultRelayAcceptanceMinWaitFor(tc.candidateType)) 95 | }) 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /icecontrol.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "encoding/binary" 8 | 9 | "github.com/pion/stun/v3" 10 | ) 11 | 12 | // tiebreaker is common helper for ICE-{CONTROLLED,CONTROLLING} 13 | // and represents the so-called tiebreaker number. 14 | type tiebreaker uint64 15 | 16 | const tiebreakerSize = 8 // 64 bit 17 | 18 | // AddToAs adds tiebreaker value to m as t attribute. 19 | func (a tiebreaker) AddToAs(m *stun.Message, t stun.AttrType) error { 20 | v := make([]byte, tiebreakerSize) 21 | binary.BigEndian.PutUint64(v, uint64(a)) 22 | m.Add(t, v) 23 | 24 | return nil 25 | } 26 | 27 | // GetFromAs decodes tiebreaker value in message getting it as for t type. 28 | func (a *tiebreaker) GetFromAs(m *stun.Message, t stun.AttrType) error { 29 | v, err := m.Get(t) 30 | if err != nil { 31 | return err 32 | } 33 | if err = stun.CheckSize(t, len(v), tiebreakerSize); err != nil { 34 | return err 35 | } 36 | *a = tiebreaker(binary.BigEndian.Uint64(v)) 37 | 38 | return nil 39 | } 40 | 41 | // AttrControlled represents ICE-CONTROLLED attribute. 42 | type AttrControlled uint64 43 | 44 | // AddTo adds ICE-CONTROLLED to message. 45 | func (c AttrControlled) AddTo(m *stun.Message) error { 46 | return tiebreaker(c).AddToAs(m, stun.AttrICEControlled) 47 | } 48 | 49 | // GetFrom decodes ICE-CONTROLLED from message. 50 | func (c *AttrControlled) GetFrom(m *stun.Message) error { 51 | return (*tiebreaker)(c).GetFromAs(m, stun.AttrICEControlled) 52 | } 53 | 54 | // AttrControlling represents ICE-CONTROLLING attribute. 55 | type AttrControlling uint64 56 | 57 | // AddTo adds ICE-CONTROLLING to message. 58 | func (c AttrControlling) AddTo(m *stun.Message) error { 59 | return tiebreaker(c).AddToAs(m, stun.AttrICEControlling) 60 | } 61 | 62 | // GetFrom decodes ICE-CONTROLLING from message. 63 | func (c *AttrControlling) GetFrom(m *stun.Message) error { 64 | return (*tiebreaker)(c).GetFromAs(m, stun.AttrICEControlling) 65 | } 66 | 67 | // AttrControl is helper that wraps ICE-{CONTROLLED,CONTROLLING}. 68 | type AttrControl struct { 69 | Role Role 70 | Tiebreaker uint64 71 | } 72 | 73 | // AddTo adds ICE-CONTROLLED or ICE-CONTROLLING attribute depending on Role. 74 | func (c AttrControl) AddTo(m *stun.Message) error { 75 | if c.Role == Controlling { 76 | return tiebreaker(c.Tiebreaker).AddToAs(m, stun.AttrICEControlling) 77 | } 78 | 79 | return tiebreaker(c.Tiebreaker).AddToAs(m, stun.AttrICEControlled) 80 | } 81 | 82 | // GetFrom decodes Role and Tiebreaker value from message. 83 | func (c *AttrControl) GetFrom(m *stun.Message) error { 84 | if m.Contains(stun.AttrICEControlling) { 85 | c.Role = Controlling 86 | 87 | return (*tiebreaker)(&c.Tiebreaker).GetFromAs(m, stun.AttrICEControlling) 88 | } 89 | if m.Contains(stun.AttrICEControlled) { 90 | c.Role = Controlled 91 | 92 | return (*tiebreaker)(&c.Tiebreaker).GetFromAs(m, stun.AttrICEControlled) 93 | } 94 | 95 | return stun.ErrAttributeNotFound 96 | } 97 | -------------------------------------------------------------------------------- /internal/taskloop/taskloop.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package taskloop implements a task loop to run 5 | // tasks sequentially in a separate Goroutine. 6 | package taskloop 7 | 8 | import ( 9 | "context" 10 | "errors" 11 | "time" 12 | 13 | atomicx "github.com/pion/ice/v4/internal/atomic" 14 | ) 15 | 16 | // ErrClosed indicates that the loop has been stopped. 17 | var ErrClosed = errors.New("the agent is closed") 18 | 19 | type task struct { 20 | fn func(context.Context) 21 | done chan struct{} 22 | } 23 | 24 | // Loop runs submitted task serially in a dedicated Goroutine. 25 | type Loop struct { 26 | tasks chan task 27 | 28 | // State for closing 29 | done chan struct{} 30 | taskLoopDone chan struct{} 31 | err atomicx.Error 32 | } 33 | 34 | // New creates and starts a new task loop. 35 | func New(onClose func()) *Loop { 36 | l := &Loop{ 37 | tasks: make(chan task), 38 | done: make(chan struct{}), 39 | taskLoopDone: make(chan struct{}), 40 | } 41 | 42 | go l.runLoop(onClose) 43 | 44 | return l 45 | } 46 | 47 | // runLoop handles registered tasks and agent close. 48 | func (l *Loop) runLoop(onClose func()) { 49 | defer func() { 50 | onClose() 51 | close(l.taskLoopDone) 52 | }() 53 | 54 | for { 55 | select { 56 | case <-l.done: 57 | return 58 | case t := <-l.tasks: 59 | t.fn(l) 60 | close(t.done) 61 | } 62 | } 63 | } 64 | 65 | // Close stops the loop after finishing the execution of the current task. 66 | // Other pending tasks will not be executed. 67 | func (l *Loop) Close() { 68 | if err := l.Err(); err != nil { 69 | return 70 | } 71 | 72 | l.err.Store(ErrClosed) 73 | 74 | close(l.done) 75 | <-l.taskLoopDone 76 | } 77 | 78 | // Run serially executes the submitted callback. 79 | // Blocking tasks must be cancelable by context. 80 | func (l *Loop) Run(ctx context.Context, t func(context.Context)) error { 81 | if err := l.Err(); err != nil { 82 | return err 83 | } 84 | done := make(chan struct{}) 85 | select { 86 | case <-ctx.Done(): 87 | return ctx.Err() 88 | case <-l.done: 89 | return ErrClosed 90 | case l.tasks <- task{t, done}: 91 | <-done 92 | 93 | return nil 94 | } 95 | } 96 | 97 | // The following methods implement context.Context for TaskLoop 98 | 99 | // Done returns a channel that's closed when the task loop has been stopped. 100 | func (l *Loop) Done() <-chan struct{} { 101 | return l.done 102 | } 103 | 104 | // Err returns nil if the task loop is still running. 105 | // Otherwise it return errClosed if the loop has been closed/stopped. 106 | func (l *Loop) Err() error { 107 | select { 108 | case <-l.done: 109 | return ErrClosed 110 | default: 111 | return nil 112 | } 113 | } 114 | 115 | // Deadline returns the no valid time as task loops have no deadline. 116 | func (l *Loop) Deadline() (deadline time.Time, ok bool) { 117 | return time.Time{}, false 118 | } 119 | 120 | // Value is not supported for task loops. 121 | func (l *Loop) Value(any) any { 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /candidate.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "context" 8 | "net" 9 | "time" 10 | ) 11 | 12 | const ( 13 | receiveMTU = 8192 14 | defaultLocalPreference = 65535 15 | 16 | // ComponentRTP indicates that the candidate is used for RTP. 17 | ComponentRTP uint16 = 1 18 | // ComponentRTCP indicates that the candidate is used for RTCP. 19 | ComponentRTCP 20 | ) 21 | 22 | // Candidate represents an ICE candidate. 23 | type Candidate interface { 24 | // An arbitrary string used in the freezing algorithm to 25 | // group similar candidates. It is the same for two candidates that 26 | // have the same type, base IP address, protocol (UDP, TCP, etc.), 27 | // and STUN or TURN server. 28 | Foundation() string 29 | 30 | // ID is a unique identifier for just this candidate 31 | // Unlike the foundation this is different for each candidate 32 | ID() string 33 | 34 | // A component is a piece of a data stream. 35 | // An example is one for RTP, and one for RTCP 36 | Component() uint16 37 | SetComponent(uint16) 38 | 39 | // The last time this candidate received traffic 40 | LastReceived() time.Time 41 | 42 | // The last time this candidate sent traffic 43 | LastSent() time.Time 44 | 45 | NetworkType() NetworkType 46 | Address() string 47 | Port() int 48 | 49 | Priority() uint32 50 | 51 | // A transport address related to a 52 | // candidate, which is useful for diagnostics and other purposes 53 | RelatedAddress() *CandidateRelatedAddress 54 | 55 | // Extensions returns a copy of all extension attributes associated with the ICECandidate. 56 | // In the order of insertion, *(key value). 57 | // Extension attributes are defined in RFC 5245, Section 15.1: 58 | // https://datatracker.ietf.org/doc/html/rfc5245#section-15.1 59 | //. 60 | Extensions() []CandidateExtension 61 | // GetExtension returns the value of the extension attribute associated with the ICECandidate. 62 | // Extension attributes are defined in RFC 5245, Section 15.1: 63 | // https://datatracker.ietf.org/doc/html/rfc5245#section-15.1 64 | //. 65 | GetExtension(key string) (value CandidateExtension, ok bool) 66 | // AddExtension adds an extension attribute to the ICECandidate. 67 | // If an extension with the same key already exists, it will be overwritten. 68 | // Extension attributes are defined in RFC 5245, Section 15.1: 69 | AddExtension(extension CandidateExtension) error 70 | // RemoveExtension removes an extension attribute from the ICECandidate. 71 | // Extension attributes are defined in RFC 5245, Section 15.1: 72 | RemoveExtension(key string) (ok bool) 73 | 74 | String() string 75 | Type() CandidateType 76 | TCPType() TCPType 77 | 78 | Equal(other Candidate) bool 79 | 80 | // DeepEqual same as Equal, But it also compares the candidate extensions. 81 | DeepEqual(other Candidate) bool 82 | 83 | Marshal() string 84 | 85 | addr() net.Addr 86 | filterForLocationTracking() bool 87 | agent() *Agent 88 | context() context.Context 89 | 90 | close() error 91 | copy() (Candidate, error) 92 | seen(outbound bool) 93 | start(a *Agent, conn net.PacketConn, initializedCh <-chan struct{}) 94 | writeTo(raw []byte, dst Candidate) (int, error) 95 | } 96 | -------------------------------------------------------------------------------- /ice.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | // ConnectionState is an enum showing the state of a ICE Connection. 7 | type ConnectionState int 8 | 9 | // List of supported States. 10 | const ( 11 | // ConnectionStateUnknown represents an unknown state. 12 | ConnectionStateUnknown ConnectionState = iota 13 | 14 | // ConnectionStateNew ICE agent is gathering addresses. 15 | ConnectionStateNew 16 | 17 | // ConnectionStateChecking ICE agent has been given local and remote candidates, and is attempting to find a match. 18 | ConnectionStateChecking 19 | 20 | // ConnectionStateConnected ICE agent has a pairing, but is still checking other pairs. 21 | ConnectionStateConnected 22 | 23 | // ConnectionStateCompleted ICE agent has finished. 24 | ConnectionStateCompleted 25 | 26 | // ConnectionStateFailed ICE agent never could successfully connect. 27 | ConnectionStateFailed 28 | 29 | // ConnectionStateDisconnected ICE agent connected successfully, but has entered a failed state. 30 | ConnectionStateDisconnected 31 | 32 | // ConnectionStateClosed ICE agent has finished and is no longer handling requests. 33 | ConnectionStateClosed 34 | ) 35 | 36 | func (c ConnectionState) String() string { 37 | switch c { 38 | case ConnectionStateNew: 39 | return "New" 40 | case ConnectionStateChecking: 41 | return "Checking" 42 | case ConnectionStateConnected: 43 | return "Connected" 44 | case ConnectionStateCompleted: 45 | return "Completed" 46 | case ConnectionStateFailed: 47 | return "Failed" 48 | case ConnectionStateDisconnected: 49 | return "Disconnected" 50 | case ConnectionStateClosed: 51 | return "Closed" 52 | default: 53 | return "Invalid" 54 | } 55 | } 56 | 57 | // GatheringState describes the state of the candidate gathering process. 58 | type GatheringState int 59 | 60 | const ( 61 | // GatheringStateUnknown represents an unknown state. 62 | GatheringStateUnknown GatheringState = iota 63 | 64 | // GatheringStateNew indicates candidate gathering is not yet started. 65 | GatheringStateNew 66 | 67 | // GatheringStateGathering indicates candidate gathering is ongoing. 68 | GatheringStateGathering 69 | 70 | // GatheringStateComplete indicates candidate gathering has been completed. 71 | GatheringStateComplete 72 | ) 73 | 74 | func (t GatheringState) String() string { 75 | switch t { 76 | case GatheringStateNew: 77 | return "new" 78 | case GatheringStateGathering: 79 | return "gathering" 80 | case GatheringStateComplete: 81 | return "complete" 82 | default: 83 | return ErrUnknownType.Error() 84 | } 85 | } 86 | 87 | // ContinualGatheringPolicy defines the behavior for gathering ICE candidates. 88 | type ContinualGatheringPolicy int 89 | 90 | const ( 91 | GatherOnce ContinualGatheringPolicy = iota 92 | GatherContinually 93 | ) 94 | 95 | func (c ContinualGatheringPolicy) String() string { 96 | switch c { 97 | case GatherOnce: 98 | return "gather_once" 99 | case GatherContinually: 100 | return "gather_continually" 101 | default: 102 | return unknownStr 103 | } 104 | } 105 | 106 | const ( 107 | unknownStr = "unknown" 108 | relayProtocolDTLS = "dtls" 109 | relayProtocolTLS = "tls" 110 | ) 111 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 2 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 4 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 6 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 7 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 8 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 9 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 10 | github.com/pion/dtls/v3 v3.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM= 11 | github.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os= 12 | github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8= 13 | github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so= 14 | github.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY= 15 | github.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A= 16 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 17 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 18 | github.com/pion/stun/v3 v3.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU= 19 | github.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA= 20 | github.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM= 21 | github.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ= 22 | github.com/pion/turn/v4 v4.1.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA= 23 | github.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A= 24 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 25 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 26 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 27 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 28 | github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU= 29 | github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA= 30 | golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= 31 | golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= 32 | golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= 33 | golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= 34 | golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= 35 | golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 36 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 37 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= 38 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 39 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 40 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 41 | -------------------------------------------------------------------------------- /candidate_relay.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "net" 8 | "net/netip" 9 | ) 10 | 11 | // CandidateRelay ... 12 | type CandidateRelay struct { 13 | candidateBase 14 | 15 | relayProtocol string 16 | onClose func() error 17 | } 18 | 19 | // CandidateRelayConfig is the config required to create a new CandidateRelay. 20 | type CandidateRelayConfig struct { 21 | CandidateID string 22 | Network string 23 | Address string 24 | Port int 25 | Component uint16 26 | Priority uint32 27 | Foundation string 28 | RelAddr string 29 | RelPort int 30 | RelayProtocol string 31 | OnClose func() error 32 | } 33 | 34 | // NewCandidateRelay creates a new relay candidate. 35 | func NewCandidateRelay(config *CandidateRelayConfig) (*CandidateRelay, error) { 36 | candidateID := config.CandidateID 37 | 38 | if candidateID == "" { 39 | candidateID = globalCandidateIDGenerator.Generate() 40 | } 41 | 42 | ipAddr, err := netip.ParseAddr(config.Address) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | networkType, err := determineNetworkType(config.Network, ipAddr) 48 | if err != nil { 49 | return nil, err 50 | } 51 | 52 | return &CandidateRelay{ 53 | candidateBase: candidateBase{ 54 | id: candidateID, 55 | networkType: networkType, 56 | candidateType: CandidateTypeRelay, 57 | address: config.Address, 58 | port: config.Port, 59 | resolvedAddr: &net.UDPAddr{ 60 | IP: ipAddr.AsSlice(), 61 | Port: config.Port, 62 | Zone: ipAddr.Zone(), 63 | }, 64 | component: config.Component, 65 | foundationOverride: config.Foundation, 66 | priorityOverride: config.Priority, 67 | relatedAddress: &CandidateRelatedAddress{ 68 | Address: config.RelAddr, 69 | Port: config.RelPort, 70 | }, 71 | remoteCandidateCaches: map[AddrPort]Candidate{}, 72 | }, 73 | relayProtocol: config.RelayProtocol, 74 | onClose: config.OnClose, 75 | }, nil 76 | } 77 | 78 | // LocalPreference returns the local preference for this candidate. 79 | func (c *CandidateRelay) LocalPreference() uint16 { 80 | // These preference values come from libwebrtc 81 | // https://github.com/mozilla/libwebrtc/blob/1389c76d9c79839a2ca069df1db48aa3f2e6a1ac/p2p/base/turn_port.cc#L61 82 | var relayPreference uint16 83 | switch c.relayProtocol { 84 | case relayProtocolTLS, relayProtocolDTLS: 85 | relayPreference = 2 86 | case tcp: 87 | relayPreference = 1 88 | default: 89 | relayPreference = 0 90 | } 91 | 92 | return c.candidateBase.LocalPreference() + relayPreference 93 | } 94 | 95 | // RelayProtocol returns the protocol used between the endpoint and the relay server. 96 | func (c *CandidateRelay) RelayProtocol() string { 97 | return c.relayProtocol 98 | } 99 | 100 | func (c *CandidateRelay) close() error { 101 | err := c.candidateBase.close() 102 | if c.onClose != nil { 103 | err = c.onClose() 104 | c.onClose = nil 105 | } 106 | 107 | return err 108 | } 109 | 110 | func (c *CandidateRelay) copy() (Candidate, error) { 111 | cc, err := c.candidateBase.copy() 112 | if err != nil { 113 | return nil, err 114 | } 115 | 116 | if ccr, ok := cc.(*CandidateRelay); ok { 117 | ccr.relayProtocol = c.relayProtocol 118 | } 119 | 120 | return cc, nil 121 | } 122 | -------------------------------------------------------------------------------- /icecontrol_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pion/stun/v3" 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestControlled_GetFrom(t *testing.T) { //nolint:dupl 14 | m := new(stun.Message) 15 | var attrCtr AttrControlled 16 | require.ErrorIs(t, stun.ErrAttributeNotFound, attrCtr.GetFrom(m)) 17 | require.NoError(t, m.Build(stun.BindingRequest, &attrCtr)) 18 | 19 | m1 := new(stun.Message) 20 | _, err := m1.Write(m.Raw) 21 | require.NoError(t, err) 22 | 23 | var c1 AttrControlled 24 | require.NoError(t, c1.GetFrom(m1)) 25 | require.Equal(t, c1, attrCtr) 26 | 27 | t.Run("IncorrectSize", func(t *testing.T) { 28 | m3 := new(stun.Message) 29 | m3.Add(stun.AttrICEControlled, make([]byte, 100)) 30 | var c2 AttrControlled 31 | require.True(t, stun.IsAttrSizeInvalid(c2.GetFrom(m3))) 32 | }) 33 | } 34 | 35 | func TestControlling_GetFrom(t *testing.T) { //nolint:dupl 36 | m := new(stun.Message) 37 | var attrCtr AttrControlling 38 | require.ErrorIs(t, stun.ErrAttributeNotFound, attrCtr.GetFrom(m)) 39 | require.NoError(t, m.Build(stun.BindingRequest, &attrCtr)) 40 | 41 | m1 := new(stun.Message) 42 | _, err := m1.Write(m.Raw) 43 | require.NoError(t, err) 44 | 45 | var c1 AttrControlling 46 | require.NoError(t, c1.GetFrom(m1)) 47 | require.Equal(t, c1, attrCtr) 48 | t.Run("IncorrectSize", func(t *testing.T) { 49 | m3 := new(stun.Message) 50 | m3.Add(stun.AttrICEControlling, make([]byte, 100)) 51 | var c2 AttrControlling 52 | require.True(t, stun.IsAttrSizeInvalid(c2.GetFrom(m3))) 53 | }) 54 | } 55 | 56 | func TestControl_GetFrom(t *testing.T) { //nolint:cyclop 57 | t.Run("Blank", func(t *testing.T) { 58 | m := new(stun.Message) 59 | var c AttrControl 60 | require.ErrorIs(t, stun.ErrAttributeNotFound, c.GetFrom(m)) 61 | }) 62 | t.Run("Controlling", func(t *testing.T) { //nolint:dupl 63 | m := new(stun.Message) 64 | var attCtr AttrControl 65 | require.ErrorIs(t, stun.ErrAttributeNotFound, attCtr.GetFrom(m)) 66 | attCtr.Role = Controlling 67 | attCtr.Tiebreaker = 4321 68 | require.NoError(t, m.Build(stun.BindingRequest, &attCtr)) 69 | m1 := new(stun.Message) 70 | _, err := m1.Write(m.Raw) 71 | require.NoError(t, err) 72 | var c1 AttrControl 73 | require.NoError(t, c1.GetFrom(m1)) 74 | require.Equal(t, c1, attCtr) 75 | t.Run("IncorrectSize", func(t *testing.T) { 76 | m3 := new(stun.Message) 77 | m3.Add(stun.AttrICEControlling, make([]byte, 100)) 78 | var c2 AttrControl 79 | err := c2.GetFrom(m3) 80 | require.True(t, stun.IsAttrSizeInvalid(err)) 81 | }) 82 | }) 83 | t.Run("Controlled", func(t *testing.T) { //nolint:dupl 84 | m := new(stun.Message) 85 | var attrCtrl AttrControl 86 | require.ErrorIs(t, stun.ErrAttributeNotFound, attrCtrl.GetFrom(m)) 87 | attrCtrl.Role = Controlled 88 | attrCtrl.Tiebreaker = 1234 89 | require.NoError(t, m.Build(stun.BindingRequest, &attrCtrl)) 90 | m1 := new(stun.Message) 91 | _, err := m1.Write(m.Raw) 92 | require.NoError(t, err) 93 | 94 | var c1 AttrControl 95 | require.NoError(t, c1.GetFrom(m1)) 96 | require.Equal(t, c1, attrCtrl) 97 | t.Run("IncorrectSize", func(t *testing.T) { 98 | m3 := new(stun.Message) 99 | m3.Add(stun.AttrICEControlling, make([]byte, 100)) 100 | var c2 AttrControl 101 | err := c2.GetFrom(m3) 102 | require.True(t, stun.IsAttrSizeInvalid(err)) 103 | }) 104 | }) 105 | } 106 | -------------------------------------------------------------------------------- /networktype.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "fmt" 8 | "net/netip" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | udp = "udp" 14 | tcp = "tcp" 15 | udp4 = "udp4" 16 | udp6 = "udp6" 17 | tcp4 = "tcp4" 18 | tcp6 = "tcp6" 19 | ) 20 | 21 | func supportedNetworkTypes() []NetworkType { 22 | return []NetworkType{ 23 | NetworkTypeUDP4, 24 | NetworkTypeUDP6, 25 | NetworkTypeTCP4, 26 | NetworkTypeTCP6, 27 | } 28 | } 29 | 30 | // NetworkType represents the type of network. 31 | type NetworkType int 32 | 33 | const ( 34 | // NetworkTypeUDP4 indicates UDP over IPv4. 35 | NetworkTypeUDP4 NetworkType = iota + 1 36 | 37 | // NetworkTypeUDP6 indicates UDP over IPv6. 38 | NetworkTypeUDP6 39 | 40 | // NetworkTypeTCP4 indicates TCP over IPv4. 41 | NetworkTypeTCP4 42 | 43 | // NetworkTypeTCP6 indicates TCP over IPv6. 44 | NetworkTypeTCP6 45 | ) 46 | 47 | func (t NetworkType) String() string { 48 | switch t { 49 | case NetworkTypeUDP4: 50 | return udp4 51 | case NetworkTypeUDP6: 52 | return udp6 53 | case NetworkTypeTCP4: 54 | return tcp4 55 | case NetworkTypeTCP6: 56 | return tcp6 57 | default: 58 | return ErrUnknownType.Error() 59 | } 60 | } 61 | 62 | // IsUDP returns true when network is UDP4 or UDP6. 63 | func (t NetworkType) IsUDP() bool { 64 | return t == NetworkTypeUDP4 || t == NetworkTypeUDP6 65 | } 66 | 67 | // IsTCP returns true when network is TCP4 or TCP6. 68 | func (t NetworkType) IsTCP() bool { 69 | return t == NetworkTypeTCP4 || t == NetworkTypeTCP6 70 | } 71 | 72 | // NetworkShort returns the short network description. 73 | func (t NetworkType) NetworkShort() string { 74 | switch t { 75 | case NetworkTypeUDP4, NetworkTypeUDP6: 76 | return udp 77 | case NetworkTypeTCP4, NetworkTypeTCP6: 78 | return tcp 79 | default: 80 | return ErrUnknownType.Error() 81 | } 82 | } 83 | 84 | // IsReliable returns true if the network is reliable. 85 | func (t NetworkType) IsReliable() bool { 86 | switch t { 87 | case NetworkTypeUDP4, NetworkTypeUDP6: 88 | return false 89 | case NetworkTypeTCP4, NetworkTypeTCP6: 90 | return true 91 | } 92 | 93 | return false 94 | } 95 | 96 | // IsIPv4 returns whether the network type is IPv4 or not. 97 | func (t NetworkType) IsIPv4() bool { 98 | switch t { 99 | case NetworkTypeUDP4, NetworkTypeTCP4: 100 | return true 101 | case NetworkTypeUDP6, NetworkTypeTCP6: 102 | return false 103 | } 104 | 105 | return false 106 | } 107 | 108 | // IsIPv6 returns whether the network type is IPv6 or not. 109 | func (t NetworkType) IsIPv6() bool { 110 | switch t { 111 | case NetworkTypeUDP4, NetworkTypeTCP4: 112 | return false 113 | case NetworkTypeUDP6, NetworkTypeTCP6: 114 | return true 115 | } 116 | 117 | return false 118 | } 119 | 120 | // determineNetworkType determines the type of network based on 121 | // the short network string and an IP address. 122 | func determineNetworkType(network string, ip netip.Addr) (NetworkType, error) { 123 | // we'd rather have an IPv4-mapped IPv6 become IPv4 so that it is usable. 124 | ip = ip.Unmap() 125 | switch { 126 | case strings.HasPrefix(strings.ToLower(network), udp): 127 | if ip.Is4() { 128 | return NetworkTypeUDP4, nil 129 | } 130 | 131 | return NetworkTypeUDP6, nil 132 | 133 | case strings.HasPrefix(strings.ToLower(network), tcp): 134 | if ip.Is4() { 135 | return NetworkTypeTCP4, nil 136 | } 137 | 138 | return NetworkTypeTCP6, nil 139 | } 140 | 141 | return NetworkType(0), fmt.Errorf("%w from %s %s", ErrDetermineNetworkType, network, ip) 142 | } 143 | -------------------------------------------------------------------------------- /networktype_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "net" 8 | "testing" 9 | 10 | "github.com/stretchr/testify/require" 11 | ) 12 | 13 | func TestNetworkTypeParsing_Success(t *testing.T) { 14 | ipv4 := net.ParseIP("192.168.0.1") 15 | ipv6 := net.ParseIP("fe80::a3:6ff:fec4:5454") 16 | 17 | for _, test := range []struct { 18 | name string 19 | inNetwork string 20 | inIP net.IP 21 | expected NetworkType 22 | }{ 23 | { 24 | "lowercase UDP4", 25 | "udp", 26 | ipv4, 27 | NetworkTypeUDP4, 28 | }, 29 | { 30 | "uppercase UDP4", 31 | "UDP", 32 | ipv4, 33 | NetworkTypeUDP4, 34 | }, 35 | { 36 | "lowercase UDP6", 37 | "udp", 38 | ipv6, 39 | NetworkTypeUDP6, 40 | }, 41 | { 42 | "uppercase UDP6", 43 | "UDP", 44 | ipv6, 45 | NetworkTypeUDP6, 46 | }, 47 | } { 48 | actual, err := determineNetworkType(test.inNetwork, mustAddr(t, test.inIP)) 49 | require.NoError(t, err) 50 | require.Equal(t, test.expected, actual) 51 | } 52 | } 53 | 54 | func TestNetworkTypeParsing_Failure(t *testing.T) { 55 | ipv6 := net.ParseIP("fe80::a3:6ff:fec4:5454") 56 | 57 | for _, test := range []struct { 58 | name string 59 | inNetwork string 60 | inIP net.IP 61 | }{ 62 | { 63 | "invalid network", 64 | "junkNetwork", 65 | ipv6, 66 | }, 67 | } { 68 | _, err := determineNetworkType(test.inNetwork, mustAddr(t, test.inIP)) 69 | require.Error(t, err) 70 | } 71 | } 72 | 73 | func TestNetworkTypeIsUDP(t *testing.T) { 74 | require.True(t, NetworkTypeUDP4.IsUDP()) 75 | require.True(t, NetworkTypeUDP6.IsUDP()) 76 | require.False(t, NetworkTypeUDP4.IsTCP()) 77 | require.False(t, NetworkTypeUDP6.IsTCP()) 78 | } 79 | 80 | func TestNetworkTypeIsTCP(t *testing.T) { 81 | require.True(t, NetworkTypeTCP4.IsTCP()) 82 | require.True(t, NetworkTypeTCP6.IsTCP()) 83 | require.False(t, NetworkTypeTCP4.IsUDP()) 84 | require.False(t, NetworkTypeTCP6.IsUDP()) 85 | } 86 | 87 | func TestNetworkType_String_Default(t *testing.T) { 88 | var invalid NetworkType // 0 triggers default branch 89 | require.Equal(t, ErrUnknownType.Error(), invalid.String()) 90 | 91 | require.Equal(t, "udp4", NetworkTypeUDP4.String()) 92 | require.Equal(t, "udp6", NetworkTypeUDP6.String()) 93 | require.Equal(t, "tcp4", NetworkTypeTCP4.String()) 94 | require.Equal(t, "tcp6", NetworkTypeTCP6.String()) 95 | } 96 | 97 | func TestNetworkType_NetworkShort_Default(t *testing.T) { 98 | var invalid NetworkType 99 | require.Equal(t, ErrUnknownType.Error(), invalid.NetworkShort()) 100 | 101 | require.Equal(t, udp, NetworkTypeUDP4.NetworkShort()) 102 | require.Equal(t, udp, NetworkTypeUDP6.NetworkShort()) 103 | require.Equal(t, tcp, NetworkTypeTCP4.NetworkShort()) 104 | require.Equal(t, tcp, NetworkTypeTCP6.NetworkShort()) 105 | } 106 | 107 | func TestNetworkType_IPvFlags_Default(t *testing.T) { 108 | var invalid NetworkType 109 | require.False(t, invalid.IsIPv4()) 110 | require.False(t, invalid.IsIPv6()) 111 | 112 | require.True(t, NetworkTypeUDP4.IsIPv4()) 113 | require.True(t, NetworkTypeTCP4.IsIPv4()) 114 | require.False(t, NetworkTypeUDP6.IsIPv4()) 115 | require.False(t, NetworkTypeTCP6.IsIPv4()) 116 | 117 | require.True(t, NetworkTypeUDP6.IsIPv6()) 118 | require.True(t, NetworkTypeTCP6.IsIPv6()) 119 | require.False(t, NetworkTypeUDP4.IsIPv6()) 120 | require.False(t, NetworkTypeTCP4.IsIPv6()) 121 | } 122 | 123 | func TestNetworkType_IsReliable(t *testing.T) { 124 | // UDP is unreliable 125 | require.False(t, NetworkTypeUDP4.IsReliable()) 126 | require.False(t, NetworkTypeUDP6.IsReliable()) 127 | 128 | // TCP is reliable 129 | require.True(t, NetworkTypeTCP4.IsReliable()) 130 | require.True(t, NetworkTypeTCP6.IsReliable()) 131 | 132 | // default/unknown falls through to false 133 | var invalid NetworkType 134 | require.False(t, invalid.IsReliable()) 135 | } 136 | -------------------------------------------------------------------------------- /agent_udpmux_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "net" 11 | "testing" 12 | "time" 13 | 14 | "github.com/pion/logging" 15 | "github.com/pion/transport/v3/test" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | // newMuxForAddr creates a UDPMuxDefault with the correct socket family for the given address. 20 | // This fixes Windows dual-stack issues where IPv6 sockets don't receive IPv4 traffic by default. 21 | func newMuxForAddr(t *testing.T, addr *net.UDPAddr, loggerFactory logging.LoggerFactory) *UDPMuxDefault { 22 | t.Helper() 23 | var ( 24 | network string 25 | laddr *net.UDPAddr 26 | ) 27 | 28 | switch { 29 | case addr.IP == nil || addr.IP.IsUnspecified(): 30 | network = "udp4" 31 | laddr = &net.UDPAddr{IP: net.IPv4zero, Port: addr.Port} 32 | case addr.IP.To4() != nil: 33 | network = "udp4" 34 | laddr = &net.UDPAddr{IP: net.IPv4zero, Port: addr.Port} 35 | default: 36 | network = "udp6" 37 | laddr = &net.UDPAddr{IP: net.IPv6unspecified, Port: addr.Port} 38 | } 39 | 40 | pc, err := net.ListenUDP(network, laddr) 41 | require.NoError(t, err) 42 | t.Cleanup(func() { _ = pc.Close() }) 43 | 44 | return NewUDPMuxDefault(UDPMuxParams{ 45 | Logger: loggerFactory.NewLogger("ice"), 46 | UDPConn: pc, 47 | }) 48 | } 49 | 50 | // TestMuxAgent is an end to end test over UDP mux, ensuring two agents could connect over mux. 51 | func TestMuxAgent(t *testing.T) { 52 | defer test.CheckRoutines(t)() 53 | 54 | defer test.TimeOut(time.Second * 30).Stop() 55 | 56 | const muxPort = 7686 57 | 58 | caseAddrs := map[string]*net.UDPAddr{ 59 | "unspecified": {Port: muxPort}, 60 | "ipv4Loopback": {IP: net.IPv4(127, 0, 0, 1), Port: muxPort}, 61 | } 62 | 63 | for subTest, addr := range caseAddrs { 64 | muxAddr := addr 65 | t.Run(subTest, func(t *testing.T) { 66 | loggerFactory := logging.NewDefaultLoggerFactory() 67 | udpMux := newMuxForAddr(t, muxAddr, loggerFactory) 68 | 69 | muxedA, err := NewAgent(&AgentConfig{ 70 | UDPMux: udpMux, 71 | CandidateTypes: []CandidateType{CandidateTypeHost}, 72 | NetworkTypes: []NetworkType{ 73 | NetworkTypeUDP4, 74 | }, 75 | IncludeLoopback: addr.IP.IsLoopback(), 76 | }) 77 | require.NoError(t, err) 78 | var muxedAClosed bool 79 | defer func() { 80 | if muxedAClosed { 81 | return 82 | } 83 | require.NoError(t, muxedA.Close()) 84 | }() 85 | 86 | agent, err := NewAgent(&AgentConfig{ 87 | CandidateTypes: []CandidateType{CandidateTypeHost}, 88 | NetworkTypes: supportedNetworkTypes(), 89 | }) 90 | require.NoError(t, err) 91 | var aClosed bool 92 | defer func() { 93 | if aClosed { 94 | return 95 | } 96 | require.NoError(t, agent.Close()) 97 | }() 98 | 99 | conn, muxedConn := connect(t, agent, muxedA) 100 | 101 | pair := muxedA.getSelectedPair() 102 | require.NotNil(t, pair) 103 | require.Equal(t, muxPort, pair.Local.Port()) 104 | 105 | // Send a packet to Mux 106 | data := []byte("hello world") 107 | _, err = conn.Write(data) 108 | require.NoError(t, err) 109 | 110 | buf := make([]byte, 1024) 111 | n, err := muxedConn.Read(buf) 112 | require.NoError(t, err) 113 | require.Equal(t, data, buf[:n]) 114 | 115 | // Send a packet from Mux 116 | _, err = muxedConn.Write(data) 117 | require.NoError(t, err) 118 | 119 | n, err = conn.Read(buf) 120 | require.NoError(t, err) 121 | require.Equal(t, data, buf[:n]) 122 | 123 | // Close it down 124 | require.NoError(t, conn.Close()) 125 | aClosed = true 126 | require.NoError(t, muxedConn.Close()) 127 | muxedAClosed = true 128 | require.NoError(t, udpMux.Close()) 129 | 130 | // Expect error when reading from closed mux 131 | _, err = muxedConn.Read(data) 132 | require.Error(t, err) 133 | 134 | // Expect error when writing to closed mux 135 | _, err = muxedConn.Write(data) 136 | require.Error(t, err) 137 | }) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /addr.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | "net/netip" 10 | ) 11 | 12 | func addrWithOptionalZone(addr netip.Addr, zone string) netip.Addr { 13 | if zone == "" { 14 | return addr 15 | } 16 | if addr.Is6() && (addr.IsLinkLocalUnicast() || addr.IsLinkLocalMulticast()) { 17 | return addr.WithZone(zone) 18 | } 19 | 20 | return addr 21 | } 22 | 23 | // parseAddrFromIface should only be used when it's known the address belongs to that interface. 24 | // e.g. it's LocalAddress on a listener. 25 | func parseAddrFromIface(in net.Addr, ifcName string) (netip.Addr, int, NetworkType, error) { 26 | addr, port, nt, err := parseAddr(in) 27 | if err != nil { 28 | return netip.Addr{}, 0, 0, err 29 | } 30 | if _, ok := in.(*net.IPNet); ok { 31 | // net.IPNet does not have a Zone but we provide it from the interface 32 | addr = addrWithOptionalZone(addr, ifcName) 33 | } 34 | 35 | return addr, port, nt, nil 36 | } 37 | 38 | func parseAddr(in net.Addr) (netip.Addr, int, NetworkType, error) { 39 | host := func(ip net.IP, zone string) (netip.Addr, int, NetworkType, error) { 40 | a, err := ipAddrToNetIP(ip, zone) 41 | if err != nil { 42 | return netip.Addr{}, 0, 0, err 43 | } 44 | 45 | return a, 0, 0, nil 46 | } 47 | 48 | sock := func(ip net.IP, zone string, port int, v4, v6 NetworkType) (netip.Addr, int, NetworkType, error) { 49 | a, err := ipAddrToNetIP(ip, zone) 50 | if err != nil { 51 | return netip.Addr{}, 0, 0, err 52 | } 53 | 54 | nt := v6 55 | if a.Is4() { 56 | nt = v4 57 | } 58 | 59 | return a, port, nt, nil 60 | } 61 | 62 | switch a := in.(type) { 63 | case *net.IPNet: 64 | return host(a.IP, "") 65 | case *net.IPAddr: 66 | return host(a.IP, a.Zone) 67 | case *net.UDPAddr: 68 | return sock(a.IP, a.Zone, a.Port, NetworkTypeUDP4, NetworkTypeUDP6) 69 | case *net.TCPAddr: 70 | return sock(a.IP, a.Zone, a.Port, NetworkTypeTCP4, NetworkTypeTCP6) 71 | default: 72 | return netip.Addr{}, 0, 0, addrParseError{in} 73 | } 74 | } 75 | 76 | type addrParseError struct { 77 | addr net.Addr 78 | } 79 | 80 | func (e addrParseError) Error() string { 81 | return fmt.Sprintf("do not know how to parse address type %T", e.addr) 82 | } 83 | 84 | type ipConvertError struct { 85 | ip []byte 86 | } 87 | 88 | func (e ipConvertError) Error() string { 89 | return fmt.Sprintf("failed to convert IP '%s' to netip.Addr", e.ip) 90 | } 91 | 92 | func ipAddrToNetIP(ip []byte, zone string) (netip.Addr, error) { 93 | netIPAddr, ok := netip.AddrFromSlice(ip) 94 | if !ok { 95 | return netip.Addr{}, ipConvertError{ip} 96 | } 97 | // we'd rather have an IPv4-mapped IPv6 become IPv4 so that it is usable. 98 | netIPAddr = netIPAddr.Unmap() 99 | netIPAddr = addrWithOptionalZone(netIPAddr, zone) 100 | 101 | return netIPAddr, nil 102 | } 103 | 104 | func createAddr(network NetworkType, ip netip.Addr, port int) net.Addr { 105 | switch { 106 | case network.IsTCP(): 107 | return &net.TCPAddr{IP: ip.AsSlice(), Port: port, Zone: ip.Zone()} 108 | default: 109 | return &net.UDPAddr{IP: ip.AsSlice(), Port: port, Zone: ip.Zone()} 110 | } 111 | } 112 | 113 | func addrEqual(a, b net.Addr) bool { 114 | aIP, aPort, aType, aErr := parseAddr(a) 115 | if aErr != nil { 116 | return false 117 | } 118 | 119 | bIP, bPort, bType, bErr := parseAddr(b) 120 | if bErr != nil { 121 | return false 122 | } 123 | 124 | return aType == bType && aIP.Compare(bIP) == 0 && aPort == bPort 125 | } 126 | 127 | // AddrPort is an IP and a port number. 128 | type AddrPort [18]byte 129 | 130 | func toAddrPort(addr net.Addr) AddrPort { 131 | var ap AddrPort 132 | switch addr := addr.(type) { 133 | case *net.UDPAddr: 134 | copy(ap[:16], addr.IP.To16()) 135 | ap[16] = uint8(addr.Port >> 8) //nolint:gosec // G115 false positive 136 | ap[17] = uint8(addr.Port) //nolint:gosec // G115 false positive 137 | case *net.TCPAddr: 138 | copy(ap[:16], addr.IP.To16()) 139 | ap[16] = uint8(addr.Port >> 8) //nolint:gosec // G115 false positive 140 | ap[17] = uint8(addr.Port) //nolint:gosec // G115 false positive 141 | } 142 | 143 | return ap 144 | } 145 | -------------------------------------------------------------------------------- /addr_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "net" 11 | "net/netip" 12 | "testing" 13 | 14 | "github.com/stretchr/testify/require" 15 | ) 16 | 17 | // A net.Addr type that parseAddr doesn't handle. 18 | type unknownAddr struct{} 19 | 20 | func (unknownAddr) Network() string { return "unknown" } 21 | func (unknownAddr) String() string { return "unknown-addr" } 22 | 23 | func TestParseAddrFromIface_ErrFromParseAddr(t *testing.T) { 24 | in := unknownAddr{} 25 | ip, port, nt, err := parseAddrFromIface(in, "eth0") 26 | 27 | require.Error(t, err, "expected error from parseAddr for unknown net.Addr type") 28 | require.Zero(t, port) 29 | require.Zero(t, nt) 30 | require.True(t, !ip.IsValid(), "ip should be zero value when error is returned") 31 | } 32 | 33 | func TestParseAddr_ErrorBranches(t *testing.T) { 34 | t.Run("IPNet invalid IP -> error", func(t *testing.T) { 35 | // length 1 slice -> ipAddrToNetIP fails 36 | _, _, _, err := parseAddr(&net.IPNet{IP: net.IP{1}}) 37 | var convErr ipConvertError 38 | require.ErrorAs(t, err, &convErr) 39 | }) 40 | 41 | t.Run("IPAddr invalid IP -> error", func(t *testing.T) { 42 | _, _, _, err := parseAddr(&net.IPAddr{IP: net.IP{1}, Zone: "eth0"}) 43 | var convErr ipConvertError 44 | require.ErrorAs(t, err, &convErr) 45 | }) 46 | 47 | t.Run("UDPAddr invalid IP -> error", func(t *testing.T) { 48 | _, _, _, err := parseAddr(&net.UDPAddr{IP: net.IP{1}, Port: 3478}) 49 | var convErr ipConvertError 50 | require.ErrorAs(t, err, &convErr) 51 | }) 52 | 53 | t.Run("TCPAddr invalid IP -> error", func(t *testing.T) { 54 | _, _, _, err := parseAddr(&net.TCPAddr{IP: net.IP{1}, Port: 3478}) 55 | var convErr ipConvertError 56 | require.ErrorAs(t, err, &convErr) 57 | }) 58 | 59 | t.Run("Unknown net.Addr type -> addrParseError", func(t *testing.T) { 60 | _, _, _, err := parseAddr(unknownAddr{}) 61 | var ap addrParseError 62 | require.ErrorAs(t, err, &ap) 63 | }) 64 | } 65 | 66 | func TestParseAddr_IPAddr_Success(t *testing.T) { 67 | ip := net.ParseIP("fe80::1") 68 | require.NotNil(t, ip) 69 | 70 | gotIP, port, nt, err := parseAddr(&net.IPAddr{IP: ip, Zone: "lo0"}) 71 | require.NoError(t, err) 72 | require.Equal(t, 0, port) 73 | require.Equal(t, NetworkType(0), nt) 74 | require.True(t, gotIP.Is6()) 75 | require.Equal(t, "lo0", gotIP.Zone()) 76 | require.Equal(t, 0, gotIP.Compare(netip.MustParseAddr("fe80::1%lo0").Unmap())) 77 | } 78 | 79 | func TestAddrParseError_Error(t *testing.T) { 80 | e := addrParseError{addr: &net.TCPAddr{}} 81 | require.Equal(t, 82 | "do not know how to parse address type *net.TCPAddr", 83 | e.Error(), 84 | ) 85 | } 86 | 87 | func TestIPConvertError_Error(t *testing.T) { 88 | e := ipConvertError{ip: []byte("bad-ip")} 89 | require.Equal(t, 90 | "failed to convert IP 'bad-ip' to netip.Addr", 91 | e.Error(), 92 | ) 93 | } 94 | 95 | func TestIPAddrToNetIP_Error_InvalidBytes(t *testing.T) { 96 | bad := []byte{1} // invalid length -> AddrFromSlice returns ok=false 97 | got, err := ipAddrToNetIP(bad, "") 98 | require.Equal(t, netip.Addr{}, got, "should return zero addr on error") 99 | require.Error(t, err) 100 | require.IsType(t, ipConvertError{}, err) 101 | require.Contains(t, err.Error(), "failed to convert IP") 102 | } 103 | 104 | func TestIPAddrToNetIP_OK_IPv4(t *testing.T) { 105 | ipv4 := []byte{1, 2, 3, 4} 106 | got, err := ipAddrToNetIP(ipv4, "") 107 | require.NoError(t, err) 108 | require.True(t, got.Is4()) 109 | 110 | want := netip.AddrFrom4([4]byte{1, 2, 3, 4}) 111 | require.Equal(t, want, got) 112 | } 113 | 114 | func TestAddrEqual_FirstParseError(t *testing.T) { 115 | a := unknownAddr{} 116 | b := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 9999} 117 | 118 | require.False(t, addrEqual(a, b)) 119 | } 120 | 121 | func TestAddrEqual_SecondParseError(t *testing.T) { 122 | a := &net.UDPAddr{IP: net.IPv4(1, 2, 3, 4), Port: 9999} 123 | b := unknownAddr{} 124 | 125 | require.False(t, addrEqual(a, b)) 126 | } 127 | 128 | func TestAddrEqual_SameTypeIPPort(t *testing.T) { 129 | a := &net.UDPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 4242} 130 | b := &net.UDPAddr{IP: net.IPv4(10, 0, 0, 1), Port: 4242} 131 | 132 | require.True(t, addrEqual(a, b)) 133 | } 134 | -------------------------------------------------------------------------------- /mdns.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "net" 8 | 9 | "github.com/google/uuid" 10 | "github.com/pion/logging" 11 | "github.com/pion/mdns/v2" 12 | "github.com/pion/transport/v3" 13 | "golang.org/x/net/ipv4" 14 | "golang.org/x/net/ipv6" 15 | ) 16 | 17 | // MulticastDNSMode represents the different Multicast modes ICE can run in. 18 | type MulticastDNSMode byte 19 | 20 | // MulticastDNSMode enum. 21 | const ( 22 | // MulticastDNSModeDisabled means remote mDNS candidates will be discarded, and local host candidates will use IPs. 23 | MulticastDNSModeDisabled MulticastDNSMode = iota + 1 24 | 25 | // MulticastDNSModeQueryOnly means remote mDNS candidates will be accepted, and local host candidates will use IPs. 26 | MulticastDNSModeQueryOnly 27 | 28 | // MulticastDNSModeQueryAndGather means remote mDNS candidates will be accepted, 29 | // and local host candidates will use mDNS. 30 | MulticastDNSModeQueryAndGather 31 | ) 32 | 33 | func generateMulticastDNSName() (string, error) { 34 | // https://tools.ietf.org/id/draft-ietf-rtcweb-mdns-ice-candidates-02.html#gathering 35 | // The unique name MUST consist of a version 4 UUID as defined in [RFC4122], followed by “.local”. 36 | u, err := uuid.NewRandom() 37 | 38 | return u.String() + ".local", err 39 | } 40 | 41 | //nolint:cyclop 42 | func createMulticastDNS( 43 | netTransport transport.Net, 44 | networkTypes []NetworkType, 45 | interfaces []*transport.Interface, 46 | includeLoopback bool, 47 | mDNSMode MulticastDNSMode, 48 | mDNSName string, 49 | log logging.LeveledLogger, 50 | loggerFactory logging.LoggerFactory, 51 | ) (*mdns.Conn, MulticastDNSMode, error) { 52 | if mDNSMode == MulticastDNSModeDisabled { 53 | return nil, mDNSMode, nil 54 | } 55 | 56 | var useV4, useV6 bool 57 | if len(networkTypes) == 0 { 58 | useV4 = true 59 | useV6 = true 60 | } else { 61 | for _, nt := range networkTypes { 62 | if nt.IsIPv4() { 63 | useV4 = true 64 | 65 | continue 66 | } 67 | if nt.IsIPv6() { 68 | useV6 = true 69 | } 70 | } 71 | } 72 | 73 | addr4, mdnsErr := netTransport.ResolveUDPAddr("udp4", mdns.DefaultAddressIPv4) 74 | if mdnsErr != nil { 75 | return nil, mDNSMode, mdnsErr 76 | } 77 | addr6, mdnsErr := netTransport.ResolveUDPAddr("udp6", mdns.DefaultAddressIPv6) 78 | if mdnsErr != nil { 79 | return nil, mDNSMode, mdnsErr 80 | } 81 | 82 | var pktConnV4 *ipv4.PacketConn 83 | var mdns4Err error 84 | if useV4 { 85 | var l transport.UDPConn 86 | l, mdns4Err = netTransport.ListenUDP("udp4", addr4) 87 | if mdns4Err != nil { 88 | // If ICE fails to start MulticastDNS server just warn the user and continue 89 | log.Errorf("Failed to enable mDNS over IPv4: (%s)", mdns4Err) 90 | 91 | return nil, MulticastDNSModeDisabled, nil 92 | } 93 | pktConnV4 = ipv4.NewPacketConn(l) 94 | } 95 | 96 | var pktConnV6 *ipv6.PacketConn 97 | var mdns6Err error 98 | if useV6 { 99 | var l transport.UDPConn 100 | l, mdns6Err = netTransport.ListenUDP("udp6", addr6) 101 | if mdns6Err != nil { 102 | log.Errorf("Failed to enable mDNS over IPv6: (%s)", mdns6Err) 103 | 104 | return nil, MulticastDNSModeDisabled, nil 105 | } 106 | pktConnV6 = ipv6.NewPacketConn(l) 107 | } 108 | 109 | if mdns4Err != nil && mdns6Err != nil { 110 | // If ICE fails to start MulticastDNS server just warn the user and continue 111 | log.Errorf("Failed to enable mDNS, continuing in mDNS disabled mode") 112 | //nolint:nilerr 113 | return nil, MulticastDNSModeDisabled, nil 114 | } 115 | var ifcs []net.Interface 116 | if interfaces != nil { 117 | ifcs = make([]net.Interface, 0, len(ifcs)) 118 | for _, ifc := range interfaces { 119 | ifcs = append(ifcs, ifc.Interface) 120 | } 121 | } 122 | 123 | switch mDNSMode { 124 | case MulticastDNSModeQueryOnly: 125 | conn, err := mdns.Server(pktConnV4, pktConnV6, &mdns.Config{ 126 | Interfaces: ifcs, 127 | IncludeLoopback: includeLoopback, 128 | LoggerFactory: loggerFactory, 129 | }) 130 | 131 | return conn, mDNSMode, err 132 | case MulticastDNSModeQueryAndGather: 133 | conn, err := mdns.Server(pktConnV4, pktConnV6, &mdns.Config{ 134 | Interfaces: ifcs, 135 | IncludeLoopback: includeLoopback, 136 | LocalNames: []string{mDNSName}, 137 | LoggerFactory: loggerFactory, 138 | }) 139 | 140 | return conn, mDNSMode, err 141 | default: 142 | return nil, mDNSMode, nil 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /net.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "net" 8 | "net/netip" 9 | 10 | "github.com/pion/logging" 11 | "github.com/pion/transport/v3" 12 | ) 13 | 14 | type ifaceAddr struct { 15 | addr netip.Addr 16 | iface string 17 | } 18 | 19 | // The conditions of invalidation written below are defined in 20 | // https://tools.ietf.org/html/rfc8445#section-5.1.1.1 21 | // It is partial because the link-local check is done later in various gather local 22 | // candidate methods which conditionally accept IPv6 based on usage of mDNS or not. 23 | func isSupportedIPv6Partial(ip net.IP) bool { 24 | if len(ip) != net.IPv6len || 25 | // Deprecated IPv4-compatible IPv6 addresses [RFC4291] and IPv6 site- 26 | // local unicast addresses [RFC3879] MUST NOT be included in the 27 | // address candidates. 28 | isZeros(ip[0:12]) || // !(IPv4-compatible IPv6) 29 | ip[0] == 0xfe && ip[1]&0xc0 == 0xc0 { // !(IPv6 site-local unicast) 30 | return false 31 | } 32 | 33 | return true 34 | } 35 | 36 | func isZeros(ip net.IP) bool { 37 | for i := 0; i < len(ip); i++ { 38 | if ip[i] != 0 { 39 | return false 40 | } 41 | } 42 | 43 | return true 44 | } 45 | 46 | //nolint:gocognit,cyclop 47 | func localInterfaces( 48 | n transport.Net, 49 | interfaceFilter func(string) (keep bool), 50 | ipFilter func(net.IP) (keep bool), 51 | networkTypes []NetworkType, 52 | includeLoopback bool, 53 | ) ([]*transport.Interface, []ifaceAddr, error) { 54 | ipAddrs := []ifaceAddr{} 55 | ifaces, err := n.Interfaces() 56 | if err != nil { 57 | return nil, ipAddrs, err 58 | } 59 | 60 | filteredIfaces := make([]*transport.Interface, 0, len(ifaces)) 61 | 62 | var ipV4Requested, ipv6Requested bool 63 | if len(networkTypes) == 0 { 64 | ipV4Requested = true 65 | ipv6Requested = true 66 | } else { 67 | for _, typ := range networkTypes { 68 | if typ.IsIPv4() { 69 | ipV4Requested = true 70 | } 71 | 72 | if typ.IsIPv6() { 73 | ipv6Requested = true 74 | } 75 | } 76 | } 77 | 78 | for _, iface := range ifaces { 79 | if iface.Flags&net.FlagUp == 0 { 80 | continue // Interface down 81 | } 82 | if (iface.Flags&net.FlagLoopback != 0) && !includeLoopback { 83 | continue // Loopback interface 84 | } 85 | 86 | if interfaceFilter != nil && !interfaceFilter(iface.Name) { 87 | continue 88 | } 89 | 90 | ifaceAddrs, err := iface.Addrs() 91 | if err != nil { 92 | continue 93 | } 94 | 95 | atLeastOneAddr := false 96 | for _, addr := range ifaceAddrs { 97 | ipAddr, _, _, err := parseAddrFromIface(addr, iface.Name) 98 | if err != nil || (ipAddr.IsLoopback() && !includeLoopback) { 99 | continue 100 | } 101 | if ipAddr.Is6() { 102 | if !ipv6Requested { 103 | continue 104 | } else if !isSupportedIPv6Partial(ipAddr.AsSlice()) { 105 | continue 106 | } 107 | } else if !ipV4Requested { 108 | continue 109 | } 110 | 111 | if ipFilter != nil && !ipFilter(ipAddr.AsSlice()) { 112 | continue 113 | } 114 | 115 | atLeastOneAddr = true 116 | ipAddrs = append(ipAddrs, ifaceAddr{addr: ipAddr, iface: iface.Name}) 117 | } 118 | 119 | if atLeastOneAddr { 120 | ifaceCopy := iface 121 | filteredIfaces = append(filteredIfaces, ifaceCopy) 122 | } 123 | } 124 | 125 | return filteredIfaces, ipAddrs, nil 126 | } 127 | 128 | //nolint:cyclop 129 | func listenUDPInPortRange( 130 | netTransport transport.Net, 131 | log logging.LeveledLogger, 132 | portMax, portMin int, 133 | network string, 134 | lAddr *net.UDPAddr, 135 | ) (transport.UDPConn, error) { 136 | if (lAddr.Port != 0) || ((portMin == 0) && (portMax == 0)) { 137 | return netTransport.ListenUDP(network, lAddr) 138 | } 139 | 140 | if portMin == 0 { 141 | portMin = 1024 // Start at 1024 which is non-privileged 142 | } 143 | 144 | if portMax == 0 { 145 | portMax = 0xFFFF 146 | } 147 | 148 | if portMin > portMax { 149 | return nil, ErrPort 150 | } 151 | 152 | portStart := globalMathRandomGenerator.Intn(portMax-portMin+1) + portMin 153 | portCurrent := portStart 154 | for { 155 | addr := &net.UDPAddr{ 156 | IP: lAddr.IP, 157 | Zone: lAddr.Zone, 158 | Port: portCurrent, 159 | } 160 | 161 | c, e := netTransport.ListenUDP(network, addr) 162 | if e == nil { 163 | return c, e //nolint:nilerr 164 | } 165 | log.Debugf("Failed to listen %s: %v", lAddr.String(), e) 166 | portCurrent++ 167 | if portCurrent > portMax { 168 | portCurrent = portMin 169 | } 170 | if portCurrent == portStart { 171 | break 172 | } 173 | } 174 | 175 | return nil, ErrPort 176 | } 177 | -------------------------------------------------------------------------------- /agent_handlers.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import "sync" 7 | 8 | // OnConnectionStateChange sets a handler that is fired when the connection state changes. 9 | func (a *Agent) OnConnectionStateChange(f func(ConnectionState)) error { 10 | a.onConnectionStateChangeHdlr.Store(f) 11 | 12 | return nil 13 | } 14 | 15 | // OnSelectedCandidatePairChange sets a handler that is fired when the final candidate. 16 | // pair is selected. 17 | func (a *Agent) OnSelectedCandidatePairChange(f func(Candidate, Candidate)) error { 18 | a.onSelectedCandidatePairChangeHdlr.Store(f) 19 | 20 | return nil 21 | } 22 | 23 | // OnCandidate sets a handler that is fired when new candidates gathered. When 24 | // the gathering process complete the last candidate is nil. 25 | func (a *Agent) OnCandidate(f func(Candidate)) error { 26 | a.onCandidateHdlr.Store(f) 27 | 28 | return nil 29 | } 30 | 31 | func (a *Agent) onSelectedCandidatePairChange(p *CandidatePair) { 32 | if h, ok := a.onSelectedCandidatePairChangeHdlr.Load().(func(Candidate, Candidate)); ok && h != nil { 33 | h(p.Local, p.Remote) 34 | } 35 | } 36 | 37 | func (a *Agent) onCandidate(c Candidate) { 38 | if onCandidateHdlr, ok := a.onCandidateHdlr.Load().(func(Candidate)); ok && onCandidateHdlr != nil { 39 | onCandidateHdlr(c) 40 | } 41 | } 42 | 43 | func (a *Agent) onConnectionStateChange(s ConnectionState) { 44 | if hdlr, ok := a.onConnectionStateChangeHdlr.Load().(func(ConnectionState)); ok && hdlr != nil { 45 | hdlr(s) 46 | } 47 | } 48 | 49 | type handlerNotifier struct { 50 | sync.Mutex 51 | running bool 52 | notifiers sync.WaitGroup 53 | 54 | connectionStates []ConnectionState 55 | connectionStateFunc func(ConnectionState) 56 | 57 | candidates []Candidate 58 | candidateFunc func(Candidate) 59 | 60 | selectedCandidatePairs []*CandidatePair 61 | candidatePairFunc func(*CandidatePair) 62 | 63 | // State for closing 64 | done chan struct{} 65 | } 66 | 67 | func (h *handlerNotifier) Close(graceful bool) { 68 | if graceful { 69 | // if we were closed ungracefully before, we now 70 | // want ot wait. 71 | defer h.notifiers.Wait() 72 | } 73 | 74 | h.Lock() 75 | 76 | select { 77 | case <-h.done: 78 | h.Unlock() 79 | 80 | return 81 | default: 82 | } 83 | close(h.done) 84 | h.Unlock() 85 | } 86 | 87 | func (h *handlerNotifier) EnqueueConnectionState(state ConnectionState) { 88 | h.Lock() 89 | defer h.Unlock() 90 | 91 | select { 92 | case <-h.done: 93 | return 94 | default: 95 | } 96 | 97 | notify := func() { 98 | defer h.notifiers.Done() 99 | for { 100 | h.Lock() 101 | if len(h.connectionStates) == 0 { 102 | h.running = false 103 | h.Unlock() 104 | 105 | return 106 | } 107 | notification := h.connectionStates[0] 108 | h.connectionStates = h.connectionStates[1:] 109 | h.Unlock() 110 | h.connectionStateFunc(notification) 111 | } 112 | } 113 | 114 | h.connectionStates = append(h.connectionStates, state) 115 | if !h.running { 116 | h.running = true 117 | h.notifiers.Add(1) 118 | go notify() 119 | } 120 | } 121 | 122 | func (h *handlerNotifier) EnqueueCandidate(cand Candidate) { 123 | h.Lock() 124 | defer h.Unlock() 125 | 126 | select { 127 | case <-h.done: 128 | return 129 | default: 130 | } 131 | 132 | notify := func() { 133 | defer h.notifiers.Done() 134 | for { 135 | h.Lock() 136 | if len(h.candidates) == 0 { 137 | h.running = false 138 | h.Unlock() 139 | 140 | return 141 | } 142 | notification := h.candidates[0] 143 | h.candidates = h.candidates[1:] 144 | h.Unlock() 145 | h.candidateFunc(notification) 146 | } 147 | } 148 | 149 | h.candidates = append(h.candidates, cand) 150 | if !h.running { 151 | h.running = true 152 | h.notifiers.Add(1) 153 | go notify() 154 | } 155 | } 156 | 157 | func (h *handlerNotifier) EnqueueSelectedCandidatePair(pair *CandidatePair) { 158 | h.Lock() 159 | defer h.Unlock() 160 | 161 | select { 162 | case <-h.done: 163 | return 164 | default: 165 | } 166 | 167 | notify := func() { 168 | defer h.notifiers.Done() 169 | for { 170 | h.Lock() 171 | if len(h.selectedCandidatePairs) == 0 { 172 | h.running = false 173 | h.Unlock() 174 | 175 | return 176 | } 177 | notification := h.selectedCandidatePairs[0] 178 | h.selectedCandidatePairs = h.selectedCandidatePairs[1:] 179 | h.Unlock() 180 | h.candidatePairFunc(notification) 181 | } 182 | } 183 | 184 | h.selectedCandidatePairs = append(h.selectedCandidatePairs, pair) 185 | if !h.running { 186 | h.running = true 187 | h.notifiers.Add(1) 188 | go notify() 189 | } 190 | } 191 | -------------------------------------------------------------------------------- /examples/ping-pong/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package main implements a simple example demonstrating a Pion-to-Pion ICE connection 5 | package main 6 | 7 | import ( 8 | "bufio" 9 | "context" 10 | "flag" 11 | "fmt" 12 | "net/http" 13 | "net/url" 14 | "os" 15 | "time" 16 | 17 | "github.com/pion/ice/v4" 18 | "github.com/pion/randutil" 19 | ) 20 | 21 | //nolint:gochecknoglobals 22 | var ( 23 | isControlling bool 24 | iceAgent *ice.Agent 25 | remoteAuthChannel chan string 26 | localHTTPPort, remoteHTTPPort int 27 | ) 28 | 29 | // HTTP Listener to get ICE Credentials from remote Peer. 30 | func remoteAuth(_ http.ResponseWriter, r *http.Request) { 31 | if err := r.ParseForm(); err != nil { 32 | panic(err) 33 | } 34 | 35 | remoteAuthChannel <- r.PostForm["ufrag"][0] 36 | remoteAuthChannel <- r.PostForm["pwd"][0] 37 | } 38 | 39 | // HTTP Listener to get ICE Candidate from remote Peer. 40 | func remoteCandidate(_ http.ResponseWriter, r *http.Request) { 41 | if err := r.ParseForm(); err != nil { 42 | panic(err) 43 | } 44 | 45 | c, err := ice.UnmarshalCandidate(r.PostForm["candidate"][0]) 46 | if err != nil { 47 | panic(err) 48 | } 49 | 50 | if err := iceAgent.AddRemoteCandidate(c); err != nil { //nolint:contextcheck 51 | panic(err) 52 | } 53 | } 54 | 55 | func main() { //nolint 56 | var ( 57 | err error 58 | conn *ice.Conn 59 | ) 60 | remoteAuthChannel = make(chan string, 3) 61 | 62 | flag.BoolVar(&isControlling, "controlling", false, "is ICE Agent controlling") 63 | flag.Parse() 64 | 65 | if isControlling { 66 | localHTTPPort = 9000 67 | remoteHTTPPort = 9001 68 | } else { 69 | localHTTPPort = 9001 70 | remoteHTTPPort = 9000 71 | } 72 | 73 | http.HandleFunc("/remoteAuth", remoteAuth) 74 | http.HandleFunc("/remoteCandidate", remoteCandidate) 75 | go func() { 76 | if err = http.ListenAndServe(fmt.Sprintf(":%d", localHTTPPort), nil); err != nil { //nolint:gosec 77 | panic(err) 78 | } 79 | }() 80 | 81 | if isControlling { 82 | fmt.Println("Local Agent is controlling") 83 | } else { 84 | fmt.Println("Local Agent is controlled") 85 | } 86 | fmt.Print("Press 'Enter' when both processes have started") 87 | if _, err = bufio.NewReader(os.Stdin).ReadBytes('\n'); err != nil { 88 | panic(err) 89 | } 90 | 91 | iceAgent, err = ice.NewAgentWithOptions( 92 | ice.WithNetworkTypes([]ice.NetworkType{ice.NetworkTypeUDP4}), 93 | ) 94 | if err != nil { 95 | panic(err) 96 | } 97 | 98 | // When we have gathered a new ICE Candidate send it to the remote peer 99 | if err = iceAgent.OnCandidate(func(c ice.Candidate) { 100 | if c == nil { 101 | return 102 | } 103 | 104 | _, err = http.PostForm(fmt.Sprintf("http://localhost:%d/remoteCandidate", remoteHTTPPort), //nolint 105 | url.Values{ 106 | "candidate": {c.Marshal()}, 107 | }) 108 | if err != nil { 109 | panic(err) 110 | } 111 | }); err != nil { 112 | panic(err) 113 | } 114 | 115 | // When ICE Connection state has change print to stdout 116 | if err = iceAgent.OnConnectionStateChange(func(c ice.ConnectionState) { 117 | fmt.Printf("ICE Connection State has changed: %s\n", c.String()) 118 | }); err != nil { 119 | panic(err) 120 | } 121 | 122 | // Get the local auth details and send to remote peer 123 | localUfrag, localPwd, err := iceAgent.GetLocalUserCredentials() 124 | if err != nil { 125 | panic(err) 126 | } 127 | 128 | _, err = http.PostForm(fmt.Sprintf("http://localhost:%d/remoteAuth", remoteHTTPPort), //nolint 129 | url.Values{ 130 | "ufrag": {localUfrag}, 131 | "pwd": {localPwd}, 132 | }) 133 | if err != nil { 134 | panic(err) 135 | } 136 | 137 | remoteUfrag := <-remoteAuthChannel 138 | remotePwd := <-remoteAuthChannel 139 | 140 | if err = iceAgent.GatherCandidates(); err != nil { 141 | panic(err) 142 | } 143 | 144 | // Start the ICE Agent. One side must be controlled, and the other must be controlling 145 | if isControlling { 146 | conn, err = iceAgent.Dial(context.TODO(), remoteUfrag, remotePwd) 147 | } else { 148 | conn, err = iceAgent.Accept(context.TODO(), remoteUfrag, remotePwd) 149 | } 150 | if err != nil { 151 | panic(err) 152 | } 153 | 154 | // Send messages in a loop to the remote peer 155 | go func() { 156 | for { 157 | time.Sleep(time.Second * 3) 158 | 159 | val, err := randutil.GenerateCryptoRandomString(15, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ") 160 | if err != nil { 161 | panic(err) 162 | } 163 | if _, err = conn.Write([]byte(val)); err != nil { 164 | panic(err) 165 | } 166 | 167 | fmt.Printf("Sent: '%s'\n", val) 168 | } 169 | }() 170 | 171 | // Receive messages in a loop from the remote peer 172 | buf := make([]byte, 1500) 173 | for { 174 | n, err := conn.Read(buf) 175 | if err != nil { 176 | panic(err) 177 | } 178 | 179 | fmt.Printf("Received: '%s'\n", string(buf[:n])) 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /transport.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "context" 8 | "net" 9 | "sync/atomic" 10 | "time" 11 | 12 | "github.com/pion/stun/v3" 13 | ) 14 | 15 | // Dial connects to the remote agent, acting as the controlling ice agent. 16 | // Dial blocks until at least one ice candidate pair has successfully connected. 17 | func (a *Agent) Dial(ctx context.Context, remoteUfrag, remotePwd string) (*Conn, error) { 18 | return a.connect(ctx, true, remoteUfrag, remotePwd) 19 | } 20 | 21 | // Accept connects to the remote agent, acting as the controlled ice agent. 22 | // Accept blocks until at least one ice candidate pair has successfully connected. 23 | func (a *Agent) Accept(ctx context.Context, remoteUfrag, remotePwd string) (*Conn, error) { 24 | return a.connect(ctx, false, remoteUfrag, remotePwd) 25 | } 26 | 27 | // Conn represents the ICE connection. 28 | // At the moment the lifetime of the Conn is equal to the Agent. 29 | type Conn struct { 30 | bytesReceived atomic.Uint64 31 | bytesSent atomic.Uint64 32 | agent *Agent 33 | } 34 | 35 | // BytesSent returns the number of bytes sent. 36 | func (c *Conn) BytesSent() uint64 { 37 | return c.bytesSent.Load() 38 | } 39 | 40 | // BytesReceived returns the number of bytes received. 41 | func (c *Conn) BytesReceived() uint64 { 42 | return c.bytesReceived.Load() 43 | } 44 | 45 | func (a *Agent) connect(ctx context.Context, isControlling bool, remoteUfrag, remotePwd string) (*Conn, error) { 46 | err := a.loop.Err() 47 | if err != nil { 48 | return nil, err 49 | } 50 | err = a.startConnectivityChecks(isControlling, remoteUfrag, remotePwd) //nolint:contextcheck 51 | if err != nil { 52 | return nil, err 53 | } 54 | 55 | // Block until pair selected 56 | select { 57 | case <-a.loop.Done(): 58 | return nil, a.loop.Err() 59 | case <-ctx.Done(): 60 | return nil, ErrCanceledByCaller 61 | case <-a.onConnected: 62 | } 63 | 64 | return &Conn{ 65 | agent: a, 66 | }, nil 67 | } 68 | 69 | // Read implements the Conn Read method. 70 | func (c *Conn) Read(p []byte) (int, error) { 71 | err := c.agent.loop.Err() 72 | if err != nil { 73 | return 0, err 74 | } 75 | 76 | n, err := c.agent.buf.Read(p) 77 | c.bytesReceived.Add(uint64(n)) //nolint:gosec // G115 78 | 79 | return n, err 80 | } 81 | 82 | // Write implements the Conn Write method. 83 | func (c *Conn) Write(packet []byte) (int, error) { 84 | err := c.agent.loop.Err() 85 | if err != nil { 86 | return 0, err 87 | } 88 | 89 | if stun.IsMessage(packet) { 90 | return 0, errWriteSTUNMessageToIceConn 91 | } 92 | 93 | pair := c.agent.getSelectedPair() 94 | if pair == nil { 95 | if err = c.agent.loop.Run(c.agent.loop, func(_ context.Context) { 96 | pair = c.agent.getBestValidCandidatePair() 97 | }); err != nil { 98 | return 0, err 99 | } 100 | 101 | if pair == nil { 102 | return 0, err 103 | } 104 | } 105 | 106 | // Write application data via the selected pair and update stats with actual bytes written. 107 | n, err := pair.Write(packet) 108 | if n > 0 { 109 | c.bytesSent.Add(uint64(n)) 110 | pair.UpdatePacketSent(n) 111 | } 112 | 113 | return n, err 114 | } 115 | 116 | // Close implements the Conn Close method. It is used to close 117 | // the connection. Any calls to Read and Write will be unblocked and return an error. 118 | func (c *Conn) Close() error { 119 | return c.agent.Close() 120 | } 121 | 122 | // LocalAddr returns the local address of the current selected pair or nil if there is none. 123 | func (c *Conn) LocalAddr() net.Addr { 124 | pair := c.agent.getSelectedPair() 125 | if pair == nil { 126 | return nil 127 | } 128 | 129 | return pair.Local.addr() 130 | } 131 | 132 | // RemoteAddr returns the remote address of the current selected pair or nil if there is none. 133 | func (c *Conn) RemoteAddr() net.Addr { 134 | pair := c.agent.getSelectedPair() 135 | if pair == nil { 136 | return nil 137 | } 138 | 139 | return pair.Remote.addr() 140 | } 141 | 142 | // SetDeadline sets both read and write deadlines on the underlying ICE connection. 143 | func (c *Conn) SetDeadline(t time.Time) error { 144 | if err := c.SetReadDeadline(t); err != nil { 145 | return err 146 | } 147 | 148 | return c.SetWriteDeadline(t) 149 | } 150 | 151 | // SetReadDeadline sets the read deadline on the packet buffer used for application data. 152 | func (c *Conn) SetReadDeadline(t time.Time) error { 153 | return c.agent.buf.SetReadDeadline(t) 154 | } 155 | 156 | // SetWriteDeadline sets the write deadline on the currently selected local candidate connection. 157 | // The deadline applies to the selected candidate pair and will affect all traffic over that pair. 158 | func (c *Conn) SetWriteDeadline(t time.Time) error { 159 | pair := c.agent.getSelectedPair() 160 | if pair == nil || pair.Local == nil { 161 | return nil 162 | } 163 | 164 | if d, ok := pair.Local.(interface { 165 | setWriteDeadline(time.Time) error 166 | }); ok { 167 | return d.setWriteDeadline(t) 168 | } 169 | 170 | return nil 171 | } 172 | -------------------------------------------------------------------------------- /candidatepair_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/stretchr/testify/require" 10 | ) 11 | 12 | func hostCandidate() *CandidateHost { 13 | return &CandidateHost{ 14 | candidateBase: candidateBase{ 15 | candidateType: CandidateTypeHost, 16 | component: ComponentRTP, 17 | }, 18 | } 19 | } 20 | 21 | func prflxCandidate() *CandidatePeerReflexive { 22 | return &CandidatePeerReflexive{ 23 | candidateBase: candidateBase{ 24 | candidateType: CandidateTypePeerReflexive, 25 | component: ComponentRTP, 26 | }, 27 | } 28 | } 29 | 30 | func srflxCandidate() *CandidateServerReflexive { 31 | return &CandidateServerReflexive{ 32 | candidateBase: candidateBase{ 33 | candidateType: CandidateTypeServerReflexive, 34 | component: ComponentRTP, 35 | }, 36 | } 37 | } 38 | 39 | func relayCandidate() *CandidateRelay { 40 | return &CandidateRelay{ 41 | candidateBase: candidateBase{ 42 | candidateType: CandidateTypeRelay, 43 | component: ComponentRTP, 44 | }, 45 | } 46 | } 47 | 48 | func TestCandidatePairPriority(t *testing.T) { 49 | for _, test := range []struct { 50 | Pair *CandidatePair 51 | WantPriority uint64 52 | }{ 53 | { 54 | Pair: newCandidatePair( 55 | hostCandidate(), 56 | hostCandidate(), 57 | false, 58 | ), 59 | WantPriority: 9151314440652587007, 60 | }, 61 | { 62 | Pair: newCandidatePair( 63 | hostCandidate(), 64 | hostCandidate(), 65 | true, 66 | ), 67 | WantPriority: 9151314440652587007, 68 | }, 69 | { 70 | Pair: newCandidatePair( 71 | hostCandidate(), 72 | prflxCandidate(), 73 | true, 74 | ), 75 | WantPriority: 7998392936314175488, 76 | }, 77 | { 78 | Pair: newCandidatePair( 79 | hostCandidate(), 80 | prflxCandidate(), 81 | false, 82 | ), 83 | WantPriority: 7998392936314175487, 84 | }, 85 | { 86 | Pair: newCandidatePair( 87 | hostCandidate(), 88 | srflxCandidate(), 89 | true, 90 | ), 91 | WantPriority: 7277816996102668288, 92 | }, 93 | { 94 | Pair: newCandidatePair( 95 | hostCandidate(), 96 | srflxCandidate(), 97 | false, 98 | ), 99 | WantPriority: 7277816996102668287, 100 | }, 101 | { 102 | Pair: newCandidatePair( 103 | hostCandidate(), 104 | relayCandidate(), 105 | true, 106 | ), 107 | WantPriority: 72057593987596288, 108 | }, 109 | { 110 | Pair: newCandidatePair( 111 | hostCandidate(), 112 | relayCandidate(), 113 | false, 114 | ), 115 | WantPriority: 72057593987596287, 116 | }, 117 | } { 118 | require.Equal(t, test.Pair.priority(), test.WantPriority) 119 | } 120 | } 121 | 122 | func TestCandidatePairEquality(t *testing.T) { 123 | pairA := newCandidatePair(hostCandidate(), srflxCandidate(), true) 124 | pairB := newCandidatePair(hostCandidate(), srflxCandidate(), false) 125 | 126 | require.True(t, pairA.equal(pairB)) 127 | } 128 | 129 | func TestNilCandidatePairString(t *testing.T) { 130 | var nilCandidatePair *CandidatePair 131 | require.Equal(t, nilCandidatePair.String(), "") 132 | } 133 | 134 | func TestCandidatePairState_String(t *testing.T) { 135 | tests := []struct { 136 | name string 137 | in CandidatePairState 138 | want string 139 | }{ 140 | {"waiting", CandidatePairStateWaiting, "waiting"}, 141 | {"in-progress", CandidatePairStateInProgress, "in-progress"}, 142 | {"failed", CandidatePairStateFailed, "failed"}, 143 | {"succeeded", CandidatePairStateSucceeded, "succeeded"}, 144 | {"unknown", CandidatePairState(255), "Unknown candidate pair state"}, 145 | } 146 | 147 | for _, tt := range tests { 148 | t.Run(tt.name, func(t *testing.T) { 149 | require.Equal(t, tt.want, tt.in.String()) 150 | }) 151 | } 152 | } 153 | 154 | func TestCandidatePairEqual_NilCases(t *testing.T) { 155 | // both nil -> true 156 | var a *CandidatePair 157 | var b *CandidatePair 158 | require.True(t, a.equal(b), "both nil pairs should be equal") 159 | 160 | // left non-nil, right nil -> false 161 | a = newCandidatePair(hostCandidate(), srflxCandidate(), true) 162 | require.False(t, a.equal(nil), "non-nil vs nil should be false") 163 | 164 | // left nil, right non-nil -> false 165 | require.False(t, (*CandidatePair)(nil).equal(a), "nil vs non-nil should be false") 166 | } 167 | 168 | func TestCandidatePair_TimeGetters_DefaultZero(t *testing.T) { 169 | p := newCandidatePair(hostCandidate(), srflxCandidate(), true) 170 | 171 | require.True(t, p.FirstRequestSentAt().IsZero(), "FirstRequestSentAt should be zero by default") 172 | require.True(t, p.LastRequestSentAt().IsZero(), "LastRequestSentAt should be zero by default") 173 | require.True(t, p.FirstReponseReceivedAt().IsZero(), "FirstReponseReceivedAt should be zero by default") 174 | require.True(t, p.LastResponseReceivedAt().IsZero(), "LastResponseReceivedAt should be zero by default") 175 | require.True(t, p.FirstRequestReceivedAt().IsZero(), "FirstRequestReceivedAt should be zero by default") 176 | require.True(t, p.LastRequestReceivedAt().IsZero(), "LastRequestReceivedAt should be zero by default") 177 | } 178 | -------------------------------------------------------------------------------- /examples/continual-gathering/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2025 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | // Package main demonstrates the ContinualGatheringPolicy feature 5 | package main 6 | 7 | import ( 8 | "context" 9 | "flag" 10 | "fmt" 11 | "log" 12 | "os" 13 | "os/signal" 14 | "syscall" 15 | "time" 16 | 17 | "github.com/pion/ice/v4" 18 | "github.com/pion/logging" 19 | ) 20 | 21 | func main() { //nolint:cyclop 22 | var gatheringMode string 23 | var monitorInterval time.Duration 24 | 25 | flag.StringVar(&gatheringMode, "mode", "continually", "Gathering mode: 'once' or 'continually'") 26 | flag.DurationVar(&monitorInterval, "interval", 2*time.Second, "Network monitoring interval (for continual mode)") 27 | flag.Parse() 28 | 29 | // Determine gathering policy 30 | var policy ice.ContinualGatheringPolicy 31 | switch gatheringMode { 32 | case "once": 33 | policy = ice.GatherOnce 34 | fmt.Println("Using GatherOnce policy - gathering will complete after initial collection") 35 | case "continually": 36 | policy = ice.GatherContinually 37 | fmt.Printf("Using GatherContinually policy - monitoring for network changes every %v\n", monitorInterval) 38 | default: 39 | log.Fatalf("Invalid mode: %s. Use 'once' or 'continually'", gatheringMode) 40 | } 41 | 42 | // Create logger 43 | loggerFactory := logging.NewDefaultLoggerFactory() 44 | loggerFactory.DefaultLogLevel = logging.LogLevelDebug 45 | 46 | // Create ICE agent with the specified gathering policy using AgentOptions 47 | agent, err := ice.NewAgentWithOptions( 48 | ice.WithNetworkTypes([]ice.NetworkType{ice.NetworkTypeUDP4, ice.NetworkTypeUDP6}), 49 | ice.WithCandidateTypes([]ice.CandidateType{ice.CandidateTypeHost}), 50 | ice.WithContinualGatheringPolicy(policy), 51 | ice.WithNetworkMonitorInterval(monitorInterval), 52 | ) 53 | if err != nil { 54 | log.Fatalf("Failed to create agent: %v", err) 55 | } 56 | 57 | defer func() { 58 | if closeErr := agent.Close(); closeErr != nil { 59 | log.Printf("Failed to close agent: %v", closeErr) 60 | } 61 | }() 62 | 63 | // Track candidates 64 | candidateCount := 0 65 | candidateMap := make(map[string]ice.Candidate) 66 | 67 | // Set up candidate handler 68 | err = agent.OnCandidate(func(candidate ice.Candidate) { 69 | if candidate == nil { 70 | if policy == ice.GatherOnce { 71 | fmt.Println("\n=== Gathering completed (no more candidates) ===") 72 | } 73 | 74 | return 75 | } 76 | 77 | candidateCount++ 78 | candidateID := candidate.String() 79 | 80 | if _, exists := candidateMap[candidateID]; !exists { 81 | candidateMap[candidateID] = candidate 82 | fmt.Printf("[%s] Candidate #%d: %s\n", time.Now().Format("15:04:05"), candidateCount, candidate) 83 | } 84 | }) 85 | if err != nil { 86 | log.Fatalf("Failed to set candidate handler: %v", err) //nolint:gocritic 87 | } 88 | 89 | // Start gathering 90 | fmt.Println("\n=== Starting candidate gathering ===") 91 | err = agent.GatherCandidates() 92 | if err != nil { 93 | log.Fatalf("Failed to start gathering: %v", err) 94 | } 95 | 96 | // Set up signal handling for graceful shutdown 97 | sigChan := make(chan os.Signal, 1) 98 | signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) 99 | 100 | // Create a context for periodic status checks 101 | ctx, cancel := context.WithCancel(context.Background()) 102 | defer cancel() 103 | 104 | // Periodically check and display gathering state 105 | go func() { 106 | ticker := time.NewTicker(5 * time.Second) 107 | defer ticker.Stop() 108 | 109 | for { 110 | select { 111 | case <-ctx.Done(): 112 | return 113 | case <-ticker.C: 114 | state, err := agent.GetGatheringState() //nolint:contextcheck 115 | if err != nil { 116 | log.Printf("Failed to get gathering state: %v", err) 117 | 118 | continue 119 | } 120 | 121 | localCandidates, err := agent.GetLocalCandidates() //nolint:contextcheck 122 | if err != nil { 123 | log.Printf("Failed to get local candidates: %v", err) 124 | 125 | continue 126 | } 127 | 128 | fmt.Printf("\n[%s] Status: GatheringState=%s, Candidates=%d\n", 129 | time.Now().Format("15:04:05"), state, len(localCandidates)) 130 | 131 | if policy == ice.GatherContinually { 132 | fmt.Println("Tip: Try changing network interfaces (connect/disconnect WiFi, enable/disable network adapters)") 133 | fmt.Println(" New candidates will be discovered automatically!") 134 | } 135 | } 136 | } 137 | }() 138 | 139 | // Wait for interrupt signal 140 | fmt.Println("\nPress Ctrl+C to exit...") 141 | <-sigChan 142 | 143 | fmt.Println("\n=== Shutting down ===") 144 | cancel() 145 | 146 | // Display final statistics 147 | state, _ := agent.GetGatheringState() 148 | localCandidates, _ := agent.GetLocalCandidates() 149 | 150 | fmt.Printf("\nFinal Statistics:\n") 151 | fmt.Printf(" Gathering Policy: %s\n", policy) 152 | fmt.Printf(" Gathering State: %s\n", state) 153 | fmt.Printf(" Total Candidates Discovered: %d\n", candidateCount) 154 | fmt.Printf(" Unique Candidates: %d\n", len(candidateMap)) 155 | fmt.Printf(" Current Active Candidates: %d\n", len(localCandidates)) 156 | } 157 | -------------------------------------------------------------------------------- /active_tcp.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "context" 8 | "io" 9 | "net" 10 | "net/netip" 11 | "sync/atomic" 12 | "time" 13 | 14 | "github.com/pion/logging" 15 | "github.com/pion/transport/v3/packetio" 16 | ) 17 | 18 | type activeTCPConn struct { 19 | readBuffer, writeBuffer *packetio.Buffer 20 | localAddr, remoteAddr atomic.Value 21 | conn atomic.Value // stores net.Conn 22 | closed atomic.Bool 23 | } 24 | 25 | func newActiveTCPConn( 26 | ctx context.Context, 27 | localAddress string, 28 | remoteAddress netip.AddrPort, 29 | log logging.LeveledLogger, 30 | ) (a *activeTCPConn) { 31 | a = &activeTCPConn{ 32 | readBuffer: packetio.NewBuffer(), 33 | writeBuffer: packetio.NewBuffer(), 34 | } 35 | 36 | laddr, err := getTCPAddrOnInterface(localAddress) 37 | if err != nil { 38 | a.closed.Store(true) 39 | log.Infof("Failed to dial TCP address %s: %v", remoteAddress, err) 40 | 41 | return a 42 | } 43 | a.localAddr.Store(laddr) 44 | 45 | go func() { 46 | defer func() { 47 | a.closed.Store(true) 48 | }() 49 | 50 | dialer := &net.Dialer{ 51 | LocalAddr: laddr, 52 | } 53 | conn, err := dialer.DialContext(ctx, "tcp", remoteAddress.String()) 54 | if err != nil { 55 | log.Infof("Failed to dial TCP address %s: %v", remoteAddress, err) 56 | 57 | return 58 | } 59 | a.conn.Store(conn) 60 | a.remoteAddr.Store(conn.RemoteAddr()) 61 | 62 | go func() { 63 | buff := make([]byte, receiveMTU) 64 | 65 | for !a.closed.Load() { 66 | n, err := readStreamingPacket(conn, buff) 67 | if err != nil { 68 | log.Infof("Failed to read streaming packet: %s", err) 69 | 70 | break 71 | } 72 | 73 | if _, err := a.readBuffer.Write(buff[:n]); err != nil { 74 | log.Infof("Failed to write to buffer: %s", err) 75 | 76 | break 77 | } 78 | } 79 | }() 80 | 81 | buff := make([]byte, receiveMTU) 82 | 83 | for !a.closed.Load() { 84 | n, err := a.writeBuffer.Read(buff) 85 | if err != nil { 86 | log.Infof("Failed to read from buffer: %s", err) 87 | 88 | break 89 | } 90 | 91 | if _, err = writeStreamingPacket(conn, buff[:n]); err != nil { 92 | log.Infof("Failed to write streaming packet: %s", err) 93 | 94 | break 95 | } 96 | } 97 | 98 | if err := conn.Close(); err != nil { 99 | log.Infof("Failed to close connection: %s", err) 100 | } 101 | }() 102 | 103 | return a 104 | } 105 | 106 | func (a *activeTCPConn) ReadFrom(buff []byte) (n int, srcAddr net.Addr, err error) { 107 | if a.closed.Load() { 108 | return 0, nil, io.ErrClosedPipe 109 | } 110 | 111 | n, err = a.readBuffer.Read(buff) 112 | // RemoteAddr is assuredly set *after* we can read from the buffer 113 | srcAddr = a.RemoteAddr() 114 | 115 | return 116 | } 117 | 118 | func (a *activeTCPConn) WriteTo(buff []byte, _ net.Addr) (n int, err error) { 119 | if a.closed.Load() { 120 | return 0, io.ErrClosedPipe 121 | } 122 | 123 | return a.writeBuffer.Write(buff) 124 | } 125 | 126 | func (a *activeTCPConn) Close() error { 127 | a.closed.Store(true) 128 | _ = a.readBuffer.Close() 129 | _ = a.writeBuffer.Close() 130 | if c, ok := a.conn.Load().(net.Conn); ok { 131 | _ = c.Close() 132 | } 133 | 134 | return nil 135 | } 136 | 137 | func (a *activeTCPConn) LocalAddr() net.Addr { 138 | if v, ok := a.localAddr.Load().(*net.TCPAddr); ok { 139 | return v 140 | } 141 | 142 | return &net.TCPAddr{} 143 | } 144 | 145 | // RemoteAddr returns the remote address of the connection which is only 146 | // set once a background goroutine has successfully dialed. That means 147 | // this may return ":0" for the address prior to that happening. If this 148 | // becomes an issue, we can introduce a synchronization point between Dial 149 | // and these methods. 150 | func (a *activeTCPConn) RemoteAddr() net.Addr { 151 | if v, ok := a.remoteAddr.Load().(*net.TCPAddr); ok { 152 | return v 153 | } 154 | 155 | return &net.TCPAddr{} 156 | } 157 | 158 | func (a *activeTCPConn) SetDeadline(t time.Time) error { 159 | if a.closed.Load() { 160 | return io.EOF 161 | } 162 | if c, ok := a.conn.Load().(net.Conn); ok { 163 | return c.SetDeadline(t) 164 | } 165 | 166 | return io.EOF 167 | } 168 | 169 | func (a *activeTCPConn) SetReadDeadline(t time.Time) error { 170 | if a.closed.Load() { 171 | return io.EOF 172 | } 173 | if c, ok := a.conn.Load().(net.Conn); ok { 174 | return c.SetReadDeadline(t) 175 | } 176 | 177 | return io.EOF 178 | } 179 | 180 | func (a *activeTCPConn) SetWriteDeadline(t time.Time) error { 181 | if a.closed.Load() { 182 | return io.EOF 183 | } 184 | if c, ok := a.conn.Load().(net.Conn); ok { 185 | return c.SetWriteDeadline(t) 186 | } 187 | 188 | return io.EOF 189 | } 190 | 191 | func getTCPAddrOnInterface(address string) (*net.TCPAddr, error) { 192 | addr, err := net.ResolveTCPAddr("tcp", address) 193 | if err != nil { 194 | return nil, err 195 | } 196 | 197 | l, err := net.ListenTCP("tcp", addr) 198 | if err != nil { 199 | return nil, err 200 | } 201 | defer func() { 202 | _ = l.Close() 203 | }() 204 | 205 | tcpAddr, ok := l.Addr().(*net.TCPAddr) 206 | if !ok { 207 | return nil, errInvalidAddress 208 | } 209 | 210 | return tcpAddr, nil 211 | } 212 | -------------------------------------------------------------------------------- /agent_handlers_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "testing" 8 | "time" 9 | 10 | "github.com/pion/transport/v3/test" 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | func TestConnectionStateNotifier(t *testing.T) { 16 | t.Run("TestManyUpdates", func(t *testing.T) { 17 | defer test.CheckRoutines(t)() 18 | 19 | updates := make(chan struct{}, 1) 20 | notifier := &handlerNotifier{ 21 | connectionStateFunc: func(_ ConnectionState) { 22 | updates <- struct{}{} 23 | }, 24 | done: make(chan struct{}), 25 | } 26 | // Enqueue all updates upfront to ensure that it 27 | // doesn't block 28 | for i := 0; i < 10000; i++ { 29 | notifier.EnqueueConnectionState(ConnectionStateNew) 30 | } 31 | done := make(chan struct{}) 32 | go func() { 33 | for i := 0; i < 10000; i++ { 34 | <-updates 35 | } 36 | select { 37 | case <-updates: 38 | t.Errorf("received more updates than expected") // nolint 39 | case <-time.After(1 * time.Second): 40 | } 41 | close(done) 42 | }() 43 | <-done 44 | notifier.Close(true) 45 | }) 46 | t.Run("TestUpdateOrdering", func(t *testing.T) { 47 | defer test.CheckRoutines(t)() 48 | updates := make(chan ConnectionState) 49 | notifer := &handlerNotifier{ 50 | connectionStateFunc: func(cs ConnectionState) { 51 | updates <- cs 52 | }, 53 | done: make(chan struct{}), 54 | } 55 | done := make(chan struct{}) 56 | go func() { 57 | for i := 0; i < 10000; i++ { 58 | assert.Equal(t, ConnectionState(i), <-updates) 59 | } 60 | select { 61 | case <-updates: 62 | t.Errorf("received more updates than expected") // nolint 63 | case <-time.After(1 * time.Second): 64 | } 65 | close(done) 66 | }() 67 | for i := 0; i < 10000; i++ { 68 | notifer.EnqueueConnectionState(ConnectionState(i)) 69 | } 70 | <-done 71 | notifer.Close(true) 72 | }) 73 | } 74 | 75 | func TestHandlerNotifier_Close_AlreadyClosed(t *testing.T) { 76 | defer test.CheckRoutines(t)() 77 | 78 | notifier := &handlerNotifier{ 79 | connectionStateFunc: func(ConnectionState) {}, 80 | candidateFunc: func(Candidate) {}, 81 | candidatePairFunc: func(*CandidatePair) {}, 82 | done: make(chan struct{}), 83 | } 84 | 85 | // first close 86 | notifier.Close(false) 87 | 88 | isClosed := func(ch <-chan struct{}) bool { 89 | select { 90 | case <-ch: 91 | return true 92 | default: 93 | return false 94 | } 95 | } 96 | assert.True(t, isClosed(notifier.done), "expected h.done to be closed after first Close") 97 | 98 | // second close should hit `case <-h.done` and return immediately 99 | // without blocking on the WaitGroup. 100 | finished := make(chan struct{}, 1) 101 | go func() { 102 | notifier.Close(true) 103 | close(finished) 104 | }() 105 | 106 | assert.Eventually(t, func() bool { 107 | select { 108 | case <-finished: 109 | return true 110 | default: 111 | return false 112 | } 113 | }, 250*time.Millisecond, 10*time.Millisecond, "second Close(true) did not return promptly") 114 | 115 | // ensure still closed afterwards 116 | assert.True(t, isClosed(notifier.done), "expected h.done to remain closed after second Close") 117 | 118 | // sanity: no enqueues should start after close. 119 | require.False(t, notifier.running) 120 | require.Zero(t, len(notifier.connectionStates)) 121 | require.Zero(t, len(notifier.candidates)) 122 | require.Zero(t, len(notifier.selectedCandidatePairs)) 123 | } 124 | 125 | func TestHandlerNotifier_EnqueueConnectionState_AfterClose(t *testing.T) { 126 | defer test.CheckRoutines(t)() 127 | 128 | connCh := make(chan struct{}, 1) 129 | notifier := &handlerNotifier{ 130 | connectionStateFunc: func(ConnectionState) { connCh <- struct{}{} }, 131 | done: make(chan struct{}), 132 | } 133 | 134 | notifier.Close(false) 135 | notifier.EnqueueConnectionState(ConnectionStateConnected) 136 | 137 | assert.Never(t, func() bool { 138 | select { 139 | case <-connCh: 140 | return true 141 | default: 142 | return false 143 | } 144 | }, 250*time.Millisecond, 10*time.Millisecond, "connectionStateFunc should not be called after close") 145 | } 146 | 147 | func TestHandlerNotifier_EnqueueCandidate_AfterClose(t *testing.T) { 148 | defer test.CheckRoutines(t)() 149 | 150 | candidateCh := make(chan struct{}, 1) 151 | h := &handlerNotifier{ 152 | candidateFunc: func(Candidate) { candidateCh <- struct{}{} }, 153 | done: make(chan struct{}), 154 | } 155 | 156 | h.Close(false) 157 | h.EnqueueCandidate(nil) 158 | 159 | assert.Never(t, func() bool { 160 | select { 161 | case <-candidateCh: 162 | return true 163 | default: 164 | return false 165 | } 166 | }, 250*time.Millisecond, 10*time.Millisecond, "candidateFunc should not be called after close") 167 | } 168 | 169 | func TestHandlerNotifier_EnqueueSelectedCandidatePair_AfterClose(t *testing.T) { 170 | defer test.CheckRoutines(t)() 171 | 172 | pairCh := make(chan struct{}, 1) 173 | h := &handlerNotifier{ 174 | candidatePairFunc: func(*CandidatePair) { pairCh <- struct{}{} }, 175 | done: make(chan struct{}), 176 | } 177 | 178 | h.Close(false) 179 | h.EnqueueSelectedCandidatePair(nil) 180 | 181 | assert.Never(t, func() bool { 182 | select { 183 | case <-pairCh: 184 | return true 185 | default: 186 | return false 187 | } 188 | }, 250*time.Millisecond, 10*time.Millisecond, "candidatePairFunc should not be called after close") 189 | } 190 | -------------------------------------------------------------------------------- /udp_muxed_conn.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "io" 8 | "net" 9 | "slices" 10 | "sync" 11 | "time" 12 | 13 | "github.com/pion/logging" 14 | ) 15 | 16 | type udpMuxedConnState int 17 | 18 | const ( 19 | udpMuxedConnOpen udpMuxedConnState = iota 20 | udpMuxedConnWaiting 21 | udpMuxedConnClosed 22 | ) 23 | 24 | type udpMuxedConnParams struct { 25 | Mux *UDPMuxDefault 26 | AddrPool *sync.Pool 27 | Key string 28 | LocalAddr net.Addr 29 | Logger logging.LeveledLogger 30 | } 31 | 32 | // udpMuxedConn represents a logical packet conn for a single remote as identified by ufrag. 33 | type udpMuxedConn struct { 34 | params *udpMuxedConnParams 35 | // Remote addresses that we have sent to on this conn 36 | addresses []ipPort 37 | 38 | // FIFO queue holding incoming packets 39 | bufHead, bufTail *bufferHolder 40 | notify chan struct{} 41 | closedChan chan struct{} 42 | state udpMuxedConnState 43 | mu sync.Mutex 44 | } 45 | 46 | func newUDPMuxedConn(params *udpMuxedConnParams) *udpMuxedConn { 47 | return &udpMuxedConn{ 48 | params: params, 49 | notify: make(chan struct{}, 1), 50 | closedChan: make(chan struct{}), 51 | } 52 | } 53 | 54 | func (c *udpMuxedConn) ReadFrom(b []byte) (n int, rAddr net.Addr, err error) { 55 | for { 56 | c.mu.Lock() 57 | if c.bufTail != nil { 58 | pkt := c.bufTail 59 | c.bufTail = pkt.next 60 | 61 | if pkt == c.bufHead { 62 | c.bufHead = nil 63 | } 64 | c.mu.Unlock() 65 | 66 | if len(b) < len(pkt.buf) { 67 | err = io.ErrShortBuffer 68 | } else { 69 | n = copy(b, pkt.buf) 70 | rAddr = pkt.addr 71 | } 72 | 73 | pkt.reset() 74 | c.params.AddrPool.Put(pkt) 75 | 76 | return n, rAddr, err 77 | } 78 | 79 | if c.state == udpMuxedConnClosed { 80 | c.mu.Unlock() 81 | 82 | return 0, nil, io.EOF 83 | } 84 | 85 | c.state = udpMuxedConnWaiting 86 | c.mu.Unlock() 87 | 88 | select { 89 | case <-c.notify: 90 | case <-c.closedChan: 91 | return 0, nil, io.EOF 92 | } 93 | } 94 | } 95 | 96 | func (c *udpMuxedConn) WriteTo(buf []byte, rAddr net.Addr) (n int, err error) { 97 | if c.isClosed() { 98 | return 0, io.ErrClosedPipe 99 | } 100 | // Each time we write to a new address, we'll register it with the mux 101 | netUDPAddr, ok := rAddr.(*net.UDPAddr) 102 | if !ok { 103 | return 0, errFailedToCastUDPAddr 104 | } 105 | 106 | port := netUDPAddr.Port 107 | if port < 0 || port > 0xFFFF { 108 | return 0, ErrPort 109 | } 110 | ipAndPort, err := newIPPort(netUDPAddr.IP, netUDPAddr.Zone, uint16(port)) 111 | if err != nil { 112 | return 0, err 113 | } 114 | if !c.containsAddress(ipAndPort) { 115 | c.addAddress(ipAndPort) 116 | } 117 | 118 | return c.params.Mux.writeTo(buf, rAddr) 119 | } 120 | 121 | func (c *udpMuxedConn) LocalAddr() net.Addr { 122 | return c.params.LocalAddr 123 | } 124 | 125 | func (c *udpMuxedConn) SetDeadline(time.Time) error { 126 | return nil 127 | } 128 | 129 | func (c *udpMuxedConn) SetReadDeadline(time.Time) error { 130 | return nil 131 | } 132 | 133 | func (c *udpMuxedConn) SetWriteDeadline(time.Time) error { 134 | return nil 135 | } 136 | 137 | func (c *udpMuxedConn) CloseChannel() <-chan struct{} { 138 | return c.closedChan 139 | } 140 | 141 | func (c *udpMuxedConn) Close() error { 142 | c.mu.Lock() 143 | defer c.mu.Unlock() 144 | if c.state != udpMuxedConnClosed { 145 | for pkt := c.bufTail; pkt != nil; { 146 | next := pkt.next 147 | 148 | pkt.reset() 149 | c.params.AddrPool.Put(pkt) 150 | 151 | pkt = next 152 | } 153 | c.bufHead = nil 154 | c.bufTail = nil 155 | 156 | c.state = udpMuxedConnClosed 157 | close(c.closedChan) 158 | } 159 | 160 | return nil 161 | } 162 | 163 | func (c *udpMuxedConn) isClosed() bool { 164 | c.mu.Lock() 165 | defer c.mu.Unlock() 166 | 167 | return c.state == udpMuxedConnClosed 168 | } 169 | 170 | func (c *udpMuxedConn) getAddresses() []ipPort { 171 | c.mu.Lock() 172 | defer c.mu.Unlock() 173 | addresses := make([]ipPort, len(c.addresses)) 174 | copy(addresses, c.addresses) 175 | 176 | return addresses 177 | } 178 | 179 | func (c *udpMuxedConn) addAddress(addr ipPort) { 180 | c.mu.Lock() 181 | c.addresses = append(c.addresses, addr) 182 | c.mu.Unlock() 183 | 184 | // Map it on mux 185 | c.params.Mux.registerConnForAddress(c, addr) 186 | } 187 | 188 | func (c *udpMuxedConn) removeAddress(addr ipPort) { 189 | c.mu.Lock() 190 | defer c.mu.Unlock() 191 | 192 | newAddresses := make([]ipPort, 0, len(c.addresses)) 193 | for _, a := range c.addresses { 194 | if a != addr { 195 | newAddresses = append(newAddresses, a) 196 | } 197 | } 198 | 199 | c.addresses = newAddresses 200 | } 201 | 202 | func (c *udpMuxedConn) containsAddress(addr ipPort) bool { 203 | c.mu.Lock() 204 | defer c.mu.Unlock() 205 | 206 | return slices.Contains(c.addresses, addr) 207 | } 208 | 209 | func (c *udpMuxedConn) writePacket(data []byte, addr *net.UDPAddr) error { 210 | pkt := c.params.AddrPool.Get().(*bufferHolder) //nolint:forcetypeassert 211 | if cap(pkt.buf) < len(data) { 212 | c.params.AddrPool.Put(pkt) 213 | 214 | return io.ErrShortBuffer 215 | } 216 | 217 | pkt.buf = append(pkt.buf[:0], data...) 218 | pkt.addr = addr 219 | 220 | c.mu.Lock() 221 | if c.state == udpMuxedConnClosed { 222 | c.mu.Unlock() 223 | 224 | pkt.reset() 225 | c.params.AddrPool.Put(pkt) 226 | 227 | return io.ErrClosedPipe 228 | } 229 | 230 | if c.bufHead != nil { 231 | c.bufHead.next = pkt 232 | } 233 | c.bufHead = pkt 234 | 235 | if c.bufTail == nil { 236 | c.bufTail = pkt 237 | } 238 | 239 | state := c.state 240 | c.state = udpMuxedConnOpen 241 | c.mu.Unlock() 242 | 243 | if state == udpMuxedConnWaiting { 244 | select { 245 | case c.notify <- struct{}{}: 246 | default: 247 | } 248 | } 249 | 250 | return nil 251 | } 252 | -------------------------------------------------------------------------------- /mdns_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | //go:build !js 5 | // +build !js 6 | 7 | package ice 8 | 9 | import ( 10 | "context" 11 | "regexp" 12 | "testing" 13 | "time" 14 | 15 | "github.com/pion/transport/v3/test" 16 | "github.com/stretchr/testify/require" 17 | ) 18 | 19 | func TestMulticastDNSOnlyConnection(t *testing.T) { 20 | defer test.CheckRoutines(t)() 21 | 22 | // Limit runtime in case of deadlocks 23 | defer test.TimeOut(time.Second * 30).Stop() 24 | 25 | type testCase struct { 26 | Name string 27 | NetworkTypes []NetworkType 28 | } 29 | 30 | testCases := []testCase{ 31 | {Name: "UDP4", NetworkTypes: []NetworkType{NetworkTypeUDP4}}, 32 | } 33 | 34 | if ipv6Available(t) { 35 | testCases = append(testCases, 36 | testCase{Name: "UDP6", NetworkTypes: []NetworkType{NetworkTypeUDP6}}, 37 | testCase{Name: "UDP46", NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}}, 38 | ) 39 | } 40 | 41 | for _, tc := range testCases { 42 | t.Run(tc.Name, func(t *testing.T) { 43 | cfg := &AgentConfig{ 44 | NetworkTypes: tc.NetworkTypes, 45 | CandidateTypes: []CandidateType{CandidateTypeHost}, 46 | MulticastDNSMode: MulticastDNSModeQueryAndGather, 47 | InterfaceFilter: problematicNetworkInterfaces, 48 | } 49 | 50 | aAgent, err := NewAgent(cfg) 51 | require.NoError(t, err) 52 | defer func() { 53 | require.NoError(t, aAgent.Close()) 54 | }() 55 | 56 | aNotifier, aConnected := onConnected() 57 | require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) 58 | 59 | bAgent, err := NewAgent(cfg) 60 | require.NoError(t, err) 61 | defer func() { 62 | require.NoError(t, bAgent.Close()) 63 | }() 64 | 65 | bNotifier, bConnected := onConnected() 66 | require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) 67 | 68 | connect(t, aAgent, bAgent) 69 | <-aConnected 70 | <-bConnected 71 | }) 72 | } 73 | } 74 | 75 | func TestMulticastDNSMixedConnection(t *testing.T) { 76 | defer test.CheckRoutines(t)() 77 | 78 | // Limit runtime in case of deadlocks 79 | defer test.TimeOut(time.Second * 30).Stop() 80 | 81 | type testCase struct { 82 | Name string 83 | NetworkTypes []NetworkType 84 | } 85 | 86 | testCases := []testCase{ 87 | {Name: "UDP4", NetworkTypes: []NetworkType{NetworkTypeUDP4}}, 88 | } 89 | 90 | if ipv6Available(t) { 91 | testCases = append(testCases, 92 | testCase{Name: "UDP6", NetworkTypes: []NetworkType{NetworkTypeUDP6}}, 93 | testCase{Name: "UDP46", NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}}, 94 | ) 95 | } 96 | 97 | for _, tc := range testCases { 98 | t.Run(tc.Name, func(t *testing.T) { 99 | aAgent, err := NewAgent(&AgentConfig{ 100 | NetworkTypes: tc.NetworkTypes, 101 | CandidateTypes: []CandidateType{CandidateTypeHost}, 102 | MulticastDNSMode: MulticastDNSModeQueryAndGather, 103 | InterfaceFilter: problematicNetworkInterfaces, 104 | }) 105 | require.NoError(t, err) 106 | defer func() { 107 | require.NoError(t, aAgent.Close()) 108 | }() 109 | 110 | aNotifier, aConnected := onConnected() 111 | require.NoError(t, aAgent.OnConnectionStateChange(aNotifier)) 112 | 113 | bAgent, err := NewAgent(&AgentConfig{ 114 | NetworkTypes: tc.NetworkTypes, 115 | CandidateTypes: []CandidateType{CandidateTypeHost}, 116 | MulticastDNSMode: MulticastDNSModeQueryOnly, 117 | InterfaceFilter: problematicNetworkInterfaces, 118 | }) 119 | require.NoError(t, err) 120 | defer func() { 121 | require.NoError(t, bAgent.Close()) 122 | }() 123 | 124 | bNotifier, bConnected := onConnected() 125 | require.NoError(t, bAgent.OnConnectionStateChange(bNotifier)) 126 | 127 | connect(t, aAgent, bAgent) 128 | <-aConnected 129 | <-bConnected 130 | }) 131 | } 132 | } 133 | 134 | func TestMulticastDNSStaticHostName(t *testing.T) { 135 | defer test.CheckRoutines(t)() 136 | 137 | defer test.TimeOut(time.Second * 30).Stop() 138 | 139 | type testCase struct { 140 | Name string 141 | NetworkTypes []NetworkType 142 | } 143 | 144 | testCases := []testCase{ 145 | {Name: "UDP4", NetworkTypes: []NetworkType{NetworkTypeUDP4}}, 146 | } 147 | 148 | if ipv6Available(t) { 149 | testCases = append(testCases, 150 | testCase{Name: "UDP6", NetworkTypes: []NetworkType{NetworkTypeUDP6}}, 151 | testCase{Name: "UDP46", NetworkTypes: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}}, 152 | ) 153 | } 154 | 155 | for _, tc := range testCases { 156 | t.Run(tc.Name, func(t *testing.T) { 157 | _, err := NewAgent(&AgentConfig{ 158 | NetworkTypes: tc.NetworkTypes, 159 | CandidateTypes: []CandidateType{CandidateTypeHost}, 160 | MulticastDNSMode: MulticastDNSModeQueryAndGather, 161 | MulticastDNSHostName: "invalidHostName", 162 | InterfaceFilter: problematicNetworkInterfaces, 163 | }) 164 | require.Equal(t, err, ErrInvalidMulticastDNSHostName) 165 | 166 | agent, err := NewAgent(&AgentConfig{ 167 | NetworkTypes: tc.NetworkTypes, 168 | CandidateTypes: []CandidateType{CandidateTypeHost}, 169 | MulticastDNSMode: MulticastDNSModeQueryAndGather, 170 | MulticastDNSHostName: "validName.local", 171 | InterfaceFilter: problematicNetworkInterfaces, 172 | }) 173 | require.NoError(t, err) 174 | defer func() { 175 | require.NoError(t, agent.Close()) 176 | }() 177 | 178 | correctHostName, resolveFunc := context.WithCancel(context.Background()) 179 | require.NoError(t, agent.OnCandidate(func(c Candidate) { 180 | if c != nil && c.Address() == "validName.local" { 181 | resolveFunc() 182 | } 183 | })) 184 | 185 | require.NoError(t, agent.GatherCandidates()) 186 | <-correctHostName.Done() 187 | }) 188 | } 189 | } 190 | 191 | func TestGenerateMulticastDNSName(t *testing.T) { 192 | name, err := generateMulticastDNSName() 193 | require.NoError(t, err) 194 | isMDNSName := regexp.MustCompile( 195 | `^[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+$`, 196 | ).MatchString 197 | 198 | require.True(t, isMDNSName(name)) 199 | } 200 | -------------------------------------------------------------------------------- /agent_stats.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "context" 8 | "time" 9 | ) 10 | 11 | // GetCandidatePairsStats returns a list of candidate pair stats. 12 | func (a *Agent) GetCandidatePairsStats() []CandidatePairStats { 13 | var res []CandidatePairStats 14 | err := a.loop.Run(a.loop, func(_ context.Context) { 15 | result := make([]CandidatePairStats, 0, len(a.checklist)) 16 | for _, cp := range a.checklist { 17 | stat := CandidatePairStats{ 18 | Timestamp: time.Now(), 19 | LocalCandidateID: cp.Local.ID(), 20 | RemoteCandidateID: cp.Remote.ID(), 21 | State: cp.state, 22 | Nominated: cp.nominated, 23 | PacketsSent: cp.PacketsSent(), 24 | PacketsReceived: cp.PacketsReceived(), 25 | BytesSent: cp.BytesSent(), 26 | BytesReceived: cp.BytesReceived(), 27 | LastPacketSentTimestamp: cp.LastPacketSentAt(), 28 | LastPacketReceivedTimestamp: cp.LastPacketReceivedAt(), 29 | FirstRequestTimestamp: cp.FirstRequestSentAt(), 30 | LastRequestTimestamp: cp.LastRequestSentAt(), 31 | FirstResponseTimestamp: cp.FirstResponseReceivedAt(), 32 | LastResponseTimestamp: cp.LastResponseReceivedAt(), 33 | FirstRequestReceivedTimestamp: cp.FirstRequestReceivedAt(), 34 | LastRequestReceivedTimestamp: cp.LastRequestReceivedAt(), 35 | 36 | TotalRoundTripTime: cp.TotalRoundTripTime(), 37 | CurrentRoundTripTime: cp.CurrentRoundTripTime(), 38 | // AvailableOutgoingBitrate float64 39 | // AvailableIncomingBitrate float64 40 | // CircuitBreakerTriggerCount uint32 41 | RequestsReceived: cp.RequestsReceived(), 42 | RequestsSent: cp.RequestsSent(), 43 | ResponsesReceived: cp.ResponsesReceived(), 44 | ResponsesSent: cp.ResponsesSent(), 45 | // RetransmissionsReceived uint64 46 | // RetransmissionsSent uint64 47 | // ConsentRequestsSent uint64 48 | // ConsentExpiredTimestamp time.Time 49 | } 50 | result = append(result, stat) 51 | } 52 | res = result 53 | }) 54 | if err != nil { 55 | a.log.Errorf("Failed to get candidate pairs stats: %v", err) 56 | 57 | return []CandidatePairStats{} 58 | } 59 | 60 | return res 61 | } 62 | 63 | // GetSelectedCandidatePairStats returns a candidate pair stats for selected candidate pair. 64 | // Returns false if there is no selected pair. 65 | func (a *Agent) GetSelectedCandidatePairStats() (CandidatePairStats, bool) { 66 | isAvailable := false 67 | var res CandidatePairStats 68 | err := a.loop.Run(a.loop, func(_ context.Context) { 69 | sp := a.getSelectedPair() 70 | if sp == nil { 71 | return 72 | } 73 | 74 | isAvailable = true 75 | res = CandidatePairStats{ 76 | Timestamp: time.Now(), 77 | LocalCandidateID: sp.Local.ID(), 78 | RemoteCandidateID: sp.Remote.ID(), 79 | State: sp.state, 80 | Nominated: sp.nominated, 81 | PacketsSent: sp.PacketsSent(), 82 | PacketsReceived: sp.PacketsReceived(), 83 | BytesSent: sp.BytesSent(), 84 | BytesReceived: sp.BytesReceived(), 85 | LastPacketSentTimestamp: sp.LastPacketSentAt(), 86 | LastPacketReceivedTimestamp: sp.LastPacketReceivedAt(), 87 | // FirstRequestTimestamp time.Time 88 | // LastRequestTimestamp time.Time 89 | // LastResponseTimestamp time.Time 90 | TotalRoundTripTime: sp.TotalRoundTripTime(), 91 | CurrentRoundTripTime: sp.CurrentRoundTripTime(), 92 | // AvailableOutgoingBitrate float64 93 | // AvailableIncomingBitrate float64 94 | // CircuitBreakerTriggerCount uint32 95 | // RequestsReceived uint64 96 | // RequestsSent uint64 97 | ResponsesReceived: sp.ResponsesReceived(), 98 | // ResponsesSent uint64 99 | // RetransmissionsReceived uint64 100 | // RetransmissionsSent uint64 101 | // ConsentRequestsSent uint64 102 | // ConsentExpiredTimestamp time.Time 103 | } 104 | }) 105 | if err != nil { 106 | a.log.Errorf("Failed to get selected candidate pair stats: %v", err) 107 | 108 | return CandidatePairStats{}, false 109 | } 110 | 111 | return res, isAvailable 112 | } 113 | 114 | // GetLocalCandidatesStats returns a list of local candidates stats. 115 | func (a *Agent) GetLocalCandidatesStats() []CandidateStats { 116 | return a.getCandidatesStats(true) 117 | } 118 | 119 | // GetRemoteCandidatesStats returns a list of remote candidates stats. 120 | func (a *Agent) GetRemoteCandidatesStats() []CandidateStats { 121 | return a.getCandidatesStats(false) 122 | } 123 | 124 | // getCandidatesStats returns a list of candidates stats. 125 | func (a *Agent) getCandidatesStats(isLocal bool) []CandidateStats { 126 | var res []CandidateStats 127 | err := a.loop.Run(a.loop, func(_ context.Context) { 128 | var candidateMap map[NetworkType][]Candidate 129 | if isLocal { 130 | candidateMap = a.localCandidates 131 | } else { 132 | candidateMap = a.remoteCandidates 133 | } 134 | 135 | result := make([]CandidateStats, 0, len(candidateMap)) 136 | for networkType, candidate := range candidateMap { 137 | for _, cand := range candidate { 138 | relayProtocol := "" 139 | 140 | if isLocal && cand.Type() == CandidateTypeRelay { 141 | if cRelay, ok := cand.(*CandidateRelay); ok { 142 | relayProtocol = cRelay.RelayProtocol() 143 | } 144 | } 145 | 146 | stat := CandidateStats{ 147 | Timestamp: time.Now(), 148 | ID: cand.ID(), 149 | NetworkType: networkType, 150 | IP: cand.Address(), 151 | Port: cand.Port(), 152 | CandidateType: cand.Type(), 153 | Priority: cand.Priority(), 154 | // URL string 155 | RelayProtocol: relayProtocol, 156 | } 157 | result = append(result, stat) 158 | } 159 | } 160 | res = result 161 | }) 162 | if err != nil { 163 | a.log.Errorf("Failed to get candidate pair stats: %v", err) 164 | 165 | return []CandidateStats{} 166 | } 167 | 168 | return res 169 | } 170 | -------------------------------------------------------------------------------- /udp_mux_multi.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "fmt" 8 | "net" 9 | 10 | "github.com/pion/logging" 11 | "github.com/pion/transport/v3" 12 | "github.com/pion/transport/v3/stdnet" 13 | ) 14 | 15 | // MultiUDPMuxDefault implements both UDPMux and AllConnsGetter, 16 | // allowing users to pass multiple UDPMux instances to the ICE agent 17 | // configuration. 18 | type MultiUDPMuxDefault struct { 19 | muxes []UDPMux 20 | localAddrToMux map[string]UDPMux 21 | } 22 | 23 | // NewMultiUDPMuxDefault creates an instance of MultiUDPMuxDefault that 24 | // uses the provided UDPMux instances. 25 | func NewMultiUDPMuxDefault(muxes ...UDPMux) *MultiUDPMuxDefault { 26 | addrToMux := make(map[string]UDPMux) 27 | for _, mux := range muxes { 28 | for _, addr := range mux.GetListenAddresses() { 29 | addrToMux[addr.String()] = mux 30 | } 31 | } 32 | 33 | return &MultiUDPMuxDefault{ 34 | muxes: muxes, 35 | localAddrToMux: addrToMux, 36 | } 37 | } 38 | 39 | // GetConn returns a PacketConn given the connection's ufrag and network 40 | // creates the connection if an existing one can't be found. 41 | func (m *MultiUDPMuxDefault) GetConn(ufrag string, addr net.Addr) (net.PacketConn, error) { 42 | mux, ok := m.localAddrToMux[addr.String()] 43 | if !ok { 44 | return nil, errNoUDPMuxAvailable 45 | } 46 | 47 | return mux.GetConn(ufrag, addr) 48 | } 49 | 50 | // RemoveConnByUfrag stops and removes the muxed packet connection 51 | // from all underlying UDPMux instances. 52 | func (m *MultiUDPMuxDefault) RemoveConnByUfrag(ufrag string) { 53 | for _, mux := range m.muxes { 54 | mux.RemoveConnByUfrag(ufrag) 55 | } 56 | } 57 | 58 | // Close the multi mux, no further connections could be created. 59 | func (m *MultiUDPMuxDefault) Close() error { 60 | var err error 61 | for _, mux := range m.muxes { 62 | if e := mux.Close(); e != nil { 63 | err = e 64 | } 65 | } 66 | 67 | return err 68 | } 69 | 70 | // GetListenAddresses returns the list of addresses that this mux is listening on. 71 | func (m *MultiUDPMuxDefault) GetListenAddresses() []net.Addr { 72 | addrs := make([]net.Addr, 0, len(m.localAddrToMux)) 73 | for _, mux := range m.muxes { 74 | addrs = append(addrs, mux.GetListenAddresses()...) 75 | } 76 | 77 | return addrs 78 | } 79 | 80 | // NewMultiUDPMuxFromPort creates an instance of MultiUDPMuxDefault that 81 | // listen all interfaces on the provided port. 82 | func NewMultiUDPMuxFromPort(port int, opts ...UDPMuxFromPortOption) (*MultiUDPMuxDefault, error) { //nolint:cyclop 83 | params := multiUDPMuxFromPortParam{ 84 | networks: []NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}, 85 | } 86 | for _, opt := range opts { 87 | opt.apply(¶ms) 88 | } 89 | 90 | if params.net == nil { 91 | var err error 92 | if params.net, err = stdnet.NewNet(); err != nil { 93 | return nil, fmt.Errorf("failed to get create network: %w", err) 94 | } 95 | } 96 | 97 | _, addrs, err := localInterfaces(params.net, params.ifFilter, params.ipFilter, params.networks, params.includeLoopback) 98 | if err != nil { 99 | return nil, err 100 | } 101 | 102 | conns := make([]net.PacketConn, 0, len(addrs)) 103 | for _, addr := range addrs { 104 | conn, listenErr := params.net.ListenUDP("udp", &net.UDPAddr{ 105 | IP: addr.addr.AsSlice(), 106 | Port: port, 107 | Zone: addr.addr.Zone(), 108 | }) 109 | if listenErr != nil { 110 | err = listenErr 111 | 112 | break 113 | } 114 | if params.readBufferSize > 0 { 115 | _ = conn.SetReadBuffer(params.readBufferSize) 116 | } 117 | if params.writeBufferSize > 0 { 118 | _ = conn.SetWriteBuffer(params.writeBufferSize) 119 | } 120 | conns = append(conns, conn) 121 | } 122 | 123 | if err != nil { 124 | for _, conn := range conns { 125 | _ = conn.Close() 126 | } 127 | 128 | return nil, err 129 | } 130 | 131 | muxes := make([]UDPMux, 0, len(conns)) 132 | for _, conn := range conns { 133 | mux := NewUDPMuxDefault(UDPMuxParams{ 134 | Logger: params.logger, 135 | UDPConn: conn, 136 | Net: params.net, 137 | }) 138 | muxes = append(muxes, mux) 139 | } 140 | 141 | return NewMultiUDPMuxDefault(muxes...), nil 142 | } 143 | 144 | // UDPMuxFromPortOption provide options for NewMultiUDPMuxFromPort. 145 | type UDPMuxFromPortOption interface { 146 | apply(*multiUDPMuxFromPortParam) 147 | } 148 | 149 | type multiUDPMuxFromPortParam struct { 150 | ifFilter func(string) (keep bool) 151 | ipFilter func(ip net.IP) (keep bool) 152 | networks []NetworkType 153 | readBufferSize int 154 | writeBufferSize int 155 | logger logging.LeveledLogger 156 | includeLoopback bool 157 | net transport.Net 158 | } 159 | 160 | type udpMuxFromPortOption struct { 161 | f func(*multiUDPMuxFromPortParam) 162 | } 163 | 164 | func (o *udpMuxFromPortOption) apply(p *multiUDPMuxFromPortParam) { 165 | o.f(p) 166 | } 167 | 168 | // UDPMuxFromPortWithInterfaceFilter set the filter to filter out interfaces that should not be used. 169 | func UDPMuxFromPortWithInterfaceFilter(f func(string) (keep bool)) UDPMuxFromPortOption { 170 | return &udpMuxFromPortOption{ 171 | f: func(p *multiUDPMuxFromPortParam) { 172 | p.ifFilter = f 173 | }, 174 | } 175 | } 176 | 177 | // UDPMuxFromPortWithIPFilter set the filter to filter out IP addresses that should not be used. 178 | func UDPMuxFromPortWithIPFilter(f func(ip net.IP) (keep bool)) UDPMuxFromPortOption { 179 | return &udpMuxFromPortOption{ 180 | f: func(p *multiUDPMuxFromPortParam) { 181 | p.ipFilter = f 182 | }, 183 | } 184 | } 185 | 186 | // UDPMuxFromPortWithNetworks set the networks that should be used. default is both IPv4 and IPv6. 187 | func UDPMuxFromPortWithNetworks(networks ...NetworkType) UDPMuxFromPortOption { 188 | return &udpMuxFromPortOption{ 189 | f: func(p *multiUDPMuxFromPortParam) { 190 | p.networks = networks 191 | }, 192 | } 193 | } 194 | 195 | // UDPMuxFromPortWithReadBufferSize set the UDP connection read buffer size. 196 | func UDPMuxFromPortWithReadBufferSize(size int) UDPMuxFromPortOption { 197 | return &udpMuxFromPortOption{ 198 | f: func(p *multiUDPMuxFromPortParam) { 199 | p.readBufferSize = size 200 | }, 201 | } 202 | } 203 | 204 | // UDPMuxFromPortWithWriteBufferSize set the UDP connection write buffer size. 205 | func UDPMuxFromPortWithWriteBufferSize(size int) UDPMuxFromPortOption { 206 | return &udpMuxFromPortOption{ 207 | f: func(p *multiUDPMuxFromPortParam) { 208 | p.writeBufferSize = size 209 | }, 210 | } 211 | } 212 | 213 | // UDPMuxFromPortWithLogger set the logger for the created UDPMux. 214 | func UDPMuxFromPortWithLogger(logger logging.LeveledLogger) UDPMuxFromPortOption { 215 | return &udpMuxFromPortOption{ 216 | f: func(p *multiUDPMuxFromPortParam) { 217 | p.logger = logger 218 | }, 219 | } 220 | } 221 | 222 | // UDPMuxFromPortWithLoopback set loopback interface should be included. 223 | func UDPMuxFromPortWithLoopback() UDPMuxFromPortOption { 224 | return &udpMuxFromPortOption{ 225 | f: func(p *multiUDPMuxFromPortParam) { 226 | p.includeLoopback = true 227 | }, 228 | } 229 | } 230 | 231 | // UDPMuxFromPortWithNet sets the network transport to use. 232 | func UDPMuxFromPortWithNet(n transport.Net) UDPMuxFromPortOption { 233 | return &udpMuxFromPortOption{ 234 | f: func(p *multiUDPMuxFromPortParam) { 235 | p.net = n 236 | }, 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /net_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "errors" 8 | "net" 9 | "net/netip" 10 | "sort" 11 | "strings" 12 | "testing" 13 | 14 | "github.com/pion/logging" 15 | "github.com/pion/transport/v3" 16 | "github.com/pion/transport/v3/stdnet" 17 | "github.com/stretchr/testify/require" 18 | ) 19 | 20 | func TestIsSupportedIPv6Partial(t *testing.T) { 21 | require.False(t, isSupportedIPv6Partial(net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1})) 22 | require.False(t, isSupportedIPv6Partial(net.ParseIP("fec0::2333"))) 23 | require.True(t, isSupportedIPv6Partial(net.ParseIP("fe80::2333"))) 24 | require.True(t, isSupportedIPv6Partial(net.ParseIP("ff02::2333"))) 25 | require.True(t, isSupportedIPv6Partial(net.ParseIP("2001::1"))) 26 | } 27 | 28 | func TestCreateAddr(t *testing.T) { 29 | ipv4 := mustAddr(t, net.IP{127, 0, 0, 1}) 30 | ipv6 := mustAddr(t, net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}) 31 | port := 9000 32 | 33 | require.Equal(t, &net.UDPAddr{IP: ipv4.AsSlice(), Port: port}, createAddr(NetworkTypeUDP4, ipv4, port)) 34 | require.Equal(t, &net.UDPAddr{IP: ipv6.AsSlice(), Port: port}, createAddr(NetworkTypeUDP6, ipv6, port)) 35 | require.Equal(t, &net.TCPAddr{IP: ipv4.AsSlice(), Port: port}, createAddr(NetworkTypeTCP4, ipv4, port)) 36 | require.Equal(t, &net.TCPAddr{IP: ipv6.AsSlice(), Port: port}, createAddr(NetworkTypeTCP6, ipv6, port)) 37 | } 38 | 39 | func problematicNetworkInterfaces(s string) (keep bool) { 40 | defaultDockerBridgeNetwork := strings.Contains(s, "docker") 41 | customDockerBridgeNetwork := strings.Contains(s, "br-") 42 | 43 | // Apple filters 44 | accessPoint := strings.Contains(s, "ap") 45 | appleWirelessDirectLink := strings.Contains(s, "awdl") 46 | appleLowLatencyWLANInterface := strings.Contains(s, "llw") 47 | appleTunnelingInterface := strings.Contains(s, "utun") 48 | 49 | return !defaultDockerBridgeNetwork && 50 | !customDockerBridgeNetwork && 51 | !accessPoint && 52 | !appleWirelessDirectLink && 53 | !appleLowLatencyWLANInterface && 54 | !appleTunnelingInterface 55 | } 56 | 57 | func mustAddr(t *testing.T, ip net.IP) netip.Addr { 58 | t.Helper() 59 | addr, ok := netip.AddrFromSlice(ip) 60 | if !ok { 61 | t.Fatal(ipConvertError{ip}) // nolint 62 | } 63 | 64 | return addr 65 | } 66 | 67 | type errInterfacesNet struct { 68 | transport.Net 69 | retErr error 70 | } 71 | 72 | func (e *errInterfacesNet) Interfaces() ([]*transport.Interface, error) { 73 | return nil, e.retErr 74 | } 75 | 76 | var errBoom = errors.New("boom") 77 | 78 | func TestLocalInterfaces_ErrorFromInterfaces(t *testing.T) { 79 | base, err := stdnet.NewNet() 80 | require.NoError(t, err) 81 | 82 | wrapped := &errInterfacesNet{ 83 | Net: base, 84 | retErr: errBoom, 85 | } 86 | 87 | ifaces, addrs, gotErr := localInterfaces( 88 | wrapped, 89 | nil, 90 | nil, 91 | nil, 92 | false, 93 | ) 94 | 95 | require.ErrorIs(t, gotErr, wrapped.retErr) 96 | require.Nil(t, ifaces, "expected nil iface slice on error") 97 | require.NotNil(t, addrs, "ipAddrs should be a non-nil empty slice") 98 | require.Len(t, addrs, 0) 99 | } 100 | 101 | type fixedInterfacesNet struct { 102 | transport.Net 103 | list []*transport.Interface 104 | } 105 | 106 | func (f *fixedInterfacesNet) Interfaces() ([]*transport.Interface, error) { 107 | return f.list, nil 108 | } 109 | 110 | func TestLocalInterfaces_SkipInterfaceDown(t *testing.T) { 111 | base, err := stdnet.NewNet() 112 | require.NoError(t, err) 113 | 114 | sysIfaces, err := base.Interfaces() 115 | require.NoError(t, err) 116 | if len(sysIfaces) == 0 { 117 | t.Skip("no system network interfaces available") 118 | } 119 | 120 | clone := *sysIfaces[0] 121 | clone.Flags &^= net.FlagUp 122 | 123 | wrapped := &fixedInterfacesNet{ 124 | Net: base, 125 | list: []*transport.Interface{&clone}, 126 | } 127 | 128 | ifcs, addrs, ierr := localInterfaces( 129 | wrapped, 130 | nil, 131 | nil, 132 | nil, 133 | false, 134 | ) 135 | require.NoError(t, ierr) 136 | require.Len(t, ifcs, 0, "down interfaces must be skipped") 137 | require.Len(t, addrs, 0, "no addresses should be collected from a down interface") 138 | } 139 | 140 | func TestLocalInterfaces_SkipLoopbackAddrs_WhenIncludeLoopbackFalse(t *testing.T) { 141 | base, err := stdnet.NewNet() 142 | require.NoError(t, err) 143 | 144 | sysIfaces, err := base.Interfaces() 145 | require.NoError(t, err) 146 | 147 | var loop *transport.Interface 148 | for _, ifc := range sysIfaces { 149 | if ifc.Flags&net.FlagLoopback != 0 { 150 | loop = ifc 151 | 152 | break 153 | } 154 | } 155 | if loop == nil { 156 | t.Skip("no loopback interface found on this system") 157 | } 158 | 159 | // clone the loopback iface and clear the Loopback flag so the outer check 160 | // doesn't drop it to force the inner `(ipAddr.IsLoopback() && !includeLoopback)`. 161 | cloned := *loop 162 | cloned.Flags |= net.FlagUp 163 | cloned.Flags &^= net.FlagLoopback 164 | 165 | wrapped := &fixedInterfacesNet{ 166 | Net: base, 167 | list: []*transport.Interface{&cloned}, 168 | } 169 | 170 | ifaces, addrs, ierr := localInterfaces( 171 | wrapped, 172 | nil, // interfaceFilter 173 | nil, // ipFilter 174 | nil, // networkTypes 175 | false, // includeLoopback 176 | ) 177 | require.NoError(t, ierr) 178 | 179 | // don't assert on the number of interfaces because some systems may 180 | // report the iface as having addresses in a way that causes it to be included. 181 | // assert that all loopback addresses were skipped. 182 | for _, a := range addrs { 183 | require.False(t, a.addr.IsLoopback(), "loopback addresses must be skipped when includeLoopback=false") 184 | } 185 | 186 | _ = ifaces // intentionally don't assert on this, see above comment 187 | } 188 | 189 | // Captures ListenUDP attempts and always fails so the loop exhausts. 190 | type listenUDPCaptor struct { 191 | transport.Net 192 | attempts []int 193 | } 194 | 195 | func (c *listenUDPCaptor) ListenUDP(network string, laddr *net.UDPAddr) (transport.UDPConn, error) { 196 | c.attempts = append(c.attempts, laddr.Port) 197 | 198 | return nil, errBoom 199 | } 200 | 201 | func TestListenUDPInPortRange_DefaultsPortMinTo1024(t *testing.T) { 202 | base, err := stdnet.NewNet() 203 | require.NoError(t, err) 204 | 205 | captor := &listenUDPCaptor{Net: base} 206 | logger := logging.NewDefaultLoggerFactory().NewLogger("ice-test") 207 | 208 | // portMin == 0 (should become 1024), portMax small to keep the loop short. 209 | _, err = listenUDPInPortRange( 210 | captor, 211 | logger, 212 | 1030, // portMax 213 | 0, // portMin -> becomes 1024 214 | udp4, 215 | &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}, 216 | ) 217 | require.ErrorIs(t, err, ErrPort) 218 | 219 | // should have attempted exactly [1024..1030] in some order. 220 | sort.Ints(captor.attempts) 221 | require.Equal(t, []int{1024, 1025, 1026, 1027, 1028, 1029, 1030}, captor.attempts) 222 | } 223 | 224 | func TestListenUDPInPortRange_DefaultsPortMaxToFFFF(t *testing.T) { 225 | base, err := stdnet.NewNet() 226 | require.NoError(t, err) 227 | 228 | captor := &listenUDPCaptor{Net: base} 229 | logger := logging.NewDefaultLoggerFactory().NewLogger("ice-test") 230 | 231 | // portMax == 0 (should become 0xFFFF). Use portMin=65535 so the range is 1 port. 232 | _, err = listenUDPInPortRange( 233 | captor, 234 | logger, 235 | 0, // portMax -> becomes 65535 236 | 65535, // portMin 237 | udp4, 238 | &net.UDPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 0}, 239 | ) 240 | require.ErrorIs(t, err, ErrPort) 241 | 242 | require.Equal(t, []int{65535}, captor.attempts) 243 | } 244 | -------------------------------------------------------------------------------- /stats.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 The Pion community 2 | // SPDX-License-Identifier: MIT 3 | 4 | package ice 5 | 6 | import ( 7 | "time" 8 | ) 9 | 10 | // CandidatePairStats contains ICE candidate pair statistics. 11 | type CandidatePairStats struct { 12 | // Timestamp is the timestamp associated with this object. 13 | Timestamp time.Time 14 | 15 | // LocalCandidateID is the ID of the local candidate 16 | LocalCandidateID string 17 | 18 | // RemoteCandidateID is the ID of the remote candidate 19 | RemoteCandidateID string 20 | 21 | // State represents the state of the checklist for the local and remote 22 | // candidates in a pair. 23 | State CandidatePairState 24 | 25 | // Nominated is true when this valid pair that should be used for media 26 | // if it is the highest-priority one amongst those whose nominated flag is set 27 | Nominated bool 28 | 29 | // PacketsSent represents the total number of packets sent on this candidate pair. 30 | PacketsSent uint32 31 | 32 | // PacketsReceived represents the total number of packets received on this candidate pair. 33 | PacketsReceived uint32 34 | 35 | // BytesSent represents the total number of payload bytes sent on this candidate pair 36 | // not including headers or padding. 37 | BytesSent uint64 38 | 39 | // BytesReceived represents the total number of payload bytes received on this candidate pair 40 | // not including headers or padding. 41 | BytesReceived uint64 42 | 43 | // LastPacketSentTimestamp represents the timestamp at which the last packet was 44 | // sent on this particular candidate pair, excluding STUN packets. 45 | LastPacketSentTimestamp time.Time 46 | 47 | // LastPacketReceivedTimestamp represents the timestamp at which the last packet 48 | // was received on this particular candidate pair, excluding STUN packets. 49 | LastPacketReceivedTimestamp time.Time 50 | 51 | // FirstRequestTimestamp represents the timestamp at which the first STUN request 52 | // was sent on this particular candidate pair. 53 | FirstRequestTimestamp time.Time 54 | 55 | // LastRequestTimestamp represents the timestamp at which the last STUN request 56 | // was sent on this particular candidate pair. The average interval between two 57 | // consecutive connectivity checks sent can be calculated with 58 | // (LastRequestTimestamp - FirstRequestTimestamp) / RequestsSent. 59 | LastRequestTimestamp time.Time 60 | 61 | // FirstResponseTimestamp represents the timestamp at which the first STUN response 62 | // was received on this particular candidate pair. 63 | FirstResponseTimestamp time.Time 64 | 65 | // LastResponseTimestamp represents the timestamp at which the last STUN response 66 | // was received on this particular candidate pair. 67 | LastResponseTimestamp time.Time 68 | 69 | // FirstRequestReceivedTimestamp represents the timestamp at which the first 70 | // connectivity check request was received. 71 | FirstRequestReceivedTimestamp time.Time 72 | 73 | // LastRequestReceivedTimestamp represents the timestamp at which the last 74 | // connectivity check request was received. 75 | LastRequestReceivedTimestamp time.Time 76 | 77 | // TotalRoundTripTime represents the sum of all round trip time measurements 78 | // in seconds since the beginning of the session, based on STUN connectivity 79 | // check responses (ResponsesReceived), including those that reply to requests 80 | // that are sent in order to verify consent. The average round trip time can 81 | // be computed from TotalRoundTripTime by dividing it by ResponsesReceived. 82 | TotalRoundTripTime float64 83 | 84 | // CurrentRoundTripTime represents the latest round trip time measured in seconds, 85 | // computed from both STUN connectivity checks, including those that are sent 86 | // for consent verification. 87 | CurrentRoundTripTime float64 88 | 89 | // AvailableOutgoingBitrate is calculated by the underlying congestion control 90 | // by combining the available bitrate for all the outgoing RTP streams using 91 | // this candidate pair. The bitrate measurement does not count the size of the 92 | // IP or other transport layers like TCP or UDP. It is similar to the TIAS defined 93 | // in RFC 3890, i.e., it is measured in bits per second and the bitrate is calculated 94 | // over a 1 second window. 95 | AvailableOutgoingBitrate float64 96 | 97 | // AvailableIncomingBitrate is calculated by the underlying congestion control 98 | // by combining the available bitrate for all the incoming RTP streams using 99 | // this candidate pair. The bitrate measurement does not count the size of the 100 | // IP or other transport layers like TCP or UDP. It is similar to the TIAS defined 101 | // in RFC 3890, i.e., it is measured in bits per second and the bitrate is 102 | // calculated over a 1 second window. 103 | AvailableIncomingBitrate float64 104 | 105 | // CircuitBreakerTriggerCount represents the number of times the circuit breaker 106 | // is triggered for this particular 5-tuple, ceasing transmission. 107 | CircuitBreakerTriggerCount uint32 108 | 109 | // RequestsReceived represents the total number of connectivity check requests 110 | // received (including retransmissions). It is impossible for the receiver to 111 | // tell whether the request was sent in order to check connectivity or check 112 | // consent, so all connectivity checks requests are counted here. 113 | RequestsReceived uint64 114 | 115 | // RequestsSent represents the total number of connectivity check requests 116 | // sent (not including retransmissions). 117 | RequestsSent uint64 118 | 119 | // ResponsesReceived represents the total number of connectivity check responses received. 120 | ResponsesReceived uint64 121 | 122 | // ResponsesSent represents the total number of connectivity check responses sent. 123 | // Since we cannot distinguish connectivity check requests and consent requests, 124 | // all responses are counted. 125 | ResponsesSent uint64 126 | 127 | // RetransmissionsReceived represents the total number of connectivity check 128 | // request retransmissions received. 129 | RetransmissionsReceived uint64 130 | 131 | // RetransmissionsSent represents the total number of connectivity check 132 | // request retransmissions sent. 133 | RetransmissionsSent uint64 134 | 135 | // ConsentRequestsSent represents the total number of consent requests sent. 136 | ConsentRequestsSent uint64 137 | 138 | // ConsentExpiredTimestamp represents the timestamp at which the latest valid 139 | // STUN binding response expired. 140 | ConsentExpiredTimestamp time.Time 141 | } 142 | 143 | // CandidateStats contains ICE candidate statistics related to the ICETransport objects. 144 | type CandidateStats struct { 145 | // Timestamp is the timestamp associated with this object. 146 | Timestamp time.Time 147 | 148 | // ID is the candidate ID 149 | ID string 150 | 151 | // NetworkType represents the type of network interface used by the base of a 152 | // local candidate (the address the ICE agent sends from). Only present for 153 | // local candidates; it's not possible to know what type of network interface 154 | // a remote candidate is using. 155 | // 156 | // Note: 157 | // This stat only tells you about the network interface used by the first "hop"; 158 | // it's possible that a connection will be bottlenecked by another type of network. 159 | // For example, when using Wi-Fi tethering, the networkType of the relevant candidate 160 | // would be "wifi", even when the next hop is over a cellular connection. 161 | NetworkType NetworkType 162 | 163 | // IP is the IP address of the candidate, allowing for IPv4 addresses and 164 | // IPv6 addresses, but fully qualified domain names (FQDNs) are not allowed. 165 | IP string 166 | 167 | // Port is the port number of the candidate. 168 | Port int 169 | 170 | // CandidateType is the "Type" field of the ICECandidate. 171 | CandidateType CandidateType 172 | 173 | // Priority is the "Priority" field of the ICECandidate. 174 | Priority uint32 175 | 176 | // URL is the URL of the TURN or STUN server indicated in the that translated 177 | // this IP address. It is the URL address surfaced in an PeerConnectionICEEvent. 178 | URL string 179 | 180 | // RelayProtocol is the protocol used by the endpoint to communicate with the 181 | // TURN server. This is only present for local candidates. Valid values for 182 | // the TURN URL protocol is one of UDP, TCP, or TLS. 183 | RelayProtocol string 184 | 185 | // Deleted is true if the candidate has been deleted/freed. For host candidates, 186 | // this means that any network resources (typically a socket) associated with the 187 | // candidate have been released. For TURN candidates, this means the TURN allocation 188 | // is no longer active. 189 | // 190 | // Only defined for local candidates. For remote candidates, this property is not applicable. 191 | Deleted bool 192 | } 193 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 The Pion community 2 | # SPDX-License-Identifier: MIT 3 | 4 | version: "2" 5 | linters: 6 | enable: 7 | - asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers 8 | - bidichk # Checks for dangerous unicode character sequences 9 | - bodyclose # checks whether HTTP response body is closed successfully 10 | - containedctx # containedctx is a linter that detects struct contained context.Context field 11 | - contextcheck # check the function whether use a non-inherited context 12 | - cyclop # checks function and package cyclomatic complexity 13 | - decorder # check declaration order and count of types, constants, variables and functions 14 | - dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f()) 15 | - dupl # Tool for code clone detection 16 | - durationcheck # check for two durations multiplied together 17 | - err113 # Golang linter to check the errors handling expressions 18 | - errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases 19 | - 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. 20 | - errname # Checks that sentinel errors are prefixed with the `Err` and error types are suffixed with the `Error`. 21 | - 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. 22 | - exhaustive # check exhaustiveness of enum switch statements 23 | - forbidigo # Forbids identifiers 24 | - forcetypeassert # finds forced type assertions 25 | - gochecknoglobals # Checks that no globals are present in Go code 26 | - gocognit # Computes and checks the cognitive complexity of functions 27 | - goconst # Finds repeated strings that could be replaced by a constant 28 | - gocritic # The most opinionated Go source code linter 29 | - gocyclo # Computes and checks the cyclomatic complexity of functions 30 | - godot # Check if comments end in a period 31 | - godox # Tool for detection of FIXME, TODO and other comment keywords 32 | - goheader # Checks is file header matches to pattern 33 | - gomoddirectives # Manage the use of 'replace', 'retract', and 'excludes' directives in go.mod. 34 | - goprintffuncname # Checks that printf-like functions are named with `f` at the end 35 | - gosec # Inspects source code for security problems 36 | - govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string 37 | - grouper # An analyzer to analyze expression groups. 38 | - importas # Enforces consistent import aliases 39 | - ineffassign # Detects when assignments to existing variables are not used 40 | - lll # Reports long lines 41 | - maintidx # maintidx measures the maintainability index of each function. 42 | - makezero # Finds slice declarations with non-zero initial length 43 | - misspell # Finds commonly misspelled English words in comments 44 | - nakedret # Finds naked returns in functions greater than a specified function length 45 | - nestif # Reports deeply nested if statements 46 | - nilerr # Finds the code that returns nil even if it checks that the error is not nil. 47 | - nilnil # Checks that there is no simultaneous return of `nil` error and an invalid value. 48 | - nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity 49 | - noctx # noctx finds sending http request without context.Context 50 | - predeclared # find code that shadows one of Go's predeclared identifiers 51 | - revive # golint replacement, finds style mistakes 52 | - staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks 53 | - tagliatelle # Checks the struct tags. 54 | - thelper # thelper detects golang test helpers without t.Helper() call and checks the consistency of test helpers 55 | - unconvert # Remove unnecessary type conversions 56 | - unparam # Reports unused function parameters 57 | - unused # Checks Go code for unused constants, variables, functions and types 58 | - varnamelen # checks that the length of a variable's name matches its scope 59 | - wastedassign # wastedassign finds wasted assignment statements 60 | - whitespace # Tool for detection of leading and trailing whitespace 61 | disable: 62 | - depguard # Go linter that checks if package imports are in a list of acceptable packages 63 | - funlen # Tool for detection of long functions 64 | - gochecknoinits # Checks that no init functions are present in Go code 65 | - 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. 66 | - interfacebloat # A linter that checks length of interface. 67 | - ireturn # Accept Interfaces, Return Concrete Types 68 | - mnd # An analyzer to detect magic numbers 69 | - nolintlint # Reports ill-formed or insufficient nolint directives 70 | - paralleltest # paralleltest detects missing usage of t.Parallel() method in your Go test 71 | - prealloc # Finds slice declarations that could potentially be preallocated 72 | - promlinter # Check Prometheus metrics naming via promlint 73 | - rowserrcheck # checks whether Err of rows is checked successfully 74 | - sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed. 75 | - testpackage # linter that makes you use a separate _test package 76 | - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes 77 | - wrapcheck # Checks that errors returned from external packages are wrapped 78 | - wsl # Whitespace Linter - Forces you to use empty lines! 79 | settings: 80 | staticcheck: 81 | checks: 82 | - all 83 | - -QF1008 # "could remove embedded field", to keep it explicit! 84 | - -QF1003 # "could use tagged switch on enum", Cases conflicts with exhaustive! 85 | exhaustive: 86 | default-signifies-exhaustive: true 87 | forbidigo: 88 | forbid: 89 | - pattern: ^fmt.Print(f|ln)?$ 90 | - pattern: ^log.(Panic|Fatal|Print)(f|ln)?$ 91 | - pattern: ^os.Exit$ 92 | - pattern: ^panic$ 93 | - pattern: ^print(ln)?$ 94 | - pattern: ^testing.T.(Error|Errorf|Fatal|Fatalf|Fail|FailNow)$ 95 | pkg: ^testing$ 96 | msg: use testify/assert instead 97 | analyze-types: true 98 | gomodguard: 99 | blocked: 100 | modules: 101 | - github.com/pkg/errors: 102 | recommendations: 103 | - errors 104 | govet: 105 | enable: 106 | - shadow 107 | revive: 108 | rules: 109 | # Prefer 'any' type alias over 'interface{}' for Go 1.18+ compatibility 110 | - name: use-any 111 | severity: warning 112 | disabled: false 113 | misspell: 114 | locale: US 115 | varnamelen: 116 | max-distance: 12 117 | min-name-length: 2 118 | ignore-type-assert-ok: true 119 | ignore-map-index-ok: true 120 | ignore-chan-recv-ok: true 121 | ignore-decls: 122 | - i int 123 | - n int 124 | - w io.Writer 125 | - r io.Reader 126 | - b []byte 127 | exclusions: 128 | generated: lax 129 | rules: 130 | - linters: 131 | - forbidigo 132 | - gocognit 133 | path: (examples|main\.go) 134 | - linters: 135 | - gocognit 136 | path: _test\.go 137 | - linters: 138 | - forbidigo 139 | path: cmd 140 | formatters: 141 | enable: 142 | - gci # Gci control golang package import order and make it always deterministic. 143 | - gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification 144 | - gofumpt # Gofumpt checks whether code was gofumpt-ed. 145 | - goimports # Goimports does everything that gofmt does. Additionally it checks unused imports 146 | exclusions: 147 | generated: lax 148 | --------------------------------------------------------------------------------