Skip to content

Commit

Permalink
[MM-45001] Add support for generating short-lived TURN credentials (#63)
Browse files Browse the repository at this point in the history
* Add support for generating short-lived TURN credentials

* Validate all URLs

* Use dedicated config struct
  • Loading branch information
streamer45 authored Jul 5, 2022
1 parent ddcf484 commit f54cfd9
Show file tree
Hide file tree
Showing 8 changed files with 286 additions and 30 deletions.
2 changes: 2 additions & 0 deletions config/config.sample.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ security.admin_secret_key = ""
ice_port_udp = 8443
ice_host_override = ""
ice_servers = []
turn.static_auth_secret = ""
turn.credentials_expiration_minutes = 1440

[store]
data_source = "/tmp/rtcd_db"
Expand Down
42 changes: 22 additions & 20 deletions docs/env_config.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
### Config Environment Overrides

```
KEY TYPE
RTCD_API_HTTP_LISTENADDRESS String
RTCD_API_HTTP_TLS_ENABLE True or False
RTCD_API_HTTP_TLS_CERTFILE String
RTCD_API_HTTP_TLS_CERTKEY String
RTCD_API_SECURITY_ENABLEADMIN True or False
RTCD_API_SECURITY_ADMINSECRETKEY String
RTCD_API_SECURITY_ALLOWSELFREGISTRATION True or False
RTCD_RTC_ICEPORTUDP Integer
RTCD_RTC_ICEHOSTOVERRIDE String
RTCD_RTC_ICESERVERS Comma-separated list of
RTCD_STORE_DATASOURCE String
RTCD_LOGGER_ENABLECONSOLE True or False
RTCD_LOGGER_CONSOLEJSON True or False
RTCD_LOGGER_CONSOLELEVEL String
RTCD_LOGGER_ENABLEFILE True or False
RTCD_LOGGER_FILEJSON True or False
RTCD_LOGGER_FILELEVEL String
RTCD_LOGGER_FILELOCATION String
RTCD_LOGGER_ENABLECOLOR True or False
KEY TYPE
RTCD_API_HTTP_LISTENADDRESS String
RTCD_API_HTTP_TLS_ENABLE True or False
RTCD_API_HTTP_TLS_CERTFILE String
RTCD_API_HTTP_TLS_CERTKEY String
RTCD_API_SECURITY_ENABLEADMIN True or False
RTCD_API_SECURITY_ADMINSECRETKEY String
RTCD_API_SECURITY_ALLOWSELFREGISTRATION True or False
RTCD_RTC_ICEPORTUDP Integer
RTCD_RTC_ICEHOSTOVERRIDE String
RTCD_RTC_ICESERVERS Comma-separated list of
RTCD_RTC_TURNCONFIG_STATICAUTHSECRET String
RTCD_RTC_TURNCONFIG_CREDENTIALSEXPIRATIONMINUTES Integer
RTCD_STORE_DATASOURCE String
RTCD_LOGGER_ENABLECONSOLE True or False
RTCD_LOGGER_CONSOLEJSON True or False
RTCD_LOGGER_CONSOLELEVEL String
RTCD_LOGGER_ENABLEFILE True or False
RTCD_LOGGER_FILEJSON True or False
RTCD_LOGGER_FILELEVEL String
RTCD_LOGGER_FILELOCATION String
RTCD_LOGGER_ENABLECOLOR True or False
```
5 changes: 5 additions & 0 deletions service/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ func (c Config) IsValid() error {
return err
}

if err := c.RTC.IsValid(); err != nil {
return err
}

if err := c.Store.IsValid(); err != nil {
return err
}
Expand All @@ -77,6 +81,7 @@ func (c Config) IsValid() error {
func (c *Config) SetDefaults() {
c.API.HTTP.ListenAddress = ":8045"
c.RTC.ICEPortUDP = 8443
c.RTC.TURNConfig.CredentialsExpirationMinutes = 1440
c.Store.DataSource = "/tmp/rtcd_db"
c.Logger.EnableConsole = true
c.Logger.ConsoleJSON = false
Expand Down
31 changes: 26 additions & 5 deletions service/rtc/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type ServerConfig struct {
ICEHostOverride string `toml:"ice_host_override"`
// A list of ICE server (STUN/TURN) configurations to use.
ICEServers ICEServers `toml:"ice_servers"`
TURNConfig TURNConfig `toml:"turn"`
}

func (c ServerConfig) IsValid() error {
Expand All @@ -28,6 +29,10 @@ func (c ServerConfig) IsValid() error {
return fmt.Errorf("invalid ICEServers value: %w", err)
}

if err := c.TURNConfig.IsValid(); err != nil {
return fmt.Errorf("invalid TURNConfig: %w", err)
}

return nil
}

Expand Down Expand Up @@ -79,13 +84,31 @@ func (c ICEServerConfig) IsValid() error {
return fmt.Errorf("invalid empty URL")
}

if !strings.HasPrefix(u, "stun:") && !strings.HasPrefix(u, "turn:") {
if !c.IsSTUN() && !c.IsTURN() {
return fmt.Errorf("URL is not a valid STUN/TURN server")
}
}
return nil
}

func (c ICEServerConfig) IsTURN() bool {
for _, u := range c.URLs {
if !strings.HasPrefix(u, "turn:") {
return false
}
}
return len(c.URLs) > 0
}

func (c ICEServerConfig) IsSTUN() bool {
for _, u := range c.URLs {
if !strings.HasPrefix(u, "stun:") {
return false
}
}
return len(c.URLs) > 0
}

func (s ICEServers) IsValid() error {
for _, cfg := range s {
if err := cfg.IsValid(); err != nil {
Expand All @@ -97,10 +120,8 @@ func (s ICEServers) IsValid() error {

func (s ICEServers) getSTUN() string {
for _, cfg := range s {
for _, u := range cfg.URLs {
if strings.HasPrefix(u, "stun:") {
return u
}
if cfg.IsSTUN() {
return cfg.URLs[0]
}
}
return ""
Expand Down
27 changes: 27 additions & 0 deletions service/rtc/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,24 @@ func TestServerConfigIsValid(t *testing.T) {
require.Equal(t, "invalid ICEPortUDP value: 65000 is not in allowed range [80, 49151]", err.Error())
})

t.Run("invalid TURNCredentialsExpirationMinutes", func(t *testing.T) {
var cfg ServerConfig
cfg.ICEPortUDP = 8443
cfg.TURNConfig.StaticAuthSecret = "secret"
err := cfg.IsValid()
require.Error(t, err)
require.Equal(t, "invalid TURNConfig: invalid CredentialsExpirationMinutes value: should be a positive number", err.Error())

cfg.TURNConfig.CredentialsExpirationMinutes = 20000
err = cfg.IsValid()
require.Error(t, err)
require.Equal(t, "invalid TURNConfig: invalid CredentialsExpirationMinutes value: should be less than 1 week", err.Error())
})

t.Run("valid", func(t *testing.T) {
var cfg ServerConfig
cfg.ICEPortUDP = 8443
cfg.TURNConfig.CredentialsExpirationMinutes = 1440
err := cfg.IsValid()
require.NoError(t, err)
})
Expand Down Expand Up @@ -172,6 +187,18 @@ func TestICEServerConfigIsValid(t *testing.T) {
require.Equal(t, "URL is not a valid STUN/TURN server", err.Error())
})

t.Run("partially valid", func(t *testing.T) {
cfg := ICEServerConfig{
URLs: []string{
"turn:turn1.localhost:3478",
"turn2.localhost:3478",
},
}
err := cfg.IsValid()
require.Error(t, err)
require.Equal(t, "URL is not a valid STUN/TURN server", err.Error())
})

t.Run("valid", func(t *testing.T) {
cfg := ICEServerConfig{
URLs: []string{
Expand Down
25 changes: 20 additions & 5 deletions service/rtc/sfu.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,14 +90,29 @@ func initInterceptors(m *webrtc.MediaEngine) (*interceptor.Registry, error) {
func (s *Server) InitSession(cfg SessionConfig, closeCb func() error) error {
s.metrics.IncRTCSessions(cfg.GroupID, cfg.CallID)

iceServers := make([]webrtc.ICEServer, len(s.cfg.ICEServers))
for _, cfg := range s.cfg.ICEServers {
iceServers := make([]webrtc.ICEServer, 0, len(s.cfg.ICEServers))
for _, iceCfg := range s.cfg.ICEServers {
// generating short-lived TURN credentials if needed.
if iceCfg.IsTURN() && s.cfg.TURNConfig.StaticAuthSecret == "" {
continue
}
if iceCfg.IsTURN() && iceCfg.Username == "" && iceCfg.Credential == "" {
ts := time.Now().Add(time.Duration(s.cfg.TURNConfig.CredentialsExpirationMinutes) * time.Minute).Unix()
username, password, err := genTURNCredentials(cfg.SessionID, s.cfg.TURNConfig.StaticAuthSecret, ts)
if err != nil {
s.log.Error("failed to generate TURN credentials", mlog.Err(err))
continue
}
iceCfg.Username = username
iceCfg.Credential = password
}
iceServers = append(iceServers, webrtc.ICEServer{
URLs: cfg.URLs,
Username: cfg.Username,
Credential: cfg.Credential,
URLs: iceCfg.URLs,
Username: iceCfg.Username,
Credential: iceCfg.Credential,
})
}

peerConnConfig := webrtc.Configuration{
ICEServers: iceServers,
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
Expand Down
84 changes: 84 additions & 0 deletions service/rtc/turn.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
// Copyright (c) 2022-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.

package rtc

import (
"crypto/hmac"
"crypto/sha1"
"encoding/base64"
"fmt"
"time"
)

const MaxTURNCredentialsExpiration = 7 * 24 * 60 // 1 week in minutes

type TURNConfig struct {
// The secret key used to generate TURN short-lived authentication
// credentials.
StaticAuthSecret string `toml:"static_auth_secret"`
// The number of minutes that the generated TURN credentials will be valid for.
CredentialsExpirationMinutes int `toml:"credentials_expiration_minutes"`
}

func (c TURNConfig) IsValid() error {
if c.StaticAuthSecret != "" {
if c.CredentialsExpirationMinutes <= 0 {
return fmt.Errorf("invalid CredentialsExpirationMinutes value: should be a positive number")
}
if c.CredentialsExpirationMinutes >= MaxTURNCredentialsExpiration {
return fmt.Errorf("invalid CredentialsExpirationMinutes value: should be less than 1 week")
}
}

return nil
}

func genTURNCredentials(username, secret string, expirationTS int64) (string, string, error) {
if username == "" {
return "", "", fmt.Errorf("username should not be empty")
}

if secret == "" {
return "", "", fmt.Errorf("secret should not be empty")
}

if expirationTS <= 0 {
return "", "", fmt.Errorf("expirationTS should be a positive number")
}

if expirationTS > time.Now().Add(MaxTURNCredentialsExpiration*time.Minute).Unix() {
return "", "", fmt.Errorf("expirationTS cannot be more than a week into the future")
}

h := hmac.New(sha1.New, []byte(secret))
username = fmt.Sprintf("%d:%s", expirationTS, username)
_, err := h.Write([]byte(username))
if err != nil {
return "", "", fmt.Errorf("failed to write hmac: %w", err)
}
password := base64.StdEncoding.EncodeToString(h.Sum(nil))
return username, password, nil
}

func GenTURNConfigs(turnServers ICEServers, username, secret string, expiryMinutes int) (ICEServers, error) {
var configs ICEServers
ts := time.Now().Add(time.Duration(expiryMinutes) * time.Minute).Unix()

for _, cfg := range turnServers {
if cfg.Username != "" || cfg.Credential != "" {
continue
}
username, password, err := genTURNCredentials(username, secret, ts)
if err != nil {
return nil, err
}
configs = append(configs, ICEServerConfig{
URLs: cfg.URLs,
Username: username,
Credential: password,
})
}

return configs, nil
}
Loading

0 comments on commit f54cfd9

Please sign in to comment.