Add ice-proxy example

This commit is contained in:
Anton Manakin
2025-08-17 18:59:17 +03:00
committed by Joe Turki
parent 1557d318e2
commit afcb3487d9
8 changed files with 439 additions and 1 deletions

View File

@@ -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 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 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 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. * [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. * [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.

View 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.

View 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)
}
}()
}

View 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
View 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
View 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
}

View 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
View File

@@ -16,6 +16,7 @@ require (
github.com/pion/srtp/v3 v3.0.7 github.com/pion/srtp/v3 v3.0.7
github.com/pion/stun/v3 v3.0.0 github.com/pion/stun/v3 v3.0.0
github.com/pion/transport/v3 v3.0.7 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/sclevine/agouti v3.0.0+incompatible
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.10.0
golang.org/x/net v0.35.0 golang.org/x/net v0.35.0
@@ -27,7 +28,6 @@ require (
github.com/onsi/ginkgo v1.16.5 // indirect github.com/onsi/ginkgo v1.16.5 // indirect
github.com/onsi/gomega v1.17.0 // indirect github.com/onsi/gomega v1.17.0 // indirect
github.com/pion/mdns/v2 v2.0.7 // 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/pmezard/go-difflib v1.0.0 // indirect
github.com/wlynxg/anet v0.0.5 // indirect github.com/wlynxg/anet v0.0.5 // indirect
golang.org/x/crypto v0.33.0 // indirect golang.org/x/crypto v0.33.0 // indirect