// Copyright 2023 LiveKit, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package service import ( "crypto/sha256" "crypto/tls" "fmt" "net" "strconv" "strings" "time" "github.com/jxskiss/base62" "github.com/pion/stun/v3" "github.com/pion/turn/v5" "github.com/pkg/errors" "github.com/livekit/protocol/auth" "github.com/livekit/protocol/livekit" "github.com/livekit/protocol/logger" "github.com/livekit/protocol/logger/pionlogger" "github.com/livekit/livekit-server/pkg/config" "github.com/livekit/livekit-server/pkg/telemetry" "github.com/livekit/livekit-server/pkg/telemetry/prometheus" ) const ( LivekitRealm = "livekit" allocateRetries = 50 ) var ErrExpired = errors.New("expired") func NewTurnServer(conf *config.Config, authHandler turn.AuthHandler, standalone bool) (*turn.Server, error) { turnConf := conf.TURN if !turnConf.Enabled { return nil, nil } if turnConf.TLSPort <= 0 && turnConf.UDPPort <= 0 { return nil, errors.New("invalid TURN ports") } else if turnConf.TLSPort > 0 { if turnConf.Domain == "" { return nil, errors.New("TURN domain required") } if !IsValidDomain(turnConf.Domain) { return nil, errors.New("TURN domain is not correct") } } serverConfig := turn.ServerConfig{ Realm: LivekitRealm, AuthHandler: authHandler, LoggerFactory: pionlogger.NewLoggerFactory(logger.GetLogger()), } var logValues []any logValues = append(logValues, "turn.relay_range_start", turnConf.RelayPortRangeStart) logValues = append(logValues, "turn.relay_range_end", turnConf.RelayPortRangeEnd) for _, addr := range turnConf.BindAddresses { var nodeIP string if net.ParseIP(addr).To4() != nil { nodeIP = conf.RTC.NodeIP.V4 } else { nodeIP = conf.RTC.NodeIP.V6 } if nodeIP == "" { return nil, errors.New("no matching node IP for relay") } var relayAddrGen turn.RelayAddressGenerator = &turn.RelayAddressGeneratorPortRange{ RelayAddress: net.ParseIP(nodeIP), Address: addr, MinPort: turnConf.RelayPortRangeStart, MaxPort: turnConf.RelayPortRangeEnd, MaxRetries: allocateRetries, } if 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 if turnConf.ExternalTLS { listener, listenerErr = net.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(turnConf.TLSPort))) } else { cert, err := tls.LoadX509KeyPair(turnConf.CertFile, turnConf.KeyFile) if err != nil { return nil, errors.Wrap(err, "TURN tls cert required") } listener, listenerErr = tls.Listen("tcp", net.JoinHostPort(addr, strconv.Itoa(turnConf.TLSPort)), &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{cert}, }) } if listenerErr != nil { return nil, errors.Wrap(listenerErr, "could not listen on TURN TCP port") } if standalone { listener = telemetry.NewListener(listener) } listenerConfig := turn.ListenerConfig{ Listener: listener, RelayAddressGenerator: relayAddrGen, PermissionHandler: permissionHandler, } serverConfig.ListenerConfigs = append(serverConfig.ListenerConfigs, listenerConfig) logValues = append(logValues, "turn.portTLS", turnConf.TLSPort, "turn.externalTLS", turnConf.ExternalTLS) } if turnConf.UDPPort > 0 { udpListener, err := net.ListenPacket("udp", net.JoinHostPort(addr, strconv.Itoa(turnConf.UDPPort))) if err != nil { return nil, errors.Wrap(err, "could not listen on TURN UDP port") } if standalone { udpListener = telemetry.NewPacketConn(udpListener, prometheus.Incoming) } packetConfig := turn.PacketConnConfig{ PacketConn: udpListener, RelayAddressGenerator: relayAddrGen, PermissionHandler: permissionHandler, } serverConfig.PacketConnConfigs = append(serverConfig.PacketConnConfigs, packetConfig) logValues = append(logValues, "turn.portUDP", turnConf.UDPPort) } } logger.Infow("Starting TURN server", logValues...) return turn.NewServer(serverConfig) } func getTURNAuthHandlerFunc(handler *TURNAuthHandler) turn.AuthHandler { return handler.HandleAuth } type TURNAuthHandler struct { keyProvider auth.KeyProvider } func NewTURNAuthHandler(keyProvider auth.KeyProvider) *TURNAuthHandler { return &TURNAuthHandler{ keyProvider: keyProvider, } } func (h *TURNAuthHandler) CreateUsername(apiKey string, pID livekit.ParticipantID, ttlSeconds int) (string, int64) { expiry := time.Now().Add(time.Duration(ttlSeconds) * time.Second).Unix() return base62.EncodeToString(fmt.Appendf(nil, "%s|%s|%d", apiKey, pID, expiry)), expiry } func (h *TURNAuthHandler) ParseUsername(username string) (string, livekit.ParticipantID, int64, error) { decoded, err := base62.DecodeString(username) if err != nil { return "", "", 0, err } parts := strings.Split(string(decoded), "|") if len(parts) != 3 { return "", "", 0, errors.New("invalid username") } expiry, err := strconv.ParseInt(parts[2], 10, 64) if err != nil { return "", "", 0, err } if expiry == 0 { return "", "", 0, ErrExpired } return parts[0], livekit.ParticipantID(parts[1]), expiry, nil } func (h *TURNAuthHandler) CreatePassword(apiKey string, pID livekit.ParticipantID, expiry int64) (string, error) { if expiry == 0 || time.Now().After(time.Unix(expiry, 0)) { return "", ErrExpired } return h.computePassword(apiKey, pID, expiry) } func (h *TURNAuthHandler) computePassword(apiKey string, pID livekit.ParticipantID, expiry int64) (string, error) { secret := h.keyProvider.GetSecret(apiKey) if secret == "" { return "", ErrInvalidAPIKey } keyInput := fmt.Sprintf("%s|%s|%d", secret, pID, expiry) sum := sha256.Sum256([]byte(keyInput)) return base62.EncodeToString(sum[:]), nil } func (h *TURNAuthHandler) HandleAuth(ra *turn.RequestAttributes) (userID string, key []byte, ok bool) { username := ra.Username decoded, err := base62.DecodeString(username) if err != nil { return "", nil, false } parts := strings.Split(string(decoded), "|") if len(parts) != 3 { return "", nil, false } expiry, err := strconv.ParseInt(parts[2], 10, 64) if err != nil { return "", nil, false } if expiry == 0 { return "", nil, false } expiryTime := time.Unix(expiry, 0) if time.Now().After(expiryTime) { // TTL only applies to initial allocation. Refresh / CreatePermission / // ChannelBind / Send / Data requests are still authenticated against the // username/password but skip the TTL check so long-running sessions can // keep refreshing past the credential expiry. if ra.Method == stun.MethodAllocate { logger.Infow("TURN credential expired", "username", decoded, "participantID", parts[1], "expiry", expiryTime, "method", ra.Method) return "", nil, false } } password, err := h.computePassword(parts[0], livekit.ParticipantID(parts[1]), expiry) if err != nil { logger.Warnw("could not create TURN password", err, "username", decoded) return "", nil, false } return parts[1], turn.GenerateAuthKey(username, LivekitRealm, password), true }