// SPDX-FileCopyrightText: 2025 The Pion community // SPDX-License-Identifier: MIT //go:build !js // +build !js package ice import ( "net" "testing" "time" "github.com/pion/ice/v4/internal/fakenet" "github.com/pion/stun/v3" "github.com/stretchr/testify/assert" ) const ( testLocalUfrag = "localufrag" testLocalPwd = "localpwd" testRemoteUfrag = "remoteufrag" testRemotePwd = "remotepwd" ) // Mock packet conn that captures sent packets. type mockPacketConnWithCapture struct { sentPackets [][]byte sentAddrs []net.Addr } func (m *mockPacketConnWithCapture) ReadFrom([]byte) (n int, addr net.Addr, err error) { return 0, nil, nil } func (m *mockPacketConnWithCapture) WriteTo(b []byte, addr net.Addr) (n int, err error) { // Capture the packet packet := make([]byte, len(b)) copy(packet, b) m.sentPackets = append(m.sentPackets, packet) m.sentAddrs = append(m.sentAddrs, addr) return len(b), nil } func (m *mockPacketConnWithCapture) Close() error { return nil } func (m *mockPacketConnWithCapture) LocalAddr() net.Addr { return nil } func (m *mockPacketConnWithCapture) SetDeadline(time.Time) error { return nil } func (m *mockPacketConnWithCapture) SetReadDeadline(time.Time) error { return nil } func (m *mockPacketConnWithCapture) SetWriteDeadline(time.Time) error { return nil } // createRenominationTestAgent creates a test agent with renomination enabled and returns local/remote candidates. func createRenominationTestAgent(t *testing.T, controlling bool) (*Agent, Candidate, Candidate) { t.Helper() agent, err := NewAgentWithOptions(WithRenomination(func() uint32 { return 1 })) assert.NoError(t, err) agent.isControlling.Store(controlling) local, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 12345, Component: 1, }) assert.NoError(t, err) remote, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 54321, Component: 1, }) assert.NoError(t, err) return agent, local, remote } func TestNominationAttribute(t *testing.T) { t.Run("AddTo and GetFrom", func(t *testing.T) { m := &stun.Message{} attr := NominationAttribute{Value: 0x123456} err := attr.AddTo(m) assert.NoError(t, err) var parsed NominationAttribute err = parsed.GetFrom(m) assert.NoError(t, err) assert.Equal(t, uint32(0x123456), parsed.Value) }) t.Run("24-bit value boundary", func(t *testing.T) { m := &stun.Message{} maxValue := uint32((1 << 24) - 1) // 24-bit max value attr := NominationAttribute{Value: maxValue} err := attr.AddTo(m) assert.NoError(t, err) var parsed NominationAttribute err = parsed.GetFrom(m) assert.NoError(t, err) assert.Equal(t, maxValue, parsed.Value) }) t.Run("String representation", func(t *testing.T) { attr := NominationAttribute{Value: 12345} str := attr.String() assert.Contains(t, str, "NOMINATION") assert.Contains(t, str, "12345") }) t.Run("Nomination helper function", func(t *testing.T) { attr := Nomination(42) assert.Equal(t, uint32(42), attr.Value) }) } func TestRenominationConfiguration(t *testing.T) { nominationCounter := uint32(0) agent, err := NewAgentWithOptions(WithRenomination(func() uint32 { nominationCounter++ return nominationCounter })) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() assert.True(t, agent.enableRenomination) assert.NotNil(t, agent.nominationValueGenerator) // Test nomination value generation value1 := agent.nominationValueGenerator() value2 := agent.nominationValueGenerator() assert.Equal(t, uint32(1), value1) assert.Equal(t, uint32(2), value2) } func TestControlledSelectorNominationAcceptance(t *testing.T) { agent, err := NewAgentWithOptions(WithRenomination(DefaultNominationValueGenerator())) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() selector := &controlledSelector{ agent: agent, log: agent.log, } selector.Start() // First nomination should be accepted nomination1 := uint32(5) assert.True(t, selector.shouldAcceptNomination(&nomination1)) // Higher nomination should be accepted nomination2 := uint32(10) assert.True(t, selector.shouldAcceptNomination(&nomination2)) // Lower nomination should be rejected nomination3 := uint32(7) assert.False(t, selector.shouldAcceptNomination(&nomination3)) // Equal nomination should be rejected nomination4 := uint32(10) assert.False(t, selector.shouldAcceptNomination(&nomination4)) // Nil nomination should be accepted (standard ICE) assert.True(t, selector.shouldAcceptNomination(nil)) } func TestControlledSelectorNominationDisabled(t *testing.T) { config := &AgentConfig{ // Renomination disabled by default } agent, err := NewAgent(config) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() selector := &controlledSelector{ agent: agent, log: agent.log, } selector.Start() // Standard ICE nomination (no value) should be accepted assert.True(t, selector.shouldAcceptNomination(nil)) // When controlling side uses renomination (sends nomination values), // controlled side should apply "last nomination wins" regardless of local config nomination1 := uint32(5) assert.True(t, selector.shouldAcceptNomination(&nomination1)) nomination2 := uint32(3) // Lower value should be rejected assert.False(t, selector.shouldAcceptNomination(&nomination2)) nomination3 := uint32(8) // Higher value should be accepted assert.True(t, selector.shouldAcceptNomination(&nomination3)) } func TestAgentRenominateCandidate(t *testing.T) { t.Run("controlling agent can renominate", func(t *testing.T) { nominationCounter := uint32(0) agent, err := NewAgentWithOptions(WithRenomination(func() uint32 { nominationCounter++ return nominationCounter })) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() // Set up credentials for STUN authentication agent.localUfrag = testLocalUfrag agent.localPwd = testLocalPwd agent.remoteUfrag = testRemoteUfrag agent.remotePwd = testRemotePwd // Set agent as controlling agent.isControlling.Store(true) // Create test candidates with mock connection local, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 12345, Component: 1, }) assert.NoError(t, err) remote, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 54321, Component: 1, }) assert.NoError(t, err) // Mock the connection for the local candidate to avoid nil pointer mockConn := &fakenet.MockPacketConn{} local.conn = mockConn // Add pair to agent pair := agent.addPair(local, remote) pair.state = CandidatePairStateSucceeded // Test renomination err = agent.RenominateCandidate(local, remote) assert.NoError(t, err) }) t.Run("non-controlling agent cannot renominate", func(t *testing.T) { agent, local, remote := createRenominationTestAgent(t, false) defer func() { assert.NoError(t, agent.Close()) }() err := agent.RenominateCandidate(local, remote) assert.Error(t, err) assert.Contains(t, err.Error(), "only controlling agent can renominate") }) t.Run("renomination when disabled", func(t *testing.T) { config := &AgentConfig{ // Renomination disabled by default } agent, err := NewAgent(config) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() agent.isControlling.Store(true) local, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 12345, Component: 1, }) assert.NoError(t, err) remote, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 54321, Component: 1, }) assert.NoError(t, err) err = agent.RenominateCandidate(local, remote) assert.Error(t, err) assert.Contains(t, err.Error(), "renomination is not enabled") }) t.Run("renomination with non-existent candidate pair", func(t *testing.T) { agent, local, remote := createRenominationTestAgent(t, true) defer func() { assert.NoError(t, agent.Close()) }() // Don't add pair to agent - should fail err := agent.RenominateCandidate(local, remote) assert.Error(t, err) assert.Contains(t, err.Error(), "candidate pair not found") }) } func TestSendNominationRequest(t *testing.T) { t.Run("STUN message contains nomination attribute", func(t *testing.T) { nominationCounter := uint32(0) agent, err := NewAgentWithOptions(WithRenomination(func() uint32 { nominationCounter++ return nominationCounter })) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() // Set up credentials for STUN authentication agent.localUfrag = testLocalUfrag agent.localPwd = testLocalPwd agent.remoteUfrag = testRemoteUfrag agent.remotePwd = testRemotePwd agent.isControlling.Store(true) // Create test candidates local, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 12345, Component: 1, }) assert.NoError(t, err) remote, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 54321, Component: 1, }) assert.NoError(t, err) // Mock connection to capture sent messages mockConn := &mockPacketConnWithCapture{} local.conn = mockConn pair := agent.addPair(local, remote) pair.state = CandidatePairStateSucceeded // Test sendNominationRequest directly nominationValue := uint32(123) err = agent.sendNominationRequest(pair, nominationValue) assert.NoError(t, err) // Verify message was sent assert.True(t, len(mockConn.sentPackets) > 0) // Parse the sent STUN message msg := &stun.Message{} err = msg.UnmarshalBinary(mockConn.sentPackets[0]) assert.NoError(t, err) // Verify it's a binding request assert.True(t, msg.Type.Method == stun.MethodBinding) assert.True(t, msg.Type.Class == stun.ClassRequest) // Verify USE-CANDIDATE is present assert.True(t, msg.Contains(stun.AttrUseCandidate)) // Verify nomination attribute is present var nomination NominationAttribute err = nomination.GetFrom(msg) assert.NoError(t, err) assert.Equal(t, nominationValue, nomination.Value) }) t.Run("STUN message without nomination when disabled", func(t *testing.T) { config := &AgentConfig{ // Renomination disabled by default } agent, err := NewAgent(config) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() // Set up credentials agent.localUfrag = testLocalUfrag agent.localPwd = testLocalPwd agent.remoteUfrag = testRemoteUfrag agent.remotePwd = testRemotePwd agent.isControlling.Store(true) local, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 12345, Component: 1, }) assert.NoError(t, err) remote, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 54321, Component: 1, }) assert.NoError(t, err) mockConn := &mockPacketConnWithCapture{} local.conn = mockConn pair := agent.addPair(local, remote) // Send nomination with value 0 (should not include nomination attribute) err = agent.sendNominationRequest(pair, 0) assert.NoError(t, err) // Parse the sent message msg := &stun.Message{} err = msg.UnmarshalBinary(mockConn.sentPackets[0]) assert.NoError(t, err) // Verify USE-CANDIDATE is present assert.True(t, msg.Contains(stun.AttrUseCandidate)) // Verify nomination attribute is NOT present var nomination NominationAttribute err = nomination.GetFrom(msg) assert.Error(t, err) // Should fail since attribute is not present }) } func TestRenominationErrorCases(t *testing.T) { t.Run("getNominationValue with nil generator", func(t *testing.T) { // Try to create agent with nil generator - should fail _, err := NewAgentWithOptions(WithRenomination(nil)) assert.ErrorIs(t, err, ErrInvalidNominationValueGenerator) // Create agent without renomination for testing agent, err := NewAgentWithOptions() assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() // Should return 0 when no generator is set value := agent.getNominationValue() assert.Equal(t, uint32(0), value) }) t.Run("STUN message build with invalid attributes", func(t *testing.T) { agent, err := NewAgentWithOptions(WithRenomination(func() uint32 { return 1 })) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() // Set up minimal credentials but missing remote password agent.localUfrag = "localufrag" agent.localPwd = "localpwd" agent.remoteUfrag = "remoteufrag" // agent.remotePwd = "" // Missing remote password agent.isControlling.Store(true) local, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 12345, Component: 1, }) assert.NoError(t, err) remote, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 54321, Component: 1, }) assert.NoError(t, err) mockConn := &mockPacketConnWithCapture{} local.conn = mockConn pair := agent.addPair(local, remote) // This should succeed even with missing remote password // as the STUN library will still build the message err = agent.sendNominationRequest(pair, 1) assert.NoError(t, err) }) } func TestNominationValueBoundaries(t *testing.T) { t.Run("24-bit maximum value", func(t *testing.T) { maxValue := uint32((1 << 24) - 1) // 0xFFFFFF attr := NominationAttribute{Value: maxValue} m := &stun.Message{} err := attr.AddTo(m) assert.NoError(t, err) var parsed NominationAttribute err = parsed.GetFrom(m) assert.NoError(t, err) assert.Equal(t, maxValue, parsed.Value) }) t.Run("zero nomination value", func(t *testing.T) { attr := NominationAttribute{Value: 0} m := &stun.Message{} err := attr.AddTo(m) assert.NoError(t, err) var parsed NominationAttribute err = parsed.GetFrom(m) assert.NoError(t, err) assert.Equal(t, uint32(0), parsed.Value) }) t.Run("nomination value overflow", func(t *testing.T) { // Test value larger than 24-bit overflowValue := uint32(1 << 25) // Larger than 24-bit max attr := NominationAttribute{Value: overflowValue} m := &stun.Message{} err := attr.AddTo(m) assert.NoError(t, err) var parsed NominationAttribute err = parsed.GetFrom(m) assert.NoError(t, err) // Should be truncated to 24-bit value expectedValue := overflowValue & 0xFFFFFF assert.Equal(t, expectedValue, parsed.Value) }) t.Run("invalid attribute size", func(t *testing.T) { m := &stun.Message{} // Add a nomination attribute with invalid size (too short) m.Add(DefaultNominationAttribute, []byte{0x01, 0x02}) // Only 2 bytes instead of 4 var nomination NominationAttribute err := nomination.GetFrom(m) assert.Error(t, err) assert.Equal(t, stun.ErrAttributeSizeInvalid, err) }) t.Run("configurable nomination attribute type", func(t *testing.T) { // Test with custom attribute type customAttrType := stun.AttrType(0x0040) attr := NominationAttribute{Value: 12345} m := &stun.Message{} err := attr.AddToWithType(m, customAttrType) assert.NoError(t, err) // Try to read with default type - should fail var parsed1 NominationAttribute err = parsed1.GetFrom(m) assert.Error(t, err) // Read with custom type - should succeed var parsed2 NominationAttribute err = parsed2.GetFromWithType(m, customAttrType) assert.NoError(t, err) assert.Equal(t, uint32(12345), parsed2.Value) }) t.Run("NominationSetter with custom attribute type", func(t *testing.T) { customAttrType := stun.AttrType(0x0050) setter := NominationSetter{ Value: 98765, AttrType: customAttrType, } m := &stun.Message{} err := setter.AddTo(m) assert.NoError(t, err) // Verify the attribute was added with custom type var parsed NominationAttribute err = parsed.GetFromWithType(m, customAttrType) assert.NoError(t, err) assert.Equal(t, uint32(98765), parsed.Value) // Verify it wasn't added with default type var parsedDefault NominationAttribute err = parsedDefault.GetFrom(m) assert.Error(t, err) }) } func TestControlledSelectorWithActualSTUNMessages(t *testing.T) { t.Run("HandleBindingRequest with nomination attribute", func(t *testing.T) { agent, err := NewAgentWithOptions(WithRenomination(DefaultNominationValueGenerator())) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() // Set up credentials for STUN agent.localUfrag = testLocalUfrag agent.localPwd = testLocalPwd agent.remoteUfrag = testRemoteUfrag agent.remotePwd = testRemotePwd selector := &controlledSelector{ agent: agent, log: agent.log, } selector.Start() // Create test candidates local, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 12345, Component: 1, }) assert.NoError(t, err) remote, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 54321, Component: 1, }) assert.NoError(t, err) // Mock connection for response mockConn := &mockPacketConnWithCapture{} local.conn = mockConn // Create STUN binding request with nomination and USE-CANDIDATE msg, err := stun.Build( stun.BindingRequest, stun.TransactionID, stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag), UseCandidate(), Nomination(5), // First nomination value stun.NewShortTermIntegrity(agent.localPwd), stun.Fingerprint, ) assert.NoError(t, err) // Handle the binding request selector.HandleBindingRequest(msg, local, remote) // Verify selector accepted the nomination assert.NotNil(t, selector.lastNomination) assert.Equal(t, uint32(5), *selector.lastNomination) // Create another STUN request with higher nomination value msg2, err := stun.Build( stun.BindingRequest, stun.TransactionID, stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag), UseCandidate(), Nomination(10), // Higher nomination value stun.NewShortTermIntegrity(agent.localPwd), stun.Fingerprint, ) assert.NoError(t, err) // Handle the second binding request selector.HandleBindingRequest(msg2, local, remote) // Should accept higher nomination assert.Equal(t, uint32(10), *selector.lastNomination) // Create another STUN request with lower nomination value msg3, err := stun.Build( stun.BindingRequest, stun.TransactionID, stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag), UseCandidate(), Nomination(7), // Lower nomination value stun.NewShortTermIntegrity(agent.localPwd), stun.Fingerprint, ) assert.NoError(t, err) // Handle the third binding request selector.HandleBindingRequest(msg3, local, remote) // Should reject lower nomination (lastNomination should remain 10) assert.Equal(t, uint32(10), *selector.lastNomination) }) t.Run("HandleBindingRequest without nomination attribute", func(t *testing.T) { agent, err := NewAgentWithOptions(WithRenomination(DefaultNominationValueGenerator())) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() agent.localUfrag = testLocalUfrag agent.localPwd = testLocalPwd agent.remoteUfrag = testRemoteUfrag agent.remotePwd = testRemotePwd selector := &controlledSelector{ agent: agent, log: agent.log, } selector.Start() local, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 12345, Component: 1, }) assert.NoError(t, err) remote, err := NewCandidateHost(&CandidateHostConfig{ Network: "udp", Address: "127.0.0.1", Port: 54321, Component: 1, }) assert.NoError(t, err) mockConn := &mockPacketConnWithCapture{} local.conn = mockConn // Create STUN binding request without nomination (standard ICE) msg, err := stun.Build( stun.BindingRequest, stun.TransactionID, stun.NewUsername(agent.localUfrag+":"+agent.remoteUfrag), UseCandidate(), // No nomination attribute stun.NewShortTermIntegrity(agent.localPwd), stun.Fingerprint, ) assert.NoError(t, err) // Handle the binding request selector.HandleBindingRequest(msg, local, remote) // Without nomination attribute, lastNomination should remain nil assert.Nil(t, selector.lastNomination) }) } func TestInvalidRenominationConfig(t *testing.T) { t.Run("nil nomination generator with renomination enabled", func(t *testing.T) { config := &AgentConfig{} // Without renomination, agent should work fine agent, err := NewAgent(config) assert.NoError(t, err) defer func() { assert.NoError(t, agent.Close()) }() // Agent should be created successfully without renomination assert.False(t, agent.enableRenomination) assert.Nil(t, agent.nominationValueGenerator) // getNominationValue should return 0 value := agent.getNominationValue() assert.Equal(t, uint32(0), value) }) t.Run("different generator behaviors", func(t *testing.T) { // Test constant generator agent1, err := NewAgentWithOptions(WithRenomination(func() uint32 { return 42 })) assert.NoError(t, err) defer func() { assert.NoError(t, agent1.Close()) }() value1 := agent1.getNominationValue() value2 := agent1.getNominationValue() assert.Equal(t, uint32(42), value1) assert.Equal(t, uint32(42), value2) // Test incrementing generator counter := uint32(0) agent2, err := NewAgentWithOptions(WithRenomination(func() uint32 { counter++ return counter })) assert.NoError(t, err) defer func() { assert.NoError(t, agent2.Close()) }() value3 := agent2.getNominationValue() value4 := agent2.getNominationValue() assert.Equal(t, uint32(1), value3) assert.Equal(t, uint32(2), value4) }) t.Run("controlled agent handles renomination regardless of local config", func(t *testing.T) { // Create controlled agent with renomination DISABLED controlledAgent, err := NewAgent(&AgentConfig{ // Renomination disabled by default // Disabled locally }) assert.NoError(t, err) defer func() { assert.NoError(t, controlledAgent.Close()) }() // Set up as controlled (non-controlling) controlledAgent.isControlling.Store(false) // Create controlled selector to test nomination handling selector := &controlledSelector{ agent: controlledAgent, log: controlledAgent.log, } // Test 1: Should accept nomination without value (standard ICE) assert.True(t, selector.shouldAcceptNomination(nil)) // Test 2: Should accept first nomination with value (renomination from controlling side) value1 := uint32(1) assert.True(t, selector.shouldAcceptNomination(&value1)) assert.Equal(t, &value1, selector.lastNomination) // Test 3: Should accept higher nomination value value2 := uint32(2) assert.True(t, selector.shouldAcceptNomination(&value2)) assert.Equal(t, &value2, selector.lastNomination) // Test 4: Should reject lower nomination value value0 := uint32(0) assert.False(t, selector.shouldAcceptNomination(&value0)) assert.Equal(t, &value2, selector.lastNomination) // Should remain unchanged }) } func TestAgentWithCustomNominationAttribute(t *testing.T) { t.Run("agent uses custom nomination attribute with option", func(t *testing.T) { customAttr := uint16(0x0042) // Create agent with custom nomination attribute using option agent, err := NewAgentWithOptions( WithRenomination(func() uint32 { return 100 }), WithNominationAttribute(customAttr), ) assert.NoError(t, err) defer agent.Close() //nolint:errcheck // Verify the agent has the custom attribute configured assert.Equal(t, stun.AttrType(customAttr), agent.nominationAttribute) }) t.Run("agent uses default nomination attribute when not configured", func(t *testing.T) { // Create agent without custom nomination attribute agentConfig := &AgentConfig{ NetworkTypes: []NetworkType{NetworkTypeUDP4}, } agent, err := NewAgent(agentConfig) assert.NoError(t, err) defer agent.Close() //nolint:errcheck // Verify the agent has the default attribute assert.Equal(t, stun.AttrType(0x0030), agent.nominationAttribute) }) t.Run("multiple options can be applied", func(t *testing.T) { customAttr := uint16(0x0055) // Test that multiple options can be applied agent, err := NewAgentWithOptions( WithRenomination(func() uint32 { return 200 }), WithNominationAttribute(customAttr), ) assert.NoError(t, err) defer agent.Close() //nolint:errcheck assert.Equal(t, stun.AttrType(customAttr), agent.nominationAttribute) }) t.Run("WithNominationAttribute returns error for invalid value", func(t *testing.T) { // Test that 0x0000 is rejected as invalid _, err := NewAgentWithOptions(WithNominationAttribute(0x0000)) assert.ErrorIs(t, err, ErrInvalidNominationAttribute) }) }