mirror of
https://github.com/pion/webrtc.git
synced 2025-09-27 03:25:58 +08:00
Add ice-proxy example
This commit is contained in:
@@ -32,6 +32,7 @@ For more full featured examples that use 3rd party libraries see our **[example-
|
||||
* [ICE Restart](ice-restart) Example ice-restart demonstrates how a WebRTC connection can roam between networks. This example restarts ICE in a loop and prints the new addresses it uses each time.
|
||||
* [ICE Single Port](ice-single-port) Example ice-single-port demonstrates how multiple WebRTC connections can be served from a single port. By default Pion listens on a new port for every PeerConnection. Pion can be configured to use a single port for multiple connections.
|
||||
* [ICE TCP](ice-tcp) Example ice-tcp demonstrates how a WebRTC connection can be made over TCP instead of UDP. By default Pion only does UDP. Pion can be configured to use a TCP port, and this TCP port can be used for many connections.
|
||||
* [ICE Proxy](ice-proxy) Example ice-proxy demonstrates how to use a proxy for TURN connections.
|
||||
* [Trickle ICE](trickle-ice) Example trickle-ice example demonstrates Pion WebRTC's Trickle ICE APIs. This is important to use since it allows ICE Gathering and Connecting to happen concurrently.
|
||||
* [VNet](vnet) Example vnet demonstrates Pion's network virtualisation library. This example connects two PeerConnections over a virtual network and prints statistics about the data traveling over it.
|
||||
|
||||
|
26
examples/ice-proxy/README.md
Normal file
26
examples/ice-proxy/README.md
Normal file
@@ -0,0 +1,26 @@
|
||||
# ICE Proxy
|
||||
`ice-proxy` demonstrates Pion WebRTC's capabilities for utilizing a proxy in WebRTC connections.
|
||||
|
||||
This proxy functionality is particularly useful when direct peer-to-peer communication is restricted, such as in environments with strict firewalls. It primarily leverages TURN (Traversal Using Relays around NAT) with TCP connections to enable communication with the outside world.
|
||||
|
||||
## Instructions
|
||||
|
||||
### Download ice-proxy
|
||||
The example is self-contained and requires no input.
|
||||
|
||||
```bash
|
||||
go install github.com/pion/webrtc/v4/examples/ice-proxy@latest
|
||||
```
|
||||
|
||||
### Run ice-proxy
|
||||
```bash
|
||||
ice-proxy
|
||||
```
|
||||
|
||||
Upon execution, four distinct entities will be launched:
|
||||
* `TURN Server`: This server facilitates relaying media traffic when direct communication between agents is not possible, simulating a scenario where peers are behind restrictive NATs.
|
||||
* `Proxy HTTP Server`: A straightforward HTTP proxy designed to forward all TCP traffic to a specified target.
|
||||
* `Offering Agent`: In a typical WebRTC setup, this would be a web browser. In this example, it's a simplified Pion client that initiates the WebRTC connection. This agent attempts direct communication with the answering agent.
|
||||
* `Answering Agent`: This typically represents a web server. In this demonstration, it's configured to use the TURN server, simulating a scenario where the agent is not directly reachable. This agent exclusively uses a relay connection via the TURN server, with a proxy acting as an intermediary between the agent and the TURN server.
|
||||
|
||||
|
117
examples/ice-proxy/answer.go
Normal file
117
examples/ice-proxy/answer.go
Normal file
@@ -0,0 +1,117 @@
|
||||
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// nolint:cyclop
|
||||
func setupAnsweringAgent() {
|
||||
// Create and start a simple HTTP proxy server.
|
||||
proxyURL := newHTTPProxy()
|
||||
// Create a proxy dialer that will use the created HTTP proxy.
|
||||
proxyDialer := newProxyDialer(proxyURL)
|
||||
|
||||
var settingEngine webrtc.SettingEngine
|
||||
// Set the ICEProxyDialer to use the proxy for TURN+TCP connections.
|
||||
settingEngine.SetICEProxyDialer(proxyDialer)
|
||||
api := webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine))
|
||||
|
||||
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
URLs: []string{turnServerURL},
|
||||
Username: turnUsername,
|
||||
Credential: turnPassword,
|
||||
},
|
||||
},
|
||||
// ICETransportPolicyRelay forces the connection to go through a TURN server.
|
||||
// This is required for the proxy to be used.
|
||||
ICETransportPolicy: webrtc.ICETransportPolicyRelay,
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Log peer connection and ICE connection state changes.
|
||||
peerConnection.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) {
|
||||
log.Printf("[Answerer] Peer Connection State has changed: %s", pcs.String())
|
||||
})
|
||||
peerConnection.OnICEConnectionStateChange(func(ics webrtc.ICEConnectionState) {
|
||||
log.Printf("[Answerer] ICE Connection State has changed: %s", ics.String())
|
||||
})
|
||||
|
||||
// Register a handler for when a data channel is created by the remote peer.
|
||||
peerConnection.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||
icePair, err := d.Transport().Transport().ICETransport().GetSelectedCandidatePair()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Log the chosen ICE candidate pair.
|
||||
log.Printf("[Answerer] New DataChannel %s, ICE pair: (%s)<->(%s)",
|
||||
d.Label(), icePair.Local.String(), icePair.Remote.String())
|
||||
// Register a handler to echo messages back to the sender.
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
if err := d.SendText(string(msg.Data)); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// HTTP handler that accepts an offer, creates an answer,
|
||||
// and sends it back to the offering agent.
|
||||
http.HandleFunc("/sdp", func(rw http.ResponseWriter, r *http.Request) {
|
||||
var sdp webrtc.SessionDescription
|
||||
if err := json.NewDecoder(r.Body).Decode(&sdp); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := peerConnection.SetRemoteDescription(sdp); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
|
||||
|
||||
answer, err := peerConnection.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
resp, err := json.Marshal(*peerConnection.LocalDescription())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if _, err := rw.Write(resp); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
|
||||
// Start an HTTP server to handle the SDP exchange from the offering agent.
|
||||
go func() {
|
||||
// The HTTP server is not gracefully shutdown in this example.
|
||||
// nolint:gosec
|
||||
err := http.ListenAndServe("localhost:8080", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
}
|
29
examples/ice-proxy/main.go
Normal file
29
examples/ice-proxy/main.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
// ice-proxy demonstrates Pion WebRTC's proxy abilities.
|
||||
package main
|
||||
|
||||
const (
|
||||
turnServerAddr = "localhost:17342"
|
||||
turnServerURL = "turn:" + turnServerAddr + "?transport=tcp"
|
||||
turnUsername = "turn_username"
|
||||
turnPassword = "turn_password"
|
||||
)
|
||||
|
||||
func main() {
|
||||
// Setup TURN server.
|
||||
turnServer := newTURNServer()
|
||||
defer turnServer.Close() // nolint:errcheck
|
||||
|
||||
// Setup answering agent with proxy and TURN.
|
||||
setupAnsweringAgent()
|
||||
// Setup offering agent with only direct communication.
|
||||
setupOfferingAgent()
|
||||
|
||||
// Block forever
|
||||
select {}
|
||||
}
|
100
examples/ice-proxy/offer.go
Normal file
100
examples/ice-proxy/offer.go
Normal file
@@ -0,0 +1,100 @@
|
||||
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/pion/webrtc/v4"
|
||||
)
|
||||
|
||||
// nolint:cyclop
|
||||
func setupOfferingAgent() {
|
||||
var settingEngine webrtc.SettingEngine
|
||||
// Allow loopback candidates.
|
||||
settingEngine.SetIncludeLoopbackCandidate(true)
|
||||
api := webrtc.NewAPI(webrtc.WithSettingEngine(settingEngine))
|
||||
|
||||
// Create a new RTCPeerConnection.
|
||||
peerConnection, err := api.NewPeerConnection(webrtc.Configuration{})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Log peer connection and ICE connection state changes.
|
||||
peerConnection.OnConnectionStateChange(func(pcs webrtc.PeerConnectionState) {
|
||||
log.Printf("[Offerer] Peer Connection State has changed: %s", pcs.String())
|
||||
})
|
||||
peerConnection.OnICEConnectionStateChange(func(ics webrtc.ICEConnectionState) {
|
||||
log.Printf("[Offerer] ICE Connection State has changed: %s", ics.String())
|
||||
})
|
||||
|
||||
// Create a data channel for measuring round-trip time.
|
||||
dc, err := peerConnection.CreateDataChannel("data-channel", nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
dc.OnOpen(func() {
|
||||
// Send the current time every 3 seconds.
|
||||
for range time.Tick(3 * time.Second) {
|
||||
if sendErr := dc.SendText(time.Now().Format(time.RFC3339Nano)); sendErr != nil {
|
||||
panic(sendErr)
|
||||
}
|
||||
}
|
||||
})
|
||||
dc.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
// Receive the echoed time from the remote agent and calculate the round-trip time.
|
||||
sendTime, parseErr := time.Parse(time.RFC3339Nano, string(msg.Data))
|
||||
if parseErr != nil {
|
||||
panic(parseErr)
|
||||
}
|
||||
log.Printf("[Offerer] Data channel round-trip time: %s", time.Since(sendTime))
|
||||
})
|
||||
|
||||
gatherComplete := webrtc.GatheringCompletePromise(peerConnection)
|
||||
|
||||
// Create an offer to send to the answering agent.
|
||||
offer, err := peerConnection.CreateOffer(nil)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err = peerConnection.SetLocalDescription(offer); 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
|
||||
|
||||
offerJSON, err := json.Marshal(*peerConnection.LocalDescription())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Send offer to the answering agent.
|
||||
// nolint:noctx
|
||||
resp, err := http.Post("http://localhost:8080/sdp", "application/json", bytes.NewBuffer(offerJSON))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer resp.Body.Close() // nolint:errcheck
|
||||
|
||||
// Receive answer and set remote description.
|
||||
var answer webrtc.SessionDescription
|
||||
if err = json.NewDecoder(resp.Body).Decode(&answer); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = peerConnection.SetRemoteDescription(answer); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
125
examples/ice-proxy/proxy.go
Normal file
125
examples/ice-proxy/proxy.go
Normal file
@@ -0,0 +1,125 @@
|
||||
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
)
|
||||
|
||||
var _ proxy.Dialer = &proxyDialer{}
|
||||
|
||||
type proxyDialer struct {
|
||||
proxyAddr string
|
||||
}
|
||||
|
||||
func newProxyDialer(u *url.URL) proxy.Dialer {
|
||||
if u.Scheme != "http" {
|
||||
panic("unsupported proxy scheme")
|
||||
}
|
||||
|
||||
return &proxyDialer{
|
||||
proxyAddr: u.Host,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *proxyDialer) Dial(network, addr string) (net.Conn, error) {
|
||||
if network != "tcp" && network != "tcp4" && network != "tcp6" {
|
||||
panic("unsupported proxy network type")
|
||||
}
|
||||
|
||||
conn, err := net.Dial(network, d.proxyAddr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Create a CONNECT request to the proxy with target address.
|
||||
req := &http.Request{
|
||||
Method: http.MethodConnect,
|
||||
URL: &url.URL{Host: addr},
|
||||
Header: http.Header{
|
||||
"Proxy-Connection": []string{"Keep-Alive"},
|
||||
},
|
||||
}
|
||||
|
||||
err = req.Write(conn)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
resp, err := http.ReadResponse(bufio.NewReader(conn), req)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
if err := resp.Body.Close(); err != nil {
|
||||
log.Printf("close response body: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
panic("unexpected proxy status code: " + resp.Status)
|
||||
}
|
||||
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
func newHTTPProxy() *url.URL {
|
||||
listener, err := net.Listen("tcp", "localhost:0")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go func() {
|
||||
for {
|
||||
conn, err := listener.Accept()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
go proxyHandleConn(conn)
|
||||
}
|
||||
}()
|
||||
|
||||
return &url.URL{
|
||||
Scheme: "http",
|
||||
Host: fmt.Sprintf("localhost:%d", listener.Addr().(*net.TCPAddr).Port), // nolint:forcetypeassert
|
||||
}
|
||||
}
|
||||
|
||||
func proxyHandleConn(clientConn net.Conn) {
|
||||
// Read the request from the client
|
||||
req, err := http.ReadRequest(bufio.NewReader(clientConn))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if req.Method != http.MethodConnect {
|
||||
panic("unexpected request method: " + req.Method)
|
||||
}
|
||||
|
||||
// Establish a connection to the target server
|
||||
targetConn, err := net.Dial("tcp", req.URL.Host)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Answer to the client with a 200 OK response
|
||||
if _, err := clientConn.Write([]byte("HTTP/1.1 200 OK\r\n\r\n")); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Copy data between client and target
|
||||
go io.Copy(clientConn, targetConn) // nolint: errcheck
|
||||
go io.Copy(targetConn, clientConn) // nolint: errcheck
|
||||
}
|
40
examples/ice-proxy/turn.go
Normal file
40
examples/ice-proxy/turn.go
Normal file
@@ -0,0 +1,40 @@
|
||||
// SPDX-FileCopyrightText: 2025 The Pion community <https://pion.ly>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build !js
|
||||
// +build !js
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
|
||||
"github.com/pion/turn/v4"
|
||||
)
|
||||
|
||||
func newTURNServer() *turn.Server {
|
||||
tcpListener, err := net.Listen("tcp4", turnServerAddr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
server, err := turn.NewServer(turn.ServerConfig{
|
||||
AuthHandler: func(_, realm string, _ net.Addr) ([]byte, bool) {
|
||||
// Accept any request with provided username and password.
|
||||
return turn.GenerateAuthKey(turnUsername, realm, turnPassword), true
|
||||
},
|
||||
ListenerConfigs: []turn.ListenerConfig{
|
||||
{
|
||||
Listener: tcpListener,
|
||||
RelayAddressGenerator: &turn.RelayAddressGeneratorNone{
|
||||
Address: "localhost",
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return server
|
||||
}
|
2
go.mod
2
go.mod
@@ -16,6 +16,7 @@ require (
|
||||
github.com/pion/srtp/v3 v3.0.7
|
||||
github.com/pion/stun/v3 v3.0.0
|
||||
github.com/pion/transport/v3 v3.0.7
|
||||
github.com/pion/turn/v4 v4.0.0
|
||||
github.com/sclevine/agouti v3.0.0+incompatible
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/net v0.35.0
|
||||
@@ -27,7 +28,6 @@ require (
|
||||
github.com/onsi/ginkgo v1.16.5 // indirect
|
||||
github.com/onsi/gomega v1.17.0 // indirect
|
||||
github.com/pion/mdns/v2 v2.0.7 // indirect
|
||||
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
|
Reference in New Issue
Block a user