From 71de7d2b18a3a43e17a7d6878a2b01e1566e5667 Mon Sep 17 00:00:00 2001 From: Kevin Wang Date: Mon, 25 Aug 2025 11:08:31 -0400 Subject: [PATCH] Add more configuration to AgentOptions --- agent_options.go | 85 +++++++++++++++++++ agent_options_test.go | 188 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 273 insertions(+) diff --git a/agent_options.go b/agent_options.go index 5a75e73..3a7534f 100644 --- a/agent_options.go +++ b/agent_options.go @@ -67,3 +67,88 @@ func WithNominationAttribute(attrType uint16) AgentOption { return nil } } + +// WithIncludeLoopback includes loopback addresses in the candidate list. +// By default, loopback addresses are excluded. +// +// Example: +// +// agent, err := NewAgentWithOptions(WithIncludeLoopback()) +func WithIncludeLoopback() AgentOption { + return func(a *Agent) error { + a.includeLoopback = true + + return nil + } +} + +// WithTCPPriorityOffset sets a number which is subtracted from the default (UDP) candidate type preference +// for host, srflx and prfx candidate types. It helps to configure relative preference of UDP candidates +// against TCP ones. Relay candidates for TCP and UDP are always 0 and not affected by this setting. +// When not set, defaultTCPPriorityOffset (27) is used. +// +// Example: +// +// agent, err := NewAgentWithOptions(WithTCPPriorityOffset(50)) +func WithTCPPriorityOffset(offset uint16) AgentOption { + return func(a *Agent) error { + a.tcpPriorityOffset = offset + + return nil + } +} + +// WithDisableActiveTCP disables Active TCP candidates. +// When TCP is enabled, Active TCP candidates will be created when a new passive TCP remote candidate is added +// unless this option is used. +// +// Example: +// +// agent, err := NewAgentWithOptions(WithDisableActiveTCP()) +func WithDisableActiveTCP() AgentOption { + return func(a *Agent) error { + a.disableActiveTCP = true + + return nil + } +} + +// WithBindingRequestHandler sets a handler to allow applications to perform logic on incoming STUN Binding Requests. +// This was implemented to allow users to: +// - Log incoming Binding Requests for debugging +// - Implement draft-thatcher-ice-renomination +// - Implement custom CandidatePair switching logic +// +// Example: +// +// handler := func(m *stun.Message, local, remote Candidate, pair *CandidatePair) bool { +// log.Printf("Binding request from %s to %s", remote.Address(), local.Address()) +// return true // Accept the request +// } +// agent, err := NewAgentWithOptions(WithBindingRequestHandler(handler)) +func WithBindingRequestHandler( + handler func(m *stun.Message, local, remote Candidate, pair *CandidatePair) bool, +) AgentOption { + return func(a *Agent) error { + a.userBindingRequestHandler = handler + + return nil + } +} + +// WithEnableUseCandidateCheckPriority enables checking for equal or higher priority when +// switching selected candidate pair if the peer requests USE-CANDIDATE and agent is a lite agent. +// This is disabled by default, i.e. when peer requests USE-CANDIDATE, the selected pair will be +// switched to that irrespective of relative priority between current selected pair +// and priority of the pair being switched to. +// +// Example: +// +// agent, err := NewAgentWithOptions(WithEnableUseCandidateCheckPriority()) +func WithEnableUseCandidateCheckPriority() AgentOption { + return func(a *Agent) error { + a.enableUseCandidateCheckPriority = true + + return nil + } +} diff --git a/agent_options_test.go b/agent_options_test.go index bdf3257..38ad0e6 100644 --- a/agent_options_test.go +++ b/agent_options_test.go @@ -10,6 +10,46 @@ import ( "github.com/stretchr/testify/assert" ) +// testBooleanOption is a helper function to test boolean agent options. +type booleanOptionTest struct { + optionFunc func() AgentOption + getValue func(*Agent) bool + configSetter func(*AgentConfig, bool) +} + +func testBooleanOption(t *testing.T, test booleanOptionTest, optionName string) { + t.Helper() + + t.Run("enables "+optionName, func(t *testing.T) { + agent, err := NewAgentWithOptions(test.optionFunc()) + assert.NoError(t, err) + defer agent.Close() //nolint:errcheck + + assert.True(t, test.getValue(agent)) + }) + + t.Run("default is false", func(t *testing.T) { + agent, err := NewAgentWithOptions() + assert.NoError(t, err) + defer agent.Close() //nolint:errcheck + + assert.False(t, test.getValue(agent)) + }) + + t.Run("works with config", func(t *testing.T) { + config := &AgentConfig{ + NetworkTypes: []NetworkType{NetworkTypeUDP4}, + } + test.configSetter(config, true) + + agent, err := NewAgent(config) + assert.NoError(t, err) + defer agent.Close() //nolint:errcheck + + assert.True(t, test.getValue(agent)) + }) +} + func TestDefaultNominationValueGenerator(t *testing.T) { t.Run("generates incrementing values", func(t *testing.T) { generator := DefaultNominationValueGenerator() @@ -108,3 +148,151 @@ func TestWithNominationAttribute(t *testing.T) { assert.Equal(t, stun.AttrType(0x0030), agent.nominationAttribute) }) } + +func TestWithIncludeLoopback(t *testing.T) { + testBooleanOption(t, booleanOptionTest{ + optionFunc: WithIncludeLoopback, + getValue: func(a *Agent) bool { return a.includeLoopback }, + configSetter: func(c *AgentConfig, v bool) { c.IncludeLoopback = v }, + }, "loopback addresses") +} + +func TestWithTCPPriorityOffset(t *testing.T) { + t.Run("sets custom TCP priority offset", func(t *testing.T) { + customOffset := uint16(50) + agent, err := NewAgentWithOptions(WithTCPPriorityOffset(customOffset)) + assert.NoError(t, err) + defer agent.Close() //nolint:errcheck + + assert.Equal(t, customOffset, agent.tcpPriorityOffset) + }) + + t.Run("default is 27", func(t *testing.T) { + agent, err := NewAgentWithOptions() + assert.NoError(t, err) + defer agent.Close() //nolint:errcheck + + // The default is set via initWithDefaults + assert.Equal(t, uint16(27), agent.tcpPriorityOffset) + }) + + t.Run("works with config", func(t *testing.T) { + customOffset := uint16(100) + config := &AgentConfig{ + NetworkTypes: []NetworkType{NetworkTypeUDP4}, + TCPPriorityOffset: &customOffset, + } + + agent, err := NewAgent(config) + assert.NoError(t, err) + defer agent.Close() //nolint:errcheck + + assert.Equal(t, customOffset, agent.tcpPriorityOffset) + }) +} + +func TestWithDisableActiveTCP(t *testing.T) { + testBooleanOption(t, booleanOptionTest{ + optionFunc: WithDisableActiveTCP, + getValue: func(a *Agent) bool { return a.disableActiveTCP }, + configSetter: func(c *AgentConfig, v bool) { c.DisableActiveTCP = v }, + }, "active TCP disabling") +} + +func TestWithBindingRequestHandler(t *testing.T) { + t.Run("sets binding request handler", func(t *testing.T) { + handlerCalled := false + handler := func(m *stun.Message, local, remote Candidate, pair *CandidatePair) bool { + handlerCalled = true + + return true + } + + agent, err := NewAgentWithOptions(WithBindingRequestHandler(handler)) + assert.NoError(t, err) + defer agent.Close() //nolint:errcheck + + assert.NotNil(t, agent.userBindingRequestHandler) + + // Test that the handler is actually the one we set + // We can't directly compare functions, but we can call it + if agent.userBindingRequestHandler != nil { + agent.userBindingRequestHandler(nil, nil, nil, nil) + assert.True(t, handlerCalled) + } + }) + + t.Run("default is nil", func(t *testing.T) { + agent, err := NewAgentWithOptions() + assert.NoError(t, err) + defer agent.Close() //nolint:errcheck + + assert.Nil(t, agent.userBindingRequestHandler) + }) + + t.Run("works with config", func(t *testing.T) { + handlerCalled := false + handler := func(m *stun.Message, local, remote Candidate, pair *CandidatePair) bool { + handlerCalled = true + + return true + } + + config := &AgentConfig{ + NetworkTypes: []NetworkType{NetworkTypeUDP4}, + BindingRequestHandler: handler, + } + + agent, err := NewAgent(config) + assert.NoError(t, err) + defer agent.Close() //nolint:errcheck + + assert.NotNil(t, agent.userBindingRequestHandler) + + if agent.userBindingRequestHandler != nil { + agent.userBindingRequestHandler(nil, nil, nil, nil) + assert.True(t, handlerCalled) + } + }) +} + +func TestWithEnableUseCandidateCheckPriority(t *testing.T) { + testBooleanOption(t, booleanOptionTest{ + optionFunc: WithEnableUseCandidateCheckPriority, + getValue: func(a *Agent) bool { return a.enableUseCandidateCheckPriority }, + configSetter: func(c *AgentConfig, v bool) { c.EnableUseCandidateCheckPriority = v }, + }, "use candidate check priority") +} + +func TestMultipleConfigOptions(t *testing.T) { + t.Run("can apply multiple options", func(t *testing.T) { + customOffset := uint16(100) + handlerCalled := false + handler := func(m *stun.Message, local, remote Candidate, pair *CandidatePair) bool { + handlerCalled = true + + return true + } + + agent, err := NewAgentWithOptions( + WithIncludeLoopback(), + WithTCPPriorityOffset(customOffset), + WithDisableActiveTCP(), + WithBindingRequestHandler(handler), + WithEnableUseCandidateCheckPriority(), + ) + assert.NoError(t, err) + defer agent.Close() //nolint:errcheck + + assert.True(t, agent.includeLoopback) + assert.Equal(t, customOffset, agent.tcpPriorityOffset) + assert.True(t, agent.disableActiveTCP) + assert.NotNil(t, agent.userBindingRequestHandler) + assert.True(t, agent.enableUseCandidateCheckPriority) + + if agent.userBindingRequestHandler != nil { + agent.userBindingRequestHandler(nil, nil, nil, nil) + assert.True(t, handlerCalled) + } + }) +}