From 64db7d0f804635c7cda7b33d23e1cab6a9ec7b34 Mon Sep 17 00:00:00 2001 From: Giacomo Vacca Date: Sun, 4 Feb 2024 17:51:13 +0100 Subject: [PATCH] Add TURN REST format support --- .../turn-server/lt-cred-turn-rest/main.go | 72 +++++++++++++++++++ go.mod | 11 ++- lt_cred.go | 38 ++++++++++ lt_cred_test.go | 48 +++++++++++++ 4 files changed, 168 insertions(+), 1 deletion(-) create mode 100644 examples/turn-server/lt-cred-turn-rest/main.go diff --git a/examples/turn-server/lt-cred-turn-rest/main.go b/examples/turn-server/lt-cred-turn-rest/main.go new file mode 100644 index 00000000..c527c6b7 --- /dev/null +++ b/examples/turn-server/lt-cred-turn-rest/main.go @@ -0,0 +1,72 @@ +// Package main implements a TURN server using +// ephemeral credentials. +package main + +import ( + "flag" + "log" + "net" + "os" + "os/signal" + "strconv" + "syscall" + + "github.com/pion/logging" + "github.com/pion/turn/v3" +) + +func main() { + publicIP := flag.String("public-ip", "", "IP Address that TURN can be contacted by.") + port := flag.Int("port", 3478, "Listening port.") + authSecret := flag.String("authSecret", "", "Shared secret for the Long Term Credential Mechanism") + realm := flag.String("realm", "pion.ly", "Realm (defaults to \"pion.ly\")") + flag.Parse() + + if len(*publicIP) == 0 { + log.Fatalf("'public-ip' is required") + } else if len(*authSecret) == 0 { + log.Fatalf("'authSecret' is required") + } + + // Create a UDP listener to pass into pion/turn + // pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in + // this allows us to add logging, storage or modify inbound/outbound traffic + udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(*port)) + if err != nil { + log.Panicf("Failed to create TURN server listener: %s", err) + } + + // NewLongTermAuthHandler takes a pion.LeveledLogger. This allows you to intercept messages + // and process them yourself. + logger := logging.NewDefaultLeveledLoggerForScope("lt-creds", logging.LogLevelTrace, os.Stdout) + + s, err := turn.NewServer(turn.ServerConfig{ + Realm: *realm, + // Set AuthHandler callback + // This is called everytime a user tries to authenticate with the TURN server + // Return the key for that user, or false when no user is found + AuthHandler: turn.LongTermTURNRESTAuthHandler(*authSecret, logger), + // PacketConnConfigs is a list of UDP Listeners and the configuration around them + PacketConnConfigs: []turn.PacketConnConfig{ + { + PacketConn: udpListener, + RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{ + RelayAddress: net.ParseIP(*publicIP), // Claim that we are listening on IP passed by user (This should be your Public IP) + Address: "0.0.0.0", // But actually be listening on every interface + }, + }, + }, + }) + if err != nil { + log.Panic(err) + } + + // Block until user sends SIGINT or SIGTERM + sigs := make(chan os.Signal, 1) + signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) + <-sigs + + if err = s.Close(); err != nil { + log.Panic(err) + } +} diff --git a/go.mod b/go.mod index e9fb819e..a355b985 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/pion/turn/v3 -go 1.13 +go 1.19 require ( github.com/pion/logging v0.2.2 @@ -10,3 +10,12 @@ require ( github.com/stretchr/testify v1.8.4 golang.org/x/sys v0.15.0 ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pion/dtls/v2 v2.2.7 // indirect + github.com/pion/transport/v2 v2.2.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.12.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/lt_cred.go b/lt_cred.go index 4308ed90..2691fbb1 100644 --- a/lt_cred.go +++ b/lt_cred.go @@ -9,6 +9,7 @@ import ( //nolint:gci "encoding/base64" "net" "strconv" + "strings" "time" "github.com/pion/logging" @@ -22,6 +23,15 @@ func GenerateLongTermCredentials(sharedSecret string, duration time.Duration) (s return username, password, err } +// GenerateLongTermTURNRESTCredentials can be used to create credentials valid for [duration] time +func GenerateLongTermTURNRESTCredentials(sharedSecret string, user string, duration time.Duration) (string, string, error) { + t := time.Now().Add(duration).Unix() + timestamp := strconv.FormatInt(t, 10) + username := timestamp + ":" + user + password, err := longTermCredentials(username, sharedSecret) + return username, password, err +} + func longTermCredentials(username string, sharedSecret string) (string, error) { mac := hmac.New(sha1.New, []byte(sharedSecret)) _, err := mac.Write([]byte(username)) @@ -57,3 +67,31 @@ func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHa return GenerateAuthKey(username, realm, password), true } } + +// LongTermTURNRESTAuthHandler returns a turn.AuthAuthHandler used with Long Term (or Time Windowed) Credentials. +// https://tools.ietf.org/search/rfc5389#section-10.2 +// It supports the format timestamp:username used with the TURN REST API +func LongTermTURNRESTAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler { + if l == nil { + l = logging.NewDefaultLoggerFactory().NewLogger("turn") + } + return func(username, realm string, srcAddr net.Addr) (key []byte, ok bool) { + l.Tracef("Authentication username=%q realm=%q srcAddr=%v\n", username, realm, srcAddr) + timestamp := strings.Split(username, ":")[0] + t, err := strconv.Atoi(timestamp) + if err != nil { + l.Errorf("Invalid time-windowed username %q", username) + return nil, false + } + if int64(t) < time.Now().Unix() { + l.Errorf("Expired time-windowed username %q", username) + return nil, false + } + password, err := longTermCredentials(username, sharedSecret) + if err != nil { + l.Error(err.Error()) + return nil, false + } + return GenerateAuthKey(username, realm, password), true + } +} diff --git a/lt_cred_test.go b/lt_cred_test.go index 83164de1..e93be15d 100644 --- a/lt_cred_test.go +++ b/lt_cred_test.go @@ -75,3 +75,51 @@ func TestNewLongTermAuthHandler(t *testing.T) { assert.NoError(t, conn.Close()) assert.NoError(t, server.Close()) } + +func TestLongTermTURNRESTAuthHandler(t *testing.T) { + const sharedSecret = "HELLO_WORLD" + + serverListener, err := net.ListenPacket("udp4", "0.0.0.0:3478") + assert.NoError(t, err) + + server, err := NewServer(ServerConfig{ + AuthHandler: LongTermTURNRESTAuthHandler(sharedSecret, nil), + PacketConnConfigs: []PacketConnConfig{ + { + PacketConn: serverListener, + RelayAddressGenerator: &RelayAddressGeneratorStatic{ + RelayAddress: net.ParseIP("127.0.0.1"), + Address: "0.0.0.0", + }, + }, + }, + Realm: "pion.ly", + LoggerFactory: logging.NewDefaultLoggerFactory(), + }) + assert.NoError(t, err) + + conn, err := net.ListenPacket("udp4", "0.0.0.0:0") + assert.NoError(t, err) + + username, password, err := GenerateLongTermTURNRESTCredentials(sharedSecret, "testuser", time.Minute) + assert.NoError(t, err) + + client, err := NewClient(&ClientConfig{ + STUNServerAddr: "0.0.0.0:3478", + TURNServerAddr: "0.0.0.0:3478", + Conn: conn, + Username: username, + Password: password, + LoggerFactory: logging.NewDefaultLoggerFactory(), + }) + assert.NoError(t, err) + assert.NoError(t, client.Listen()) + + relayConn, err := client.Allocate() + assert.NoError(t, err) + + client.Close() + assert.NoError(t, relayConn.Close()) + assert.NoError(t, conn.Close()) + assert.NoError(t, server.Close()) +}