diff --git a/pkg/port.go b/pkg/port.go index f18c77c..539d1c7 100644 --- a/pkg/port.go +++ b/pkg/port.go @@ -13,6 +13,7 @@ type ( Port struct { Protocol string Ports [2]int + Map [2]int // 映射端口范围,通常用于 NAT 或端口转发 } IPort interface { IsTCP() bool @@ -22,10 +23,23 @@ type ( ) func (p Port) String() string { + var result string 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 { @@ -40,6 +54,36 @@ func (p Port) IsRange() bool { 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) { var port Port port, err = ParsePort(conf) @@ -58,10 +102,84 @@ func ParsePort2(conf string) (ret any, err error) { 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) { - var port string + var port, mapPort string 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 { min, err = strconv.Atoi(r[0]) if err != nil { @@ -76,7 +194,12 @@ func ParsePort(conf string) (ret Port, err error) { } else { 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 } return diff --git a/pkg/port_test.go b/pkg/port_test.go new file mode 100644 index 0000000..28ee6dd --- /dev/null +++ b/pkg/port_test.go @@ -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) + } + } + }) + } +} diff --git a/plugin/webrtc/index.go b/plugin/webrtc/index.go index 6a9ca01..db50715 100644 --- a/plugin/webrtc/index.go +++ b/plugin/webrtc/index.go @@ -37,12 +37,13 @@ var ( type WebRTCPlugin struct { m7s.Plugin - ICEServers []ICEServer `desc:"ice服务器配置"` - Port string `default:"tcp:9000" desc:"监听端口"` - PLI time.Duration `default:"2s" desc:"发送PLI请求间隔"` // 视频流丢包后,发送PLI请求 - EnableDC bool `default:"true" desc:"是否启用DataChannel"` // 在不支持编码格式的情况下是否启用DataChannel传输 - MimeType []string `desc:"MimeType过滤列表,为空则不过滤"` // MimeType过滤列表,支持的格式如:video/H264, audio/opus - s SettingEngine + ICEServers []ICEServer `desc:"ice服务器配置"` + Port string `default:"tcp:9000" desc:"监听端口"` + PLI time.Duration `default:"2s" desc:"发送PLI请求间隔"` // 视频流丢包后,发送PLI请求 + EnableDC bool `default:"true" desc:"是否启用DataChannel"` // 在不支持编码格式的情况下是否启用DataChannel传输 + MimeType []string `desc:"MimeType过滤列表,为空则不过滤"` // MimeType过滤列表,支持的格式如:video/H264, audio/opus + s SettingEngine + portMapping map[int]int // 内部端口到外部端口的映射 } func (p *WebRTCPlugin) RegisterHandler() map[string]http.HandlerFunc { @@ -306,50 +307,90 @@ func (p *WebRTCPlugin) initSettingEngine() error { // configurePort 配置端口设置 func (p *WebRTCPlugin) configurePort() error { - ports, err := ParsePort2(p.Port) + // 使用 ParsePort 而不是 ParsePort2 来获取端口映射信息 + portInfo, err := ParsePort(p.Port) if err != nil { p.Error("webrtc port config error", "error", err, "port", p.Port) return err } - switch v := ports.(type) { - case TCPPort: - tcpport := int(v) - tcpl, err := net.ListenTCP("tcp", &net.TCPAddr{ - IP: net.IP{0, 0, 0, 0}, - Port: tcpport, - }) - p.OnDispose(func() { - _ = tcpl.Close() - }) - if err != nil { - p.Error("webrtc listener tcp", "error", err) + // 初始化端口映射 + p.portMapping = make(map[int]int) + + // 如果有端口映射,存储映射关系 + 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.SetDescription("tcp", fmt.Sprintf("%d", tcpport)) - p.Info("webrtc start listen", "port", tcpport) - p.s.SetICETCPMux(NewICETCPMux(nil, tcpl, 4096)) - p.s.SetNetworkTypes([]NetworkType{NetworkTypeTCP4, NetworkTypeTCP6}) - p.s.DisableSRTPReplayProtection(true) - case UDPRangePort: - p.s.SetEphemeralUDPPortRange(uint16(v[0]), uint16(v[1])) - p.SetDescription("udp", fmt.Sprintf("%d-%d", v[0], v[1])) - case UDPPort: - // 创建共享WEBRTC端口 默认9000 - udpListener, err := net.ListenUDP("udp", &net.UDPAddr{ - IP: net.IP{0, 0, 0, 0}, - Port: int(v), - }) - p.OnDispose(func() { - _ = udpListener.Close() - }) - if err != nil { - p.Error("webrtc listener udp", "error", err) - return err + 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{ + IP: net.IP{0, 0, 0, 0}, + Port: tcpport, + }) + p.OnDispose(func() { + _ = tcpl.Close() + }) + if err != nil { + p.Error("webrtc listener tcp", "error", err) + return err + } + p.SetDescription("tcp", fmt.Sprintf("%d", tcpport)) + p.Info("webrtc start listen", "port", tcpport) + p.s.SetICETCPMux(NewICETCPMux(nil, tcpl, 4096)) + p.s.SetNetworkTypes([]NetworkType{NetworkTypeTCP4, NetworkTypeTCP6}) + p.s.DisableSRTPReplayProtection(true) + } + } else { + // UDP配置 + if portInfo.IsRange() { + // 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{ + IP: net.IP{0, 0, 0, 0}, + Port: udpport, + }) + p.OnDispose(func() { + _ = udpListener.Close() + }) + if err != nil { + p.Error("webrtc listener udp", "error", err) + return err + } + p.SetDescription("udp", fmt.Sprintf("%d", udpport)) + p.Info("webrtc start listen", "port", udpport) + p.s.SetICEUDPMux(NewICEUDPMux(nil, udpListener)) + p.s.SetNetworkTypes([]NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}) } - p.SetDescription("udp", fmt.Sprintf("%d", v)) - p.Info("webrtc start listen", "port", v) - p.s.SetICEUDPMux(NewICEUDPMux(nil, udpListener)) - p.s.SetNetworkTypes([]NetworkType{NetworkTypeUDP4, NetworkTypeUDP6}) } return nil @@ -368,9 +409,33 @@ func (p *WebRTCPlugin) CreatePC(sd SessionDescription, conf Configuration) (pc * return } pc, err = api.NewPeerConnection(conf) - if err == nil { - err = pc.SetRemoteDescription(sd) + if err != nil { + 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 }