mirror of
https://github.com/pion/ice.git
synced 2025-12-24 13:38:03 +08:00
1680 lines
45 KiB
Go
1680 lines
45 KiB
Go
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
package ice
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/pion/logging"
|
|
"github.com/pion/stun/v3"
|
|
"github.com/pion/transport/v3"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/net/proxy"
|
|
)
|
|
|
|
// 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()
|
|
|
|
// Should generate incrementing values starting from 1
|
|
assert.Equal(t, uint32(1), generator())
|
|
assert.Equal(t, uint32(2), generator())
|
|
assert.Equal(t, uint32(3), generator())
|
|
})
|
|
|
|
t.Run("each generator has independent counter", func(t *testing.T) {
|
|
gen1 := DefaultNominationValueGenerator()
|
|
gen2 := DefaultNominationValueGenerator()
|
|
|
|
assert.Equal(t, uint32(1), gen1())
|
|
assert.Equal(t, uint32(1), gen2()) // Should also start at 1
|
|
assert.Equal(t, uint32(2), gen1())
|
|
assert.Equal(t, uint32(2), gen2())
|
|
})
|
|
}
|
|
|
|
func TestWithLite(t *testing.T) {
|
|
t.Run("enables lite with host candidates", func(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(
|
|
WithCandidateTypes([]CandidateType{CandidateTypeHost}),
|
|
WithICELite(true),
|
|
)
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.True(t, agent.lite)
|
|
})
|
|
|
|
t.Run("default is not lite", func(t *testing.T) {
|
|
agent, err := NewAgentWithOptions()
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.False(t, agent.lite)
|
|
})
|
|
|
|
t.Run("config sets lite", func(t *testing.T) {
|
|
config := &AgentConfig{
|
|
Lite: true,
|
|
CandidateTypes: []CandidateType{CandidateTypeHost},
|
|
NetworkTypes: []NetworkType{NetworkTypeUDP4},
|
|
}
|
|
|
|
agent, err := NewAgent(config)
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.True(t, agent.lite)
|
|
})
|
|
|
|
t.Run("errors when candidate types include non-host", func(t *testing.T) {
|
|
_, err := NewAgentWithOptions(WithICELite(true))
|
|
assert.ErrorIs(t, err, ErrLiteUsingNonHostCandidates)
|
|
})
|
|
}
|
|
|
|
func TestWithUrls(t *testing.T) {
|
|
stunURL, err := stun.ParseURI("stun:example.com:3478")
|
|
require.NoError(t, err)
|
|
|
|
input := []*stun.URI{stunURL}
|
|
agent, err := NewAgentWithOptions(WithUrls(input))
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
require.Len(t, agent.urls, 1)
|
|
assert.Equal(t, stunURL.String(), agent.urls[0].String())
|
|
|
|
input[0] = nil
|
|
require.Len(t, agent.urls, 1)
|
|
assert.NotNil(t, agent.urls[0])
|
|
}
|
|
|
|
func TestWithPortRange(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(WithPortRange(1000, 2000))
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, uint16(1000), agent.portMin)
|
|
assert.Equal(t, uint16(2000), agent.portMax)
|
|
|
|
agent.Close() //nolint:gosec,errcheck
|
|
agent, err = NewAgentWithOptions(WithPortRange(2000, 0))
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:gosec,errcheck
|
|
|
|
assert.Equal(t, uint16(2000), agent.portMin)
|
|
assert.Equal(t, uint16(0), agent.portMax)
|
|
}
|
|
|
|
func TestWithTimeoutOptions(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(
|
|
WithDisconnectedTimeout(10*time.Second),
|
|
WithFailedTimeout(20*time.Second),
|
|
WithKeepaliveInterval(3*time.Second),
|
|
WithCheckInterval(150*time.Millisecond),
|
|
)
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.Equal(t, 10*time.Second, agent.disconnectedTimeout)
|
|
assert.Equal(t, 20*time.Second, agent.failedTimeout)
|
|
assert.Equal(t, 3*time.Second, agent.keepaliveInterval)
|
|
assert.Equal(t, 150*time.Millisecond, agent.checkInterval)
|
|
}
|
|
|
|
func TestWithAcceptanceWaitOptions(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(
|
|
WithHostAcceptanceMinWait(1*time.Second),
|
|
WithSrflxAcceptanceMinWait(2*time.Second),
|
|
WithPrflxAcceptanceMinWait(3*time.Second),
|
|
WithRelayAcceptanceMinWait(4*time.Second),
|
|
)
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.Equal(t, 1*time.Second, agent.hostAcceptanceMinWait)
|
|
assert.Equal(t, 2*time.Second, agent.srflxAcceptanceMinWait)
|
|
assert.Equal(t, 3*time.Second, agent.prflxAcceptanceMinWait)
|
|
assert.Equal(t, 4*time.Second, agent.relayAcceptanceMinWait)
|
|
}
|
|
|
|
func TestWithSTUNGatherTimeout(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(WithSTUNGatherTimeout(7 * time.Second))
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.Equal(t, 7*time.Second, agent.stunGatherTimeout)
|
|
}
|
|
|
|
func TestWithIPFilterOption(t *testing.T) {
|
|
filter := func(ip net.IP) bool {
|
|
return ip.IsLoopback()
|
|
}
|
|
|
|
agent, err := NewAgentWithOptions(WithIPFilter(filter))
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
require.NotNil(t, agent.ipFilter)
|
|
assert.True(t, agent.ipFilter(net.IPv4(127, 0, 0, 1)))
|
|
assert.False(t, agent.ipFilter(net.IPv4(192, 0, 2, 1)))
|
|
}
|
|
|
|
func TestWithNetOption(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := NewAgentWithOptions(WithNet(stub))
|
|
require.NoError(t, err)
|
|
defer func() { require.NoError(t, agent.Close()) }()
|
|
|
|
assert.Equal(t, stub, agent.net)
|
|
}
|
|
|
|
func TestWithMulticastDNSOptions(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(
|
|
WithMulticastDNSMode(MulticastDNSModeDisabled),
|
|
WithMulticastDNSHostName("pion-test.local"),
|
|
)
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.Equal(t, MulticastDNSModeDisabled, agent.mDNSMode)
|
|
assert.Equal(t, "pion-test.local", agent.mDNSName)
|
|
|
|
_, err = NewAgentWithOptions(WithMulticastDNSHostName("invalid-host"))
|
|
assert.ErrorIs(t, err, ErrInvalidMulticastDNSHostName)
|
|
}
|
|
|
|
func TestWithLocalCredentials(t *testing.T) {
|
|
password := strings.Repeat("p", 16)
|
|
|
|
agent, err := NewAgentWithOptions(WithLocalCredentials("abcd", password))
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.Equal(t, "abcd", agent.localUfrag)
|
|
assert.Equal(t, password, agent.localPwd)
|
|
|
|
_, err = NewAgentWithOptions(WithLocalCredentials("ab", password))
|
|
assert.ErrorIs(t, err, ErrLocalUfragInsufficientBits)
|
|
|
|
shortPassword := strings.Repeat("p", 10)
|
|
_, err = NewAgentWithOptions(WithLocalCredentials("abcd", shortPassword))
|
|
assert.ErrorIs(t, err, ErrLocalPwdInsufficientBits)
|
|
}
|
|
|
|
func TestWithMuxOptions(t *testing.T) {
|
|
tcpMux := &stubTCPMux{}
|
|
udpMux := &stubUDPMux{}
|
|
udpMuxSrflx := &stubUniversalUDPMux{}
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithTCPMux(tcpMux),
|
|
WithUDPMux(udpMux),
|
|
WithUDPMuxSrflx(udpMuxSrflx),
|
|
)
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.Equal(t, tcpMux, agent.tcpMux)
|
|
assert.Equal(t, udpMux, agent.udpMux)
|
|
assert.Equal(t, udpMuxSrflx, agent.udpMuxSrflx)
|
|
}
|
|
|
|
func TestWithProxyDialer(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(WithProxyDialer(proxy.Direct))
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.Equal(t, proxy.Direct, agent.proxyDialer)
|
|
}
|
|
|
|
func TestWithMaxBindingRequests(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(WithMaxBindingRequests(3))
|
|
require.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.Equal(t, uint16(3), agent.maxBindingRequests)
|
|
}
|
|
|
|
func TestWithRenomination(t *testing.T) {
|
|
t.Run("enables renomination with custom generator", func(t *testing.T) {
|
|
counter := uint32(0)
|
|
customGen := func() uint32 {
|
|
counter++
|
|
|
|
return counter * 10
|
|
}
|
|
|
|
agent, err := NewAgentWithOptions(WithRenomination(customGen))
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.True(t, agent.enableRenomination)
|
|
assert.NotNil(t, agent.nominationValueGenerator)
|
|
assert.Equal(t, uint32(10), agent.getNominationValue())
|
|
assert.Equal(t, uint32(20), agent.getNominationValue())
|
|
})
|
|
|
|
t.Run("enables renomination with default generator", func(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(WithRenomination(DefaultNominationValueGenerator()))
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.True(t, agent.enableRenomination)
|
|
assert.NotNil(t, agent.nominationValueGenerator)
|
|
assert.Equal(t, uint32(1), agent.getNominationValue())
|
|
assert.Equal(t, uint32(2), agent.getNominationValue())
|
|
})
|
|
|
|
t.Run("rejects nil generator", func(t *testing.T) {
|
|
_, err := NewAgentWithOptions(WithRenomination(nil))
|
|
assert.ErrorIs(t, err, ErrInvalidNominationValueGenerator)
|
|
})
|
|
|
|
t.Run("default agent has renomination disabled", func(t *testing.T) {
|
|
config := &AgentConfig{
|
|
NetworkTypes: []NetworkType{NetworkTypeUDP4},
|
|
}
|
|
|
|
agent, err := NewAgent(config)
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.False(t, agent.enableRenomination)
|
|
assert.Nil(t, agent.nominationValueGenerator)
|
|
assert.Equal(t, uint32(0), agent.getNominationValue())
|
|
})
|
|
}
|
|
|
|
func TestWithNominationAttribute(t *testing.T) {
|
|
t.Run("sets custom nomination attribute", func(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(WithNominationAttribute(0x0045))
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.Equal(t, stun.AttrType(0x0045), agent.nominationAttribute)
|
|
})
|
|
|
|
t.Run("rejects invalid attribute 0x0000", func(t *testing.T) {
|
|
_, err := NewAgentWithOptions(WithNominationAttribute(0x0000))
|
|
assert.ErrorIs(t, err, ErrInvalidNominationAttribute)
|
|
})
|
|
|
|
t.Run("default value when no option", func(t *testing.T) {
|
|
config := &AgentConfig{
|
|
NetworkTypes: []NetworkType{NetworkTypeUDP4},
|
|
}
|
|
|
|
agent, err := NewAgent(config)
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
// Should use default value 0x0030
|
|
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
|
|
|
|
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(_ *stun.Message, _, _ Candidate, _ *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(_ *stun.Message, _, _ Candidate, _ *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(_ *stun.Message, _, _ Candidate, _ *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)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWithInterfaceFilter(t *testing.T) {
|
|
t.Run("sets interface filter", func(t *testing.T) {
|
|
filter := func(interfaceName string) bool {
|
|
return interfaceName == "eth0"
|
|
}
|
|
|
|
agent, err := NewAgentWithOptions(WithInterfaceFilter(filter))
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.NotNil(t, agent.interfaceFilter)
|
|
assert.True(t, agent.interfaceFilter("eth0"))
|
|
assert.False(t, agent.interfaceFilter("wlan0"))
|
|
})
|
|
|
|
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.interfaceFilter)
|
|
})
|
|
|
|
t.Run("works with config", func(t *testing.T) {
|
|
filter := func(interfaceName string) bool {
|
|
return interfaceName == "lo"
|
|
}
|
|
|
|
config := &AgentConfig{
|
|
NetworkTypes: []NetworkType{NetworkTypeUDP4},
|
|
InterfaceFilter: filter,
|
|
}
|
|
|
|
agent, err := NewAgent(config)
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.NotNil(t, agent.interfaceFilter)
|
|
assert.True(t, agent.interfaceFilter("lo"))
|
|
assert.False(t, agent.interfaceFilter("eth0"))
|
|
})
|
|
}
|
|
|
|
func TestWithLoggerFactory(t *testing.T) {
|
|
t.Run("sets logger factory", func(t *testing.T) {
|
|
loggerFactory := logging.NewDefaultLoggerFactory()
|
|
loggerFactory.DefaultLogLevel = logging.LogLevelDebug
|
|
|
|
agent, err := NewAgentWithOptions(WithLoggerFactory(loggerFactory))
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.NotNil(t, agent.log)
|
|
})
|
|
|
|
t.Run("default uses default logger", func(t *testing.T) {
|
|
agent, err := NewAgentWithOptions()
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.NotNil(t, agent.log)
|
|
})
|
|
|
|
t.Run("works with config", func(t *testing.T) {
|
|
loggerFactory := logging.NewDefaultLoggerFactory()
|
|
config := &AgentConfig{
|
|
NetworkTypes: []NetworkType{NetworkTypeUDP4},
|
|
LoggerFactory: loggerFactory,
|
|
}
|
|
|
|
agent, err := NewAgent(config)
|
|
assert.NoError(t, err)
|
|
defer agent.Close() //nolint:errcheck
|
|
|
|
assert.NotNil(t, agent.log)
|
|
})
|
|
}
|
|
|
|
func TestWithNetworkTypesAppliedBeforeRestart(t *testing.T) {
|
|
t.Run("ipv6 listen skipped when network types option restricts to ipv4", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := newAgentFromConfig(&AgentConfig{
|
|
Net: stub,
|
|
}, WithNetworkTypes([]NetworkType{NetworkTypeUDP4}))
|
|
require.NoError(t, err)
|
|
defer func() { require.NoError(t, agent.Close()) }()
|
|
|
|
assert.Zero(t, stub.udp6ListenCount, "unexpected ipv6 listen before restart")
|
|
})
|
|
}
|
|
|
|
func TestWithCandidateTypesAffectsURLValidation(t *testing.T) {
|
|
stunURL, err := stun.ParseURI("stun:example.com:3478")
|
|
require.NoError(t, err)
|
|
|
|
t.Run("default candidate types accept urls", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := newAgentFromConfig(&AgentConfig{
|
|
Urls: []*stun.URI{stunURL},
|
|
Net: stub,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
t.Run("host only candidate types reject urls", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
_, err := newAgentFromConfig(&AgentConfig{
|
|
Urls: []*stun.URI{stunURL},
|
|
Net: stub,
|
|
}, WithCandidateTypes([]CandidateType{CandidateTypeHost}))
|
|
require.ErrorIs(t, err, ErrUselessUrlsProvided)
|
|
})
|
|
}
|
|
|
|
func TestWithCandidateTypesNAT1To1Validation(t *testing.T) {
|
|
t.Run("host mapping requires host candidates", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
_, err := newAgentFromConfig(&AgentConfig{
|
|
NAT1To1IPs: []string{"1.2.3.4"},
|
|
NAT1To1IPCandidateType: CandidateTypeHost,
|
|
Net: stub,
|
|
}, WithCandidateTypes([]CandidateType{CandidateTypeRelay}))
|
|
require.ErrorIs(t, err, ErrIneffectiveNAT1To1IPMappingHost)
|
|
})
|
|
|
|
t.Run("srflx mapping requires srflx candidates", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
_, err := newAgentFromConfig(&AgentConfig{
|
|
NAT1To1IPs: []string{"1.2.3.4"},
|
|
NAT1To1IPCandidateType: CandidateTypeServerReflexive,
|
|
Net: stub,
|
|
}, WithCandidateTypes([]CandidateType{CandidateTypeHost}))
|
|
require.ErrorIs(t, err, ErrIneffectiveNAT1To1IPMappingSrflx)
|
|
})
|
|
}
|
|
|
|
func TestWith1To1CandidateIPOptions(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
rules []AddressRewriteRule
|
|
candidateType CandidateType
|
|
expectedFirstIP string
|
|
expectedSecondIP string
|
|
lookupLocalIP string
|
|
}{
|
|
{
|
|
name: "host candidates",
|
|
rules: []AddressRewriteRule{
|
|
{
|
|
External: []string{"1.2.3.4"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
},
|
|
{
|
|
External: []string{"5.6.7.8"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
},
|
|
},
|
|
candidateType: CandidateTypeHost,
|
|
expectedFirstIP: "1.2.3.4",
|
|
expectedSecondIP: "5.6.7.8",
|
|
lookupLocalIP: "10.0.0.1",
|
|
},
|
|
{
|
|
name: "srflx candidates",
|
|
rules: []AddressRewriteRule{
|
|
{
|
|
External: []string{"5.6.7.8"},
|
|
AsCandidateType: CandidateTypeServerReflexive,
|
|
},
|
|
{
|
|
External: []string{"9.9.9.9"},
|
|
AsCandidateType: CandidateTypeServerReflexive,
|
|
},
|
|
},
|
|
candidateType: CandidateTypeServerReflexive,
|
|
expectedFirstIP: "5.6.7.8",
|
|
expectedSecondIP: "9.9.9.9",
|
|
lookupLocalIP: "0.0.0.0",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assertAddressRewriteOption(
|
|
t,
|
|
tc.rules,
|
|
tc.candidateType,
|
|
tc.expectedFirstIP,
|
|
tc.expectedSecondIP,
|
|
tc.lookupLocalIP,
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
func assertAddressRewriteOption(
|
|
t *testing.T,
|
|
rules []AddressRewriteRule,
|
|
candidateType CandidateType,
|
|
expectedFirstIP string,
|
|
expectedSecondIP string,
|
|
lookupLocalIP string,
|
|
) {
|
|
t.Helper()
|
|
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(rules...),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, agent.addressRewriteRules, len(rules))
|
|
|
|
firstRule := agent.addressRewriteRules[0]
|
|
require.Equal(t, candidateType, firstRule.AsCandidateType)
|
|
require.Equal(t, []string{expectedFirstIP}, firstRule.External)
|
|
|
|
secondRule := agent.addressRewriteRules[1]
|
|
require.Equal(t, candidateType, secondRule.AsCandidateType)
|
|
require.Equal(t, []string{expectedSecondIP}, secondRule.External)
|
|
|
|
require.NotNil(t, agent.addressRewriteMapper)
|
|
extIP := requireFirstExternalIP(t, agent.addressRewriteMapper, candidateType, lookupLocalIP)
|
|
require.Equal(t, expectedFirstIP, extIP.String())
|
|
}
|
|
|
|
func requireFirstExternalIP(
|
|
t *testing.T,
|
|
mapper *addressRewriteMapper,
|
|
candidateType CandidateType,
|
|
localIP string,
|
|
) net.IP {
|
|
t.Helper()
|
|
|
|
ips, matched, _, err := mapper.findExternalIPs(candidateType, localIP, "")
|
|
require.NoError(t, err)
|
|
require.True(t, matched)
|
|
require.NotEmpty(t, ips)
|
|
|
|
return ips[0]
|
|
}
|
|
|
|
func requireFirstMappingIP(t *testing.T, mapping *ipMapping, localIP net.IP) net.IP {
|
|
t.Helper()
|
|
|
|
ips := mapping.findExternalIPs(localIP)
|
|
require.NotEmpty(t, ips)
|
|
|
|
return ips[0]
|
|
}
|
|
|
|
func TestWith1To1RulesOption(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
originalRules := []AddressRewriteRule{
|
|
{
|
|
External: []string{"9.9.9.9"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
},
|
|
}
|
|
|
|
// With append semantics the option stacks, so call twice and ensure accumulation.
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(originalRules...),
|
|
WithAddressRewriteRules(AddressRewriteRule{
|
|
External: []string{"4.4.4.4"},
|
|
AsCandidateType: CandidateTypeServerReflexive,
|
|
}),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, agent.addressRewriteRules, 2)
|
|
require.Equal(t, []string{"9.9.9.9"}, agent.addressRewriteRules[0].External)
|
|
require.Equal(t, []string{"4.4.4.4"}, agent.addressRewriteRules[1].External)
|
|
|
|
// mutate the original rules after option applied, agent copy should remain unchanged.
|
|
originalRules[0].External[0] = "0.0.0.0"
|
|
require.Equal(t, "9.9.9.9", agent.addressRewriteRules[0].External[0])
|
|
}
|
|
|
|
func TestWith1To1RulesEmptyNoop(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(AddressRewriteRule{
|
|
External: []string{"1.2.3.4"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
}),
|
|
WithAddressRewriteRules(),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, agent.addressRewriteRules, 1)
|
|
require.Equal(t, []string{"1.2.3.4"}, agent.addressRewriteRules[0].External)
|
|
require.Equal(t, CandidateTypeHost, agent.addressRewriteRules[0].AsCandidateType)
|
|
require.NotNil(t, agent.addressRewriteMapper)
|
|
}
|
|
|
|
func TestWithAddressRewriteRulesWarnOnConflicts(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
logger := &recordingLogger{}
|
|
factory := &recordingLoggerFactory{logger: logger}
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithLoggerFactory(factory),
|
|
WithAddressRewriteRules(AddressRewriteRule{
|
|
External: []string{"203.0.113.10"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
}),
|
|
WithAddressRewriteRules(AddressRewriteRule{
|
|
External: []string{"198.51.100.50"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
}),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, logger.warnings, 1)
|
|
require.Contains(t, logger.warnings[0], "overlapping address rewrite rule")
|
|
require.Contains(t, logger.warnings[0], "candidate=host")
|
|
require.Contains(t, logger.warnings[0], "iface=*")
|
|
require.Contains(t, logger.warnings[0], "networks=*")
|
|
require.Contains(t, logger.warnings[0], "local=family:ipv4")
|
|
require.Contains(t, logger.warnings[0], "203.0.113.10")
|
|
require.Contains(t, logger.warnings[0], "198.51.100.50")
|
|
|
|
ips, matched, mode, err := agent.addressRewriteMapper.findExternalIPs(CandidateTypeHost, "10.0.0.1", "")
|
|
require.NoError(t, err)
|
|
require.True(t, matched)
|
|
require.Equal(t, AddressRewriteReplace, mode)
|
|
require.NotEmpty(t, ips)
|
|
require.Equal(t, "203.0.113.10", ips[0].String())
|
|
}
|
|
|
|
func TestWithAddressRewriteRulesConflictingModesWarningAndPrecedence(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
logger := &recordingLogger{}
|
|
factory := &recordingLoggerFactory{logger: logger}
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithLoggerFactory(factory),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"203.0.113.10"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
Mode: AddressRewriteReplace,
|
|
},
|
|
AddressRewriteRule{
|
|
External: []string{"198.51.100.50"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
Mode: AddressRewriteAppend,
|
|
},
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.NotEmpty(t, logger.warnings)
|
|
require.Contains(t, logger.warnings[0], "overlapping address rewrite rule")
|
|
|
|
ips, matched, mode, err := agent.addressRewriteMapper.findExternalIPs(CandidateTypeHost, "10.0.0.1", "")
|
|
require.NoError(t, err)
|
|
require.True(t, matched)
|
|
require.NotEmpty(t, ips)
|
|
require.Equal(t, "203.0.113.10", ips[0].String())
|
|
require.Equal(t, AddressRewriteReplace, mode)
|
|
}
|
|
|
|
func TestWithAddressRewriteRulesNoFalsePositiveConflicts(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
logger := &recordingLogger{}
|
|
factory := &recordingLoggerFactory{logger: logger}
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithLoggerFactory(factory),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"203.0.113.10"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
Networks: []NetworkType{NetworkTypeUDP4},
|
|
},
|
|
AddressRewriteRule{
|
|
External: []string{"2001:db8::10"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
Networks: []NetworkType{NetworkTypeUDP6},
|
|
},
|
|
),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"198.51.100.10"},
|
|
AsCandidateType: CandidateTypeServerReflexive,
|
|
},
|
|
),
|
|
)
|
|
assert.NoError(t, err)
|
|
if agent != nil {
|
|
t.Cleanup(func() {
|
|
assert.NoError(t, agent.Close())
|
|
})
|
|
}
|
|
|
|
assert.Empty(t, logger.warnings)
|
|
}
|
|
|
|
func TestLegacyAndNewAddressRewriteOrdering(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := newAgentFromConfig(
|
|
&AgentConfig{
|
|
Net: stub,
|
|
NAT1To1IPs: []string{"203.0.113.10"},
|
|
},
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"198.51.100.5"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
},
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, agent.addressRewriteRules, 2)
|
|
extIP := requireFirstExternalIP(t, agent.addressRewriteMapper, CandidateTypeHost, "10.0.0.1")
|
|
require.Equal(t, "203.0.113.10", extIP.String())
|
|
}
|
|
|
|
func TestLegacyNAT1To1TranslationOrder(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := NewAgent(&AgentConfig{
|
|
Net: stub,
|
|
NAT1To1IPs: []string{
|
|
"203.0.113.1/10.0.0.1",
|
|
"203.0.113.2",
|
|
},
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, agent.addressRewriteRules, 2)
|
|
|
|
firstRule := agent.addressRewriteRules[0]
|
|
require.Equal(t, "203.0.113.1", firstRule.External[0])
|
|
require.Equal(t, "10.0.0.1", firstRule.Local)
|
|
|
|
secondRule := agent.addressRewriteRules[1]
|
|
require.Equal(t, "203.0.113.2", secondRule.External[0])
|
|
require.Empty(t, secondRule.Local)
|
|
|
|
extIP := requireFirstExternalIP(t, agent.addressRewriteMapper, CandidateTypeHost, "10.0.0.2")
|
|
require.Equal(t, "203.0.113.2", extIP.String())
|
|
}
|
|
|
|
func TestLegacyAddressRewriteParityWithRules(t *testing.T) {
|
|
t.Run("host candidate parity", func(t *testing.T) {
|
|
legacyStub := newStubNet(t)
|
|
legacyAgent, err := NewAgent(&AgentConfig{
|
|
Net: legacyStub,
|
|
NAT1To1IPs: []string{
|
|
"203.0.113.10",
|
|
"198.51.100.20/10.0.0.20",
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
if legacyAgent != nil {
|
|
t.Cleanup(func() {
|
|
assert.NoError(t, legacyAgent.Close())
|
|
})
|
|
}
|
|
|
|
modernStub := newStubNet(t)
|
|
modernAgent, err := NewAgentWithOptions(
|
|
WithNet(modernStub),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"203.0.113.10"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
},
|
|
),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"198.51.100.20"},
|
|
Local: "10.0.0.20",
|
|
AsCandidateType: CandidateTypeHost,
|
|
},
|
|
),
|
|
)
|
|
assert.NoError(t, err)
|
|
if modernAgent != nil {
|
|
t.Cleanup(func() {
|
|
assert.NoError(t, modernAgent.Close())
|
|
})
|
|
}
|
|
|
|
for _, loc := range []string{"10.0.0.20", "10.0.0.21"} {
|
|
legacyIPs, legacyMatched, _, legacyErr := legacyAgent.addressRewriteMapper.findExternalIPs(
|
|
CandidateTypeHost, loc, "",
|
|
)
|
|
assert.NoError(t, legacyErr)
|
|
assert.True(t, legacyMatched)
|
|
assert.NotEmpty(t, legacyIPs)
|
|
|
|
modernIPs, modernMatched, _, modernErr := modernAgent.addressRewriteMapper.findExternalIPs(
|
|
CandidateTypeHost, loc, "",
|
|
)
|
|
assert.NoError(t, modernErr)
|
|
assert.True(t, modernMatched)
|
|
assert.NotEmpty(t, modernIPs)
|
|
|
|
assert.Equal(t, legacyIPs[0].String(), modernIPs[0].String())
|
|
}
|
|
})
|
|
|
|
t.Run("srflx candidate parity", func(t *testing.T) {
|
|
legacyStub := newStubNet(t)
|
|
legacyAgent, err := NewAgent(&AgentConfig{
|
|
Net: legacyStub,
|
|
NAT1To1IPs: []string{"198.51.100.77"},
|
|
NAT1To1IPCandidateType: CandidateTypeServerReflexive,
|
|
})
|
|
assert.NoError(t, err)
|
|
if legacyAgent != nil {
|
|
t.Cleanup(func() {
|
|
assert.NoError(t, legacyAgent.Close())
|
|
})
|
|
}
|
|
|
|
modernStub := newStubNet(t)
|
|
modernAgent, err := NewAgentWithOptions(
|
|
WithNet(modernStub),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"198.51.100.77"},
|
|
AsCandidateType: CandidateTypeServerReflexive,
|
|
},
|
|
),
|
|
)
|
|
assert.NoError(t, err)
|
|
if modernAgent != nil {
|
|
t.Cleanup(func() {
|
|
assert.NoError(t, modernAgent.Close())
|
|
})
|
|
}
|
|
|
|
legacyIPs, legacyMatched, _, legacyErr := legacyAgent.addressRewriteMapper.findExternalIPs(
|
|
CandidateTypeServerReflexive,
|
|
"0.0.0.0",
|
|
"",
|
|
)
|
|
assert.NoError(t, legacyErr)
|
|
assert.True(t, legacyMatched)
|
|
assert.NotEmpty(t, legacyIPs)
|
|
|
|
modernIPs, modernMatched, _, modernErr := modernAgent.addressRewriteMapper.findExternalIPs(
|
|
CandidateTypeServerReflexive,
|
|
"0.0.0.0",
|
|
"",
|
|
)
|
|
assert.NoError(t, modernErr)
|
|
assert.True(t, modernMatched)
|
|
assert.NotEmpty(t, modernIPs)
|
|
|
|
assert.Equal(t, legacyIPs[0].String(), modernIPs[0].String())
|
|
})
|
|
}
|
|
|
|
func TestOverlapWarningPerCandidateType(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
logger := &recordingLogger{}
|
|
factory := &recordingLoggerFactory{logger: logger}
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithLoggerFactory(factory),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"203.0.113.10"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
},
|
|
),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"198.51.100.10"},
|
|
AsCandidateType: CandidateTypeServerReflexive,
|
|
},
|
|
AddressRewriteRule{
|
|
External: []string{"198.51.100.20"},
|
|
AsCandidateType: CandidateTypeServerReflexive,
|
|
},
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, logger.warnings, 1)
|
|
require.Contains(t, logger.warnings[0], "candidate=srflx")
|
|
}
|
|
|
|
func TestWithNAT1To1IPValidation(t *testing.T) {
|
|
t.Run("dedupe and trim host IPs", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(AddressRewriteRule{
|
|
External: []string{" 203.0.113.1 ", "203.0.113.1", "203.0.113.2 "},
|
|
AsCandidateType: CandidateTypeHost,
|
|
}),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, agent.addressRewriteRules, 1)
|
|
require.Equal(t, []string{"203.0.113.1", "203.0.113.2"}, agent.addressRewriteRules[0].External)
|
|
})
|
|
|
|
t.Run("reject hostname entry", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(AddressRewriteRule{
|
|
External: []string{"example.com"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
}),
|
|
)
|
|
require.Nil(t, agent)
|
|
require.ErrorIs(t, err, ErrInvalidNAT1To1IPMapping)
|
|
})
|
|
|
|
t.Run("reject slash mapping in address rewrite rules", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(AddressRewriteRule{
|
|
External: []string{"203.0.113.1/10.0.0.1"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
}),
|
|
)
|
|
require.Nil(t, agent)
|
|
require.ErrorIs(t, err, ErrInvalidNAT1To1IPMapping)
|
|
})
|
|
|
|
t.Run("reject invalid rule entry", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(AddressRewriteRule{
|
|
External: []string{"1.2.3.4", "bad-ip"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
}),
|
|
)
|
|
require.Nil(t, agent)
|
|
require.ErrorIs(t, err, ErrInvalidNAT1To1IPMapping)
|
|
})
|
|
}
|
|
|
|
func TestWithAddressRewriteRulesIPv6(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(AddressRewriteRule{
|
|
External: []string{"2001:db8::2"},
|
|
Local: "2001:db8:1::2",
|
|
AsCandidateType: CandidateTypeHost,
|
|
Networks: []NetworkType{NetworkTypeUDP6},
|
|
}),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, agent.addressRewriteRules, 1)
|
|
require.Equal(t, []NetworkType{NetworkTypeUDP6}, agent.addressRewriteRules[0].Networks)
|
|
require.NotNil(t, agent.addressRewriteMapper)
|
|
|
|
mappings := agent.addressRewriteMapper.rulesByCandidateType[CandidateTypeHost]
|
|
require.Len(t, mappings, 1)
|
|
require.True(t, mappings[0].ipv6Mapping.valid)
|
|
_, ok := mappings[0].ipv6Mapping.ipMap["2001:db8:1::2"]
|
|
require.True(t, ok)
|
|
for key := range mappings[0].ipv6Mapping.ipMap {
|
|
t.Logf("stored ipv6 mapping key: %q", key)
|
|
}
|
|
locIP := net.ParseIP("2001:db8:1::2")
|
|
require.NotNil(t, locIP)
|
|
t.Logf("parsed ipv6 string: %q", locIP.String())
|
|
directExt := requireFirstMappingIP(t, &mappings[0].ipv6Mapping, locIP)
|
|
require.Equal(t, "2001:db8::2", directExt.String())
|
|
|
|
mapper, err := newAddressRewriteMapper(agent.addressRewriteRules)
|
|
require.NoError(t, err)
|
|
extIP := requireFirstExternalIP(t, mapper, CandidateTypeHost, "2001:db8:1::2")
|
|
require.Equal(t, "2001:db8::2", extIP.String())
|
|
|
|
_, matched, _, err := mapper.findExternalIPs(CandidateTypeHost, "2001:db8:1::3", "")
|
|
require.NoError(t, err)
|
|
require.False(t, matched)
|
|
}
|
|
|
|
func TestAddressRewriteRulesRejectWithMDNSQueryAndGather(t *testing.T) {
|
|
agent := &Agent{
|
|
candidateTypes: []CandidateType{CandidateTypeHost},
|
|
mDNSMode: MulticastDNSModeQueryAndGather,
|
|
addressRewriteRules: []AddressRewriteRule{
|
|
{
|
|
External: []string{"203.0.113.200"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
},
|
|
},
|
|
log: logging.NewDefaultLoggerFactory().NewLogger("test"),
|
|
}
|
|
|
|
err := applyAddressRewriteMapping(agent)
|
|
assert.ErrorIs(t, err, ErrMulticastDNSWithNAT1To1IPMapping)
|
|
}
|
|
|
|
func TestAgentAddressRewriteModeIntegration(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
t.Run("defaults host replace srflx append", func(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"203.0.113.10"},
|
|
AsCandidateType: CandidateTypeHost,
|
|
},
|
|
AddressRewriteRule{
|
|
External: []string{"198.51.100.10"},
|
|
Local: "0.0.0.0",
|
|
AsCandidateType: CandidateTypeServerReflexive,
|
|
},
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, agent.addressRewriteRules, 2)
|
|
assert.Equal(t, AddressRewriteReplace, agent.addressRewriteRules[0].Mode)
|
|
assert.Equal(t, AddressRewriteAppend, agent.addressRewriteRules[1].Mode)
|
|
})
|
|
|
|
t.Run("host append honored", func(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"203.0.113.20"},
|
|
Local: "10.0.0.5",
|
|
AsCandidateType: CandidateTypeHost,
|
|
Mode: AddressRewriteAppend,
|
|
},
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, agent.addressRewriteRules, 1)
|
|
assert.Equal(t, AddressRewriteAppend, agent.addressRewriteRules[0].Mode)
|
|
})
|
|
|
|
t.Run("srflx replace honored", func(t *testing.T) {
|
|
agent, err := NewAgentWithOptions(
|
|
WithNet(stub),
|
|
WithAddressRewriteRules(
|
|
AddressRewriteRule{
|
|
External: []string{"198.51.100.50"},
|
|
Local: "0.0.0.0",
|
|
AsCandidateType: CandidateTypeServerReflexive,
|
|
Mode: AddressRewriteReplace,
|
|
},
|
|
),
|
|
)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.Len(t, agent.addressRewriteRules, 1)
|
|
assert.Equal(t, AddressRewriteReplace, agent.addressRewriteRules[0].Mode)
|
|
})
|
|
}
|
|
|
|
func TestAddressRewriteModeOverrides(t *testing.T) {
|
|
t.Run("host append preserves local candidate", func(t *testing.T) {
|
|
mapper, err := newAddressRewriteMapper([]AddressRewriteRule{
|
|
{
|
|
External: []string{"203.0.113.99"},
|
|
Local: "10.0.0.99",
|
|
AsCandidateType: CandidateTypeHost,
|
|
Mode: AddressRewriteAppend,
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
agent := &Agent{
|
|
addressRewriteMapper: mapper,
|
|
log: logging.NewDefaultLoggerFactory().NewLogger("test"),
|
|
}
|
|
|
|
local := netip.MustParseAddr("10.0.0.99")
|
|
mapped, ok := agent.applyHostAddressRewrite(local, []netip.Addr{local}, "")
|
|
assert.True(t, ok)
|
|
assert.Equal(t, []netip.Addr{
|
|
local,
|
|
netip.MustParseAddr("203.0.113.99"),
|
|
}, mapped)
|
|
})
|
|
|
|
t.Run("srflx replace overrides default append", func(t *testing.T) {
|
|
mapper, err := newAddressRewriteMapper([]AddressRewriteRule{
|
|
{
|
|
External: []string{"198.51.100.99"},
|
|
Local: "0.0.0.0",
|
|
AsCandidateType: CandidateTypeServerReflexive,
|
|
Mode: AddressRewriteReplace,
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
agent := &Agent{
|
|
addressRewriteMapper: mapper,
|
|
log: logging.NewDefaultLoggerFactory().NewLogger("test"),
|
|
}
|
|
|
|
assert.True(t, agent.addressRewriteMapper.shouldReplace(CandidateTypeServerReflexive))
|
|
|
|
ips, matched, mode, err := agent.addressRewriteMapper.findExternalIPs(
|
|
CandidateTypeServerReflexive,
|
|
"0.0.0.0",
|
|
"",
|
|
)
|
|
assert.NoError(t, err)
|
|
assert.True(t, matched)
|
|
assert.Equal(t, AddressRewriteReplace, mode)
|
|
assert.Equal(t, "198.51.100.99", ips[0].String())
|
|
})
|
|
}
|
|
|
|
func TestAddressRewriteMixedFamilyApplication(t *testing.T) {
|
|
logger := logging.NewDefaultLoggerFactory().NewLogger("test")
|
|
|
|
mapper, err := newAddressRewriteMapper([]AddressRewriteRule{
|
|
{
|
|
External: []string{"203.0.113.123"},
|
|
Local: "2001:db8::123",
|
|
AsCandidateType: CandidateTypeHost,
|
|
Mode: AddressRewriteReplace,
|
|
},
|
|
})
|
|
assert.NoError(t, err)
|
|
|
|
agent := &Agent{
|
|
addressRewriteMapper: mapper,
|
|
log: logger,
|
|
}
|
|
|
|
local := netip.MustParseAddr("2001:db8::123")
|
|
mapped, ok := agent.applyHostAddressRewrite(local, []netip.Addr{local}, "")
|
|
assert.True(t, ok)
|
|
assert.Len(t, mapped, 1)
|
|
assert.Equal(t, netip.MustParseAddr("203.0.113.123"), mapped[0])
|
|
}
|
|
|
|
type recordingLogger struct {
|
|
warnings []string
|
|
}
|
|
|
|
func (l *recordingLogger) Trace(string) {}
|
|
|
|
func (l *recordingLogger) Tracef(string, ...any) {}
|
|
|
|
func (l *recordingLogger) Debug(string) {}
|
|
|
|
func (l *recordingLogger) Debugf(string, ...any) {}
|
|
|
|
func (l *recordingLogger) Info(string) {}
|
|
|
|
func (l *recordingLogger) Infof(string, ...any) {}
|
|
|
|
func (l *recordingLogger) Warn(msg string) {
|
|
l.warnings = append(l.warnings, msg)
|
|
}
|
|
|
|
func (l *recordingLogger) Warnf(format string, args ...any) {
|
|
l.warnings = append(l.warnings, fmt.Sprintf(format, args...))
|
|
}
|
|
|
|
func (l *recordingLogger) Error(string) {}
|
|
|
|
func (l *recordingLogger) Errorf(string, ...any) {}
|
|
|
|
type recordingLoggerFactory struct {
|
|
logger *recordingLogger
|
|
}
|
|
|
|
func (f *recordingLoggerFactory) NewLogger(string) logging.LeveledLogger {
|
|
return f.logger
|
|
}
|
|
|
|
func TestAgentConfigNAT1To1IPs(t *testing.T) {
|
|
testCases := []struct {
|
|
name string
|
|
config AgentConfig
|
|
candidateType CandidateType
|
|
localIP string
|
|
expectedIP string
|
|
}{
|
|
{
|
|
name: "host candidate default type",
|
|
config: AgentConfig{
|
|
NAT1To1IPs: []string{"1.2.3.4"},
|
|
},
|
|
candidateType: CandidateTypeHost,
|
|
localIP: "10.0.0.1",
|
|
expectedIP: "1.2.3.4",
|
|
},
|
|
{
|
|
name: "srflx candidate explicit type",
|
|
config: AgentConfig{
|
|
NAT1To1IPs: []string{"5.6.7.8"},
|
|
NAT1To1IPCandidateType: CandidateTypeServerReflexive,
|
|
},
|
|
candidateType: CandidateTypeServerReflexive,
|
|
localIP: "0.0.0.0",
|
|
expectedIP: "5.6.7.8",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
tc := tc
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
config := tc.config
|
|
config.Net = stub
|
|
|
|
agent, err := NewAgent(&config)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
require.NotNil(t, agent.addressRewriteMapper)
|
|
extIP := requireFirstExternalIP(t, agent.addressRewriteMapper, tc.candidateType, tc.localIP)
|
|
require.Equal(t, tc.expectedIP, extIP.String())
|
|
})
|
|
}
|
|
|
|
t.Run("deprecated multiple config IPs reject", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
//nolint:godox
|
|
// TODO: remove once AgentConfig.NAT1To1IPs is deprecated.
|
|
agent, err := NewAgent(&AgentConfig{
|
|
Net: stub,
|
|
NAT1To1IPs: []string{"1.2.3.4", "5.6.7.8"},
|
|
})
|
|
require.ErrorIs(t, err, ErrInvalidNAT1To1IPMapping)
|
|
require.Nil(t, agent)
|
|
})
|
|
|
|
t.Run("legacy config allows slash pair syntax", func(t *testing.T) {
|
|
stub := newStubNet(t)
|
|
|
|
agent, err := NewAgent(&AgentConfig{
|
|
Net: stub,
|
|
NAT1To1IPs: []string{"203.0.113.20/10.0.0.20"},
|
|
})
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, agent.Close())
|
|
})
|
|
|
|
extIP := requireFirstExternalIP(t, agent.addressRewriteMapper, CandidateTypeHost, "10.0.0.20")
|
|
require.Equal(t, "203.0.113.20", extIP.String())
|
|
})
|
|
}
|
|
|
|
var errStubNotImplemented = errors.New("stub not implemented")
|
|
|
|
type stubTCPMux struct{}
|
|
|
|
func (m *stubTCPMux) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (m *stubTCPMux) GetConnByUfrag(string, bool, net.IP) (net.PacketConn, error) {
|
|
return nil, errStubNotImplemented
|
|
}
|
|
|
|
func (m *stubTCPMux) RemoveConnByUfrag(string) {}
|
|
|
|
type stubUDPMux struct{}
|
|
|
|
func (m *stubUDPMux) Close() error {
|
|
return nil
|
|
}
|
|
|
|
func (m *stubUDPMux) GetConn(string, net.Addr) (net.PacketConn, error) {
|
|
return nil, errStubNotImplemented
|
|
}
|
|
|
|
func (m *stubUDPMux) RemoveConnByUfrag(string) {}
|
|
|
|
func (m *stubUDPMux) GetListenAddresses() []net.Addr {
|
|
return nil
|
|
}
|
|
|
|
type stubUniversalUDPMux struct {
|
|
stubUDPMux
|
|
}
|
|
|
|
func (m *stubUniversalUDPMux) GetXORMappedAddr(net.Addr, time.Duration) (*stun.XORMappedAddress, error) {
|
|
return nil, errStubNotImplemented
|
|
}
|
|
|
|
func (m *stubUniversalUDPMux) GetRelayedAddr(net.Addr, time.Duration) (*net.Addr, error) {
|
|
return nil, errStubNotImplemented
|
|
}
|
|
|
|
func (m *stubUniversalUDPMux) GetConnForURL(string, string, net.Addr) (net.PacketConn, error) {
|
|
return nil, errStubNotImplemented
|
|
}
|
|
|
|
type stubNet struct {
|
|
t *testing.T
|
|
udp6ListenCount int
|
|
}
|
|
|
|
func newStubNet(t *testing.T) *stubNet {
|
|
t.Helper()
|
|
|
|
return &stubNet{t: t}
|
|
}
|
|
|
|
func (n *stubNet) ListenPacket(network, address string) (net.PacketConn, error) {
|
|
return nil, transport.ErrNotSupported
|
|
}
|
|
|
|
func (n *stubNet) ListenUDP(network string, locAddr *net.UDPAddr) (transport.UDPConn, error) {
|
|
if network == "udp6" {
|
|
n.udp6ListenCount++
|
|
}
|
|
|
|
return nil, fmt.Errorf("stub net does not listen on %s", network) //nolint:err113
|
|
}
|
|
|
|
func (n *stubNet) ListenTCP(network string, laddr *net.TCPAddr) (transport.TCPListener, error) {
|
|
return nil, transport.ErrNotSupported
|
|
}
|
|
|
|
func (n *stubNet) Dial(network, address string) (net.Conn, error) {
|
|
return nil, transport.ErrNotSupported
|
|
}
|
|
|
|
func (n *stubNet) DialUDP(network string, laddr, raddr *net.UDPAddr) (transport.UDPConn, error) {
|
|
return nil, transport.ErrNotSupported
|
|
}
|
|
|
|
func (n *stubNet) DialTCP(network string, laddr, raddr *net.TCPAddr) (transport.TCPConn, error) {
|
|
return nil, transport.ErrNotSupported
|
|
}
|
|
|
|
func (n *stubNet) ResolveIPAddr(network, address string) (*net.IPAddr, error) {
|
|
return net.ResolveIPAddr(network, address)
|
|
}
|
|
|
|
func (n *stubNet) ResolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
|
return net.ResolveUDPAddr(network, address)
|
|
}
|
|
|
|
func (n *stubNet) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) {
|
|
return net.ResolveTCPAddr(network, address)
|
|
}
|
|
|
|
func (n *stubNet) Interfaces() ([]*transport.Interface, error) {
|
|
iface := transport.NewInterface(net.Interface{
|
|
Index: 1,
|
|
MTU: 1500,
|
|
Name: "stub0",
|
|
Flags: net.FlagUp,
|
|
})
|
|
iface.AddAddress(&net.IPNet{
|
|
IP: net.IPv4(192, 0, 2, 1),
|
|
Mask: net.CIDRMask(24, 32),
|
|
})
|
|
|
|
return []*transport.Interface{iface}, nil
|
|
}
|
|
|
|
func (n *stubNet) InterfaceByIndex(index int) (*transport.Interface, error) {
|
|
ifaces, err := n.Interfaces()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, iface := range ifaces {
|
|
if iface.Index == index {
|
|
return iface, nil
|
|
}
|
|
}
|
|
|
|
return nil, transport.ErrInterfaceNotFound
|
|
}
|
|
|
|
func (n *stubNet) InterfaceByName(name string) (*transport.Interface, error) {
|
|
ifaces, err := n.Interfaces()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
for _, iface := range ifaces {
|
|
if iface.Name == name {
|
|
return iface, nil
|
|
}
|
|
}
|
|
|
|
return nil, transport.ErrInterfaceNotFound
|
|
}
|
|
|
|
func (n *stubNet) CreateDialer(dialer *net.Dialer) transport.Dialer {
|
|
return nil
|
|
}
|