diff --git a/config-sample.yaml b/config-sample.yaml index 189a8f263..74628cdce 100644 --- a/config-sample.yaml +++ b/config-sample.yaml @@ -277,6 +277,21 @@ keys: # # optional (set only if not using external TLS termination) # # cert_file: /path/to/cert.pem # # key_file: /path/to/key.pem +# # TTL of the TURN credentials in seconds - defaults to 300 +# ttl_seconds: 300 +# # list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to. +# # By default (i. e. empty list), all restricted peer CIDRs are denied access. +# # When not empty, only the specified CIDRs are allowed access. +# # Note that this check is applied to restricted peer CIDRs only. +# allow_restricted_peer_cidrs: +# - 10.0.0.0/8 +# - 192.168.0.0/16 +# # list of peer CIDRs to deny access to. +# # This applies to all peer CIDRs, including restricted ones. +# # Deny list takes precedence over allow list. +# deny_peer_cidrs: +# - 10.0.0.0/8 +# - 192.168.0.0/16 # ingress server # ingress: diff --git a/pkg/config/config.go b/pkg/config/config.go index 1da51fd5d..f155eb59d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -227,6 +227,17 @@ type TURNConfig struct { RelayPortRangeEnd uint16 `yaml:"relay_range_end,omitempty"` ExternalTLS bool `yaml:"external_tls,omitempty"` BindAddresses []string `yaml:"bind_addresses,omitempty"` + // TTL of the TURN credentials in seconds - defaults to 300 + TTLSeconds int `yaml:"ttl_seconds,omitempty"` + // list of restricted peer CIDRs (loopback, link-local (unicast, multicast), multicast, private, unspecified) to allow access to. + // By default (i. e. empty list), all restricted peer CIDRs are denied access. + // When not empty, only the specified CIDRs are allowed access. + // Note that this check is applied to restricted peer CIDRs only. + AllowRestrictedPeerCIDRs []string `yaml:"allow_restricted_peer_cidrs,omitempty"` + // list of peer CIDRs to deny access to + // This applies to all peer CIDRs, including restricted ones. + // Deny list takes precedence over allow list. + DenyPeerCIDRs []string `yaml:"deny_peer_cidrs,omitempty"` } type NodeSelectorConfig struct { @@ -421,6 +432,7 @@ var DefaultConfig = Config{ TURN: TURNConfig{ Enabled: false, BindAddresses: []string{"0.0.0.0"}, + TTLSeconds: 300, }, NodeSelector: NodeSelectorConfig{ Kind: "any", diff --git a/pkg/service/roommanager.go b/pkg/service/roommanager.go index 472fd2efb..40a242cb0 100644 --- a/pkg/service/roommanager.go +++ b/pkg/service/roommanager.go @@ -1036,7 +1036,7 @@ func (r *RoomManager) iceServersForParticipant(apiKey string, participant types. urls = append(urls, fmt.Sprintf("turns:%s:443?transport=tcp", r.config.TURN.Domain)) } if len(urls) > 0 { - username := r.turnAuthHandler.CreateUsername(apiKey, participant.ID()) + username := r.turnAuthHandler.CreateUsername(apiKey, participant.ID(), r.config.TURN.TTLSeconds) password, err := r.turnAuthHandler.CreatePassword(apiKey, participant.ID()) if err != nil { participant.GetLogger().Warnw("could not create turn password", err) diff --git a/pkg/service/turn.go b/pkg/service/turn.go index a37091c4f..b2aefb885 100644 --- a/pkg/service/turn.go +++ b/pkg/service/turn.go @@ -21,6 +21,7 @@ import ( "net" "strconv" "strings" + "time" "github.com/jxskiss/base62" "github.com/pion/turn/v4" @@ -92,6 +93,41 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone relayAddrGen = telemetry.NewRelayAddressGenerator(relayAddrGen) } + permissionHandler := func(_clientAddr net.Addr, peerIP net.IP) bool { + // restricted peer IP is denied by default, unless allowed by the allow list, + if peerIP.IsLoopback() || + peerIP.IsLinkLocalUnicast() || + peerIP.IsLinkLocalMulticast() || + peerIP.IsMulticast() || + peerIP.IsPrivate() || + peerIP.IsUnspecified() { + allowed := false + for _, cidr := range turnConf.AllowRestrictedPeerCIDRs { + if _, ipnet, err := net.ParseCIDR(cidr); err == nil { + if ipnet.Contains(peerIP) { + allowed = true + break + } + } + } + if !allowed { + return false + } + + // if allowed, check deny list for overrides + } + + for _, cidr := range turnConf.DenyPeerCIDRs { + if _, ipnet, err := net.ParseCIDR(cidr); err == nil { + if ipnet.Contains(peerIP) { + return false + } + } + } + + return true + } + if turnConf.TLSPort > 0 { var listener net.Listener var listenerErr error @@ -121,6 +157,7 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone listenerConfig := turn.ListenerConfig{ Listener: listener, RelayAddressGenerator: relayAddrGen, + PermissionHandler: permissionHandler, } serverConfig.ListenerConfigs = append(serverConfig.ListenerConfigs, listenerConfig) @@ -140,6 +177,7 @@ func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone packetConfig := turn.PacketConnConfig{ PacketConn: udpListener, RelayAddressGenerator: relayAddrGen, + PermissionHandler: permissionHandler, } serverConfig.PacketConnConfigs = append(serverConfig.PacketConnConfigs, packetConfig) logValues = append(logValues, "turn.portUDP", turnConf.UDPPort) @@ -164,21 +202,30 @@ func NewTURNAuthHandler(keyProvider auth.KeyProvider) *TURNAuthHandler { } } -func (h *TURNAuthHandler) CreateUsername(apiKey string, pID livekit.ParticipantID) string { - return base62.EncodeToString([]byte(fmt.Sprintf("%s|%s", apiKey, pID))) +func (h *TURNAuthHandler) CreateUsername(apiKey string, pID livekit.ParticipantID, ttlSeconds int) string { + expiry := time.Now().Add(time.Duration(ttlSeconds) * time.Second).Unix() + return base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", apiKey, pID, expiry)) } -func (h *TURNAuthHandler) ParseUsername(username string) (apiKey string, pID livekit.ParticipantID, err error) { +func (h *TURNAuthHandler) ParseUsername(username string) (apiKey string, pID livekit.ParticipantID, expiry time.Time, err error) { decoded, err := base62.DecodeString(username) if err != nil { - return "", "", err + return "", "", time.Time{}, err } parts := strings.Split(string(decoded), "|") - if len(parts) != 2 { - return "", "", errors.New("invalid username") + if len(parts) != 2 && len(parts) != 3 { + return "", "", time.Time{}, errors.New("invalid username") + } + expiry = time.Time{} + if len(parts) == 3 { + if unixTime, err := strconv.ParseInt(parts[2], 10, 64); err != nil { + return "", "", time.Time{}, err + } else { + expiry = time.Unix(unixTime, 0) + } } - return parts[0], livekit.ParticipantID(parts[1]), nil + return parts[0], livekit.ParticipantID(parts[1]), expiry, nil } func (h *TURNAuthHandler) CreatePassword(apiKey string, pID livekit.ParticipantID) (string, error) { @@ -197,9 +244,19 @@ func (h *TURNAuthHandler) HandleAuth(username, realm string, srcAddr net.Addr) ( return nil, false } parts := strings.Split(string(decoded), "|") - if len(parts) != 2 { + if len(parts) != 2 && len(parts) != 3 { return nil, false } + if len(parts) == 3 { + if unixTime, err := strconv.ParseInt(parts[2], 10, 64); err != nil { + return nil, false + } else { + expiry := time.Unix(unixTime, 0) + if time.Now().After(expiry) { + return nil, false + } + } + } password, err := h.CreatePassword(parts[0], livekit.ParticipantID(parts[1])) if err != nil { logger.Warnw("could not create TURN password", err, "username", username) diff --git a/test/client/client.go b/test/client/client.go index f44c884d8..bb38cdaf3 100644 --- a/test/client/client.go +++ b/test/client/client.go @@ -691,8 +691,11 @@ func (c *RTCClient) handleSignalResponse(res *livekit.SignalResponse) error { return nil } -func (c *RTCClient) WaitUntilConnected() error { - ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second) +func (c *RTCClient) WaitUntilConnected(timeout time.Duration) error { + if timeout == 0 { + timeout = 20 * time.Second + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() for { select { diff --git a/test/integration_helpers.go b/test/integration_helpers.go index 101121440..3ccde58d9 100644 --- a/test/integration_helpers.go +++ b/test/integration_helpers.go @@ -132,14 +132,30 @@ func waitUntilConnected(t *testing.T, clients ...*testclient.RTCClient) { wg := sync.WaitGroup{} for i := range clients { c := clients[i] - wg.Add(1) - go func() { - defer wg.Done() - err := c.WaitUntilConnected() + wg.Go(func() { + err := c.WaitUntilConnected(5 * time.Second) if err != nil { t.Error(err) } - }() + }) + } + wg.Wait() + if t.Failed() { + t.FailNow() + } +} + +func ensureNotConnected(t *testing.T, clients ...*testclient.RTCClient) { + logger.Infow("checking if clients connect") + wg := sync.WaitGroup{} + for i := range clients { + c := clients[i] + wg.Go(func() { + err := c.WaitUntilConnected(5 * time.Second) + if err == nil { + t.Error(fmt.Errorf("expected client to not connect: %s", c.ID())) + } + }) } wg.Wait() if t.Failed() { diff --git a/test/singlenode_test.go b/test/singlenode_test.go index 177dbbaaa..d40edf3a5 100644 --- a/test/singlenode_test.go +++ b/test/singlenode_test.go @@ -1321,31 +1321,67 @@ func TestTurnRelay(t *testing.T) { return } - s := createSingleNodeServer(func(c *config.Config) { - c.TURN.Enabled = true - c.TURN.UDPPort = 3478 - }) - go func() { - if err := s.Start(); err != nil { - logger.Errorw("server returned error", err) - } - }() - defer s.Stop(true) + testCases := []struct { + name string + allowRestrictedPeerCIDRs []string + denyPeerCIDRs []string + expectedToConnect bool + }{ + { + "allow", + []string{"10.0.0.0/8", "192.168.0.0/16"}, + nil, + true, + }, + { + "not-allowed", + nil, + nil, + false, + }, + { + "denied-overrides-allowed", + []string{"10.0.0.0/8", "192.168.0.0/16"}, + []string{"10.0.0.0/8", "192.168.0.0/16"}, + false, + }, + } - waitForServerToStart(s) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + s := createSingleNodeServer(func(c *config.Config) { + c.TURN.Enabled = true + c.TURN.UDPPort = 3478 + c.TURN.AllowRestrictedPeerCIDRs = tc.allowRestrictedPeerCIDRs + c.TURN.DenyPeerCIDRs = tc.denyPeerCIDRs + }) + go func() { + if err := s.Start(); err != nil { + logger.Errorw("server returned error", err) + } + }() + defer s.Stop(true) - c1 := createRTCClient("relay_c1", defaultServerPort, testRTCServicePathv0, &testclient.Options{ - AutoSubscribe: true, - ForceRelay: true, - }) - defer c1.Stop() + waitForServerToStart(s) - waitUntilConnected(t, c1) + c1 := createRTCClient("relay_c1", defaultServerPort, testRTCServicePathv0, &testclient.Options{ + AutoSubscribe: true, + ForceRelay: true, + }) + defer c1.Stop() - testutils.WithTimeout(t, func() string { - if !c1.IsLocalCandidateRelaySelected() { - return "expected local candidate to be relay" - } - return "" - }) + if tc.expectedToConnect { + waitUntilConnected(t, c1) + + testutils.WithTimeout(t, func() string { + if !c1.IsLocalCandidateRelaySelected() { + return "expected local candidate to be relay" + } + return "" + }) + } else { + ensureNotConnected(t, c1) + } + }) + } }