1
0
Fork 0
mirror of https://github.com/ossrs/srs.git synced 2025-03-09 15:49:59 +00:00

For regression test, add srs-bench to 3rdparty

This commit is contained in:
winlin 2021-03-04 13:23:01 +08:00
parent de87dd427d
commit 876210f6c9
1158 changed files with 256967 additions and 3 deletions

View file

@ -0,0 +1,24 @@
### JetBrains IDE ###
#####################
.idea/
### Emacs Temporary Files ###
#############################
*~
### Folders ###
###############
bin/
vendor/
node_modules/
### Files ###
#############
*.ivf
*.ogg
tags
cover.out
*.sw[poe]
*.wasm
examples/sfu-ws/cert.pem
examples/sfu-ws/key.pem

View file

@ -0,0 +1,89 @@
linters-settings:
govet:
check-shadowing: true
misspell:
locale: US
exhaustive:
default-signifies-exhaustive: true
gomodguard:
blocked:
modules:
- github.com/pkg/errors:
recommendations:
- errors
linters:
enable:
- asciicheck # Simple linter to check that your code does not contain non-ASCII identifiers
- bodyclose # checks whether HTTP response body is closed successfully
- deadcode # Finds unused code
- depguard # Go linter that checks if package imports are in a list of acceptable packages
- dogsled # Checks assignments with too many blank identifiers (e.g. x, _, _, _, := f())
- dupl # Tool for code clone detection
- errcheck # Errcheck is a program for checking for unchecked errors in go programs. These unchecked errors can be critical bugs in some cases
- exhaustive # check exhaustiveness of enum switch statements
- exportloopref # checks for pointers to enclosing loop variables
- gci # Gci control golang package import order and make it always deterministic.
- gochecknoglobals # Checks that no globals are present in Go code
- gochecknoinits # Checks that no init functions are present in Go code
- gocognit # Computes and checks the cognitive complexity of functions
- goconst # Finds repeated strings that could be replaced by a constant
- gocritic # The most opinionated Go source code linter
- godox # Tool for detection of FIXME, TODO and other comment keywords
- goerr113 # Golang linter to check the errors handling expressions
- gofmt # Gofmt checks whether code was gofmt-ed. By default this tool runs with -s option to check for code simplification
- gofumpt # Gofumpt checks whether code was gofumpt-ed.
- goheader # Checks is file header matches to pattern
- goimports # Goimports does everything that gofmt does. Additionally it checks unused imports
- golint # Golint differs from gofmt. Gofmt reformats Go source code, whereas golint prints out style mistakes
- gomodguard # Allow and block list linter for direct Go module dependencies. This is different from depguard where there are different block types for example version constraints and module recommendations.
- goprintffuncname # Checks that printf-like functions are named with `f` at the end
- gosec # Inspects source code for security problems
- gosimple # Linter for Go source code that specializes in simplifying a code
- govet # Vet examines Go source code and reports suspicious constructs, such as Printf calls whose arguments do not align with the format string
- ineffassign # Detects when assignments to existing variables are not used
- misspell # Finds commonly misspelled English words in comments
- nakedret # Finds naked returns in functions greater than a specified function length
- noctx # noctx finds sending http request without context.Context
- scopelint # Scopelint checks for unpinned variables in go programs
- staticcheck # Staticcheck is a go vet on steroids, applying a ton of static analysis checks
- structcheck # Finds unused struct fields
- stylecheck # Stylecheck is a replacement for golint
- typecheck # Like the front-end of a Go compiler, parses and type-checks Go code
- unconvert # Remove unnecessary type conversions
- unparam # Reports unused function parameters
- unused # Checks Go code for unused constants, variables, functions and types
- varcheck # Finds unused global variables and constants
- whitespace # Tool for detection of leading and trailing whitespace
disable:
- funlen # Tool for detection of long functions
- gocyclo # Computes and checks the cyclomatic complexity of functions
- godot # Check if comments end in a period
- gomnd # An analyzer to detect magic numbers.
- lll # Reports long lines
- maligned # Tool to detect Go structs that would take less memory if their fields were sorted
- nestif # Reports deeply nested if statements
- nlreturn # nlreturn checks for a new line before return and branch statements to increase code clarity
- nolintlint # Reports ill-formed or insufficient nolint directives
- prealloc # Finds slice declarations that could potentially be preallocated
- rowserrcheck # checks whether Err of rows is checked successfully
- sqlclosecheck # Checks that sql.Rows and sql.Stmt are closed.
- testpackage # linter that makes you use a separate _test package
- wsl # Whitespace Linter - Forces you to use empty lines!
issues:
exclude-use-default: false
exclude-rules:
# Allow complex tests, better to be self contained
- path: _test\.go
linters:
- gocognit
# Allow complex main function in examples
- path: examples
text: "of func `main` is high"
linters:
- gocognit
run:
skip-dirs-use-default: false

View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1,66 @@
<h1 align="center">
<br>
Pion ICE
<br>
</h1>
<h4 align="center">A Go implementation of ICE</h4>
<p align="center">
<a href="https://pion.ly"><img src="https://img.shields.io/badge/pion-ice-gray.svg?longCache=true&colorB=brightgreen" alt="Pion transport"></a>
<a href="http://gophers.slack.com/messages/pion"><img src="https://img.shields.io/badge/join-us%20on%20slack-gray.svg?longCache=true&logo=slack&colorB=brightgreen" alt="Slack Widget"></a>
<br>
<a href="https://travis-ci.org/pion/ice"><img src="https://travis-ci.org/pion/ice.svg?branch=master" alt="Build Status"></a>
<a href="https://pkg.go.dev/github.com/pion/ice"><img src="https://godoc.org/github.com/pion/ice?status.svg" alt="GoDoc"></a>
<a href="https://codecov.io/gh/pion/ice"><img src="https://codecov.io/gh/pion/ice/branch/master/graph/badge.svg" alt="Coverage Status"></a>
<a href="https://goreportcard.com/report/github.com/pion/ice"><img src="https://goreportcard.com/badge/github.com/pion/ice" alt="Go Report Card"></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-yellow.svg" alt="License: MIT"></a>
</p>
<br>
### Roadmap
The library is used as a part of our WebRTC implementation. Please refer to that [roadmap](https://github.com/pion/webrtc/issues/9) to track our major milestones.
### Community
Pion has an active community on the [Golang Slack](https://invite.slack.golangbridge.org/). Sign up and join the **#pion** channel for discussions and support. You can also use [Pion mailing list](https://groups.google.com/forum/#!forum/pion).
We are always looking to support **your projects**. Please reach out if you have something to build!
If you need commercial support or don't want to use public methods you can contact us at [team@pion.ly](mailto:team@pion.ly)
### Contributing
Check out the **[contributing wiki](https://github.com/pion/webrtc/wiki/Contributing)** to join the group of amazing people making this project possible:
* [John Bradley](https://github.com/kc5nra) - *Original Author*
* [Sean DuBois](https://github.com/Sean-Der) - *Original Author*
* [Michael MacDonald](https://github.com/mjmac) - *Original Author*
* [Michiel De Backker](https://github.com/backkem) - *Original Author*
* [Konstantin Itskov](https://github.com/trivigy) - *Original Author*
* [Luke Curley](https://github.com/kixelated)
* [Hugo Arregui](https://github.com/hugoArregui)
* [Adam Kiss](https://github.com/masterada)
* [Aleksandr Razumov](https://github.com/ernado)
* [Yutaka Takeda](https://github.com/enobufs)
* [Atsushi Watanabe](https://github.com/at-wat)
* [Robert Eperjesi](https://github.com/epes)
* [Sebastian Waisbrot](https://github.com/seppo0010)
* [Zizheng Tai](https://github.com/ZizhengTai)
* [Aaron France](https://github.com/AeroNotix)
* [Chao Yuan](https://github.com/yuanchao0310)
* [Jason Maldonis](https://github.com/jjmaldonis)
* [Nevio Vesic](https://github.com/0x19)
* [David Hamilton](https://github.com/dihamilton)
* [adwpc](https://github.com/adwpc)
* [Ori Bernstein](https://eigenstate.org)
* [Sam Lancia](https://github.com/nerd2)
* [Lander Noterman](https://github.com/LanderN)
* [BUPTCZQ](https://github.com/buptczq)
* [Henry](https://github.com/cryptix)
* [Jerko Steiner](https://github.com/jeremija)
* [Sidney San Martín](https://github.com/s4y)
* [JooYoung Lim](https://github.com/DevRockstarZ)
* [Kory Miller](https://github.com/korymiller1489)
* [ZHENK](https://github.com/scorpionknifes)
* [Assad Obaid](https://github.com/assadobaid)
* [Antoine Baché](https://github.com/Antonito)
### License
MIT License - see [LICENSE](LICENSE) for full text

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,252 @@
package ice
import (
"time"
"github.com/pion/logging"
"github.com/pion/transport/vnet"
"golang.org/x/net/proxy"
)
const (
// defaultCheckInterval is the interval at which the agent performs candidate checks in the connecting phase
defaultCheckInterval = 200 * time.Millisecond
// keepaliveInterval used to keep candidates alive
defaultKeepaliveInterval = 2 * time.Second
// defaultDisconnectedTimeout is the default time till an Agent transitions disconnected
defaultDisconnectedTimeout = 5 * time.Second
// defaultFailedTimeout is the default time till an Agent transitions to failed after disconnected
defaultFailedTimeout = 25 * time.Second
// wait time before nominating a host candidate
defaultHostAcceptanceMinWait = 0
// wait time before nominating a srflx candidate
defaultSrflxAcceptanceMinWait = 500 * time.Millisecond
// wait time before nominating a prflx candidate
defaultPrflxAcceptanceMinWait = 1000 * time.Millisecond
// wait time before nominating a relay candidate
defaultRelayAcceptanceMinWait = 2000 * time.Millisecond
// max binding request before considering a pair failed
defaultMaxBindingRequests = 7
// the number of bytes that can be buffered before we start to error
maxBufferSize = 1000 * 1000 // 1MB
// wait time before binding requests can be deleted
maxBindingRequestTimeout = 4000 * time.Millisecond
)
func defaultCandidateTypes() []CandidateType {
return []CandidateType{CandidateTypeHost, CandidateTypeServerReflexive, CandidateTypeRelay}
}
// AgentConfig collects the arguments to ice.Agent construction into
// a single structure, for future-proofness of the interface
type AgentConfig struct {
Urls []*URL
// PortMin and PortMax are optional. Leave them 0 for the default UDP port allocation strategy.
PortMin uint16
PortMax uint16
// LocalUfrag and LocalPwd values used to perform connectivity
// checks. The values MUST be unguessable, with at least 128 bits of
// random number generator output used to generate the password, and
// at least 24 bits of output to generate the username fragment.
LocalUfrag string
LocalPwd string
// MulticastDNSMode controls mDNS behavior for the ICE agent
MulticastDNSMode MulticastDNSMode
// MulticastDNSHostName controls the hostname for this agent. If none is specified a random one will be generated
MulticastDNSHostName string
// DisconnectedTimeout defaults to 5 seconds when this property is nil.
// If the duration is 0, the ICE Agent will never go to disconnected
DisconnectedTimeout *time.Duration
// FailedTimeout defaults to 25 seconds when this property is nil.
// If the duration is 0, we will never go to failed.
FailedTimeout *time.Duration
// KeepaliveInterval determines how often should we send ICE
// keepalives (should be less then connectiontimeout above)
// when this is nil, it defaults to 10 seconds.
// A keepalive interval of 0 means we never send keepalive packets
KeepaliveInterval *time.Duration
// NetworkTypes is an optional configuration for disabling or enabling
// support for specific network types.
NetworkTypes []NetworkType
// CandidateTypes is an optional configuration for disabling or enabling
// support for specific candidate types.
CandidateTypes []CandidateType
LoggerFactory logging.LoggerFactory
// checkInterval controls how often our internal task loop runs when
// in the connecting state. Only useful for testing.
checkInterval time.Duration
// MaxBindingRequests is the max amount of binding requests the agent will send
// over a candidate pair for validation or nomination, if after MaxBindingRequests
// the candidate is yet to answer a binding request or a nomination we set the pair as failed
MaxBindingRequests *uint16
// Lite agents do not perform connectivity check and only provide host candidates.
Lite bool
// NAT1To1IPCandidateType is used along with NAT1To1IPs to specify which candidate type
// the 1:1 NAT IP addresses should be mapped to.
// If unspecified or CandidateTypeHost, NAT1To1IPs are used to replace host candidate IPs.
// If CandidateTypeServerReflexive, it will insert a srflx candidate (as if it was dervied
// from a STUN server) with its port number being the one for the actual host candidate.
// Other values will result in an error.
NAT1To1IPCandidateType CandidateType
// NAT1To1IPs contains a list of public IP addresses that are to be used as a host
// candidate or srflx candidate. This is used typically for servers that are behind
// 1:1 D-NAT (e.g. AWS EC2 instances) and to eliminate the need of server reflexisive
// candidate gathering.
NAT1To1IPs []string
// HostAcceptanceMinWait specify a minimum wait time before selecting host candidates
HostAcceptanceMinWait *time.Duration
// HostAcceptanceMinWait specify a minimum wait time before selecting srflx candidates
SrflxAcceptanceMinWait *time.Duration
// HostAcceptanceMinWait specify a minimum wait time before selecting prflx candidates
PrflxAcceptanceMinWait *time.Duration
// HostAcceptanceMinWait specify a minimum wait time before selecting relay candidates
RelayAcceptanceMinWait *time.Duration
// Net is the our abstracted network interface for internal development purpose only
// (see github.com/pion/transport/vnet)
Net *vnet.Net
// InterfaceFilter is a function that you can use in order to whitelist or blacklist
// the interfaces which are used to gather ICE candidates.
InterfaceFilter func(string) bool
// InsecureSkipVerify controls if self-signed certificates are accepted when connecting
// to TURN servers via TLS or DTLS
InsecureSkipVerify bool
// TCPMux will be used for multiplexing incoming TCP connections for ICE TCP.
// Currently only passive candidates are supported. This functionality is
// experimental and the API might change in the future.
TCPMux TCPMux
// Proxy Dialer is a dialer that should be implemented by the user based on golang.org/x/net/proxy
// dial interface in order to support corporate proxies
ProxyDialer proxy.Dialer
}
// initWithDefaults populates an agent and falls back to defaults if fields are unset
func (config *AgentConfig) initWithDefaults(a *Agent) {
if config.MaxBindingRequests == nil {
a.maxBindingRequests = defaultMaxBindingRequests
} else {
a.maxBindingRequests = *config.MaxBindingRequests
}
if config.HostAcceptanceMinWait == nil {
a.hostAcceptanceMinWait = defaultHostAcceptanceMinWait
} else {
a.hostAcceptanceMinWait = *config.HostAcceptanceMinWait
}
if config.SrflxAcceptanceMinWait == nil {
a.srflxAcceptanceMinWait = defaultSrflxAcceptanceMinWait
} else {
a.srflxAcceptanceMinWait = *config.SrflxAcceptanceMinWait
}
if config.PrflxAcceptanceMinWait == nil {
a.prflxAcceptanceMinWait = defaultPrflxAcceptanceMinWait
} else {
a.prflxAcceptanceMinWait = *config.PrflxAcceptanceMinWait
}
if config.RelayAcceptanceMinWait == nil {
a.relayAcceptanceMinWait = defaultRelayAcceptanceMinWait
} else {
a.relayAcceptanceMinWait = *config.RelayAcceptanceMinWait
}
if config.DisconnectedTimeout == nil {
a.disconnectedTimeout = defaultDisconnectedTimeout
} else {
a.disconnectedTimeout = *config.DisconnectedTimeout
}
if config.FailedTimeout == nil {
a.failedTimeout = defaultFailedTimeout
} else {
a.failedTimeout = *config.FailedTimeout
}
if config.KeepaliveInterval == nil {
a.keepaliveInterval = defaultKeepaliveInterval
} else {
a.keepaliveInterval = *config.KeepaliveInterval
}
if config.checkInterval == 0 {
a.checkInterval = defaultCheckInterval
} else {
a.checkInterval = config.checkInterval
}
if config.CandidateTypes == nil || len(config.CandidateTypes) == 0 {
a.candidateTypes = defaultCandidateTypes()
} else {
a.candidateTypes = config.CandidateTypes
}
}
func (config *AgentConfig) initExtIPMapping(a *Agent) error {
var err error
a.extIPMapper, err = newExternalIPMapper(config.NAT1To1IPCandidateType, config.NAT1To1IPs)
if err != nil {
return err
}
if a.extIPMapper == nil {
return nil // this may happen when config.NAT1To1IPs is an empty array
}
if a.extIPMapper.candidateType == CandidateTypeHost {
if a.mDNSMode == MulticastDNSModeQueryAndGather {
return ErrMulticastDNSWithNAT1To1IPMapping
}
candiHostEnabled := false
for _, candiType := range a.candidateTypes {
if candiType == CandidateTypeHost {
candiHostEnabled = true
break
}
}
if !candiHostEnabled {
return ErrIneffectiveNAT1To1IPMappingHost
}
} else if a.extIPMapper.candidateType == CandidateTypeServerReflexive {
candiSrflxEnabled := false
for _, candiType := range a.candidateTypes {
if candiType == CandidateTypeServerReflexive {
candiSrflxEnabled = true
break
}
}
if !candiSrflxEnabled {
return ErrIneffectiveNAT1To1IPMappingSrflx
}
}
return nil
}

View file

@ -0,0 +1,113 @@
package ice
import (
"context"
"time"
)
// GetCandidatePairsStats returns a list of candidate pair stats
func (a *Agent) GetCandidatePairsStats() []CandidatePairStats {
var res []CandidatePairStats
err := a.run(a.context(), func(ctx context.Context, agent *Agent) {
result := make([]CandidatePairStats, 0, len(agent.checklist))
for _, cp := range agent.checklist {
stat := CandidatePairStats{
Timestamp: time.Now(),
LocalCandidateID: cp.local.ID(),
RemoteCandidateID: cp.remote.ID(),
State: cp.state,
Nominated: cp.nominated,
// PacketsSent uint32
// PacketsReceived uint32
// BytesSent uint64
// BytesReceived uint64
// LastPacketSentTimestamp time.Time
// LastPacketReceivedTimestamp time.Time
// FirstRequestTimestamp time.Time
// LastRequestTimestamp time.Time
// LastResponseTimestamp time.Time
// TotalRoundTripTime float64
// CurrentRoundTripTime float64
// AvailableOutgoingBitrate float64
// AvailableIncomingBitrate float64
// CircuitBreakerTriggerCount uint32
// RequestsReceived uint64
// RequestsSent uint64
// ResponsesReceived uint64
// ResponsesSent uint64
// RetransmissionsReceived uint64
// RetransmissionsSent uint64
// ConsentRequestsSent uint64
// ConsentExpiredTimestamp time.Time
}
result = append(result, stat)
}
res = result
})
if err != nil {
a.log.Errorf("error getting candidate pairs stats %v", err)
return []CandidatePairStats{}
}
return res
}
// GetLocalCandidatesStats returns a list of local candidates stats
func (a *Agent) GetLocalCandidatesStats() []CandidateStats {
var res []CandidateStats
err := a.run(a.context(), func(ctx context.Context, agent *Agent) {
result := make([]CandidateStats, 0, len(agent.localCandidates))
for networkType, localCandidates := range agent.localCandidates {
for _, c := range localCandidates {
stat := CandidateStats{
Timestamp: time.Now(),
ID: c.ID(),
NetworkType: networkType,
IP: c.Address(),
Port: c.Port(),
CandidateType: c.Type(),
Priority: c.Priority(),
// URL string
RelayProtocol: "udp",
// Deleted bool
}
result = append(result, stat)
}
}
res = result
})
if err != nil {
a.log.Errorf("error getting candidate pairs stats %v", err)
return []CandidateStats{}
}
return res
}
// GetRemoteCandidatesStats returns a list of remote candidates stats
func (a *Agent) GetRemoteCandidatesStats() []CandidateStats {
var res []CandidateStats
err := a.run(a.context(), func(ctx context.Context, agent *Agent) {
result := make([]CandidateStats, 0, len(agent.remoteCandidates))
for networkType, localCandidates := range agent.remoteCandidates {
for _, c := range localCandidates {
stat := CandidateStats{
Timestamp: time.Now(),
ID: c.ID(),
NetworkType: networkType,
IP: c.Address(),
Port: c.Port(),
CandidateType: c.Type(),
Priority: c.Priority(),
// URL string
RelayProtocol: "udp",
}
result = append(result, stat)
}
}
res = result
})
if err != nil {
a.log.Errorf("error getting candidate pairs stats %v", err)
return []CandidateStats{}
}
return res
}

View file

@ -0,0 +1,68 @@
package ice
import (
"context"
"net"
"time"
)
const (
receiveMTU = 8192
defaultLocalPreference = 65535
// ComponentRTP indicates that the candidate is used for RTP
ComponentRTP uint16 = 1
// ComponentRTCP indicates that the candidate is used for RTCP
ComponentRTCP
)
// Candidate represents an ICE candidate
type Candidate interface {
// An arbitrary string used in the freezing algorithm to
// group similar candidates. It is the same for two candidates that
// have the same type, base IP address, protocol (UDP, TCP, etc.),
// and STUN or TURN server.
Foundation() string
// ID is a unique identifier for just this candidate
// Unlike the foundation this is different for each candidate
ID() string
// A component is a piece of a data stream.
// An example is one for RTP, and one for RTCP
Component() uint16
SetComponent(uint16)
// The last time this candidate received traffic
LastReceived() time.Time
// The last time this candidate sent traffic
LastSent() time.Time
NetworkType() NetworkType
Address() string
Port() int
Priority() uint32
// A transport address related to a
// candidate, which is useful for diagnostics and other purposes
RelatedAddress() *CandidateRelatedAddress
String() string
Type() CandidateType
TCPType() TCPType
Equal(other Candidate) bool
Marshal() string
addr() net.Addr
agent() *Agent
context() context.Context
close() error
seen(outbound bool)
start(a *Agent, conn net.PacketConn, initializedCh <-chan struct{})
writeTo(raw []byte, dst Candidate) (int, error)
}

View file

@ -0,0 +1,496 @@
package ice
import (
"context"
"fmt"
"hash/crc32"
"net"
"strconv"
"strings"
"sync/atomic"
"time"
"github.com/pion/logging"
"github.com/pion/stun"
)
type candidateBase struct {
id string
networkType NetworkType
candidateType CandidateType
component uint16
address string
port int
relatedAddress *CandidateRelatedAddress
tcpType TCPType
resolvedAddr net.Addr
lastSent atomic.Value
lastReceived atomic.Value
conn net.PacketConn
currAgent *Agent
closeCh chan struct{}
closedCh chan struct{}
foundationOverride string
priorityOverride uint32
}
// Done implements context.Context
func (c *candidateBase) Done() <-chan struct{} {
return c.closeCh
}
// Err implements context.Context
func (c *candidateBase) Err() error {
select {
case <-c.closedCh:
return ErrRunCanceled
default:
return nil
}
}
// Deadline implements context.Context
func (c *candidateBase) Deadline() (deadline time.Time, ok bool) {
return time.Time{}, false
}
// Value implements context.Context
func (c *candidateBase) Value(key interface{}) interface{} {
return nil
}
// ID returns Candidate ID
func (c *candidateBase) ID() string {
return c.id
}
func (c *candidateBase) Foundation() string {
if c.foundationOverride != "" {
return c.foundationOverride
}
return fmt.Sprintf("%d", crc32.ChecksumIEEE([]byte(c.Type().String()+c.address+c.networkType.String())))
}
// Address returns Candidate Address
func (c *candidateBase) Address() string {
return c.address
}
// Port returns Candidate Port
func (c *candidateBase) Port() int {
return c.port
}
// Type returns candidate type
func (c *candidateBase) Type() CandidateType {
return c.candidateType
}
// NetworkType returns candidate NetworkType
func (c *candidateBase) NetworkType() NetworkType {
return c.networkType
}
// Component returns candidate component
func (c *candidateBase) Component() uint16 {
return c.component
}
func (c *candidateBase) SetComponent(component uint16) {
c.component = component
}
// LocalPreference returns the local preference for this candidate
func (c *candidateBase) LocalPreference() uint16 {
if c.NetworkType().IsTCP() {
// RFC 6544, section 4.2
//
// In Section 4.1.2.1 of [RFC5245], a recommended formula for UDP ICE
// candidate prioritization is defined. For TCP candidates, the same
// formula and candidate type preferences SHOULD be used, and the
// RECOMMENDED type preferences for the new candidate types defined in
// this document (see Section 5) are 105 for NAT-assisted candidates and
// 75 for UDP-tunneled candidates.
//
// (...)
//
// With TCP candidates, the local preference part of the recommended
// priority formula is updated to also include the directionality
// (active, passive, or simultaneous-open) of the TCP connection. The
// RECOMMENDED local preference is then defined as:
//
// local preference = (2^13) * direction-pref + other-pref
//
// The direction-pref MUST be between 0 and 7 (both inclusive), with 7
// being the most preferred. The other-pref MUST be between 0 and 8191
// (both inclusive), with 8191 being the most preferred. It is
// RECOMMENDED that the host, UDP-tunneled, and relayed TCP candidates
// have the direction-pref assigned as follows: 6 for active, 4 for
// passive, and 2 for S-O. For the NAT-assisted and server reflexive
// candidates, the RECOMMENDED values are: 6 for S-O, 4 for active, and
// 2 for passive.
//
// (...)
//
// If any two candidates have the same type-preference and direction-
// pref, they MUST have a unique other-pref. With this specification,
// this usually only happens with multi-homed hosts, in which case
// other-pref is the preference for the particular IP address from which
// the candidate was obtained. When there is only a single IP address,
// this value SHOULD be set to the maximum allowed value (8191).
var otherPref uint16 = 8191
directionPref := func() uint16 {
switch c.Type() {
case CandidateTypeHost, CandidateTypeRelay:
switch c.tcpType {
case TCPTypeActive:
return 6
case TCPTypePassive:
return 4
case TCPTypeSimultaneousOpen:
return 2
case TCPTypeUnspecified:
return 0
}
case CandidateTypePeerReflexive, CandidateTypeServerReflexive:
switch c.tcpType {
case TCPTypeSimultaneousOpen:
return 6
case TCPTypeActive:
return 4
case TCPTypePassive:
return 2
case TCPTypeUnspecified:
return 0
}
case CandidateTypeUnspecified:
return 0
}
return 0
}()
return (1<<13)*directionPref + otherPref
}
return defaultLocalPreference
}
// RelatedAddress returns *CandidateRelatedAddress
func (c *candidateBase) RelatedAddress() *CandidateRelatedAddress {
return c.relatedAddress
}
func (c *candidateBase) TCPType() TCPType {
return c.tcpType
}
// start runs the candidate using the provided connection
func (c *candidateBase) start(a *Agent, conn net.PacketConn, initializedCh <-chan struct{}) {
if c.conn != nil {
c.agent().log.Warn("Can't start already started candidateBase")
return
}
c.currAgent = a
c.conn = conn
c.closeCh = make(chan struct{})
c.closedCh = make(chan struct{})
go c.recvLoop(initializedCh)
}
func (c *candidateBase) recvLoop(initializedCh <-chan struct{}) {
defer func() {
close(c.closedCh)
}()
select {
case <-initializedCh:
case <-c.closeCh:
return
}
log := c.agent().log
buffer := make([]byte, receiveMTU)
for {
n, srcAddr, err := c.conn.ReadFrom(buffer)
if err != nil {
return
}
handleInboundCandidateMsg(c, c, buffer[:n], srcAddr, log)
}
}
func handleInboundCandidateMsg(ctx context.Context, c Candidate, buffer []byte, srcAddr net.Addr, log logging.LeveledLogger) {
if stun.IsMessage(buffer) {
m := &stun.Message{
Raw: make([]byte, len(buffer)),
}
// Explicitly copy raw buffer so Message can own the memory.
copy(m.Raw, buffer)
if err := m.Decode(); err != nil {
log.Warnf("Failed to handle decode ICE from %s to %s: %v", c.addr(), srcAddr, err)
return
}
err := c.agent().run(ctx, func(ctx context.Context, agent *Agent) {
agent.handleInbound(m, c, srcAddr)
})
if err != nil {
log.Warnf("Failed to handle message: %v", err)
}
return
}
if !c.agent().validateNonSTUNTraffic(c, srcAddr) {
log.Warnf("Discarded message from %s, not a valid remote candidate", c.addr())
return
}
// NOTE This will return packetio.ErrFull if the buffer ever manages to fill up.
if _, err := c.agent().buffer.Write(buffer); err != nil {
log.Warnf("failed to write packet")
}
}
// close stops the recvLoop
func (c *candidateBase) close() error {
// If conn has never been started will be nil
if c.Done() == nil {
return nil
}
// Assert that conn has not already been closed
select {
case <-c.Done():
return nil
default:
}
var firstErr error
// Unblock recvLoop
close(c.closeCh)
if err := c.conn.SetDeadline(time.Now()); err != nil {
firstErr = err
}
// Close the conn
if err := c.conn.Close(); err != nil && firstErr == nil {
firstErr = err
}
if firstErr != nil {
return firstErr
}
// Wait until the recvLoop is closed
<-c.closedCh
return nil
}
func (c *candidateBase) writeTo(raw []byte, dst Candidate) (int, error) {
n, err := c.conn.WriteTo(raw, dst.addr())
if err != nil {
c.agent().log.Warnf("%s: %v", errSendPacket, err)
return n, nil
}
c.seen(true)
return n, nil
}
// Priority computes the priority for this ICE Candidate
func (c *candidateBase) Priority() uint32 {
if c.priorityOverride != 0 {
return c.priorityOverride
}
// The local preference MUST be an integer from 0 (lowest preference) to
// 65535 (highest preference) inclusive. When there is only a single IP
// address, this value SHOULD be set to 65535. If there are multiple
// candidates for a particular component for a particular data stream
// that have the same type, the local preference MUST be unique for each
// one.
return (1<<24)*uint32(c.Type().Preference()) +
(1<<8)*uint32(c.LocalPreference()) +
uint32(256-c.Component())
}
// Equal is used to compare two candidateBases
func (c *candidateBase) Equal(other Candidate) bool {
return c.NetworkType() == other.NetworkType() &&
c.Type() == other.Type() &&
c.Address() == other.Address() &&
c.Port() == other.Port() &&
c.TCPType() == other.TCPType() &&
c.RelatedAddress().Equal(other.RelatedAddress())
}
// String makes the candidateBase printable
func (c *candidateBase) String() string {
return fmt.Sprintf("%s %s %s:%d%s", c.NetworkType(), c.Type(), c.Address(), c.Port(), c.relatedAddress)
}
// LastReceived returns a time.Time indicating the last time
// this candidate was received
func (c *candidateBase) LastReceived() time.Time {
lastReceived := c.lastReceived.Load()
if lastReceived == nil {
return time.Time{}
}
return lastReceived.(time.Time)
}
func (c *candidateBase) setLastReceived(t time.Time) {
c.lastReceived.Store(t)
}
// LastSent returns a time.Time indicating the last time
// this candidate was sent
func (c *candidateBase) LastSent() time.Time {
lastSent := c.lastSent.Load()
if lastSent == nil {
return time.Time{}
}
return lastSent.(time.Time)
}
func (c *candidateBase) setLastSent(t time.Time) {
c.lastSent.Store(t)
}
func (c *candidateBase) seen(outbound bool) {
if outbound {
c.setLastSent(time.Now())
} else {
c.setLastReceived(time.Now())
}
}
func (c *candidateBase) addr() net.Addr {
return c.resolvedAddr
}
func (c *candidateBase) agent() *Agent {
return c.currAgent
}
func (c *candidateBase) context() context.Context {
return c
}
// Marshal returns the string representation of the ICECandidate
func (c *candidateBase) Marshal() string {
val := fmt.Sprintf("%s %d %s %d %s %d typ %s",
c.Foundation(),
c.Component(),
c.NetworkType().NetworkShort(),
c.Priority(),
c.Address(),
c.Port(),
c.Type())
if c.tcpType != TCPTypeUnspecified {
val += fmt.Sprintf(" tcptype %s", c.tcpType.String())
}
if c.RelatedAddress() != nil {
val = fmt.Sprintf("%s raddr %s rport %d",
val,
c.RelatedAddress().Address,
c.RelatedAddress().Port)
}
return val
}
// UnmarshalCandidate creates a Candidate from its string representation
func UnmarshalCandidate(raw string) (Candidate, error) {
split := strings.Fields(raw)
if len(split) < 8 {
return nil, fmt.Errorf("%w (%d)", errAttributeTooShortICECandidate, len(split))
}
// Foundation
foundation := split[0]
// Component
rawComponent, err := strconv.ParseUint(split[1], 10, 16)
if err != nil {
return nil, fmt.Errorf("%w: %v", errParseComponent, err)
}
component := uint16(rawComponent)
// Protocol
protocol := split[2]
// Priority
priorityRaw, err := strconv.ParseUint(split[3], 10, 32)
if err != nil {
return nil, fmt.Errorf("%w: %v", errParsePriority, err)
}
priority := uint32(priorityRaw)
// Address
address := split[4]
// Port
rawPort, err := strconv.ParseUint(split[5], 10, 16)
if err != nil {
return nil, fmt.Errorf("%w: %v", errParsePort, err)
}
port := int(rawPort)
typ := split[7]
relatedAddress := ""
relatedPort := 0
tcpType := TCPTypeUnspecified
if len(split) > 8 {
split = split[8:]
if split[0] == "raddr" {
if len(split) < 4 {
return nil, fmt.Errorf("%w: incorrect length", errParseRelatedAddr)
}
// RelatedAddress
relatedAddress = split[1]
// RelatedPort
rawRelatedPort, parseErr := strconv.ParseUint(split[3], 10, 16)
if parseErr != nil {
return nil, fmt.Errorf("%w: %v", errParsePort, parseErr)
}
relatedPort = int(rawRelatedPort)
} else if split[0] == "tcptype" {
if len(split) < 2 {
return nil, fmt.Errorf("%w: incorrect length", errParseTypType)
}
tcpType = NewTCPType(split[1])
}
}
switch typ {
case "host":
return NewCandidateHost(&CandidateHostConfig{"", protocol, address, port, component, priority, foundation, tcpType})
case "srflx":
return NewCandidateServerReflexive(&CandidateServerReflexiveConfig{"", protocol, address, port, component, priority, foundation, relatedAddress, relatedPort})
case "prflx":
return NewCandidatePeerReflexive(&CandidatePeerReflexiveConfig{"", protocol, address, port, component, priority, foundation, relatedAddress, relatedPort})
case "relay":
return NewCandidateRelay(&CandidateRelayConfig{"", protocol, address, port, component, priority, foundation, relatedAddress, relatedPort, nil})
default:
}
return nil, fmt.Errorf("%w (%s)", errUnknownCandidateTyp, typ)
}

View file

@ -0,0 +1,76 @@
package ice
import (
"net"
"strings"
)
// CandidateHost is a candidate of type host
type CandidateHost struct {
candidateBase
network string
}
// CandidateHostConfig is the config required to create a new CandidateHost
type CandidateHostConfig struct {
CandidateID string
Network string
Address string
Port int
Component uint16
Priority uint32
Foundation string
TCPType TCPType
}
// NewCandidateHost creates a new host candidate
func NewCandidateHost(config *CandidateHostConfig) (*CandidateHost, error) {
candidateID := config.CandidateID
if candidateID == "" {
candidateID = globalCandidateIDGenerator.Generate()
}
c := &CandidateHost{
candidateBase: candidateBase{
id: candidateID,
address: config.Address,
candidateType: CandidateTypeHost,
component: config.Component,
port: config.Port,
tcpType: config.TCPType,
foundationOverride: config.Foundation,
priorityOverride: config.Priority,
},
network: config.Network,
}
if !strings.HasSuffix(config.Address, ".local") {
ip := net.ParseIP(config.Address)
if ip == nil {
return nil, ErrAddressParseFailed
}
if err := c.setIP(ip); err != nil {
return nil, err
}
} else {
// Until mDNS candidate is resolved assume it is UDPv4
c.candidateBase.networkType = NetworkTypeUDP4
}
return c, nil
}
func (c *CandidateHost) setIP(ip net.IP) error {
networkType, err := determineNetworkType(c.network, ip)
if err != nil {
return err
}
c.candidateBase.networkType = networkType
c.candidateBase.resolvedAddr = createAddr(networkType, ip, c.port)
return nil
}

View file

@ -0,0 +1,60 @@
// Package ice ...
//nolint:dupl
package ice
import "net"
// CandidatePeerReflexive ...
type CandidatePeerReflexive struct {
candidateBase
}
// CandidatePeerReflexiveConfig is the config required to create a new CandidatePeerReflexive
type CandidatePeerReflexiveConfig struct {
CandidateID string
Network string
Address string
Port int
Component uint16
Priority uint32
Foundation string
RelAddr string
RelPort int
}
// NewCandidatePeerReflexive creates a new peer reflective candidate
func NewCandidatePeerReflexive(config *CandidatePeerReflexiveConfig) (*CandidatePeerReflexive, error) {
ip := net.ParseIP(config.Address)
if ip == nil {
return nil, ErrAddressParseFailed
}
networkType, err := determineNetworkType(config.Network, ip)
if err != nil {
return nil, err
}
candidateID := config.CandidateID
candidateIDGenerator := newCandidateIDGenerator()
if candidateID == "" {
candidateID = candidateIDGenerator.Generate()
}
return &CandidatePeerReflexive{
candidateBase: candidateBase{
id: candidateID,
networkType: networkType,
candidateType: CandidateTypePeerReflexive,
address: config.Address,
port: config.Port,
resolvedAddr: createAddr(networkType, ip, config.Port),
component: config.Component,
foundationOverride: config.Foundation,
priorityOverride: config.Priority,
relatedAddress: &CandidateRelatedAddress{
Address: config.RelAddr,
Port: config.RelPort,
},
},
}, nil
}

View file

@ -0,0 +1,73 @@
package ice
import (
"net"
)
// CandidateRelay ...
type CandidateRelay struct {
candidateBase
onClose func() error
}
// CandidateRelayConfig is the config required to create a new CandidateRelay
type CandidateRelayConfig struct {
CandidateID string
Network string
Address string
Port int
Component uint16
Priority uint32
Foundation string
RelAddr string
RelPort int
OnClose func() error
}
// NewCandidateRelay creates a new relay candidate
func NewCandidateRelay(config *CandidateRelayConfig) (*CandidateRelay, error) {
candidateID := config.CandidateID
if candidateID == "" {
candidateID = globalCandidateIDGenerator.Generate()
}
ip := net.ParseIP(config.Address)
if ip == nil {
return nil, ErrAddressParseFailed
}
networkType, err := determineNetworkType(config.Network, ip)
if err != nil {
return nil, err
}
return &CandidateRelay{
candidateBase: candidateBase{
id: candidateID,
networkType: networkType,
candidateType: CandidateTypeRelay,
address: config.Address,
port: config.Port,
resolvedAddr: &net.UDPAddr{IP: ip, Port: config.Port},
component: config.Component,
foundationOverride: config.Foundation,
priorityOverride: config.Priority,
relatedAddress: &CandidateRelatedAddress{
Address: config.RelAddr,
Port: config.RelPort,
},
},
onClose: config.OnClose,
}, nil
}
func (c *CandidateRelay) close() error {
err := c.candidateBase.close()
if c.onClose != nil {
err = c.onClose()
c.onClose = nil
}
return err
}

View file

@ -0,0 +1,59 @@
// Package ice ...
//nolint:dupl
package ice
import "net"
// CandidateServerReflexive ...
type CandidateServerReflexive struct {
candidateBase
}
// CandidateServerReflexiveConfig is the config required to create a new CandidateServerReflexive
type CandidateServerReflexiveConfig struct {
CandidateID string
Network string
Address string
Port int
Component uint16
Priority uint32
Foundation string
RelAddr string
RelPort int
}
// NewCandidateServerReflexive creates a new server reflective candidate
func NewCandidateServerReflexive(config *CandidateServerReflexiveConfig) (*CandidateServerReflexive, error) {
ip := net.ParseIP(config.Address)
if ip == nil {
return nil, ErrAddressParseFailed
}
networkType, err := determineNetworkType(config.Network, ip)
if err != nil {
return nil, err
}
candidateID := config.CandidateID
if candidateID == "" {
candidateID = globalCandidateIDGenerator.Generate()
}
return &CandidateServerReflexive{
candidateBase: candidateBase{
id: candidateID,
networkType: networkType,
candidateType: CandidateTypeServerReflexive,
address: config.Address,
port: config.Port,
resolvedAddr: &net.UDPAddr{IP: ip, Port: config.Port},
component: config.Component,
foundationOverride: config.Foundation,
priorityOverride: config.Priority,
relatedAddress: &CandidateRelatedAddress{
Address: config.RelAddr,
Port: config.RelPort,
},
},
}, nil
}

View file

@ -0,0 +1,94 @@
package ice
import (
"fmt"
"github.com/pion/stun"
)
func newCandidatePair(local, remote Candidate, controlling bool) *candidatePair {
return &candidatePair{
iceRoleControlling: controlling,
remote: remote,
local: local,
state: CandidatePairStateWaiting,
}
}
// candidatePair represents a combination of a local and remote candidate
type candidatePair struct {
iceRoleControlling bool
remote Candidate
local Candidate
bindingRequestCount uint16
state CandidatePairState
nominated bool
}
func (p *candidatePair) String() string {
return fmt.Sprintf("prio %d (local, prio %d) %s <-> %s (remote, prio %d)",
p.Priority(), p.local.Priority(), p.local, p.remote, p.remote.Priority())
}
func (p *candidatePair) Equal(other *candidatePair) bool {
if p == nil && other == nil {
return true
}
if p == nil || other == nil {
return false
}
return p.local.Equal(other.local) && p.remote.Equal(other.remote)
}
// RFC 5245 - 5.7.2. Computing Pair Priority and Ordering Pairs
// Let G be the priority for the candidate provided by the controlling
// agent. Let D be the priority for the candidate provided by the
// controlled agent.
// pair priority = 2^32*MIN(G,D) + 2*MAX(G,D) + (G>D?1:0)
func (p *candidatePair) Priority() uint64 {
var g uint32
var d uint32
if p.iceRoleControlling {
g = p.local.Priority()
d = p.remote.Priority()
} else {
g = p.remote.Priority()
d = p.local.Priority()
}
// Just implement these here rather
// than fooling around with the math package
min := func(x, y uint32) uint64 {
if x < y {
return uint64(x)
}
return uint64(y)
}
max := func(x, y uint32) uint64 {
if x > y {
return uint64(x)
}
return uint64(y)
}
cmp := func(x, y uint32) uint64 {
if x > y {
return uint64(1)
}
return uint64(0)
}
// 1<<32 overflows uint32; and if both g && d are
// maxUint32, this result would overflow uint64
return (1<<32-1)*min(g, d) + 2*max(g, d) + cmp(g, d)
}
func (p *candidatePair) Write(b []byte) (int, error) {
return p.local.writeTo(b, p.remote)
}
func (a *Agent) sendSTUN(msg *stun.Message, local, remote Candidate) {
_, err := local.writeTo(msg.Raw, remote)
if err != nil {
a.log.Tracef("failed to send STUN message: %s", err)
}
}

View file

@ -0,0 +1,37 @@
package ice
// CandidatePairState represent the ICE candidate pair state
type CandidatePairState int
const (
// CandidatePairStateWaiting means a check has not been performed for
// this pair
CandidatePairStateWaiting = iota + 1
// CandidatePairStateInProgress means a check has been sent for this pair,
// but the transaction is in progress.
CandidatePairStateInProgress
// CandidatePairStateFailed means a check for this pair was already done
// and failed, either never producing any response or producing an unrecoverable
// failure response.
CandidatePairStateFailed
// CandidatePairStateSucceeded means a check for this pair was already
// done and produced a successful result.
CandidatePairStateSucceeded
)
func (c CandidatePairState) String() string {
switch c {
case CandidatePairStateWaiting:
return "waiting"
case CandidatePairStateInProgress:
return "in-progress"
case CandidatePairStateFailed:
return "failed"
case CandidatePairStateSucceeded:
return "succeeded"
}
return "Unknown candidate pair state"
}

View file

@ -0,0 +1,30 @@
package ice
import "fmt"
// CandidateRelatedAddress convey transport addresses related to the
// candidate, useful for diagnostics and other purposes.
type CandidateRelatedAddress struct {
Address string
Port int
}
// String makes CandidateRelatedAddress printable
func (c *CandidateRelatedAddress) String() string {
if c == nil {
return ""
}
return fmt.Sprintf(" related %s:%d", c.Address, c.Port)
}
// Equal allows comparing two CandidateRelatedAddresses.
// The CandidateRelatedAddress are allowed to be nil.
func (c *CandidateRelatedAddress) Equal(other *CandidateRelatedAddress) bool {
if c == nil && other == nil {
return true
}
return c != nil && other != nil &&
c.Address == other.Address &&
c.Port == other.Port
}

View file

@ -0,0 +1,62 @@
package ice
// CandidateType represents the type of candidate
type CandidateType byte
// CandidateType enum
const (
CandidateTypeUnspecified CandidateType = iota
CandidateTypeHost
CandidateTypeServerReflexive
CandidateTypePeerReflexive
CandidateTypeRelay
)
// String makes CandidateType printable
func (c CandidateType) String() string {
switch c {
case CandidateTypeHost:
return "host"
case CandidateTypeServerReflexive:
return "srflx"
case CandidateTypePeerReflexive:
return "prflx"
case CandidateTypeRelay:
return "relay"
case CandidateTypeUnspecified:
return "Unknown candidate type"
}
return "Unknown candidate type"
}
// Preference returns the preference weight of a CandidateType
//
// 4.1.2.2. Guidelines for Choosing Type and Local Preferences
// The RECOMMENDED values are 126 for host candidates, 100
// for server reflexive candidates, 110 for peer reflexive candidates,
// and 0 for relayed candidates.
func (c CandidateType) Preference() uint16 {
switch c {
case CandidateTypeHost:
return 126
case CandidateTypePeerReflexive:
return 110
case CandidateTypeServerReflexive:
return 100
case CandidateTypeRelay, CandidateTypeUnspecified:
return 0
}
return 0
}
func containsCandidateType(candidateType CandidateType, candidateTypeList []CandidateType) bool {
if candidateTypeList == nil {
return false
}
for _, ct := range candidateTypeList {
if ct == candidateType {
return true
}
}
return false
}

View file

@ -0,0 +1,20 @@
#
# DO NOT EDIT THIS FILE
#
# It is automatically copied from https://github.com/pion/.goassets repository.
#
coverage:
status:
project:
default:
# Allow decreasing 2% of total coverage to avoid noise.
threshold: 2%
patch:
default:
target: 70%
only_pulls: true
ignore:
- "examples/*"
- "examples/**/*"

View file

@ -0,0 +1,37 @@
package ice
import (
"context"
"time"
)
func (a *Agent) context() context.Context {
return agentContext(a.done)
}
type agentContext chan struct{}
// Done implements context.Context
func (a agentContext) Done() <-chan struct{} {
return (chan struct{})(a)
}
// Err implements context.Context
func (a agentContext) Err() error {
select {
case <-(chan struct{})(a):
return ErrRunCanceled
default:
return nil
}
}
// Deadline implements context.Context
func (a agentContext) Deadline() (deadline time.Time, ok bool) {
return time.Time{}, false
}
// Value implements context.Context
func (a agentContext) Value(key interface{}) interface{} {
return nil
}

View file

@ -0,0 +1,132 @@
package ice
import "errors"
var (
// ErrUnknownType indicates an error with Unknown info.
ErrUnknownType = errors.New("Unknown")
// ErrSchemeType indicates the scheme type could not be parsed.
ErrSchemeType = errors.New("unknown scheme type")
// ErrSTUNQuery indicates query arguments are provided in a STUN URL.
ErrSTUNQuery = errors.New("queries not supported in stun address")
// ErrInvalidQuery indicates an malformed query is provided.
ErrInvalidQuery = errors.New("invalid query")
// ErrHost indicates malformed hostname is provided.
ErrHost = errors.New("invalid hostname")
// ErrPort indicates malformed port is provided.
ErrPort = errors.New("invalid port")
// ErrLocalUfragInsufficientBits indicates local username fragment insufficient bits are provided.
// Have to be at least 24 bits long
ErrLocalUfragInsufficientBits = errors.New("local username fragment is less than 24 bits long")
// ErrLocalPwdInsufficientBits indicates local passoword insufficient bits are provided.
// Have to be at least 128 bits long
ErrLocalPwdInsufficientBits = errors.New("local password is less than 128 bits long")
// ErrProtoType indicates an unsupported transport type was provided.
ErrProtoType = errors.New("invalid transport protocol type")
// ErrClosed indicates the agent is closed
ErrClosed = errors.New("the agent is closed")
// ErrNoCandidatePairs indicates agent does not have a valid candidate pair
ErrNoCandidatePairs = errors.New("no candidate pairs available")
// ErrCanceledByCaller indicates agent connection was canceled by the caller
ErrCanceledByCaller = errors.New("connecting canceled by caller")
// ErrMultipleStart indicates agent was started twice
ErrMultipleStart = errors.New("attempted to start agent twice")
// ErrRemoteUfragEmpty indicates agent was started with an empty remote ufrag
ErrRemoteUfragEmpty = errors.New("remote ufrag is empty")
// ErrRemotePwdEmpty indicates agent was started with an empty remote pwd
ErrRemotePwdEmpty = errors.New("remote pwd is empty")
// ErrNoOnCandidateHandler indicates agent was started without OnCandidate
ErrNoOnCandidateHandler = errors.New("no OnCandidate provided")
// ErrMultipleGatherAttempted indicates GatherCandidates has been called multiple times
ErrMultipleGatherAttempted = errors.New("attempting to gather candidates during gathering state")
// ErrUsernameEmpty indicates agent was give TURN URL with an empty Username
ErrUsernameEmpty = errors.New("username is empty")
// ErrPasswordEmpty indicates agent was give TURN URL with an empty Password
ErrPasswordEmpty = errors.New("password is empty")
// ErrAddressParseFailed indicates we were unable to parse a candidate address
ErrAddressParseFailed = errors.New("failed to parse address")
// ErrLiteUsingNonHostCandidates indicates non host candidates were selected for a lite agent
ErrLiteUsingNonHostCandidates = errors.New("lite agents must only use host candidates")
// ErrUselessUrlsProvided indicates that one or more URL was provided to the agent but no host
// candidate required them
ErrUselessUrlsProvided = errors.New("agent does not need URL with selected candidate types")
// ErrUnsupportedNAT1To1IPCandidateType indicates that the specified NAT1To1IPCandidateType is
// unsupported
ErrUnsupportedNAT1To1IPCandidateType = errors.New("unsupported 1:1 NAT IP candidate type")
// ErrInvalidNAT1To1IPMapping indicates that the given 1:1 NAT IP mapping is invalid
ErrInvalidNAT1To1IPMapping = errors.New("invalid 1:1 NAT IP mapping")
// ErrExternalMappedIPNotFound in NAT1To1IPMapping
ErrExternalMappedIPNotFound = errors.New("external mapped IP not found")
// ErrMulticastDNSWithNAT1To1IPMapping indicates that the mDNS gathering cannot be used along
// with 1:1 NAT IP mapping for host candidate.
ErrMulticastDNSWithNAT1To1IPMapping = errors.New("mDNS gathering cannot be used with 1:1 NAT IP mapping for host candidate")
// ErrIneffectiveNAT1To1IPMappingHost indicates that 1:1 NAT IP mapping for host candidate is
// requested, but the host candidate type is disabled.
ErrIneffectiveNAT1To1IPMappingHost = errors.New("1:1 NAT IP mapping for host candidate ineffective")
// ErrIneffectiveNAT1To1IPMappingSrflx indicates that 1:1 NAT IP mapping for srflx candidate is
// requested, but the srflx candidate type is disabled.
ErrIneffectiveNAT1To1IPMappingSrflx = errors.New("1:1 NAT IP mapping for srflx candidate ineffective")
// ErrInvalidMulticastDNSHostName indicates an invalid MulticastDNSHostName
ErrInvalidMulticastDNSHostName = errors.New("invalid mDNS HostName, must end with .local and can only contain a single '.'")
// ErrRestartWhenGathering indicates Restart was called when Agent is in GatheringStateGathering
ErrRestartWhenGathering = errors.New("ICE Agent can not be restarted when gathering")
// ErrRunCanceled indicates a run operation was canceled by its individual done
ErrRunCanceled = errors.New("run was canceled by done")
// ErrTCPMuxNotInitialized indicates TCPMux is not initialized and that invalidTCPMux is used.
ErrTCPMuxNotInitialized = errors.New("TCPMux is not initialized")
// ErrTCPRemoteAddrAlreadyExists indicates we already have the connection with same remote addr.
ErrTCPRemoteAddrAlreadyExists = errors.New("conn with same remote addr already exists")
errSendPacket = errors.New("failed to send packet")
errAttributeTooShortICECandidate = errors.New("attribute not long enough to be ICE candidate")
errParseComponent = errors.New("could not parse component")
errParsePriority = errors.New("could not parse priority")
errParsePort = errors.New("could not parse port")
errParseRelatedAddr = errors.New("could not parse related addresses")
errParseTypType = errors.New("could not parse typtype")
errUnknownCandidateTyp = errors.New("unknown candidate typ")
errGetXorMappedAddrResponse = errors.New("failed to get XOR-MAPPED-ADDRESS response")
errConnectionAddrAlreadyExist = errors.New("connection with same remote address already exists")
errReadingStreamingPacket = errors.New("error reading streaming packet")
errWriting = errors.New("error writing to")
errClosingConnection = errors.New("error closing connection")
errDetermineNetworkType = errors.New("unable to determine networkType")
errMissingProtocolScheme = errors.New("missing protocol scheme")
errTooManyColonsAddr = errors.New("too many colons in address")
errRead = errors.New("unexpected error trying to read")
errUnknownRole = errors.New("unknown role")
errMismatchUsername = errors.New("username mismatch")
errICEWriteSTUNMessage = errors.New("the ICE conn can't write STUN messages")
)

View file

@ -0,0 +1,143 @@
package ice
import (
"net"
"strings"
)
func validateIPString(ipStr string) (net.IP, bool, error) {
ip := net.ParseIP(ipStr)
if ip == nil {
return nil, false, ErrInvalidNAT1To1IPMapping
}
return ip, (ip.To4() != nil), nil
}
// ipMapping holds the mapping of local and external IP address for a particular IP family
type ipMapping struct {
ipSole net.IP // when non-nil, this is the sole external IP for one local IP assumed
ipMap map[string]net.IP // local-to-external IP mapping (k: local, v: external)
}
func (m *ipMapping) setSoleIP(ip net.IP) error {
if m.ipSole != nil || len(m.ipMap) > 0 {
return ErrInvalidNAT1To1IPMapping
}
m.ipSole = ip
return nil
}
func (m *ipMapping) addIPMapping(locIP, extIP net.IP) error {
if m.ipSole != nil {
return ErrInvalidNAT1To1IPMapping
}
locIPStr := locIP.String()
// check if dup of local IP
if _, ok := m.ipMap[locIPStr]; ok {
return ErrInvalidNAT1To1IPMapping
}
m.ipMap[locIPStr] = extIP
return nil
}
func (m *ipMapping) findExternalIP(locIP net.IP) (net.IP, error) {
if m.ipSole != nil {
return m.ipSole, nil
}
extIP, ok := m.ipMap[locIP.String()]
if !ok {
return nil, ErrExternalMappedIPNotFound
}
return extIP, nil
}
type externalIPMapper struct {
ipv4Mapping ipMapping
ipv6Mapping ipMapping
candidateType CandidateType
}
func newExternalIPMapper(candidateType CandidateType, ips []string) (*externalIPMapper, error) { //nolint:gocognit
if len(ips) == 0 {
return nil, nil
}
if candidateType == CandidateTypeUnspecified {
candidateType = CandidateTypeHost // defaults to host
} else if candidateType != CandidateTypeHost && candidateType != CandidateTypeServerReflexive {
return nil, ErrUnsupportedNAT1To1IPCandidateType
}
m := &externalIPMapper{
ipv4Mapping: ipMapping{ipMap: map[string]net.IP{}},
ipv6Mapping: ipMapping{ipMap: map[string]net.IP{}},
candidateType: candidateType,
}
for _, extIPStr := range ips {
ipPair := strings.Split(extIPStr, "/")
if len(ipPair) == 0 || len(ipPair) > 2 {
return nil, ErrInvalidNAT1To1IPMapping
}
extIP, isExtIPv4, err := validateIPString(ipPair[0])
if err != nil {
return nil, err
}
if len(ipPair) == 1 {
if isExtIPv4 {
if err := m.ipv4Mapping.setSoleIP(extIP); err != nil {
return nil, err
}
} else {
if err := m.ipv6Mapping.setSoleIP(extIP); err != nil {
return nil, err
}
}
} else {
locIP, isLocIPv4, err := validateIPString(ipPair[1])
if err != nil {
return nil, err
}
if isExtIPv4 {
if !isLocIPv4 {
return nil, ErrInvalidNAT1To1IPMapping
}
if err := m.ipv4Mapping.addIPMapping(locIP, extIP); err != nil {
return nil, err
}
} else {
if isLocIPv4 {
return nil, ErrInvalidNAT1To1IPMapping
}
if err := m.ipv6Mapping.addIPMapping(locIP, extIP); err != nil {
return nil, err
}
}
}
}
return m, nil
}
func (m *externalIPMapper) findExternalIP(localIPStr string) (net.IP, error) {
locIP, isLocIPv4, err := validateIPString(localIPStr)
if err != nil {
return nil, err
}
if isLocIPv4 {
return m.ipv4Mapping.findExternalIP(locIP)
}
return m.ipv6Mapping.findExternalIP(locIP)
}

View file

@ -0,0 +1,497 @@
package ice
import (
"context"
"crypto/tls"
"errors"
"fmt"
"net"
"reflect"
"sync"
"time"
"github.com/pion/dtls/v2"
"github.com/pion/logging"
"github.com/pion/turn/v2"
)
const (
stunGatherTimeout = time.Second * 5
)
type closeable interface {
Close() error
}
// Close a net.Conn and log if we have a failure
func closeConnAndLog(c closeable, log logging.LeveledLogger, msg string) {
if c == nil || (reflect.ValueOf(c).Kind() == reflect.Ptr && reflect.ValueOf(c).IsNil()) {
log.Warnf("Conn is not allocated (%s)", msg)
return
}
log.Warnf(msg)
if err := c.Close(); err != nil {
log.Warnf("Failed to close conn: %v", err)
}
}
// fakePacketConn wraps a net.Conn and emulates net.PacketConn
type fakePacketConn struct {
nextConn net.Conn
}
func (f *fakePacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {
n, err = f.nextConn.Read(p)
addr = f.nextConn.RemoteAddr()
return
}
func (f *fakePacketConn) Close() error { return f.nextConn.Close() }
func (f *fakePacketConn) LocalAddr() net.Addr { return f.nextConn.LocalAddr() }
func (f *fakePacketConn) SetDeadline(t time.Time) error { return f.nextConn.SetDeadline(t) }
func (f *fakePacketConn) SetReadDeadline(t time.Time) error { return f.nextConn.SetReadDeadline(t) }
func (f *fakePacketConn) SetWriteDeadline(t time.Time) error { return f.nextConn.SetWriteDeadline(t) }
func (f *fakePacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {
return f.nextConn.Write(p)
}
// GatherCandidates initiates the trickle based gathering process.
func (a *Agent) GatherCandidates() error {
var gatherErr error
if runErr := a.run(a.context(), func(ctx context.Context, agent *Agent) {
if a.gatheringState != GatheringStateNew {
gatherErr = ErrMultipleGatherAttempted
return
} else if a.onCandidateHdlr.Load() == nil {
gatherErr = ErrNoOnCandidateHandler
return
}
a.gatherCandidateCancel() // Cancel previous gathering routine
ctx, cancel := context.WithCancel(ctx)
a.gatherCandidateCancel = cancel
go a.gatherCandidates(ctx)
}); runErr != nil {
return runErr
}
return gatherErr
}
func (a *Agent) gatherCandidates(ctx context.Context) {
if err := a.setGatheringState(GatheringStateGathering); err != nil {
a.log.Warnf("failed to set gatheringState to GatheringStateGathering: %v", err)
return
}
var wg sync.WaitGroup
for _, t := range a.candidateTypes {
switch t {
case CandidateTypeHost:
wg.Add(1)
go func() {
a.gatherCandidatesLocal(ctx, a.networkTypes)
wg.Done()
}()
case CandidateTypeServerReflexive:
wg.Add(1)
go func() {
a.gatherCandidatesSrflx(ctx, a.urls, a.networkTypes)
wg.Done()
}()
if a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeServerReflexive {
wg.Add(1)
go func() {
a.gatherCandidatesSrflxMapped(ctx, a.networkTypes)
wg.Done()
}()
}
case CandidateTypeRelay:
wg.Add(1)
go func() {
a.gatherCandidatesRelay(ctx, a.urls)
wg.Done()
}()
case CandidateTypePeerReflexive, CandidateTypeUnspecified:
}
}
// Block until all STUN and TURN URLs have been gathered (or timed out)
wg.Wait()
if err := a.setGatheringState(GatheringStateComplete); err != nil {
a.log.Warnf("failed to set gatheringState to GatheringStateComplete: %v", err)
}
}
func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []NetworkType) { //nolint:gocognit
networks := map[string]struct{}{}
for _, networkType := range networkTypes {
if networkType.IsTCP() {
networks[tcp] = struct{}{}
} else {
networks[udp] = struct{}{}
}
}
localIPs, err := localInterfaces(a.net, a.interfaceFilter, networkTypes)
if err != nil {
a.log.Warnf("failed to iterate local interfaces, host candidates will not be gathered %s", err)
return
}
for _, ip := range localIPs {
mappedIP := ip
if a.mDNSMode != MulticastDNSModeQueryAndGather && a.extIPMapper != nil && a.extIPMapper.candidateType == CandidateTypeHost {
if _mappedIP, err := a.extIPMapper.findExternalIP(ip.String()); err == nil {
mappedIP = _mappedIP
} else {
a.log.Warnf("1:1 NAT mapping is enabled but no external IP is found for %s\n", ip.String())
}
}
address := mappedIP.String()
if a.mDNSMode == MulticastDNSModeQueryAndGather {
address = a.mDNSName
}
for network := range networks {
var port int
var conn net.PacketConn
var err error
var tcpType TCPType
switch network {
case tcp:
// Handle ICE TCP passive mode
a.log.Debugf("GetConn by ufrag: %s\n", a.localUfrag)
conn, err = a.tcpMux.GetConnByUfrag(a.localUfrag)
if err != nil {
if !errors.Is(err, ErrTCPMuxNotInitialized) {
a.log.Warnf("error getting tcp conn by ufrag: %s %s %s\n", network, ip, a.localUfrag)
}
continue
}
port = conn.LocalAddr().(*net.TCPAddr).Port
tcpType = TCPTypePassive
// is there a way to verify that the listen address is even
// accessible from the current interface.
case udp:
conn, err = listenUDPInPortRange(a.net, a.log, int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: ip, Port: 0})
if err != nil {
a.log.Warnf("could not listen %s %s\n", network, ip)
continue
}
port = conn.LocalAddr().(*net.UDPAddr).Port
}
hostConfig := CandidateHostConfig{
Network: network,
Address: address,
Port: port,
Component: ComponentRTP,
TCPType: tcpType,
}
c, err := NewCandidateHost(&hostConfig)
if err != nil {
closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to create host candidate: %s %s %d: %v\n", network, mappedIP, port, err))
continue
}
if a.mDNSMode == MulticastDNSModeQueryAndGather {
if err = c.setIP(ip); err != nil {
closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to create host candidate: %s %s %d: %v\n", network, mappedIP, port, err))
continue
}
}
if err := a.addCandidate(ctx, c, conn); err != nil {
if closeErr := c.close(); closeErr != nil {
a.log.Warnf("Failed to close candidate: %v", closeErr)
}
a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
}
}
}
}
func (a *Agent) gatherCandidatesSrflxMapped(ctx context.Context, networkTypes []NetworkType) {
var wg sync.WaitGroup
defer wg.Wait()
for _, networkType := range networkTypes {
if networkType.IsTCP() {
continue
}
network := networkType.String()
wg.Add(1)
go func() {
defer wg.Done()
conn, err := listenUDPInPortRange(a.net, a.log, int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: nil, Port: 0})
if err != nil {
a.log.Warnf("Failed to listen %s: %v\n", network, err)
return
}
laddr := conn.LocalAddr().(*net.UDPAddr)
mappedIP, err := a.extIPMapper.findExternalIP(laddr.IP.String())
if err != nil {
closeConnAndLog(conn, a.log, fmt.Sprintf("1:1 NAT mapping is enabled but no external IP is found for %s\n", laddr.IP.String()))
return
}
srflxConfig := CandidateServerReflexiveConfig{
Network: network,
Address: mappedIP.String(),
Port: laddr.Port,
Component: ComponentRTP,
RelAddr: laddr.IP.String(),
RelPort: laddr.Port,
}
c, err := NewCandidateServerReflexive(&srflxConfig)
if err != nil {
closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to create server reflexive candidate: %s %s %d: %v\n",
network,
mappedIP.String(),
laddr.Port,
err))
return
}
if err := a.addCandidate(ctx, c, conn); err != nil {
if closeErr := c.close(); closeErr != nil {
a.log.Warnf("Failed to close candidate: %v", closeErr)
}
a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
}
}()
}
}
func (a *Agent) gatherCandidatesSrflx(ctx context.Context, urls []*URL, networkTypes []NetworkType) {
var wg sync.WaitGroup
defer wg.Wait()
for _, networkType := range networkTypes {
if networkType.IsTCP() {
continue
}
for i := range urls {
wg.Add(1)
go func(url URL, network string) {
defer wg.Done()
hostPort := fmt.Sprintf("%s:%d", url.Host, url.Port)
serverAddr, err := a.net.ResolveUDPAddr(network, hostPort)
if err != nil {
a.log.Warnf("failed to resolve stun host: %s: %v", hostPort, err)
return
}
conn, err := listenUDPInPortRange(a.net, a.log, int(a.portmax), int(a.portmin), network, &net.UDPAddr{IP: nil, Port: 0})
if err != nil {
closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to listen for %s: %v\n", serverAddr.String(), err))
return
}
xoraddr, err := getXORMappedAddr(conn, serverAddr, stunGatherTimeout)
if err != nil {
closeConnAndLog(conn, a.log, fmt.Sprintf("could not get server reflexive address %s %s: %v\n", network, url, err))
return
}
ip := xoraddr.IP
port := xoraddr.Port
laddr := conn.LocalAddr().(*net.UDPAddr)
srflxConfig := CandidateServerReflexiveConfig{
Network: network,
Address: ip.String(),
Port: port,
Component: ComponentRTP,
RelAddr: laddr.IP.String(),
RelPort: laddr.Port,
}
c, err := NewCandidateServerReflexive(&srflxConfig)
if err != nil {
closeConnAndLog(conn, a.log, fmt.Sprintf("Failed to create server reflexive candidate: %s %s %d: %v\n", network, ip, port, err))
return
}
if err := a.addCandidate(ctx, c, conn); err != nil {
if closeErr := c.close(); closeErr != nil {
a.log.Warnf("Failed to close candidate: %v", closeErr)
}
a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
}
}(*urls[i], networkType.String())
}
}
}
func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*URL) { //nolint:gocognit
var wg sync.WaitGroup
defer wg.Wait()
network := NetworkTypeUDP4.String()
for i := range urls {
switch {
case urls[i].Scheme != SchemeTypeTURN && urls[i].Scheme != SchemeTypeTURNS:
continue
case urls[i].Username == "":
a.log.Errorf("Failed to gather relay candidates: %v", ErrUsernameEmpty)
return
case urls[i].Password == "":
a.log.Errorf("Failed to gather relay candidates: %v", ErrPasswordEmpty)
return
}
wg.Add(1)
go func(url URL) {
defer wg.Done()
TURNServerAddr := fmt.Sprintf("%s:%d", url.Host, url.Port)
var (
locConn net.PacketConn
err error
RelAddr string
RelPort int
)
switch {
case url.Proto == ProtoTypeUDP && url.Scheme == SchemeTypeTURN:
if locConn, err = a.net.ListenPacket(network, "0.0.0.0:0"); err != nil {
a.log.Warnf("Failed to listen %s: %v\n", network, err)
return
}
RelAddr = locConn.LocalAddr().(*net.UDPAddr).IP.String()
RelPort = locConn.LocalAddr().(*net.UDPAddr).Port
case a.proxyDialer != nil && url.Proto == ProtoTypeTCP &&
(url.Scheme == SchemeTypeTURN || url.Scheme == SchemeTypeTURNS):
conn, connectErr := a.proxyDialer.Dial(NetworkTypeTCP4.String(), TURNServerAddr)
if connectErr != nil {
a.log.Warnf("Failed to Dial TCP Addr %s via proxy dialer: %v\n", TURNServerAddr, connectErr)
return
}
RelAddr = conn.LocalAddr().(*net.TCPAddr).IP.String()
RelPort = conn.LocalAddr().(*net.TCPAddr).Port
locConn = turn.NewSTUNConn(conn)
case url.Proto == ProtoTypeTCP && url.Scheme == SchemeTypeTURN:
tcpAddr, connectErr := net.ResolveTCPAddr(NetworkTypeTCP4.String(), TURNServerAddr)
if connectErr != nil {
a.log.Warnf("Failed to resolve TCP Addr %s: %v\n", TURNServerAddr, connectErr)
return
}
conn, connectErr := net.DialTCP(NetworkTypeTCP4.String(), nil, tcpAddr)
if connectErr != nil {
a.log.Warnf("Failed to Dial TCP Addr %s: %v\n", TURNServerAddr, connectErr)
return
}
RelAddr = conn.LocalAddr().(*net.TCPAddr).IP.String()
RelPort = conn.LocalAddr().(*net.TCPAddr).Port
locConn = turn.NewSTUNConn(conn)
case url.Proto == ProtoTypeUDP && url.Scheme == SchemeTypeTURNS:
udpAddr, connectErr := net.ResolveUDPAddr(network, TURNServerAddr)
if connectErr != nil {
a.log.Warnf("Failed to resolve UDP Addr %s: %v\n", TURNServerAddr, connectErr)
return
}
conn, connectErr := dtls.Dial(network, udpAddr, &dtls.Config{
InsecureSkipVerify: a.insecureSkipVerify, //nolint:gosec
})
if connectErr != nil {
a.log.Warnf("Failed to Dial DTLS Addr %s: %v\n", TURNServerAddr, connectErr)
return
}
RelAddr = conn.LocalAddr().(*net.UDPAddr).IP.String()
RelPort = conn.LocalAddr().(*net.UDPAddr).Port
locConn = &fakePacketConn{conn}
case url.Proto == ProtoTypeTCP && url.Scheme == SchemeTypeTURNS:
conn, connectErr := tls.Dial(NetworkTypeTCP4.String(), TURNServerAddr, &tls.Config{
InsecureSkipVerify: a.insecureSkipVerify, //nolint:gosec
})
if connectErr != nil {
a.log.Warnf("Failed to Dial TLS Addr %s: %v\n", TURNServerAddr, connectErr)
return
}
RelAddr = conn.LocalAddr().(*net.TCPAddr).IP.String()
RelPort = conn.LocalAddr().(*net.TCPAddr).Port
locConn = turn.NewSTUNConn(conn)
default:
a.log.Warnf("Unable to handle URL in gatherCandidatesRelay %v\n", url)
return
}
client, err := turn.NewClient(&turn.ClientConfig{
TURNServerAddr: TURNServerAddr,
Conn: locConn,
Username: url.Username,
Password: url.Password,
LoggerFactory: a.loggerFactory,
Net: a.net,
})
if err != nil {
closeConnAndLog(locConn, a.log, fmt.Sprintf("Failed to build new turn.Client %s %s\n", TURNServerAddr, err))
return
}
if err = client.Listen(); err != nil {
client.Close()
closeConnAndLog(locConn, a.log, fmt.Sprintf("Failed to listen on turn.Client %s %s\n", TURNServerAddr, err))
return
}
relayConn, err := client.Allocate()
if err != nil {
client.Close()
closeConnAndLog(locConn, a.log, fmt.Sprintf("Failed to allocate on turn.Client %s %s\n", TURNServerAddr, err))
return
}
raddr := relayConn.LocalAddr().(*net.UDPAddr)
relayConfig := CandidateRelayConfig{
Network: network,
Component: ComponentRTP,
Address: raddr.IP.String(),
Port: raddr.Port,
RelAddr: RelAddr,
RelPort: RelPort,
OnClose: func() error {
client.Close()
return locConn.Close()
},
}
relayConnClose := func() {
if relayConErr := relayConn.Close(); relayConErr != nil {
a.log.Warnf("Failed to close relay %v", relayConErr)
}
}
candidate, err := NewCandidateRelay(&relayConfig)
if err != nil {
relayConnClose()
client.Close()
closeConnAndLog(locConn, a.log, fmt.Sprintf("Failed to create relay candidate: %s %s: %v\n", network, raddr.String(), err))
return
}
if err := a.addCandidate(ctx, candidate, relayConn); err != nil {
relayConnClose()
if closeErr := candidate.close(); closeErr != nil {
a.log.Warnf("Failed to close candidate: %v", closeErr)
}
a.log.Warnf("Failed to append to localCandidates and run onCandidateHdlr: %v\n", err)
}
}(*urls[i])
}
}

View file

@ -0,0 +1,16 @@
module github.com/pion/ice/v2
go 1.13
require (
github.com/google/uuid v1.1.2
github.com/pion/dtls/v2 v2.0.4
github.com/pion/logging v0.2.2
github.com/pion/mdns v0.0.4
github.com/pion/randutil v0.1.0
github.com/pion/stun v0.3.5
github.com/pion/transport v0.12.0
github.com/pion/turn/v2 v2.0.5
github.com/stretchr/testify v1.6.1
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7
)

View file

@ -0,0 +1,60 @@
github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pion/dtls/v2 v2.0.4 h1:WuUcqi6oYMu/noNTz92QrF1DaFj4eXbhQ6dzaaAwOiI=
github.com/pion/dtls/v2 v2.0.4/go.mod h1:qAkFscX0ZHoI1E07RfYPoRw3manThveu+mlTDdOxoGI=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY=
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM=
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/transport v0.12.0 h1:UFmOBBZkTZ3LgvLRf/NGrfWdZEubcU6zkLU3PsA9YvU=
github.com/pion/transport v0.12.0/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/turn/v2 v2.0.5 h1:iwMHqDfPEDEOFzwWKT56eFmh6DYC6o/+xnLAEzgISbA=
github.com/pion/turn/v2 v2.0.5/go.mod h1:APg43CFyt/14Uy7heYUOGWdkem/Wu4PhCO/bjyrTqMw=
github.com/pion/udp v0.1.0 h1:uGxQsNyrqG3GLINv36Ff60covYmfrLoxzwnCsIYspXI=
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897 h1:pLI5jrR7OSLijeIDcmRxNmw2api+jEfxLoykJVice/E=
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102 h1:42cLlJJdEh+ySyeUUbEQ5bsTiq8voBeTuweGVkY6Puw=
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7 h1:3uJsdck53FDIpWwLeAXlia9p4C8j0BO2xZrqzKpL0D8=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -0,0 +1,76 @@
package ice
// ConnectionState is an enum showing the state of a ICE Connection
type ConnectionState int
// List of supported States
const (
// ConnectionStateNew ICE agent is gathering addresses
ConnectionStateNew = iota + 1
// ConnectionStateChecking ICE agent has been given local and remote candidates, and is attempting to find a match
ConnectionStateChecking
// ConnectionStateConnected ICE agent has a pairing, but is still checking other pairs
ConnectionStateConnected
// ConnectionStateCompleted ICE agent has finished
ConnectionStateCompleted
// ConnectionStateFailed ICE agent never could successfully connect
ConnectionStateFailed
// ConnectionStateDisconnected ICE agent connected successfully, but has entered a failed state
ConnectionStateDisconnected
// ConnectionStateClosed ICE agent has finished and is no longer handling requests
ConnectionStateClosed
)
func (c ConnectionState) String() string {
switch c {
case ConnectionStateNew:
return "New"
case ConnectionStateChecking:
return "Checking"
case ConnectionStateConnected:
return "Connected"
case ConnectionStateCompleted:
return "Completed"
case ConnectionStateFailed:
return "Failed"
case ConnectionStateDisconnected:
return "Disconnected"
case ConnectionStateClosed:
return "Closed"
default:
return "Invalid"
}
}
// GatheringState describes the state of the candidate gathering process
type GatheringState int
const (
// GatheringStateNew indicates candidate gatering is not yet started
GatheringStateNew GatheringState = iota + 1
// GatheringStateGathering indicates candidate gatering is ongoing
GatheringStateGathering
// GatheringStateComplete indicates candidate gatering has been completed
GatheringStateComplete
)
func (t GatheringState) String() string {
switch t {
case GatheringStateNew:
return "new"
case GatheringStateGathering:
return "gathering"
case GatheringStateComplete:
return "complete"
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,87 @@
package ice
import (
"encoding/binary"
"github.com/pion/stun"
)
// tiebreaker is common helper for ICE-{CONTROLLED,CONTROLLING}
// and represents the so-called tiebreaker number.
type tiebreaker uint64
const tiebreakerSize = 8 // 64 bit
// AddToAs adds tiebreaker value to m as t attribute.
func (a tiebreaker) AddToAs(m *stun.Message, t stun.AttrType) error {
v := make([]byte, tiebreakerSize)
binary.BigEndian.PutUint64(v, uint64(a))
m.Add(t, v)
return nil
}
// GetFromAs decodes tiebreaker value in message getting it as for t type.
func (a *tiebreaker) GetFromAs(m *stun.Message, t stun.AttrType) error {
v, err := m.Get(t)
if err != nil {
return err
}
if err = stun.CheckSize(t, len(v), tiebreakerSize); err != nil {
return err
}
*a = tiebreaker(binary.BigEndian.Uint64(v))
return nil
}
// AttrControlled represents ICE-CONTROLLED attribute.
type AttrControlled uint64
// AddTo adds ICE-CONTROLLED to message.
func (c AttrControlled) AddTo(m *stun.Message) error {
return tiebreaker(c).AddToAs(m, stun.AttrICEControlled)
}
// GetFrom decodes ICE-CONTROLLED from message.
func (c *AttrControlled) GetFrom(m *stun.Message) error {
return (*tiebreaker)(c).GetFromAs(m, stun.AttrICEControlled)
}
// AttrControlling represents ICE-CONTROLLING attribute.
type AttrControlling uint64
// AddTo adds ICE-CONTROLLING to message.
func (c AttrControlling) AddTo(m *stun.Message) error {
return tiebreaker(c).AddToAs(m, stun.AttrICEControlling)
}
// GetFrom decodes ICE-CONTROLLING from message.
func (c *AttrControlling) GetFrom(m *stun.Message) error {
return (*tiebreaker)(c).GetFromAs(m, stun.AttrICEControlling)
}
// AttrControl is helper that wraps ICE-{CONTROLLED,CONTROLLING}.
type AttrControl struct {
Role Role
Tiebreaker uint64
}
// AddTo adds ICE-CONTROLLED or ICE-CONTROLLING attribute depending on Role.
func (c AttrControl) AddTo(m *stun.Message) error {
if c.Role == Controlling {
return tiebreaker(c.Tiebreaker).AddToAs(m, stun.AttrICEControlling)
}
return tiebreaker(c.Tiebreaker).AddToAs(m, stun.AttrICEControlled)
}
// GetFrom decodes Role and Tiebreaker value from message.
func (c *AttrControl) GetFrom(m *stun.Message) error {
if m.Contains(stun.AttrICEControlling) {
c.Role = Controlling
return (*tiebreaker)(&c.Tiebreaker).GetFromAs(m, stun.AttrICEControlling)
}
if m.Contains(stun.AttrICEControlled) {
c.Role = Controlled
return (*tiebreaker)(&c.Tiebreaker).GetFromAs(m, stun.AttrICEControlled)
}
return stun.ErrAttributeNotFound
}

View file

@ -0,0 +1,63 @@
package ice
import (
"net"
"github.com/google/uuid"
"github.com/pion/logging"
"github.com/pion/mdns"
"golang.org/x/net/ipv4"
)
// MulticastDNSMode represents the different Multicast modes ICE can run in
type MulticastDNSMode byte
// MulticastDNSMode enum
const (
// MulticastDNSModeDisabled means remote mDNS candidates will be discarded, and local host candidates will use IPs
MulticastDNSModeDisabled MulticastDNSMode = iota + 1
// MulticastDNSModeQueryOnly means remote mDNS candidates will be accepted, and local host candidates will use IPs
MulticastDNSModeQueryOnly
// MulticastDNSModeQueryAndGather means remote mDNS candidates will be accepted, and local host candidates will use mDNS
MulticastDNSModeQueryAndGather
)
func generateMulticastDNSName() (string, error) {
// https://tools.ietf.org/id/draft-ietf-rtcweb-mdns-ice-candidates-02.html#gathering
// The unique name MUST consist of a version 4 UUID as defined in [RFC4122], followed by “.local”.
u, err := uuid.NewRandom()
return u.String() + ".local", err
}
func createMulticastDNS(mDNSMode MulticastDNSMode, mDNSName string, log logging.LeveledLogger) (*mdns.Conn, MulticastDNSMode, error) {
if mDNSMode == MulticastDNSModeDisabled {
return nil, mDNSMode, nil
}
addr, mdnsErr := net.ResolveUDPAddr("udp4", mdns.DefaultAddress)
if mdnsErr != nil {
return nil, mDNSMode, mdnsErr
}
l, mdnsErr := net.ListenUDP("udp4", addr)
if mdnsErr != nil {
// If ICE fails to start MulticastDNS server just warn the user and continue
log.Errorf("Failed to enable mDNS, continuing in mDNS disabled mode: (%s)", mdnsErr)
return nil, MulticastDNSModeDisabled, nil
}
switch mDNSMode {
case MulticastDNSModeQueryOnly:
conn, err := mdns.Server(ipv4.NewPacketConn(l), &mdns.Config{})
return conn, mDNSMode, err
case MulticastDNSModeQueryAndGather:
conn, err := mdns.Server(ipv4.NewPacketConn(l), &mdns.Config{
LocalNames: []string{mDNSName},
})
return conn, mDNSMode, err
default:
return nil, mDNSMode, nil
}
}

View file

@ -0,0 +1,130 @@
package ice
import (
"fmt"
"net"
"strings"
)
const (
udp = "udp"
tcp = "tcp"
)
func supportedNetworkTypes() []NetworkType {
return []NetworkType{
NetworkTypeUDP4,
NetworkTypeUDP6,
NetworkTypeTCP4,
NetworkTypeTCP6,
}
}
// NetworkType represents the type of network
type NetworkType int
const (
// NetworkTypeUDP4 indicates UDP over IPv4.
NetworkTypeUDP4 NetworkType = iota + 1
// NetworkTypeUDP6 indicates UDP over IPv6.
NetworkTypeUDP6
// NetworkTypeTCP4 indicates TCP over IPv4.
NetworkTypeTCP4
// NetworkTypeTCP6 indicates TCP over IPv6.
NetworkTypeTCP6
)
func (t NetworkType) String() string {
switch t {
case NetworkTypeUDP4:
return "udp4"
case NetworkTypeUDP6:
return "udp6"
case NetworkTypeTCP4:
return "tcp4"
case NetworkTypeTCP6:
return "tcp6"
default:
return ErrUnknownType.Error()
}
}
// IsUDP returns true when network is UDP4 or UDP6.
func (t NetworkType) IsUDP() bool {
return t == NetworkTypeUDP4 || t == NetworkTypeUDP6
}
// IsTCP returns true when network is TCP4 or TCP6.
func (t NetworkType) IsTCP() bool {
return t == NetworkTypeTCP4 || t == NetworkTypeTCP6
}
// NetworkShort returns the short network description
func (t NetworkType) NetworkShort() string {
switch t {
case NetworkTypeUDP4, NetworkTypeUDP6:
return udp
case NetworkTypeTCP4, NetworkTypeTCP6:
return tcp
default:
return ErrUnknownType.Error()
}
}
// IsReliable returns true if the network is reliable
func (t NetworkType) IsReliable() bool {
switch t {
case NetworkTypeUDP4, NetworkTypeUDP6:
return false
case NetworkTypeTCP4, NetworkTypeTCP6:
return true
}
return false
}
// IsIPv4 returns whether the network type is IPv4 or not.
func (t NetworkType) IsIPv4() bool {
switch t {
case NetworkTypeUDP4, NetworkTypeTCP4:
return true
case NetworkTypeUDP6, NetworkTypeTCP6:
return false
}
return false
}
// IsIPv6 returns whether the network type is IPv6 or not.
func (t NetworkType) IsIPv6() bool {
switch t {
case NetworkTypeUDP4, NetworkTypeTCP4:
return false
case NetworkTypeUDP6, NetworkTypeTCP6:
return true
}
return false
}
// determineNetworkType determines the type of network based on
// the short network string and an IP address.
func determineNetworkType(network string, ip net.IP) (NetworkType, error) {
ipv4 := ip.To4() != nil
switch {
case strings.HasPrefix(strings.ToLower(network), udp):
if ipv4 {
return NetworkTypeUDP4, nil
}
return NetworkTypeUDP6, nil
case strings.HasPrefix(strings.ToLower(network), tcp):
if ipv4 {
return NetworkTypeTCP4, nil
}
return NetworkTypeTCP6, nil
}
return NetworkType(0), fmt.Errorf("%w from %s %s", errDetermineNetworkType, network, ip)
}

View file

@ -0,0 +1,33 @@
package ice
import (
"encoding/binary"
"github.com/pion/stun"
)
// PriorityAttr represents PRIORITY attribute.
type PriorityAttr uint32
const prioritySize = 4 // 32 bit
// AddTo adds PRIORITY attribute to message.
func (p PriorityAttr) AddTo(m *stun.Message) error {
v := make([]byte, prioritySize)
binary.BigEndian.PutUint32(v, uint32(p))
m.Add(stun.AttrPriority, v)
return nil
}
// GetFrom decodes PRIORITY attribute from message.
func (p *PriorityAttr) GetFrom(m *stun.Message) error {
v, err := m.Get(stun.AttrPriority)
if err != nil {
return err
}
if err = stun.CheckSize(stun.AttrPriority, len(v), prioritySize); err != nil {
return err
}
*p = PriorityAttr(binary.BigEndian.Uint32(v))
return nil
}

View file

@ -0,0 +1,53 @@
package ice
import "github.com/pion/randutil"
const (
runesAlpha = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
runesDigit = "0123456789"
runesCandidateIDFoundation = runesAlpha + runesDigit + "+/"
lenUFrag = 16
lenPwd = 32
)
// Seeding random generator each time limits number of generated sequence to 31-bits,
// and causes collision on low time accuracy environments.
// Use global random generator seeded by crypto grade random.
var (
globalMathRandomGenerator = randutil.NewMathRandomGenerator() //nolint:gochecknoglobals
globalCandidateIDGenerator = candidateIDGenerator{globalMathRandomGenerator} //nolint:gochecknoglobals
)
// candidateIDGenerator is a random candidate ID generator.
// Candidate ID is used in SDP and always shared to the other peer.
// It doesn't require cryptographic random.
type candidateIDGenerator struct {
randutil.MathRandomGenerator
}
func newCandidateIDGenerator() *candidateIDGenerator {
return &candidateIDGenerator{
randutil.NewMathRandomGenerator(),
}
}
func (g *candidateIDGenerator) Generate() string {
// https://tools.ietf.org/html/rfc5245#section-15.1
// candidate-id = "candidate" ":" foundation
// foundation = 1*32ice-char
// ice-char = ALPHA / DIGIT / "+" / "/"
return "candidate:" + g.MathRandomGenerator.GenerateString(32, runesCandidateIDFoundation)
}
// generatePwd generates ICE pwd.
// This internally uses generateCryptoRandomString.
func generatePwd() (string, error) {
return randutil.GenerateCryptoRandomString(lenPwd, runesAlpha)
}
// generateUFrag generates ICE user fragment.
// This internally uses generateCryptoRandomString.
func generateUFrag() (string, error) {
return randutil.GenerateCryptoRandomString(lenUFrag, runesAlpha)
}

View file

@ -0,0 +1,15 @@
{
"extends": [
"config:base"
],
"postUpdateOptions": [
"gomodTidy"
],
"commitBody": "Generated by renovateBot",
"packageRules": [
{
"packagePatterns": ["^golang.org/x/"],
"schedule": ["on the first day of the month"]
}
]
}

View file

@ -0,0 +1,43 @@
package ice
import (
"fmt"
)
// Role represents ICE agent role, which can be controlling or controlled.
type Role byte
// Possible ICE agent roles.
const (
Controlling Role = iota
Controlled
)
// UnmarshalText implements TextUnmarshaler.
func (r *Role) UnmarshalText(text []byte) error {
switch string(text) {
case "controlling":
*r = Controlling
case "controlled":
*r = Controlled
default:
return fmt.Errorf("%w %q", errUnknownRole, text)
}
return nil
}
// MarshalText implements TextMarshaler.
func (r Role) MarshalText() (text []byte, err error) {
return []byte(r.String()), nil
}
func (r Role) String() string {
switch r {
case Controlling:
return "controlling"
case Controlled:
return "controlled"
default:
return "unknown"
}
}

View file

@ -0,0 +1,287 @@
package ice
import (
"net"
"time"
"github.com/pion/logging"
"github.com/pion/stun"
)
type pairCandidateSelector interface {
Start()
ContactCandidates()
PingCandidate(local, remote Candidate)
HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr)
HandleBindingRequest(m *stun.Message, local, remote Candidate)
}
type controllingSelector struct {
startTime time.Time
agent *Agent
nominatedPair *candidatePair
log logging.LeveledLogger
}
func (s *controllingSelector) Start() {
s.startTime = time.Now()
s.nominatedPair = nil
}
func (s *controllingSelector) isNominatable(c Candidate) bool {
switch {
case c.Type() == CandidateTypeHost:
return time.Since(s.startTime).Nanoseconds() > s.agent.hostAcceptanceMinWait.Nanoseconds()
case c.Type() == CandidateTypeServerReflexive:
return time.Since(s.startTime).Nanoseconds() > s.agent.srflxAcceptanceMinWait.Nanoseconds()
case c.Type() == CandidateTypePeerReflexive:
return time.Since(s.startTime).Nanoseconds() > s.agent.prflxAcceptanceMinWait.Nanoseconds()
case c.Type() == CandidateTypeRelay:
return time.Since(s.startTime).Nanoseconds() > s.agent.relayAcceptanceMinWait.Nanoseconds()
}
s.log.Errorf("isNominatable invalid candidate type %s", c.Type().String())
return false
}
func (s *controllingSelector) ContactCandidates() {
switch {
case s.agent.getSelectedPair() != nil:
if s.agent.validateSelectedPair() {
s.log.Trace("checking keepalive")
s.agent.checkKeepalive()
}
case s.nominatedPair != nil:
s.nominatePair(s.nominatedPair)
default:
p := s.agent.getBestValidCandidatePair()
if p != nil && s.isNominatable(p.local) && s.isNominatable(p.remote) {
s.log.Tracef("Nominatable pair found, nominating (%s, %s)", p.local.String(), p.remote.String())
p.nominated = true
s.nominatedPair = p
s.nominatePair(p)
return
}
s.agent.pingAllCandidates()
}
}
func (s *controllingSelector) nominatePair(pair *candidatePair) {
// The controlling agent MUST include the USE-CANDIDATE attribute in
// order to nominate a candidate pair (Section 8.1.1). The controlled
// agent MUST NOT include the USE-CANDIDATE attribute in a Binding
// request.
msg, err := stun.Build(stun.BindingRequest, stun.TransactionID,
stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag),
UseCandidate(),
AttrControlling(s.agent.tieBreaker),
PriorityAttr(pair.local.Priority()),
stun.NewShortTermIntegrity(s.agent.remotePwd),
stun.Fingerprint,
)
if err != nil {
s.log.Error(err.Error())
return
}
s.log.Tracef("ping STUN (nominate candidate pair) from %s to %s\n", pair.local.String(), pair.remote.String())
s.agent.sendBindingRequest(msg, pair.local, pair.remote)
}
func (s *controllingSelector) HandleBindingRequest(m *stun.Message, local, remote Candidate) {
s.agent.sendBindingSuccess(m, local, remote)
p := s.agent.findPair(local, remote)
if p == nil {
s.agent.addPair(local, remote)
return
}
if p.state == CandidatePairStateSucceeded && s.nominatedPair == nil && s.agent.getSelectedPair() == nil {
bestPair := s.agent.getBestAvailableCandidatePair()
if bestPair == nil {
s.log.Tracef("No best pair available\n")
} else if bestPair.Equal(p) && s.isNominatable(p.local) && s.isNominatable(p.remote) {
s.log.Tracef("The candidate (%s, %s) is the best candidate available, marking it as nominated\n",
p.local.String(), p.remote.String())
s.nominatedPair = p
s.nominatePair(p)
}
}
}
func (s *controllingSelector) HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr) {
ok, pendingRequest := s.agent.handleInboundBindingSuccess(m.TransactionID)
if !ok {
s.log.Warnf("discard message from (%s), unknown TransactionID 0x%x", remote, m.TransactionID)
return
}
transactionAddr := pendingRequest.destination
// Assert that NAT is not symmetric
// https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1
if !addrEqual(transactionAddr, remoteAddr) {
s.log.Debugf("discard message: transaction source and destination does not match expected(%s), actual(%s)", transactionAddr, remote)
return
}
s.log.Tracef("inbound STUN (SuccessResponse) from %s to %s", remote.String(), local.String())
p := s.agent.findPair(local, remote)
if p == nil {
// This shouldn't happen
s.log.Error("Success response from invalid candidate pair")
return
}
p.state = CandidatePairStateSucceeded
s.log.Tracef("Found valid candidate pair: %s", p)
if pendingRequest.isUseCandidate && s.agent.getSelectedPair() == nil {
s.agent.setSelectedPair(p)
}
}
func (s *controllingSelector) PingCandidate(local, remote Candidate) {
msg, err := stun.Build(stun.BindingRequest, stun.TransactionID,
stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag),
AttrControlling(s.agent.tieBreaker),
PriorityAttr(local.Priority()),
stun.NewShortTermIntegrity(s.agent.remotePwd),
stun.Fingerprint,
)
if err != nil {
s.log.Error(err.Error())
return
}
s.agent.sendBindingRequest(msg, local, remote)
}
type controlledSelector struct {
agent *Agent
log logging.LeveledLogger
}
func (s *controlledSelector) Start() {
}
func (s *controlledSelector) ContactCandidates() {
if s.agent.getSelectedPair() != nil {
if s.agent.validateSelectedPair() {
s.log.Trace("checking keepalive")
s.agent.checkKeepalive()
}
} else {
s.agent.pingAllCandidates()
}
}
func (s *controlledSelector) PingCandidate(local, remote Candidate) {
msg, err := stun.Build(stun.BindingRequest, stun.TransactionID,
stun.NewUsername(s.agent.remoteUfrag+":"+s.agent.localUfrag),
AttrControlled(s.agent.tieBreaker),
PriorityAttr(local.Priority()),
stun.NewShortTermIntegrity(s.agent.remotePwd),
stun.Fingerprint,
)
if err != nil {
s.log.Error(err.Error())
return
}
s.agent.sendBindingRequest(msg, local, remote)
}
func (s *controlledSelector) HandleSuccessResponse(m *stun.Message, local, remote Candidate, remoteAddr net.Addr) {
// nolint:godox
// TODO according to the standard we should specifically answer a failed nomination:
// https://tools.ietf.org/html/rfc8445#section-7.3.1.5
// If the controlled agent does not accept the request from the
// controlling agent, the controlled agent MUST reject the nomination
// request with an appropriate error code response (e.g., 400)
// [RFC5389].
ok, pendingRequest := s.agent.handleInboundBindingSuccess(m.TransactionID)
if !ok {
s.log.Warnf("discard message from (%s), unknown TransactionID 0x%x", remote, m.TransactionID)
return
}
transactionAddr := pendingRequest.destination
// Assert that NAT is not symmetric
// https://tools.ietf.org/html/rfc8445#section-7.2.5.2.1
if !addrEqual(transactionAddr, remoteAddr) {
s.log.Debugf("discard message: transaction source and destination does not match expected(%s), actual(%s)", transactionAddr, remote)
return
}
s.log.Tracef("inbound STUN (SuccessResponse) from %s to %s", remote.String(), local.String())
p := s.agent.findPair(local, remote)
if p == nil {
// This shouldn't happen
s.log.Error("Success response from invalid candidate pair")
return
}
p.state = CandidatePairStateSucceeded
s.log.Tracef("Found valid candidate pair: %s", p)
}
func (s *controlledSelector) HandleBindingRequest(m *stun.Message, local, remote Candidate) {
useCandidate := m.Contains(stun.AttrUseCandidate)
p := s.agent.findPair(local, remote)
if p == nil {
p = s.agent.addPair(local, remote)
}
if useCandidate {
// https://tools.ietf.org/html/rfc8445#section-7.3.1.5
if p.state == CandidatePairStateSucceeded {
// If the state of this pair is Succeeded, it means that the check
// previously sent by this pair produced a successful response and
// generated a valid pair (Section 7.2.5.3.2). The agent sets the
// nominated flag value of the valid pair to true.
if selectedPair := s.agent.getSelectedPair(); selectedPair == nil {
s.agent.setSelectedPair(p)
}
s.agent.sendBindingSuccess(m, local, remote)
} else {
// If the received Binding request triggered a new check to be
// enqueued in the triggered-check queue (Section 7.3.1.4), once the
// check is sent and if it generates a successful response, and
// generates a valid pair, the agent sets the nominated flag of the
// pair to true. If the request fails (Section 7.2.5.2), the agent
// MUST remove the candidate pair from the valid list, set the
// candidate pair state to Failed, and set the checklist state to
// Failed.
s.PingCandidate(local, remote)
}
} else {
s.agent.sendBindingSuccess(m, local, remote)
s.PingCandidate(local, remote)
}
}
type liteSelector struct {
pairCandidateSelector
}
// A lite selector should not contact candidates
func (s *liteSelector) ContactCandidates() {
if _, ok := s.pairCandidateSelector.(*controllingSelector); ok {
// nolint:godox
// pion/ice#96
// TODO: implement lite controlling agent. For now falling back to full agent.
// This only happens if both peers are lite. See RFC 8445 S6.1.1 and S6.2
s.pairCandidateSelector.ContactCandidates()
} else if v, ok := s.pairCandidateSelector.(*controlledSelector); ok {
v.agent.validateSelectedPair()
}
}

View file

@ -0,0 +1,177 @@
package ice
import (
"time"
)
// CandidatePairStats contains ICE candidate pair statistics
type CandidatePairStats struct {
// Timestamp is the timestamp associated with this object.
Timestamp time.Time
// LocalCandidateID is the ID of the local candidate
LocalCandidateID string
// RemoteCandidateID is the ID of the remote candidate
RemoteCandidateID string
// State represents the state of the checklist for the local and remote
// candidates in a pair.
State CandidatePairState
// Nominated is true when this valid pair that should be used for media
// if it is the highest-priority one amongst those whose nominated flag is set
Nominated bool
// PacketsSent represents the total number of packets sent on this candidate pair.
PacketsSent uint32
// PacketsReceived represents the total number of packets received on this candidate pair.
PacketsReceived uint32
// BytesSent represents the total number of payload bytes sent on this candidate pair
// not including headers or padding.
BytesSent uint64
// BytesReceived represents the total number of payload bytes received on this candidate pair
// not including headers or padding.
BytesReceived uint64
// LastPacketSentTimestamp represents the timestamp at which the last packet was
// sent on this particular candidate pair, excluding STUN packets.
LastPacketSentTimestamp time.Time
// LastPacketReceivedTimestamp represents the timestamp at which the last packet
// was received on this particular candidate pair, excluding STUN packets.
LastPacketReceivedTimestamp time.Time
// FirstRequestTimestamp represents the timestamp at which the first STUN request
// was sent on this particular candidate pair.
FirstRequestTimestamp time.Time
// LastRequestTimestamp represents the timestamp at which the last STUN request
// was sent on this particular candidate pair. The average interval between two
// consecutive connectivity checks sent can be calculated with
// (LastRequestTimestamp - FirstRequestTimestamp) / RequestsSent.
LastRequestTimestamp time.Time
// LastResponseTimestamp represents the timestamp at which the last STUN response
// was received on this particular candidate pair.
LastResponseTimestamp time.Time
// TotalRoundTripTime represents the sum of all round trip time measurements
// in seconds since the beginning of the session, based on STUN connectivity
// check responses (ResponsesReceived), including those that reply to requests
// that are sent in order to verify consent. The average round trip time can
// be computed from TotalRoundTripTime by dividing it by ResponsesReceived.
TotalRoundTripTime float64
// CurrentRoundTripTime represents the latest round trip time measured in seconds,
// computed from both STUN connectivity checks, including those that are sent
// for consent verification.
CurrentRoundTripTime float64
// AvailableOutgoingBitrate is calculated by the underlying congestion control
// by combining the available bitrate for all the outgoing RTP streams using
// this candidate pair. The bitrate measurement does not count the size of the
// IP or other transport layers like TCP or UDP. It is similar to the TIAS defined
// in RFC 3890, i.e., it is measured in bits per second and the bitrate is calculated
// over a 1 second window.
AvailableOutgoingBitrate float64
// AvailableIncomingBitrate is calculated by the underlying congestion control
// by combining the available bitrate for all the incoming RTP streams using
// this candidate pair. The bitrate measurement does not count the size of the
// IP or other transport layers like TCP or UDP. It is similar to the TIAS defined
// in RFC 3890, i.e., it is measured in bits per second and the bitrate is
// calculated over a 1 second window.
AvailableIncomingBitrate float64
// CircuitBreakerTriggerCount represents the number of times the circuit breaker
// is triggered for this particular 5-tuple, ceasing transmission.
CircuitBreakerTriggerCount uint32
// RequestsReceived represents the total number of connectivity check requests
// received (including retransmissions). It is impossible for the receiver to
// tell whether the request was sent in order to check connectivity or check
// consent, so all connectivity checks requests are counted here.
RequestsReceived uint64
// RequestsSent represents the total number of connectivity check requests
// sent (not including retransmissions).
RequestsSent uint64
// ResponsesReceived represents the total number of connectivity check responses received.
ResponsesReceived uint64
// ResponsesSent epresents the total number of connectivity check responses sent.
// Since we cannot distinguish connectivity check requests and consent requests,
// all responses are counted.
ResponsesSent uint64
// RetransmissionsReceived represents the total number of connectivity check
// request retransmissions received.
RetransmissionsReceived uint64
// RetransmissionsSent represents the total number of connectivity check
// request retransmissions sent.
RetransmissionsSent uint64
// ConsentRequestsSent represents the total number of consent requests sent.
ConsentRequestsSent uint64
// ConsentExpiredTimestamp represents the timestamp at which the latest valid
// STUN binding response expired.
ConsentExpiredTimestamp time.Time
}
// CandidateStats contains ICE candidate statistics related to the ICETransport objects.
type CandidateStats struct {
// Timestamp is the timestamp associated with this object.
Timestamp time.Time
// ID is the candidate ID
ID string
// NetworkType represents the type of network interface used by the base of a
// local candidate (the address the ICE agent sends from). Only present for
// local candidates; it's not possible to know what type of network interface
// a remote candidate is using.
//
// Note:
// This stat only tells you about the network interface used by the first "hop";
// it's possible that a connection will be bottlenecked by another type of network.
// For example, when using Wi-Fi tethering, the networkType of the relevant candidate
// would be "wifi", even when the next hop is over a cellular connection.
NetworkType NetworkType
// IP is the IP address of the candidate, allowing for IPv4 addresses and
// IPv6 addresses, but fully qualified domain names (FQDNs) are not allowed.
IP string
// Port is the port number of the candidate.
Port int
// CandidateType is the "Type" field of the ICECandidate.
CandidateType CandidateType
// Priority is the "Priority" field of the ICECandidate.
Priority uint32
// URL is the URL of the TURN or STUN server indicated in the that translated
// this IP address. It is the URL address surfaced in an PeerConnectionICEEvent.
URL string
// RelayProtocol is the protocol used by the endpoint to communicate with the
// TURN server. This is only present for local candidates. Valid values for
// the TURN URL protocol is one of udp, tcp, or tls.
RelayProtocol string
// Deleted is true if the candidate has been deleted/freed. For host candidates,
// this means that any network resources (typically a socket) associated with the
// candidate have been released. For TURN candidates, this means the TURN allocation
// is no longer active.
//
// Only defined for local candidates. For remote candidates, this property is not applicable.
Deleted bool
}

View file

@ -0,0 +1,24 @@
package ice
import (
"fmt"
"github.com/pion/stun"
)
func assertInboundUsername(m *stun.Message, expectedUsername string) error {
var username stun.Username
if err := username.GetFrom(m); err != nil {
return err
}
if string(username) != expectedUsername {
return fmt.Errorf("%w expected(%x) actual(%x)", errMismatchUsername, expectedUsername, string(username))
}
return nil
}
func assertInboundMessageIntegrity(m *stun.Message, key []byte) error {
messageIntegrityAttr := stun.MessageIntegrity(key)
return messageIntegrityAttr.Check(m)
}

View file

@ -0,0 +1,295 @@
package ice
import (
"encoding/binary"
"io"
"net"
"strings"
"sync"
"github.com/pion/logging"
"github.com/pion/stun"
)
// TCPMux is allows grouping multiple TCP net.Conns and using them like UDP
// net.PacketConns. The main implementation of this is TCPMuxDefault, and this
// interface exists to:
// 1. prevent SEGV panics when TCPMuxDefault is not initialized by using the
// invalidTCPMux implementation, and
// 2. allow mocking in tests.
type TCPMux interface {
io.Closer
GetConnByUfrag(ufrag string) (net.PacketConn, error)
RemoveConnByUfrag(ufrag string)
}
// invalidTCPMux is an implementation of TCPMux that always returns ErroTCPMuxNotInitialized.
type invalidTCPMux struct {
}
func newInvalidTCPMux() *invalidTCPMux {
return &invalidTCPMux{}
}
// Close implements TCPMux interface.
func (m *invalidTCPMux) Close() error {
return ErrTCPMuxNotInitialized
}
// GetConnByUfrag implements TCPMux interface.
func (m *invalidTCPMux) GetConnByUfrag(ufrag string) (net.PacketConn, error) {
return nil, ErrTCPMuxNotInitialized
}
// RemoveConnByUfrag implements TCPMux interface.
func (m *invalidTCPMux) RemoveConnByUfrag(ufrag string) {}
// TCPMuxDefault muxes TCP net.Conns into net.PacketConns and groups them by
// Ufrag. It is a default implementation of TCPMux interface.
type TCPMuxDefault struct {
params *TCPMuxParams
closed bool
// conns is a map of all tcpPacketConns indexed by ufrag
conns map[string]*tcpPacketConn
mu sync.Mutex
wg sync.WaitGroup
}
// TCPMuxParams are parameters for TCPMux.
type TCPMuxParams struct {
Listener net.Listener
Logger logging.LeveledLogger
ReadBufferSize int
}
// NewTCPMuxDefault creates a new instance of TCPMuxDefault.
func NewTCPMuxDefault(params TCPMuxParams) *TCPMuxDefault {
if params.Logger == nil {
params.Logger = logging.NewDefaultLoggerFactory().NewLogger("ice")
}
m := &TCPMuxDefault{
params: &params,
conns: map[string]*tcpPacketConn{},
}
m.wg.Add(1)
go func() {
defer m.wg.Done()
m.start()
}()
return m
}
func (m *TCPMuxDefault) start() {
m.params.Logger.Infof("Listening TCP on %s\n", m.params.Listener.Addr())
for {
conn, err := m.params.Listener.Accept()
if err != nil {
m.params.Logger.Infof("Error accepting connection: %s\n", err)
return
}
m.params.Logger.Debugf("Accepted connection from: %s to %s", conn.RemoteAddr(), conn.LocalAddr())
m.wg.Add(1)
go func() {
defer m.wg.Done()
m.handleConn(conn)
}()
}
}
// LocalAddr returns the listening address of this TCPMuxDefault.
func (m *TCPMuxDefault) LocalAddr() net.Addr {
return m.params.Listener.Addr()
}
// GetConnByUfrag retrieves an existing or creates a new net.PacketConn.
func (m *TCPMuxDefault) GetConnByUfrag(ufrag string) (net.PacketConn, error) {
m.mu.Lock()
defer m.mu.Unlock()
if m.closed {
return nil, io.ErrClosedPipe
}
conn, ok := m.conns[ufrag]
if ok {
return conn, nil
// return nil, fmt.Errorf("duplicate ufrag %v", ufrag)
}
conn = m.createConn(ufrag, m.LocalAddr())
return conn, nil
}
func (m *TCPMuxDefault) createConn(ufrag string, localAddr net.Addr) *tcpPacketConn {
conn := newTCPPacketConn(tcpPacketParams{
ReadBuffer: m.params.ReadBufferSize,
LocalAddr: localAddr,
Logger: m.params.Logger,
})
m.conns[ufrag] = conn
m.wg.Add(1)
go func() {
defer m.wg.Done()
<-conn.CloseChannel()
m.RemoveConnByUfrag(ufrag)
}()
return conn
}
func (m *TCPMuxDefault) closeAndLogError(closer io.Closer) {
err := closer.Close()
if err != nil {
m.params.Logger.Warnf("Error closing connection: %s", err)
}
}
func (m *TCPMuxDefault) handleConn(conn net.Conn) {
buf := make([]byte, receiveMTU)
n, err := readStreamingPacket(conn, buf)
if err != nil {
m.params.Logger.Warnf("Error reading first packet: %s", err)
return
}
buf = buf[:n]
msg := &stun.Message{
Raw: make([]byte, len(buf)),
}
// Explicitly copy raw buffer so Message can own the memory.
copy(msg.Raw, buf)
if err = msg.Decode(); err != nil {
m.closeAndLogError(conn)
m.params.Logger.Warnf("Failed to handle decode ICE from %s to %s: %v\n", conn.RemoteAddr(), conn.LocalAddr(), err)
return
}
if m == nil || msg.Type.Method != stun.MethodBinding { // not a stun
m.closeAndLogError(conn)
m.params.Logger.Warnf("Not a STUN message from %s to %s\n", conn.RemoteAddr(), conn.LocalAddr())
return
}
for _, attr := range msg.Attributes {
m.params.Logger.Debugf("msg attr: %s\n", attr.String())
}
attr, err := msg.Get(stun.AttrUsername)
if err != nil {
m.closeAndLogError(conn)
m.params.Logger.Warnf("No Username attribute in STUN message from %s to %s\n", conn.RemoteAddr(), conn.LocalAddr())
return
}
ufrag := strings.Split(string(attr), ":")[0]
m.params.Logger.Debugf("Ufrag: %s\n", ufrag)
m.mu.Lock()
defer m.mu.Unlock()
packetConn, ok := m.conns[ufrag]
if !ok {
packetConn = m.createConn(ufrag, conn.LocalAddr())
}
if err := packetConn.AddConn(conn, buf); err != nil {
m.closeAndLogError(conn)
m.params.Logger.Warnf("Error adding conn to tcpPacketConn from %s to %s: %s\n", conn.RemoteAddr(), conn.LocalAddr(), err)
return
}
}
// Close closes the listener and waits for all goroutines to exit.
func (m *TCPMuxDefault) Close() error {
m.mu.Lock()
m.closed = true
for _, conn := range m.conns {
m.closeAndLogError(conn)
}
m.conns = map[string]*tcpPacketConn{}
err := m.params.Listener.Close()
m.mu.Unlock()
m.wg.Wait()
return err
}
// RemoveConnByUfrag closes and removes a net.PacketConn by Ufrag.
func (m *TCPMuxDefault) RemoveConnByUfrag(ufrag string) {
m.mu.Lock()
defer m.mu.Unlock()
if conn, ok := m.conns[ufrag]; ok {
m.closeAndLogError(conn)
delete(m.conns, ufrag)
}
}
const streamingPacketHeaderLen = 2
// readStreamingPacket reads 1 packet from stream
// read packet bytes https://tools.ietf.org/html/rfc4571#section-2
// 2-byte length header prepends each packet:
// 0 1 2 3
// 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
// -----------------------------------------------------------------
// | LENGTH | RTP or RTCP packet ... |
// -----------------------------------------------------------------
func readStreamingPacket(conn net.Conn, buf []byte) (int, error) {
header := make([]byte, streamingPacketHeaderLen)
var bytesRead, n int
var err error
for bytesRead < streamingPacketHeaderLen {
if n, err = conn.Read(header[bytesRead:streamingPacketHeaderLen]); err != nil {
return 0, err
}
bytesRead += n
}
length := int(binary.BigEndian.Uint16(header))
if length > cap(buf) {
return length, io.ErrShortBuffer
}
bytesRead = 0
for bytesRead < length {
if n, err = conn.Read(buf[bytesRead:length]); err != nil {
return 0, err
}
bytesRead += n
}
return bytesRead, nil
}
func writeStreamingPacket(conn net.Conn, buf []byte) (int, error) {
bufferCopy := make([]byte, streamingPacketHeaderLen+len(buf))
binary.BigEndian.PutUint16(bufferCopy, uint16(len(buf)))
copy(bufferCopy[2:], buf)
n, err := conn.Write(bufferCopy)
if err != nil {
return 0, err
}
return n - streamingPacketHeaderLen, nil
}

View file

@ -0,0 +1,240 @@
package ice
import (
"fmt"
"io"
"net"
"sync"
"time"
"github.com/pion/logging"
)
type tcpPacketConn struct {
params *tcpPacketParams
// conns is a map of net.Conns indexed by remote net.Addr.String()
conns map[string]net.Conn
recvChan chan streamingPacket
mu sync.Mutex
wg sync.WaitGroup
closedChan chan struct{}
closeOnce sync.Once
}
type streamingPacket struct {
Data []byte
RAddr net.Addr
Err error
}
type tcpPacketParams struct {
ReadBuffer int
LocalAddr net.Addr
Logger logging.LeveledLogger
}
func newTCPPacketConn(params tcpPacketParams) *tcpPacketConn {
p := &tcpPacketConn{
params: &params,
conns: map[string]net.Conn{},
recvChan: make(chan streamingPacket, params.ReadBuffer),
closedChan: make(chan struct{}),
}
return p
}
func (t *tcpPacketConn) AddConn(conn net.Conn, firstPacketData []byte) error {
t.params.Logger.Infof("AddConn: %s %s", conn.RemoteAddr().Network(), conn.RemoteAddr())
t.mu.Lock()
defer t.mu.Unlock()
select {
case <-t.closedChan:
return io.ErrClosedPipe
default:
}
if _, ok := t.conns[conn.RemoteAddr().String()]; ok {
return fmt.Errorf("%w: %s", errConnectionAddrAlreadyExist, conn.RemoteAddr().String())
}
t.conns[conn.RemoteAddr().String()] = conn
t.wg.Add(1)
go func() {
if firstPacketData != nil {
t.recvChan <- streamingPacket{firstPacketData, conn.RemoteAddr(), nil}
}
defer t.wg.Done()
t.startReading(conn)
}()
return nil
}
func (t *tcpPacketConn) startReading(conn net.Conn) {
buf := make([]byte, receiveMTU)
for {
n, err := readStreamingPacket(conn, buf)
// t.params.Logger.Infof("readStreamingPacket read %d bytes", n)
if err != nil {
t.params.Logger.Infof("%w: %s\n", errReadingStreamingPacket, err)
t.handleRecv(streamingPacket{nil, conn.RemoteAddr(), err})
t.removeConn(conn)
return
}
data := make([]byte, n)
copy(data, buf[:n])
// t.params.Logger.Infof("Writing read streaming packet to recvChan: %d bytes", len(data))
t.handleRecv(streamingPacket{data, conn.RemoteAddr(), nil})
}
}
func (t *tcpPacketConn) handleRecv(pkt streamingPacket) {
t.mu.Lock()
recvChan := t.recvChan
if t.isClosed() {
recvChan = nil
}
t.mu.Unlock()
select {
case recvChan <- pkt:
case <-t.closedChan:
}
}
func (t *tcpPacketConn) isClosed() bool {
select {
case <-t.closedChan:
return true
default:
return false
}
}
// WriteTo is for passive and s-o candidates.
func (t *tcpPacketConn) ReadFrom(b []byte) (n int, raddr net.Addr, err error) {
pkt, ok := <-t.recvChan
if !ok {
return 0, nil, io.ErrClosedPipe
}
if pkt.Err != nil {
return 0, pkt.RAddr, pkt.Err
}
if cap(b) < len(pkt.Data) {
return 0, pkt.RAddr, io.ErrShortBuffer
}
n = len(pkt.Data)
copy(b, pkt.Data[:n])
return n, pkt.RAddr, err
}
// WriteTo is for active and s-o candidates.
func (t *tcpPacketConn) WriteTo(buf []byte, raddr net.Addr) (n int, err error) {
t.mu.Lock()
defer t.mu.Unlock()
conn, ok := t.conns[raddr.String()]
if !ok {
return 0, io.ErrClosedPipe
// conn, err := net.DialTCP(tcp, nil, raddr.(*net.TCPAddr))
// if err != nil {
// t.params.Logger.Tracef("DialTCP error: %s", err)
// return 0, err
// }
// go t.startReading(conn)
// t.conns[raddr.String()] = conn
}
n, err = writeStreamingPacket(conn, buf)
if err != nil {
t.params.Logger.Tracef("%w %s\n", errWriting, raddr)
return n, err
}
return n, err
}
func (t *tcpPacketConn) closeAndLogError(closer io.Closer) {
err := closer.Close()
if err != nil {
t.params.Logger.Warnf("%w: %s", errClosingConnection, err)
}
}
func (t *tcpPacketConn) removeConn(conn net.Conn) {
t.mu.Lock()
defer t.mu.Unlock()
t.closeAndLogError(conn)
delete(t.conns, conn.RemoteAddr().String())
}
func (t *tcpPacketConn) Close() error {
t.mu.Lock()
var shouldCloseRecvChan bool
t.closeOnce.Do(func() {
close(t.closedChan)
shouldCloseRecvChan = true
})
for _, conn := range t.conns {
t.closeAndLogError(conn)
delete(t.conns, conn.RemoteAddr().String())
}
t.mu.Unlock()
t.wg.Wait()
if shouldCloseRecvChan {
close(t.recvChan)
}
return nil
}
func (t *tcpPacketConn) LocalAddr() net.Addr {
return t.params.LocalAddr
}
func (t *tcpPacketConn) SetDeadline(tm time.Time) error {
return nil
}
func (t *tcpPacketConn) SetReadDeadline(tm time.Time) error {
return nil
}
func (t *tcpPacketConn) SetWriteDeadline(tm time.Time) error {
return nil
}
func (t *tcpPacketConn) CloseChannel() <-chan struct{} {
return t.closedChan
}
func (t *tcpPacketConn) String() string {
return fmt.Sprintf("tcpPacketConn{LocalAddr: %s}", t.params.LocalAddr)
}

View file

@ -0,0 +1,48 @@
package ice
import "strings"
// TCPType is the type of ICE TCP candidate as described in
// ttps://tools.ietf.org/html/rfc6544#section-4.5
type TCPType int
const (
// TCPTypeUnspecified is the default value. For example UDP candidates do not
// need this field.
TCPTypeUnspecified TCPType = iota
// TCPTypeActive is active TCP candidate, which initiates TCP connections.
TCPTypeActive
// TCPTypePassive is passive TCP candidate, only accepts TCP connections.
TCPTypePassive
// TCPTypeSimultaneousOpen is like active and passive at the same time.
TCPTypeSimultaneousOpen
)
// NewTCPType creates a new TCPType from string.
func NewTCPType(value string) TCPType {
switch strings.ToLower(value) {
case "active":
return TCPTypeActive
case "passive":
return TCPTypePassive
case "so":
return TCPTypeSimultaneousOpen
default:
return TCPTypeUnspecified
}
}
func (t TCPType) String() string {
switch t {
case TCPTypeUnspecified:
return ""
case TCPTypeActive:
return "active"
case TCPTypePassive:
return "passive"
case TCPTypeSimultaneousOpen:
return "so"
default:
return ErrUnknownType.Error()
}
}

View file

@ -0,0 +1,145 @@
package ice
import (
"context"
"net"
"sync/atomic"
"time"
"github.com/pion/stun"
)
// Dial connects to the remote agent, acting as the controlling ice agent.
// Dial blocks until at least one ice candidate pair has successfully connected.
func (a *Agent) Dial(ctx context.Context, remoteUfrag, remotePwd string) (*Conn, error) {
return a.connect(ctx, true, remoteUfrag, remotePwd)
}
// Accept connects to the remote agent, acting as the controlled ice agent.
// Accept blocks until at least one ice candidate pair has successfully connected.
func (a *Agent) Accept(ctx context.Context, remoteUfrag, remotePwd string) (*Conn, error) {
return a.connect(ctx, false, remoteUfrag, remotePwd)
}
// Conn represents the ICE connection.
// At the moment the lifetime of the Conn is equal to the Agent.
type Conn struct {
bytesReceived uint64
bytesSent uint64
agent *Agent
}
// BytesSent returns the number of bytes sent
func (c *Conn) BytesSent() uint64 {
return atomic.LoadUint64(&c.bytesSent)
}
// BytesReceived returns the number of bytes received
func (c *Conn) BytesReceived() uint64 {
return atomic.LoadUint64(&c.bytesReceived)
}
func (a *Agent) connect(ctx context.Context, isControlling bool, remoteUfrag, remotePwd string) (*Conn, error) {
err := a.ok()
if err != nil {
return nil, err
}
err = a.startConnectivityChecks(isControlling, remoteUfrag, remotePwd)
if err != nil {
return nil, err
}
// block until pair selected
select {
case <-a.done:
return nil, a.getErr()
case <-ctx.Done():
return nil, ErrCanceledByCaller
case <-a.onConnected:
}
return &Conn{
agent: a,
}, nil
}
// Read implements the Conn Read method.
func (c *Conn) Read(p []byte) (int, error) {
err := c.agent.ok()
if err != nil {
return 0, err
}
n, err := c.agent.buffer.Read(p)
atomic.AddUint64(&c.bytesReceived, uint64(n))
return n, err
}
// Write implements the Conn Write method.
func (c *Conn) Write(p []byte) (int, error) {
err := c.agent.ok()
if err != nil {
return 0, err
}
if stun.IsMessage(p) {
return 0, errICEWriteSTUNMessage
}
pair := c.agent.getSelectedPair()
if pair == nil {
if err = c.agent.run(c.agent.context(), func(ctx context.Context, a *Agent) {
pair = a.getBestValidCandidatePair()
}); err != nil {
return 0, err
}
if pair == nil {
return 0, err
}
}
atomic.AddUint64(&c.bytesSent, uint64(len(p)))
return pair.Write(p)
}
// Close implements the Conn Close method. It is used to close
// the connection. Any calls to Read and Write will be unblocked and return an error.
func (c *Conn) Close() error {
return c.agent.Close()
}
// LocalAddr returns the local address of the current selected pair or nil if there is none.
func (c *Conn) LocalAddr() net.Addr {
pair := c.agent.getSelectedPair()
if pair == nil {
return nil
}
return pair.local.addr()
}
// RemoteAddr returns the remote address of the current selected pair or nil if there is none.
func (c *Conn) RemoteAddr() net.Addr {
pair := c.agent.getSelectedPair()
if pair == nil {
return nil
}
return pair.remote.addr()
}
// SetDeadline is a stub
func (c *Conn) SetDeadline(t time.Time) error {
return nil
}
// SetReadDeadline is a stub
func (c *Conn) SetReadDeadline(t time.Time) error {
return nil
}
// SetWriteDeadline is a stub
func (c *Conn) SetWriteDeadline(t time.Time) error {
return nil
}

View file

@ -0,0 +1,225 @@
package ice
import (
"net"
"net/url"
"strconv"
)
// SchemeType indicates the type of server used in the ice.URL structure.
type SchemeType int
// Unknown defines default public constant to use for "enum" like struct
// comparisons when no value was defined.
const Unknown = iota
const (
// SchemeTypeSTUN indicates the URL represents a STUN server.
SchemeTypeSTUN SchemeType = iota + 1
// SchemeTypeSTUNS indicates the URL represents a STUNS (secure) server.
SchemeTypeSTUNS
// SchemeTypeTURN indicates the URL represents a TURN server.
SchemeTypeTURN
// SchemeTypeTURNS indicates the URL represents a TURNS (secure) server.
SchemeTypeTURNS
)
// NewSchemeType defines a procedure for creating a new SchemeType from a raw
// string naming the scheme type.
func NewSchemeType(raw string) SchemeType {
switch raw {
case "stun":
return SchemeTypeSTUN
case "stuns":
return SchemeTypeSTUNS
case "turn":
return SchemeTypeTURN
case "turns":
return SchemeTypeTURNS
default:
return SchemeType(Unknown)
}
}
func (t SchemeType) String() string {
switch t {
case SchemeTypeSTUN:
return "stun"
case SchemeTypeSTUNS:
return "stuns"
case SchemeTypeTURN:
return "turn"
case SchemeTypeTURNS:
return "turns"
default:
return ErrUnknownType.Error()
}
}
// ProtoType indicates the transport protocol type that is used in the ice.URL
// structure.
type ProtoType int
const (
// ProtoTypeUDP indicates the URL uses a UDP transport.
ProtoTypeUDP ProtoType = iota + 1
// ProtoTypeTCP indicates the URL uses a TCP transport.
ProtoTypeTCP
)
// NewProtoType defines a procedure for creating a new ProtoType from a raw
// string naming the transport protocol type.
func NewProtoType(raw string) ProtoType {
switch raw {
case "udp":
return ProtoTypeUDP
case "tcp":
return ProtoTypeTCP
default:
return ProtoType(Unknown)
}
}
func (t ProtoType) String() string {
switch t {
case ProtoTypeUDP:
return "udp"
case ProtoTypeTCP:
return "tcp"
default:
return ErrUnknownType.Error()
}
}
// URL represents a STUN (rfc7064) or TURN (rfc7065) URL
type URL struct {
Scheme SchemeType
Host string
Port int
Username string
Password string
Proto ProtoType
}
// ParseURL parses a STUN or TURN urls following the ABNF syntax described in
// https://tools.ietf.org/html/rfc7064 and https://tools.ietf.org/html/rfc7065
// respectively.
func ParseURL(raw string) (*URL, error) { //nolint:gocognit
rawParts, err := url.Parse(raw)
if err != nil {
return nil, err
}
var u URL
u.Scheme = NewSchemeType(rawParts.Scheme)
if u.Scheme == SchemeType(Unknown) {
return nil, ErrSchemeType
}
var rawPort string
if u.Host, rawPort, err = net.SplitHostPort(rawParts.Opaque); err != nil {
if e, ok := err.(*net.AddrError); ok {
if e.Err == "missing port in address" {
nextRawURL := u.Scheme.String() + ":" + rawParts.Opaque
switch {
case u.Scheme == SchemeTypeSTUN || u.Scheme == SchemeTypeTURN:
nextRawURL += ":3478"
if rawParts.RawQuery != "" {
nextRawURL += "?" + rawParts.RawQuery
}
return ParseURL(nextRawURL)
case u.Scheme == SchemeTypeSTUNS || u.Scheme == SchemeTypeTURNS:
nextRawURL += ":5349"
if rawParts.RawQuery != "" {
nextRawURL += "?" + rawParts.RawQuery
}
return ParseURL(nextRawURL)
}
}
}
return nil, err
}
if u.Host == "" {
return nil, ErrHost
}
if u.Port, err = strconv.Atoi(rawPort); err != nil {
return nil, ErrPort
}
switch u.Scheme {
case SchemeTypeSTUN:
qArgs, err := url.ParseQuery(rawParts.RawQuery)
if err != nil || len(qArgs) > 0 {
return nil, ErrSTUNQuery
}
u.Proto = ProtoTypeUDP
case SchemeTypeSTUNS:
qArgs, err := url.ParseQuery(rawParts.RawQuery)
if err != nil || len(qArgs) > 0 {
return nil, ErrSTUNQuery
}
u.Proto = ProtoTypeTCP
case SchemeTypeTURN:
proto, err := parseProto(rawParts.RawQuery)
if err != nil {
return nil, err
}
u.Proto = proto
if u.Proto == ProtoType(Unknown) {
u.Proto = ProtoTypeUDP
}
case SchemeTypeTURNS:
proto, err := parseProto(rawParts.RawQuery)
if err != nil {
return nil, err
}
u.Proto = proto
if u.Proto == ProtoType(Unknown) {
u.Proto = ProtoTypeTCP
}
}
return &u, nil
}
func parseProto(raw string) (ProtoType, error) {
qArgs, err := url.ParseQuery(raw)
if err != nil || len(qArgs) > 1 {
return ProtoType(Unknown), ErrInvalidQuery
}
var proto ProtoType
if rawProto := qArgs.Get("transport"); rawProto != "" {
if proto = NewProtoType(rawProto); proto == ProtoType(0) {
return ProtoType(Unknown), ErrProtoType
}
return proto, nil
}
if len(qArgs) > 0 {
return ProtoType(Unknown), ErrInvalidQuery
}
return proto, nil
}
func (u URL) String() string {
rawURL := u.Scheme.String() + ":" + net.JoinHostPort(u.Host, strconv.Itoa(u.Port))
if u.Scheme == SchemeTypeTURN || u.Scheme == SchemeTypeTURNS {
rawURL += "?transport=" + u.Proto.String()
}
return rawURL
}
// IsSecure returns whether the this URL's scheme describes secure scheme or not.
func (u URL) IsSecure() bool {
return u.Scheme == SchemeTypeSTUNS || u.Scheme == SchemeTypeTURNS
}

View file

@ -0,0 +1,23 @@
package ice
import "github.com/pion/stun"
// UseCandidateAttr represents USE-CANDIDATE attribute.
type UseCandidateAttr struct{}
// AddTo adds USE-CANDIDATE attribute to message.
func (UseCandidateAttr) AddTo(m *stun.Message) error {
m.Add(stun.AttrUseCandidate, nil)
return nil
}
// IsSet returns true if USE-CANDIDATE attribute is set.
func (UseCandidateAttr) IsSet(m *stun.Message) bool {
_, err := m.Get(stun.AttrUseCandidate)
return err == nil
}
// UseCandidate is shorthand for UseCandidateAttr.
func UseCandidate() UseCandidateAttr {
return UseCandidateAttr{}
}

View file

@ -0,0 +1,233 @@
package ice
import (
"fmt"
"net"
"sync/atomic"
"time"
"github.com/pion/logging"
"github.com/pion/stun"
"github.com/pion/transport/vnet"
)
type atomicError struct{ v atomic.Value }
func (a *atomicError) Store(err error) {
a.v.Store(struct{ error }{err})
}
func (a *atomicError) Load() error {
err, _ := a.v.Load().(struct{ error })
return err.error
}
// The conditions of invalidation written below are defined in
// https://tools.ietf.org/html/rfc8445#section-5.1.1.1
func isSupportedIPv6(ip net.IP) bool {
if len(ip) != net.IPv6len ||
isZeros(ip[0:12]) || // !(IPv4-compatible IPv6)
ip[0] == 0xfe && ip[1]&0xc0 == 0xc0 || // !(IPv6 site-local unicast)
ip.IsLinkLocalUnicast() ||
ip.IsLinkLocalMulticast() {
return false
}
return true
}
func isZeros(ip net.IP) bool {
for i := 0; i < len(ip); i++ {
if ip[i] != 0 {
return false
}
}
return true
}
func parseAddr(in net.Addr) (net.IP, int, NetworkType, bool) {
switch addr := in.(type) {
case *net.UDPAddr:
return addr.IP, addr.Port, NetworkTypeUDP4, true
case *net.TCPAddr:
return addr.IP, addr.Port, NetworkTypeTCP4, true
}
return nil, 0, 0, false
}
func createAddr(network NetworkType, ip net.IP, port int) net.Addr {
switch {
case network.IsTCP():
return &net.TCPAddr{IP: ip, Port: port}
default:
return &net.UDPAddr{IP: ip, Port: port}
}
}
func addrEqual(a, b net.Addr) bool {
aIP, aPort, aType, aOk := parseAddr(a)
if !aOk {
return false
}
bIP, bPort, bType, bOk := parseAddr(b)
if !bOk {
return false
}
return aType == bType && aIP.Equal(bIP) && aPort == bPort
}
// getXORMappedAddr initiates a stun requests to serverAddr using conn, reads the response and returns
// the XORMappedAddress returned by the stun server.
//
// Adapted from stun v0.2.
func getXORMappedAddr(conn net.PacketConn, serverAddr net.Addr, deadline time.Duration) (*stun.XORMappedAddress, error) {
if deadline > 0 {
if err := conn.SetReadDeadline(time.Now().Add(deadline)); err != nil {
return nil, err
}
}
defer func() {
if deadline > 0 {
_ = conn.SetReadDeadline(time.Time{})
}
}()
resp, err := stunRequest(
func(p []byte) (int, error) {
n, _, errr := conn.ReadFrom(p)
return n, errr
},
func(b []byte) (int, error) {
return conn.WriteTo(b, serverAddr)
},
)
if err != nil {
return nil, err
}
var addr stun.XORMappedAddress
if err = addr.GetFrom(resp); err != nil {
return nil, fmt.Errorf("%w: %v", errGetXorMappedAddrResponse, err)
}
return &addr, nil
}
func stunRequest(read func([]byte) (int, error), write func([]byte) (int, error)) (*stun.Message, error) {
req, err := stun.Build(stun.BindingRequest, stun.TransactionID)
if err != nil {
return nil, err
}
if _, err = write(req.Raw); err != nil {
return nil, err
}
const maxMessageSize = 1280
bs := make([]byte, maxMessageSize)
n, err := read(bs)
if err != nil {
return nil, err
}
res := &stun.Message{Raw: bs[:n]}
if err := res.Decode(); err != nil {
return nil, err
}
return res, nil
}
func localInterfaces(vnet *vnet.Net, interfaceFilter func(string) bool, networkTypes []NetworkType) ([]net.IP, error) { //nolint:gocognit
ips := []net.IP{}
ifaces, err := vnet.Interfaces()
if err != nil {
return ips, err
}
var IPv4Requested, IPv6Requested bool
for _, typ := range networkTypes {
if typ.IsIPv4() {
IPv4Requested = true
}
if typ.IsIPv6() {
IPv6Requested = true
}
}
for _, iface := range ifaces {
if iface.Flags&net.FlagUp == 0 {
continue // interface down
}
if iface.Flags&net.FlagLoopback != 0 {
continue // loopback interface
}
if interfaceFilter != nil && !interfaceFilter(iface.Name) {
continue
}
addrs, err := iface.Addrs()
if err != nil {
continue
}
for _, addr := range addrs {
var ip net.IP
switch addr := addr.(type) {
case *net.IPNet:
ip = addr.IP
case *net.IPAddr:
ip = addr.IP
}
if ip == nil || ip.IsLoopback() {
continue
}
if ipv4 := ip.To4(); ipv4 == nil {
if !IPv6Requested {
continue
} else if !isSupportedIPv6(ip) {
continue
}
} else if !IPv4Requested {
continue
}
ips = append(ips, ip)
}
}
return ips, nil
}
func listenUDPInPortRange(vnet *vnet.Net, log logging.LeveledLogger, portMax, portMin int, network string, laddr *net.UDPAddr) (vnet.UDPPacketConn, error) {
if (laddr.Port != 0) || ((portMin == 0) && (portMax == 0)) {
return vnet.ListenUDP(network, laddr)
}
var i, j int
i = portMin
if i == 0 {
i = 1
}
j = portMax
if j == 0 {
j = 0xFFFF
}
if i > j {
return nil, ErrPort
}
portStart := globalMathRandomGenerator.Intn(j-i+1) + i
portCurrent := portStart
for {
laddr = &net.UDPAddr{IP: laddr.IP, Port: portCurrent}
c, e := vnet.ListenUDP(network, laddr)
if e == nil {
return c, e
}
log.Debugf("failed to listen %s: %v", laddr.String(), e)
portCurrent++
if portCurrent > j {
portCurrent = i
}
if portCurrent == portStart {
break
}
}
return nil, ErrPort
}