[PATCH v2] avformat/rtmpproto: add rtmp_strict_paths option

Renato CRON via ffmpeg-devel <[email protected]> Sat, 1 Aug 2026 23:58:41 -0300
Newsgroups gmane.comp.video.ffmpeg.devel
Message-ID <[email protected]>
From: Will Martin <[email protected]>

In a multi-tenant RTMP ingest that assigns one listening port per
customer, ports are necessarily reused as customers come and go. A
customer that has been moved to a different port, but whose encoder is
still configured with the old one, will reconnect to a port that now
belongs to somebody else. Today the server logs a warning about the
mismatched application and stream name and then ingests the stream
anyway, so one tenant's video ends up in another tenant's pipeline.

The application and stream name in the URL are the only per-tenant
discriminator available at the protocol level, so make it possible to
enforce them. With -rtmp_strict_paths 1, a mismatch of the connect
"app" field or of the published stream name against the listening URL
fails the connection instead of only logging a warning. The default
behavior is unchanged.

This is a filter on which peers an ingest accepts, not authentication:
the path is transmitted in clear by plain RTMP and offers no secrecy
against an attacker who can observe or guess it. It is intended to stop
stale and misconfigured clients from landing on the wrong tenant.

The option is protocol level, so the existing -strict /
strict_std_compliance cannot be used for it: URLContext has no
compliance field, and strict_std_compliance describes specification
conformance rather than which peers an ingest is willing to accept.

Signed-off-by: Will Martin <[email protected]>
[renato.cron: rebased on master; dropped the librtmp.c hunk; fixed the
 packet leak in read_connect(); added documentation; reworded the log
 messages]
Signed-off-by: Renato CRON <[email protected]>
---

Resubmission of a patch from 2019 that never got applied:
https://patchwork.ffmpeg.org/project/ffmpeg/patch/[email protected]/

I hit exactly the problem it describes in production and would like to
revive it. Authorship is left with Will Martin since the design and most
of the code are theirs; I have kept them in CC in case they object to
this being carried forward. Rebased onto current master and updated to
address the review comments the original submission received:

 - Michael: the option is now documented in doc/protocols.texi.

 - Reino: "App field don't match up" -> "doesn't". The pre-existing
   warning is corrected too so both messages read the same.

 - Carl Eugen: I kept a dedicated option instead of reusing -strict.
   strict_std_compliance lives on AVCodecContext/AVFormatContext, while
   this check runs in a URLProtocol, and URLContext has no compliance
   field to consult. The meaning also differs: strict_std_compliance is
   about conforming to a specification, not about which peers an ingest
   is willing to accept. I am happy to change this if you disagree.

Two errors in the original patch are fixed as well:

 - The libavformat/librtmp.c hunk is dropped entirely. It declared the
   field as bool without including <stdbool.h>, which does not compile
   under -std=c17, and it declared an AV_OPT_TYPE_BOOL option against a
   bool field, which av_opt_set_int would write to as an int. It was
   dead weight regardless: librtmp.c implements no listen mode, so
   nothing there could ever read the flag.

 - The new error path in read_connect() returned without calling
   ff_rtmp_packet_destroy(), leaking the connect packet.

Testing, ffmpeg acting as both listener and publisher over loopback:

  strict=1, stream name mismatch -> connection rejected, EIO
  strict=1, app name mismatch    -> connection rejected, EIO
  strict=1, everything matches   -> stream received normally
  strict=0, stream name mismatch -> warning only, stream received,
                                    i.e. current behavior preserved

The leak fix was confirmed with a gcc-asan build: the app-mismatch case
reports "Direct leak of 141 byte(s) in 1 object(s)" without the
ff_rtmp_packet_destroy() call and is clean with it.

One limitation worth stating plainly: this does not make -listen 1 a
persistent server. ff_listen_bind() accepts a single connection and then
closes the listening socket, so a rejected client ends the process
rather than freeing the port for the next one. That is pre-existing
behavior and not something this option changes, but it does mean a
stale client that reconnects in a loop can keep a supervised ingest
flapping. I still consider refusing the stream better than ingesting it
into the wrong tenant.
 doc/protocols.texi      |  7 +++++++
 libavformat/rtmpproto.c | 23 ++++++++++++++++++++---
 2 files changed, 27 insertions(+), 3 deletions(-)

diff --git a/doc/protocols.texi b/doc/protocols.texi
index f7e65b1c9d..0f9cceb19f 100644
--- a/doc/protocols.texi
+++ b/doc/protocols.texi
@@ -977,6 +977,13 @@ value will be sent.
 Stream identifier to play or to publish. This option overrides the
 parameter specified in the URI.

+@item rtmp_strict_paths=@var{1|0}
+When listening for incoming connections, reject a connection whose
+application name or stream name does not match the one given in the
+listening URL, instead of only logging a warning and accepting it.
+Useful when several RTMP ingest points share the same port. Default
+value is 0.
+
 @item rtmp_subscribe
 Name of live stream to subscribe to. By default no value will be sent.
 It is only sent if the option is specified or if rtmp_live
diff --git a/libavformat/rtmpproto.c b/libavformat/rtmpproto.c
index 785455e06d..74ca6bd016 100644
--- a/libavformat/rtmpproto.c
+++ b/libavformat/rtmpproto.c
@@ -134,6 +134,7 @@ typedef struct RTMPContext {
     char          auth_params[500];
     int           do_reconnect;
     int           auth_tried;
+    int           strict_paths;
 } RTMPContext;

 #define PLAYER_KEY_OPEN_PART_LEN 30   ///< length of partial key used for first client digest signing
@@ -540,9 +541,17 @@ static int read_connect(URLContext *s, RTMPContext *rt)
                                  "app", tmpstr, sizeof(tmpstr));
     if (ret)
         av_log(s, AV_LOG_WARNING, "App field not found in connect\n");
-    if (!ret && strcmp(tmpstr, rt->app))
-        av_log(s, AV_LOG_WARNING, "App field don't match up: %s <-> %s\n",
+    if (!ret && strcmp(tmpstr, rt->app)) {
+        if (rt->strict_paths) {
+            av_log(s, AV_LOG_ERROR, "App field doesn't match up: %s <-> %s, "
+                   "rejecting connection since rtmp_strict_paths is set\n",
+                   tmpstr, rt->app);
+            ff_rtmp_packet_destroy(&pkt);
+            return AVERROR(EIO);
+        }
+        av_log(s, AV_LOG_WARNING, "App field doesn't match up: %s <-> %s\n",
                tmpstr, rt->app);
+    }
     ff_rtmp_packet_destroy(&pkt);

     // Send Window Acknowledgement Size (as defined in specification)
@@ -2029,9 +2038,16 @@ static int send_invoke_response(URLContext *s, RTMPPacket *pkt)
                 pchar = s->filename;
             }
             pchar++;
-            if (strcmp(pchar, filename))
+            if (strcmp(pchar, filename)) {
+                if (rt->strict_paths) {
+                    av_log(s, AV_LOG_ERROR, "Unexpected stream %s, expecting %s, "
+                           "rejecting publish since rtmp_strict_paths is set\n",
+                           filename, pchar);
+                    return AVERROR(EIO);
+                }
                 av_log(s, AV_LOG_WARNING, "Unexpected stream %s, expecting"
                        " %s\n", filename, pchar);
+            }
         }
         rt->state = STATE_RECEIVING;
     }
@@ -3215,6 +3231,7 @@ static const AVOption rtmp_options[] = {
     {"recorded", "recorded stream", 0, AV_OPT_TYPE_CONST, {.i64 = 0}, 0, 0, DEC, .unit = "rtmp_live"},
     {"rtmp_pageurl", "URL of the web page in which the media was embedded. By default no value will be sent.", OFFSET(pageurl), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC},
     {"rtmp_playpath", "Stream identifier to play or to publish", OFFSET(playpath), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC|ENC},
+    {"rtmp_strict_paths", "Error instead of warn for mismatch on stream or application path in url", OFFSET(strict_paths), AV_OPT_TYPE_BOOL, {.i64 = 0 }, 0, 1, DEC},
     {"rtmp_subscribe", "Name of live stream to subscribe to. Defaults to rtmp_playpath.", OFFSET(subscribe), AV_OPT_TYPE_STRING, {.str = NULL }, 0, 0, DEC},
     {"rtmp_swfhash", "SHA256 hash of the decompressed SWF file (32 bytes).", OFFSET(swfhash), AV_OPT_TYPE_BINARY, .flags = DEC},
     {"rtmp_swfsize", "Size of the decompressed SWF file, required for SWFVerification.", OFFSET(swfsize), AV_OPT_TYPE_INT, {.i64 = 0}, 0, INT_MAX, DEC},
--
2.34.1

_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]