PR #24565 opened by dangowrt
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24565
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/24565.patch

# Summary of changes

On a multi-homed host, `-local_addr` binds only the first connection of a 
segmented stream. `ffio_copy_url_options()` decides what the child connections 
of hls, dash and imf inherit, and it can only carry options it is able to read 
back from the parent `AVIOContext`. `local_addr` lives on the innermost TCP 
context, which that lookup cannot reach: a `URLContext` offers its own private 
context as an AVOptions child, so a lookup on an http connection finds the HTTP 
options and stops there. The result is that the playlist leaves through the 
interface the caller asked for while every segment goes out of whichever one 
the route table picks, and a service that geolocates serves the playlist and 
then refuses the segments.

Exposing the nested context as another AVOptions child would fix the lookup but 
change option resolution everywhere: `av_opt_find2()` searches children before 
the object itself, so tcp would shadow the http options that share a name with 
it, `listen` and `timeout` among them. The first patch therefore adds an 
explicit `url_get_nested()` protocol callback, implemented for http and tls, 
which lets a caller walk the chain deliberately and leaves AVOptions semantics 
untouched. The second patch walks that chain in `ffio_copy_url_options()` and 
carries `local_addr` and `local_port`.

Tested on a host with two uplinks, playing a Twitch HLS stream through the 
interface named by `local_addr`: before the change `netstat` shows the playlist 
connection bound to the chosen address and every segment connection bound to 
the default route, afterwards all of them are bound to the chosen address. 
`make fate` passes, 2846 tests, on a gnutls build, and the series also 
cross-builds for aarch64 against mbedtls.


>From bfb923cc44f184f2a7104c50b69f91089ebad16f Mon Sep 17 00:00:00 2001
From: Daniel Golle <[email protected]>
Date: Thu, 17 Sep 2026 23:11:18 +0100
Subject: [PATCH 1/2] avformat: let a protocol expose the one it runs over

Options of a nested protocol cannot be read back from the enclosing one.
A URLContext offers its own private context as an AVOptions child, so a
lookup on an http connection reaches the HTTP options and stops there,
while the TCP options underneath, among them the local bind address,
stay out of reach. Exposing the nested context as another AVOptions
child would not do: children are searched before the object itself, so
tcp would shadow the http options that share a name with it, listen and
timeout among them.

Add url_get_nested() and implement it for http and tls, which lets a
caller walk the chain deliberately. It has no user yet.

Signed-off-by: Daniel Golle <[email protected]>
---
 libavformat/avio.c                |  7 +++++++
 libavformat/http.c                |  8 ++++++++
 libavformat/tls.c                 |  7 +++++++
 libavformat/tls.h                 |  2 ++
 libavformat/tls_gnutls.c          |  2 ++
 libavformat/tls_libtls.c          |  1 +
 libavformat/tls_mbedtls.c         |  2 ++
 libavformat/tls_openssl.c         |  2 ++
 libavformat/tls_schannel.c        |  2 ++
 libavformat/tls_securetransport.c |  1 +
 libavformat/url.h                 | 12 ++++++++++++
 11 files changed, 46 insertions(+)

diff --git a/libavformat/avio.c b/libavformat/avio.c
index a2c94e4384..afc2a5d392 100644
--- a/libavformat/avio.c
+++ b/libavformat/avio.c
@@ -919,6 +919,13 @@ int ffurl_get_short_seek(void *urlcontext)
     return h->prot->url_get_short_seek(h);
 }
 
+URLContext *ffurl_get_nested(URLContext *h)
+{
+    if (!h || !h->prot || !h->prot->url_get_nested)
+        return NULL;
+    return h->prot->url_get_nested(h);
+}
+
 int ffurl_shutdown(URLContext *h, int flags)
 {
     if (!h || !h->prot || !h->prot->url_shutdown)
diff --git a/libavformat/http.c b/libavformat/http.c
index fff0f25e36..f9d7854943 100644
--- a/libavformat/http.c
+++ b/libavformat/http.c
@@ -2282,6 +2282,12 @@ static int http_get_short_seek(URLContext *h)
     return ffurl_get_short_seek(s->hd);
 }
 
+static URLContext *http_get_nested(URLContext *h)
+{
+    HTTPContext *s = h->priv_data;
+    return s->hd;
+}
+
 #define HTTP_CLASS(flavor)                          \
 static const AVClass flavor ## _context_class = {   \
     .class_name = # flavor,                         \
@@ -2304,6 +2310,7 @@ const URLProtocol ff_http_protocol = {
     .url_close           = http_close,
     .url_get_file_handle = http_get_file_handle,
     .url_get_short_seek  = http_get_short_seek,
+    .url_get_nested      = http_get_nested,
     .url_shutdown        = http_shutdown,
     .priv_data_size      = sizeof(HTTPContext),
     .priv_data_class     = &http_context_class,
@@ -2324,6 +2331,7 @@ const URLProtocol ff_https_protocol = {
     .url_close           = http_close,
     .url_get_file_handle = http_get_file_handle,
     .url_get_short_seek  = http_get_short_seek,
+    .url_get_nested      = http_get_nested,
     .url_shutdown        = http_shutdown,
     .priv_data_size      = sizeof(HTTPContext),
     .priv_data_class     = &https_context_class,
diff --git a/libavformat/tls.c b/libavformat/tls.c
index 3eab305f56..81aa9b4362 100644
--- a/libavformat/tls.c
+++ b/libavformat/tls.c
@@ -51,6 +51,13 @@ int ff_tls_parse_host(TLSShared *s, char *hostname, int 
hostname_size, int *port
     return 0;
 }
 
+URLContext *ff_tls_get_nested(URLContext *h)
+{
+    TLSShared *c = h->priv_data;
+
+    return c->is_dtls ? c->udp : c->tcp;
+}
+
 int ff_tls_open_underlying(TLSShared *c, URLContext *parent, const char *uri, 
AVDictionary **options)
 {
     int port;
diff --git a/libavformat/tls.h b/libavformat/tls.h
index 570c245ff4..79cb1b6526 100644
--- a/libavformat/tls.h
+++ b/libavformat/tls.h
@@ -112,6 +112,8 @@ int ff_tls_parse_host(TLSShared *s, char *hostname, int 
hostname_size, int *port
 
 int ff_tls_open_underlying(TLSShared *c, URLContext *parent, const char *uri, 
AVDictionary **options);
 
+URLContext *ff_tls_get_nested(URLContext *h);
+
 int ff_url_read_all(const char *url, AVBPrint *bp);
 
 int ff_tls_set_external_socket(URLContext *h, URLContext *sock);
diff --git a/libavformat/tls_gnutls.c b/libavformat/tls_gnutls.c
index aedbc66e56..d673c17b64 100644
--- a/libavformat/tls_gnutls.c
+++ b/libavformat/tls_gnutls.c
@@ -776,6 +776,7 @@ const URLProtocol ff_tls_protocol = {
     .url_close      = tls_close,
     .url_get_file_handle = tls_get_file_handle,
     .url_get_short_seek  = tls_get_short_seek,
+    .url_get_nested      = ff_tls_get_nested,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &tls_class,
@@ -797,6 +798,7 @@ const URLProtocol ff_dtls_protocol = {
     .url_close      = tls_close,
     .url_get_file_handle = tls_get_file_handle,
     .url_get_short_seek  = tls_get_short_seek,
+    .url_get_nested      = ff_tls_get_nested,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &dtls_class,
diff --git a/libavformat/tls_libtls.c b/libavformat/tls_libtls.c
index 7210f6cd14..fdba7ed659 100644
--- a/libavformat/tls_libtls.c
+++ b/libavformat/tls_libtls.c
@@ -211,6 +211,7 @@ const URLProtocol ff_tls_protocol = {
     .url_close      = ff_tls_close,
     .url_get_file_handle = tls_get_file_handle,
     .url_get_short_seek  = tls_get_short_seek,
+    .url_get_nested      = ff_tls_get_nested,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &tls_class,
diff --git a/libavformat/tls_mbedtls.c b/libavformat/tls_mbedtls.c
index 444148be8a..15b21477be 100644
--- a/libavformat/tls_mbedtls.c
+++ b/libavformat/tls_mbedtls.c
@@ -810,6 +810,7 @@ const URLProtocol ff_tls_protocol = {
     .url_close      = tls_close,
     .url_get_file_handle = tls_get_file_handle,
     .url_get_short_seek  = tls_get_short_seek,
+    .url_get_nested      = ff_tls_get_nested,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &tls_class,
@@ -831,6 +832,7 @@ const URLProtocol ff_dtls_protocol = {
     .url_close      = tls_close,
     .url_get_file_handle = tls_get_file_handle,
     .url_get_short_seek  = tls_get_short_seek,
+    .url_get_nested      = ff_tls_get_nested,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &dtls_class,
diff --git a/libavformat/tls_openssl.c b/libavformat/tls_openssl.c
index 48b4a2226a..f5b03afca1 100644
--- a/libavformat/tls_openssl.c
+++ b/libavformat/tls_openssl.c
@@ -1007,6 +1007,7 @@ const URLProtocol ff_tls_protocol = {
     .url_close      = tls_close,
     .url_get_file_handle = tls_get_file_handle,
     .url_get_short_seek  = tls_get_short_seek,
+    .url_get_nested      = ff_tls_get_nested,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &tls_class,
@@ -1028,6 +1029,7 @@ const URLProtocol ff_dtls_protocol = {
     .url_write      = tls_write,
     .url_get_file_handle = tls_get_file_handle,
     .url_get_short_seek  = tls_get_short_seek,
+    .url_get_nested      = ff_tls_get_nested,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &dtls_class,
diff --git a/libavformat/tls_schannel.c b/libavformat/tls_schannel.c
index 6708302b65..6a65c6c17e 100644
--- a/libavformat/tls_schannel.c
+++ b/libavformat/tls_schannel.c
@@ -1472,6 +1472,7 @@ const URLProtocol ff_tls_protocol = {
     .url_close      = tls_close,
     .url_get_file_handle = tls_get_file_handle,
     .url_get_short_seek  = tls_get_short_seek,
+    .url_get_nested      = ff_tls_get_nested,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &tls_class,
@@ -1495,6 +1496,7 @@ const URLProtocol ff_dtls_protocol = {
     .url_write      = tls_write,
     .url_get_file_handle = tls_get_file_handle,
     .url_get_short_seek  = tls_get_short_seek,
+    .url_get_nested      = ff_tls_get_nested,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &dtls_class,
diff --git a/libavformat/tls_securetransport.c 
b/libavformat/tls_securetransport.c
index 01965439e9..2b980a5e29 100644
--- a/libavformat/tls_securetransport.c
+++ b/libavformat/tls_securetransport.c
@@ -424,6 +424,7 @@ const URLProtocol ff_tls_protocol = {
     .url_close      = tls_close,
     .url_get_file_handle = tls_get_file_handle,
     .url_get_short_seek  = tls_get_short_seek,
+    .url_get_nested      = ff_tls_get_nested,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &tls_class,
diff --git a/libavformat/url.h b/libavformat/url.h
index 1075014333..b22fed6e61 100644
--- a/libavformat/url.h
+++ b/libavformat/url.h
@@ -86,6 +86,10 @@ typedef struct URLProtocol {
     int (*url_get_multi_file_handle)(URLContext *h, int **handles,
                                      int *numhandles);
     int (*url_get_short_seek)(URLContext *h);
+    /**
+     * Return the protocol this one runs over, NULL while not connected.
+     */
+    URLContext *(*url_get_nested)(URLContext *h);
     int (*url_shutdown)(URLContext *h, int flags);
     const AVClass *priv_data_class;
     int priv_data_size;
@@ -266,6 +270,14 @@ int ffurl_get_multi_file_handle(URLContext *h, int 
**handles, int *numhandles);
  */
 int ffurl_get_short_seek(void *urlcontext);
 
+/**
+ * Return the protocol this URL runs over, for example the TCP connection
+ * carrying an HTTP request.
+ *
+ * @return the nested resource, or NULL if there is none.
+ */
+URLContext *ffurl_get_nested(URLContext *h);
+
 /**
  * Signal the URLContext that we are done reading or writing the stream.
  *
-- 
2.52.0


>From 822ef3e26d27e2cf315204e805b16226d3d0562f Mon Sep 17 00:00:00 2001
From: Daniel Golle <[email protected]>
Date: Thu, 17 Sep 2026 23:11:25 +0100
Subject: [PATCH 2/2] avformat/aviobuf: inherit the bind address in child
 connections

A segmented format opens the playlist and then a connection per segment,
and ffio_copy_url_options() decides what those children inherit. It
carries the HTTP identity options but not local_addr, so on a multi-homed
host the playlist leaves through the interface the caller asked for while
every segment goes out of whichever one the route table picks. A service
that geolocates then serves the playlist and refuses the segments.

Walk the protocol chain with ffurl_get_nested() so an option living
below the outermost protocol is found, and carry local_addr and
local_port.

Signed-off-by: Daniel Golle <[email protected]>
---
 libavformat/aviobuf.c | 36 +++++++++++++++++++++++++++---------
 1 file changed, 27 insertions(+), 9 deletions(-)

diff --git a/libavformat/aviobuf.c b/libavformat/aviobuf.c
index edbbc5ffd6..5b8e46f5ae 100644
--- a/libavformat/aviobuf.c
+++ b/libavformat/aviobuf.c
@@ -31,6 +31,7 @@
 #include "avio.h"
 #include "avio_internal.h"
 #include "internal.h"
+#include "url.h"
 #include <stdarg.h>
 
 #define IO_BUFFER_SIZE 32768
@@ -950,23 +951,40 @@ void ffio_write_lines(AVIOContext *s, const unsigned char 
*buf, int size,
     }
 }
 
+static uint8_t *url_option_find(AVIOContext *pb, const char *name)
+{
+    URLContext *h = ffio_geturlcontext(pb);
+    uint8_t *buf = NULL;
+
+    if (av_opt_get(pb, name, AV_OPT_SEARCH_CHILDREN, &buf) >= 0 && buf[0] != 
'\0')
+        return buf;
+
+    av_freep(&buf);
+
+    for (h = ffurl_get_nested(h); h; h = ffurl_get_nested(h)) {
+        if (av_opt_get(h, name, AV_OPT_SEARCH_CHILDREN, &buf) >= 0 && buf[0] 
!= '\0')
+            return buf;
+        av_freep(&buf);
+    }
+
+    return NULL;
+}
+
 int ffio_copy_url_options(AVIOContext* pb, AVDictionary** avio_opts)
 {
     const char *opts[] = {
-        "headers", "user_agent", "cookies", "http_proxy", "referer", 
"rw_timeout", "icy", "prefer_libcurl", NULL };
+        "headers", "user_agent", "cookies", "http_proxy", "referer", 
"rw_timeout", "icy",
+        "prefer_libcurl", "local_addr", "local_port", NULL };
     const char **opt = opts;
     uint8_t *buf = NULL;
     int ret = 0;
 
     while (*opt) {
-        if (av_opt_get(pb, *opt, AV_OPT_SEARCH_CHILDREN, &buf) >= 0) {
-            if (buf[0] != '\0') {
-                ret = av_dict_set(avio_opts, *opt, buf, 
AV_DICT_DONT_STRDUP_VAL);
-                if (ret < 0)
-                    return ret;
-            } else {
-                av_freep(&buf);
-            }
+        buf = url_option_find(pb, *opt);
+        if (buf) {
+            ret = av_dict_set(avio_opts, *opt, buf, AV_DICT_DONT_STRDUP_VAL);
+            if (ret < 0)
+                return ret;
         }
         opt++;
     }
-- 
2.52.0

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

Reply via email to