Files
webrtc/icegatherer_test.go
2025-12-19 19:30:46 +02:00

2852 lines
73 KiB
Go

// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT
//go:build !js
// +build !js
package webrtc
import (
"context"
"fmt"
"net"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
"github.com/pion/ice/v4"
"github.com/pion/logging"
"github.com/pion/stun/v3"
"github.com/pion/transport/v3/test"
"github.com/pion/transport/v3/vnet"
"github.com/pion/turn/v4"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestNewICEGatherer_Success(t *testing.T) {
// Limit runtime in case of deadlocks
lim := test.TimeOut(time.Second * 20)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
opts := ICEGatherOptions{
ICEServers: []ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}},
}
gatherer, err := NewAPI().NewICEGatherer(opts)
assert.NoError(t, err)
assert.Equal(t, ICEGathererStateNew, gatherer.State())
gatherFinished := make(chan struct{})
gatherer.OnLocalCandidate(func(i *ICECandidate) {
if i == nil {
close(gatherFinished)
}
})
assert.NoError(t, gatherer.Gather())
<-gatherFinished
params, err := gatherer.GetLocalParameters()
assert.NoError(t, err)
assert.NotEmpty(t, params.UsernameFragment, "Empty local username frag")
assert.NotEmpty(t, params.Password, "Empty local password")
candidates, err := gatherer.GetLocalCandidates()
assert.NoError(t, err)
assert.NotEmpty(t, candidates, "No candidates gathered")
assert.NoError(t, gatherer.Close())
}
func TestICEGather_mDNSCandidateGathering(t *testing.T) {
// Limit runtime in case of deadlocks
lim := test.TimeOut(time.Second * 20)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
s := SettingEngine{}
s.SetICEMulticastDNSMode(ice.MulticastDNSModeQueryAndGather)
gatherer, err := NewAPI(WithSettingEngine(s)).NewICEGatherer(ICEGatherOptions{})
assert.NoError(t, err)
gotMulticastDNSCandidate, resolveFunc := context.WithCancel(context.Background())
gatherer.OnLocalCandidate(func(c *ICECandidate) {
if c != nil && strings.HasSuffix(c.Address, ".local") {
resolveFunc()
}
})
assert.NoError(t, gatherer.Gather())
<-gotMulticastDNSCandidate.Done()
assert.NoError(t, gatherer.Close())
}
func TestICEGatherer_InvalidMDNSHostName(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeQueryAndGather)
se.SetMulticastDNSHostName("bad..local")
gatherer, err := NewAPI(WithSettingEngine(se)).NewICEGatherer(ICEGatherOptions{})
assert.NoError(t, err)
err = gatherer.Gather()
assert.ErrorIs(t, err, ice.ErrInvalidMulticastDNSHostName)
}
func TestLegacyNAT1To1AddressRewriteRules(t *testing.T) {
t.Run("empty", func(t *testing.T) {
assert.Empty(t, legacyNAT1To1AddressRewriteRules(nil, ice.CandidateTypeHost))
})
t.Run("mapping and catch-all", func(t *testing.T) {
ips := []string{
"1.2.3.4/10.0.0.1",
"5.6.7.8/10.0.0.2",
"9.9.9.9",
}
rules := legacyNAT1To1AddressRewriteRules(ips, ice.CandidateTypeServerReflexive)
assert.Equal(t, []ice.AddressRewriteRule{
{
External: []string{"1.2.3.4"},
Local: "10.0.0.1",
AsCandidateType: ice.CandidateTypeServerReflexive,
},
{
External: []string{"5.6.7.8"},
Local: "10.0.0.2",
AsCandidateType: ice.CandidateTypeServerReflexive,
},
{
External: []string{
"1.2.3.4",
"5.6.7.8",
"9.9.9.9",
},
AsCandidateType: ice.CandidateTypeServerReflexive,
},
}, rules)
})
}
func TestLegacyNAT1To1AddressRewriteRulesVNet(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
externalIP = "203.0.113.1"
localIP = "10.0.0.1"
)
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
assert.NoError(t, err)
nw, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: localIP,
})
assert.NoError(t, err)
assert.NoError(t, router.AddNet(nw))
assert.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
run := func(candidateType ICECandidateType) []ICECandidate {
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(nw)
se.SetNAT1To1IPs([]string{fmt.Sprintf("%s/%s", externalIP, localIP)}, candidateType)
gatherer, err := NewAPI(WithSettingEngine(se)).NewICEGatherer(ICEGatherOptions{})
assert.NoError(t, err)
defer func() {
assert.NoError(t, gatherer.Close())
}()
done := make(chan struct{})
var candidates []ICECandidate
gatherer.OnLocalCandidate(func(c *ICECandidate) {
if c == nil {
close(done)
} else {
candidates = append(candidates, *c)
}
})
assert.NoError(t, gatherer.Gather())
select {
case <-done:
case <-time.After(3 * time.Second):
assert.Fail(t, "gather did not complete")
}
return candidates
}
t.Run("HostReplace", func(t *testing.T) {
candidates := run(ICECandidateTypeHost)
assert.NotEmpty(t, candidates)
var hostAddrs []string
for _, c := range candidates {
if c.Typ == ICECandidateTypeHost {
hostAddrs = append(hostAddrs, c.Address)
}
}
assert.NotEmpty(t, hostAddrs, "expected host candidates")
assert.Subset(t, hostAddrs, []string{externalIP})
for _, addr := range hostAddrs {
assert.NotEqual(t, localIP, addr)
}
})
t.Run("SrflxAppend", func(t *testing.T) {
candidates := run(ICECandidateTypeSrflx)
assert.NotEmpty(t, candidates)
var hostAddrs []string
var srflx ICECandidate
var haveSrflx bool
for _, c := range candidates {
switch c.Typ {
case ICECandidateTypeHost:
hostAddrs = append(hostAddrs, c.Address)
case ICECandidateTypeSrflx:
srflx = c
haveSrflx = true
default:
}
}
assert.NotEmpty(t, hostAddrs, "expected host candidates")
assert.Contains(t, hostAddrs, localIP)
assert.True(t, haveSrflx, "expected srflx candidate")
assert.Equal(t, externalIP, srflx.Address)
})
}
func TestICEAddressRewriteRulesWithNAT1To1Conflict(t *testing.T) {
lim := test.TimeOut(time.Second * 5)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
t.Run("SetterError", func(t *testing.T) {
se := SettingEngine{}
se.SetNAT1To1IPs([]string{"203.0.113.1"}, ICECandidateTypeHost)
err := se.SetICEAddressRewriteRules(ICEAddressRewriteRule{
External: []string{"198.51.100.1"},
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteReplace,
})
assert.ErrorIs(t, err, errAddressRewriteWithNAT1To1)
})
t.Run("RuntimeError", func(t *testing.T) {
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
nw, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: "10.0.0.1",
})
require.NoError(t, err)
require.NoError(t, router.AddNet(nw))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(nw)
require.NoError(t, se.SetICEAddressRewriteRules(ICEAddressRewriteRule{
External: []string{"198.51.100.2"},
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteReplace,
}))
se.SetNAT1To1IPs([]string{"203.0.113.2"}, ICECandidateTypeHost)
gatherer, err := NewAPI(WithSettingEngine(se)).NewICEGatherer(ICEGatherOptions{})
require.NoError(t, err)
err = gatherer.Gather()
assert.ErrorIs(t, err, errAddressRewriteWithNAT1To1)
assert.NoError(t, gatherer.Close())
})
}
func gatherCandidatesWithSettingEngine(t *testing.T, se SettingEngine, opts ICEGatherOptions) []ICECandidate {
t.Helper()
gatherer, err := NewAPI(WithSettingEngine(se)).NewICEGatherer(opts)
require.NoError(t, err)
done := make(chan struct{})
var candidates []ICECandidate
gatherer.OnLocalCandidate(func(c *ICECandidate) {
if c == nil {
close(done)
return
}
candidates = append(candidates, *c)
})
require.NoError(t, gatherer.Gather())
select {
case <-done:
case <-time.After(5 * time.Second):
assert.Fail(t, "gather did not complete")
}
assert.NoError(t, gatherer.Close())
return candidates
}
func TestICEGatherer_NoHostPolicyVNet(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 20)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
stunIP = "1.2.3.4"
stunPort = 3478
externalIP = "1.2.3.10"
localIP = "10.0.0.1"
realm = "pion.ly"
timeout = 3 * time.Second
)
loggerFactory := logging.NewDefaultLoggerFactory()
wan, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "1.2.3.0/24",
LoggerFactory: loggerFactory,
})
assert.NoError(t, err)
stunNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: stunIP,
})
assert.NoError(t, err)
assert.NoError(t, wan.AddNet(stunNet))
clientLAN, err := vnet.NewRouter(&vnet.RouterConfig{
StaticIPs: []string{fmt.Sprintf("%s/%s", externalIP, localIP)},
CIDR: "10.0.0.0/24",
NATType: &vnet.NATType{
Mode: vnet.NATModeNAT1To1,
},
LoggerFactory: loggerFactory,
})
assert.NoError(t, err)
clientNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: localIP,
})
assert.NoError(t, err)
assert.NoError(t, clientLAN.AddNet(clientNet))
assert.NoError(t, wan.AddRouter(clientLAN))
assert.NoError(t, wan.Start())
defer func() {
assert.NoError(t, wan.Stop())
}()
stunListener, err := stunNet.ListenPacket("udp4", net.JoinHostPort(stunIP, fmt.Sprintf("%d", stunPort)))
assert.NoError(t, err)
turnServer, err := turn.NewServer(turn.ServerConfig{
Realm: realm,
LoggerFactory: loggerFactory,
PacketConnConfigs: []turn.PacketConnConfig{
{
PacketConn: stunListener,
RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
RelayAddress: net.ParseIP(stunIP),
Address: "0.0.0.0",
Net: stunNet,
},
},
},
})
assert.NoError(t, err)
defer func() {
assert.NoError(t, turnServer.Close())
}()
iceServer := ICEServer{
URLs: []string{fmt.Sprintf("stun:%s:%d", stunIP, stunPort)},
}
collect := func(t *testing.T, policy ICETransportPolicy) []ICECandidate {
t.Helper()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(clientNet)
gatherer, err := NewAPI(WithSettingEngine(se)).NewICEGatherer(ICEGatherOptions{
ICEServers: []ICEServer{iceServer},
ICEGatherPolicy: policy,
})
assert.NoError(t, err)
defer func() {
assert.NoError(t, gatherer.Close())
}()
done := make(chan struct{})
var candidates []ICECandidate
gatherer.OnLocalCandidate(func(c *ICECandidate) {
if c == nil {
close(done)
} else {
candidates = append(candidates, *c)
}
})
assert.NoError(t, gatherer.Gather())
select {
case <-done:
case <-time.After(timeout):
assert.Fail(t, "gathering did not complete")
}
return candidates
}
t.Run("All", func(t *testing.T) {
candidates := collect(t, ICETransportPolicyAll)
assert.NotEmpty(t, candidates)
var haveHost, haveSrflx bool
for _, c := range candidates {
switch c.Typ {
case ICECandidateTypeHost:
haveHost = true
case ICECandidateTypeSrflx:
haveSrflx = true
assert.Equal(t, externalIP, c.Address)
default:
}
}
assert.True(t, haveHost, "expected host candidate")
assert.True(t, haveSrflx, "expected srflx candidate")
})
t.Run("NoHost", func(t *testing.T) {
candidates := collect(t, ICETransportPolicyNoHost)
if assert.NotEmpty(t, candidates) {
for _, c := range candidates {
assert.Equal(t, ICECandidateTypeSrflx, c.Typ)
assert.Equal(t, externalIP, c.Address)
}
for _, c := range candidates {
assert.NotEqual(t, ICECandidateTypeHost, c.Typ)
}
}
})
}
func TestICEGatherer_AddressRewriteRulesVNet(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
externalIP = "203.0.113.10"
localIP = "10.0.0.1"
)
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
nw, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: localIP,
})
require.NoError(t, err)
require.NoError(t, router.AddNet(nw))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
run := func(rule ICEAddressRewriteRule) []ICECandidate {
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(nw)
require.NoError(t, se.SetICEAddressRewriteRules(rule))
return gatherCandidatesWithSettingEngine(t, se, ICEGatherOptions{})
}
t.Run("HostReplace", func(t *testing.T) {
candidates := run(ICEAddressRewriteRule{
External: []string{externalIP},
Local: localIP,
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteReplace,
})
assert.NotEmpty(t, candidates)
var hostAddrs []string
for _, c := range candidates {
if c.Typ == ICECandidateTypeHost {
hostAddrs = append(hostAddrs, c.Address)
}
}
assert.NotEmpty(t, hostAddrs, "expected host candidates")
assert.Subset(t, hostAddrs, []string{externalIP})
for _, addr := range hostAddrs {
assert.NotEqual(t, localIP, addr)
}
})
t.Run("SrflxAppend", func(t *testing.T) {
candidates := run(ICEAddressRewriteRule{
External: []string{externalIP},
AsCandidateType: ICECandidateTypeSrflx,
})
assert.NotEmpty(t, candidates)
var hostAddrs []string
var srflx ICECandidate
var haveSrflx bool
for _, c := range candidates {
switch c.Typ {
case ICECandidateTypeHost:
hostAddrs = append(hostAddrs, c.Address)
case ICECandidateTypeSrflx:
srflx = c
haveSrflx = true
default:
}
}
assert.NotEmpty(t, hostAddrs, "expected host candidates")
assert.Contains(t, hostAddrs, localIP)
assert.True(t, haveSrflx, "expected srflx candidate")
assert.Equal(t, externalIP, srflx.Address)
})
}
func TestICEGatherer_AddressRewriteRuleFilters(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
t.Run("CIDR", func(t *testing.T) {
const (
firstIP = "10.0.0.2"
secondIP = "10.0.1.2"
externalIP = "203.0.113.20"
)
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/16",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
nw, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{firstIP, secondIP},
})
require.NoError(t, err)
require.NoError(t, router.AddNet(nw))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(nw)
require.NoError(t, se.SetICEAddressRewriteRules(ICEAddressRewriteRule{
External: []string{externalIP},
CIDR: "10.0.0.0/24",
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteReplace,
}))
candidates := gatherCandidatesWithSettingEngine(t, se, ICEGatherOptions{})
var hostAddrs []string
for _, c := range candidates {
if c.Typ == ICECandidateTypeHost {
hostAddrs = append(hostAddrs, c.Address)
}
}
assert.Contains(t, hostAddrs, externalIP)
assert.Contains(t, hostAddrs, secondIP)
assert.NotContains(t, hostAddrs, firstIP)
})
t.Run("NetworkTypes", func(t *testing.T) {
const (
localIP = "10.0.0.50"
externalIP = "203.0.113.50"
)
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
nw, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: localIP,
})
require.NoError(t, err)
require.NoError(t, router.AddNet(nw))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(nw)
require.NoError(t, se.SetICEAddressRewriteRules(ICEAddressRewriteRule{
External: []string{externalIP},
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteReplace,
Networks: []NetworkType{NetworkTypeUDP6},
}))
candidates := gatherCandidatesWithSettingEngine(t, se, ICEGatherOptions{})
var hostAddrs []string
for _, c := range candidates {
if c.Typ == ICECandidateTypeHost {
hostAddrs = append(hostAddrs, c.Address)
}
}
assert.Contains(t, hostAddrs, localIP)
assert.NotContains(t, hostAddrs, externalIP)
})
}
func TestICEGatherer_AddressRewriteHostAppendAndReplace(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
firstLocal = "10.0.0.2"
secondLocal = "10.0.0.3"
firstExternal = "203.0.113.30"
secondExternal = "203.0.113.31"
)
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
nw, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{firstLocal, secondLocal},
})
require.NoError(t, err)
require.NoError(t, router.AddNet(nw))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(nw)
require.NoError(t, se.SetICEAddressRewriteRules(
ICEAddressRewriteRule{
Local: firstLocal,
External: []string{firstExternal},
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteReplace,
},
ICEAddressRewriteRule{
Local: secondLocal,
External: []string{secondExternal},
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteAppend,
},
))
candidates := gatherCandidatesWithSettingEngine(t, se, ICEGatherOptions{})
var hostAddrs []string
for _, c := range candidates {
if c.Typ == ICECandidateTypeHost {
hostAddrs = append(hostAddrs, c.Address)
}
}
assert.Contains(t, hostAddrs, firstExternal)
assert.NotContains(t, hostAddrs, firstLocal)
assert.Contains(t, hostAddrs, secondLocal)
assert.Contains(t, hostAddrs, secondExternal)
}
func TestICEGatherer_AddressRewriteSrflxReplace(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
localIP = "10.0.0.60"
externalIP = "203.0.113.60"
)
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
nw, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: localIP,
})
require.NoError(t, err)
require.NoError(t, router.AddNet(nw))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(nw)
require.NoError(t, se.SetICEAddressRewriteRules(ICEAddressRewriteRule{
External: []string{externalIP},
AsCandidateType: ICECandidateTypeSrflx,
Mode: ICEAddressRewriteReplace,
}))
candidates := gatherCandidatesWithSettingEngine(t, se, ICEGatherOptions{})
var hostAddrs []string
var srflxAddrs []string
for _, c := range candidates {
switch c.Typ {
case ICECandidateTypeHost:
hostAddrs = append(hostAddrs, c.Address)
case ICECandidateTypeSrflx:
srflxAddrs = append(srflxAddrs, c.Address)
default:
t.Logf("unexpected candidate type: %s", c.Typ)
}
}
assert.Contains(t, hostAddrs, localIP)
assert.Contains(t, srflxAddrs, externalIP)
assert.NotContains(t, srflxAddrs, localIP)
}
func TestICEGatherer_AddressRewriteSrflxAppendWithCatchAll(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
localIP = "10.0.0.80"
appendIP = "203.0.113.81"
replaceIP = "203.0.113.80"
)
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
nw, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: localIP,
})
require.NoError(t, err)
require.NoError(t, router.AddNet(nw))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(nw)
require.NoError(t, se.SetICEAddressRewriteRules(
ICEAddressRewriteRule{
External: []string{appendIP},
AsCandidateType: ICECandidateTypeSrflx,
Mode: ICEAddressRewriteAppend,
},
ICEAddressRewriteRule{
External: []string{replaceIP},
AsCandidateType: ICECandidateTypeSrflx,
Mode: ICEAddressRewriteReplace,
},
))
candidates := gatherCandidatesWithSettingEngine(t, se, ICEGatherOptions{})
var srflxAddrs []string
for _, c := range candidates {
if c.Typ == ICECandidateTypeSrflx {
srflxAddrs = append(srflxAddrs, c.Address)
}
}
assert.Contains(t, srflxAddrs, appendIP)
assert.NotContains(t, srflxAddrs, replaceIP)
assert.NotContains(t, srflxAddrs, localIP)
}
func TestICEGatherer_AddressRewriteMultipleRulesOrdering(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
localIP = "10.0.0.70"
otherLocalIP = "10.0.0.71"
externalIP = "203.0.113.70"
)
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
nw, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{localIP, otherLocalIP},
})
require.NoError(t, err)
require.NoError(t, router.AddNet(nw))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(nw)
require.NoError(t, se.SetICEAddressRewriteRules(
ICEAddressRewriteRule{
CIDR: "10.0.0.0/24",
External: []string{externalIP},
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteReplace,
},
ICEAddressRewriteRule{
Local: otherLocalIP,
External: []string{otherLocalIP},
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteAppend,
},
))
candidates := gatherCandidatesWithSettingEngine(t, se, ICEGatherOptions{})
var hostAddrs []string
for _, c := range candidates {
if c.Typ == ICECandidateTypeHost {
hostAddrs = append(hostAddrs, c.Address)
}
}
assert.Contains(t, hostAddrs, externalIP)
assert.NotContains(t, hostAddrs, localIP)
assert.Contains(t, hostAddrs, otherLocalIP)
}
func TestICEGatherer_AddressRewriteIfaceScope(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
localIP = "10.0.0.90"
externalIP = "203.0.113.90"
)
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
nw, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: localIP,
})
require.NoError(t, err)
require.NoError(t, router.AddNet(nw))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(nw)
require.NoError(t, se.SetICEAddressRewriteRules(
ICEAddressRewriteRule{
Iface: "bad0",
External: []string{"198.51.100.90"},
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteReplace,
},
ICEAddressRewriteRule{
Iface: "eth0",
External: []string{externalIP},
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteReplace,
},
))
candidates := gatherCandidatesWithSettingEngine(t, se, ICEGatherOptions{})
var hostAddrs []string
for _, c := range candidates {
if c.Typ == ICECandidateTypeHost {
hostAddrs = append(hostAddrs, c.Address)
}
}
assert.Contains(t, hostAddrs, externalIP)
assert.NotContains(t, hostAddrs, localIP)
assert.NotContains(t, hostAddrs, "198.51.100.90")
}
func TestICEConnection_AddressRewriteAppend(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 15)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
offerIP = "1.2.3.4"
answerIP = "1.2.3.5"
offerExternal = "203.0.113.200"
)
wan, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "1.2.3.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
require.NoError(t, err)
offerNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{offerIP},
})
require.NoError(t, err)
answerNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{answerIP},
})
require.NoError(t, err)
require.NoError(t, wan.AddNet(offerNet))
require.NoError(t, wan.AddNet(answerNet))
require.NoError(t, wan.Start())
defer func() {
assert.NoError(t, wan.Stop())
}()
offerSE := SettingEngine{}
offerSE.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
offerSE.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
offerSE.SetNet(offerNet)
require.NoError(t, offerSE.SetICEAddressRewriteRules(ICEAddressRewriteRule{
External: []string{offerExternal},
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteAppend,
}))
answerSE := SettingEngine{}
answerSE.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
answerSE.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
answerSE.SetNet(answerNet)
offerPC, err := NewAPI(WithSettingEngine(offerSE)).NewPeerConnection(Configuration{})
require.NoError(t, err)
answerPC, err := NewAPI(WithSettingEngine(answerSE)).NewPeerConnection(Configuration{})
require.NoError(t, err)
defer closePairNow(t, offerPC, answerPC)
var offerCandidates []ICECandidate
offerPC.OnICECandidate(func(c *ICECandidate) {
if c != nil {
offerCandidates = append(offerCandidates, *c)
}
})
assert.NoError(t, signalPair(offerPC, answerPC))
connected := untilConnectionState(PeerConnectionStateConnected, offerPC, answerPC)
connected.Wait()
var hostAddrs []string
for _, c := range offerCandidates {
if c.Typ == ICECandidateTypeHost {
hostAddrs = append(hostAddrs, c.Address)
}
}
assert.Contains(t, hostAddrs, offerIP)
assert.Contains(t, hostAddrs, offerExternal)
}
func TestICEAddressRewriteDropRule(t *testing.T) {
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
err := se.SetICEAddressRewriteRules(ICEAddressRewriteRule{
External: nil,
AsCandidateType: ICECandidateTypeHost,
Mode: ICEAddressRewriteReplace,
})
assert.NoError(t, err, "rule is allowed to be configured, validation happens in ice")
gatherer, gErr := NewAPI(WithSettingEngine(se)).NewICEGatherer(ICEGatherOptions{})
require.NoError(t, gErr)
defer func() {
assert.NoError(t, gatherer.Close())
}()
assert.ErrorIs(t, gatherer.Gather(), ice.ErrInvalidAddressRewriteMapping)
}
func TestICEGatherer_AddressRewriteRelayVNet(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 15)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
turnIP = "10.0.0.2"
clientIP = "10.0.0.3"
relayExternal = "203.0.113.77"
turnListenPort = "3478"
)
loggerFactory := logging.NewDefaultLoggerFactory()
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: loggerFactory,
})
require.NoError(t, err)
turnNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: turnIP,
})
require.NoError(t, err)
clientNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: clientIP,
})
require.NoError(t, err)
require.NoError(t, router.AddNet(turnNet))
require.NoError(t, router.AddNet(clientNet))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
turnListener, err := turnNet.ListenPacket("udp4", net.JoinHostPort(turnIP, turnListenPort))
require.NoError(t, err)
authKey := turn.GenerateAuthKey("user", "pion.ly", "pass")
turnServer, err := turn.NewServer(turn.ServerConfig{
Realm: "pion.ly",
AuthHandler: func(u, r string, _ net.Addr) ([]byte, bool) {
if u == "user" && r == "pion.ly" {
return authKey, true
}
return nil, false
},
PacketConnConfigs: []turn.PacketConnConfig{
{
PacketConn: turnListener,
RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
RelayAddress: net.ParseIP(turnIP),
Address: "0.0.0.0",
Net: turnNet,
},
},
},
LoggerFactory: loggerFactory,
})
require.NoError(t, err)
defer func() {
assert.NoError(t, turnServer.Close())
}()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(clientNet)
require.NoError(t, se.SetICEAddressRewriteRules(ICEAddressRewriteRule{
External: []string{relayExternal},
AsCandidateType: ICECandidateTypeRelay,
Mode: ICEAddressRewriteReplace,
}))
candidates := gatherCandidatesWithSettingEngine(t, se, ICEGatherOptions{
ICEServers: []ICEServer{
{
URLs: []string{fmt.Sprintf("turn:%s:%s?transport=udp", turnIP, turnListenPort)},
Username: "user",
Credential: "pass",
},
},
ICEGatherPolicy: ICETransportPolicyRelay,
})
var relayAddrs []string
for _, c := range candidates {
if c.Typ == ICECandidateTypeRelay {
relayAddrs = append(relayAddrs, c.Address)
}
}
assert.NotEmpty(t, relayAddrs, "expected relay candidates")
assert.Subset(t, relayAddrs, []string{relayExternal})
assert.NotContains(t, relayAddrs, turnIP)
}
func TestICEGatherer_AddressRewriteRelayAppendVNet(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 15)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
turnIP = "10.0.0.4"
clientIP = "10.0.0.5"
relayExternal = "203.0.113.78"
turnListenPort = "3478"
)
loggerFactory := logging.NewDefaultLoggerFactory()
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: loggerFactory,
})
require.NoError(t, err)
turnNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: turnIP,
})
require.NoError(t, err)
clientNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: clientIP,
})
require.NoError(t, err)
require.NoError(t, router.AddNet(turnNet))
require.NoError(t, router.AddNet(clientNet))
require.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
turnListener, err := turnNet.ListenPacket("udp4", net.JoinHostPort(turnIP, turnListenPort))
require.NoError(t, err)
authKey := turn.GenerateAuthKey("user", "pion.ly", "pass")
turnServer, err := turn.NewServer(turn.ServerConfig{
Realm: "pion.ly",
AuthHandler: func(u, r string, _ net.Addr) ([]byte, bool) {
if u == "user" && r == "pion.ly" {
return authKey, true
}
return nil, false
},
PacketConnConfigs: []turn.PacketConnConfig{
{
PacketConn: turnListener,
RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
RelayAddress: net.ParseIP(turnIP),
Address: "0.0.0.0",
Net: turnNet,
},
},
},
LoggerFactory: loggerFactory,
})
require.NoError(t, err)
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(clientNet)
require.NoError(t, se.SetICEAddressRewriteRules(ICEAddressRewriteRule{
External: []string{relayExternal},
AsCandidateType: ICECandidateTypeRelay,
Mode: ICEAddressRewriteAppend,
}))
candidates := gatherCandidatesWithSettingEngine(t, se, ICEGatherOptions{
ICEServers: []ICEServer{
{
URLs: []string{fmt.Sprintf("turn:%s:%s?transport=udp", turnIP, turnListenPort)},
Username: "user",
Credential: "pass",
},
},
ICEGatherPolicy: ICETransportPolicyRelay,
})
var relayAddrs []string
for _, c := range candidates {
if c.Typ == ICECandidateTypeRelay {
relayAddrs = append(relayAddrs, c.Address)
}
}
assert.Contains(t, relayAddrs, turnIP)
assert.Contains(t, relayAddrs, relayExternal)
if err := turnServer.Close(); err != nil {
t.Logf("turn server close: %v", err)
}
if err := turnListener.Close(); err != nil {
t.Logf("turn listener close: %v", err)
}
}
func TestICEGatherer_StaticLocalCredentialsVNet(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 20)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
parseCreds := func(sdp string) (string, string) {
var ufrag, pwd string
for _, l := range strings.Split(sdp, "\n") {
l = strings.TrimSpace(l)
if after, ok := strings.CutPrefix(l, "a=ice-ufrag:"); ok {
ufrag = after
} else if after, ok := strings.CutPrefix(l, "a=ice-pwd:"); ok {
pwd = after
}
}
return ufrag, pwd
}
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
assert.NoError(t, err)
offerNet, err := vnet.NewNet(&vnet.NetConfig{StaticIPs: []string{"10.0.0.2"}})
assert.NoError(t, err)
answerNet, err := vnet.NewNet(&vnet.NetConfig{StaticIPs: []string{"10.0.0.3"}})
assert.NoError(t, err)
assert.NoError(t, router.AddNet(offerNet))
assert.NoError(t, router.AddNet(answerNet))
assert.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
buildSE := func(n *vnet.Net, ufrag, pwd string) SettingEngine {
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetNet(n)
se.SetICECredentials(ufrag, pwd)
return se
}
const (
offerUfrag = "offerufrag123"
offerPwd = "offerpassword123456"
answerUfrag = "answerufrag123"
answerPwd = "answerpassword123456"
)
pcOffer, err := NewAPI(WithSettingEngine(buildSE(offerNet, offerUfrag, offerPwd))).NewPeerConnection(Configuration{})
assert.NoError(t, err)
pcAnswer, err := NewAPI(
WithSettingEngine(buildSE(answerNet, answerUfrag, answerPwd)),
).NewPeerConnection(Configuration{})
assert.NoError(t, err)
defer closePairNow(t, pcOffer, pcAnswer)
connected := untilConnectionState(PeerConnectionStateConnected, pcOffer, pcAnswer)
assert.NoError(t, signalPair(pcOffer, pcAnswer))
connected.Wait()
gotUfrag, gotPwd := parseCreds(pcOffer.LocalDescription().SDP)
assert.Equal(t, offerUfrag, gotUfrag)
assert.Equal(t, offerPwd, gotPwd)
gotUfrag, gotPwd = parseCreds(pcAnswer.LocalDescription().SDP)
assert.Equal(t, answerUfrag, gotUfrag)
assert.Equal(t, answerPwd, gotPwd)
}
func TestICEGatherer_AlreadyClosed(t *testing.T) {
// Limit runtime in case of deadlocks
lim := test.TimeOut(time.Second * 20)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
opts := ICEGatherOptions{
ICEServers: []ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}},
}
t.Run("Gather", func(t *testing.T) {
gatherer, err := NewAPI().NewICEGatherer(opts)
assert.NoError(t, err)
err = gatherer.createAgent()
assert.NoError(t, err)
err = gatherer.Close()
assert.NoError(t, err)
err = gatherer.Gather()
assert.ErrorIs(t, err, errICEAgentNotExist)
})
t.Run("GetLocalParameters", func(t *testing.T) {
gatherer, err := NewAPI().NewICEGatherer(opts)
assert.NoError(t, err)
err = gatherer.createAgent()
assert.NoError(t, err)
err = gatherer.Close()
assert.NoError(t, err)
_, err = gatherer.GetLocalParameters()
assert.ErrorIs(t, err, errICEAgentNotExist)
})
t.Run("GetLocalCandidates", func(t *testing.T) {
gatherer, err := NewAPI().NewICEGatherer(opts)
assert.NoError(t, err)
err = gatherer.createAgent()
assert.NoError(t, err)
err = gatherer.Close()
assert.NoError(t, err)
_, err = gatherer.GetLocalCandidates()
assert.ErrorIs(t, err, errICEAgentNotExist)
})
}
func TestICEGatherer_MaxBindingRequests(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const maxReq uint16 = 2
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "1.2.3.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
if !assert.NoError(t, err) {
return
}
offerNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{"1.2.3.4"},
})
if !assert.NoError(t, err) {
return
}
answerNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{"1.2.3.5"},
})
if !assert.NoError(t, err) {
return
}
if !assert.NoError(t, router.AddNet(offerNet)) {
return
}
if !assert.NoError(t, router.AddNet(answerNet)) {
return
}
answerIP := net.ParseIP("1.2.3.5")
router.AddChunkFilter(func(c vnet.Chunk) bool {
if addr, ok := c.SourceAddr().(*net.UDPAddr); ok {
// drop all packets originating from the answerer so the offerer
// never receives binding responses.
return !addr.IP.Equal(answerIP)
}
return true
})
if !assert.NoError(t, router.Start()) {
return
}
defer func() {
assert.NoError(t, router.Stop())
}()
offerS := SettingEngine{}
offerS.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
offerS.SetICEMaxBindingRequests(maxReq)
offerS.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
offerS.SetNet(offerNet)
var bindingRequests atomic.Uint32
firstRequest := make(chan struct{})
answerSE := SettingEngine{}
answerSE.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
answerSE.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
answerSE.SetNet(answerNet)
answerSE.SetICEBindingRequestHandler(func(_ *stun.Message, _, _ ice.Candidate, _ *ice.CandidatePair) bool {
bindingRequests.Add(1)
select {
case firstRequest <- struct{}{}:
default:
}
return false
})
pcOffer, err := NewAPI(WithSettingEngine(offerS)).NewPeerConnection(Configuration{})
if !assert.NoError(t, err) {
return
}
pcAnswer, err := NewAPI(WithSettingEngine(answerSE)).NewPeerConnection(Configuration{})
if !assert.NoError(t, err) {
return
}
defer closePairNow(t, pcOffer, pcAnswer)
assert.NoError(t, signalPair(pcOffer, pcAnswer))
select {
case <-firstRequest:
case <-time.After(2 * time.Second):
assert.Fail(t, "did not receive any binding request")
}
expected := uint32(maxReq) + 1
finalCount := func() uint32 {
last := bindingRequests.Load()
deadline := time.Now().Add(5 * time.Second)
for time.Now().Before(deadline) {
time.Sleep(150 * time.Millisecond)
next := bindingRequests.Load()
if next == last && next >= expected {
return next
}
last = next
}
return bindingRequests.Load()
}()
assert.Equal(t, expected, finalCount, "max binding requests should limit retransmits")
}
func TestICEGatherer_DisableActiveTCP(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
tests := []struct {
name string
disableActive bool
expectConnected bool
}{
{
name: "ActiveTCPEnabled",
disableActive: false,
expectConnected: true,
},
{
name: "ActiveTCPDisabled",
disableActive: true,
expectConnected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
listener, err := (&net.ListenConfig{}).Listen(context.Background(), "tcp4", "127.0.0.1:0")
if err != nil || listener == nil {
t.Skip("tcp listener unavailable in this environment")
}
defer func() {
assert.NoError(t, listener.Close())
}()
accepted := make(chan struct{})
go func() {
conn, acceptErr := listener.Accept()
if acceptErr == nil {
if closeErr := conn.Close(); closeErr != nil {
t.Logf("close accepted conn: %v", closeErr)
}
}
close(accepted)
}()
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeTCP4})
se.SetICETimeouts(time.Second, 2*time.Second, 500*time.Millisecond)
se.SetIncludeLoopbackCandidate(true)
se.DisableActiveTCP(tt.disableActive)
gatherer, err := NewAPI(WithSettingEngine(se)).NewICEGatherer(ICEGatherOptions{})
assert.NoError(t, err)
defer func() {
assert.NoError(t, gatherer.Close())
}()
assert.NoError(t, gatherer.createAgent())
agent := gatherer.getAgent()
if !assert.NotNil(t, agent) {
return
}
addr, ok := listener.Addr().(*net.TCPAddr)
if !assert.True(t, ok) {
return
}
c, err := ice.NewCandidateHost(&ice.CandidateHostConfig{
Network: "tcp4",
Address: addr.IP.String(),
Port: addr.Port,
Component: ice.ComponentRTP,
TCPType: ice.TCPTypePassive,
})
assert.NoError(t, err)
assert.NoError(t, agent.AddRemoteCandidate(c))
select {
case <-accepted:
assert.False(t, tt.disableActive, "active TCP dialed despite being disabled")
case <-time.After(3 * time.Second):
assert.True(t, tt.disableActive, "expected active TCP dial when enabled")
}
})
}
}
func TestICEGatherer_HostAcceptanceMinWait(t *testing.T) {
lim := test.TimeOut(time.Second * 20)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const wait = 500 * time.Millisecond
pcOffer, pcAnswer, wan := createVNetPair(t, nil)
defer func() {
assert.NoError(t, wan.Stop())
closePairNow(t, pcOffer, pcAnswer)
}()
pcOffer.api.settingEngine.timeout.ICEHostAcceptanceMinWait = func() *time.Duration {
d := wait
return &d
}()
start := time.Now()
assert.NoError(t, signalPair(pcOffer, pcAnswer))
connected := untilConnectionState(PeerConnectionStateConnected, pcOffer, pcAnswer)
connected.Wait()
assert.GreaterOrEqual(t, time.Since(start), wait)
}
func TestICEGatherer_SrflxAcceptanceMinWait(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 40)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
stunIP = "1.2.3.4"
stunPort = 3478
defaultSrflxMinWait = 500 * time.Millisecond
offerExternalIP = "1.2.3.10"
offerLocalIP = "10.0.0.1"
answerExternalIP = "1.2.3.11"
answerLocalIP = "10.0.1.1"
externalRouterSubnet = "1.2.3.0/24"
)
wait := 900 * time.Millisecond
loggerFactory := logging.NewDefaultLoggerFactory()
wan, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: externalRouterSubnet,
LoggerFactory: loggerFactory,
})
assert.NoError(t, err)
stunNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIP: stunIP,
})
assert.NoError(t, err)
assert.NoError(t, wan.AddNet(stunNet))
offerLAN, err := vnet.NewRouter(&vnet.RouterConfig{
StaticIPs: []string{fmt.Sprintf("%s/%s", offerExternalIP, offerLocalIP)},
CIDR: "10.0.0.0/24",
NATType: &vnet.NATType{
Mode: vnet.NATModeNAT1To1,
},
LoggerFactory: loggerFactory,
})
assert.NoError(t, err)
offerNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{offerLocalIP},
})
assert.NoError(t, err)
assert.NoError(t, offerLAN.AddNet(offerNet))
assert.NoError(t, wan.AddRouter(offerLAN))
answerLAN, err := vnet.NewRouter(&vnet.RouterConfig{
StaticIPs: []string{fmt.Sprintf("%s/%s", answerExternalIP, answerLocalIP)},
CIDR: "10.0.1.0/24",
NATType: &vnet.NATType{
Mode: vnet.NATModeNAT1To1,
},
LoggerFactory: loggerFactory,
})
assert.NoError(t, err)
answerNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{answerLocalIP},
})
assert.NoError(t, err)
assert.NoError(t, answerLAN.AddNet(answerNet))
assert.NoError(t, wan.AddRouter(answerLAN))
assert.NoError(t, wan.Start())
defer func() {
assert.NoError(t, wan.Stop())
}()
stunListener, err := stunNet.ListenPacket("udp4", net.JoinHostPort(stunIP, fmt.Sprintf("%d", stunPort)))
assert.NoError(t, err)
authKey := turn.GenerateAuthKey("user", "pion.ly", "pass")
turnServer, err := turn.NewServer(turn.ServerConfig{
Realm: "pion.ly",
AuthHandler: func(u, r string, _ net.Addr) ([]byte, bool) {
return authKey, true
},
PacketConnConfigs: []turn.PacketConnConfig{
{
PacketConn: stunListener,
RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
RelayAddress: net.ParseIP(stunIP),
Address: "0.0.0.0",
Net: stunNet,
},
},
},
LoggerFactory: loggerFactory,
})
assert.NoError(t, err)
defer func() {
assert.NoError(t, turnServer.Close())
}()
buildSettingEngine := func(n *vnet.Net) SettingEngine {
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetSrflxAcceptanceMinWait(wait)
se.SetICETimeouts(2*time.Second, 4*time.Second, 500*time.Millisecond)
se.SetNet(n)
return se
}
iceServer := ICEServer{
URLs: []string{fmt.Sprintf("stun:%s:%d", stunIP, stunPort)},
}
offerPC, err := NewAPI(WithSettingEngine(buildSettingEngine(offerNet))).NewPeerConnection(Configuration{
ICEServers: []ICEServer{iceServer},
})
assert.NoError(t, err)
answerPC, err := NewAPI(WithSettingEngine(buildSettingEngine(answerNet))).NewPeerConnection(Configuration{
ICEServers: []ICEServer{iceServer},
})
assert.NoError(t, err)
defer closePairNow(t, offerPC, answerPC)
connected := untilConnectionState(PeerConnectionStateConnected, offerPC, answerPC)
start := time.Now()
assert.NoError(t, signalPair(offerPC, answerPC))
connected.Wait()
elapsed := time.Since(start)
assert.GreaterOrEqual(t, elapsed, wait)
assert.Less(t, elapsed, 2*wait)
}
func TestICEGatherer_PrflxAcceptanceMinWait(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 40)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
wait = 300 * time.Millisecond
defaultPrflxMinWait = time.Second
)
pcOffer, pcAnswer, wan := createVNetPair(t, nil)
defer func() {
assert.NoError(t, wan.Stop())
closePairNow(t, pcOffer, pcAnswer)
}()
pcOffer.api.settingEngine.timeout.ICEPrflxAcceptanceMinWait = func() *time.Duration {
d := wait
return &d
}()
var answerCandidate *ICECandidate
candidateReady := make(chan struct{})
pcAnswer.OnICECandidate(func(c *ICECandidate) {
if c == nil || answerCandidate != nil {
return
}
cCopy := *c
answerCandidate = &cCopy
close(candidateReady)
})
_, err := pcOffer.CreateDataChannel("data", nil)
assert.NoError(t, err)
offer, err := pcOffer.CreateOffer(nil)
assert.NoError(t, err)
offerGatheringDone := GatheringCompletePromise(pcOffer)
assert.NoError(t, pcOffer.SetLocalDescription(offer))
<-offerGatheringDone
assert.NoError(t, pcAnswer.SetRemoteDescription(*pcOffer.LocalDescription()))
answer, err := pcAnswer.CreateAnswer(nil)
assert.NoError(t, err)
answerGatheringDone := GatheringCompletePromise(pcAnswer)
assert.NoError(t, pcAnswer.SetLocalDescription(answer))
<-answerGatheringDone
if answerCandidate == nil {
<-candidateReady
}
filteredAnswer := *pcAnswer.LocalDescription()
filteredAnswer.SDP = func(sdp string) string {
lines := strings.Split(sdp, "\n")
filtered := lines[:0]
for _, l := range lines {
if strings.HasPrefix(l, "a=candidate:") || strings.HasPrefix(l, "a=end-of-candidates") {
continue
}
filtered = append(filtered, l)
}
return strings.Join(filtered, "\n")
}(filteredAnswer.SDP)
assert.NoError(t, pcOffer.SetRemoteDescription(filteredAnswer))
prflx := *answerCandidate
prflx.Typ = ICECandidateTypePrflx
prflx.RelatedAddress = answerCandidate.Address
prflx.RelatedPort = answerCandidate.Port
start := time.Now()
assert.NoError(t, pcOffer.AddICECandidate(prflx.ToJSON()))
connected := untilConnectionState(PeerConnectionStateConnected, pcOffer, pcAnswer)
connected.Wait()
elapsed := time.Since(start)
assert.GreaterOrEqual(t, elapsed, wait)
assert.Less(t, elapsed, defaultPrflxMinWait)
}
func TestICEGatherer_STUNGatherTimeout(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
timeout := 200 * time.Millisecond
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
assert.NoError(t, err)
net, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{"10.0.0.2"},
})
assert.NoError(t, err)
assert.NoError(t, router.AddNet(net))
assert.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
se := SettingEngine{}
se.SetSTUNGatherTimeout(timeout)
se.SetNet(net)
opts := ICEGatherOptions{
ICEServers: []ICEServer{{URLs: []string{"stun:10.0.0.1:9"}}},
}
gatherer, err := NewAPI(WithSettingEngine(se)).NewICEGatherer(opts)
assert.NoError(t, err)
defer func() {
assert.NoError(t, gatherer.Close())
}()
gatheringDone := make(chan struct{})
gatherer.OnLocalCandidate(func(c *ICECandidate) {
if c == nil {
close(gatheringDone)
}
})
start := time.Now()
assert.NoError(t, gatherer.Gather())
select {
case <-gatheringDone:
case <-time.After(3 * time.Second):
assert.Fail(t, "gathering did not complete")
}
assert.LessOrEqual(t, time.Since(start), timeout*10)
}
func TestICEGatherer_RelayAcceptanceMinWait(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 40)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
const (
turnIP = "10.0.0.1"
turnPort = 3478
username = "user"
password = "pass"
realm = "pion.ly"
defaultRelayMinWait = 2 * time.Second
)
wait := 500 * time.Millisecond
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
assert.NoError(t, err)
turnNet, err := vnet.NewNet(&vnet.NetConfig{StaticIPs: []string{turnIP}})
assert.NoError(t, err)
offerNet, err := vnet.NewNet(&vnet.NetConfig{StaticIPs: []string{"10.0.0.2"}})
assert.NoError(t, err)
answerNet, err := vnet.NewNet(&vnet.NetConfig{StaticIPs: []string{"10.0.0.3"}})
assert.NoError(t, err)
assert.NoError(t, router.AddNet(turnNet))
assert.NoError(t, router.AddNet(offerNet))
assert.NoError(t, router.AddNet(answerNet))
assert.NoError(t, router.Start())
defer func() {
assert.NoError(t, router.Stop())
}()
turnListener, err := turnNet.ListenPacket("udp4", net.JoinHostPort(turnIP, fmt.Sprintf("%d", turnPort)))
assert.NoError(t, err)
authKey := turn.GenerateAuthKey(username, realm, password)
turnServer, err := turn.NewServer(turn.ServerConfig{
Realm: realm,
AuthHandler: func(u, r string, _ net.Addr) ([]byte, bool) {
if u == username && r == realm {
return authKey, true
}
return nil, false
},
PacketConnConfigs: []turn.PacketConnConfig{
{
PacketConn: turnListener,
RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
RelayAddress: net.ParseIP(turnIP),
Address: turnIP,
Net: turnNet,
},
},
},
})
assert.NoError(t, err)
defer func() {
assert.NoError(t, turnServer.Close())
}()
buildSettingEngine := func(n *vnet.Net) SettingEngine {
se := SettingEngine{}
se.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
se.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
se.SetRelayAcceptanceMinWait(wait)
se.SetICETimeouts(2*time.Second, 4*time.Second, 500*time.Millisecond)
se.SetNet(n)
return se
}
iceServer := ICEServer{
URLs: []string{fmt.Sprintf("turn:%s:%d?transport=udp", turnIP, turnPort)},
Username: username,
Credential: password,
CredentialType: ICECredentialTypePassword,
}
offerPC, err := NewAPI(WithSettingEngine(buildSettingEngine(offerNet))).NewPeerConnection(Configuration{
ICEServers: []ICEServer{iceServer},
ICETransportPolicy: ICETransportPolicyRelay,
})
assert.NoError(t, err)
answerPC, err := NewAPI(WithSettingEngine(buildSettingEngine(answerNet))).NewPeerConnection(Configuration{
ICEServers: []ICEServer{iceServer},
ICETransportPolicy: ICETransportPolicyRelay,
})
assert.NoError(t, err)
defer closePairNow(t, offerPC, answerPC)
connected := untilConnectionState(PeerConnectionStateConnected, offerPC, answerPC)
start := time.Now()
assert.NoError(t, signalPair(offerPC, answerPC))
connected.Wait()
elapsed := time.Since(start)
assert.GreaterOrEqual(t, elapsed, wait)
assert.Less(t, elapsed, defaultRelayMinWait)
}
func TestNewICEGathererSetMediaStreamIdentification(t *testing.T) { //nolint:cyclop
// Limit runtime in case of deadlocks
lim := test.TimeOut(time.Second * 20)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
opts := ICEGatherOptions{
ICEServers: []ICEServer{{URLs: []string{"stun:stun.l.google.com:19302"}}},
}
gatherer, err := NewAPI().NewICEGatherer(opts)
assert.NoError(t, err)
expectedMid := "5"
expectedMLineIndex := uint16(1)
gatherer.setMediaStreamIdentification(expectedMid, expectedMLineIndex)
assert.Equal(t, ICEGathererStateNew, gatherer.State())
gatherFinished := make(chan struct{})
gatherer.OnLocalCandidate(func(i *ICECandidate) {
if i == nil {
close(gatherFinished)
} else {
assert.Equal(t, expectedMid, i.SDPMid)
assert.Equal(t, expectedMLineIndex, i.SDPMLineIndex)
}
})
assert.NoError(t, gatherer.Gather())
<-gatherFinished
params, err := gatherer.GetLocalParameters()
assert.NoError(t, err)
assert.NotEmpty(t, params.UsernameFragment, "Empty local username frag")
assert.NotEmpty(t, params.Password, "Empty local password")
candidates, err := gatherer.GetLocalCandidates()
assert.NoError(t, err)
assert.NotEmpty(t, candidates, "No candidates gathered")
for _, c := range candidates {
assert.Equal(t, expectedMid, c.SDPMid)
assert.Equal(t, expectedMLineIndex, c.SDPMLineIndex)
}
assert.NoError(t, gatherer.Close())
}
func TestICEGatherer_RenominationOptions(t *testing.T) {
se := SettingEngine{}
assert.NoError(t, se.SetICERenomination())
assert.True(t, se.renomination.enabled)
assert.True(t, se.renomination.automatic)
assert.Nil(t, se.renomination.automaticInterval)
assert.NotNil(t, se.renomination.generator)
}
func TestICEGatherer_RenominationOptionsDisabled(t *testing.T) {
lim := test.TimeOut(time.Second * 10)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
offerPC, answerPC, cleanup := buildRenominationVNetPair(t, false, false, nil)
defer cleanup()
connectAndWaitForICE(t, offerPC, answerPC)
agent := getAgent(t, offerPC)
selectedPair, err := agent.GetSelectedCandidatePair()
assert.NoError(t, err)
assert.NotNil(t, selectedPair)
err = agent.RenominateCandidate(selectedPair.Local, selectedPair.Remote)
assert.Error(t, err)
assert.ErrorIs(t, err, ice.ErrRenominationNotEnabled)
}
func TestICEGatherer_RenominationSendsNomination(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 35)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
nominationCh := make(chan uint32, 2)
handler := func(m *stun.Message, _, _ ice.Candidate, _ *ice.CandidatePair) bool {
var attr ice.NominationAttribute
if err := attr.GetFrom(m); err == nil {
select {
case nominationCh <- attr.Value:
default:
}
}
return false
}
offerPC, answerPC, offerSender, answerSender, cleanup := buildStagedRenominationPair(t, handler)
defer cleanup()
recvCh := make(chan string, 4)
negotiated := true
id := uint16(0)
offerDC, err := offerPC.CreateDataChannel("renomination-dc", &DataChannelInit{
Negotiated: &negotiated,
ID: &id,
})
assert.NoError(t, err)
answerDC, err := answerPC.CreateDataChannel("renomination-dc", &DataChannelInit{
Negotiated: &negotiated,
ID: &id,
})
assert.NoError(t, err)
answerDC.OnMessage(func(msg DataChannelMessage) {
select {
case recvCh <- string(msg.Data):
default:
}
})
connected := make(chan struct{})
var once sync.Once
offerPC.OnICEConnectionStateChange(func(state ICEConnectionState) {
if state == ICEConnectionStateConnected {
once.Do(func() {
close(connected)
})
}
})
startTrickleRenomination(t, offerPC, answerPC, offerSender, answerSender)
assert.NoError(t, offerSender.errValue())
assert.NoError(t, answerSender.errValue())
select {
case <-connected:
case <-time.After(15 * time.Second):
assert.Fail(t, "timed out waiting for ICE to connect")
}
pair := selectedCandidatePair(t, offerPC)
assert.NotNil(t, pair)
if pair.Remote.Type() != ice.CandidateTypeServerReflexive {
t.Logf("initial remote candidate type %s (expected srflx), continuing", pair.Remote.Type())
}
initialStat, initialStatOK := getAgent(t, offerPC).GetSelectedCandidatePairStats()
assert.True(t, initialStatOK)
assert.NoError(t, offerSender.flushHost())
assert.NoError(t, answerSender.flushHost())
waitDataChannelOpen(t, offerDC)
waitDataChannelOpen(t, answerDC)
sendAndExpect(t, offerDC, recvCh, "before-renom")
waitForTwoRemoteCandidates(t, offerPC)
waitForTwoRemoteCandidates(t, answerPC)
var switchLocal ice.Candidate
var switchRemote ice.Candidate
agent := getAgent(t, offerPC)
assert.Eventuallyf(t, func() bool {
switchLocal, switchRemote = findSwitchTarget(t, offerPC, initialStat.RemoteCandidateID)
return switchLocal != nil && switchRemote != nil
}, 10*time.Second, 50*time.Millisecond, "no alternate succeeded pair found; pairs: %s", candidatePairSummary(t, agent))
assert.NoError(t, agent.RenominateCandidate(switchLocal, switchRemote))
sendAndExpect(t, offerDC, recvCh, "after-renom")
select {
case v := <-nominationCh:
assert.Greater(t, v, uint32(0))
case <-time.After(20 * time.Second):
assert.Fail(t, "did not observe nomination attribute on binding request")
}
}
func TestICEGatherer_RenominationSwitchesPair(t *testing.T) { //nolint:cyclop
lim := test.TimeOut(time.Second * 45)
defer lim.Stop()
report := test.CheckRoutines(t)
defer report()
offerPC, answerPC, offerSender, answerSender, cleanup := buildStagedRenominationPair(t, nil)
defer cleanup()
recvCh := make(chan string, 4)
negotiated := true
id := uint16(0)
offerDC, err := offerPC.CreateDataChannel("renomination-dc", &DataChannelInit{
Negotiated: &negotiated,
ID: &id,
})
assert.NoError(t, err)
answerDC, err := answerPC.CreateDataChannel("renomination-dc", &DataChannelInit{
Negotiated: &negotiated,
ID: &id,
})
assert.NoError(t, err)
answerDC.OnMessage(func(msg DataChannelMessage) {
select {
case recvCh <- string(msg.Data):
default:
}
})
connected := make(chan struct{})
offerPC.OnICEConnectionStateChange(func(state ICEConnectionState) {
if state == ICEConnectionStateConnected {
select {
case <-connected:
default:
close(connected)
}
}
})
var flushHostOnce sync.Once
flushHosts := func() {
flushHostOnce.Do(func() {
assert.NoError(t, offerSender.flushHost())
assert.NoError(t, answerSender.flushHost())
})
}
startTrickleRenomination(t, offerPC, answerPC, offerSender, answerSender)
assert.NoError(t, offerSender.errValue())
assert.NoError(t, answerSender.errValue())
// Fallback: release host candidates even if the initial selection check stalls.
go func() {
time.Sleep(time.Second)
flushHosts()
}()
select {
case <-connected:
case <-time.After(15 * time.Second):
agent := getAgent(t, offerPC)
assert.Fail(t, "timed out waiting for initial connection; pairs: %s", candidatePairSummary(t, agent))
}
var initialRemoteType ice.CandidateType
if !assert.Eventuallyf(
t, func() bool {
if pair := selectedCandidatePair(t, offerPC); pair == nil {
return false
} else {
initialRemoteType = pair.Remote.Type()
return initialRemoteType == ice.CandidateTypeServerReflexive ||
initialRemoteType == ice.CandidateTypePeerReflexive
}
},
12*time.Second, 30*time.Millisecond,
"expected to start on a srflx/prflx remote candidate (got %s)", initialRemoteType,
) {
flushHosts()
assert.Fail(t, "expected to start on a srflx/prflx remote candidate")
}
flushHosts()
waitDataChannelOpen(t, offerDC)
waitDataChannelOpen(t, answerDC)
sendAndExpect(t, offerDC, recvCh, "before-switch")
initialPair := selectedCandidatePair(t, offerPC)
initialStat, initialStatOK := getAgent(t, offerPC).GetSelectedCandidatePairStats()
t.Logf("initial selected pair: %s<->%s (%s/%s)",
initialPair.Local.Address(), initialPair.Remote.Address(), initialPair.Local.Type(), initialPair.Remote.Type())
waitForTwoRemoteCandidates(t, offerPC)
waitForTwoRemoteCandidates(t, answerPC)
assert.True(t, initialStatOK, "missing initial selected pair stats")
switchLocal, switchRemote := findSwitchTarget(t, offerPC, initialStat.RemoteCandidateID)
assert.NotNil(t, switchLocal)
assert.NotNil(t, switchRemote)
assert.NotNil(t, switchLocal.Type())
assert.NotNil(t, switchRemote.Type())
assert.False(t, switchLocal.Equal(switchRemote), "switch local and remote candidates should be different")
t.Logf(
"renomination target: %s/%s -> %s/%s",
switchLocal.Address(), switchLocal.Type(), switchRemote.Address(), switchRemote.Type(),
)
agent := getAgent(t, offerPC)
if !assert.Eventually(t, func() bool {
pair := selectedCandidatePair(t, offerPC)
if pair != nil && pair.Local.Equal(switchLocal) && pair.Remote.Equal(switchRemote) {
return true
}
if err := agent.RenominateCandidate(switchLocal, switchRemote); err != nil {
t.Logf("renomination attempt: %v", err)
}
return false
}, 10*time.Second, 50*time.Millisecond, "selected pair should change after renomination") {
assert.Fail(t, "selected pair did not switch; pairs: %s", candidatePairSummary(t, agent))
}
finalStat, ok := agent.GetSelectedCandidatePairStats()
assert.True(t, ok)
assert.NotEqual(
t, initialStat.RemoteCandidateID, finalStat.RemoteCandidateID, "selected pair should change after renomination",
)
finalLocal := findCandidateByID(t, agent, finalStat.LocalCandidateID, true)
finalRemote := findCandidateByID(t, agent, finalStat.RemoteCandidateID, false)
assert.NotNil(t, finalLocal)
assert.NotNil(t, finalRemote)
assert.Equal(t, ice.CandidateTypeHost, finalLocal.Type())
assert.NotEqual(t, ice.CandidateTypeServerReflexive, finalRemote.Type())
finalPair := selectedCandidatePair(t, offerPC)
assert.NotNil(t, finalPair)
sendAndExpect(t, offerDC, recvCh, "after-switch")
assert.False(t, initialPair.Remote.Equal(finalPair.Remote), "expected remote candidate to change after renomination")
}
func buildRenominationVNetPair(
t *testing.T,
enableRenomination bool,
automatic bool,
bindingHandler func(*stun.Message, ice.Candidate, ice.Candidate, *ice.CandidatePair) bool,
) (*PeerConnection, *PeerConnection, func()) {
t.Helper()
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "1.2.3.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
assert.NoError(t, err)
netStack, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{"1.2.3.4"},
})
assert.NoError(t, err)
assert.NoError(t, router.AddNet(netStack))
answerNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{"1.2.3.5"},
})
assert.NoError(t, err)
assert.NoError(t, router.AddNet(answerNet))
assert.NoError(t, router.Start())
offerSE := SettingEngine{}
offerSE.SetNet(netStack)
offerSE.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
offerSE.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
if enableRenomination {
assert.NoError(t, offerSE.SetICERenomination())
if automatic {
assert.NoError(t, offerSE.SetICERenomination())
}
}
answerSE := SettingEngine{}
answerSE.SetNet(answerNet)
answerSE.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
answerSE.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
if enableRenomination {
assert.NoError(t, answerSE.SetICERenomination())
if automatic {
assert.NoError(t, answerSE.SetICERenomination())
}
}
if bindingHandler != nil {
answerSE.SetICEBindingRequestHandler(bindingHandler)
}
offerPC, err := NewAPI(WithSettingEngine(offerSE)).NewPeerConnection(Configuration{})
assert.NoError(t, err)
answerPC, err := NewAPI(WithSettingEngine(answerSE)).NewPeerConnection(Configuration{})
assert.NoError(t, err)
cleanup := func() {
closePairNow(t, offerPC, answerPC)
assert.NoError(t, router.Stop())
}
return offerPC, answerPC, cleanup
}
func connectAndWaitForICE(t *testing.T, offerPC, answerPC *PeerConnection) {
t.Helper()
connected := make(chan struct{})
var once sync.Once
offerPC.OnICEConnectionStateChange(func(state ICEConnectionState) {
if state == ICEConnectionStateConnected {
once.Do(func() {
close(connected)
})
}
})
assert.NoError(t, signalPair(offerPC, answerPC))
select {
case <-connected:
case <-time.After(5 * time.Second):
assert.Fail(t, "timed out waiting for ICE to connect")
}
}
func selectedCandidatePair(t *testing.T, pc *PeerConnection) *ice.CandidatePair {
t.Helper()
agent := getAgent(t, pc)
pair, err := agent.GetSelectedCandidatePair()
assert.NoError(t, err)
return pair
}
func waitForTwoRemoteCandidates(t *testing.T, pc *PeerConnection) {
t.Helper()
assert.Eventually(t, func() bool {
agent := getAgent(t, pc)
remotes, err := agent.GetRemoteCandidates()
assert.NoError(t, err)
return len(remotes) >= 2
}, 5*time.Second, 20*time.Millisecond)
}
func findCandidateByID(t *testing.T, agent *ice.Agent, id string, local bool) ice.Candidate {
t.Helper()
var cands []ice.Candidate
var err error
if local {
cands, err = agent.GetLocalCandidates()
} else {
cands, err = agent.GetRemoteCandidates()
}
assert.NoError(t, err)
for _, cand := range cands {
if cand.ID() == id {
return cand
}
}
return nil
}
//nolint:cyclop
func findSwitchTarget(
t *testing.T, pc *PeerConnection, excludeRemoteID string,
) (ice.Candidate, ice.Candidate) {
t.Helper()
agent := getAgent(t, pc)
var targetLocal ice.Candidate
var targetRemote ice.Candidate
for _, stat := range agent.GetCandidatePairsStats() {
if stat.State != ice.CandidatePairStateSucceeded ||
stat.LocalCandidateID == "" || stat.RemoteCandidateID == "" ||
stat.RemoteCandidateID == excludeRemoteID {
continue
}
local := findCandidateByID(t, agent, stat.LocalCandidateID, true)
remote := findCandidateByID(t, agent, stat.RemoteCandidateID, false)
if local == nil || remote == nil {
continue
}
if local.Type() != ice.CandidateTypeHost {
continue
}
if remote.Type() == ice.CandidateTypeHost {
return local, remote
}
if remote.Type() == ice.CandidateTypePeerReflexive {
targetLocal = local
targetRemote = remote
}
}
return targetLocal, targetRemote
}
func getAgent(t *testing.T, pc *PeerConnection) *ice.Agent {
t.Helper()
pc.iceTransport.lock.RLock()
agent := pc.iceTransport.gatherer.getAgent()
pc.iceTransport.lock.RUnlock()
assert.NotNil(t, agent)
return agent
}
func candidatePairSummary(t *testing.T, agent *ice.Agent) string {
t.Helper()
locals, err := agent.GetLocalCandidates()
assert.NoError(t, err)
remotes, err := agent.GetRemoteCandidates()
assert.NoError(t, err)
localMap := map[string]string{}
for _, cand := range locals {
localMap[cand.ID()] = fmt.Sprintf("%s/%s", cand.Address(), cand.Type())
}
remoteMap := map[string]string{}
for _, cand := range remotes {
remoteMap[cand.ID()] = fmt.Sprintf("%s/%s", cand.Address(), cand.Type())
}
stats := agent.GetCandidatePairsStats()
summary := make([]string, 0, len(stats))
for _, stat := range stats {
summary = append(summary, fmt.Sprintf(
"%s<->%s state=%s nominated=%v rtt=%.2fms",
localMap[stat.LocalCandidateID],
remoteMap[stat.RemoteCandidateID],
stat.State,
stat.Nominated,
stat.CurrentRoundTripTime*1000,
))
}
return strings.Join(summary, "; ")
}
func waitDataChannelOpen(t *testing.T, dc *DataChannel) {
t.Helper()
if dc.ReadyState() == DataChannelStateOpen {
return
}
done := make(chan struct{})
dc.OnOpen(func() {
close(done)
})
select {
case <-done:
case <-time.After(5 * time.Second):
assert.Fail(t, "data channel did not open")
}
}
func sendAndExpect(t *testing.T, sender *DataChannel, recvCh chan string, msg string) {
t.Helper()
err := sender.SendText(msg)
assert.NoError(t, err)
select {
case got := <-recvCh:
assert.Equal(t, msg, got)
case <-time.After(5 * time.Second):
assert.Fail(t, "did not receive data channel message")
}
}
type stagedCandidateSender struct {
remote *PeerConnection
mu sync.Mutex
srflx []ICECandidateInit
host []ICECandidateInit
err error
}
func (s *stagedCandidateSender) addCandidate(cand ICECandidateInit, srflx bool) {
s.mu.Lock()
defer s.mu.Unlock()
if s.err != nil {
return
}
if srflx && s.remote.RemoteDescription() != nil {
if err := s.remote.AddICECandidate(cand); err != nil {
s.err = err
}
return
}
if srflx {
s.srflx = append(s.srflx, cand)
} else {
s.host = append(s.host, cand)
}
}
func (s *stagedCandidateSender) flushSrflx() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.err != nil {
return s.err
}
for _, cand := range s.srflx {
if err := s.remote.AddICECandidate(cand); err != nil {
s.err = err
return err
}
}
s.srflx = nil
return s.err
}
func (s *stagedCandidateSender) flushHost() error {
s.mu.Lock()
defer s.mu.Unlock()
if s.err != nil {
return s.err
}
for _, cand := range s.host {
if err := s.remote.AddICECandidate(cand); err != nil {
s.err = err
return err
}
}
s.host = nil
return s.err
}
func (s *stagedCandidateSender) errValue() error {
s.mu.Lock()
defer s.mu.Unlock()
return s.err
}
func makeSrflxCandidateInit(c ICECandidate) ICECandidateInit {
init := c.ToJSON()
replacement := fmt.Sprintf("typ srflx raddr %s rport %d", c.Address, c.Port)
init.Candidate = strings.Replace(init.Candidate, "typ host", replacement, 1)
return init
}
func buildStagedRenominationPair(
t *testing.T,
bindingHandler func(*stun.Message, ice.Candidate, ice.Candidate, *ice.CandidatePair) bool,
) (*PeerConnection, *PeerConnection, *stagedCandidateSender, *stagedCandidateSender, func()) {
t.Helper()
const (
primaryOfferIP = "10.0.0.2"
secondaryOfferIP = "10.0.0.4"
primaryAnswerIP = "10.0.0.3"
secondaryAnswerIP = "10.0.0.5"
)
router, err := vnet.NewRouter(&vnet.RouterConfig{
CIDR: "10.0.0.0/24",
LoggerFactory: logging.NewDefaultLoggerFactory(),
})
assert.NoError(t, err)
offerNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{primaryOfferIP, secondaryOfferIP},
})
assert.NoError(t, err)
assert.NoError(t, router.AddNet(offerNet))
answerNet, err := vnet.NewNet(&vnet.NetConfig{
StaticIPs: []string{primaryAnswerIP, secondaryAnswerIP},
})
assert.NoError(t, err)
assert.NoError(t, router.AddNet(answerNet))
assert.NoError(t, router.Start())
offerSE := SettingEngine{}
offerSE.SetNet(offerNet)
offerSE.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
offerSE.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
offerSE.SetICETimeouts(5*time.Second, 15*time.Second, 200*time.Millisecond)
// prefer srflx/prflx nomination first so the test reliably observes the switch to host via renomination.
offerSE.SetSrflxAcceptanceMinWait(0)
offerSE.SetHostAcceptanceMinWait(3 * time.Second)
assert.NoError(t, offerSE.SetICERenomination(WithRenominationInterval(200*time.Millisecond)))
answerSE := SettingEngine{}
answerSE.SetNet(answerNet)
answerSE.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled)
answerSE.SetNetworkTypes([]NetworkType{NetworkTypeUDP4})
answerSE.SetICETimeouts(5*time.Second, 15*time.Second, 200*time.Millisecond)
answerSE.SetSrflxAcceptanceMinWait(0)
answerSE.SetHostAcceptanceMinWait(3 * time.Second)
assert.NoError(t, answerSE.SetICERenomination(WithRenominationInterval(200*time.Millisecond)))
if bindingHandler != nil {
answerSE.SetICEBindingRequestHandler(bindingHandler)
}
offerPC, err := NewAPI(WithSettingEngine(offerSE)).NewPeerConnection(Configuration{})
assert.NoError(t, err)
answerPC, err := NewAPI(WithSettingEngine(answerSE)).NewPeerConnection(Configuration{})
assert.NoError(t, err)
offerSender := &stagedCandidateSender{remote: answerPC}
answerSender := &stagedCandidateSender{remote: offerPC}
offerPC.OnICECandidate(func(c *ICECandidate) {
if c == nil {
return
}
switch c.Address {
case primaryOfferIP:
offerSender.addCandidate(makeSrflxCandidateInit(*c), true)
host := *c
host.Priority = 1
offerSender.addCandidate(host.ToJSON(), false)
case secondaryOfferIP:
host := *c
host.Priority = 1
offerSender.addCandidate(host.ToJSON(), false)
}
})
answerPC.OnICECandidate(func(c *ICECandidate) {
if c == nil {
return
}
switch c.Address {
case primaryAnswerIP:
answerSender.addCandidate(makeSrflxCandidateInit(*c), true)
host := *c
host.Priority = 1
answerSender.addCandidate(host.ToJSON(), false)
case secondaryAnswerIP:
host := *c
host.Priority = 1
answerSender.addCandidate(host.ToJSON(), false)
}
})
cleanup := func() {
closePairNow(t, offerPC, answerPC)
assert.NoError(t, router.Stop())
}
return offerPC, answerPC, offerSender, answerSender, cleanup
}
func startTrickleRenomination(
t *testing.T,
offerPC, answerPC *PeerConnection,
offerSender, answerSender *stagedCandidateSender,
) {
t.Helper()
_, err := offerPC.CreateDataChannel("renomination-data", nil)
assert.NoError(t, err)
offer, err := offerPC.CreateOffer(nil)
assert.NoError(t, err)
assert.NoError(t, offerPC.SetLocalDescription(offer))
assert.NoError(t, answerPC.SetRemoteDescription(offer))
answer, err := answerPC.CreateAnswer(nil)
assert.NoError(t, err)
assert.NoError(t, answerPC.SetLocalDescription(answer))
assert.NoError(t, offerPC.SetRemoteDescription(*answerPC.LocalDescription()))
assert.NoError(t, offerSender.flushSrflx())
assert.NoError(t, answerSender.flushSrflx())
}