diff --git a/internal/ngrok/ngrok.go b/internal/ngrok/ngrok.go index c73dc124..6b945668 100644 --- a/internal/ngrok/ngrok.go +++ b/internal/ngrok/ngrok.go @@ -50,7 +50,7 @@ func Init() { log.Info().Str("addr", address).Msg("[ngrok] add external candidate for WebRTC") - webrtc.AddCandidate(address) + webrtc.AddCandidate(address, "tcp") } } }) diff --git a/internal/webrtc/README.md b/internal/webrtc/README.md index 48e9c955..3511d602 100644 --- a/internal/webrtc/README.md +++ b/internal/webrtc/README.md @@ -1,3 +1,17 @@ +## Config + +- supported networks: IPv4 (default), IPv6, both +- supported TCP: fixed port (default), disabled +- supported UDP: random port (default), fixed port + +| Config examples | IPv4 | IPv6 | TCP | UDP | +|------------------------------|------|------|-------|--------| +| `listen: "0.0.0.0:8555/tcp"` | yes | no | fixed | random | +| `listen: "0.0.0.0:8555/udp"` | yes | no | no | fixed | +| `listen: "[::]:8555/tcp"` | no | yes | fixed | random | +| `listen: ":8555"` | yes | yes | fixed | fixed | +| `listen: ""` | yes | yes | no | random | + ## Userful links - https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html diff --git a/internal/webrtc/candidates.go b/internal/webrtc/candidates.go index 4fdbce2a..65f2e213 100644 --- a/internal/webrtc/candidates.go +++ b/internal/webrtc/candidates.go @@ -1,58 +1,66 @@ package webrtc import ( + "net" + "github.com/AlexxIT/go2rtc/internal/api/ws" "github.com/AlexxIT/go2rtc/pkg/webrtc" "github.com/pion/sdp/v3" - "strconv" - "strings" ) type Address struct { - Host string - Port int + Host string + Port string + Network string + Offset int } -var addresses []Address - -func AddCandidate(address string) { - var port int - - // try to get port from address string - if i := strings.LastIndexByte(address, ':'); i > 0 { - if v, _ := strconv.Atoi(address[i+1:]); v != 0 { - address = address[:i] - port = v +func (a *Address) Marshal() string { + host := a.Host + if host == "stun" { + ip, err := webrtc.GetCachedPublicIP() + if err != nil { + return "" } + host = ip.String() } - // use default WebRTC port - if port == 0 { - port, _ = strconv.Atoi(Port) + switch a.Network { + case "udp": + return webrtc.CandidateManualHostUDP(host, a.Port, a.Offset) + case "tcp": + return webrtc.CandidateManualHostTCPPassive(host, a.Port, a.Offset) } - addresses = append(addresses, Address{Host: address, Port: port}) + return "" +} + +var addresses []*Address + +func AddCandidate(address, network string) { + host, port, err := net.SplitHostPort(address) + if err != nil { + return + } + + offset := -1 - len(addresses) // every next candidate will have a lower priority + + switch network { + case "tcp", "udp": + addresses = append(addresses, &Address{host, port, network, offset}) + default: + addresses = append( + addresses, &Address{host, port, "udp", offset}, &Address{host, port, "tcp", offset}, + ) + } } func GetCandidates() (candidates []string) { for _, address := range addresses { - // using stun server for receive public IP-address - if address.Host == "stun" { - ip, err := webrtc.GetCachedPublicIP() - if err != nil { - continue - } - // this is a copy, original host unchanged - address.Host = ip.String() + if candidate := address.Marshal(); candidate != "" { + candidates = append(candidates, candidate) } - - candidates = append( - candidates, - webrtc.CandidateManualHostUDP(address.Host, address.Port), - webrtc.CandidateManualHostTCPPassive(address.Host, address.Port), - ) } - return } diff --git a/internal/webrtc/kinesis.go b/internal/webrtc/kinesis.go index 4ec733e1..7ef9d9bb 100644 --- a/internal/webrtc/kinesis.go +++ b/internal/webrtc/kinesis.go @@ -55,7 +55,7 @@ func kinesisClient(rawURL string, query url.Values, desc string) (core.Producer, defer conn.Close() // 3. Create Peer Connection - api, err := webrtc.NewAPI("") + api, err := webrtc.NewAPI() if err != nil { return nil, err } diff --git a/internal/webrtc/openipc.go b/internal/webrtc/openipc.go index a6fb2eae..8055ea91 100644 --- a/internal/webrtc/openipc.go +++ b/internal/webrtc/openipc.go @@ -33,7 +33,7 @@ func openIPCClient(rawURL string, query url.Values) (core.Producer, error) { defer conn.Close() // 3. Create Peer Connection - api, err := webrtc.NewAPI("") + api, err := webrtc.NewAPI() if err != nil { return nil, err } diff --git a/internal/webrtc/webrtc.go b/internal/webrtc/webrtc.go index 2c4ec9d9..a54326bb 100644 --- a/internal/webrtc/webrtc.go +++ b/internal/webrtc/webrtc.go @@ -2,7 +2,7 @@ package webrtc import ( "errors" - "net" + "strings" "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/api/ws" @@ -32,10 +32,20 @@ func Init() { log = app.GetLogger("webrtc") - address := cfg.Mod.Listen + address, network, _ := strings.Cut(cfg.Mod.Listen, "/") + + var candidateHost []string + for _, candidate := range cfg.Mod.Candidates { + if strings.HasPrefix(candidate, "host:") { + candidateHost = append(candidateHost, candidate[5:]) + continue + } + + AddCandidate(candidate, network) + } // create pionAPI with custom codecs list and custom network settings - serverAPI, err := webrtc.NewAPI(address) + serverAPI, err := webrtc.NewServerAPI(address, network, candidateHost) if err != nil { log.Error().Err(err).Caller().Send() return @@ -46,9 +56,8 @@ func Init() { if address != "" { log.Info().Str("addr", address).Msg("[webrtc] listen") - _, Port, _ = net.SplitHostPort(address) - clientAPI, _ = webrtc.NewAPI("") + clientAPI, _ = webrtc.NewAPI() } pionConf := pion.Configuration{ @@ -65,10 +74,6 @@ func Init() { } } - for _, candidate := range cfg.Mod.Candidates { - AddCandidate(candidate) - } - // async WebRTC server (two API versions) ws.HandleFunc("webrtc", asyncHandler) ws.HandleFunc("webrtc/offer", asyncHandler) @@ -81,7 +86,6 @@ func Init() { streams.HandleFunc("webrtc", streamsHandler) } -var Port string var log zerolog.Logger var PeerConnection func(active bool) (*pion.PeerConnection, error) diff --git a/pkg/hass/client.go b/pkg/hass/client.go index 5b9a227a..c1ed5b4b 100644 --- a/pkg/hass/client.go +++ b/pkg/hass/client.go @@ -2,10 +2,11 @@ package hass import ( "errors" + "net/url" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" - "net/url" ) type Client struct { @@ -48,7 +49,7 @@ func NewClient(rawURL string) (*Client, error) { defer hassAPI.Close() // 2. Create WebRTC client - rtcAPI, err := webrtc.NewAPI("") + rtcAPI, err := webrtc.NewAPI() if err != nil { return nil, err } diff --git a/pkg/nest/client.go b/pkg/nest/client.go index 5e8cad3a..b2b0c964 100644 --- a/pkg/nest/client.go +++ b/pkg/nest/client.go @@ -2,10 +2,11 @@ package nest import ( "errors" + "net/url" + "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/webrtc" pion "github.com/pion/webrtc/v3" - "net/url" ) type Client struct { @@ -34,7 +35,7 @@ func NewClient(rawURL string) (*Client, error) { return nil, err } - rtcAPI, err := webrtc.NewAPI("") + rtcAPI, err := webrtc.NewAPI() if err != nil { return nil, err } diff --git a/pkg/roborock/client.go b/pkg/roborock/client.go index 1d838710..39caab88 100644 --- a/pkg/roborock/client.go +++ b/pkg/roborock/client.go @@ -90,7 +90,7 @@ func (c *Client) Connect() error { } // 4. Create Peer Connection - api, err := webrtc.NewAPI("") + api, err := webrtc.NewAPI() if err != nil { return err } diff --git a/pkg/webrtc/api.go b/pkg/webrtc/api.go index edb2daf6..6c7ecada 100644 --- a/pkg/webrtc/api.go +++ b/pkg/webrtc/api.go @@ -2,9 +2,7 @@ package webrtc import ( "net" - "strings" - "github.com/pion/ice/v2" "github.com/pion/interceptor" "github.com/pion/webrtc/v3" ) @@ -13,7 +11,11 @@ import ( // https://ffmpeg.org/ffmpeg-all.html#Muxer const ReceiveMTU = 1472 -func NewAPI(address string) (*webrtc.API, error) { +func NewAPI() (*webrtc.API, error) { + return NewServerAPI("", "", nil) +} + +func NewServerAPI(address, network string, candidateHost []string) (*webrtc.API, error) { // for debug logs add to env: `PION_LOG_DEBUG=all` m := &webrtc.MediaEngine{} //if err := m.RegisterDefaultCodecs(); err != nil { @@ -35,34 +37,33 @@ func NewAPI(address string) (*webrtc.API, error) { return name != "hassio" && name != "docker0" }) - // disable mDNS listener - s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled) - - // UDP6 may have problems with DNS resolving for STUN servers - s.SetNetworkTypes([]webrtc.NetworkType{ - webrtc.NetworkTypeUDP4, webrtc.NetworkTypeTCP4, - webrtc.NetworkTypeUDP6, webrtc.NetworkTypeTCP6, - }) - // fix https://github.com/pion/webrtc/pull/2407 s.SetDTLSInsecureSkipHelloVerify(true) s.SetReceiveMTU(ReceiveMTU) + s.SetNAT1To1IPs(candidateHost, webrtc.ICECandidateTypeHost) + + // by default enable IPv4 + IPv6 modes + s.SetNetworkTypes([]webrtc.NetworkType{ + webrtc.NetworkTypeUDP4, webrtc.NetworkTypeTCP4, + webrtc.NetworkTypeUDP6, webrtc.NetworkTypeTCP6, + }) + if address != "" { - address, network, _ := strings.Cut(address, "/") - if network == "" || network == "udp" { - if ln, err := net.ListenPacket("udp", address); err == nil { - udpMux := webrtc.NewICEUDPMux(nil, ln) - s.SetICEUDPMux(udpMux) - } - } if network == "" || network == "tcp" { if ln, err := net.Listen("tcp", address); err == nil { tcpMux := webrtc.NewICETCPMux(nil, ln, 8) s.SetICETCPMux(tcpMux) } } + + if network == "" || network == "udp" { + if ln, err := net.ListenPacket("udp", address); err == nil { + udpMux := webrtc.NewICEUDPMux(nil, ln) + s.SetICEUDPMux(udpMux) + } + } } return webrtc.NewAPI( diff --git a/pkg/webrtc/client_test.go b/pkg/webrtc/client_test.go index 82b52b25..45c8c88d 100644 --- a/pkg/webrtc/client_test.go +++ b/pkg/webrtc/client_test.go @@ -10,7 +10,7 @@ import ( ) func TestClient(t *testing.T) { - api, err := NewAPI("") + api, err := NewAPI() require.Nil(t, err) pc, err := api.NewPeerConnection(webrtc.Configuration{}) diff --git a/pkg/webrtc/helpers.go b/pkg/webrtc/helpers.go index ed1daf1e..9a494792 100644 --- a/pkg/webrtc/helpers.go +++ b/pkg/webrtc/helpers.go @@ -260,17 +260,15 @@ func MimeType(codec *core.Codec) string { // for server reflexive candidates, 110 for peer reflexive candidates, // and 0 for relayed candidates. -// We use new priority 120 for Manual Host. It is lower than real Host, -// but more then any other candidates. +const PriorityTypeHostUDP = (1 << 24) * int(126) +const PriorityTypeHostTCP = (1 << 24) * int(126-27) +const PriorityLocalUDP = (1 << 8) * int(65535) +const PriorityLocalTCPPassive = (1 << 8) * int((1<<13)*4+8191) +const PriorityComponentRTP = 1 * int(256-ice.ComponentRTP) -const PriorityManualHost = (1 << 24) * uint32(120) -const PriorityLocalUDP = (1 << 8) * uint32(65535) -const PriorityLocalTCPPassive = (1 << 8) * uint32((1<<13)*4+8191) -const PriorityComponentRTP = uint32(256 - ice.ComponentRTP) - -func CandidateManualHostUDP(host string, port int) string { +func CandidateManualHostUDP(host, port string, offset int) string { foundation := crc32.ChecksumIEEE([]byte("host" + host + "udp4")) - priority := PriorityManualHost + PriorityLocalUDP + PriorityComponentRTP + priority := PriorityTypeHostUDP + PriorityLocalUDP + PriorityComponentRTP + offset // 1. Foundation // 2. Component, always 1 because RTP @@ -279,19 +277,15 @@ func CandidateManualHostUDP(host string, port int) string { // 5. Host - IP4 or IP6 or domain name // 6. Port // 7. typ host - return fmt.Sprintf( - "candidate:%d 1 udp %d %s %d typ host", - foundation, priority, host, port, - ) + return fmt.Sprintf("candidate:%d 1 udp %d %s %s typ host", foundation, priority, host, port) } -func CandidateManualHostTCPPassive(address string, port int) string { - foundation := crc32.ChecksumIEEE([]byte("host" + address + "tcp4")) - priority := PriorityManualHost + PriorityLocalTCPPassive + PriorityComponentRTP +func CandidateManualHostTCPPassive(host, port string, offset int) string { + foundation := crc32.ChecksumIEEE([]byte("host" + host + "tcp4")) + priority := PriorityTypeHostTCP + PriorityLocalTCPPassive + PriorityComponentRTP + offset return fmt.Sprintf( - "candidate:%d 1 tcp %d %s %d typ host tcptype passive", - foundation, priority, address, port, + "candidate:%d 1 tcp %d %s %s typ host tcptype passive", foundation, priority, host, port, ) }