mirror of
https://github.com/pion/webrtc.git
synced 2025-12-24 11:51:03 +08:00
2703 lines
70 KiB
Go
2703 lines
70 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_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())
|
|
}
|