mirror of
https://github.com/pion/ice.git
synced 2026-04-22 16:17:11 +08:00
Fixed TCP bugs, added TURN transport type option (#908)
Fix relay transport semantics and decouple TURN control transport from relay candidate network type ## Summary This change fixes relay transport handling in two critical areas: 1. Some relay candidates were exposed with `tcp` network in the candidate line, even though relayed endpoints are UDP. 2. Relay connectivity could fail when pion/ice was configured with TCP-only network types, because TURN control transport selection and relay candidate transport were incorrectly coupled. The patch separates these concerns: - Relay candidate transport is treated as UDP (current TURN allocation behavior). - TURN client-to-server transport is selected independently from URL scheme/proto and a dedicated TURN transport protocol configuration. ## Bugs Fixed ### 1) Incorrect relay candidate network type in candidate lines Before this change, relay candidates created through TURN over TCP/TLS paths could carry TCP network in candidate metadata. Now relay candidates are always emitted as UDP network candidates, matching actual relayed endpoint behavior. ### 2) Relay connection failure in TCP-only configurations Before this change, relay gathering used candidate network type filtering to drive TURN control connection transport, which could block valid TURN/TCP or TURNS flows depending on configured candidate network types. Now TURN control transport selection is independent and based on: - Effective URL transport (explicit `transport` or implicit defaults: TURN->UDP, TURNS->TCP) - Allowed TURN transport protocols configuration This enables relay connectivity in scenarios where TURN control transport is TCP while relay endpoints remain UDP. ## Key Implementation Changes - Introduced dedicated TURN control transport filtering path for relay gathering. - Kept relay candidate publication gated by UDP candidate support (relay allocations currently produce UDP endpoints). - Ensured relay candidate creation uses UDP network type regardless of TURN control channel transport. - Removed early remote candidate rejection based only on local network type filters, so candidates are stored and compatibility is handled later at pairing/sending stages. - Added sanitization/deduplication for network-type based options and TURN transport protocol options. ## API/Config Notes - `WithNetworkTypes(...)` continues to control candidate network types exposed/used for pairing. - `WithTURNTransportProtocols(...)` controls TURN client<->server transport independently. - Config comments were updated to clarify these responsibilities and defaults. ## Test Coverage Added/Updated - Added comprehensive relay/srflx/host transport matrix coverage in `transport_filtering_matrix_test.go`. - Added explicit TURN/TURNS and implicit transport-default test cases. - Added regression test verifying TURN-over-TCP still produces UDP relay candidates. - Updated remote-candidate behavior tests to reflect store-first semantics. - Added/updated option sanitization tests for network type and TURN transport protocol handling.
This commit is contained in:
@@ -119,9 +119,10 @@ type Agent struct {
|
||||
|
||||
selectedPair atomic.Value // *CandidatePair
|
||||
|
||||
urls []*stun.URI
|
||||
networkTypes []NetworkType
|
||||
addressRewriteRules []AddressRewriteRule
|
||||
urls []*stun.URI
|
||||
networkTypes []NetworkType
|
||||
turnTransportProtocols []NetworkType
|
||||
addressRewriteRules []AddressRewriteRule
|
||||
|
||||
buf *packetio.Buffer
|
||||
|
||||
@@ -324,6 +325,16 @@ func createAgentBase(config *AgentConfig) (*Agent, error) {
|
||||
return nil, ErrPort
|
||||
}
|
||||
|
||||
normalizedNetworkTypes, err := sanitizeTransportNetworkTypes(config.NetworkTypes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
normalizedTURNTransportProtocols, err := sanitizeTransportNetworkTypes(config.turnTransportProtocols)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mDNSName, mDNSMode, err := setupMDNSConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -346,7 +357,8 @@ func createAgentBase(config *AgentConfig) (*Agent, error) {
|
||||
remoteCandidates: make(map[NetworkType][]Candidate),
|
||||
pairsByID: make(map[uint64]*CandidatePair),
|
||||
urls: config.Urls,
|
||||
networkTypes: config.NetworkTypes,
|
||||
networkTypes: normalizedNetworkTypes,
|
||||
turnTransportProtocols: normalizedTURNTransportProtocols,
|
||||
onConnected: make(chan struct{}),
|
||||
buf: packetio.NewBuffer(),
|
||||
startedCh: startedCtx.Done(),
|
||||
@@ -1152,12 +1164,6 @@ func (a *Agent) addRemoteCandidate(cand Candidate) bool { //nolint:cyclop
|
||||
return false
|
||||
}
|
||||
|
||||
if len(a.networkTypes) > 0 && !slices.Contains(a.networkTypes, cand.NetworkType()) {
|
||||
a.log.Infof("Ignoring remote candidate with disabled network type %s: %s", cand.NetworkType(), cand)
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
set := a.remoteCandidates[cand.NetworkType()]
|
||||
|
||||
for _, candidate := range set {
|
||||
@@ -1174,10 +1180,8 @@ func (a *Agent) addRemoteCandidate(cand Candidate) bool { //nolint:cyclop
|
||||
acceptRemotePassiveTCPCandidate := false
|
||||
// Assert that TCP4 or TCP6 is a enabled NetworkType locally
|
||||
if !a.disableActiveTCP && cand.TCPType() == TCPTypePassive {
|
||||
for _, networkType := range a.networkTypes {
|
||||
if cand.NetworkType() == networkType {
|
||||
acceptRemotePassiveTCPCandidate = true
|
||||
}
|
||||
if slices.Contains(configuredNetworkTypes(a.networkTypes), cand.NetworkType()) {
|
||||
acceptRemotePassiveTCPCandidate = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-2
@@ -112,10 +112,24 @@ type AgentConfig struct {
|
||||
// connecting state.
|
||||
CheckInterval *time.Duration
|
||||
|
||||
// NetworkTypes is an optional configuration for disabling or enabling
|
||||
// support for specific network types.
|
||||
// NetworkTypes controls the candidate network types exposed in ICE candidates
|
||||
// and used for pairing.
|
||||
//
|
||||
// This is independent from the TURN client-to-server transport configured via
|
||||
// WithTURNTransportProtocols. Supported values are the NetworkType variants
|
||||
// (NetworkTypeUDP4, NetworkTypeUDP6, NetworkTypeTCP4, NetworkTypeTCP6).
|
||||
// When empty, all candidate network types are enabled by default.
|
||||
NetworkTypes []NetworkType
|
||||
|
||||
// turnTransportProtocols restricts protocols used internally by this agent when connecting
|
||||
// to TURN servers (the TURN client <-> TURN server transport).
|
||||
//
|
||||
// This is independent from NetworkTypes, which controls candidate network types
|
||||
// exposed in ICE and used for pairing. Configure this via
|
||||
// WithTURNTransportProtocols. Supported values are the NetworkType variants
|
||||
// (NetworkTypeUDP4, NetworkTypeUDP6, NetworkTypeTCP4, NetworkTypeTCP6).
|
||||
turnTransportProtocols []NetworkType
|
||||
|
||||
// CandidateTypes is an optional configuration for disabling or enabling
|
||||
// support for specific candidate types.
|
||||
CandidateTypes []CandidateType
|
||||
|
||||
+55
-2
@@ -856,7 +856,9 @@ func WithNetworkMonitorInterval(interval time.Duration) AgentOption {
|
||||
}
|
||||
}
|
||||
|
||||
// WithNetworkTypes sets the enabled network types for candidate gathering.
|
||||
// WithNetworkTypes sets the enabled candidate network types for candidate gathering.
|
||||
// This controls the network types exposed in ICE candidates and used for pairing.
|
||||
// Use WithTURNTransportProtocols to control the local TURN client-to-server transport.
|
||||
// By default, all network types are enabled.
|
||||
//
|
||||
// Example:
|
||||
@@ -870,12 +872,63 @@ func WithNetworkTypes(networkTypes []NetworkType) AgentOption {
|
||||
return ErrAgentOptionNotUpdatable
|
||||
}
|
||||
|
||||
a.networkTypes = networkTypes
|
||||
normalized, err := sanitizeTransportNetworkTypes(networkTypes)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.networkTypes = normalized
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// WithTURNTransportProtocols restricts protocols used by this agent when
|
||||
// connecting to TURN servers (TURN client <-> TURN server transport).
|
||||
//
|
||||
// This is independent from WithNetworkTypes, which controls ICE candidate
|
||||
// network types announced to the peer. Supported values are
|
||||
// NetworkTypeUDP4/UDP6 and NetworkTypeTCP4/TCP6.
|
||||
func WithTURNTransportProtocols(protocols []NetworkType) AgentOption {
|
||||
return func(a *Agent) error {
|
||||
if a.constructed {
|
||||
return ErrAgentOptionNotUpdatable
|
||||
}
|
||||
|
||||
normalized, err := sanitizeTransportNetworkTypes(protocols)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a.turnTransportProtocols = normalized
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func sanitizeTransportNetworkTypes(types []NetworkType) ([]NetworkType, error) {
|
||||
if len(types) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
seen := map[NetworkType]struct{}{}
|
||||
out := make([]NetworkType, 0, len(types))
|
||||
for _, networkType := range types {
|
||||
if !networkType.IsUDP() && !networkType.IsTCP() {
|
||||
return nil, ErrProtoType
|
||||
}
|
||||
|
||||
if _, ok := seen[networkType]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
seen[networkType] = struct{}{}
|
||||
out = append(out, networkType)
|
||||
}
|
||||
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// WithCandidateTypes sets the enabled candidate types for gathering.
|
||||
// By default, host, server reflexive, and relay candidates are enabled.
|
||||
//
|
||||
|
||||
@@ -616,6 +616,86 @@ func TestWithNetworkTypesAppliedBeforeRestart(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithNetworkTypes(t *testing.T) {
|
||||
t.Run("applies option", func(t *testing.T) {
|
||||
agent, err := NewAgentWithOptions(
|
||||
WithNetworkTypes([]NetworkType{NetworkTypeUDP4, NetworkTypeTCP4}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
require.Equal(t, []NetworkType{NetworkTypeUDP4, NetworkTypeTCP4}, agent.networkTypes)
|
||||
})
|
||||
|
||||
t.Run("deduplicates values", func(t *testing.T) {
|
||||
agent, err := NewAgentWithOptions(
|
||||
WithNetworkTypes([]NetworkType{NetworkTypeUDP4, NetworkTypeUDP4, NetworkTypeTCP4}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
require.Equal(t, []NetworkType{NetworkTypeUDP4, NetworkTypeTCP4}, agent.networkTypes)
|
||||
})
|
||||
|
||||
t.Run("rejects unsupported value", func(t *testing.T) {
|
||||
_, err := NewAgentWithOptions(
|
||||
WithNetworkTypes([]NetworkType{NetworkType(0)}),
|
||||
)
|
||||
require.ErrorIs(t, err, ErrProtoType)
|
||||
})
|
||||
|
||||
t.Run("rejects unsupported value from config", func(t *testing.T) {
|
||||
_, err := NewAgent(&AgentConfig{
|
||||
NetworkTypes: []NetworkType{NetworkType(0)},
|
||||
})
|
||||
require.ErrorIs(t, err, ErrProtoType)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithTURNTransportProtocols(t *testing.T) {
|
||||
t.Run("applies option", func(t *testing.T) {
|
||||
agent, err := NewAgentWithOptions(
|
||||
WithTURNTransportProtocols([]NetworkType{NetworkTypeTCP4}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
require.Equal(t, []NetworkType{NetworkTypeTCP4}, agent.turnTransportProtocols)
|
||||
})
|
||||
|
||||
t.Run("deduplicates protocols", func(t *testing.T) {
|
||||
agent, err := NewAgentWithOptions(
|
||||
WithTURNTransportProtocols([]NetworkType{NetworkTypeTCP4, NetworkTypeTCP4, NetworkTypeUDP4}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
require.Equal(t, []NetworkType{NetworkTypeTCP4, NetworkTypeUDP4}, agent.turnTransportProtocols)
|
||||
})
|
||||
|
||||
t.Run("rejects unsupported proto", func(t *testing.T) {
|
||||
_, err := NewAgentWithOptions(
|
||||
WithTURNTransportProtocols([]NetworkType{NetworkType(0)}),
|
||||
)
|
||||
require.ErrorIs(t, err, ErrProtoType)
|
||||
})
|
||||
|
||||
t.Run("rejects unsupported proto from config", func(t *testing.T) {
|
||||
_, err := NewAgent(&AgentConfig{
|
||||
turnTransportProtocols: []NetworkType{NetworkType(0)},
|
||||
})
|
||||
require.ErrorIs(t, err, ErrProtoType)
|
||||
})
|
||||
}
|
||||
|
||||
func TestWithCandidateTypesAffectsURLValidation(t *testing.T) {
|
||||
stunURL, err := stun.ParseURI("stun:example.com:3478")
|
||||
require.NoError(t, err)
|
||||
|
||||
+149
-4
@@ -443,7 +443,7 @@ func TestHandlePeerReflexive(t *testing.T) { //nolint:cyclop,maintidx
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("prflx candidate is discarded when network type is disabled", func(t *testing.T) {
|
||||
t.Run("prflx candidate is stored even when network type is disabled", func(t *testing.T) {
|
||||
agent, err := NewAgentWithOptions(
|
||||
WithNetworkTypes([]NetworkType{NetworkTypeTCP4}),
|
||||
)
|
||||
@@ -479,7 +479,8 @@ func TestHandlePeerReflexive(t *testing.T) { //nolint:cyclop,maintidx
|
||||
|
||||
// nolint: contextcheck
|
||||
agent.handleInbound(msg, local, remote)
|
||||
require.Len(t, agent.remoteCandidates, 0)
|
||||
require.Len(t, agent.remoteCandidates, 1)
|
||||
require.Len(t, agent.remoteCandidates[NetworkTypeUDP4], 1)
|
||||
}))
|
||||
})
|
||||
|
||||
@@ -522,7 +523,7 @@ func TestHandlePeerReflexive(t *testing.T) { //nolint:cyclop,maintidx
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddRemoteCandidateRespectsNetworkTypes(t *testing.T) {
|
||||
func TestAddRemoteCandidateStoresCandidatesIndependentlyOfNetworkTypes(t *testing.T) {
|
||||
defer test.CheckRoutines(t)()
|
||||
|
||||
agent, err := NewAgentWithOptions(
|
||||
@@ -547,7 +548,21 @@ func TestAddRemoteCandidateRespectsNetworkTypes(t *testing.T) {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(actual) == 1 && actual[0].Address() == udpCandidate.Address()
|
||||
if len(actual) != 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
var hasUDP, hasTCP bool
|
||||
for _, c := range actual {
|
||||
if c.Address() == udpCandidate.Address() {
|
||||
hasUDP = true
|
||||
}
|
||||
if c.Address() == tcpCandidate.Address() {
|
||||
hasTCP = true
|
||||
}
|
||||
}
|
||||
|
||||
return hasUDP && hasTCP
|
||||
}, time.Second, 10*time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -3104,6 +3119,7 @@ func TestAgentUpdateOptions(t *testing.T) {
|
||||
"WithContinualGatheringPolicy": WithContinualGatheringPolicy(GatherOnce),
|
||||
"WithNetworkMonitorInterval": WithNetworkMonitorInterval(time.Second),
|
||||
"WithNetworkTypes": WithNetworkTypes([]NetworkType{NetworkTypeUDP4}),
|
||||
"WithTURNTransportProtocols": WithTURNTransportProtocols([]NetworkType{NetworkTypeTCP4}),
|
||||
"WithCandidateTypes": WithCandidateTypes([]CandidateType{CandidateTypeHost}),
|
||||
"WithAutomaticRenomination": WithAutomaticRenomination(time.Second),
|
||||
"WithInterfaceFilter": WithInterfaceFilter(func(string) bool { return true }),
|
||||
@@ -3163,6 +3179,135 @@ func TestMDNSQueryTimeout(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestAddRemoteCandidateIndependentFromTURNTransportSelection(t *testing.T) {
|
||||
t.Run("accepts UDP relay candidate with tcp-only configured network types and TURN/TCP URL", func(t *testing.T) {
|
||||
agent, err := NewAgent(&AgentConfig{
|
||||
NetworkTypes: []NetworkType{NetworkTypeTCP4},
|
||||
CandidateTypes: []CandidateType{CandidateTypeRelay},
|
||||
Urls: []*stun.URI{{
|
||||
Scheme: stun.SchemeTypeTURN,
|
||||
Proto: stun.ProtoTypeTCP,
|
||||
Host: "turn.example.com",
|
||||
Port: 3478,
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
cand, err := NewCandidateRelay(&CandidateRelayConfig{
|
||||
Network: udp,
|
||||
Address: "198.51.100.2",
|
||||
Port: 5000,
|
||||
Component: ComponentRTP,
|
||||
RelAddr: "192.0.2.10",
|
||||
RelPort: 4000,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, agent.loop.Run(agent.loop, func(_ context.Context) {
|
||||
accepted := agent.addRemoteCandidate(cand) // nolint:contextcheck
|
||||
require.True(t, accepted)
|
||||
require.Len(t, agent.remoteCandidates[NetworkTypeUDP4], 1)
|
||||
}))
|
||||
})
|
||||
|
||||
// nolint:dupl
|
||||
t.Run("accepts UDP host candidate with tcp-only configured network types and TURN/TCP URL", func(t *testing.T) {
|
||||
agent, err := NewAgent(&AgentConfig{
|
||||
NetworkTypes: []NetworkType{NetworkTypeTCP4},
|
||||
CandidateTypes: []CandidateType{CandidateTypeRelay},
|
||||
Urls: []*stun.URI{{
|
||||
Scheme: stun.SchemeTypeTURN,
|
||||
Proto: stun.ProtoTypeTCP,
|
||||
Host: "turn.example.com",
|
||||
Port: 3478,
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
cand, err := UnmarshalCandidate("1052353102 1 udp 1675624447 198.51.100.20 5002 typ host")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, agent.loop.Run(agent.loop, func(_ context.Context) {
|
||||
accepted := agent.addRemoteCandidate(cand) // nolint:contextcheck
|
||||
require.True(t, accepted)
|
||||
require.Len(t, agent.remoteCandidates[NetworkTypeUDP4], 1)
|
||||
}))
|
||||
})
|
||||
|
||||
// nolint:dupl
|
||||
t.Run("accepts UDP srflx candidate with tcp-only configured network types and TURN/TCP URL", func(t *testing.T) {
|
||||
agent, err := NewAgent(&AgentConfig{
|
||||
NetworkTypes: []NetworkType{NetworkTypeTCP4},
|
||||
CandidateTypes: []CandidateType{CandidateTypeRelay},
|
||||
Urls: []*stun.URI{{
|
||||
Scheme: stun.SchemeTypeTURN,
|
||||
Proto: stun.ProtoTypeTCP,
|
||||
Host: "turn.example.com",
|
||||
Port: 3478,
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
cand, err := UnmarshalCandidate(
|
||||
"1052353102 1 udp 1675624447 198.51.100.21 5003 typ srflx raddr 192.0.2.21 rport 4003")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, agent.loop.Run(agent.loop, func(_ context.Context) {
|
||||
accepted := agent.addRemoteCandidate(cand) // nolint:contextcheck
|
||||
require.True(t, accepted)
|
||||
require.Len(t, agent.remoteCandidates[NetworkTypeUDP4], 1)
|
||||
}))
|
||||
})
|
||||
|
||||
t.Run("stores UDP relay candidate regardless of TURN URL transport", func(t *testing.T) {
|
||||
agent, err := NewAgent(&AgentConfig{
|
||||
NetworkTypes: []NetworkType{NetworkTypeTCP4},
|
||||
CandidateTypes: []CandidateType{CandidateTypeRelay},
|
||||
Urls: []*stun.URI{{
|
||||
Scheme: stun.SchemeTypeTURN,
|
||||
Proto: stun.ProtoTypeUDP,
|
||||
Host: "turn.example.com",
|
||||
Port: 3478,
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
cand, err := NewCandidateRelay(&CandidateRelayConfig{
|
||||
Network: udp,
|
||||
Address: "198.51.100.3",
|
||||
Port: 5001,
|
||||
Component: ComponentRTP,
|
||||
RelAddr: "192.0.2.11",
|
||||
RelPort: 4001,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
require.NoError(t, agent.loop.Run(agent.loop, func(_ context.Context) {
|
||||
accepted := agent.addRemoteCandidate(cand) // nolint:contextcheck
|
||||
require.True(t, accepted)
|
||||
require.Len(t, agent.remoteCandidates[NetworkTypeUDP4], 1)
|
||||
}))
|
||||
})
|
||||
}
|
||||
|
||||
type localAddrTCPMux struct {
|
||||
addr net.Addr
|
||||
}
|
||||
|
||||
@@ -64,30 +64,38 @@ func urlSupportsSrflxGathering(url stun.URI) bool {
|
||||
return url.Scheme == stun.SchemeTypeSTUN || url.Scheme == stun.SchemeTypeTURN
|
||||
}
|
||||
|
||||
func relayNetworkTypesForURL(url stun.URI, networkTypes []NetworkType) []NetworkType {
|
||||
func relayNetworkTypesForConfiguredCandidates(networkTypes []NetworkType) []NetworkType {
|
||||
// Relay allocations currently produce UDP relay endpoints, so relay candidate
|
||||
// publication must be gated by configured UDP candidate network types.
|
||||
res := []NetworkType{}
|
||||
for _, networkType := range configuredNetworkTypes(networkTypes) {
|
||||
if networkType.IsUDP() {
|
||||
res = append(res, networkType)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func turnNetworkTypesForURL(url stun.URI, networkTypes []NetworkType) []NetworkType {
|
||||
proto := effectiveURLProtoType(url)
|
||||
switch proto {
|
||||
case stun.ProtoTypeUDP:
|
||||
res := []NetworkType{}
|
||||
for _, networkType := range configuredNetworkTypes(networkTypes) {
|
||||
res := []NetworkType{}
|
||||
|
||||
for _, networkType := range configuredNetworkTypes(networkTypes) {
|
||||
switch proto {
|
||||
case stun.ProtoTypeUDP:
|
||||
if networkType.IsUDP() {
|
||||
res = append(res, networkType)
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
case stun.ProtoTypeTCP:
|
||||
res := []NetworkType{}
|
||||
for _, networkType := range configuredNetworkTypes(networkTypes) {
|
||||
case stun.ProtoTypeTCP:
|
||||
if networkType.IsTCP() {
|
||||
res = append(res, networkType)
|
||||
}
|
||||
default:
|
||||
}
|
||||
|
||||
return res
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
// Close a net.Conn and log if we have a failure.
|
||||
@@ -932,6 +940,13 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) {
|
||||
localAddrs := []ifaceAddr{}
|
||||
if useFilteredLocalAddrs {
|
||||
localAddrs = append(localAddrs, ifaces...)
|
||||
if len(localAddrs) == 0 {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(relayNetworkTypesForConfiguredCandidates(a.networkTypes)) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for _, url := range urls {
|
||||
@@ -948,7 +963,9 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) {
|
||||
return
|
||||
}
|
||||
|
||||
networkTypes := relayNetworkTypesForURL(*url, a.networkTypes)
|
||||
urlProto := effectiveURLProtoType(*url)
|
||||
|
||||
networkTypes := turnNetworkTypesForURL(*url, a.turnTransportProtocols)
|
||||
if len(networkTypes) == 0 {
|
||||
continue
|
||||
}
|
||||
@@ -960,8 +977,6 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) {
|
||||
}
|
||||
|
||||
network := networkType.String()
|
||||
urlProto := effectiveURLProtoType(*url)
|
||||
|
||||
bindAddrs := []string{}
|
||||
if !useFilteredLocalAddrs { // nolint:nestif
|
||||
if networkType.IsIPv6() {
|
||||
@@ -1175,8 +1190,10 @@ func (a *Agent) gatherCandidatesRelay(ctx context.Context, urls []*stun.URI) {
|
||||
return
|
||||
}
|
||||
|
||||
// Relay allocations currently produce UDP relay endpoints regardless of
|
||||
// whether the TURN control connection uses UDP/TCP/TLS/DTLS.
|
||||
a.addRelayCandidates(ctx, relayEndpoint{
|
||||
network: network,
|
||||
network: udp,
|
||||
address: rAddr.IP,
|
||||
port: rAddr.Port,
|
||||
relAddr: relAddr,
|
||||
|
||||
+83
-1
@@ -1602,12 +1602,13 @@ func TestGatherCandidatesRelayRespectsNetworkTypeAndTransport(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("skips TCP transport URL when only UDP network types are enabled", func(t *testing.T) {
|
||||
t.Run("skips TCP transport URL when TURN transport protocols allow only UDP", func(t *testing.T) {
|
||||
stubClient := &stubTurnClient{}
|
||||
|
||||
agent, err := NewAgentWithOptions(
|
||||
WithNet(newRelayGatherNet(&net.UDPAddr{IP: net.IPv4(10, 0, 0, 3), Port: 50000})),
|
||||
WithNetworkTypes([]NetworkType{NetworkTypeUDP4}),
|
||||
WithTURNTransportProtocols([]NetworkType{NetworkTypeUDP4}),
|
||||
WithCandidateTypes([]CandidateType{CandidateTypeRelay}),
|
||||
WithUrls([]*stun.URI{
|
||||
{
|
||||
@@ -1724,6 +1725,27 @@ func (m *mockConn) SetDeadline(time.Time) error { return io.EOF }
|
||||
func (m *mockConn) SetReadDeadline(time.Time) error { return io.EOF }
|
||||
func (m *mockConn) SetWriteDeadline(time.Time) error { return io.EOF }
|
||||
|
||||
type relayTCPProxyDialer struct {
|
||||
localAddr *net.TCPAddr
|
||||
}
|
||||
|
||||
func (d *relayTCPProxyDialer) Dial(string, string) (net.Conn, error) {
|
||||
return &relayTCPProxyConn{localAddr: d.localAddr}, nil
|
||||
}
|
||||
|
||||
type relayTCPProxyConn struct {
|
||||
localAddr *net.TCPAddr
|
||||
}
|
||||
|
||||
func (c *relayTCPProxyConn) Read([]byte) (int, error) { return 0, io.EOF }
|
||||
func (c *relayTCPProxyConn) Write(p []byte) (int, error) { return len(p), nil }
|
||||
func (c *relayTCPProxyConn) Close() error { return nil }
|
||||
func (c *relayTCPProxyConn) LocalAddr() net.Addr { return c.localAddr }
|
||||
func (c *relayTCPProxyConn) RemoteAddr() net.Addr { return &net.TCPAddr{} }
|
||||
func (c *relayTCPProxyConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *relayTCPProxyConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *relayTCPProxyConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
func (m *mockProxy) Dial(string, string) (net.Conn, error) {
|
||||
m.proxyWasDialed()
|
||||
|
||||
@@ -1778,6 +1800,66 @@ func TestTURNProxyDialer(t *testing.T) {
|
||||
<-proxyWasDialed.Done()
|
||||
}
|
||||
|
||||
func TestGatherCandidatesRelayTURNOverTCPProducesUDPRelayCandidate(t *testing.T) {
|
||||
defer test.CheckRoutines(t)()
|
||||
|
||||
stubClient := &stubTurnClient{}
|
||||
stubClient.relayConn = newStubPacketConn(&net.UDPAddr{IP: net.IPv4(203, 0, 113, 10), Port: 6000})
|
||||
|
||||
agent, err := NewAgentWithOptions(
|
||||
WithNet(newRelayGatherNet(&net.UDPAddr{IP: net.IPv4(10, 0, 0, 5), Port: 50000})),
|
||||
WithNetworkTypes([]NetworkType{NetworkTypeUDP4, NetworkTypeTCP4}),
|
||||
WithCandidateTypes([]CandidateType{CandidateTypeRelay}),
|
||||
WithUrls([]*stun.URI{
|
||||
{
|
||||
Scheme: stun.SchemeTypeTURN,
|
||||
Host: "example.com",
|
||||
Port: 3478,
|
||||
Username: "username",
|
||||
Password: "password",
|
||||
Proto: stun.ProtoTypeTCP,
|
||||
},
|
||||
}),
|
||||
WithMulticastDNSMode(MulticastDNSModeDisabled),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
agent.proxyDialer = &relayTCPProxyDialer{localAddr: &net.TCPAddr{IP: net.IPv4(10, 0, 0, 5), Port: 55000}}
|
||||
agent.turnClientFactory = func(cfg *turn.ClientConfig) (turnClient, error) {
|
||||
stubClient.cfgConn = cfg.Conn
|
||||
|
||||
return stubClient, nil
|
||||
}
|
||||
|
||||
relayCandidateCh := make(chan *CandidateRelay, 1)
|
||||
require.NoError(t, agent.OnCandidate(func(c Candidate) {
|
||||
if c == nil || c.Type() != CandidateTypeRelay {
|
||||
return
|
||||
}
|
||||
|
||||
relay, ok := c.(*CandidateRelay)
|
||||
require.True(t, ok)
|
||||
|
||||
select {
|
||||
case relayCandidateCh <- relay:
|
||||
default:
|
||||
}
|
||||
}))
|
||||
|
||||
agent.gatherCandidatesRelay(context.Background(), agent.urls)
|
||||
|
||||
select {
|
||||
case relay := <-relayCandidateCh:
|
||||
require.Equal(t, NetworkTypeUDP4, relay.NetworkType())
|
||||
require.Equal(t, tcp, relay.RelayProtocol())
|
||||
case <-time.After(time.Second):
|
||||
require.FailNow(t, "expected relay candidate for TURN over TCP")
|
||||
}
|
||||
}
|
||||
|
||||
func buildSimpleVNet(t *testing.T) (*vnet.Router, *vnet.Net) {
|
||||
t.Helper()
|
||||
|
||||
|
||||
@@ -0,0 +1,772 @@
|
||||
// SPDX-FileCopyrightText: 2026 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !js
|
||||
|
||||
package ice
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/pion/stun/v3"
|
||||
transport "github.com/pion/transport/v4"
|
||||
"github.com/pion/transport/v4/test"
|
||||
"github.com/pion/turn/v4"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
var errFirewallBlocked = errors.New("firewall blocked protocol")
|
||||
|
||||
type firewallNet struct {
|
||||
allowUDP bool
|
||||
allowTCP bool
|
||||
|
||||
mu sync.Mutex
|
||||
listenUDPCalls int
|
||||
listenPktCalls int
|
||||
dialUDPCalls int
|
||||
dialTCPCalls int
|
||||
listenPktAddrs []string
|
||||
listenPktNets []string
|
||||
listenUDPNets []string
|
||||
listenUDPAddrs []string
|
||||
dialUDPNets []string
|
||||
dialTCPNets []string
|
||||
}
|
||||
|
||||
func newFirewallNet(allowUDP, allowTCP bool) *firewallNet {
|
||||
return &firewallNet{allowUDP: allowUDP, allowTCP: allowTCP}
|
||||
}
|
||||
|
||||
func (n *firewallNet) ListenPacket(network, address string) (net.PacketConn, error) {
|
||||
n.mu.Lock()
|
||||
n.listenPktCalls++
|
||||
n.listenPktNets = append(n.listenPktNets, network)
|
||||
n.listenPktAddrs = append(n.listenPktAddrs, address)
|
||||
n.mu.Unlock()
|
||||
|
||||
if isUDPNetworkName(network) {
|
||||
if !n.allowUDP {
|
||||
return nil, errFirewallBlocked
|
||||
}
|
||||
|
||||
udpAddr, err := net.ResolveUDPAddr(network, address)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return newStubPacketConn(udpAddr), nil
|
||||
}
|
||||
|
||||
if isTCPNetworkName(network) && !n.allowTCP {
|
||||
return nil, errFirewallBlocked
|
||||
}
|
||||
|
||||
return nil, transport.ErrNotSupported
|
||||
}
|
||||
|
||||
func (n *firewallNet) ListenUDP(network string, laddr *net.UDPAddr) (transport.UDPConn, error) {
|
||||
n.mu.Lock()
|
||||
n.listenUDPCalls++
|
||||
n.listenUDPNets = append(n.listenUDPNets, network)
|
||||
if laddr != nil {
|
||||
n.listenUDPAddrs = append(n.listenUDPAddrs, laddr.String())
|
||||
} else {
|
||||
n.listenUDPAddrs = append(n.listenUDPAddrs, "")
|
||||
}
|
||||
n.mu.Unlock()
|
||||
|
||||
if !n.allowUDP {
|
||||
return nil, errFirewallBlocked
|
||||
}
|
||||
|
||||
if laddr == nil {
|
||||
laddr = &net.UDPAddr{IP: net.IPv4zero, Port: 0}
|
||||
}
|
||||
|
||||
return net.ListenUDP(network, laddr) //nolint:wrapcheck
|
||||
}
|
||||
|
||||
func (n *firewallNet) ListenTCP(string, *net.TCPAddr) (transport.TCPListener, error) {
|
||||
return nil, transport.ErrNotSupported
|
||||
}
|
||||
|
||||
func (n *firewallNet) Dial(string, string) (net.Conn, error) {
|
||||
return nil, transport.ErrNotSupported
|
||||
}
|
||||
|
||||
func (n *firewallNet) DialUDP(network string, laddr, raddr *net.UDPAddr) (transport.UDPConn, error) {
|
||||
n.mu.Lock()
|
||||
n.dialUDPCalls++
|
||||
n.dialUDPNets = append(n.dialUDPNets, network)
|
||||
n.mu.Unlock()
|
||||
|
||||
if !n.allowUDP {
|
||||
return nil, errFirewallBlocked
|
||||
}
|
||||
|
||||
return net.DialUDP(network, laddr, raddr) //nolint:wrapcheck
|
||||
}
|
||||
|
||||
func (n *firewallNet) DialTCP(network string, laddr, raddr *net.TCPAddr) (transport.TCPConn, error) {
|
||||
n.mu.Lock()
|
||||
n.dialTCPCalls++
|
||||
n.dialTCPNets = append(n.dialTCPNets, network)
|
||||
n.mu.Unlock()
|
||||
|
||||
if !n.allowTCP {
|
||||
return nil, errFirewallBlocked
|
||||
}
|
||||
|
||||
return net.DialTCP(network, laddr, raddr) //nolint:wrapcheck
|
||||
}
|
||||
|
||||
func (n *firewallNet) ResolveIPAddr(network, address string) (*net.IPAddr, error) {
|
||||
return net.ResolveIPAddr(network, address)
|
||||
}
|
||||
|
||||
func (n *firewallNet) ResolveUDPAddr(network, address string) (*net.UDPAddr, error) {
|
||||
return net.ResolveUDPAddr(network, address)
|
||||
}
|
||||
|
||||
func (n *firewallNet) ResolveTCPAddr(network, address string) (*net.TCPAddr, error) {
|
||||
return net.ResolveTCPAddr(network, address)
|
||||
}
|
||||
|
||||
func (n *firewallNet) Interfaces() ([]*transport.Interface, error) {
|
||||
iface := transport.NewInterface(net.Interface{
|
||||
Index: 1,
|
||||
MTU: 1500,
|
||||
Name: "fw0",
|
||||
Flags: net.FlagUp | net.FlagLoopback,
|
||||
})
|
||||
iface.AddAddress(&net.IPNet{IP: net.IPv4(127, 0, 0, 1), Mask: net.CIDRMask(8, 32)})
|
||||
|
||||
return []*transport.Interface{iface}, nil
|
||||
}
|
||||
|
||||
func (n *firewallNet) 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 *firewallNet) 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 *firewallNet) CreateDialer(*net.Dialer) transport.Dialer {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *firewallNet) CreateListenConfig(*net.ListenConfig) transport.ListenConfig {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (n *firewallNet) counts() (listenUDP, listenPacket, dialUDP, dialTCP int) {
|
||||
n.mu.Lock()
|
||||
defer n.mu.Unlock()
|
||||
|
||||
return n.listenUDPCalls, n.listenPktCalls, n.dialUDPCalls, n.dialTCPCalls
|
||||
}
|
||||
|
||||
type firewallProxyDialer struct {
|
||||
allowTCP bool
|
||||
|
||||
mu sync.Mutex
|
||||
dialCalls int
|
||||
}
|
||||
|
||||
func (d *firewallProxyDialer) Dial(network, _ string) (net.Conn, error) {
|
||||
d.mu.Lock()
|
||||
d.dialCalls++
|
||||
d.mu.Unlock()
|
||||
|
||||
if !isTCPNetworkName(network) || !d.allowTCP {
|
||||
return nil, errFirewallBlocked
|
||||
}
|
||||
|
||||
return &matrixTCPConn{
|
||||
local: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 50000},
|
||||
remote: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 3478},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *firewallProxyDialer) count() int {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
return d.dialCalls
|
||||
}
|
||||
|
||||
type matrixTCPConn struct {
|
||||
local net.Addr
|
||||
remote net.Addr
|
||||
}
|
||||
|
||||
func (c *matrixTCPConn) Read([]byte) (int, error) { return 0, io.EOF }
|
||||
func (c *matrixTCPConn) Write(p []byte) (int, error) { return len(p), nil }
|
||||
func (c *matrixTCPConn) Close() error { return nil }
|
||||
func (c *matrixTCPConn) LocalAddr() net.Addr { return c.local }
|
||||
func (c *matrixTCPConn) RemoteAddr() net.Addr { return c.remote }
|
||||
func (c *matrixTCPConn) SetDeadline(time.Time) error { return nil }
|
||||
func (c *matrixTCPConn) SetReadDeadline(time.Time) error { return nil }
|
||||
func (c *matrixTCPConn) SetWriteDeadline(time.Time) error { return nil }
|
||||
|
||||
func isUDPNetworkName(network string) bool {
|
||||
return strings.HasPrefix(network, "udp")
|
||||
}
|
||||
|
||||
func isTCPNetworkName(network string) bool {
|
||||
return strings.HasPrefix(network, "tcp")
|
||||
}
|
||||
|
||||
func gatherAndCollectCandidates(t *testing.T, agent *Agent) []Candidate {
|
||||
t.Helper()
|
||||
|
||||
var (
|
||||
mu sync.Mutex
|
||||
candidates []Candidate
|
||||
done = make(chan struct{})
|
||||
)
|
||||
|
||||
require.NoError(t, agent.OnCandidate(func(c Candidate) {
|
||||
if c == nil {
|
||||
close(done)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
candidates = append(candidates, c)
|
||||
mu.Unlock()
|
||||
}))
|
||||
|
||||
require.NoError(t, agent.GatherCandidates())
|
||||
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(3 * time.Second):
|
||||
assert.FailNow(t, "candidate gathering did not finish in time")
|
||||
}
|
||||
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
|
||||
out := make([]Candidate, len(candidates))
|
||||
copy(out, candidates)
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func TestTransportFilteringRelayMatrix(t *testing.T) { // nolint:cyclop
|
||||
defer test.CheckRoutines(t)()
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
|
||||
allowUDP bool
|
||||
allowTCP bool
|
||||
|
||||
networkTypes []NetworkType
|
||||
turnScheme stun.SchemeType
|
||||
turnProto stun.ProtoType
|
||||
turnAllowed []NetworkType
|
||||
|
||||
expectFactoryMinCalls int
|
||||
expectRelayCandidate bool
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "tcp-only firewall with udp relay config and TURN/TCP gathers relay",
|
||||
allowUDP: false,
|
||||
allowTCP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeTCP,
|
||||
expectFactoryMinCalls: 1,
|
||||
expectRelayCandidate: true,
|
||||
},
|
||||
{
|
||||
name: "udp-only firewall with udp-only config and TURN/UDP gathers relay",
|
||||
allowUDP: true,
|
||||
allowTCP: false,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeUDP,
|
||||
expectFactoryMinCalls: 1,
|
||||
expectRelayCandidate: true,
|
||||
},
|
||||
{
|
||||
name: "tcp-only candidate config skips relay gathering",
|
||||
allowUDP: false,
|
||||
allowTCP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeTCP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeUDP,
|
||||
expectFactoryMinCalls: 0,
|
||||
expectRelayCandidate: false,
|
||||
},
|
||||
{
|
||||
name: "udp relay config with TURN/TCP fails when tcp blocked by firewall",
|
||||
allowUDP: true,
|
||||
allowTCP: false,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeTCP,
|
||||
expectFactoryMinCalls: 0,
|
||||
expectRelayCandidate: false,
|
||||
},
|
||||
{
|
||||
name: "tcp-only firewall with udp relay config and TURNS/TCP gathers relay",
|
||||
allowUDP: false,
|
||||
allowTCP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURNS,
|
||||
turnProto: stun.ProtoTypeTCP,
|
||||
expectFactoryMinCalls: 1,
|
||||
expectRelayCandidate: true,
|
||||
},
|
||||
{
|
||||
name: "tcp-only config with TURN URL without transport param defaults to UDP and is filtered",
|
||||
allowUDP: false,
|
||||
allowTCP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeTCP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeUnknown,
|
||||
expectFactoryMinCalls: 0,
|
||||
expectRelayCandidate: false,
|
||||
},
|
||||
{
|
||||
name: "udp-only config with TURN URL without transport param defaults to UDP and gathers relay",
|
||||
allowUDP: true,
|
||||
allowTCP: false,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeUnknown,
|
||||
expectFactoryMinCalls: 1,
|
||||
expectRelayCandidate: true,
|
||||
},
|
||||
{
|
||||
name: "udp relay config with TURNS URL without transport param defaults to TCP and gathers relay",
|
||||
allowUDP: false,
|
||||
allowTCP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURNS,
|
||||
turnProto: stun.ProtoTypeUnknown,
|
||||
expectFactoryMinCalls: 1,
|
||||
expectRelayCandidate: true,
|
||||
},
|
||||
{
|
||||
name: "udp-only config with TURNS URL without transport param defaults to TCP and is filtered",
|
||||
allowUDP: true,
|
||||
allowTCP: false,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURNS,
|
||||
turnProto: stun.ProtoTypeUnknown,
|
||||
expectFactoryMinCalls: 0,
|
||||
expectRelayCandidate: false,
|
||||
},
|
||||
{
|
||||
name: "TURN/TCP URL blocked by TURN transport option allowing UDP only",
|
||||
allowUDP: false,
|
||||
allowTCP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeTCP,
|
||||
turnAllowed: []NetworkType{NetworkTypeUDP4},
|
||||
expectFactoryMinCalls: 0,
|
||||
expectRelayCandidate: false,
|
||||
},
|
||||
{
|
||||
name: "TURNS default TCP allowed by TURN transport option",
|
||||
allowUDP: false,
|
||||
allowTCP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURNS,
|
||||
turnProto: stun.ProtoTypeUnknown,
|
||||
turnAllowed: []NetworkType{NetworkTypeTCP4},
|
||||
expectFactoryMinCalls: 1,
|
||||
expectRelayCandidate: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
netFW := newFirewallNet(tc.allowUDP, tc.allowTCP)
|
||||
proxyDialer := &firewallProxyDialer{allowTCP: tc.allowTCP}
|
||||
|
||||
url := &stun.URI{
|
||||
Scheme: tc.turnScheme,
|
||||
Proto: tc.turnProto,
|
||||
Host: "turn.example.com",
|
||||
Port: 3478,
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
}
|
||||
|
||||
opts := []AgentOption{
|
||||
WithNet(netFW),
|
||||
WithProxyDialer(proxy.Dialer(proxyDialer)),
|
||||
WithCandidateTypes([]CandidateType{CandidateTypeRelay}),
|
||||
WithNetworkTypes(tc.networkTypes),
|
||||
WithMulticastDNSMode(MulticastDNSModeDisabled),
|
||||
WithIncludeLoopback(),
|
||||
WithUrls([]*stun.URI{url}),
|
||||
}
|
||||
if len(tc.turnAllowed) > 0 {
|
||||
opts = append(opts, WithTURNTransportProtocols(tc.turnAllowed))
|
||||
}
|
||||
|
||||
agent, err := NewAgentWithOptions(
|
||||
opts...,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
factoryCalls := 0
|
||||
agent.turnClientFactory = func(*turn.ClientConfig) (turnClient, error) {
|
||||
factoryCalls++
|
||||
|
||||
return &stubTurnClient{
|
||||
relayConn: newStubPacketConn(&net.UDPAddr{IP: net.IPv4(203, 0, 113, 50), Port: 6000}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
candidates := gatherAndCollectCandidates(t, agent)
|
||||
relayCandidates := 0
|
||||
for _, c := range candidates {
|
||||
if c.Type() == CandidateTypeRelay {
|
||||
relayCandidates++
|
||||
require.True(t, c.NetworkType().IsUDP(), "relay endpoint must be UDP")
|
||||
}
|
||||
}
|
||||
|
||||
require.GreaterOrEqual(t, factoryCalls, tc.expectFactoryMinCalls)
|
||||
if tc.expectRelayCandidate {
|
||||
require.Greater(t, relayCandidates, 0)
|
||||
} else {
|
||||
require.Equal(t, 0, relayCandidates)
|
||||
}
|
||||
|
||||
_, _, _, dialTCP := netFW.counts()
|
||||
if effectiveURLProtoType(*url) == stun.ProtoTypeTCP { // nolint:nestif
|
||||
tcpAllowedByOption := len(tc.turnAllowed) == 0
|
||||
if !tcpAllowedByOption {
|
||||
for _, nt := range tc.turnAllowed {
|
||||
if nt.IsTCP() {
|
||||
tcpAllowedByOption = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if tcpAllowedByOption {
|
||||
require.Greater(t, proxyDialer.count()+dialTCP, 0, "expected TURN/TCP dial attempt")
|
||||
} else {
|
||||
require.Equal(t, 0, proxyDialer.count()+dialTCP, "unexpected TURN/TCP dial attempt")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransportFilteringSrflxMatrix(t *testing.T) {
|
||||
defer test.CheckRoutines(t)()
|
||||
|
||||
serverPort := randomPort(t)
|
||||
serverListener, err := net.ListenPacket("udp4", localhostIPStr+":"+strconv.Itoa(serverPort)) // nolint: noctx
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
_ = serverListener.Close()
|
||||
}()
|
||||
|
||||
server, err := turn.NewServer(turn.ServerConfig{
|
||||
Realm: "pion.ly",
|
||||
AuthHandler: optimisticAuthHandler,
|
||||
PacketConnConfigs: []turn.PacketConnConfig{{
|
||||
PacketConn: serverListener,
|
||||
RelayAddressGenerator: &turn.RelayAddressGeneratorNone{Address: localhostIPStr},
|
||||
}},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, server.Close())
|
||||
}()
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
allowUDP bool
|
||||
networkTypes []NetworkType
|
||||
turnScheme stun.SchemeType
|
||||
turnProto stun.ProtoType
|
||||
expectSrflx bool
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "udp allowed and udp config with TURN/UDP gathers srflx",
|
||||
allowUDP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeUDP,
|
||||
expectSrflx: true,
|
||||
},
|
||||
{
|
||||
name: "tcp-only config with TURN/TCP does not gather srflx",
|
||||
allowUDP: false,
|
||||
networkTypes: []NetworkType{NetworkTypeTCP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeTCP,
|
||||
expectSrflx: false,
|
||||
},
|
||||
{
|
||||
name: "udp config with TURN/TCP transport is filtered for srflx",
|
||||
allowUDP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeTCP,
|
||||
expectSrflx: false,
|
||||
},
|
||||
{
|
||||
name: "udp config with TURN URL without transport param defaults to UDP and gathers srflx",
|
||||
allowUDP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURN,
|
||||
turnProto: stun.ProtoTypeUnknown,
|
||||
expectSrflx: true,
|
||||
},
|
||||
{
|
||||
name: "udp config with TURNS URL without transport param defaults to TCP and is filtered for srflx",
|
||||
allowUDP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURNS,
|
||||
turnProto: stun.ProtoTypeUnknown,
|
||||
expectSrflx: false,
|
||||
},
|
||||
{
|
||||
name: "udp config with explicit TURNS/TCP is filtered for srflx",
|
||||
allowUDP: true,
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
turnScheme: stun.SchemeTypeTURNS,
|
||||
turnProto: stun.ProtoTypeTCP,
|
||||
expectSrflx: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
netFW := newFirewallNet(tc.allowUDP, true)
|
||||
url := &stun.URI{
|
||||
Scheme: tc.turnScheme,
|
||||
Proto: tc.turnProto,
|
||||
Host: localhostIPStr,
|
||||
Port: serverPort,
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
}
|
||||
|
||||
agent, err := NewAgentWithOptions(
|
||||
WithNet(netFW),
|
||||
WithCandidateTypes([]CandidateType{CandidateTypeServerReflexive}),
|
||||
WithNetworkTypes(tc.networkTypes),
|
||||
WithMulticastDNSMode(MulticastDNSModeDisabled),
|
||||
WithIncludeLoopback(),
|
||||
WithSTUNGatherTimeout(200*time.Millisecond),
|
||||
WithUrls([]*stun.URI{url}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
candidates := gatherAndCollectCandidates(t, agent)
|
||||
srflxCandidates := 0
|
||||
for _, c := range candidates {
|
||||
if c.Type() == CandidateTypeServerReflexive {
|
||||
srflxCandidates++
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectSrflx {
|
||||
require.Greater(t, srflxCandidates, 0)
|
||||
} else {
|
||||
require.Equal(t, 0, srflxCandidates)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransportFilteringHostMatrix(t *testing.T) {
|
||||
defer test.CheckRoutines(t)()
|
||||
|
||||
type testCase struct {
|
||||
name string
|
||||
networkTypes []NetworkType
|
||||
allowUDP bool
|
||||
useTCPMux bool
|
||||
expectHost bool
|
||||
expectTCP bool
|
||||
}
|
||||
|
||||
testCases := []testCase{
|
||||
{
|
||||
name: "udp config gathers udp host candidate",
|
||||
networkTypes: []NetworkType{NetworkTypeUDP4},
|
||||
allowUDP: true,
|
||||
useTCPMux: false,
|
||||
expectHost: true,
|
||||
expectTCP: false,
|
||||
},
|
||||
{
|
||||
name: "tcp config gathers tcp host candidate with tcp mux",
|
||||
networkTypes: []NetworkType{NetworkTypeTCP4},
|
||||
allowUDP: false,
|
||||
useTCPMux: true,
|
||||
expectHost: true,
|
||||
expectTCP: true,
|
||||
},
|
||||
{
|
||||
name: "tcp config without tcp mux yields no host candidate",
|
||||
networkTypes: []NetworkType{NetworkTypeTCP4},
|
||||
allowUDP: false,
|
||||
useTCPMux: false,
|
||||
expectHost: false,
|
||||
expectTCP: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
netFW := newFirewallNet(tc.allowUDP, true)
|
||||
|
||||
opts := []AgentOption{
|
||||
WithNet(netFW),
|
||||
WithCandidateTypes([]CandidateType{CandidateTypeHost}),
|
||||
WithNetworkTypes(tc.networkTypes),
|
||||
WithMulticastDNSMode(MulticastDNSModeDisabled),
|
||||
WithIncludeLoopback(),
|
||||
}
|
||||
if tc.useTCPMux {
|
||||
opts = append(opts, WithTCPMux(&boundTCPMux{
|
||||
localAddr: &net.TCPAddr{IP: net.IPv4(127, 0, 0, 1), Port: 34567},
|
||||
}))
|
||||
}
|
||||
|
||||
agent, err := NewAgentWithOptions(opts...)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
candidates := gatherAndCollectCandidates(t, agent)
|
||||
hostCandidates := 0
|
||||
tcpHosts := 0
|
||||
for _, c := range candidates {
|
||||
if c.Type() != CandidateTypeHost {
|
||||
continue
|
||||
}
|
||||
|
||||
hostCandidates++
|
||||
if c.NetworkType().IsTCP() {
|
||||
tcpHosts++
|
||||
}
|
||||
}
|
||||
|
||||
if tc.expectHost {
|
||||
require.Greater(t, hostCandidates, 0)
|
||||
} else {
|
||||
require.Equal(t, 0, hostCandidates)
|
||||
}
|
||||
|
||||
if tc.expectTCP {
|
||||
require.Greater(t, tcpHosts, 0)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTransportFilteringRelayTCPOnlyFirewallUDPRelayConfigTURNTCP(t *testing.T) {
|
||||
defer test.CheckRoutines(t)()
|
||||
|
||||
netFW := newFirewallNet(false, true)
|
||||
proxyDialer := &firewallProxyDialer{allowTCP: true}
|
||||
|
||||
agent, err := NewAgentWithOptions(
|
||||
WithNet(netFW),
|
||||
WithProxyDialer(proxy.Dialer(proxyDialer)),
|
||||
WithCandidateTypes([]CandidateType{CandidateTypeRelay}),
|
||||
WithNetworkTypes([]NetworkType{NetworkTypeUDP4}),
|
||||
WithMulticastDNSMode(MulticastDNSModeDisabled),
|
||||
WithIncludeLoopback(),
|
||||
WithUrls([]*stun.URI{{
|
||||
Scheme: stun.SchemeTypeTURN,
|
||||
Proto: stun.ProtoTypeTCP,
|
||||
Host: "turn.example.com",
|
||||
Port: 3478,
|
||||
Username: "user",
|
||||
Password: "pass",
|
||||
}}),
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer func() {
|
||||
require.NoError(t, agent.Close())
|
||||
}()
|
||||
|
||||
factoryCalls := 0
|
||||
agent.turnClientFactory = func(*turn.ClientConfig) (turnClient, error) {
|
||||
factoryCalls++
|
||||
|
||||
return &stubTurnClient{
|
||||
relayConn: newStubPacketConn(&net.UDPAddr{IP: net.IPv4(203, 0, 113, 77), Port: 6100}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
candidates := gatherAndCollectCandidates(t, agent)
|
||||
|
||||
require.GreaterOrEqual(t, proxyDialer.count(), 1)
|
||||
require.GreaterOrEqual(t, factoryCalls, 1)
|
||||
|
||||
relayCandidates := 0
|
||||
for _, c := range candidates {
|
||||
if c.Type() != CandidateTypeRelay {
|
||||
continue
|
||||
}
|
||||
relayCandidates++
|
||||
require.True(t, c.NetworkType().IsUDP())
|
||||
}
|
||||
require.Greater(t, relayCandidates, 0)
|
||||
}
|
||||
Reference in New Issue
Block a user