Files
ice/gather_test.go
cnderrauber 04a6027e93 Use MultiUDPMux to implement listen any address
In #475, import low-level API (ReadMsgUDP) to determine
destination interface for packets received by UDPConn listen
at any(unspecified) address, to fix msg received by incorrect
candidate that shared same ufrags. But the api has compatibility
issues, also not reliable in some special network cases like
AWS/ECS.
So this pr revert that change, and make UDPMuxDefault not
accept Conn listen at unspecified address. Also provide a
NewMultiUDPMuxFromPort helper function to create a MultiUDPMux
to listen at all addresses.
For ice gather, it will use UDPMux's listen address to generate
canidates instead of create it from interfaces.
2022-10-10 20:22:30 +08:00

701 lines
19 KiB
Go

//go:build !js
// +build !js
package ice
import (
"context"
"crypto/tls"
"io"
"net"
"net/url"
"reflect"
"sort"
"strconv"
"sync"
"testing"
"time"
"github.com/pion/dtls/v2"
"github.com/pion/dtls/v2/pkg/crypto/selfsign"
"github.com/pion/logging"
"github.com/pion/stun"
"github.com/pion/transport/test"
"github.com/pion/turn/v2"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/net/proxy"
)
func TestListenUDP(t *testing.T) {
a, err := NewAgent(&AgentConfig{})
assert.NoError(t, err)
localIPs, err := localInterfaces(a.net, a.interfaceFilter, a.ipFilter, []NetworkType{NetworkTypeUDP4})
assert.NotEqual(t, len(localIPs), 0, "localInterfaces found no interfaces, unable to test")
assert.NoError(t, err)
ip := localIPs[0]
conn, err := listenUDPInPortRange(a.net, a.log, 0, 0, udp, &net.UDPAddr{IP: ip, Port: 0})
assert.NoError(t, err, "listenUDP error with no port restriction")
assert.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn")
_, err = listenUDPInPortRange(a.net, a.log, 4999, 5000, udp, &net.UDPAddr{IP: ip, Port: 0})
assert.Equal(t, err, ErrPort, "listenUDP with invalid port range did not return ErrPort")
conn, err = listenUDPInPortRange(a.net, a.log, 5000, 5000, udp, &net.UDPAddr{IP: ip, Port: 0})
assert.NoError(t, err, "listenUDP error with no port restriction")
assert.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn")
_, port, err := net.SplitHostPort(conn.LocalAddr().String())
assert.NoError(t, err)
assert.Equal(t, port, "5000", "listenUDP with port restriction of 5000 listened on incorrect port")
portMin := 5100
portMax := 5109
total := portMax - portMin + 1
result := make([]int, 0, total)
portRange := make([]int, 0, total)
for i := 0; i < total; i++ {
conn, err = listenUDPInPortRange(a.net, a.log, portMax, portMin, udp, &net.UDPAddr{IP: ip, Port: 0})
assert.NoError(t, err, "listenUDP error with no port restriction")
assert.NotNil(t, conn, "listenUDP error with no port restriction return a nil conn")
_, port, err = net.SplitHostPort(conn.LocalAddr().String())
if err != nil {
t.Fatal(err)
}
p, _ := strconv.Atoi(port)
if p < portMin || p > portMax {
t.Fatalf("listenUDP with port restriction [%d, %d] listened on incorrect port (%s)", portMin, portMax, port)
}
result = append(result, p)
portRange = append(portRange, portMin+i)
}
if sort.IntsAreSorted(result) {
t.Fatalf("listenUDP with port restriction [%d, %d], ports result should be random", portMin, portMax)
}
sort.Ints(result)
if !reflect.DeepEqual(result, portRange) {
t.Fatalf("listenUDP with port restriction [%d, %d], got:%v, want:%v", portMin, portMax, result, portRange)
}
_, err = listenUDPInPortRange(a.net, a.log, portMax, portMin, udp, &net.UDPAddr{IP: ip, Port: 0})
assert.Equal(t, err, ErrPort, "listenUDP with port restriction [%d, %d], did not return ErrPort", portMin, portMax)
assert.NoError(t, a.Close())
}
// Assert that STUN gathering is done concurrently
func TestSTUNConcurrency(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
serverPort := randomPort(t)
serverListener, err := net.ListenPacket("udp4", "127.0.0.1:"+strconv.Itoa(serverPort))
assert.NoError(t, err)
server, err := turn.NewServer(turn.ServerConfig{
Realm: "pion.ly",
AuthHandler: optimisticAuthHandler,
PacketConnConfigs: []turn.PacketConnConfig{
{
PacketConn: serverListener,
RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"},
},
},
})
assert.NoError(t, err)
urls := []*URL{}
for i := 0; i <= 10; i++ {
urls = append(urls, &URL{
Scheme: SchemeTypeSTUN,
Host: "127.0.0.1",
Port: serverPort + 1,
})
}
urls = append(urls, &URL{
Scheme: SchemeTypeSTUN,
Host: "127.0.0.1",
Port: serverPort,
})
listener, err := net.ListenTCP("tcp", &net.TCPAddr{
IP: net.IP{127, 0, 0, 1},
})
require.NoError(t, err)
defer func() {
_ = listener.Close()
}()
a, err := NewAgent(&AgentConfig{
NetworkTypes: supportedNetworkTypes(),
Urls: urls,
CandidateTypes: []CandidateType{CandidateTypeHost, CandidateTypeServerReflexive},
TCPMux: NewTCPMuxDefault(
TCPMuxParams{
Listener: listener,
Logger: logging.NewDefaultLoggerFactory().NewLogger("ice"),
ReadBufferSize: 8,
},
),
})
assert.NoError(t, err)
candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background())
assert.NoError(t, a.OnCandidate(func(c Candidate) {
if c == nil {
candidateGatheredFunc()
return
}
t.Log(c.NetworkType(), c.Priority(), c)
}))
assert.NoError(t, a.GatherCandidates())
<-candidateGathered.Done()
assert.NoError(t, a.Close())
assert.NoError(t, server.Close())
}
// Assert that TURN gathering is done concurrently
func TestTURNConcurrency(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
runTest := func(protocol ProtoType, scheme SchemeType, packetConn net.PacketConn, listener net.Listener, serverPort int) {
packetConnConfigs := []turn.PacketConnConfig{}
if packetConn != nil {
packetConnConfigs = append(packetConnConfigs, turn.PacketConnConfig{
PacketConn: packetConn,
RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"},
})
}
listenerConfigs := []turn.ListenerConfig{}
if listener != nil {
listenerConfigs = append(listenerConfigs, turn.ListenerConfig{
Listener: listener,
RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"},
})
}
server, err := turn.NewServer(turn.ServerConfig{
Realm: "pion.ly",
AuthHandler: optimisticAuthHandler,
PacketConnConfigs: packetConnConfigs,
ListenerConfigs: listenerConfigs,
})
assert.NoError(t, err)
urls := []*URL{}
for i := 0; i <= 10; i++ {
urls = append(urls, &URL{
Scheme: scheme,
Host: "127.0.0.1",
Username: "username",
Password: "password",
Proto: protocol,
Port: serverPort + 1 + i,
})
}
urls = append(urls, &URL{
Scheme: scheme,
Host: "127.0.0.1",
Username: "username",
Password: "password",
Proto: protocol,
Port: serverPort,
})
a, err := NewAgent(&AgentConfig{
CandidateTypes: []CandidateType{CandidateTypeRelay},
InsecureSkipVerify: true,
NetworkTypes: supportedNetworkTypes(),
Urls: urls,
})
assert.NoError(t, err)
candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background())
assert.NoError(t, a.OnCandidate(func(c Candidate) {
if c != nil {
candidateGatheredFunc()
}
}))
assert.NoError(t, a.GatherCandidates())
<-candidateGathered.Done()
assert.NoError(t, a.Close())
assert.NoError(t, server.Close())
}
t.Run("UDP Relay", func(t *testing.T) {
serverPort := randomPort(t)
serverListener, err := net.ListenPacket("udp", "127.0.0.1:"+strconv.Itoa(serverPort))
assert.NoError(t, err)
runTest(ProtoTypeUDP, SchemeTypeTURN, serverListener, nil, serverPort)
})
t.Run("TCP Relay", func(t *testing.T) {
serverPort := randomPort(t)
serverListener, err := net.Listen("tcp", "127.0.0.1:"+strconv.Itoa(serverPort))
assert.NoError(t, err)
runTest(ProtoTypeTCP, SchemeTypeTURN, nil, serverListener, serverPort)
})
t.Run("TLS Relay", func(t *testing.T) {
certificate, genErr := selfsign.GenerateSelfSigned()
assert.NoError(t, genErr)
serverPort := randomPort(t)
serverListener, err := tls.Listen("tcp", "127.0.0.1:"+strconv.Itoa(serverPort), &tls.Config{ //nolint:gosec
Certificates: []tls.Certificate{certificate},
})
assert.NoError(t, err)
runTest(ProtoTypeTCP, SchemeTypeTURNS, nil, serverListener, serverPort)
})
t.Run("DTLS Relay", func(t *testing.T) {
certificate, genErr := selfsign.GenerateSelfSigned()
assert.NoError(t, genErr)
serverPort := randomPort(t)
serverListener, err := dtls.Listen("udp", &net.UDPAddr{IP: net.ParseIP("127.0.0.1"), Port: serverPort}, &dtls.Config{
Certificates: []tls.Certificate{certificate},
})
assert.NoError(t, err)
runTest(ProtoTypeUDP, SchemeTypeTURNS, nil, serverListener, serverPort)
})
}
// Assert that STUN and TURN gathering are done concurrently
func TestSTUNTURNConcurrency(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
lim := test.TimeOut(time.Second * 8)
defer lim.Stop()
serverPort := randomPort(t)
serverListener, err := net.ListenPacket("udp4", "127.0.0.1:"+strconv.Itoa(serverPort))
assert.NoError(t, err)
server, err := turn.NewServer(turn.ServerConfig{
Realm: "pion.ly",
AuthHandler: optimisticAuthHandler,
PacketConnConfigs: []turn.PacketConnConfig{
{
PacketConn: serverListener,
RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"},
},
},
})
assert.NoError(t, err)
urls := []*URL{}
for i := 0; i <= 10; i++ {
urls = append(urls, &URL{
Scheme: SchemeTypeSTUN,
Host: "127.0.0.1",
Port: serverPort + 1,
})
}
urls = append(urls, &URL{
Scheme: SchemeTypeTURN,
Proto: ProtoTypeUDP,
Host: "127.0.0.1",
Port: serverPort,
Username: "username",
Password: "password",
})
a, err := NewAgent(&AgentConfig{
NetworkTypes: supportedNetworkTypes(),
Urls: urls,
CandidateTypes: []CandidateType{CandidateTypeServerReflexive, CandidateTypeRelay},
})
assert.NoError(t, err)
{
gatherLim := test.TimeOut(time.Second * 3) // As TURN and STUN should be checked in parallel, this should complete before the default STUN timeout (5s)
candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background())
assert.NoError(t, a.OnCandidate(func(c Candidate) {
if c != nil {
candidateGatheredFunc()
}
}))
assert.NoError(t, a.GatherCandidates())
<-candidateGathered.Done()
gatherLim.Stop()
}
assert.NoError(t, a.Close())
assert.NoError(t, server.Close())
}
// Assert that srflx candidates can be gathered from TURN servers
//
// When TURN servers are utilized, both types of candidates
// (i.e. srflx and relay) are obtained from the TURN server.
//
// https://tools.ietf.org/html/rfc5245#section-2.1
func TestTURNSrflx(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
serverPort := randomPort(t)
serverListener, err := net.ListenPacket("udp4", "127.0.0.1:"+strconv.Itoa(serverPort))
assert.NoError(t, err)
server, err := turn.NewServer(turn.ServerConfig{
Realm: "pion.ly",
AuthHandler: optimisticAuthHandler,
PacketConnConfigs: []turn.PacketConnConfig{
{
PacketConn: serverListener,
RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: "127.0.0.1"},
},
},
})
assert.NoError(t, err)
urls := []*URL{{
Scheme: SchemeTypeTURN,
Proto: ProtoTypeUDP,
Host: "127.0.0.1",
Port: serverPort,
Username: "username",
Password: "password",
}}
a, err := NewAgent(&AgentConfig{
NetworkTypes: supportedNetworkTypes(),
Urls: urls,
CandidateTypes: []CandidateType{CandidateTypeServerReflexive, CandidateTypeRelay},
})
assert.NoError(t, err)
candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background())
assert.NoError(t, a.OnCandidate(func(c Candidate) {
if c != nil && c.Type() == CandidateTypeServerReflexive {
candidateGatheredFunc()
}
}))
assert.NoError(t, a.GatherCandidates())
<-candidateGathered.Done()
assert.NoError(t, a.Close())
assert.NoError(t, server.Close())
}
func TestCloseConnLog(t *testing.T) {
a, err := NewAgent(&AgentConfig{})
assert.NoError(t, err)
closeConnAndLog(nil, a.log, "normal nil")
var nc *net.UDPConn
closeConnAndLog(nc, a.log, "nil ptr")
assert.NoError(t, a.Close())
}
type mockProxy struct {
proxyWasDialed func()
}
type mockConn struct{}
func (m *mockConn) Read(b []byte) (n int, err error) { return 0, io.EOF }
func (m *mockConn) Write(b []byte) (int, error) { return 0, io.EOF }
func (m *mockConn) Close() error { return io.EOF }
func (m *mockConn) LocalAddr() net.Addr { return &net.TCPAddr{} }
func (m *mockConn) RemoteAddr() net.Addr { return &net.TCPAddr{} }
func (m *mockConn) SetDeadline(t time.Time) error { return io.EOF }
func (m *mockConn) SetReadDeadline(t time.Time) error { return io.EOF }
func (m *mockConn) SetWriteDeadline(t time.Time) error { return io.EOF }
func (m *mockProxy) Dial(network, addr string) (net.Conn, error) {
m.proxyWasDialed()
return &mockConn{}, nil
}
func TestTURNProxyDialer(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
proxyWasDialed, proxyWasDialedFunc := context.WithCancel(context.Background())
proxy.RegisterDialerType("tcp", func(*url.URL, proxy.Dialer) (proxy.Dialer, error) {
return &mockProxy{proxyWasDialedFunc}, nil
})
tcpProxyURI, err := url.Parse("tcp://fakeproxy:3128")
assert.NoError(t, err)
proxyDialer, err := proxy.FromURL(tcpProxyURI, proxy.Direct)
assert.NoError(t, err)
a, err := NewAgent(&AgentConfig{
CandidateTypes: []CandidateType{CandidateTypeRelay},
NetworkTypes: supportedNetworkTypes(),
Urls: []*URL{
{
Scheme: SchemeTypeTURN,
Host: "127.0.0.1",
Username: "username",
Password: "password",
Proto: ProtoTypeTCP,
Port: 5000,
},
},
ProxyDialer: proxyDialer,
})
assert.NoError(t, err)
candidateGatherFinish, candidateGatherFinishFunc := context.WithCancel(context.Background())
assert.NoError(t, a.OnCandidate(func(c Candidate) {
if c == nil {
candidateGatherFinishFunc()
}
}))
assert.NoError(t, a.GatherCandidates())
<-candidateGatherFinish.Done()
<-proxyWasDialed.Done()
assert.NoError(t, a.Close())
}
// Assert that candidates are given for each mux in a MultiUDPMux
func TestMultiUDPMuxUsage(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
var expectedPorts []int
var udpMuxInstances []UDPMux
for i := 0; i < 3; i++ {
port := randomPort(t)
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: port})
assert.NoError(t, err)
defer func() {
_ = conn.Close()
}()
expectedPorts = append(expectedPorts, port)
muxDefault, err := NewUDPMuxDefault(UDPMuxParams{UDPConn: conn})
assert.NoError(t, err)
udpMuxInstances = append(udpMuxInstances, muxDefault)
idx := i
defer func() {
_ = udpMuxInstances[idx].Close()
}()
}
a, err := NewAgent(&AgentConfig{
NetworkTypes: supportedNetworkTypes(),
CandidateTypes: []CandidateType{CandidateTypeHost},
UDPMux: NewMultiUDPMuxDefault(udpMuxInstances...),
})
assert.NoError(t, err)
candidateCh := make(chan Candidate)
assert.NoError(t, a.OnCandidate(func(c Candidate) {
if c == nil {
close(candidateCh)
return
}
candidateCh <- c
}))
assert.NoError(t, a.GatherCandidates())
portFound := make(map[int]bool)
for c := range candidateCh {
portFound[c.Port()] = true
assert.True(t, c.NetworkType().IsUDP(), "All candidates should be UDP")
}
assert.Len(t, portFound, len(expectedPorts))
for _, port := range expectedPorts {
assert.True(t, portFound[port], "There should be a candidate for each UDP mux port")
}
assert.NoError(t, a.Close())
}
// Assert that candidates are given for each mux in a MultiTCPMux
func TestMultiTCPMuxUsage(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
var expectedPorts []int
var tcpMuxInstances []TCPMux
for i := 0; i < 3; i++ {
port := randomPort(t)
listener, err := net.ListenTCP("tcp", &net.TCPAddr{
IP: net.IP{127, 0, 0, 1},
Port: port,
})
assert.NoError(t, err)
defer func() {
_ = listener.Close()
}()
expectedPorts = append(expectedPorts, port)
tcpMuxInstances = append(tcpMuxInstances, NewTCPMuxDefault(TCPMuxParams{
Listener: listener,
ReadBufferSize: 8,
}))
}
a, err := NewAgent(&AgentConfig{
NetworkTypes: supportedNetworkTypes(),
CandidateTypes: []CandidateType{CandidateTypeHost},
TCPMux: NewMultiTCPMuxDefault(tcpMuxInstances...),
})
assert.NoError(t, err)
candidateCh := make(chan Candidate)
assert.NoError(t, a.OnCandidate(func(c Candidate) {
if c == nil {
close(candidateCh)
return
}
candidateCh <- c
}))
assert.NoError(t, a.GatherCandidates())
portFound := make(map[int]bool)
for c := range candidateCh {
if c.NetworkType().IsTCP() {
portFound[c.Port()] = true
}
}
assert.Len(t, portFound, len(expectedPorts))
for _, port := range expectedPorts {
assert.True(t, portFound[port], "There should be a candidate for each TCP mux port")
}
assert.NoError(t, a.Close())
}
// Assert that UniversalUDPMux is used while gathering when configured in the Agent
func TestUniversalUDPMuxUsage(t *testing.T) {
report := test.CheckRoutines(t)
defer report()
lim := test.TimeOut(time.Second * 30)
defer lim.Stop()
conn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IP{127, 0, 0, 1}, Port: randomPort(t)})
assert.NoError(t, err)
defer func() {
_ = conn.Close()
}()
udpMuxSrflx := &universalUDPMuxMock{
conn: conn,
}
numSTUNS := 3
urls := []*URL{}
for i := 0; i < numSTUNS; i++ {
urls = append(urls, &URL{
Scheme: SchemeTypeSTUN,
Host: "127.0.0.1",
Port: 3478 + i,
})
}
a, err := NewAgent(&AgentConfig{
NetworkTypes: supportedNetworkTypes(),
Urls: urls,
CandidateTypes: []CandidateType{CandidateTypeServerReflexive},
UDPMuxSrflx: udpMuxSrflx,
})
assert.NoError(t, err)
candidateGathered, candidateGatheredFunc := context.WithCancel(context.Background())
assert.NoError(t, a.OnCandidate(func(c Candidate) {
if c == nil {
candidateGatheredFunc()
return
}
t.Log(c.NetworkType(), c.Priority(), c)
}))
assert.NoError(t, a.GatherCandidates())
<-candidateGathered.Done()
assert.NoError(t, a.Close())
// twice because of 2 STUN servers configured
assert.Equal(t, numSTUNS, udpMuxSrflx.getXORMappedAddrUsedTimes, "expected times that GetXORMappedAddr should be called")
// one for Restart() when agent has been initialized and one time when Close() the agent
assert.Equal(t, 2, udpMuxSrflx.removeConnByUfragTimes, "expected times that RemoveConnByUfrag should be called")
// twice because of 2 STUN servers configured
assert.Equal(t, numSTUNS, udpMuxSrflx.getConnForURLTimes, "expected times that GetConnForURL should be called")
}
type universalUDPMuxMock struct {
UDPMux
getXORMappedAddrUsedTimes int
removeConnByUfragTimes int
getConnForURLTimes int
mu sync.Mutex
conn *net.UDPConn
}
func (m *universalUDPMuxMock) GetRelayedAddr(turnAddr net.Addr, deadline time.Duration) (*net.Addr, error) {
return nil, errNotImplemented
}
func (m *universalUDPMuxMock) GetConnForURL(ufrag string, url string, addr net.Addr) (net.PacketConn, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.getConnForURLTimes++
return m.conn, nil
}
func (m *universalUDPMuxMock) GetXORMappedAddr(serverAddr net.Addr, deadline time.Duration) (*stun.XORMappedAddress, error) {
m.mu.Lock()
defer m.mu.Unlock()
m.getXORMappedAddrUsedTimes++
return &stun.XORMappedAddress{IP: net.IP{100, 64, 0, 1}, Port: 77878}, nil
}
func (m *universalUDPMuxMock) RemoveConnByUfrag(ufrag string) {
m.mu.Lock()
defer m.mu.Unlock()
m.removeConnByUfragTimes++
}
func (m *universalUDPMuxMock) GetListenAddresses() []net.Addr {
return []net.Addr{m.conn.LocalAddr()}
}