feat: add port map to webrtc

This commit is contained in:
langhuihui
2025-06-13 16:54:29 +08:00
parent 827f6eac8d
commit a1b40bd7b8
3 changed files with 608 additions and 50 deletions

View File

@@ -13,6 +13,7 @@ type (
Port struct { Port struct {
Protocol string Protocol string
Ports [2]int Ports [2]int
Map [2]int // 映射端口范围,通常用于 NAT 或端口转发
} }
IPort interface { IPort interface {
IsTCP() bool IsTCP() bool
@@ -22,10 +23,23 @@ type (
) )
func (p Port) String() string { func (p Port) String() string {
var result string
if p.Ports[0] == p.Ports[1] { if p.Ports[0] == p.Ports[1] {
return p.Protocol + ":" + strconv.Itoa(p.Ports[0]) result = p.Protocol + ":" + strconv.Itoa(p.Ports[0])
} else {
result = p.Protocol + ":" + strconv.Itoa(p.Ports[0]) + "-" + strconv.Itoa(p.Ports[1])
} }
return p.Protocol + ":" + strconv.Itoa(p.Ports[0]) + "-" + strconv.Itoa(p.Ports[1])
// 如果有端口映射,添加映射信息
if p.HasMapping() {
if p.Map[0] == p.Map[1] {
result += ":" + strconv.Itoa(p.Map[0])
} else {
result += ":" + strconv.Itoa(p.Map[0]) + "-" + strconv.Itoa(p.Map[1])
}
}
return result
} }
func (p Port) IsTCP() bool { func (p Port) IsTCP() bool {
@@ -40,6 +54,36 @@ func (p Port) IsRange() bool {
return p.Ports[0] != p.Ports[1] return p.Ports[0] != p.Ports[1]
} }
func (p Port) HasMapping() bool {
return p.Map[0] > 0 || p.Map[1] > 0
}
func (p Port) IsRangeMapping() bool {
return p.HasMapping() && p.Map[0] != p.Map[1]
}
// ParsePort2 解析端口配置字符串并返回对应的端口类型实例
// 根据协议类型和端口范围返回不同的类型:
// - TCP单端口返回 TCPPort
// - TCP端口范围返回 TCPRangePort
// - UDP单端口返回 UDPPort
// - UDP端口范围返回 UDPRangePort
//
// 参数:
//
// conf - 端口配置字符串格式protocol:port 或 protocol:port1-port2
//
// 返回值:
//
// ret - 端口实例 (TCPPort/UDPPort/TCPRangePort/UDPRangePort)
// err - 解析错误
//
// 示例:
//
// ParsePort2("tcp:8080") // 返回 TCPPort(8080)
// ParsePort2("tcp:8080-8090") // 返回 TCPRangePort([2]int{8080, 8090})
// ParsePort2("udp:5000") // 返回 UDPPort(5000)
// ParsePort2("udp:5000-5010") // 返回 UDPRangePort([2]int{5000, 5010})
func ParsePort2(conf string) (ret any, err error) { func ParsePort2(conf string) (ret any, err error) {
var port Port var port Port
port, err = ParsePort(conf) port, err = ParsePort(conf)
@@ -58,10 +102,84 @@ func ParsePort2(conf string) (ret any, err error) {
return UDPPort(port.Ports[0]), nil return UDPPort(port.Ports[0]), nil
} }
// ParsePort 解析端口配置字符串为 Port 结构体
// 支持协议前缀、端口号/端口范围以及端口映射的解析
//
// 参数:
//
// conf - 端口配置字符串,格式:
// - "protocol:port" 单端口,如 "tcp:8080"
// - "protocol:port1-port2" 端口范围,如 "tcp:8080-8090"
// - "protocol:port:mapPort" 单端口映射,如 "tcp:8080:9090"
// - "protocol:port:mapPort1-mapPort2" 单端口映射到端口范围,如 "tcp:8080:9000-9010"
// - "protocol:port1-port2:mapPort1-mapPort2" 端口范围映射,如 "tcp:8080-8090:9000-9010"
//
// 返回值:
//
// ret - Port 结构体,包含协议、端口和映射端口信息
// err - 解析错误
//
// 注意:
// - 如果端口范围中 min > max会自动交换顺序
// - 单端口时Ports[0] 和 Ports[1] 值相同
// - 端口映射时Map[0] 和 Map[1] 存储映射的目标端口范围
// - 单个映射端口时Map[0] 和 Map[1] 值相同
//
// 示例:
//
// ParsePort("tcp:8080") // Port{Protocol:"tcp", Ports:[2]int{8080, 8080}, Map:[2]int{0, 0}}
// ParsePort("tcp:8080-8090") // Port{Protocol:"tcp", Ports:[2]int{8080, 8090}, Map:[2]int{0, 0}}
// ParsePort("tcp:8080:9090") // Port{Protocol:"tcp", Ports:[2]int{8080, 8080}, Map:[2]int{9090, 9090}}
// ParsePort("tcp:8080:9000-9010") // Port{Protocol:"tcp", Ports:[2]int{8080, 8080}, Map:[2]int{9000, 9010}}
// ParsePort("tcp:8080-8090:9000-9010") // Port{Protocol:"tcp", Ports:[2]int{8080, 8090}, Map:[2]int{9000, 9010}}
// ParsePort("udp:5000") // Port{Protocol:"udp", Ports:[2]int{5000, 5000}, Map:[2]int{0, 0}}
// ParsePort("udp:5010-5000") // Port{Protocol:"udp", Ports:[2]int{5000, 5010}, Map:[2]int{0, 0}}
func ParsePort(conf string) (ret Port, err error) { func ParsePort(conf string) (ret Port, err error) {
var port string var port, mapPort string
var min, max int var min, max int
ret.Protocol, port, _ = strings.Cut(conf, ":")
// 按冒号分割,支持端口映射
parts := strings.Split(conf, ":")
if len(parts) < 2 || len(parts) > 3 {
err = strconv.ErrSyntax
return
}
ret.Protocol = parts[0]
port = parts[1]
// 处理端口映射
if len(parts) == 3 {
mapPort = parts[2]
// 解析映射端口,支持单端口和端口范围
if mapRange := strings.Split(mapPort, "-"); len(mapRange) == 2 {
// 映射端口范围
var mapMin, mapMax int
mapMin, err = strconv.Atoi(mapRange[0])
if err != nil {
return
}
mapMax, err = strconv.Atoi(mapRange[1])
if err != nil {
return
}
if mapMin < mapMax {
ret.Map[0], ret.Map[1] = mapMin, mapMax
} else {
ret.Map[0], ret.Map[1] = mapMax, mapMin
}
} else {
// 单个映射端口
var mapPortNum int
mapPortNum, err = strconv.Atoi(mapPort)
if err != nil {
return
}
ret.Map[0], ret.Map[1] = mapPortNum, mapPortNum
}
}
// 处理端口范围
if r := strings.Split(port, "-"); len(r) == 2 { if r := strings.Split(port, "-"); len(r) == 2 {
min, err = strconv.Atoi(r[0]) min, err = strconv.Atoi(r[0])
if err != nil { if err != nil {
@@ -76,7 +194,12 @@ func ParsePort(conf string) (ret Port, err error) {
} else { } else {
ret.Ports[0], ret.Ports[1] = max, min ret.Ports[0], ret.Ports[1] = max, min
} }
} else if p, err := strconv.Atoi(port); err == nil { } else {
var p int
p, err = strconv.Atoi(port)
if err != nil {
return
}
ret.Ports[0], ret.Ports[1] = p, p ret.Ports[0], ret.Ports[1] = p, p
} }
return return

370
pkg/port_test.go Normal file
View File

@@ -0,0 +1,370 @@
package pkg
import (
"testing"
)
func TestParsePort(t *testing.T) {
tests := []struct {
name string
input string
expected Port
hasError bool
}{
{
name: "TCP单端口",
input: "tcp:8080",
expected: Port{
Protocol: "tcp",
Ports: [2]int{8080, 8080},
Map: [2]int{0, 0},
},
hasError: false,
},
{
name: "TCP端口范围",
input: "tcp:8080-8090",
expected: Port{
Protocol: "tcp",
Ports: [2]int{8080, 8090},
Map: [2]int{0, 0},
},
hasError: false,
},
{
name: "TCP端口范围反序",
input: "tcp:8090-8080",
expected: Port{
Protocol: "tcp",
Ports: [2]int{8080, 8090},
Map: [2]int{0, 0},
},
hasError: false,
},
{
name: "TCP单端口映射到单端口",
input: "tcp:8080:9090",
expected: Port{
Protocol: "tcp",
Ports: [2]int{8080, 8080},
Map: [2]int{9090, 9090},
},
hasError: false,
},
{
name: "TCP单端口映射到端口范围",
input: "tcp:8080:9000-9010",
expected: Port{
Protocol: "tcp",
Ports: [2]int{8080, 8080},
Map: [2]int{9000, 9010},
},
hasError: false,
},
{
name: "TCP端口范围映射到端口范围",
input: "tcp:8080-8090:9000-9010",
expected: Port{
Protocol: "tcp",
Ports: [2]int{8080, 8090},
Map: [2]int{9000, 9010},
},
hasError: false,
},
{
name: "UDP单端口",
input: "udp:5000",
expected: Port{
Protocol: "udp",
Ports: [2]int{5000, 5000},
Map: [2]int{0, 0},
},
hasError: false,
},
{
name: "UDP端口范围",
input: "udp:5000-5010",
expected: Port{
Protocol: "udp",
Ports: [2]int{5000, 5010},
Map: [2]int{0, 0},
},
hasError: false,
},
{
name: "UDP端口映射",
input: "udp:5000:6000",
expected: Port{
Protocol: "udp",
Ports: [2]int{5000, 5000},
Map: [2]int{6000, 6000},
},
hasError: false,
},
{
name: "UDP端口范围映射映射范围反序",
input: "udp:5000-5010:6010-6000",
expected: Port{
Protocol: "udp",
Ports: [2]int{5000, 5010},
Map: [2]int{6000, 6010},
},
hasError: false,
},
// 错误情况
{
name: "缺少协议",
input: "8080",
expected: Port{},
hasError: true,
},
{
name: "过多冒号",
input: "tcp:8080:9090:extra",
expected: Port{},
hasError: true,
},
{
name: "无效端口号",
input: "tcp:abc",
expected: Port{},
hasError: true,
},
{
name: "无效映射端口号",
input: "tcp:8080:abc",
expected: Port{},
hasError: true,
},
{
name: "无效端口范围",
input: "tcp:8080-abc",
expected: Port{},
hasError: true,
},
{
name: "无效映射端口范围",
input: "tcp:8080:9000-abc",
expected: Port{},
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParsePort(tt.input)
if tt.hasError {
if err == nil {
t.Errorf("期望有错误,但没有错误")
}
return
}
if err != nil {
t.Errorf("意外的错误: %v", err)
return
}
if result.Protocol != tt.expected.Protocol {
t.Errorf("协议不匹配: 期望 %s, 得到 %s", tt.expected.Protocol, result.Protocol)
}
if result.Ports != tt.expected.Ports {
t.Errorf("端口不匹配: 期望 %v, 得到 %v", tt.expected.Ports, result.Ports)
}
if result.Map != tt.expected.Map {
t.Errorf("映射端口不匹配: 期望 %v, 得到 %v", tt.expected.Map, result.Map)
}
})
}
}
func TestPortMethods(t *testing.T) {
tests := []struct {
name string
port Port
expectTCP bool
expectUDP bool
expectRange bool
expectMapping bool
expectRangeMap bool
expectString string
}{
{
name: "TCP单端口",
port: Port{
Protocol: "tcp",
Ports: [2]int{8080, 8080},
Map: [2]int{0, 0},
},
expectTCP: true,
expectUDP: false,
expectRange: false,
expectMapping: false,
expectRangeMap: false,
expectString: "tcp:8080",
},
{
name: "TCP端口范围",
port: Port{
Protocol: "tcp",
Ports: [2]int{8080, 8090},
Map: [2]int{0, 0},
},
expectTCP: true,
expectUDP: false,
expectRange: true,
expectMapping: false,
expectRangeMap: false,
expectString: "tcp:8080-8090",
},
{
name: "TCP单端口映射",
port: Port{
Protocol: "tcp",
Ports: [2]int{8080, 8080},
Map: [2]int{9090, 9090},
},
expectTCP: true,
expectUDP: false,
expectRange: false,
expectMapping: true,
expectRangeMap: false,
expectString: "tcp:8080:9090",
},
{
name: "TCP端口范围映射",
port: Port{
Protocol: "tcp",
Ports: [2]int{8080, 8090},
Map: [2]int{9000, 9010},
},
expectTCP: true,
expectUDP: false,
expectRange: true,
expectMapping: true,
expectRangeMap: true,
expectString: "tcp:8080-8090:9000-9010",
},
{
name: "UDP单端口映射到端口范围",
port: Port{
Protocol: "udp",
Ports: [2]int{5000, 5000},
Map: [2]int{6000, 6010},
},
expectTCP: false,
expectUDP: true,
expectRange: false,
expectMapping: true,
expectRangeMap: true,
expectString: "udp:5000:6000-6010",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.port.IsTCP() != tt.expectTCP {
t.Errorf("IsTCP(): 期望 %v, 得到 %v", tt.expectTCP, tt.port.IsTCP())
}
if tt.port.IsUDP() != tt.expectUDP {
t.Errorf("IsUDP(): 期望 %v, 得到 %v", tt.expectUDP, tt.port.IsUDP())
}
if tt.port.IsRange() != tt.expectRange {
t.Errorf("IsRange(): 期望 %v, 得到 %v", tt.expectRange, tt.port.IsRange())
}
if tt.port.HasMapping() != tt.expectMapping {
t.Errorf("HasMapping(): 期望 %v, 得到 %v", tt.expectMapping, tt.port.HasMapping())
}
if tt.port.IsRangeMapping() != tt.expectRangeMap {
t.Errorf("IsRangeMapping(): 期望 %v, 得到 %v", tt.expectRangeMap, tt.port.IsRangeMapping())
}
if tt.port.String() != tt.expectString {
t.Errorf("String(): 期望 %s, 得到 %s", tt.expectString, tt.port.String())
}
})
}
}
func TestParsePort2(t *testing.T) {
tests := []struct {
name string
input string
expectedType string
hasError bool
}{
{
name: "TCP单端口",
input: "tcp:8080",
expectedType: "TCPPort",
hasError: false,
},
{
name: "TCP端口范围",
input: "tcp:8080-8090",
expectedType: "TCPRangePort",
hasError: false,
},
{
name: "UDP单端口",
input: "udp:5000",
expectedType: "UDPPort",
hasError: false,
},
{
name: "UDP端口范围",
input: "udp:5000-5010",
expectedType: "UDPRangePort",
hasError: false,
},
{
name: "无效输入",
input: "invalid",
hasError: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result, err := ParsePort2(tt.input)
if tt.hasError {
if err == nil {
t.Errorf("期望有错误,但没有错误")
}
return
}
if err != nil {
t.Errorf("意外的错误: %v", err)
return
}
switch tt.expectedType {
case "TCPPort":
if _, ok := result.(TCPPort); !ok {
t.Errorf("期望类型 TCPPort, 得到 %T", result)
}
case "TCPRangePort":
if _, ok := result.(TCPRangePort); !ok {
t.Errorf("期望类型 TCPRangePort, 得到 %T", result)
}
case "UDPPort":
if _, ok := result.(UDPPort); !ok {
t.Errorf("期望类型 UDPPort, 得到 %T", result)
}
case "UDPRangePort":
if _, ok := result.(UDPRangePort); !ok {
t.Errorf("期望类型 UDPRangePort, 得到 %T", result)
}
}
})
}
}

View File

@@ -43,6 +43,7 @@ type WebRTCPlugin struct {
EnableDC bool `default:"true" desc:"是否启用DataChannel"` // 在不支持编码格式的情况下是否启用DataChannel传输 EnableDC bool `default:"true" desc:"是否启用DataChannel"` // 在不支持编码格式的情况下是否启用DataChannel传输
MimeType []string `desc:"MimeType过滤列表为空则不过滤"` // MimeType过滤列表支持的格式如video/H264, audio/opus MimeType []string `desc:"MimeType过滤列表为空则不过滤"` // MimeType过滤列表支持的格式如video/H264, audio/opus
s SettingEngine s SettingEngine
portMapping map[int]int // 内部端口到外部端口的映射
} }
func (p *WebRTCPlugin) RegisterHandler() map[string]http.HandlerFunc { func (p *WebRTCPlugin) RegisterHandler() map[string]http.HandlerFunc {
@@ -306,15 +307,48 @@ func (p *WebRTCPlugin) initSettingEngine() error {
// configurePort 配置端口设置 // configurePort 配置端口设置
func (p *WebRTCPlugin) configurePort() error { func (p *WebRTCPlugin) configurePort() error {
ports, err := ParsePort2(p.Port) // 使用 ParsePort 而不是 ParsePort2 来获取端口映射信息
portInfo, err := ParsePort(p.Port)
if err != nil { if err != nil {
p.Error("webrtc port config error", "error", err, "port", p.Port) p.Error("webrtc port config error", "error", err, "port", p.Port)
return err return err
} }
switch v := ports.(type) { // 初始化端口映射
case TCPPort: p.portMapping = make(map[int]int)
tcpport := int(v)
// 如果有端口映射,存储映射关系
if portInfo.HasMapping() {
if portInfo.IsRange() {
// 端口范围映射
for i := 0; i <= portInfo.Ports[1]-portInfo.Ports[0]; i++ {
internalPort := portInfo.Ports[0] + i
var externalPort int
if portInfo.IsRangeMapping() {
// 映射端口也是范围
externalPort = portInfo.Map[0] + i
} else {
// 映射端口是单个端口
externalPort = portInfo.Map[0]
}
p.portMapping[internalPort] = externalPort
}
} else {
// 单端口映射
p.portMapping[portInfo.Ports[0]] = portInfo.Map[0]
}
p.Info("Port mapping configured", "mapping", p.portMapping)
}
// 根据协议类型进行配置
if portInfo.IsTCP() {
if portInfo.IsRange() {
// TCP端口范围这里可能需要特殊处理
p.Error("TCP port range not supported in current implementation")
return fmt.Errorf("TCP port range not supported")
} else {
// TCP单端口
tcpport := portInfo.Ports[0]
tcpl, err := net.ListenTCP("tcp", &net.TCPAddr{ tcpl, err := net.ListenTCP("tcp", &net.TCPAddr{
IP: net.IP{0, 0, 0, 0}, IP: net.IP{0, 0, 0, 0},
Port: tcpport, Port: tcpport,
@@ -324,20 +358,26 @@ func (p *WebRTCPlugin) configurePort() error {
}) })
if err != nil { if err != nil {
p.Error("webrtc listener tcp", "error", err) p.Error("webrtc listener tcp", "error", err)
return err
} }
p.SetDescription("tcp", fmt.Sprintf("%d", tcpport)) p.SetDescription("tcp", fmt.Sprintf("%d", tcpport))
p.Info("webrtc start listen", "port", tcpport) p.Info("webrtc start listen", "port", tcpport)
p.s.SetICETCPMux(NewICETCPMux(nil, tcpl, 4096)) p.s.SetICETCPMux(NewICETCPMux(nil, tcpl, 4096))
p.s.SetNetworkTypes([]NetworkType{NetworkTypeTCP4, NetworkTypeTCP6}) p.s.SetNetworkTypes([]NetworkType{NetworkTypeTCP4, NetworkTypeTCP6})
p.s.DisableSRTPReplayProtection(true) p.s.DisableSRTPReplayProtection(true)
case UDPRangePort: }
p.s.SetEphemeralUDPPortRange(uint16(v[0]), uint16(v[1])) } else {
p.SetDescription("udp", fmt.Sprintf("%d-%d", v[0], v[1])) // UDP配置
case UDPPort: if portInfo.IsRange() {
// 创建共享WEBRTC端口 默认9000 // UDP端口范围
p.s.SetEphemeralUDPPortRange(uint16(portInfo.Ports[0]), uint16(portInfo.Ports[1]))
p.SetDescription("udp", fmt.Sprintf("%d-%d", portInfo.Ports[0], portInfo.Ports[1]))
} else {
// UDP单端口
udpport := portInfo.Ports[0]
udpListener, err := net.ListenUDP("udp", &net.UDPAddr{ udpListener, err := net.ListenUDP("udp", &net.UDPAddr{
IP: net.IP{0, 0, 0, 0}, IP: net.IP{0, 0, 0, 0},
Port: int(v), Port: udpport,
}) })
p.OnDispose(func() { p.OnDispose(func() {
_ = udpListener.Close() _ = udpListener.Close()
@@ -346,11 +386,12 @@ func (p *WebRTCPlugin) configurePort() error {
p.Error("webrtc listener udp", "error", err) p.Error("webrtc listener udp", "error", err)
return err return err
} }
p.SetDescription("udp", fmt.Sprintf("%d", v)) p.SetDescription("udp", fmt.Sprintf("%d", udpport))
p.Info("webrtc start listen", "port", v) p.Info("webrtc start listen", "port", udpport)
p.s.SetICEUDPMux(NewICEUDPMux(nil, udpListener)) p.s.SetICEUDPMux(NewICEUDPMux(nil, udpListener))
p.s.SetNetworkTypes([]NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}) p.s.SetNetworkTypes([]NetworkType{NetworkTypeUDP4, NetworkTypeUDP6})
} }
}
return nil return nil
} }
@@ -368,9 +409,33 @@ func (p *WebRTCPlugin) CreatePC(sd SessionDescription, conf Configuration) (pc *
return return
} }
pc, err = api.NewPeerConnection(conf) pc, err = api.NewPeerConnection(conf)
if err == nil { if err != nil {
err = pc.SetRemoteDescription(sd) return
} }
// 如果有端口映射配置,记录 ICE 候选者信息以供调试
if len(p.portMapping) > 0 {
pc.OnICECandidate(func(candidate *ICECandidate) {
if candidate != nil {
// 记录端口映射信息(用于调试和监控)
if mappedPort, exists := p.portMapping[int(candidate.Port)]; exists {
p.Debug("ICE candidate with port mapping detected",
"original_port", candidate.Port,
"mapped_port", mappedPort,
"candidate_address", candidate.Address,
"candidate_type", candidate.Typ)
candidate.Port = uint16(mappedPort) // 更新候选者端口为映射后的端口
} else {
p.Debug("ICE candidate generated",
"port", candidate.Port,
"address", candidate.Address,
"type", candidate.Typ)
}
}
})
}
err = pc.SetRemoteDescription(sd)
return return
} }