Skip to content

Commit

Permalink
Add TURN REST format support
Browse files Browse the repository at this point in the history
  • Loading branch information
giavac authored and rg0now committed Feb 9, 2024
1 parent fb0ab51 commit a9681a0
Show file tree
Hide file tree
Showing 4 changed files with 174 additions and 1 deletion.
12 changes: 12 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
72 changes: 72 additions & 0 deletions examples/turn-server/lt-cred-turn-rest/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// SPDX-FileCopyrightText: 2024 The Pion community <https://pion.ly>
// 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)
}
}
43 changes: 42 additions & 1 deletion lt_cred.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import ( //nolint:gci
"encoding/base64"
"net"
"strconv"
"strings"
"time"

"github.com/pion/logging"
Expand All @@ -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))
Expand All @@ -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")
Expand All @@ -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
}
}
48 changes: 48 additions & 0 deletions lt_cred_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
}

0 comments on commit a9681a0

Please sign in to comment.