Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,9 @@ type Agent struct {
// 1:1 D-NAT IP address mapping
extIPMapper *externalIPMapper

// Port mapping for Docker deployments
portMapper *portMapper

// Callback that allows user to implement custom behavior
// for STUN Binding Requests
userBindingRequestHandler func(m *stun.Message, local, remote Candidate, pair *CandidatePair) bool
Expand Down Expand Up @@ -301,6 +304,12 @@ func NewAgent(config *AgentConfig) (*Agent, error) { //nolint:gocognit,cyclop
return nil, err
}

if err = config.initPortMapping(agent); err != nil {
agent.closeMulticastConn()

return nil, err
}

agent.loop = taskloop.New(func() {
agent.removeUfragFromMux()
agent.deleteAllCandidates()
Expand Down
17 changes: 17 additions & 0 deletions agent_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ func defaultCandidateTypes() []CandidateType {
return []CandidateType{CandidateTypeHost, CandidateTypeServerReflexive, CandidateTypeRelay}
}

type PortMapping struct {
InternalPort int
ExternalPort int
Protocol string
}

// AgentConfig collects the arguments to ice.Agent construction into
// a single structure, for future-proofness of the interface.
type AgentConfig struct {
Expand Down Expand Up @@ -207,6 +213,10 @@ type AgentConfig struct {
// switched to that irrespective of relative priority between current selected pair
// and priority of the pair being switched to.
EnableUseCandidateCheckPriority bool

// PortMappings is a list of port mappings that will be used to map internal ports to external ports
// Useful in Docker.
PortMappings []PortMapping
}

// initWithDefaults populates an agent and falls back to defaults if fields are unset.
Expand Down Expand Up @@ -284,6 +294,13 @@ func (config *AgentConfig) initWithDefaults(agent *Agent) { //nolint:cyclop
}
}

func (config *AgentConfig) initPortMapping(agent *Agent) error {
var err error
agent.portMapper, err = newPortMapper(config.PortMappings)

return err
}

func (config *AgentConfig) initExtIPMapping(agent *Agent) error { //nolint:cyclop
var err error
agent.extIPMapper, err = newExternalIPMapper(config.NAT1To1IPCandidateType, config.NAT1To1IPs)
Expand Down
Empty file added changes.patch
Empty file.
6 changes: 4 additions & 2 deletions gather.go
Original file line number Diff line number Diff line change
Expand Up @@ -247,10 +247,11 @@ func (a *Agent) gatherCandidatesLocal(ctx context.Context, networkTypes []Networ
}

for _, connAndPort := range conns {
externalPort := a.portMapper.getExternalPort(connAndPort.port, network)
hostConfig := CandidateHostConfig{
Network: network,
Address: address,
Port: connAndPort.port,
Port: externalPort,
Component: ComponentRTP,
TCPType: tcpType,
// we will still process this candidate so that we start up the right
Expand Down Expand Up @@ -367,10 +368,11 @@ func (a *Agent) gatherCandidatesLocalUDPMux(ctx context.Context) error { //nolin
isLocationTracked = shouldFilterLocationTracked(candidateIP)
}

externalPort := a.portMapper.getExternalPort(udpAddr.Port, "udp")
hostConfig := CandidateHostConfig{
Network: udp,
Address: address,
Port: udpAddr.Port,
Port: externalPort,
Component: ComponentRTP,
IsLocationTracked: isLocationTracked,
}
Expand Down
58 changes: 58 additions & 0 deletions port_mapper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package ice

import "fmt"

type portMapper struct {
udpMap map[int]int
tcpMap map[int]int
}

func newPortMapper(mappings []PortMapping) (*portMapper, error) {
pm := &portMapper{
udpMap: make(map[int]int),
tcpMap: make(map[int]int),
}

for _, mapping := range mappings {
if err := pm.mapPort(mapping.InternalPort, mapping.ExternalPort, mapping.Protocol); err != nil {
return nil, err
}
}

return pm, nil
}

// Add the port mapping to port mapper. Adding same internal port will override previous mapping.
// Not enforcing that each internal port is mapped to a unique external port.
func (pm *portMapper) mapPort(internalPort int, externalPort int, protocol string) error {
switch protocol {
case "udp":
pm.udpMap[internalPort] = externalPort

return nil
case "", "tcp":
pm.tcpMap[internalPort] = externalPort

return nil
}

return fmt.Errorf("unsupported protocol (supported: udp, tcp)")

Check failure on line 42 in port_mapper.go

View workflow job for this annotation

GitHub Actions / lint / Go

do not define dynamic errors, use wrapped static errors instead: "fmt.Errorf(\"unsupported protocol (supported: udp, tcp)\")" (err113)
}

func (pm *portMapper) getExternalPort(internalPort int, protocol string) int {
switch protocol {
case "udp":
if externalPort, exists := pm.udpMap[internalPort]; exists {
return externalPort
}
case "tcp":
if externalPort, exists := pm.tcpMap[internalPort]; exists {
return externalPort
}
}

return internalPort
}
78 changes: 78 additions & 0 deletions port_mapper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
// SPDX-License-Identifier: MIT

package ice

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestPortMapper(t *testing.T) {
t.Run("Empty mappings", func(t *testing.T) {
_, err := newPortMapper([]PortMapping{})
require.NoError(t, err)
})

t.Run("Single UDP mapping", func(t *testing.T) {
mappings := []PortMapping{
{InternalPort: 3478, ExternalPort: 5000, Protocol: "udp"},
}
pm, err := newPortMapper(mappings)
require.NoError(t, err)
require.NotNil(t, pm)

// Test UDP mapping
require.Equal(t, 5000, pm.getExternalPort(3478, "udp"))

// Test non-mapped port (should return original)
require.Equal(t, 3479, pm.getExternalPort(3479, "udp"))

// Test TCP (no mapping, should return original)
require.Equal(t, 3478, pm.getExternalPort(3478, "tcp"))
})

t.Run("Multiple protocol mappings", func(t *testing.T) {
mappings := []PortMapping{
{InternalPort: 3478, ExternalPort: 5000, Protocol: "udp"},
{InternalPort: 3478, ExternalPort: 5001, Protocol: "tcp"},
{InternalPort: 3479, ExternalPort: 5002, Protocol: "udp"},
}
pm, err := newPortMapper(mappings)
require.NoError(t, err)
require.NotNil(t, pm)

// Test UDP mappings
require.Equal(t, 5000, pm.getExternalPort(3478, "udp"))
require.Equal(t, 5002, pm.getExternalPort(3479, "udp"))

// Test TCP mapping
require.Equal(t, 5001, pm.getExternalPort(3478, "tcp"))

// Test unmapped TCP port
require.Equal(t, 3479, pm.getExternalPort(3479, "tcp"))
})

t.Run("Default protocol to TCP", func(t *testing.T) {
mappings := []PortMapping{
{InternalPort: 3478, ExternalPort: 5000}, // No protocol specified
}
pm, err := newPortMapper(mappings)
require.NoError(t, err)
require.NotNil(t, pm)

// Should default to TCP
require.Equal(t, 5000, pm.getExternalPort(3478, "tcp"))
require.Equal(t, 3478, pm.getExternalPort(3478, "udp"))
})

t.Run("Invalid protocol error", func(t *testing.T) {
mappings := []PortMapping{
{InternalPort: 3478, ExternalPort: 5000, Protocol: "invalid"},
}
_, err := newPortMapper(mappings)
require.Error(t, err)
require.Contains(t, err.Error(), "unsupported protocol")
})
}
Loading