From f6f8d61805aa96ff4dc0f3d716268ef81fb10646 Mon Sep 17 00:00:00 2001 From: Benjamin Pracht Date: Thu, 27 Aug 2026 16:24:11 -0700 Subject: [PATCH 1/5] Add UDP URL pull support behind a config flag Support udp:// URLs in the URL pull source using a gstreamer udpsrc element, gated by the enable_udp_url_pull service config option. Add a multicast_interface option to pick the network interface to join multicast groups on, and log udpsrc stats alongside the other input sources. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/config/config.go | 3 +++ pkg/media/urlpull/source.go | 28 +++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 15bba80d..2bbf2f38 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -61,6 +61,9 @@ type ServiceConfig struct { Logging logger.Config `yaml:"logging"` Development bool `yaml:"development"` PSRPCSkipClaim bool `yaml:"psrpc_skip_claim,omitempty"` // Lets psrpc servers skip the claim handshake on queue rpcs + EnableUDPURLPull bool `yaml:"enable_udp_url_pull,omitempty"` + // Network interface to join multicast groups on for UDP url pull. Empty means let the OS decide. + MulticastInterface string `yaml:"multicast_interface,omitempty"` // Used for WHIP transport RTCConfig rtcconfig.RTCConfig `yaml:"rtc_config"` diff --git a/pkg/media/urlpull/source.go b/pkg/media/urlpull/source.go index 867db5de..9334cd5c 100644 --- a/pkg/media/urlpull/source.go +++ b/pkg/media/urlpull/source.go @@ -22,9 +22,10 @@ import ( "github.com/frostbyte73/core" "github.com/go-gst/go-gst/gst" + "github.com/livekit/protocol/logger" + "github.com/livekit/ingress/pkg/errors" "github.com/livekit/ingress/pkg/params" - "github.com/livekit/protocol/logger" ) var ( @@ -85,6 +86,31 @@ func NewURLSource(_ context.Context, p *params.Params) (*URLSource, error) { } } } + } else if p.Config.EnableUDPURLPull && strings.HasPrefix(p.Url, "udp://") { + elem, err = gst.NewElement("udpsrc") + if err != nil { + return nil, err + } + err = elem.SetProperty("uri", p.Url) + if err != nil { + return nil, err + } + + if p.Config.MulticastInterface != "" { + err = elem.SetProperty("multicast-iface", p.Config.MulticastInterface) + if err != nil { + return nil, err + } + } + + printStats = func() { + str, _ := elem.GetProperty("stats") + if str != nil { + if v, ok := str.(*gst.Structure); ok { + logger.Infow("UDP input stats", "stats", v.String()) + } + } + } } else { return nil, errors.ErrUnsupportedURLFormat } From 4d6eb945dd1604691934afc7b02121f82cc7c8e4 Mon Sep 17 00:00:00 2001 From: Benjamin Pracht Date: Thu, 27 Aug 2026 16:33:19 -0700 Subject: [PATCH 2/5] Fix static checks --- pkg/media/urlpull/source.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/media/urlpull/source.go b/pkg/media/urlpull/source.go index 9334cd5c..6b0bb844 100644 --- a/pkg/media/urlpull/source.go +++ b/pkg/media/urlpull/source.go @@ -86,7 +86,7 @@ func NewURLSource(_ context.Context, p *params.Params) (*URLSource, error) { } } } - } else if p.Config.EnableUDPURLPull && strings.HasPrefix(p.Url, "udp://") { + } else if p.EnableUDPURLPull && strings.HasPrefix(p.Url, "udp://") { elem, err = gst.NewElement("udpsrc") if err != nil { return nil, err @@ -96,8 +96,8 @@ func NewURLSource(_ context.Context, p *params.Params) (*URLSource, error) { return nil, err } - if p.Config.MulticastInterface != "" { - err = elem.SetProperty("multicast-iface", p.Config.MulticastInterface) + if p.MulticastInterface != "" { + err = elem.SetProperty("multicast-iface", p.MulticastInterface) if err != nil { return nil, err } From e5c702271ae234b9fa82a6dd378aa4e2255e4965 Mon Sep 17 00:00:00 2001 From: Benjamin Pracht Date: Thu, 27 Aug 2026 16:41:22 -0700 Subject: [PATCH 3/5] Drop the no-op stats logging for UDP url pull udpsrc has no stats property, so the periodic GetProperty("stats") call always failed and logged nothing. Leave printStats unset for UDP, which makes Start skip the ticker goroutine entirely. Co-Authored-By: Claude Opus 5 (1M context) --- pkg/media/urlpull/source.go | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/pkg/media/urlpull/source.go b/pkg/media/urlpull/source.go index 6b0bb844..b676ca11 100644 --- a/pkg/media/urlpull/source.go +++ b/pkg/media/urlpull/source.go @@ -103,14 +103,7 @@ func NewURLSource(_ context.Context, p *params.Params) (*URLSource, error) { } } - printStats = func() { - str, _ := elem.GetProperty("stats") - if str != nil { - if v, ok := str.(*gst.Structure); ok { - logger.Infow("UDP input stats", "stats", v.String()) - } - } - } + // udpsrc doesn't expose a stats property, so leave printStats unset } else { return nil, errors.ErrUnsupportedURLFormat } From 03b6610f2f85681ca43ded2bd788166bd224fd12 Mon Sep 17 00:00:00 2001 From: Benjamin Pracht Date: Thu, 27 Aug 2026 17:18:02 -0700 Subject: [PATCH 4/5] Document the new UDP url pull config entries in the README Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index ae5647c2..5466e5ae 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,8 @@ rtmp_port: port to listen to incoming RTMP connection on (default 1935) whip_port: port to listen to incoming WHIP calls on (default 8080) http_relay_port: port used to relay data from the main service process to the per ingress handler process (default 9090) rtc_config: configuration for ICE and other RTC related settings, same settings livekit-server RTC configuration. Used for WHIP. +enable_udp_url_pull: allow URL pull ingresses to pull from udp:// urls (default false) +multicast_interface: network interface to join multicast groups on for UDP url pull. Empty lets the OS decide # cpu costs for various Ingress types with their default values cpu_cost: From 9fc7cb18b8002738cdf820bc734d44263d7b4081 Mon Sep 17 00:00:00 2001 From: Benjamin Pracht Date: Tue, 1 Sep 2026 11:25:30 -0700 Subject: [PATCH 5/5] Document the security implications of UDP url pull udpsrc binds a local socket on the caller supplied address and port instead of connecting out like the http and srt sources do. Spell out what that means for operators in both the config field doc comment and the README: caller controlled local port binding, unauthenticated and spoofable input, and multicast relaying of traffic on the handler's local network. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 17 ++++++++++++++++- pkg/config/config.go | 14 +++++++++++++- 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 5466e5ae..7d3dac75 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ rtmp_port: port to listen to incoming RTMP connection on (default 1935) whip_port: port to listen to incoming WHIP calls on (default 8080) http_relay_port: port used to relay data from the main service process to the per ingress handler process (default 9090) rtc_config: configuration for ICE and other RTC related settings, same settings livekit-server RTC configuration. Used for WHIP. -enable_udp_url_pull: allow URL pull ingresses to pull from udp:// urls (default false) +enable_udp_url_pull: allow URL pull ingresses to pull from udp:// urls (default false, see the security note below) multicast_interface: network interface to join multicast groups on for UDP url pull. Empty lets the OS decide # cpu costs for various Ingress types with their default values @@ -75,6 +75,21 @@ cpu_cost: The config file can be added to a mounted volume with its location passed in the INGRESS_CONFIG_FILE env var, or its body can be passed in the INGRESS_CONFIG_BODY env var. +> **Security note on `enable_udp_url_pull`** +> +> Only enable UDP url pull if you trust both the callers allowed to create ingresses and the network the +> ingress handlers run on. Unlike `http://` and `srt://` urls, a `udp://` url doesn't make the handler +> connect out to the url host: the handler binds a local socket on the address and port taken from the +> url, and joins the multicast group if one is given. As a result, a caller creating a URL pull ingress +> can: +> +> - Choose which local port the handler binds, potentially colliding with other services on the host. +> - Have the handler ingest unauthenticated traffic. UDP is connectionless, so any host able to reach +> that port can inject media into the session, or spoof the sender address to disrupt a legitimate feed. +> - Have the handler join arbitrary multicast groups and republish whatever it receives into a LiveKit +> room, using the ingress as a relay for streams on the handler's local network that the caller has no +> direct access to. + In order for the LiveKit server to be able to create Ingress sessions, an `ingress` section must also be added to the livekit-server configuration: ```yaml diff --git a/pkg/config/config.go b/pkg/config/config.go index 2bbf2f38..b98d6c7d 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -61,7 +61,19 @@ type ServiceConfig struct { Logging logger.Config `yaml:"logging"` Development bool `yaml:"development"` PSRPCSkipClaim bool `yaml:"psrpc_skip_claim,omitempty"` // Lets psrpc servers skip the claim handshake on queue rpcs - EnableUDPURLPull bool `yaml:"enable_udp_url_pull,omitempty"` + // Allow URL pull ingresses to pull from udp:// urls. Disabled by default, and should only be + // enabled on deployments where both the API callers and the network the handlers run on are trusted. + // Unlike the http and srt sources, udpsrc doesn't connect out to the url host: it binds a local + // socket on the address and port taken from the caller provided url, and joins the multicast group + // if one is given. This has a few consequences: + // - The caller controls which local port the handler binds, and can collide with other services + // running on the host. + // - UDP is connectionless and unauthenticated, so any host able to reach that port can inject + // media into the session, or spoof the sender address to disrupt a legitimate feed. + // - The caller can make the handler join arbitrary multicast groups and republish whatever + // traffic it receives into a LiveKit room, turning the ingress into a relay for streams on + // the handler's local network that the caller couldn't otherwise reach. + EnableUDPURLPull bool `yaml:"enable_udp_url_pull,omitempty"` // Network interface to join multicast groups on for UDP url pull. Empty means let the OS decide. MulticastInterface string `yaml:"multicast_interface,omitempty"`