From 2236ddeafd18b1fc4553c0dba8884d4ecd7ff9c9 Mon Sep 17 00:00:00 2001 From: Jerko Steiner Date: Tue, 7 Jul 2020 22:05:06 +0200 Subject: [PATCH] Add experimental support for ICE TCP See pion/ice issue and PR: - https://github.com/pion/ice/tree/issue-196 - https://github.com/pion/ice/pull/226 --- examples/ice-tcp/README.md | 22 +++++++++ examples/ice-tcp/index.html | 54 +++++++++++++++++++++ examples/ice-tcp/main.go | 97 +++++++++++++++++++++++++++++++++++++ go.mod | 2 +- go.sum | 16 ++++-- icecandidate.go | 43 ++++++++++++---- icecandidate_test.go | 43 ++++++++++++++++ icegatherer.go | 1 + settingengine.go | 7 +++ 9 files changed, 272 insertions(+), 13 deletions(-) create mode 100644 examples/ice-tcp/README.md create mode 100644 examples/ice-tcp/index.html create mode 100644 examples/ice-tcp/main.go diff --git a/examples/ice-tcp/README.md b/examples/ice-tcp/README.md new file mode 100644 index 00000000..1f505eaf --- /dev/null +++ b/examples/ice-tcp/README.md @@ -0,0 +1,22 @@ +# ice-tcp +ice-tcp demonstrates Pion WebRTC's ICE TCP abilities. + +## Instructions + +### Download ice-tcp +This example requires you to clone the repo since it is serving static HTML. + +``` +mkdir -p $GOPATH/src/github.com/pion +cd $GOPATH/src/github.com/pion +git clone https://github.com/pion/webrtc.git +cd webrtc/examples/ice-tcp +``` + +### Run ice-tcp +Execute `go run *.go` + +### Open the Web UI +Open [http://localhost:8080](http://localhost:8080). This will automatically start a PeerConnection. This page will now prints stats about the PeerConnection. The UDP candidates will be filtered out from the SDP. + +Congrats, you have used Pion WebRTC! Now start building something cool diff --git a/examples/ice-tcp/index.html b/examples/ice-tcp/index.html new file mode 100644 index 00000000..1b04cc76 --- /dev/null +++ b/examples/ice-tcp/index.html @@ -0,0 +1,54 @@ + + + ice-tcp + + + +
+ + +

ICE Connection States

+

+ +

Inbound DataChannel Messages

+
+ + + + diff --git a/examples/ice-tcp/main.go b/examples/ice-tcp/main.go new file mode 100644 index 00000000..a158b750 --- /dev/null +++ b/examples/ice-tcp/main.go @@ -0,0 +1,97 @@ +package main + +import ( + "encoding/json" + "fmt" + "net/http" + "time" + + "github.com/pion/webrtc/v3" +) + +var peerConnection *webrtc.PeerConnection //nolint + +func doSignaling(w http.ResponseWriter, r *http.Request) { + var err error + + if peerConnection == nil { + m := webrtc.MediaEngine{} + m.RegisterDefaultCodecs() + + settingEngine := webrtc.SettingEngine{} + + // Enable support only for TCP ICE candidates. + settingEngine.SetNetworkTypes([]webrtc.NetworkType{ + webrtc.NetworkTypeTCP4, + webrtc.NetworkTypeTCP6, + }) + settingEngine.SetICETCPPort(8443) + + api := webrtc.NewAPI( + webrtc.WithMediaEngine(m), + webrtc.WithSettingEngine(settingEngine), + ) + if peerConnection, err = api.NewPeerConnection(webrtc.Configuration{}); err != nil { + panic(err) + } + + // Set the handler for ICE connection state + // This will notify you when the peer has connected/disconnected + peerConnection.OnICEConnectionStateChange(func(connectionState webrtc.ICEConnectionState) { + fmt.Printf("ICE Connection State has changed: %s\n", connectionState.String()) + }) + + // Send the current time via a DataChannel to the remote peer every 3 seconds + peerConnection.OnDataChannel(func(d *webrtc.DataChannel) { + d.OnOpen(func() { + for range time.Tick(time.Second * 3) { + if err = d.SendText(time.Now().String()); err != nil { + panic(err) + } + } + }) + }) + } + + var offer webrtc.SessionDescription + if err = json.NewDecoder(r.Body).Decode(&offer); err != nil { + panic(err) + } + + if err = peerConnection.SetRemoteDescription(offer); err != nil { + panic(err) + } + + // Create channel that is blocked until ICE Gathering is complete + gatherComplete := webrtc.GatheringCompletePromise(peerConnection) + + answer, err := peerConnection.CreateAnswer(nil) + if err != nil { + panic(err) + } else if err = peerConnection.SetLocalDescription(answer); err != nil { + panic(err) + } + + // Block until ICE Gathering is complete, disabling trickle ICE + // we do this because we only can exchange one signaling message + // in a production application you should exchange ICE Candidates via OnICECandidate + <-gatherComplete + + response, err := json.Marshal(*peerConnection.LocalDescription()) + if err != nil { + panic(err) + } + + w.Header().Set("Content-Type", "application/json") + if _, err := w.Write(response); err != nil { + panic(err) + } +} + +func main() { + http.Handle("/", http.FileServer(http.Dir("."))) + http.HandleFunc("/doSignaling", doSignaling) + + fmt.Println("Open http://localhost:8080 to access this demo") + panic(http.ListenAndServe(":8080", nil)) +} diff --git a/go.mod b/go.mod index 9d9f0f7d..18000ba1 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.12 require ( github.com/pion/datachannel v1.4.17 github.com/pion/dtls/v2 v2.0.1 - github.com/pion/ice/v2 v2.0.0-rc.5 + github.com/pion/ice/v2 v2.0.0-rc.6 github.com/pion/logging v0.2.2 github.com/pion/quic v0.1.1 github.com/pion/randutil v0.1.0 diff --git a/go.sum b/go.sum index dd200c55..a1d315d1 100644 --- a/go.sum +++ b/go.sum @@ -31,20 +31,22 @@ github.com/pion/datachannel v1.4.17 h1:8CChK5VrJoGrwKCysoTscoWvshCAFpUkgY11Tqgz5 github.com/pion/datachannel v1.4.17/go.mod h1:+vPQfypU9vSsyPXogYj1hBThWQ6MNXEQoQAzxoPvjYM= github.com/pion/dtls/v2 v2.0.1 h1:ddE7+V0faYRbyh4uPsRZ2vLdRrjVZn+wmCfI7jlBfaA= github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U= -github.com/pion/ice/v2 v2.0.0-rc.5 h1:HSSnTn3QEBgRgxwGj/kiI4iBgseNWpTQpb9GZZfLBcY= -github.com/pion/ice/v2 v2.0.0-rc.5/go.mod h1:qfkp2BfgVTocUA3C9W559kFzW3IeCZxGplCIHAMyBZs= +github.com/pion/ice/v2 v2.0.0-rc.3 h1:GvQ6nMGIGz7GltCUC9EU0m9JyQMan2vbifO4i8Y6T6A= +github.com/pion/ice/v2 v2.0.0-rc.3/go.mod h1:5sP3yQ8Kd/azvPS4UrVTSgs/p5jfXMy3Ft2dQZBWyI8= +github.com/pion/ice/v2 v2.0.0-rc.6 h1:Jz88W1iXzHBYJG6I5QbRTm+xuKD1vbgUW+NP1MtUPAg= +github.com/pion/ice/v2 v2.0.0-rc.6/go.mod h1:VvpoDXwdierv9sPB8LAV3+T33ncCt0IG2NeI+CZYmTg= github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/mdns v0.0.4 h1:O4vvVqr4DGX63vzmO6Fw9vpy3lfztVWHGCQfyw0ZLSY= github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0= github.com/pion/quic v0.1.1 h1:D951FV+TOqI9A0rTF7tHx0Loooqz+nyzjEyj8o3PuMA= github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k= -github.com/pion/randutil v0.0.0 h1:aLWLVhTG2jzoD25F0OlW6nXvXrjoGwiXq2Sz7j7NzL0= github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= github.com/pion/rtcp v1.2.3 h1:2wrhKnqgSz91Q5nzYTO07mQXztYPtxL8a0XOss4rJqA= github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I= +github.com/pion/rtp v1.5.4/go.mod h1:bg60AL5GotNOlYZsqycbhDtEV3TkfbpXG0KBiUq29Mg= github.com/pion/rtp v1.5.5 h1:WTqWdmBuIj+luh8Wg6XVX+w7OytZHAIgtC7uSvgEl9Y= github.com/pion/rtp v1.5.5/go.mod h1:bg60AL5GotNOlYZsqycbhDtEV3TkfbpXG0KBiUq29Mg= github.com/pion/rtp v1.6.0 h1:4Ssnl/T5W2LzxHj9ssYpGVEQh3YYhQFNVmSWO88MMwk= @@ -53,10 +55,15 @@ github.com/pion/sctp v1.7.6 h1:8qZTdJtbKfAns/Hv5L0PAj8FyXcsKhMH1pKUCGisQg4= github.com/pion/sctp v1.7.6/go.mod h1:ichkYQ5tlgCQwEwvgfdcAolqx1nHbYCxo4D7zK/K0X8= github.com/pion/sctp v1.7.7 h1:6KVHBstRFV9+2si2B8H39CUpNn03oQ9yk/3dJ1TnkOs= github.com/pion/sctp v1.7.7/go.mod h1:E0K0acHLowZ2Ua21lHlQe4pHJoRzMU0HXqZVQEk061k= +github.com/pion/sdp/v2 v2.3.9 h1:KQMzypCMOcbHnx20t2r/Kuh9rKqWBa7RVy2tZ8Zk2MA= +github.com/pion/sdp/v2 v2.3.9/go.mod h1:sbxACjjlmwAgXMk0Qqw9uzFaazLIdPv4m0mIreLzPVk= github.com/pion/sdp/v2 v2.4.0 h1:luUtaETR5x2KNNpvEMv/r4Y+/kzImzbz4Lm1z8eQNQI= github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E= +github.com/pion/srtp v1.3.4 h1:idh+9/W7tLOsHjcYYketIPSShb9k2Dz+RVrqyCm2LQE= +github.com/pion/srtp v1.3.4/go.mod h1:M3+LQiqLfVcV/Jo46KYJ3z9PP8DjmGPW8fUOQrF6q/M= github.com/pion/srtp v1.4.0 h1:Qg/RYeCOY59fpjaHgAaybj+Wdu7EBSmrqWqlb0hjrdE= github.com/pion/srtp v1.4.0/go.mod h1:LSHkbwXr484DujfzX9bY1PsoQbAqDO+WMKd1KBq5yW0= +github.com/pion/stun v0.3.3/go.mod h1:xrCld6XM+6GWDZdvjPlLMsTU21rNxnO6UO8XsAvHr/M= github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE= @@ -65,8 +72,11 @@ github.com/pion/transport v0.10.0 h1:9M12BSneJm6ggGhJyWpDveFOstJsTiQjkLf4M44rm80 github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE= github.com/pion/transport v0.10.1 h1:2W+yJT+0mOQ160ThZYUx5Zp2skzshiNgxrNE9GUfhJM= github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A= +github.com/pion/turn/v2 v2.0.3 h1:SJUUIbcPoehlyZgMyIUbBBDhI03sBx32x3JuSIBKBWA= +github.com/pion/turn/v2 v2.0.3/go.mod h1:kl1hmT3NxcLynpXVnwJgObL8C9NaCyPTeqI2DcCpSZs= github.com/pion/turn/v2 v2.0.4 h1:oDguhEv2L/4rxwbL9clGLgtzQPjtuZwCdoM7Te8vQVk= github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= diff --git a/icecandidate.go b/icecandidate.go index e4d70acb..4807bb92 100644 --- a/icecandidate.go +++ b/icecandidate.go @@ -19,6 +19,7 @@ type ICECandidate struct { Component uint16 `json:"component"` RelatedAddress string `json:"relatedAddress"` RelatedPort uint16 `json:"relatedPort"` + TCPType string `json:"tcpType"` } // Conversion for package ice @@ -56,6 +57,7 @@ func newICECandidateFromICE(i ice.Candidate) (ICECandidate, error) { Port: uint16(i.Port()), Component: i.Component(), Typ: typ, + TCPType: i.TCPType().String(), } if i.RelatedAddress() != nil { @@ -76,6 +78,7 @@ func (c ICECandidate) toICE() (ice.Candidate, error) { Address: c.Address, Port: int(c.Port), Component: c.Component, + TCPType: ice.NewTCPType(c.TCPType), } return ice.NewCandidateHost(&config) case ICECandidateTypeSrflx: @@ -140,16 +143,26 @@ func (c ICECandidate) String() string { } func iceCandidateToSDP(c ICECandidate) sdp.ICECandidate { + var extensions []sdp.ICECandidateAttribute + + if c.Protocol == ICEProtocolTCP && c.TCPType != "" { + extensions = append(extensions, sdp.ICECandidateAttribute{ + Key: "tcptype", + Value: c.TCPType, + }) + } + return sdp.ICECandidate{ - Foundation: c.Foundation, - Priority: c.Priority, - Address: c.Address, - Protocol: c.Protocol.String(), - Port: c.Port, - Component: c.Component, - Typ: c.Typ.String(), - RelatedAddress: c.RelatedAddress, - RelatedPort: c.RelatedPort, + Foundation: c.Foundation, + Priority: c.Priority, + Address: c.Address, + Protocol: c.Protocol.String(), + Port: c.Port, + Component: c.Component, + Typ: c.Typ.String(), + RelatedAddress: c.RelatedAddress, + RelatedPort: c.RelatedPort, + ExtensionAttributes: extensions, } } @@ -162,6 +175,17 @@ func newICECandidateFromSDP(c sdp.ICECandidate) (ICECandidate, error) { if err != nil { return ICECandidate{}, err } + + var tcpType string + if protocol == ICEProtocolTCP { + for _, attr := range c.ExtensionAttributes { + if attr.Key == "tcptype" { + tcpType = attr.Value + break + } + } + } + return ICECandidate{ Foundation: c.Foundation, Priority: c.Priority, @@ -172,6 +196,7 @@ func newICECandidateFromSDP(c sdp.ICECandidate) (ICECandidate, error) { Typ: typ, RelatedAddress: c.RelatedAddress, RelatedPort: c.RelatedPort, + TCPType: tcpType, }, nil } diff --git a/icecandidate_test.go b/icecandidate_test.go index b5a00766..c2b014b0 100644 --- a/icecandidate_test.go +++ b/icecandidate_test.go @@ -4,7 +4,9 @@ import ( "testing" "github.com/pion/ice/v2" + "github.com/pion/sdp/v2" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestICECandidate_Convert(t *testing.T) { @@ -129,6 +131,47 @@ func TestICECandidate_Convert(t *testing.T) { } } +func TestICECandidate_ConvertTCP(t *testing.T) { + candidate := ICECandidate{ + Foundation: "foundation", + Priority: 128, + Address: "1.0.0.1", + Protocol: ICEProtocolTCP, + Port: 1234, + Typ: ICECandidateTypeHost, + Component: 1, + TCPType: "passive", + } + + got, err := candidate.toICE() + require.NoError(t, err) + + want, err := ice.NewCandidateHost(&ice.CandidateHostConfig{ + CandidateID: got.ID(), + Address: "1.0.0.1", + Component: 1, + Network: "tcp", + Port: 1234, + TCPType: ice.TCPTypePassive, + }) + require.NoError(t, err) + + assert.Equal(t, want, got) + + sdpCandidate := iceCandidateToSDP(candidate) + assert.Equal(t, []sdp.ICECandidateAttribute{ + { + Key: "tcptype", + Value: "passive", + }, + }, sdpCandidate.ExtensionAttributes) + + candidate2, err := newICECandidateFromSDP(sdpCandidate) + require.NoError(t, err) + + assert.Equal(t, candidate, candidate2) +} + func TestConvertTypeFromICE(t *testing.T) { t.Run("host", func(t *testing.T) { ct, err := convertTypeFromICE(ice.CandidateTypeHost) diff --git a/icegatherer.go b/icegatherer.go index 80cbb843..fb05d293 100644 --- a/icegatherer.go +++ b/icegatherer.go @@ -109,6 +109,7 @@ func (g *ICEGatherer) createAgent() error { MulticastDNSHostName: g.api.settingEngine.candidates.MulticastDNSHostName, LocalUfrag: g.api.settingEngine.candidates.UsernameFragment, LocalPwd: g.api.settingEngine.candidates.Password, + TCPListenPort: g.api.settingEngine.iceTCPPort, } requestedNetworkTypes := g.api.settingEngine.candidates.ICENetworkTypes diff --git a/settingengine.go b/settingengine.go index 316079c3..2adf2d11 100644 --- a/settingengine.go +++ b/settingengine.go @@ -56,6 +56,7 @@ type SettingEngine struct { disableSRTCPReplayProtection bool vnet *vnet.Net LoggerFactory logging.LoggerFactory + iceTCPPort int } // DetachDataChannels enables detaching data channels. When enabled @@ -242,6 +243,12 @@ func (e *SettingEngine) SetSDPMediaLevelFingerprints(sdpMediaLevelFingerprints b e.sdpMediaLevelFingerprints = sdpMediaLevelFingerprints } +// SetICETCPPort to a non-zero value enables ICE-TCP listener. This API is experimental and +// is likely to change in the future. +func (e *SettingEngine) SetICETCPPort(port int) { + e.iceTCPPort = port +} + // AddSDPExtensions adds available and offered extensions for media type. // // Ext IDs are optional and generated if you do not provide them