| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660 |
- // SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
- // SPDX-License-Identifier: MIT
- //go:build !js
- // +build !js
- package ice
- import (
- "context"
- "fmt"
- "net"
- "sync/atomic"
- "testing"
- "time"
- "github.com/pion/logging"
- "github.com/pion/stun"
- "github.com/pion/transport/v2/test"
- "github.com/pion/transport/v2/vnet"
- "github.com/pion/turn/v2"
- "github.com/stretchr/testify/assert"
- )
- const (
- vnetGlobalIPA = "27.1.1.1"
- vnetLocalIPA = "192.168.0.1"
- vnetLocalSubnetMaskA = "24"
- vnetGlobalIPB = "28.1.1.1"
- vnetLocalIPB = "10.2.0.1"
- vnetLocalSubnetMaskB = "24"
- vnetSTUNServerIP = "1.2.3.4"
- vnetSTUNServerPort = 3478
- )
- type virtualNet struct {
- wan *vnet.Router
- net0 *vnet.Net
- net1 *vnet.Net
- server *turn.Server
- }
- func (v *virtualNet) close() {
- v.server.Close() //nolint:errcheck,gosec
- v.wan.Stop() //nolint:errcheck,gosec
- }
- func buildVNet(natType0, natType1 *vnet.NATType) (*virtualNet, error) {
- loggerFactory := logging.NewDefaultLoggerFactory()
- // WAN
- wan, err := vnet.NewRouter(&vnet.RouterConfig{
- CIDR: "0.0.0.0/0",
- LoggerFactory: loggerFactory,
- })
- if err != nil {
- return nil, err
- }
- wanNet, err := vnet.NewNet(&vnet.NetConfig{
- StaticIP: vnetSTUNServerIP, // Will be assigned to eth0
- })
- if err != nil {
- return nil, err
- }
- err = wan.AddNet(wanNet)
- if err != nil {
- return nil, err
- }
- // LAN 0
- lan0, err := vnet.NewRouter(&vnet.RouterConfig{
- StaticIPs: func() []string {
- if natType0.Mode == vnet.NATModeNAT1To1 {
- return []string{
- vnetGlobalIPA + "/" + vnetLocalIPA,
- }
- }
- return []string{
- vnetGlobalIPA,
- }
- }(),
- CIDR: vnetLocalIPA + "/" + vnetLocalSubnetMaskA,
- NATType: natType0,
- LoggerFactory: loggerFactory,
- })
- if err != nil {
- return nil, err
- }
- net0, err := vnet.NewNet(&vnet.NetConfig{
- StaticIPs: []string{vnetLocalIPA},
- })
- if err != nil {
- return nil, err
- }
- err = lan0.AddNet(net0)
- if err != nil {
- return nil, err
- }
- err = wan.AddRouter(lan0)
- if err != nil {
- return nil, err
- }
- // LAN 1
- lan1, err := vnet.NewRouter(&vnet.RouterConfig{
- StaticIPs: func() []string {
- if natType1.Mode == vnet.NATModeNAT1To1 {
- return []string{
- vnetGlobalIPB + "/" + vnetLocalIPB,
- }
- }
- return []string{
- vnetGlobalIPB,
- }
- }(),
- CIDR: vnetLocalIPB + "/" + vnetLocalSubnetMaskB,
- NATType: natType1,
- LoggerFactory: loggerFactory,
- })
- if err != nil {
- return nil, err
- }
- net1, err := vnet.NewNet(&vnet.NetConfig{
- StaticIPs: []string{vnetLocalIPB},
- })
- if err != nil {
- return nil, err
- }
- err = lan1.AddNet(net1)
- if err != nil {
- return nil, err
- }
- err = wan.AddRouter(lan1)
- if err != nil {
- return nil, err
- }
- // Start routers
- err = wan.Start()
- if err != nil {
- return nil, err
- }
- server, err := addVNetSTUN(wanNet, loggerFactory)
- if err != nil {
- return nil, err
- }
- return &virtualNet{
- wan: wan,
- net0: net0,
- net1: net1,
- server: server,
- }, nil
- }
- func addVNetSTUN(wanNet *vnet.Net, loggerFactory logging.LoggerFactory) (*turn.Server, error) {
- // Run TURN(STUN) server
- credMap := map[string]string{}
- credMap["user"] = "pass"
- wanNetPacketConn, err := wanNet.ListenPacket("udp", fmt.Sprintf("%s:%d", vnetSTUNServerIP, vnetSTUNServerPort))
- if err != nil {
- return nil, err
- }
- server, err := turn.NewServer(turn.ServerConfig{
- AuthHandler: func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) {
- if pw, ok := credMap[username]; ok {
- return turn.GenerateAuthKey(username, realm, pw), true
- }
- return nil, false
- },
- PacketConnConfigs: []turn.PacketConnConfig{
- {
- PacketConn: wanNetPacketConn,
- RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
- RelayAddress: net.ParseIP(vnetSTUNServerIP),
- Address: "0.0.0.0",
- Net: wanNet,
- },
- },
- },
- Realm: "pion.ly",
- LoggerFactory: loggerFactory,
- })
- if err != nil {
- return nil, err
- }
- return server, err
- }
- func connectWithVNet(aAgent, bAgent *Agent) (*Conn, *Conn) {
- // Manual signaling
- aUfrag, aPwd, err := aAgent.GetLocalUserCredentials()
- check(err)
- bUfrag, bPwd, err := bAgent.GetLocalUserCredentials()
- check(err)
- gatherAndExchangeCandidates(aAgent, bAgent)
- accepted := make(chan struct{})
- var aConn *Conn
- go func() {
- var acceptErr error
- aConn, acceptErr = aAgent.Accept(context.TODO(), bUfrag, bPwd)
- check(acceptErr)
- close(accepted)
- }()
- bConn, err := bAgent.Dial(context.TODO(), aUfrag, aPwd)
- check(err)
- // Ensure accepted
- <-accepted
- return aConn, bConn
- }
- type agentTestConfig struct {
- urls []*stun.URI
- nat1To1IPCandidateType CandidateType
- }
- func pipeWithVNet(v *virtualNet, a0TestConfig, a1TestConfig *agentTestConfig) (*Conn, *Conn) {
- aNotifier, aConnected := onConnected()
- bNotifier, bConnected := onConnected()
- var nat1To1IPs []string
- if a0TestConfig.nat1To1IPCandidateType != CandidateTypeUnspecified {
- nat1To1IPs = []string{
- vnetGlobalIPA,
- }
- }
- cfg0 := &AgentConfig{
- Urls: a0TestConfig.urls,
- NetworkTypes: supportedNetworkTypes(),
- MulticastDNSMode: MulticastDNSModeDisabled,
- NAT1To1IPs: nat1To1IPs,
- NAT1To1IPCandidateType: a0TestConfig.nat1To1IPCandidateType,
- Net: v.net0,
- }
- aAgent, err := NewAgent(cfg0)
- if err != nil {
- panic(err)
- }
- err = aAgent.OnConnectionStateChange(aNotifier)
- if err != nil {
- panic(err)
- }
- if a1TestConfig.nat1To1IPCandidateType != CandidateTypeUnspecified {
- nat1To1IPs = []string{
- vnetGlobalIPB,
- }
- }
- cfg1 := &AgentConfig{
- Urls: a1TestConfig.urls,
- NetworkTypes: supportedNetworkTypes(),
- MulticastDNSMode: MulticastDNSModeDisabled,
- NAT1To1IPs: nat1To1IPs,
- NAT1To1IPCandidateType: a1TestConfig.nat1To1IPCandidateType,
- Net: v.net1,
- }
- bAgent, err := NewAgent(cfg1)
- if err != nil {
- panic(err)
- }
- err = bAgent.OnConnectionStateChange(bNotifier)
- if err != nil {
- panic(err)
- }
- aConn, bConn := connectWithVNet(aAgent, bAgent)
- // Ensure pair selected
- // Note: this assumes ConnectionStateConnected is thrown after selecting the final pair
- <-aConnected
- <-bConnected
- return aConn, bConn
- }
- func closePipe(t *testing.T, ca *Conn, cb *Conn) bool {
- err := ca.Close()
- if !assert.NoError(t, err, "should succeed") {
- return false
- }
- err = cb.Close()
- return assert.NoError(t, err, "should succeed")
- }
- func TestConnectivityVNet(t *testing.T) {
- report := test.CheckRoutines(t)
- defer report()
- stunServerURL := &stun.URI{
- Scheme: stun.SchemeTypeSTUN,
- Host: vnetSTUNServerIP,
- Port: vnetSTUNServerPort,
- Proto: stun.ProtoTypeUDP,
- }
- turnServerURL := &stun.URI{
- Scheme: stun.SchemeTypeTURN,
- Host: vnetSTUNServerIP,
- Port: vnetSTUNServerPort,
- Username: "user",
- Password: "pass",
- Proto: stun.ProtoTypeUDP,
- }
- t.Run("Full-cone NATs on both ends", func(t *testing.T) {
- loggerFactory := logging.NewDefaultLoggerFactory()
- log := loggerFactory.NewLogger("test")
- // buildVNet with a Full-cone NATs both LANs
- natType := &vnet.NATType{
- MappingBehavior: vnet.EndpointIndependent,
- FilteringBehavior: vnet.EndpointIndependent,
- }
- v, err := buildVNet(natType, natType)
- if !assert.NoError(t, err, "should succeed") {
- return
- }
- defer v.close()
- log.Debug("Connecting...")
- a0TestConfig := &agentTestConfig{
- urls: []*stun.URI{
- stunServerURL,
- },
- }
- a1TestConfig := &agentTestConfig{
- urls: []*stun.URI{
- stunServerURL,
- },
- }
- ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig)
- time.Sleep(1 * time.Second)
- log.Debug("Closing...")
- if !closePipe(t, ca, cb) {
- return
- }
- })
- t.Run("Symmetric NATs on both ends", func(t *testing.T) {
- loggerFactory := logging.NewDefaultLoggerFactory()
- log := loggerFactory.NewLogger("test")
- // buildVNet with a Symmetric NATs for both LANs
- natType := &vnet.NATType{
- MappingBehavior: vnet.EndpointAddrPortDependent,
- FilteringBehavior: vnet.EndpointAddrPortDependent,
- }
- v, err := buildVNet(natType, natType)
- if !assert.NoError(t, err, "should succeed") {
- return
- }
- defer v.close()
- log.Debug("Connecting...")
- a0TestConfig := &agentTestConfig{
- urls: []*stun.URI{
- stunServerURL,
- turnServerURL,
- },
- }
- a1TestConfig := &agentTestConfig{
- urls: []*stun.URI{
- stunServerURL,
- },
- }
- ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig)
- log.Debug("Closing...")
- if !closePipe(t, ca, cb) {
- return
- }
- })
- t.Run("1:1 NAT with host candidate vs Symmetric NATs", func(t *testing.T) {
- loggerFactory := logging.NewDefaultLoggerFactory()
- log := loggerFactory.NewLogger("test")
- // Agent0 is behind 1:1 NAT
- natType0 := &vnet.NATType{
- Mode: vnet.NATModeNAT1To1,
- }
- // Agent1 is behind a symmetric NAT
- natType1 := &vnet.NATType{
- MappingBehavior: vnet.EndpointAddrPortDependent,
- FilteringBehavior: vnet.EndpointAddrPortDependent,
- }
- v, err := buildVNet(natType0, natType1)
- if !assert.NoError(t, err, "should succeed") {
- return
- }
- defer v.close()
- log.Debug("Connecting...")
- a0TestConfig := &agentTestConfig{
- urls: []*stun.URI{},
- nat1To1IPCandidateType: CandidateTypeHost, // Use 1:1 NAT IP as a host candidate
- }
- a1TestConfig := &agentTestConfig{
- urls: []*stun.URI{},
- }
- ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig)
- log.Debug("Closing...")
- if !closePipe(t, ca, cb) {
- return
- }
- })
- t.Run("1:1 NAT with srflx candidate vs Symmetric NATs", func(t *testing.T) {
- loggerFactory := logging.NewDefaultLoggerFactory()
- log := loggerFactory.NewLogger("test")
- // Agent0 is behind 1:1 NAT
- natType0 := &vnet.NATType{
- Mode: vnet.NATModeNAT1To1,
- }
- // Agent1 is behind a symmetric NAT
- natType1 := &vnet.NATType{
- MappingBehavior: vnet.EndpointAddrPortDependent,
- FilteringBehavior: vnet.EndpointAddrPortDependent,
- }
- v, err := buildVNet(natType0, natType1)
- if !assert.NoError(t, err, "should succeed") {
- return
- }
- defer v.close()
- log.Debug("Connecting...")
- a0TestConfig := &agentTestConfig{
- urls: []*stun.URI{},
- nat1To1IPCandidateType: CandidateTypeServerReflexive, // Use 1:1 NAT IP as a srflx candidate
- }
- a1TestConfig := &agentTestConfig{
- urls: []*stun.URI{},
- }
- ca, cb := pipeWithVNet(v, a0TestConfig, a1TestConfig)
- log.Debug("Closing...")
- if !closePipe(t, ca, cb) {
- return
- }
- })
- }
- // TestDisconnectedToConnected asserts that an agent can go to disconnected, and then return to connected successfully
- func TestDisconnectedToConnected(t *testing.T) {
- report := test.CheckRoutines(t)
- defer report()
- lim := test.TimeOut(time.Second * 10)
- defer lim.Stop()
- loggerFactory := logging.NewDefaultLoggerFactory()
- // Create a network with two interfaces
- wan, err := vnet.NewRouter(&vnet.RouterConfig{
- CIDR: "0.0.0.0/0",
- LoggerFactory: loggerFactory,
- })
- assert.NoError(t, err)
- var dropAllData uint64
- wan.AddChunkFilter(func(vnet.Chunk) bool {
- return atomic.LoadUint64(&dropAllData) != 1
- })
- net0, err := vnet.NewNet(&vnet.NetConfig{
- StaticIPs: []string{"192.168.0.1"},
- })
- assert.NoError(t, err)
- assert.NoError(t, wan.AddNet(net0))
- net1, err := vnet.NewNet(&vnet.NetConfig{
- StaticIPs: []string{"192.168.0.2"},
- })
- assert.NoError(t, err)
- assert.NoError(t, wan.AddNet(net1))
- assert.NoError(t, wan.Start())
- disconnectTimeout := time.Second
- keepaliveInterval := time.Millisecond * 20
- // Create two agents and connect them
- controllingAgent, err := NewAgent(&AgentConfig{
- NetworkTypes: supportedNetworkTypes(),
- MulticastDNSMode: MulticastDNSModeDisabled,
- Net: net0,
- DisconnectedTimeout: &disconnectTimeout,
- KeepaliveInterval: &keepaliveInterval,
- CheckInterval: &keepaliveInterval,
- })
- assert.NoError(t, err)
- controlledAgent, err := NewAgent(&AgentConfig{
- NetworkTypes: supportedNetworkTypes(),
- MulticastDNSMode: MulticastDNSModeDisabled,
- Net: net1,
- DisconnectedTimeout: &disconnectTimeout,
- KeepaliveInterval: &keepaliveInterval,
- CheckInterval: &keepaliveInterval,
- })
- assert.NoError(t, err)
- controllingStateChanges := make(chan ConnectionState, 100)
- assert.NoError(t, controllingAgent.OnConnectionStateChange(func(c ConnectionState) {
- controllingStateChanges <- c
- }))
- controlledStateChanges := make(chan ConnectionState, 100)
- assert.NoError(t, controlledAgent.OnConnectionStateChange(func(c ConnectionState) {
- controlledStateChanges <- c
- }))
- connectWithVNet(controllingAgent, controlledAgent)
- blockUntilStateSeen := func(expectedState ConnectionState, stateQueue chan ConnectionState) {
- for s := range stateQueue {
- if s == expectedState {
- return
- }
- }
- }
- // Assert we have gone to connected
- blockUntilStateSeen(ConnectionStateConnected, controllingStateChanges)
- blockUntilStateSeen(ConnectionStateConnected, controlledStateChanges)
- // Drop all packets, and block until we have gone to disconnected
- atomic.StoreUint64(&dropAllData, 1)
- blockUntilStateSeen(ConnectionStateDisconnected, controllingStateChanges)
- blockUntilStateSeen(ConnectionStateDisconnected, controlledStateChanges)
- // Allow all packets through again, block until we have gone to connected
- atomic.StoreUint64(&dropAllData, 0)
- blockUntilStateSeen(ConnectionStateConnected, controllingStateChanges)
- blockUntilStateSeen(ConnectionStateConnected, controlledStateChanges)
- assert.NoError(t, wan.Stop())
- assert.NoError(t, controllingAgent.Close())
- assert.NoError(t, controlledAgent.Close())
- }
- // Agent.Write should use the best valid pair if a selected pair is not yet available
- func TestWriteUseValidPair(t *testing.T) {
- report := test.CheckRoutines(t)
- defer report()
- lim := test.TimeOut(time.Second * 10)
- defer lim.Stop()
- loggerFactory := logging.NewDefaultLoggerFactory()
- // Create a network with two interfaces
- wan, err := vnet.NewRouter(&vnet.RouterConfig{
- CIDR: "0.0.0.0/0",
- LoggerFactory: loggerFactory,
- })
- assert.NoError(t, err)
- wan.AddChunkFilter(func(c vnet.Chunk) bool {
- if stun.IsMessage(c.UserData()) {
- m := &stun.Message{
- Raw: c.UserData(),
- }
- if decErr := m.Decode(); decErr != nil {
- return false
- } else if m.Contains(stun.AttrUseCandidate) {
- return false
- }
- }
- return true
- })
- net0, err := vnet.NewNet(&vnet.NetConfig{
- StaticIPs: []string{"192.168.0.1"},
- })
- assert.NoError(t, err)
- assert.NoError(t, wan.AddNet(net0))
- net1, err := vnet.NewNet(&vnet.NetConfig{
- StaticIPs: []string{"192.168.0.2"},
- })
- assert.NoError(t, err)
- assert.NoError(t, wan.AddNet(net1))
- assert.NoError(t, wan.Start())
- // Create two agents and connect them
- controllingAgent, err := NewAgent(&AgentConfig{
- NetworkTypes: supportedNetworkTypes(),
- MulticastDNSMode: MulticastDNSModeDisabled,
- Net: net0,
- })
- assert.NoError(t, err)
- controlledAgent, err := NewAgent(&AgentConfig{
- NetworkTypes: supportedNetworkTypes(),
- MulticastDNSMode: MulticastDNSModeDisabled,
- Net: net1,
- })
- assert.NoError(t, err)
- gatherAndExchangeCandidates(controllingAgent, controlledAgent)
- controllingUfrag, controllingPwd, err := controllingAgent.GetLocalUserCredentials()
- assert.NoError(t, err)
- controlledUfrag, controlledPwd, err := controlledAgent.GetLocalUserCredentials()
- assert.NoError(t, err)
- assert.NoError(t, controllingAgent.startConnectivityChecks(true, controlledUfrag, controlledPwd))
- assert.NoError(t, controlledAgent.startConnectivityChecks(false, controllingUfrag, controllingPwd))
- testMessage := []byte("Test Message")
- go func() {
- for {
- if _, writeErr := (&Conn{agent: controllingAgent}).Write(testMessage); writeErr != nil {
- return
- }
- time.Sleep(20 * time.Millisecond)
- }
- }()
- readBuf := make([]byte, len(testMessage))
- _, err = (&Conn{agent: controlledAgent}).Read(readBuf)
- assert.NoError(t, err)
- assert.Equal(t, readBuf, testMessage)
- assert.NoError(t, wan.Stop())
- assert.NoError(t, controllingAgent.Close())
- assert.NoError(t, controlledAgent.Close())
- }
|