From e762598f51f3068d2ad65e642543b151783a2778 Mon Sep 17 00:00:00 2001 From: Alex Hu Date: Mon, 21 Jul 2025 00:17:50 -0700 Subject: [PATCH] maybe works --- agent.go | 9 ++++++ agent_config.go | 17 ++++++++++ changes.patch | 0 gather.go | 6 ++-- port_mapper.go | 58 +++++++++++++++++++++++++++++++++ port_mapper_test.go | 78 +++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 166 insertions(+), 2 deletions(-) create mode 100644 changes.patch create mode 100644 port_mapper.go create mode 100644 port_mapper_test.go diff --git a/agent.go b/agent.go index a7f090dc..5fc5af6f 100644 --- a/agent.go +++ b/agent.go @@ -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 @@ -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() diff --git a/agent_config.go b/agent_config.go index 708aab56..826251d3 100644 --- a/agent_config.go +++ b/agent_config.go @@ -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 { @@ -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. @@ -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) diff --git a/changes.patch b/changes.patch new file mode 100644 index 00000000..e69de29b diff --git a/gather.go b/gather.go index ebf2999b..52774387 100644 --- a/gather.go +++ b/gather.go @@ -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 @@ -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, } diff --git a/port_mapper.go b/port_mapper.go new file mode 100644 index 00000000..53ca3b40 --- /dev/null +++ b/port_mapper.go @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// 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)") +} + +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 +} diff --git a/port_mapper_test.go b/port_mapper_test.go new file mode 100644 index 00000000..58394737 --- /dev/null +++ b/port_mapper_test.go @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2023 The Pion community +// 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") + }) +}