package ice import ( "context" "net" "sync" "testing" "time" "github.com/pion/logging" "github.com/pion/stun" "github.com/pion/transport/test" ) type mockPacketConn struct { } func (m *mockPacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) { return 0, nil, nil } func (m *mockPacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) { return 0, nil } func (m *mockPacketConn) Close() error { return nil } func (m *mockPacketConn) LocalAddr() net.Addr { return nil } func (m *mockPacketConn) SetDeadline(t time.Time) error { return nil } func (m *mockPacketConn) SetReadDeadline(t time.Time) error { return nil } func (m *mockPacketConn) SetWriteDeadline(t time.Time) error { return nil } func TestPairSearch(t *testing.T) { // Limit runtime in case of deadlocks lim := test.TimeOut(time.Second * 10) defer lim.Stop() var config AgentConfig a, err := NewAgent(&config) if err != nil { t.Fatalf("Error constructing ice.Agent") } if len(a.checklist) != 0 { t.Fatalf("TestPairSearch is only a valid test if a.validPairs is empty on construction") } cp := a.getBestAvailableCandidatePair() if cp != nil { t.Fatalf("No Candidate pairs should exist") } err = a.Close() if err != nil { t.Fatalf("Close agent emits error %v", err) } } func TestPairPriority(t *testing.T) { // avoid deadlocks? defer test.TimeOut(1 * time.Second).Stop() a, err := NewAgent(&AgentConfig{}) if err != nil { t.Fatalf("Failed to create agent: %s", err) } hostLocal, err := NewCandidateHost( "udp", net.ParseIP("192.168.1.1"), 19216, 1, ) if err != nil { t.Fatalf("Failed to construct local host candidate: %s", err) } relayRemote, err := NewCandidateRelay( "udp", net.ParseIP("1.2.3.4"), 12340, 1, "4.3.2.1", 43210, ) if err != nil { t.Fatalf("Failed to construct remote relay candidate: %s", err) } srflxRemote, err := NewCandidateServerReflexive( "udp", net.ParseIP("10.10.10.2"), 19218, 1, "4.3.2.1", 43212, ) if err != nil { t.Fatalf("Failed to construct remote srflx candidate: %s", err) } prflxRemote, err := NewCandidatePeerReflexive( "udp", net.ParseIP("10.10.10.2"), 19217, 1, "4.3.2.1", 43211, ) if err != nil { t.Fatalf("Failed to construct remote prflx candidate: %s", err) } hostRemote, err := NewCandidateHost( "udp", net.ParseIP("1.2.3.5"), 12350, 1, ) if err != nil { t.Fatalf("Failed to construct remote host candidate: %s", err) } for _, remote := range []Candidate{relayRemote, srflxRemote, prflxRemote, hostRemote} { p := a.findPair(hostLocal, remote) if p == nil { p = a.addPair(hostLocal, remote) } p.state = candidatePairStateValid bestPair := a.getBestValidCandidatePair() if bestPair.String() != (&candidatePair{remote: remote, local: hostLocal}).String() { t.Fatalf("Unexpected bestPair %s (expected remote: %s)", bestPair, remote) } } if err := a.Close(); err != nil { t.Fatalf("Error on agent.Close(): %s", err) } } func TestOnSelectedCandidatePairChange(t *testing.T) { // avoid deadlocks? defer test.TimeOut(1 * time.Second).Stop() a, err := NewAgent(&AgentConfig{}) if err != nil { t.Fatalf("Failed to create agent: %s", err) } callbackCalled := make(chan struct{}, 1) if err = a.OnSelectedCandidatePairChange(func(local, remote Candidate) { close(callbackCalled) }); err != nil { t.Fatalf("Failed to set agent OnCandidatePairChange callback: %s", err) } hostLocal, err := NewCandidateHost( "udp", net.ParseIP("192.168.1.1"), 19216, 1, ) if err != nil { t.Fatalf("Failed to construct local host candidate: %s", err) } relayRemote, err := NewCandidateRelay( "udp", net.ParseIP("1.2.3.4"), 12340, 1, "4.3.2.1", 43210, ) if err != nil { t.Fatalf("Failed to construct remote relay candidate: %s", err) } // select the pair if err = a.run(func(agent *Agent) { p := newCandidatePair(hostLocal, relayRemote, false) agent.setSelectedPair(p) }); err != nil { t.Fatalf("Failed to setValidPair(): %s", err) } // ensure that the callback fired on setting the pair <-callbackCalled } type BadAddr struct{} func (ba *BadAddr) Network() string { return "xxx" } func (ba *BadAddr) String() string { return "yyy" } func runAgentTest(t *testing.T, config *AgentConfig, task func(a *Agent)) { a, err := NewAgent(config) if err != nil { t.Fatalf("Error constructing ice.Agent") } if err := a.run(task); err != nil { t.Fatalf("Agent run failure: %v", err) } } func TestHandlePeerReflexive(t *testing.T) { // Limit runtime in case of deadlocks lim := test.TimeOut(time.Second * 2) defer lim.Stop() t.Run("UDP pflx candidate from handleInbound()", func(t *testing.T) { var config AgentConfig runAgentTest(t, &config, func(a *Agent) { a.selector = &controllingSelector{agent: a, log: a.log} ip := net.ParseIP("192.168.0.2") local, err := NewCandidateHost("udp", ip, 777, 1) local.conn = &mockPacketConn{} if err != nil { t.Fatalf("failed to create a new candidate: %v", err) } remote := &net.UDPAddr{IP: net.ParseIP("172.17.0.3"), Port: 999} msg, err := stun.Build(stun.BindingRequest, stun.TransactionID, stun.NewUsername(a.localUfrag+":"+a.remoteUfrag), UseCandidate, AttrControlling(a.tieBreaker), PriorityAttr(local.Priority()), stun.NewShortTermIntegrity(a.localPwd), stun.Fingerprint, ) if err != nil { t.Fatal(err) } a.handleInbound(msg, local, remote) // length of remote candidate list must be one now if len(a.remoteCandidates) != 1 { t.Fatal("failed to add a network type to the remote candidate list") } // length of remote candidate list for a network type must be 1 set := a.remoteCandidates[local.NetworkType()] if len(set) != 1 { t.Fatal("failed to add prflx candidate to remote candidate list") } c := set[0] if c.Type() != CandidateTypePeerReflexive { t.Fatal("candidate type must be prflx") } if !c.IP().Equal(net.ParseIP("172.17.0.3")) { t.Fatal("IP address mismatch") } if c.Port() != 999 { t.Fatal("Port number mismatch") } err = a.Close() if err != nil { t.Fatalf("Close agent emits error %v", err) } }) }) t.Run("Bad network type with handleInbound()", func(t *testing.T) { var config AgentConfig runAgentTest(t, &config, func(a *Agent) { a.selector = &controllingSelector{agent: a, log: a.log} ip := net.ParseIP("192.168.0.2") local, err := NewCandidateHost("tcp", ip, 777, 1) if err != nil { t.Fatalf("failed to create a new candidate: %v", err) } remote := &BadAddr{} a.handleInbound(nil, local, remote) if len(a.remoteCandidates) != 0 { t.Fatal("bad address should not be added to the remote candidate list") } err = a.Close() if err != nil { t.Fatalf("Close agent emits error %v", err) } }) }) t.Run("Success from unknown remote, prflx candidate MUST only be created via Binding Request", func(t *testing.T) { var config AgentConfig runAgentTest(t, &config, func(a *Agent) { a.selector = &controllingSelector{agent: a, log: a.log} tID := [stun.TransactionIDSize]byte{} copy(tID[:], []byte("ABC")) a.pendingBindingRequests = []bindingRequest{ {tID, &net.UDPAddr{}, false}, } local, err := NewCandidateHost("udp", net.ParseIP("192.168.0.2"), 777, 1) local.conn = &mockPacketConn{} if err != nil { t.Fatalf("failed to create a new candidate: %v", err) } remote := &net.UDPAddr{IP: net.ParseIP("172.17.0.3"), Port: 999} msg, err := stun.Build(stun.BindingSuccess, stun.NewTransactionIDSetter(tID), stun.NewShortTermIntegrity(a.remotePwd), stun.Fingerprint, ) if err != nil { t.Fatal(err) } a.handleInbound(msg, local, remote) if len(a.remoteCandidates) != 0 { t.Fatal("unknown remote was able to create a candidate") } }) }) } // Assert that Agent on startup sends message, and doesn't wait for taskloop to start // github.com/pion/ice/issues/15 func TestConnectivityOnStartup(t *testing.T) { lim := test.TimeOut(time.Second * 5) defer lim.Stop() cfg := &AgentConfig{ Urls: []*URL{}, Trickle: false, NetworkTypes: supportedNetworkTypes, taskLoopInterval: time.Hour, LoggerFactory: logging.NewDefaultLoggerFactory(), } aNotifier, aConnected := onConnected() bNotifier, bConnected := onConnected() aAgent, err := NewAgent(cfg) if err != nil { t.Error(err) } err = aAgent.OnConnectionStateChange(aNotifier) if err != nil { panic(err) } bAgent, err := NewAgent(cfg) if err != nil { t.Error(err) } err = bAgent.OnConnectionStateChange(bNotifier) if err != nil { panic(err) } connect(aAgent, bAgent) <-aConnected <-bConnected } func TestInboundValidity(t *testing.T) { buildMsg := func(class stun.MessageClass, username, key string) *stun.Message { msg, err := stun.Build(stun.NewType(stun.MethodBinding, class), stun.TransactionID, stun.NewUsername(username), stun.NewShortTermIntegrity(key), stun.Fingerprint, ) if err != nil { t.Fatal(err) } return msg } remote := &net.UDPAddr{IP: net.ParseIP("172.17.0.3"), Port: 999} local, err := NewCandidateHost("udp", net.ParseIP("192.168.0.2"), 777, 1) local.conn = &mockPacketConn{} if err != nil { t.Fatalf("failed to create a new candidate: %v", err) } t.Run("Invalid Binding requests should be discarded", func(t *testing.T) { a, err := NewAgent(&AgentConfig{}) if err != nil { t.Fatalf("Error constructing ice.Agent") } a.handleInbound(buildMsg(stun.ClassRequest, "invalid", a.localPwd), local, remote) if len(a.remoteCandidates) == 1 { t.Fatal("Binding with invalid Username was able to create prflx candidate") } a.handleInbound(buildMsg(stun.ClassRequest, a.localUfrag+":"+a.remoteUfrag, "Invalid"), local, remote) if len(a.remoteCandidates) == 1 { t.Fatal("Binding with invalid MessageIntegrity was able to create prflx candidate") } }) t.Run("Invalid Binding success responses should be discarded", func(t *testing.T) { a, err := NewAgent(&AgentConfig{}) if err != nil { t.Fatalf("Error constructing ice.Agent") } a.handleInbound(buildMsg(stun.ClassSuccessResponse, a.localUfrag+":"+a.remoteUfrag, "Invalid"), local, remote) if len(a.remoteCandidates) == 1 { t.Fatal("Binding with invalid MessageIntegrity was able to create prflx candidate") } }) t.Run("Discard non-binding messages", func(t *testing.T) { a, err := NewAgent(&AgentConfig{}) if err != nil { t.Fatalf("Error constructing ice.Agent") } a.handleInbound(buildMsg(stun.ClassErrorResponse, a.localUfrag+":"+a.remoteUfrag, "Invalid"), local, remote) if len(a.remoteCandidates) == 1 { t.Fatal("non-binding message was able to create prflxRemote") } }) t.Run("Valid bind request", func(t *testing.T) { a, err := NewAgent(&AgentConfig{}) if err != nil { t.Fatalf("Error constructing ice.Agent") } err = a.run(func(a *Agent) { a.selector = &controllingSelector{agent: a, log: a.log} a.handleInbound(buildMsg(stun.ClassRequest, a.localUfrag+":"+a.remoteUfrag, a.localPwd), local, remote) if len(a.remoteCandidates) != 1 { t.Fatal("Binding with valid values was unable to create prflx candidate") } }) if err != nil { t.Fatalf("Agent run failure: %v", err) } }) t.Run("Valid bind without fingerprint", func(t *testing.T) { var config AgentConfig runAgentTest(t, &config, func(a *Agent) { a.selector = &controllingSelector{agent: a, log: a.log} msg, err := stun.Build(stun.BindingRequest, stun.TransactionID, stun.NewUsername(a.localUfrag+":"+a.remoteUfrag), stun.NewShortTermIntegrity(a.localPwd), ) if err != nil { t.Fatal(err) } a.handleInbound(msg, local, remote) if len(a.remoteCandidates) != 1 { t.Fatal("Binding with valid values (but no fingerprint) was unable to create prflx candidate") } }) }) t.Run("Success with invalid TransactionID", func(t *testing.T) { a, err := NewAgent(&AgentConfig{}) if err != nil { t.Fatalf("Error constructing ice.Agent") } local, err := NewCandidateHost("udp", net.ParseIP("192.168.0.2"), 777, 1) local.conn = &mockPacketConn{} if err != nil { t.Fatalf("failed to create a new candidate: %v", err) } remote := &net.UDPAddr{IP: net.ParseIP("172.17.0.3"), Port: 999} tID := [stun.TransactionIDSize]byte{} copy(tID[:], []byte("ABC")) msg, err := stun.Build(stun.BindingSuccess, stun.NewTransactionIDSetter(tID), stun.NewShortTermIntegrity(a.remotePwd), stun.Fingerprint, ) if err != nil { t.Fatal(err) } a.handleInbound(msg, local, remote) if len(a.remoteCandidates) != 0 { t.Fatal("unknown remote was able to create a candidate") } }) } func TestInvalidAgentStarts(t *testing.T) { a, err := NewAgent(&AgentConfig{}) if err != nil { t.Fatal(err) } ctx := context.Background() ctx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) defer cancel() if _, err = a.Dial(ctx, "", "bar"); err != nil && err != ErrRemoteUfragEmpty { t.Fatal(err) } if _, err = a.Dial(ctx, "foo", ""); err != nil && err != ErrRemotePwdEmpty { t.Fatal(err) } if _, err = a.Dial(ctx, "foo", "bar"); err != nil && err != ErrCanceledByCaller { t.Fatal(err) } if _, err = a.Dial(context.TODO(), "foo", "bar"); err != nil && err != ErrMultipleStart { t.Fatal(err) } } // Assert that Agent emits Connecting/Connected/Disconnected/Closed messages func TestConnectionStateCallback(t *testing.T) { lim := test.TimeOut(time.Second * 5) defer lim.Stop() var wg sync.WaitGroup wg.Add(2) timeoutDuration := time.Second KeepaliveInterval := time.Duration(0) cfg := &AgentConfig{ Urls: []*URL{}, Trickle: true, NetworkTypes: supportedNetworkTypes, ConnectionTimeout: &timeoutDuration, KeepaliveInterval: &KeepaliveInterval, taskLoopInterval: 500 * time.Millisecond, } aAgent, err := NewAgent(cfg) if err != nil { t.Error(err) } err = aAgent.OnCandidate(func(candidate Candidate) { if candidate == nil { wg.Done() } }) if err != nil { panic(err) } err = aAgent.GatherCandidates() if err != nil { panic(err) } bAgent, err := NewAgent(cfg) if err != nil { t.Error(err) } err = bAgent.OnCandidate(func(candidate Candidate) { if candidate == nil { wg.Done() } }) if err != nil { panic(err) } err = bAgent.GatherCandidates() if err != nil { panic(err) } isChecking := make(chan interface{}) isConnected := make(chan interface{}) isDisconnected := make(chan interface{}) isClosed := make(chan interface{}) err = aAgent.OnConnectionStateChange(func(c ConnectionState) { switch c { case ConnectionStateChecking: close(isChecking) case ConnectionStateConnected: close(isConnected) case ConnectionStateDisconnected: close(isDisconnected) case ConnectionStateClosed: close(isClosed) } }) if err != nil { t.Error(err) } wg.Wait() connect(aAgent, bAgent) <-isChecking <-isConnected <-isDisconnected if err = aAgent.Close(); err != nil { t.Error(err) } if err = bAgent.Close(); err != nil { t.Error(err) } <-isClosed } func TestInvalidGather(t *testing.T) { t.Run("Gather with Trickle enable and no OnCandidate should error", func(t *testing.T) { a, err := NewAgent(&AgentConfig{Trickle: true}) if err != nil { t.Fatalf("Error constructing ice.Agent") } err = a.GatherCandidates() if err != ErrNoOnCandidateHandler { t.Fatal("trickle GatherCandidates succeeded without OnCandidate") } }) }