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 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.
|
||||||
|
|
||||||
|
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/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
|
||||||
|
Reference in New Issue
Block a user