mirror of
https://github.com/pion/ice.git
synced 2025-09-26 19:41:11 +08:00
907 lines
25 KiB
Go
907 lines
25 KiB
Go
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
|
|
// 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)
|
|
})
|
|
}
|