Pure-Go WebRTC. Pass ICEServers in your webrtc.Configuration.
// go get github.com/pion/webrtc/v4
package main
import "github.com/pion/webrtc/v4"
func newPC() (*webrtc.PeerConnection, error) {
cfg := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{ URLs: []string{"stun:stun.expressturn.com:3478"} },
{
URLs: []string{
"turn:relay1.expressturn.com:3478?transport=udp",
"turn:relay1.expressturn.com:3478?transport=tcp",
"turns:relay1.expressturn.com:443?transport=tcp",
},
Username: "YOUR_EXPRESSTURN_USERNAME",
Credential: "YOUR_EXPRESSTURN_PASSWORD",
},
},
ICETransportPolicy: webrtc.ICETransportPolicyAll,
}
return webrtc.NewPeerConnection(cfg)
}
cfg.ICETransportPolicy = webrtc.ICETransportPolicyRelay
This forces every connection through TURN — useful for catching configuration mistakes early.
If you're running an authoritative game server (Pion is popular for this), the server's PeerConnection usually doesn't need TURN — it has a public address. Add TURN to the client's ICE config so connecting players get a relay path.
Embedded uses (IoT cameras, set-top boxes) are different — those endpoints sit behind NAT and absolutely need TURN.
import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"time"
)
func turnCreds(secret, userID string) (username, credential string) {
expiry := time.Now().Unix() + 3600
username = fmt.Sprintf("%d:%s", expiry, userID)
h := hmac.New(sha1.New, []byte(secret))
h.Write([]byte(username))
credential = base64.StdEncoding.EncodeToString(h.Sum(nil))
return
}
See shared-secret examples in other languages.
v2/webrtc. The API for ICEServers is largely the same, but check your import paths.defer pc.Close() on connection failure paths.Related: Janus recipe · TURN for multiplayer games · TURN for IP cameras