From 7b2a1d4ca1e888eef85c0a3c119a6226d5269b99 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 --- examples/README.md | 12 ++++ .../turn-server/lt-cred-turn-rest/main.go | 72 +++++++++++++++++++ lt_cred.go | 43 ++++++++++- lt_cred_test.go | 48 +++++++++++++ 4 files changed, 174 insertions(+), 1 deletion(-) create mode 100644 examples/turn-server/lt-cred-turn-rest/main.go diff --git a/examples/README.md b/examples/README.md index 691711eb..5499a772 100644 --- a/examples/README.md +++ b/examples/README.md @@ -48,6 +48,18 @@ The only downside is that you can't revoke a single username/password. You need * -authSecret : Shared secret for the Long Term Credential Mechanism +#### lt-cred-turn-rest + +This example shows how to use ephemeral credentials, generated by a REST API, with the user part formatted as `timestamp:username`. + +The REST API and TURN server use the same shared secret to compute the credentials. + +The timestamp part specifies when the credentials will expire. + +This mechanism is described in https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00 + +* -authSecret : Shared secret for the ephemeral Credential Mechanism + #### perm-filter This example demonstrates the use of a permission handler in the PION TURN server. The example implements a filtering policy that lets clients to connect back to their own host or server-reflexive address but will drop everything else. This will let the client ping-test through but will block essentially all other peer connection attempts. 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..b85e8061 --- /dev/null +++ b/examples/turn-server/lt-cred-turn-rest/main.go @@ -0,0 +1,72 @@ +// SPDX-FileCopyrightText: 2024 The Pion community +// SPDX-License-Identifier: MIT + +// 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, + 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/lt_cred.go b/lt_cred.go index 4308ed90..42466c38 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)) @@ -33,7 +43,7 @@ func longTermCredentials(username string, sharedSecret string) (string, error) { } // NewLongTermAuthHandler returns a turn.AuthAuthHandler used with Long Term (or Time Windowed) Credentials. -// See: https://tools.ietf.org/search/rfc5389#section-10.2 +// See: https://datatracker.ietf.org/doc/html/rfc8489#section-9.2 func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHandler { if l == nil { l = logging.NewDefaultLoggerFactory().NewLogger("turn") @@ -57,3 +67,34 @@ func NewLongTermAuthHandler(sharedSecret string, l logging.LeveledLogger) AuthHa return GenerateAuthKey(username, realm, password), true } } + +// LongTermTURNRESTAuthHandler returns a turn.AuthAuthHandler that can be used to authenticate +// time-windowed ephemeral credentials generated by the TURN REST API as described in +// https://datatracker.ietf.org/doc/html/draft-uberti-behave-turn-rest-00 +// +// The supported format of is timestamp:username, where username is an arbitrary user id and the +// timestamp specifies the expiry of the credential. +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()) +}