Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
 help / color / mirror / Atom feed
* [FFmpeg-devel] [PATCH v2 0/8] WHIP + TLS + UDP fixes and SChannel DTLS support
@ 2025-07-06 18:36 Timo Rothenpieler
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 1/8] avformat/tls: move whip specific init out of generic tls code Timo Rothenpieler
                   ` (7 more replies)
  0 siblings, 8 replies; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-06 18:36 UTC (permalink / raw)
  To: ffmpeg-devel; +Cc: Timo Rothenpieler

Second version of the series, with a bunch of the more trivial already
merged it reduced in size a bit.
Remaining patches have been revised after comments, and some further fixes
have been added.

Original Cover-Letter was:

This was originally just me attempting to add DTLS support to SChannel,
so it can be used with the WHIP protocol. But on the way there, a lot of
random fixes and enhancements accumulated, resulting in this series.

The main new features are DTLS support for SChannel, which also happens
to enable support for listen-mode, which was previous unsupported for
normal TLS as well.

To enable that, udp.c had to be enhanced to allow reporting the address
a packet was received from.
In the process of that I realized that udp.c was clearly not made with
bidirectional communication in mind, so that had to be fixed as well (
The fifo buffer was used by both read and write without any checks,
meaning the two would interfere with each other).

The rest are misc fixes for issues in WHIP and the associated new tls
code I found.

Timo Rothenpieler (8):
  avformat/tls: move whip specific init out of generic tls code
  avformat/udp: make recv addr of each packet available
  avformat/udp: separate rx and tx fifo
  avformat/udp: add function to set remote address directly
  avformat/tls: make passing an external socket universal
  avformat/tls_schannel: add DTLS support
  avformat/tls_schannel: add option to load server certificate from
    store
  avformat/tls_schannel: fix non-blocking write breaking TLS sessions

 configure                  |    6 +-
 libavformat/network.h      |    3 +
 libavformat/tls.c          |    9 -
 libavformat/tls.h          |   11 +-
 libavformat/tls_openssl.c  |   26 +-
 libavformat/tls_schannel.c | 1025 ++++++++++++++++++++++++++++++++----
 libavformat/udp.c          |   98 +++-
 libavformat/whip.c         |    9 +-
 8 files changed, 1044 insertions(+), 143 deletions(-)

-- 
2.49.0

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* [FFmpeg-devel] [PATCH v2 1/8] avformat/tls: move whip specific init out of generic tls code
  2025-07-06 18:36 [FFmpeg-devel] [PATCH v2 0/8] WHIP + TLS + UDP fixes and SChannel DTLS support Timo Rothenpieler
@ 2025-07-06 18:36 ` Timo Rothenpieler
  2025-07-07  6:30   ` Jack Lau
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 2/8] avformat/udp: make recv addr of each packet available Timo Rothenpieler
                   ` (6 subsequent siblings)
  7 siblings, 1 reply; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-06 18:36 UTC (permalink / raw)
  To: ffmpeg-devel; +Cc: Timo Rothenpieler

---
 libavformat/tls.c         |  9 ---------
 libavformat/tls_openssl.c | 12 ++++++++----
 libavformat/whip.c        |  5 +++++
 3 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/libavformat/tls.c b/libavformat/tls.c
index c0adaf61ce..bd9c05e6dc 100644
--- a/libavformat/tls.c
+++ b/libavformat/tls.c
@@ -141,15 +141,6 @@ int ff_tls_open_underlying(TLSShared *c, URLContext *parent, const char *uri, AV
     ret = ffurl_open_whitelist(c->is_dtls ? &c->udp : &c->tcp, buf, AVIO_FLAG_READ_WRITE,
                                &parent->interrupt_callback, options,
                                parent->protocol_whitelist, parent->protocol_blacklist, parent);
-    if (c->is_dtls) {
-        if (ret < 0) {
-            av_log(c, AV_LOG_ERROR, "Failed to open udp://%s:%d\n", c->underlying_host, port);
-            return ret;
-        }
-        /* Make the socket non-blocking, set to READ and WRITE mode after connected */
-        ff_socket_nonblock(ffurl_get_file_handle(c->udp), 1);
-        c->udp->flags |= AVIO_FLAG_READ | AVIO_FLAG_NONBLOCK;
-    }
     return ret;
 }
 
diff --git a/libavformat/tls_openssl.c b/libavformat/tls_openssl.c
index 08527418b0..0c76f110e3 100644
--- a/libavformat/tls_openssl.c
+++ b/libavformat/tls_openssl.c
@@ -1128,14 +1128,16 @@ static int tls_write(URLContext *h, const uint8_t *buf, int size)
 
 static int tls_get_file_handle(URLContext *h)
 {
-    TLSContext *c = h->priv_data;
-    return ffurl_get_file_handle(c->tls_shared.tcp);
+    TLSContext *p = h->priv_data;
+    TLSShared *c = &p->tls_shared;
+    return ffurl_get_file_handle(c->is_dtls ? c->udp : c->tcp);
 }
 
 static int tls_get_short_seek(URLContext *h)
 {
-    TLSContext *s = h->priv_data;
-    return ffurl_get_short_seek(s->tls_shared.tcp);
+    TLSContext *p = h->priv_data;
+    TLSShared *c = &p->tls_shared;
+    return ffurl_get_short_seek(c->is_dtls ? c->udp : c->tcp);
 }
 
 static const AVOption options[] = {
@@ -1177,6 +1179,8 @@ const URLProtocol ff_dtls_protocol = {
     .url_close      = dtls_close,
     .url_read       = tls_read,
     .url_write      = tls_write,
+    .url_get_file_handle = tls_get_file_handle,
+    .url_get_short_seek  = tls_get_short_seek,
     .priv_data_size = sizeof(TLSContext),
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &dtls_class,
diff --git a/libavformat/whip.c b/libavformat/whip.c
index 84d4c5a1f3..4ac76e79f2 100644
--- a/libavformat/whip.c
+++ b/libavformat/whip.c
@@ -388,6 +388,11 @@ static av_cold int dtls_initialize(AVFormatContext *s)
     WHIPContext *whip = s->priv_data;
     /* reuse the udp created by whip */
     ff_dtls_set_udp(whip->dtls_uc, whip->udp);
+
+    /* Make the socket non-blocking */
+    ff_socket_nonblock(ffurl_get_file_handle(whip->dtls_uc), 1);
+    whip->dtls_uc->flags |= AVIO_FLAG_NONBLOCK;
+
     return 0;
 }
 
-- 
2.49.0

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* [FFmpeg-devel] [PATCH v2 2/8] avformat/udp: make recv addr of each packet available
  2025-07-06 18:36 [FFmpeg-devel] [PATCH v2 0/8] WHIP + TLS + UDP fixes and SChannel DTLS support Timo Rothenpieler
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 1/8] avformat/tls: move whip specific init out of generic tls code Timo Rothenpieler
@ 2025-07-06 18:36 ` Timo Rothenpieler
  2025-07-07  8:03   ` Jack Lau
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 3/8] avformat/udp: separate rx and tx fifo Timo Rothenpieler
                   ` (5 subsequent siblings)
  7 siblings, 1 reply; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-06 18:36 UTC (permalink / raw)
  To: ffmpeg-devel; +Cc: Timo Rothenpieler

---
 libavformat/network.h |  2 ++
 libavformat/udp.c     | 25 +++++++++++++++++--------
 2 files changed, 19 insertions(+), 8 deletions(-)

diff --git a/libavformat/network.h b/libavformat/network.h
index ca214087fc..48bb75a758 100644
--- a/libavformat/network.h
+++ b/libavformat/network.h
@@ -338,4 +338,6 @@ int ff_connect_parallel(struct addrinfo *addrs, int timeout_ms_per_address,
                         int parallel, URLContext *h, int *fd,
                         int (*customize_fd)(void *, int, int), void *customize_ctx);
 
+void ff_udp_get_last_recv_addr(URLContext *h, struct sockaddr_storage *addr);
+
 #endif /* AVFORMAT_NETWORK_H */
diff --git a/libavformat/udp.c b/libavformat/udp.c
index 30f075de8e..7d64ff07ed 100644
--- a/libavformat/udp.c
+++ b/libavformat/udp.c
@@ -107,7 +107,7 @@ typedef struct UDPContext {
     pthread_cond_t cond;
     int thread_started;
 #endif
-    uint8_t tmp[UDP_MAX_PKT_SIZE+4];
+    uint8_t tmp[UDP_MAX_PKT_SIZE + 4 + sizeof(struct sockaddr_storage)];
     int remaining_in_dg;
     char *localaddr;
     int timeout;
@@ -115,6 +115,7 @@ typedef struct UDPContext {
     char *sources;
     char *block;
     IPSourceFilters filters;
+    struct sockaddr_storage last_recv_addr;
 } UDPContext;
 
 #define OFFSET(x) offsetof(UDPContext, x)
@@ -467,6 +468,12 @@ int ff_udp_get_local_port(URLContext *h)
     return s->local_port;
 }
 
+void ff_udp_get_last_recv_addr(URLContext *h, struct sockaddr_storage *addr)
+{
+    UDPContext *s = h->priv_data;
+    *addr = s->last_recv_addr;
+}
+
 /**
  * Return the udp file handle for select() usage to wait for several RTP
  * streams at the same time.
@@ -498,13 +505,14 @@ static void *circular_buffer_task_rx( void *_URLContext)
         int len;
         struct sockaddr_storage addr;
         socklen_t addr_len = sizeof(addr);
+        const int header_sz = 4 + addr_len;
 
         pthread_mutex_unlock(&s->mutex);
         /* Blocking operations are always cancellation points;
            see "General Information" / "Thread Cancelation Overview"
            in Single Unix. */
         pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_cancelstate);
-        len = recvfrom(s->udp_fd, s->tmp+4, sizeof(s->tmp)-4, 0, (struct sockaddr *)&addr, &addr_len);
+        len = recvfrom(s->udp_fd, s->tmp + header_sz, sizeof(s->tmp) - header_sz, 0, (struct sockaddr *)&addr, &addr_len);
         pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelstate);
         pthread_mutex_lock(&s->mutex);
         if (len < 0) {
@@ -517,8 +525,9 @@ static void *circular_buffer_task_rx( void *_URLContext)
         if (ff_ip_check_source_lists(&addr, &s->filters))
             continue;
         AV_WL32(s->tmp, len);
+        memcpy(s->tmp + 4, &addr, sizeof(addr));
 
-        if (av_fifo_can_write(s->fifo) < len + 4) {
+        if (av_fifo_can_write(s->fifo) < len + header_sz) {
             /* No Space left */
             if (s->overrun_nonfatal) {
                 av_log(h, AV_LOG_WARNING, "Circular buffer overrun. "
@@ -532,7 +541,7 @@ static void *circular_buffer_task_rx( void *_URLContext)
                 goto end;
             }
         }
-        av_fifo_write(s->fifo, s->tmp, len + 4);
+        av_fifo_write(s->fifo, s->tmp, len + header_sz);
         pthread_cond_signal(&s->cond);
     }
 
@@ -991,8 +1000,7 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
 {
     UDPContext *s = h->priv_data;
     int ret;
-    struct sockaddr_storage addr;
-    socklen_t addr_len = sizeof(addr);
+    socklen_t addr_len = sizeof(s->last_recv_addr);
 #if HAVE_PTHREAD_CANCEL
     int avail, nonblock = h->flags & AVIO_FLAG_NONBLOCK;
 
@@ -1004,6 +1012,7 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
                 uint8_t tmp[4];
 
                 av_fifo_read(s->fifo, tmp, 4);
+                av_fifo_read(s->fifo, &s->last_recv_addr, sizeof(s->last_recv_addr));
                 avail = AV_RL32(tmp);
                 if(avail > size){
                     av_log(h, AV_LOG_WARNING, "Part of datagram lost due to insufficient buffer size\n");
@@ -1043,10 +1052,10 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
         if (ret < 0)
             return ret;
     }
-    ret = recvfrom(s->udp_fd, buf, size, 0, (struct sockaddr *)&addr, &addr_len);
+    ret = recvfrom(s->udp_fd, buf, size, 0, (struct sockaddr *)&s->last_recv_addr, &addr_len);
     if (ret < 0)
         return ff_neterrno();
-    if (ff_ip_check_source_lists(&addr, &s->filters))
+    if (ff_ip_check_source_lists(&s->last_recv_addr, &s->filters))
         return AVERROR(EINTR);
     return ret;
 }
-- 
2.49.0

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* [FFmpeg-devel] [PATCH v2 3/8] avformat/udp: separate rx and tx fifo
  2025-07-06 18:36 [FFmpeg-devel] [PATCH v2 0/8] WHIP + TLS + UDP fixes and SChannel DTLS support Timo Rothenpieler
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 1/8] avformat/tls: move whip specific init out of generic tls code Timo Rothenpieler
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 2/8] avformat/udp: make recv addr of each packet available Timo Rothenpieler
@ 2025-07-06 18:36 ` Timo Rothenpieler
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 4/8] avformat/udp: add function to set remote address directly Timo Rothenpieler
                   ` (4 subsequent siblings)
  7 siblings, 0 replies; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-06 18:36 UTC (permalink / raw)
  To: ffmpeg-devel; +Cc: Timo Rothenpieler

---
 libavformat/udp.c | 49 +++++++++++++++++++++++++++--------------------
 1 file changed, 28 insertions(+), 21 deletions(-)

diff --git a/libavformat/udp.c b/libavformat/udp.c
index 7d64ff07ed..e02eff0f33 100644
--- a/libavformat/udp.c
+++ b/libavformat/udp.c
@@ -96,7 +96,8 @@ typedef struct UDPContext {
 
     /* Circular Buffer variables for use in UDP receive code */
     int circular_buffer_size;
-    AVFifo *fifo;
+    AVFifo *rx_fifo;
+    AVFifo *tx_fifo;
     int circular_buffer_error;
     int64_t bitrate; /* number of bits to send per second */
     int64_t burst_bits;
@@ -527,7 +528,7 @@ static void *circular_buffer_task_rx( void *_URLContext)
         AV_WL32(s->tmp, len);
         memcpy(s->tmp + 4, &addr, sizeof(addr));
 
-        if (av_fifo_can_write(s->fifo) < len + header_sz) {
+        if (av_fifo_can_write(s->rx_fifo) < len + header_sz) {
             /* No Space left */
             if (s->overrun_nonfatal) {
                 av_log(h, AV_LOG_WARNING, "Circular buffer overrun. "
@@ -541,7 +542,7 @@ static void *circular_buffer_task_rx( void *_URLContext)
                 goto end;
             }
         }
-        av_fifo_write(s->fifo, s->tmp, len + header_sz);
+        av_fifo_write(s->rx_fifo, s->tmp, len + header_sz);
         pthread_cond_signal(&s->cond);
     }
 
@@ -577,22 +578,22 @@ static void *circular_buffer_task_tx( void *_URLContext)
         uint8_t tmp[4];
         int64_t timestamp;
 
-        len = av_fifo_can_read(s->fifo);
+        len = av_fifo_can_read(s->tx_fifo);
 
         while (len<4) {
             if (s->close_req)
                 goto end;
             pthread_cond_wait(&s->cond, &s->mutex);
-            len = av_fifo_can_read(s->fifo);
+            len = av_fifo_can_read(s->tx_fifo);
         }
 
-        av_fifo_read(s->fifo, tmp, 4);
+        av_fifo_read(s->tx_fifo, tmp, 4);
         len = AV_RL32(tmp);
 
         av_assert0(len >= 0);
         av_assert0(len <= sizeof(s->tmp));
 
-        av_fifo_read(s->fifo, s->tmp, len);
+        av_fifo_read(s->tx_fifo, s->tmp, len);
 
         pthread_mutex_unlock(&s->mutex);
 
@@ -944,11 +945,15 @@ static int udp_open(URLContext *h, const char *uri, int flags)
 
     if ((!is_output && s->circular_buffer_size) || (is_output && s->bitrate && s->circular_buffer_size)) {
         /* start the task going */
-        s->fifo = av_fifo_alloc2(s->circular_buffer_size, 1, 0);
-        if (!s->fifo) {
+        AVFifo *fifo = av_fifo_alloc2(s->circular_buffer_size, 1, 0);
+        if (!fifo) {
             ret = AVERROR(ENOMEM);
             goto fail;
         }
+        if (is_output)
+            s->tx_fifo = fifo;
+        else
+            s->rx_fifo = fifo;
         ret = pthread_mutex_init(&s->mutex, NULL);
         if (ret != 0) {
             av_log(h, AV_LOG_ERROR, "pthread_mutex_init failed : %s\n", strerror(ret));
@@ -981,7 +986,8 @@ static int udp_open(URLContext *h, const char *uri, int flags)
  fail:
     if (udp_fd >= 0)
         closesocket(udp_fd);
-    av_fifo_freep2(&s->fifo);
+    av_fifo_freep2(&s->rx_fifo);
+    av_fifo_freep2(&s->tx_fifo);
     ff_ip_reset_filters(&s->filters);
     return ret;
 }
@@ -1004,23 +1010,23 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
 #if HAVE_PTHREAD_CANCEL
     int avail, nonblock = h->flags & AVIO_FLAG_NONBLOCK;
 
-    if (s->fifo) {
+    if (s->rx_fifo) {
         pthread_mutex_lock(&s->mutex);
         do {
-            avail = av_fifo_can_read(s->fifo);
+            avail = av_fifo_can_read(s->rx_fifo);
             if (avail) { // >=size) {
                 uint8_t tmp[4];
 
-                av_fifo_read(s->fifo, tmp, 4);
-                av_fifo_read(s->fifo, &s->last_recv_addr, sizeof(s->last_recv_addr));
+                av_fifo_read(s->rx_fifo, tmp, 4);
+                av_fifo_read(s->rx_fifo, &s->last_recv_addr, sizeof(s->last_recv_addr));
                 avail = AV_RL32(tmp);
                 if(avail > size){
                     av_log(h, AV_LOG_WARNING, "Part of datagram lost due to insufficient buffer size\n");
                     avail = size;
                 }
 
-                av_fifo_read(s->fifo, buf, avail);
-                av_fifo_drain2(s->fifo, AV_RL32(tmp) - avail);
+                av_fifo_read(s->rx_fifo, buf, avail);
+                av_fifo_drain2(s->rx_fifo, AV_RL32(tmp) - avail);
                 pthread_mutex_unlock(&s->mutex);
                 return avail;
             } else if(s->circular_buffer_error){
@@ -1066,7 +1072,7 @@ static int udp_write(URLContext *h, const uint8_t *buf, int size)
     int ret;
 
 #if HAVE_PTHREAD_CANCEL
-    if (s->fifo) {
+    if (s->tx_fifo) {
         uint8_t tmp[4];
 
         pthread_mutex_lock(&s->mutex);
@@ -1081,14 +1087,14 @@ static int udp_write(URLContext *h, const uint8_t *buf, int size)
             return err;
         }
 
-        if (av_fifo_can_write(s->fifo) < size + 4) {
+        if (av_fifo_can_write(s->tx_fifo) < size + 4) {
             /* What about a partial packet tx ? */
             pthread_mutex_unlock(&s->mutex);
             return AVERROR(ENOMEM);
         }
         AV_WL32(tmp, size);
-        av_fifo_write(s->fifo, tmp, 4); /* size of packet */
-        av_fifo_write(s->fifo, buf, size); /* the data */
+        av_fifo_write(s->tx_fifo, tmp, 4); /* size of packet */
+        av_fifo_write(s->tx_fifo, buf, size); /* the data */
         pthread_cond_signal(&s->cond);
         pthread_mutex_unlock(&s->mutex);
         return size;
@@ -1150,7 +1156,8 @@ static int udp_close(URLContext *h)
     }
 #endif
     closesocket(s->udp_fd);
-    av_fifo_freep2(&s->fifo);
+    av_fifo_freep2(&s->rx_fifo);
+    av_fifo_freep2(&s->tx_fifo);
     ff_ip_reset_filters(&s->filters);
     return 0;
 }
-- 
2.49.0

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* [FFmpeg-devel] [PATCH v2 4/8] avformat/udp: add function to set remote address directly
  2025-07-06 18:36 [FFmpeg-devel] [PATCH v2 0/8] WHIP + TLS + UDP fixes and SChannel DTLS support Timo Rothenpieler
                   ` (2 preceding siblings ...)
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 3/8] avformat/udp: separate rx and tx fifo Timo Rothenpieler
@ 2025-07-06 18:36 ` Timo Rothenpieler
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 5/8] avformat/tls: make passing an external socket universal Timo Rothenpieler
                   ` (3 subsequent siblings)
  7 siblings, 0 replies; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-06 18:36 UTC (permalink / raw)
  To: ffmpeg-devel; +Cc: Timo Rothenpieler

---
 libavformat/network.h |  1 +
 libavformat/udp.c     | 30 ++++++++++++++++++++++++++++++
 2 files changed, 31 insertions(+)

diff --git a/libavformat/network.h b/libavformat/network.h
index 48bb75a758..5734b664cd 100644
--- a/libavformat/network.h
+++ b/libavformat/network.h
@@ -339,5 +339,6 @@ int ff_connect_parallel(struct addrinfo *addrs, int timeout_ms_per_address,
                         int (*customize_fd)(void *, int, int), void *customize_ctx);
 
 void ff_udp_get_last_recv_addr(URLContext *h, struct sockaddr_storage *addr);
+int ff_udp_set_remote_addr(URLContext *h, const struct sockaddr *dest_addr, socklen_t dest_addr_len, int do_connect);
 
 #endif /* AVFORMAT_NETWORK_H */
diff --git a/libavformat/udp.c b/libavformat/udp.c
index e02eff0f33..3657acb985 100644
--- a/libavformat/udp.c
+++ b/libavformat/udp.c
@@ -458,6 +458,36 @@ int ff_udp_set_remote_url(URLContext *h, const char *uri)
     return 0;
 }
 
+/**
+ * This function is identical to ff_udp_set_remote_url, except that it takes a sockaddr directly.
+ */
+int ff_udp_set_remote_addr(URLContext *h, const struct sockaddr *dest_addr, socklen_t dest_addr_len, int do_connect)
+{
+    UDPContext *s = h->priv_data;
+
+    /* set the destination address */
+    if (dest_addr_len < 0 || dest_addr_len > sizeof(s->dest_addr))
+        return AVERROR(EIO);
+    s->dest_addr_len = dest_addr_len;
+    memcpy(&s->dest_addr, dest_addr, dest_addr_len);
+
+    s->is_multicast = ff_is_multicast_address((struct sockaddr*) &s->dest_addr);
+    if (do_connect >= 0) {
+        int was_connected = s->is_connected;
+        s->is_connected = do_connect;
+        if (s->is_connected && !was_connected) {
+            if (connect(s->udp_fd, (struct sockaddr *) &s->dest_addr,
+                        s->dest_addr_len)) {
+                s->is_connected = 0;
+                ff_log_net_error(h, AV_LOG_ERROR, "connect");
+                return AVERROR(EIO);
+            }
+        }
+    }
+
+    return 0;
+}
+
 /**
  * Return the local port used by the UDP connection
  * @param h media file context
-- 
2.49.0

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* [FFmpeg-devel] [PATCH v2 5/8] avformat/tls: make passing an external socket universal
  2025-07-06 18:36 [FFmpeg-devel] [PATCH v2 0/8] WHIP + TLS + UDP fixes and SChannel DTLS support Timo Rothenpieler
                   ` (3 preceding siblings ...)
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 4/8] avformat/udp: add function to set remote address directly Timo Rothenpieler
@ 2025-07-06 18:36 ` Timo Rothenpieler
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 6/8] avformat/tls_schannel: add DTLS support Timo Rothenpieler
                   ` (2 subsequent siblings)
  7 siblings, 0 replies; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-06 18:36 UTC (permalink / raw)
  To: ffmpeg-devel; +Cc: Timo Rothenpieler

---
 libavformat/tls.h         | 11 +++++------
 libavformat/tls_openssl.c | 14 ++++++++++----
 libavformat/whip.c        |  4 ++--
 3 files changed, 17 insertions(+), 12 deletions(-)

diff --git a/libavformat/tls.h b/libavformat/tls.h
index 83d6b1ab6e..1ab115aa81 100644
--- a/libavformat/tls.h
+++ b/libavformat/tls.h
@@ -57,15 +57,14 @@ typedef struct TLSShared {
     char underlying_host[200];
     int numerichost;
 
+    int external_sock;
+    URLContext *udp;
     URLContext *tcp;
 
     int is_dtls;
 
     enum DTLSState state;
 
-    int use_external_udp;
-    URLContext *udp;
-
     /* The certificate and private key content used for DTLS handshake */
     char* cert_buf;
     char* key_buf;
@@ -89,14 +88,14 @@ typedef struct TLSShared {
     {"listen",     "Listen for incoming connections",     offsetof(pstruct, options_field . listen),    AV_OPT_TYPE_INT, { .i64 = 0 }, 0, 1, .flags = TLS_OPTFL }, \
     {"verifyhost", "Verify against a specific hostname",  offsetof(pstruct, options_field . host),      AV_OPT_TYPE_STRING, .flags = TLS_OPTFL }, \
     {"http_proxy", "Set proxy to tunnel through",         offsetof(pstruct, options_field . http_proxy), AV_OPT_TYPE_STRING, .flags = TLS_OPTFL }, \
-    {"use_external_udp", "Use external UDP from muxer or demuxer", offsetof(pstruct, options_field . use_external_udp), AV_OPT_TYPE_INT, { .i64 = 0}, 0, 1, .flags = TLS_OPTFL }, \
-    {"mtu",        "Maximum Transmission Unit",           offsetof(pstruct, options_field . mtu),       AV_OPT_TYPE_INT,  { .i64 = 0 }, 0, INT_MAX, .flags = TLS_OPTFL }
+    {"mtu",        "Maximum Transmission Unit",           offsetof(pstruct, options_field . mtu),       AV_OPT_TYPE_INT,  { .i64 = 0 }, 0, INT_MAX, .flags = TLS_OPTFL }, \
+    {"external_sock", "Use external socket",              offsetof(pstruct, options_field . external_sock), AV_OPT_TYPE_INT, { .i64 = 0}, 0, 1, .flags = TLS_OPTFL }
 
 int ff_tls_open_underlying(TLSShared *c, URLContext *parent, const char *uri, AVDictionary **options);
 
 int ff_url_read_all(const char *url, AVBPrint *bp);
 
-int ff_dtls_set_udp(URLContext *h, URLContext *udp);
+int ff_tls_set_external_socket(URLContext *h, URLContext *sock);
 
 int ff_dtls_export_materials(URLContext *h, char *dtls_srtp_materials, size_t materials_sz);
 
diff --git a/libavformat/tls_openssl.c b/libavformat/tls_openssl.c
index 0c76f110e3..1b702e659e 100644
--- a/libavformat/tls_openssl.c
+++ b/libavformat/tls_openssl.c
@@ -502,10 +502,16 @@ static const char* openssl_get_error(TLSContext *ctx)
     return ctx->error_message;
 }
 
-int ff_dtls_set_udp(URLContext *h, URLContext *udp)
+int ff_tls_set_external_socket(URLContext *h, URLContext *sock)
 {
     TLSContext *c = h->priv_data;
-    c->tls_shared.udp = udp;
+    TLSShared *s = &c->tls_shared;
+
+    if (s->is_dtls)
+        c->tls_shared.udp = sock;
+    else
+        c->tls_shared.tcp = sock;
+
     return 0;
 }
 
@@ -980,7 +986,7 @@ static int dtls_start(URLContext *h, const char *url, int flags, AVDictionary **
 #endif
     init_bio_method(h);
 
-    if (p->tls_shared.use_external_udp != 1) {
+    if (p->tls_shared.external_sock != 1) {
         if ((ret = ff_tls_open_underlying(&p->tls_shared, h, url, options)) < 0) {
             av_log(p, AV_LOG_ERROR, "Failed to connect %s\n", url);
             return ret;
@@ -1001,7 +1007,7 @@ static int dtls_start(URLContext *h, const char *url, int flags, AVDictionary **
      *
      * The SSL_do_handshake can't be called if DTLS hasn't prepare for udp.
      */
-    if (p->tls_shared.use_external_udp != 1) {
+    if (p->tls_shared.external_sock != 1) {
         ret = dtls_handshake(h);
         // Fatal SSL error, for example, no available suite when peer is DTLS 1.0 while we are DTLS 1.2.
         if (ret < 0) {
diff --git a/libavformat/whip.c b/libavformat/whip.c
index 4ac76e79f2..e272254a6f 100644
--- a/libavformat/whip.c
+++ b/libavformat/whip.c
@@ -387,7 +387,7 @@ static av_cold int dtls_initialize(AVFormatContext *s)
 {
     WHIPContext *whip = s->priv_data;
     /* reuse the udp created by whip */
-    ff_dtls_set_udp(whip->dtls_uc, whip->udp);
+    ff_tls_set_external_socket(whip->dtls_uc, whip->udp);
 
     /* Make the socket non-blocking */
     ff_socket_nonblock(ffurl_get_file_handle(whip->dtls_uc), 1);
@@ -1302,7 +1302,7 @@ next_packet:
                     av_dict_set(&opts, "key_file", whip->key_file, 0);
                 } else
                     av_dict_set(&opts, "key_pem", whip->key_buf, 0);
-                av_dict_set_int(&opts, "use_external_udp", 1, 0);
+                av_dict_set_int(&opts, "external_sock", 1, 0);
                 av_dict_set_int(&opts, "listen", 1, 0);
                 /* If got the first binding response, start DTLS handshake. */
                 ret = ffurl_open_whitelist(&whip->dtls_uc, buf, AVIO_FLAG_READ_WRITE, &s->interrupt_callback,
-- 
2.49.0

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* [FFmpeg-devel] [PATCH v2 6/8] avformat/tls_schannel: add DTLS support
  2025-07-06 18:36 [FFmpeg-devel] [PATCH v2 0/8] WHIP + TLS + UDP fixes and SChannel DTLS support Timo Rothenpieler
                   ` (4 preceding siblings ...)
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 5/8] avformat/tls: make passing an external socket universal Timo Rothenpieler
@ 2025-07-06 18:36 ` Timo Rothenpieler
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 7/8] avformat/tls_schannel: add option to load server certificate from store Timo Rothenpieler
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 8/8] avformat/tls_schannel: fix non-blocking write breaking TLS sessions Timo Rothenpieler
  7 siblings, 0 replies; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-06 18:36 UTC (permalink / raw)
  To: ffmpeg-devel; +Cc: Timo Rothenpieler

---
 configure                  |   6 +-
 libavformat/tls_schannel.c | 889 ++++++++++++++++++++++++++++++++++---
 2 files changed, 831 insertions(+), 64 deletions(-)

diff --git a/configure b/configure
index 63d11de207..c6aae97b19 100755
--- a/configure
+++ b/configure
@@ -3856,7 +3856,7 @@ tcp_protocol_select="network"
 tls_protocol_deps_any="gnutls openssl schannel securetransport libtls mbedtls"
 tls_protocol_select="tcp_protocol"
 # TODO: Support libtls, mbedtls, and gnutls.
-dtls_protocol_deps_any="openssl"
+dtls_protocol_deps_any="openssl schannel"
 dtls_protocol_select="udp_protocol"
 udp_protocol_select="network"
 udplite_protocol_select="network"
@@ -7268,8 +7268,10 @@ enabled securetransport &&
 
 enabled schannel &&
     check_func_headers "windows.h security.h" InitializeSecurityContext -DSECURITY_WIN32 -lsecur32 &&
+    check_func_headers "windows.h ncrypt.h" NCryptOpenStorageProvider -DSECURITY_WIN32 -lncrypt &&
+    check_func_headers "windows.h wincrypt.h" CertCreateSelfSignCertificate -DSECURITY_WIN32 -lcrypt32 &&
     test_cpp_condition winerror.h "defined(SEC_I_CONTEXT_EXPIRED)" &&
-    schannel_extralibs="-lsecur32" ||
+    schannel_extralibs="-lsecur32 -lncrypt -lcrypt32" ||
         disable schannel
 
 makeinfo --version > /dev/null 2>&1 && enable makeinfo  || disable makeinfo
diff --git a/libavformat/tls_schannel.c b/libavformat/tls_schannel.c
index ae9a311d2a..b985576b72 100644
--- a/libavformat/tls_schannel.c
+++ b/libavformat/tls_schannel.c
@@ -32,6 +32,7 @@
 #include <windows.h>
 #include <security.h>
 #include <schnlsp.h>
+#include <sddl.h>
 
 #define SCHANNEL_INITIAL_BUFFER_SIZE   4096
 #define SCHANNEL_FREE_BUFFER_SIZE      1024
@@ -41,6 +42,521 @@
 #define SECBUFFER_ALERT                17
 #endif
 
+/* This is the name used for the private key in the MS Keystore.
+ * There is as of time of writing no way to use schannel without
+ * persisting the private key. Which usually means the default MS
+ * keystore will write it to disk unencrypted, user-read/writable.
+ * To combat this as much as possible, the code makes sure to
+ * delete the private key ASAP once SChannel has gotten ahold of
+ * it.
+ * Apparently this is because SChannel neglects marshaling the
+ * private key alongside the certificate for the out-of-process
+ * tls handler.
+ * See this GitHub issue for the most detailed explanation out there:
+ * https://github.com/dotnet/runtime/issues/23749#issuecomment-485947319
+ */
+#define FF_NCRYPT_TEMP_KEY_NAME L"FFMPEG_TEMP_TLS_KEY"
+
+static int der_to_pem(const char *data, size_t len, const char *header, char *buf, size_t bufsize)
+{
+    const int line_length = 64;
+    AVBPrint pem;
+    DWORD base64len = 0;
+    char *base64 = NULL;
+    int ret = 0;
+
+    if (!CryptBinaryToStringA(data, len, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, NULL, &base64len)) {
+        av_log(NULL, AV_LOG_ERROR, "CryptBinaryToString failed\n");
+        ret = AVERROR_EXTERNAL;
+        goto end;
+    }
+
+    base64 = av_malloc(base64len);
+
+    if (!CryptBinaryToStringA(data, len, CRYPT_STRING_BASE64 | CRYPT_STRING_NOCRLF, base64, &base64len)) {
+        av_log(NULL, AV_LOG_ERROR, "CryptBinaryToString failed\n");
+        ret = AVERROR_EXTERNAL;
+        goto end;
+    }
+
+    av_bprint_init_for_buffer(&pem, buf, bufsize);
+    av_bprintf(&pem, "-----BEGIN %s-----\n", header);
+
+    for (DWORD i = 0; i < base64len; i += line_length) {
+        av_bprintf(&pem, "%.*s\n", line_length, base64 + i);
+    }
+
+    av_bprintf(&pem, "-----END %s-----\n", header);
+
+    if (!av_bprint_is_complete(&pem)) {
+        ret = AVERROR(ENOSPC);
+        goto end;
+    }
+
+end:
+    av_free(base64);
+    return ret;
+}
+
+static int pem_to_der(const char *pem, char **buf, int *out_len)
+{
+    DWORD derlen = 0;
+
+    if (!CryptStringToBinaryA(pem, 0, CRYPT_STRING_BASE64HEADER, NULL, &derlen, NULL, NULL)) {
+        av_log(NULL, AV_LOG_ERROR, "CryptStringToBinaryA failed\n");
+        return AVERROR(EINVAL);
+    }
+
+    *buf = av_malloc(derlen);
+    if (!*buf)
+        return AVERROR(ENOMEM);
+
+    if (!CryptStringToBinaryA(pem, 0, CRYPT_STRING_BASE64HEADER, *buf, &derlen, NULL, NULL)) {
+        av_log(NULL, AV_LOG_ERROR, "CryptStringToBinaryA failed\n");
+        return AVERROR(EINVAL);
+    }
+
+    *out_len = derlen;
+
+    return 0;
+}
+
+static int der_to_fingerprint(const char *data, size_t len, char **fingerprint)
+{
+    AVBPrint buf;
+    unsigned char hash[32];
+    DWORD hashsize = sizeof(hash);
+
+    if (!CryptHashCertificate2(BCRYPT_SHA256_ALGORITHM, 0, NULL, data, len, hash, &hashsize))
+    {
+        av_log(NULL, AV_LOG_ERROR, "CryptHashCertificate2 failed\n");
+        return AVERROR_EXTERNAL;
+    }
+
+    av_bprint_init(&buf, hashsize*3, hashsize*3);
+
+    for (int i = 0; i < hashsize - 1; i++)
+        av_bprintf(&buf, "%02X:", hash[i]);
+    av_bprintf(&buf, "%02X", hash[hashsize - 1]);
+
+    return av_bprint_finalize(&buf, fingerprint);
+}
+
+static int tls_gen_self_signed(NCRYPT_KEY_HANDLE *key, PCCERT_CONTEXT *crtctx)
+{
+    NCRYPT_PROV_HANDLE provider = 0;
+    CERT_NAME_BLOB subject = { 0 };
+
+    DWORD export_props = NCRYPT_ALLOW_EXPORT_FLAG | NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
+    DWORD usage_props = NCRYPT_ALLOW_ALL_USAGES;
+    LPCSTR ext_usages[] = { szOID_PKIX_KP_SERVER_AUTH };
+    BYTE key_usage = CERT_KEY_ENCIPHERMENT_KEY_USAGE | CERT_DIGITAL_SIGNATURE_KEY_USAGE;
+    CRYPT_BIT_BLOB key_usage_blob = { 0 };
+    CERT_ENHKEY_USAGE eku = { 0 };
+    CERT_BASIC_CONSTRAINTS2_INFO basic_constraints = { 0 };
+    CERT_ALT_NAME_ENTRY san_entry = { 0 };
+    CERT_ALT_NAME_INFO san_info = { 0 };
+    CERT_EXTENSION ext[4] = { 0 };
+    CERT_EXTENSIONS exts = { 0 };
+    CRYPT_ALGORITHM_IDENTIFIER sig_alg = { (LPSTR)szOID_ECDSA_SHA256 };
+    CRYPT_KEY_PROV_INFO prov_info = { 0 };
+    const char *subj_str = "CN=lavf";
+
+    SECURITY_STATUS sspi_ret;
+    int ret = 0;
+
+    *crtctx = NULL;
+
+    sspi_ret = NCryptOpenStorageProvider(&provider, MS_KEY_STORAGE_PROVIDER, 0);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "NCryptOpenStorageProvider failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    sspi_ret = NCryptCreatePersistedKey(provider, key, BCRYPT_ECDSA_P256_ALGORITHM, FF_NCRYPT_TEMP_KEY_NAME, 0, NCRYPT_OVERWRITE_KEY_FLAG);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "NCryptCreatePersistedKey failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    sspi_ret = NCryptSetProperty(*key, NCRYPT_EXPORT_POLICY_PROPERTY, (PBYTE)&export_props, sizeof(export_props), 0);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "NCryptSetProperty(NCRYPT_EXPORT_POLICY_PROPERTY) failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    sspi_ret = NCryptSetProperty(*key, NCRYPT_KEY_USAGE_PROPERTY, (PBYTE)&usage_props, sizeof(usage_props), 0);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "NCryptSetProperty(NCRYPT_KEY_USAGE_PROPERTY) failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    sspi_ret = NCryptFinalizeKey(*key, 0);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "NCryptFinalizeKey failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    if (!CertStrToNameA(X509_ASN_ENCODING, subj_str, 0, NULL, NULL, &subject.cbData, NULL))
+    {
+        av_log(NULL, AV_LOG_ERROR, "Initial subj init failed\n");
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    subject.pbData = av_malloc(subject.cbData);
+    if (!subject.pbData) {
+        ret = AVERROR(ENOMEM);
+        goto fail;
+    }
+
+    if (!CertStrToNameA(X509_ASN_ENCODING, subj_str, 0, NULL, subject.pbData, &subject.cbData, NULL))
+    {
+        av_log(NULL, AV_LOG_ERROR, "Subj init failed\n");
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    // Extended Key Usage extension
+    eku.cUsageIdentifier = 1;
+    eku.rgpszUsageIdentifier = (LPSTR*)ext_usages;
+
+    if (!CryptEncodeObjectEx(X509_ASN_ENCODING, X509_ENHANCED_KEY_USAGE, &eku,
+                             CRYPT_ENCODE_ALLOC_FLAG, NULL, &ext[0].Value.pbData, &ext[0].Value.cbData)) {
+        av_log(NULL, AV_LOG_ERROR, "CryptEncodeObjectEx for EKU failed\n");
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    ext[0].pszObjId = (LPSTR)szOID_ENHANCED_KEY_USAGE;
+    ext[0].fCritical = TRUE;
+
+    // Key usage extension
+    key_usage_blob.cbData = sizeof(key_usage);
+    key_usage_blob.pbData = &key_usage;
+
+    if (!CryptEncodeObjectEx(X509_ASN_ENCODING, X509_BITS, &key_usage_blob,
+                             CRYPT_ENCODE_ALLOC_FLAG, NULL, &ext[1].Value.pbData, &ext[1].Value.cbData)) {
+        av_log(NULL, AV_LOG_ERROR, "CryptEncodeObjectEx for KU failed\n");
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    ext[1].pszObjId = (LPSTR)szOID_KEY_USAGE;
+    ext[1].fCritical = TRUE;
+
+    // Cert Basic Constraints
+    basic_constraints.fCA = FALSE;
+
+    if (!CryptEncodeObjectEx(X509_ASN_ENCODING, X509_BASIC_CONSTRAINTS2, &basic_constraints,
+                             CRYPT_ENCODE_ALLOC_FLAG, NULL, &ext[2].Value.pbData, &ext[2].Value.cbData)) {
+        av_log(NULL, AV_LOG_ERROR, "CryptEncodeObjectEx for KU failed\n");
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    ext[2].pszObjId = (LPSTR)szOID_BASIC_CONSTRAINTS2;
+    ext[2].fCritical = TRUE;
+
+    // Subject Alt Names
+    san_entry.dwAltNameChoice = CERT_ALT_NAME_DNS_NAME;
+    san_entry.pwszDNSName = (LPWSTR)L"localhost";
+
+    san_info.cAltEntry = 1;
+    san_info.rgAltEntry = &san_entry;
+
+    if (!CryptEncodeObjectEx(X509_ASN_ENCODING, X509_ALTERNATE_NAME, &san_info,
+                             CRYPT_ENCODE_ALLOC_FLAG, NULL, &ext[3].Value.pbData, &ext[3].Value.cbData)) {
+        av_log(NULL, AV_LOG_ERROR, "CryptEncodeObjectEx for KU failed\n");
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    ext[3].pszObjId = (LPSTR)szOID_SUBJECT_ALT_NAME2;
+    ext[3].fCritical = TRUE;
+
+    exts.cExtension = 4;
+    exts.rgExtension = ext;
+
+    prov_info.pwszProvName = (LPWSTR)MS_KEY_STORAGE_PROVIDER;
+    prov_info.pwszContainerName = (LPWSTR)FF_NCRYPT_TEMP_KEY_NAME;
+    prov_info.dwFlags = CERT_SET_KEY_CONTEXT_PROP_ID;
+
+    *crtctx = CertCreateSelfSignCertificate(*key, &subject, 0, &prov_info, &sig_alg, NULL, NULL, &exts);
+    if (!*crtctx) {
+        av_log(NULL, AV_LOG_ERROR, "CertCreateSelfSignCertificate failed: %lu\n", GetLastError());
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    NCryptFreeObject(provider);
+    av_free(subject.pbData);
+    for (int i = 0; i < FF_ARRAY_ELEMS(ext); i++)
+        LocalFree(ext[i].Value.pbData);
+
+    return 0;
+
+fail:
+    if (*crtctx)
+        CertFreeCertificateContext(*crtctx);
+    if (*key)
+        if (NCryptDeleteKey(*key, NCRYPT_SILENT_FLAG) != ERROR_SUCCESS)
+            NCryptFreeObject(*key);
+    if (provider)
+        NCryptFreeObject(provider);
+    if (subject.pbData)
+        av_free(subject.pbData);
+    for (int i = 0; i < FF_ARRAY_ELEMS(ext); i++)
+        if (ext[i].Value.pbData)
+            LocalFree(ext[i].Value.pbData);
+
+    *key = 0;
+    *crtctx = NULL;
+
+    return ret;
+}
+
+static int tls_export_key_cert(NCRYPT_KEY_HANDLE key, PCCERT_CONTEXT crtctx,
+                               char *key_buf, size_t key_sz, char *cert_buf, size_t cert_sz, char **fingerprint)
+{
+    DWORD keysize = 0;
+    char *keybuf = NULL;
+
+    SECURITY_STATUS sspi_ret;
+    int ret = 0;
+
+    sspi_ret = NCryptExportKey(key, 0, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, NULL, NULL, 0, &keysize, 0);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "Initial NCryptExportKey failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto end;
+    }
+
+    keybuf = av_malloc(keysize);
+    if (!keybuf) {
+        ret = AVERROR(ENOMEM);
+        goto end;
+    }
+
+    sspi_ret = NCryptExportKey(key, 0, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, NULL, keybuf, keysize, &keysize, 0);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "Initial NCryptExportKey failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto end;
+    }
+
+    ret = der_to_pem(keybuf, keysize, "PRIVATE KEY", key_buf, key_sz);
+    if (ret < 0)
+        goto end;
+
+    ret = der_to_pem(crtctx->pbCertEncoded, crtctx->cbCertEncoded, "CERTIFICATE", cert_buf, cert_sz);
+    if (ret < 0)
+        goto end;
+
+    ret = der_to_fingerprint(crtctx->pbCertEncoded, crtctx->cbCertEncoded, fingerprint);
+    if (ret < 0)
+        goto end;
+
+end:
+    av_free(keybuf);
+    return ret;
+}
+
+int ff_ssl_gen_key_cert(char *key_buf, size_t key_sz, char *cert_buf, size_t cert_sz, char **fingerprint)
+{
+    NCRYPT_KEY_HANDLE key = 0;
+    PCCERT_CONTEXT crtctx = NULL;
+
+    int ret = tls_gen_self_signed(&key, &crtctx);
+    if (ret < 0)
+        goto end;
+
+    ret = tls_export_key_cert(key, crtctx, key_buf, key_sz, cert_buf, cert_sz, fingerprint);
+    if (ret < 0)
+        goto end;
+
+end:
+    if (key)
+        if (NCryptDeleteKey(key, NCRYPT_SILENT_FLAG) != ERROR_SUCCESS)
+            NCryptFreeObject(key);
+    if (crtctx)
+        CertFreeCertificateContext(crtctx);
+
+    return ret;
+}
+
+static int tls_import_key_cert(char *key_buf, char *cert_buf, NCRYPT_KEY_HANDLE *key, PCCERT_CONTEXT *crtctx)
+{
+    NCRYPT_PROV_HANDLE provider = 0;
+
+    DWORD export_props = NCRYPT_ALLOW_EXPORT_FLAG | NCRYPT_ALLOW_PLAINTEXT_EXPORT_FLAG;
+    DWORD usage_props = NCRYPT_ALLOW_ALL_USAGES;
+    NCryptBufferDesc buffer_desc = { 0 };
+    NCryptBuffer buffer = { 0 };
+    CRYPT_KEY_PROV_INFO prov_info = { 0 };
+
+    int key_der_len = 0, cert_der_len = 0;
+    char *key_der = NULL, *cert_der = NULL;
+
+    SECURITY_STATUS sspi_ret;
+    int ret = 0;
+
+    ret = pem_to_der(key_buf, &key_der, &key_der_len);
+    if (ret < 0)
+        goto fail;
+
+    ret = pem_to_der(cert_buf, &cert_der, &cert_der_len);
+    if (ret < 0)
+        goto fail;
+
+    sspi_ret = NCryptOpenStorageProvider(&provider, MS_KEY_STORAGE_PROVIDER, 0);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "NCryptOpenStorageProvider failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    buffer_desc.ulVersion = NCRYPTBUFFER_VERSION;
+    buffer_desc.cBuffers = 1;
+    buffer_desc.pBuffers = &buffer;
+
+    buffer.BufferType = NCRYPTBUFFER_PKCS_KEY_NAME;
+    buffer.pvBuffer = (LPWSTR)FF_NCRYPT_TEMP_KEY_NAME;
+    buffer.cbBuffer = sizeof(FF_NCRYPT_TEMP_KEY_NAME);
+
+    sspi_ret = NCryptImportKey(provider, 0, NCRYPT_PKCS8_PRIVATE_KEY_BLOB, &buffer_desc, key, key_der, key_der_len, NCRYPT_DO_NOT_FINALIZE_FLAG | NCRYPT_OVERWRITE_KEY_FLAG);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "NCryptImportKey failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    sspi_ret = NCryptSetProperty(*key, NCRYPT_EXPORT_POLICY_PROPERTY, (PBYTE)&export_props, sizeof(export_props), 0);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "NCryptSetProperty(NCRYPT_EXPORT_POLICY_PROPERTY) failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    sspi_ret = NCryptSetProperty(*key, NCRYPT_KEY_USAGE_PROPERTY, (PBYTE)&usage_props, sizeof(usage_props), 0);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "NCryptSetProperty(NCRYPT_KEY_USAGE_PROPERTY) failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    sspi_ret = NCryptFinalizeKey(*key, 0);
+    if (sspi_ret != ERROR_SUCCESS) {
+        av_log(NULL, AV_LOG_ERROR, "NCryptFinalizeKey failed(0x%lx)\n", sspi_ret);
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    *crtctx = CertCreateCertificateContext(X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, cert_der, cert_der_len);
+    if (!*crtctx) {
+        av_log(NULL, AV_LOG_ERROR, "CertCreateCertificateContext failed: %lu\n", GetLastError());
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    if (!CertSetCertificateContextProperty(*crtctx, CERT_NCRYPT_KEY_HANDLE_PROP_ID, 0, key)) {
+        av_log(NULL, AV_LOG_ERROR, "CertSetCertificateContextProperty(CERT_NCRYPT_KEY_HANDLE_PROP_ID) failed: %lu\n", GetLastError());
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    prov_info.pwszProvName = (LPWSTR)MS_KEY_STORAGE_PROVIDER;
+    prov_info.pwszContainerName = (LPWSTR)FF_NCRYPT_TEMP_KEY_NAME;
+    prov_info.dwFlags = CERT_SET_KEY_CONTEXT_PROP_ID;
+
+    if (!CertSetCertificateContextProperty(*crtctx, CERT_KEY_PROV_INFO_PROP_ID, 0, &prov_info)) {
+        av_log(NULL, AV_LOG_ERROR, "CertSetCertificateContextProperty(CERT_KEY_PROV_INFO_PROP_ID) failed: %lu\n", GetLastError());
+        ret = AVERROR_EXTERNAL;
+        goto fail;
+    }
+
+    goto end;
+
+fail:
+    if (*key)
+        if (NCryptDeleteKey(*key, NCRYPT_SILENT_FLAG) != ERROR_SUCCESS)
+            NCryptFreeObject(*key);
+    if (*crtctx)
+        CertFreeCertificateContext(*crtctx);
+
+    *key = 0;
+    *crtctx = NULL;
+
+end:
+    if (key_der)
+        av_free(key_der);
+    if (cert_der)
+        av_free(cert_der);
+    if (provider)
+        NCryptFreeObject(provider);
+    return ret;
+}
+
+static int tls_load_key_cert(char *key_url, char *cert_url, NCRYPT_KEY_HANDLE *key, PCCERT_CONTEXT *crtctx)
+{
+    AVBPrint key_bp, cert_bp;
+    int ret = 0;
+
+    av_bprint_init(&key_bp, 1, MAX_CERTIFICATE_SIZE);
+    av_bprint_init(&cert_bp, 1, MAX_CERTIFICATE_SIZE);
+
+    /* Read key file. */
+    ret = ff_url_read_all(key_url, &key_bp);
+    if (ret < 0) {
+        av_log(NULL, AV_LOG_ERROR, "Failed to open key file %s\n", key_url);
+        goto end;
+    }
+
+    ret = ff_url_read_all(cert_url, &cert_bp);
+    if (ret < 0) {
+        av_log(NULL, AV_LOG_ERROR, "Failed to open cert file %s\n", cert_url);
+        goto end;
+    }
+
+    ret = tls_import_key_cert(key_bp.str, cert_bp.str, key, crtctx);
+    if (ret < 0)
+        goto end;
+
+end:
+    av_bprint_finalize(&key_bp, NULL);
+    av_bprint_finalize(&cert_bp, NULL);
+
+    return ret;
+}
+
+int ff_ssl_read_key_cert(char *key_url, char *cert_url, char *key_buf, size_t key_sz, char *cert_buf, size_t cert_sz, char **fingerprint)
+{
+    NCRYPT_KEY_HANDLE key = 0;
+    PCCERT_CONTEXT crtctx = NULL;
+
+    int ret = tls_load_key_cert(key_url, cert_url, &key, &crtctx);
+    if (ret < 0)
+        goto end;
+
+    ret = tls_export_key_cert(key, crtctx, key_buf, key_sz, cert_buf, cert_sz, fingerprint);
+    if (ret < 0)
+        goto end;
+
+end:
+    if (key)
+        if (NCryptDeleteKey(key, NCRYPT_SILENT_FLAG) != ERROR_SUCCESS)
+            NCryptFreeObject(key);
+    if (crtctx)
+        CertFreeCertificateContext(crtctx);
+
+    return ret;
+}
+
 typedef struct TLSContext {
     const AVClass *class;
     TLSShared tls_shared;
@@ -49,6 +565,7 @@ typedef struct TLSContext {
     TimeStamp cred_timestamp;
 
     CtxtHandle ctxt_handle;
+    int have_context;
     TimeStamp ctxt_timestamp;
 
     ULONG request_flags;
@@ -69,6 +586,67 @@ typedef struct TLSContext {
     int sspi_close_notify;
 } TLSContext;
 
+int ff_tls_set_external_socket(URLContext *h, URLContext *sock)
+{
+    TLSContext *c = h->priv_data;
+    TLSShared *s = &c->tls_shared;
+
+    if (s->is_dtls)
+        c->tls_shared.udp = sock;
+    else
+        c->tls_shared.tcp = sock;
+
+    return 0;
+}
+
+int ff_dtls_export_materials(URLContext *h, char *dtls_srtp_materials, size_t materials_sz)
+{
+    TLSContext *c = h->priv_data;
+
+    SecPkgContext_KeyingMaterialInfo keying_info = { 0 };
+    SecPkgContext_KeyingMaterial keying_material = { 0 };
+
+    const char* dst = "EXTRACTOR-dtls_srtp";
+    SECURITY_STATUS sspi_ret;
+
+    if (!c->have_context)
+        return AVERROR(EINVAL);
+
+    keying_info.cbLabel = strlen(dst) + 1;
+    keying_info.pszLabel = (LPSTR)dst;
+    keying_info.cbContextValue = 0;
+    keying_info.pbContextValue = NULL;
+    keying_info.cbKeyingMaterial = materials_sz;
+
+    sspi_ret = SetContextAttributes(&c->ctxt_handle, SECPKG_ATTR_KEYING_MATERIAL_INFO, &keying_info, sizeof(keying_info));
+    if (sspi_ret != SEC_E_OK) {
+        av_log(h, AV_LOG_ERROR, "Setting keying material info failed: %lx\n", sspi_ret);
+        return AVERROR_EXTERNAL;
+    }
+
+    sspi_ret = QueryContextAttributes(&c->ctxt_handle, SECPKG_ATTR_KEYING_MATERIAL, &keying_material);
+    if (sspi_ret != SEC_E_OK) {
+        av_log(h, AV_LOG_ERROR, "Querying keying material failed: %lx\n", sspi_ret);
+        return AVERROR_EXTERNAL;
+    }
+
+    memcpy(dtls_srtp_materials, keying_material.pbKeyingMaterial, FFMIN(materials_sz, keying_material.cbKeyingMaterial));
+    FreeContextBuffer(keying_material.pbKeyingMaterial);
+
+    if (keying_material.cbKeyingMaterial > materials_sz) {
+        av_log(h, AV_LOG_WARNING, "Keying material size mismatch: %ld > %zu\n", keying_material.cbKeyingMaterial, materials_sz);
+        return AVERROR(ENOSPC);
+    }
+
+    return 0;
+}
+
+int ff_dtls_state(URLContext *h)
+{
+    TLSContext *c = h->priv_data;
+    return c->tls_shared.state;
+}
+
 static void init_sec_buffer(SecBuffer *buffer, unsigned long type,
                             void *data, unsigned long size)
 {
@@ -89,6 +667,7 @@ static int tls_shutdown_client(URLContext *h)
 {
     TLSContext *c = h->priv_data;
     TLSShared *s = &c->tls_shared;
+    URLContext *uc = s->is_dtls ? s->udp : s->tcp;
     int ret;
 
     if (c->connected) {
@@ -106,19 +685,31 @@ static int tls_shutdown_client(URLContext *h)
         if (sspi_ret != SEC_E_OK)
             av_log(h, AV_LOG_ERROR, "ApplyControlToken failed\n");
 
-        init_sec_buffer(&outbuf, SECBUFFER_EMPTY, NULL, 0);
+        init_sec_buffer(&outbuf, SECBUFFER_TOKEN, NULL, 0);
         init_sec_buffer_desc(&outbuf_desc, &outbuf, 1);
 
-        sspi_ret = InitializeSecurityContext(&c->cred_handle, &c->ctxt_handle, s->host,
-                                             c->request_flags, 0, 0, NULL, 0, &c->ctxt_handle,
-                                             &outbuf_desc, &c->context_flags, &c->ctxt_timestamp);
-        if (sspi_ret == SEC_E_OK || sspi_ret == SEC_I_CONTEXT_EXPIRED) {
-            s->tcp->flags &= ~AVIO_FLAG_NONBLOCK;
-            ret = ffurl_write(s->tcp, outbuf.pvBuffer, outbuf.cbBuffer);
-            FreeContextBuffer(outbuf.pvBuffer);
-            if (ret < 0 || ret != outbuf.cbBuffer)
-                av_log(h, AV_LOG_ERROR, "Failed to send close message\n");
-        }
+        do {
+            if (s->listen)
+                sspi_ret = AcceptSecurityContext(&c->cred_handle, &c->ctxt_handle, NULL, c->request_flags, 0,
+                                                 &c->ctxt_handle, &outbuf_desc, &c->context_flags,
+                                                 &c->ctxt_timestamp);
+            else
+                sspi_ret = InitializeSecurityContext(&c->cred_handle, &c->ctxt_handle, s->host,
+                                                     c->request_flags, 0, 0, NULL, 0, &c->ctxt_handle,
+                                                     &outbuf_desc, &c->context_flags, &c->ctxt_timestamp);
+
+            if (outbuf.pvBuffer) {
+                if (outbuf.cbBuffer > 0) {
+                    uc->flags &= ~AVIO_FLAG_NONBLOCK;
+                    ret = ffurl_write(uc, outbuf.pvBuffer, outbuf.cbBuffer);
+                    if (ret < 0 || ret != outbuf.cbBuffer)
+                        av_log(h, AV_LOG_ERROR, "Failed to send close message\n");
+                }
+                FreeContextBuffer(outbuf.pvBuffer);
+            }
+        } while(sspi_ret == SEC_I_MESSAGE_FRAGMENT || sspi_ret == SEC_I_CONTINUE_NEEDED);
+
+        av_log(h, AV_LOG_DEBUG, "Close session result: 0x%lx\n", sspi_ret);
 
         c->connected = 0;
     }
@@ -128,6 +719,7 @@ static int tls_shutdown_client(URLContext *h)
 static int tls_close(URLContext *h)
 {
     TLSContext *c = h->priv_data;
+    TLSShared *s = &c->tls_shared;
 
     tls_shutdown_client(h);
 
@@ -140,19 +732,27 @@ static int tls_close(URLContext *h)
     av_freep(&c->dec_buf);
     c->dec_buf_size = c->dec_buf_offset = 0;
 
-    ffurl_closep(&c->tls_shared.tcp);
+    if (s->is_dtls) {
+        if (!s->external_sock)
+            ffurl_closep(&c->tls_shared.udp);
+    } else {
+        ffurl_closep(&c->tls_shared.tcp);
+    }
+
     return 0;
 }
 
-static int tls_client_handshake_loop(URLContext *h, int initial)
+static int tls_handshake_loop(URLContext *h, int initial)
 {
     TLSContext *c = h->priv_data;
     TLSShared *s = &c->tls_shared;
+    URLContext *uc = s->is_dtls ? s->udp : s->tcp;
     SECURITY_STATUS sspi_ret;
     SecBuffer outbuf[3] = { 0 };
     SecBufferDesc outbuf_desc;
-    SecBuffer inbuf[2];
+    SecBuffer inbuf[3];
     SecBufferDesc inbuf_desc;
+    struct sockaddr_storage recv_addr = { 0 };
     int i, ret = 0, read_data = initial;
 
     if (c->enc_buf == NULL) {
@@ -171,6 +771,8 @@ static int tls_client_handshake_loop(URLContext *h, int initial)
         c->dec_buf_size = SCHANNEL_INITIAL_BUFFER_SIZE;
     }
 
+    uc->flags &= ~AVIO_FLAG_NONBLOCK;
+
     while (1) {
         if (c->enc_buf_size - c->enc_buf_offset < SCHANNEL_FREE_BUFFER_SIZE) {
             c->enc_buf_size = c->enc_buf_offset + SCHANNEL_FREE_BUFFER_SIZE;
@@ -182,19 +784,35 @@ static int tls_client_handshake_loop(URLContext *h, int initial)
         }
 
         if (read_data) {
-            ret = ffurl_read(c->tls_shared.tcp, c->enc_buf + c->enc_buf_offset,
-                             c->enc_buf_size - c->enc_buf_offset);
+            ret = ffurl_read(uc, c->enc_buf + c->enc_buf_offset, c->enc_buf_size - c->enc_buf_offset);
             if (ret < 0) {
                 av_log(h, AV_LOG_ERROR, "Failed to read handshake response\n");
                 goto fail;
             }
             c->enc_buf_offset += ret;
+            if (s->is_dtls && !recv_addr.ss_family) {
+                ff_udp_get_last_recv_addr(uc, &recv_addr);
+
+                if (s->listen) {
+                    ret = ff_udp_set_remote_addr(uc, (struct sockaddr *)&recv_addr, sizeof(recv_addr), 1);
+                    if (ret < 0) {
+                        av_log(h, AV_LOG_ERROR, "Failed connecting udp context\n");
+                        goto fail;
+                    }
+                    av_log(h, AV_LOG_TRACE, "Set UDP remote addr on UDP socket, now 'connected'\n");
+                }
+            }
         }
 
         /* input buffers */
         init_sec_buffer(&inbuf[0], SECBUFFER_TOKEN, av_malloc(c->enc_buf_offset), c->enc_buf_offset);
         init_sec_buffer(&inbuf[1], SECBUFFER_EMPTY, NULL, 0);
-        init_sec_buffer_desc(&inbuf_desc, inbuf, 2);
+        if (s->listen && s->is_dtls) {
+            init_sec_buffer(&inbuf[2], SECBUFFER_EXTRA, &recv_addr, sizeof(recv_addr));
+            init_sec_buffer_desc(&inbuf_desc, inbuf, 3);
+        } else {
+            init_sec_buffer_desc(&inbuf_desc, inbuf, 2);
+        }
 
         if (inbuf[0].pvBuffer == NULL) {
             av_log(h, AV_LOG_ERROR, "Failed to allocate input buffer\n");
@@ -210,17 +828,26 @@ static int tls_client_handshake_loop(URLContext *h, int initial)
         init_sec_buffer(&outbuf[2], SECBUFFER_EMPTY, NULL, 0);
         init_sec_buffer_desc(&outbuf_desc, outbuf, 3);
 
-        sspi_ret = InitializeSecurityContext(&c->cred_handle, &c->ctxt_handle, s->host, c->request_flags,
-                                             0, 0, &inbuf_desc, 0, NULL, &outbuf_desc, &c->context_flags,
-                                             &c->ctxt_timestamp);
+        if (s->listen)
+            sspi_ret = AcceptSecurityContext(&c->cred_handle, c->have_context ? &c->ctxt_handle : NULL, &inbuf_desc,
+                                             c->request_flags, 0, &c->ctxt_handle, &outbuf_desc,
+                                             &c->context_flags, &c->ctxt_timestamp);
+        else
+            sspi_ret = InitializeSecurityContext(&c->cred_handle, c->have_context ? &c->ctxt_handle : NULL,
+                                                 s->host, c->request_flags, 0, 0, &inbuf_desc, 0, &c->ctxt_handle,
+                                                 &outbuf_desc, &c->context_flags, &c->ctxt_timestamp);
         av_freep(&inbuf[0].pvBuffer);
 
+        av_log(h, AV_LOG_TRACE, "Handshake res with %d bytes of data: 0x%lx\n", c->enc_buf_offset, sspi_ret);
+
         if (sspi_ret == SEC_E_INCOMPLETE_MESSAGE) {
-            av_log(h, AV_LOG_DEBUG, "Received incomplete handshake, need more data\n");
+            av_log(h, AV_LOG_TRACE, "Received incomplete handshake, need more data\n");
             read_data = 1;
             continue;
         }
 
+        c->have_context = 1;
+
         /* remote requests a client certificate - attempt to continue without one anyway */
         if (sspi_ret == SEC_I_INCOMPLETE_CREDENTIALS &&
             !(c->request_flags & ISC_REQ_USE_SUPPLIED_CREDS)) {
@@ -231,10 +858,10 @@ static int tls_client_handshake_loop(URLContext *h, int initial)
         }
 
         /* continue handshake */
-        if (sspi_ret == SEC_I_CONTINUE_NEEDED || sspi_ret == SEC_E_OK) {
+        if (sspi_ret == SEC_I_CONTINUE_NEEDED || sspi_ret == SEC_I_MESSAGE_FRAGMENT || sspi_ret == SEC_E_OK) {
             for (i = 0; i < 3; i++) {
                 if (outbuf[i].BufferType == SECBUFFER_TOKEN && outbuf[i].cbBuffer > 0) {
-                    ret = ffurl_write(c->tls_shared.tcp, outbuf[i].pvBuffer, outbuf[i].cbBuffer);
+                    ret = ffurl_write(uc, outbuf[i].pvBuffer, outbuf[i].cbBuffer);
                     if (ret < 0 || ret != outbuf[i].cbBuffer) {
                         av_log(h, AV_LOG_VERBOSE, "Failed to send handshake data\n");
                         ret = AVERROR(EIO);
@@ -256,12 +883,19 @@ static int tls_client_handshake_loop(URLContext *h, int initial)
             goto fail;
         }
 
+        if (sspi_ret == SEC_I_MESSAGE_FRAGMENT) {
+            av_log(h, AV_LOG_TRACE, "Writing fragmented output message part\n");
+            read_data = 0;
+            continue;
+        }
+
         if (inbuf[1].BufferType == SECBUFFER_EXTRA && inbuf[1].cbBuffer > 0) {
             if (c->enc_buf_offset > inbuf[1].cbBuffer) {
                 memmove(c->enc_buf, (c->enc_buf + c->enc_buf_offset) - inbuf[1].cbBuffer,
                         inbuf[1].cbBuffer);
                 c->enc_buf_offset = inbuf[1].cbBuffer;
                 if (sspi_ret == SEC_I_CONTINUE_NEEDED) {
+                    av_log(h, AV_LOG_TRACE, "Sent reply, handshake continues. %d extra bytes\n", (int)inbuf[1].cbBuffer);
                     read_data = 0;
                     continue;
                 }
@@ -271,6 +905,7 @@ static int tls_client_handshake_loop(URLContext *h, int initial)
         }
 
         if (sspi_ret == SEC_I_CONTINUE_NEEDED) {
+            av_log(h, AV_LOG_TRACE, "Handshake continues\n");
             read_data = 1;
             continue;
         }
@@ -278,6 +913,8 @@ static int tls_client_handshake_loop(URLContext *h, int initial)
         break;
     }
 
+    av_log(h, AV_LOG_TRACE, "Handshake completed\n");
+
     return 0;
 
 fail:
@@ -289,6 +926,8 @@ fail:
         }
     }
 
+    av_log(h, AV_LOG_TRACE, "Handshake failed\n");
+
     return ret;
 }
 
@@ -296,6 +935,7 @@ static int tls_client_handshake(URLContext *h)
 {
     TLSContext *c = h->priv_data;
     TLSShared *s = &c->tls_shared;
+    URLContext *uc = s->is_dtls ? s->udp : s->tcp;
     SecBuffer outbuf;
     SecBufferDesc outbuf_desc;
     SECURITY_STATUS sspi_ret;
@@ -305,8 +945,11 @@ static int tls_client_handshake(URLContext *h)
     init_sec_buffer_desc(&outbuf_desc, &outbuf, 1);
 
     c->request_flags = ISC_REQ_SEQUENCE_DETECT | ISC_REQ_REPLAY_DETECT |
-                       ISC_REQ_CONFIDENTIALITY | ISC_REQ_ALLOCATE_MEMORY |
-                       ISC_REQ_STREAM;
+                       ISC_REQ_CONFIDENTIALITY | ISC_REQ_ALLOCATE_MEMORY;
+    if (s->is_dtls)
+        c->request_flags |= ISC_REQ_DATAGRAM;
+    else
+        c->request_flags |= ISC_REQ_STREAM;
 
     sspi_ret = InitializeSecurityContext(&c->cred_handle, NULL, s->host, c->request_flags, 0, 0,
                                          NULL, 0, &c->ctxt_handle, &outbuf_desc, &c->context_flags,
@@ -317,8 +960,10 @@ static int tls_client_handshake(URLContext *h)
         goto fail;
     }
 
-    s->tcp->flags &= ~AVIO_FLAG_NONBLOCK;
-    ret = ffurl_write(s->tcp, outbuf.pvBuffer, outbuf.cbBuffer);
+    c->have_context = 1;
+
+    uc->flags &= ~AVIO_FLAG_NONBLOCK;
+    ret = ffurl_write(uc, outbuf.pvBuffer, outbuf.cbBuffer);
     FreeContextBuffer(outbuf.pvBuffer);
     if (ret < 0 || ret != outbuf.cbBuffer) {
         av_log(h, AV_LOG_ERROR, "Failed to send initial handshake data\n");
@@ -326,44 +971,121 @@ static int tls_client_handshake(URLContext *h)
         goto fail;
     }
 
-    return tls_client_handshake_loop(h, 1);
+    return tls_handshake_loop(h, 1);
 
 fail:
     DeleteSecurityContext(&c->ctxt_handle);
     return ret;
 }
 
-static int tls_open(URLContext *h, const char *uri, int flags, AVDictionary **options)
+static int tls_server_handshake(URLContext *h)
+{
+    TLSContext *c = h->priv_data;
+    TLSShared *s = &c->tls_shared;
+
+    c->request_flags = ASC_REQ_SEQUENCE_DETECT | ASC_REQ_REPLAY_DETECT |
+                       ASC_REQ_CONFIDENTIALITY | ASC_REQ_ALLOCATE_MEMORY;
+    if (s->is_dtls)
+        c->request_flags |= ASC_REQ_DATAGRAM;
+    else
+        c->request_flags |= ASC_REQ_STREAM;
+
+    c->have_context = 0;
+
+    return tls_handshake_loop(h, 1);
+}
+
+static int tls_handshake(URLContext *h)
 {
     TLSContext *c = h->priv_data;
     TLSShared *s = &c->tls_shared;
     SECURITY_STATUS sspi_ret;
-    SCHANNEL_CRED schannel_cred = { 0 };
-    int ret;
+    int ret = 0;
 
-    if ((ret = ff_tls_open_underlying(s, h, uri, options)) < 0)
-        goto fail;
+    if (s->listen)
+        ret = tls_server_handshake(h);
+    else
+        ret = tls_client_handshake(h);
 
-    if (s->listen) {
-        av_log(h, AV_LOG_ERROR, "TLS Listen Sockets with SChannel is not implemented.\n");
-        ret = AVERROR(EINVAL);
+    if (ret < 0)
         goto fail;
+
+    if (s->is_dtls && s->mtu > 0) {
+        ULONG mtu = s->mtu;
+        sspi_ret = SetContextAttributes(&c->ctxt_handle, SECPKG_ATTR_DTLS_MTU, &mtu, sizeof(mtu));
+        if (sspi_ret != SEC_E_OK) {
+            av_log(h, AV_LOG_ERROR, "Failed setting DTLS MTU to %d.\n", s->mtu);
+            ret = AVERROR(EINVAL);
+            goto fail;
+        }
+        av_log(h, AV_LOG_VERBOSE, "Set DTLS MTU to %d\n", s->mtu);
+    }
+
+    c->connected = 1;
+    s->state = DTLS_STATE_FINISHED;
+
+fail:
+    return ret;
+}
+
+static int tls_open(URLContext *h, const char *uri, int flags, AVDictionary **options)
+{
+    TLSContext *c = h->priv_data;
+    TLSShared *s = &c->tls_shared;
+    SECURITY_STATUS sspi_ret;
+    SCHANNEL_CRED schannel_cred = { 0 };
+    PCCERT_CONTEXT crtctx = NULL;
+    NCRYPT_KEY_HANDLE key = 0;
+    int ret = 0;
+
+    if (!s->external_sock) {
+        if ((ret = ff_tls_open_underlying(s, h, uri, options)) < 0)
+            goto fail;
     }
 
     /* SChannel Options */
     schannel_cred.dwVersion = SCHANNEL_CRED_VERSION;
 
-    if (s->verify)
-        schannel_cred.dwFlags = SCH_CRED_AUTO_CRED_VALIDATION |
-                                SCH_CRED_REVOCATION_CHECK_CHAIN;
-    else
-        schannel_cred.dwFlags = SCH_CRED_MANUAL_CRED_VALIDATION |
-                                SCH_CRED_IGNORE_NO_REVOCATION_CHECK |
-                                SCH_CRED_IGNORE_REVOCATION_OFFLINE;
+    if (s->listen) {
+        if (s->key_buf && s->cert_buf) {
+            ret = tls_import_key_cert(s->key_buf, s->cert_buf, &key, &crtctx);
+            if (ret < 0)
+                goto fail;
+        } else if (s->key_file && s->cert_file) {
+            ret = tls_load_key_cert(s->key_file, s->cert_file, &key, &crtctx);
+            if (ret < 0)
+                goto fail;
+        } else {
+            av_log(h, AV_LOG_VERBOSE, "No server certificate provided, using self-signed\n");
+            ret = tls_gen_self_signed(&key, &crtctx);
+            if (ret < 0)
+                goto fail;
+        }
+
+        schannel_cred.cCreds = 1;
+        schannel_cred.paCred = &crtctx;
+
+        schannel_cred.dwFlags = SCH_CRED_NO_SYSTEM_MAPPER | SCH_CRED_MANUAL_CRED_VALIDATION;
+
+        if (s->is_dtls)
+            schannel_cred.grbitEnabledProtocols = SP_PROT_DTLS1_X_SERVER;
+    } else {
+        if (s->verify)
+            schannel_cred.dwFlags = SCH_CRED_AUTO_CRED_VALIDATION |
+                                    SCH_CRED_REVOCATION_CHECK_CHAIN;
+        else
+            schannel_cred.dwFlags = SCH_CRED_MANUAL_CRED_VALIDATION |
+                                    SCH_CRED_IGNORE_NO_REVOCATION_CHECK |
+                                    SCH_CRED_IGNORE_REVOCATION_OFFLINE;
+
+        if (s->is_dtls)
+            schannel_cred.grbitEnabledProtocols = SP_PROT_DTLS1_X_CLIENT;
+    }
 
     /* Get credential handle */
-    sspi_ret = AcquireCredentialsHandle(NULL, (TCHAR *)UNISP_NAME, SECPKG_CRED_OUTBOUND,
-                                        NULL,  &schannel_cred, NULL, NULL, &c->cred_handle,
+    sspi_ret = AcquireCredentialsHandle(NULL, (TCHAR *)UNISP_NAME,
+                                        s->listen ? SECPKG_CRED_INBOUND : SECPKG_CRED_OUTBOUND,
+                                        NULL, &schannel_cred, NULL, NULL, &c->cred_handle,
                                         &c->cred_timestamp);
     if (sspi_ret != SEC_E_OK) {
         av_log(h, AV_LOG_ERROR, "Unable to acquire security credentials (0x%lx)\n", sspi_ret);
@@ -371,23 +1093,42 @@ static int tls_open(URLContext *h, const char *uri, int flags, AVDictionary **op
         goto fail;
     }
 
-    ret = tls_client_handshake(h);
-    if (ret < 0)
-        goto fail;
-
-    c->connected = 1;
+    if (!s->external_sock) {
+        ret = tls_handshake(h);
+        if (ret < 0)
+            goto fail;
+    }
 
-    return 0;
+    goto end;
 
 fail:
     tls_close(h);
+
+end:
+    if (crtctx)
+        CertFreeCertificateContext(crtctx);
+    if (key)
+        if (NCryptDeleteKey(key, NCRYPT_SILENT_FLAG) != ERROR_SUCCESS)
+            NCryptFreeObject(key);
+
     return ret;
 }
 
+static int dtls_open(URLContext *h, const char *uri, int flags, AVDictionary **options)
+{
+    TLSContext *c = h->priv_data;
+    TLSShared *s = &c->tls_shared;
+
+    s->is_dtls = 1;
+
+    return tls_open(h, uri, flags, options);
+}
+
 static int tls_read(URLContext *h, uint8_t *buf, int len)
 {
     TLSContext *c = h->priv_data;
     TLSShared *s = &c->tls_shared;
+    URLContext *uc = s->is_dtls ? s->udp : s->tcp;
     SECURITY_STATUS sspi_ret = SEC_E_OK;
     SecBuffer inbuf[4];
     SecBufferDesc inbuf_desc;
@@ -418,10 +1159,10 @@ static int tls_read(URLContext *h, uint8_t *buf, int len)
             }
         }
 
-        s->tcp->flags &= ~AVIO_FLAG_NONBLOCK;
-        s->tcp->flags |= h->flags & AVIO_FLAG_NONBLOCK;
+        uc->flags &= ~AVIO_FLAG_NONBLOCK;
+        uc->flags |= h->flags & AVIO_FLAG_NONBLOCK;
 
-        ret = ffurl_read(s->tcp, c->enc_buf + c->enc_buf_offset,
+        ret = ffurl_read(uc, c->enc_buf + c->enc_buf_offset,
                          c->enc_buf_size - c->enc_buf_offset);
         if (ret == AVERROR_EOF) {
             c->connection_closed = 1;
@@ -489,7 +1230,7 @@ static int tls_read(URLContext *h, uint8_t *buf, int len)
                 }
 
                 av_log(h, AV_LOG_VERBOSE, "Re-negotiating security context\n");
-                ret = tls_client_handshake_loop(h, 0);
+                ret = tls_handshake_loop(h, 0);
                 if (ret < 0) {
                     goto cleanup;
                 }
@@ -536,6 +1277,7 @@ static int tls_write(URLContext *h, const uint8_t *buf, int len)
 {
     TLSContext *c = h->priv_data;
     TLSShared *s = &c->tls_shared;
+    URLContext *uc = s->is_dtls ? s->udp : s->tcp;
     SECURITY_STATUS sspi_ret;
     int ret = 0, data_size;
     uint8_t *data = NULL;
@@ -549,7 +1291,7 @@ static int tls_write(URLContext *h, const uint8_t *buf, int len)
     }
 
     /* limit how much data we can consume */
-    len = FFMIN(len, c->sizes.cbMaximumMessage);
+    len = FFMIN(len, c->sizes.cbMaximumMessage - c->sizes.cbHeader - c->sizes.cbTrailer);
 
     data_size = c->sizes.cbHeader + len + c->sizes.cbTrailer;
     data = av_malloc(data_size);
@@ -572,10 +1314,10 @@ static int tls_write(URLContext *h, const uint8_t *buf, int len)
     if (sspi_ret == SEC_E_OK)  {
         len = outbuf[0].cbBuffer + outbuf[1].cbBuffer + outbuf[2].cbBuffer;
 
-        s->tcp->flags &= ~AVIO_FLAG_NONBLOCK;
-        s->tcp->flags |= h->flags & AVIO_FLAG_NONBLOCK;
+        uc->flags &= ~AVIO_FLAG_NONBLOCK;
+        uc->flags |= h->flags & AVIO_FLAG_NONBLOCK;
 
-        ret = ffurl_write(s->tcp, data, len);
+        ret = ffurl_write(uc, data, len);
         if (ret == AVERROR(EAGAIN)) {
             goto done;
         } else if (ret < 0 || ret != len) {
@@ -600,13 +1342,15 @@ done:
 static int tls_get_file_handle(URLContext *h)
 {
     TLSContext *c = h->priv_data;
-    return ffurl_get_file_handle(c->tls_shared.tcp);
+    TLSShared *s = &c->tls_shared;
+    return ffurl_get_file_handle(s->is_dtls ? s->udp : s->tcp);
 }
 
 static int tls_get_short_seek(URLContext *h)
 {
-    TLSContext *s = h->priv_data;
-    return ffurl_get_short_seek(s->tls_shared.tcp);
+    TLSContext *c = h->priv_data;
+    TLSShared *s = &c->tls_shared;
+    return ffurl_get_short_seek(s->is_dtls ? s->udp : s->tcp);
 }
 
 static const AVOption options[] = {
@@ -633,3 +1377,24 @@ const URLProtocol ff_tls_protocol = {
     .flags          = URL_PROTOCOL_FLAG_NETWORK,
     .priv_data_class = &tls_class,
 };
+
+static const AVClass dtls_class = {
+    .class_name = "dtls",
+    .item_name  = av_default_item_name,
+    .option     = options,
+    .version    = LIBAVUTIL_VERSION_INT,
+};
+
+const URLProtocol ff_dtls_protocol = {
+    .name           = "dtls",
+    .url_open2      = dtls_open,
+    .url_handshake  = tls_handshake,
+    .url_close      = tls_close,
+    .url_read       = tls_read,
+    .url_write      = tls_write,
+    .url_get_file_handle = tls_get_file_handle,
+    .url_get_short_seek  = tls_get_short_seek,
+    .priv_data_size = sizeof(TLSContext),
+    .flags          = URL_PROTOCOL_FLAG_NETWORK,
+    .priv_data_class = &dtls_class,
+};
-- 
2.49.0

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* [FFmpeg-devel] [PATCH v2 7/8] avformat/tls_schannel: add option to load server certificate from store
  2025-07-06 18:36 [FFmpeg-devel] [PATCH v2 0/8] WHIP + TLS + UDP fixes and SChannel DTLS support Timo Rothenpieler
                   ` (5 preceding siblings ...)
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 6/8] avformat/tls_schannel: add DTLS support Timo Rothenpieler
@ 2025-07-06 18:36 ` Timo Rothenpieler
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 8/8] avformat/tls_schannel: fix non-blocking write breaking TLS sessions Timo Rothenpieler
  7 siblings, 0 replies; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-06 18:36 UTC (permalink / raw)
  To: ffmpeg-devel; +Cc: Timo Rothenpieler

---
 libavformat/tls_schannel.c | 47 ++++++++++++++++++++++++++++++++------
 1 file changed, 40 insertions(+), 7 deletions(-)

diff --git a/libavformat/tls_schannel.c b/libavformat/tls_schannel.c
index b985576b72..90d5765a80 100644
--- a/libavformat/tls_schannel.c
+++ b/libavformat/tls_schannel.c
@@ -502,6 +502,32 @@ end:
     return ret;
 }
 
+static int tls_cert_from_store(void *logctx, const char *cert_store_name, const char *cert_subj, PCCERT_CONTEXT *crtctx)
+{
+    HCERTSTORE cert_store = NULL;
+    int ret = 0;
+
+    cert_store = CertOpenStore(CERT_STORE_PROV_SYSTEM_A, 0, 0, CERT_SYSTEM_STORE_CURRENT_USER, cert_store_name);
+    if (!cert_store) {
+        av_log(logctx, AV_LOG_ERROR, "Opening user cert store %s failed\n", cert_store_name);
+        ret = AVERROR_EXTERNAL;
+        goto end;
+    }
+
+    *crtctx = CertFindCertificateInStore(cert_store, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0, CERT_FIND_SUBJECT_STR_A, cert_subj, NULL);
+    if (!*crtctx) {
+        av_log(logctx, AV_LOG_ERROR, "Could not find certificate in store\n");
+        ret = AVERROR_EXTERNAL;
+        goto end;
+    }
+
+end:
+    if (cert_store)
+        CertCloseStore(cert_store, 0);
+
+    return ret;
+}
+
 static int tls_load_key_cert(char *key_url, char *cert_url, NCRYPT_KEY_HANDLE *key, PCCERT_CONTEXT *crtctx)
 {
     AVBPrint key_bp, cert_bp;
@@ -561,6 +587,9 @@ typedef struct TLSContext {
     const AVClass *class;
     TLSShared tls_shared;
 
+    char *cert_store_subject;
+    char *cert_store_name;
+
     CredHandle cred_handle;
     TimeStamp cred_timestamp;
 
@@ -1047,21 +1076,20 @@ static int tls_open(URLContext *h, const char *uri, int flags, AVDictionary **op
     schannel_cred.dwVersion = SCHANNEL_CRED_VERSION;
 
     if (s->listen) {
-        if (s->key_buf && s->cert_buf) {
+        if (c->cert_store_name && c->cert_store_subject) {
+            ret = tls_cert_from_store(h, c->cert_store_name, c->cert_store_subject, &crtctx);
+        } else if (s->key_buf && s->cert_buf) {
             ret = tls_import_key_cert(s->key_buf, s->cert_buf, &key, &crtctx);
-            if (ret < 0)
-                goto fail;
         } else if (s->key_file && s->cert_file) {
             ret = tls_load_key_cert(s->key_file, s->cert_file, &key, &crtctx);
-            if (ret < 0)
-                goto fail;
         } else {
             av_log(h, AV_LOG_VERBOSE, "No server certificate provided, using self-signed\n");
             ret = tls_gen_self_signed(&key, &crtctx);
-            if (ret < 0)
-                goto fail;
         }
 
+        if (ret < 0)
+            goto fail;
+
         schannel_cred.cCreds = 1;
         schannel_cred.paCred = &crtctx;
 
@@ -1353,8 +1381,13 @@ static int tls_get_short_seek(URLContext *h)
     return ffurl_get_short_seek(s->is_dtls ? s->udp : s->tcp);
 }
 
+#define OFFSET(x) offsetof(TLSContext, x)
 static const AVOption options[] = {
     TLS_COMMON_OPTIONS(TLSContext, tls_shared),
+    { "cert_store_subject", "Load certificate (and associated key) from users keystore by subject",
+                            OFFSET(cert_store_subject), AV_OPT_TYPE_STRING, .flags = TLS_OPTFL },
+    { "cert_store_name",    "Name of the specific cert store to search in (for cert_store_subject)",
+                            OFFSET(cert_store_name), AV_OPT_TYPE_STRING, { .str = "MY" }, .flags = TLS_OPTFL },
     { NULL }
 };
 
-- 
2.49.0

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* [FFmpeg-devel] [PATCH v2 8/8] avformat/tls_schannel: fix non-blocking write breaking TLS sessions
  2025-07-06 18:36 [FFmpeg-devel] [PATCH v2 0/8] WHIP + TLS + UDP fixes and SChannel DTLS support Timo Rothenpieler
                   ` (6 preceding siblings ...)
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 7/8] avformat/tls_schannel: add option to load server certificate from store Timo Rothenpieler
@ 2025-07-06 18:36 ` Timo Rothenpieler
  7 siblings, 0 replies; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-06 18:36 UTC (permalink / raw)
  To: ffmpeg-devel; +Cc: Timo Rothenpieler

---
 libavformat/tls_schannel.c | 111 ++++++++++++++++++++++++++-----------
 1 file changed, 79 insertions(+), 32 deletions(-)

diff --git a/libavformat/tls_schannel.c b/libavformat/tls_schannel.c
index 90d5765a80..031d12fa27 100644
--- a/libavformat/tls_schannel.c
+++ b/libavformat/tls_schannel.c
@@ -608,6 +608,10 @@ typedef struct TLSContext {
     int dec_buf_size;
     int dec_buf_offset;
 
+    char *send_buf;
+    int send_buf_size;
+    int send_buf_offset;
+
     SecPkgContext_StreamSizes sizes;
 
     int connected;
@@ -692,6 +696,35 @@ static void init_sec_buffer_desc(SecBufferDesc *desc, SecBuffer *buffers,
     desc->cBuffers = buffer_count;
 }
 
+static int tls_process_send_buffer(URLContext *h)
+{
+    TLSContext *c = h->priv_data;
+    TLSShared *s = &c->tls_shared;
+    URLContext *uc = s->is_dtls ? s->udp : s->tcp;
+    int ret;
+
+    if (!c->send_buf)
+        return 0;
+
+    ret = ffurl_write(uc, c->send_buf + c->send_buf_offset, c->send_buf_size - c->send_buf_offset);
+    if (ret == AVERROR(EAGAIN)) {
+        return AVERROR(EAGAIN);
+    } else if (ret < 0) {
+        av_log(h, AV_LOG_ERROR, "Writing encrypted data to socket failed\n");
+        return AVERROR(EIO);
+    }
+
+    c->send_buf_offset += ret;
+
+    if (c->send_buf_offset < c->send_buf_size)
+        return AVERROR(EAGAIN);
+
+    av_freep(&c->send_buf);
+    c->send_buf_size = c->send_buf_offset = 0;
+
+    return 0;
+}
+
 static int tls_shutdown_client(URLContext *h)
 {
     TLSContext *c = h->priv_data;
@@ -710,6 +743,11 @@ static int tls_shutdown_client(URLContext *h)
         init_sec_buffer(&Buffer, SECBUFFER_TOKEN, &dwshut, sizeof(dwshut));
         init_sec_buffer_desc(&BuffDesc, &Buffer, 1);
 
+        uc->flags &= ~AVIO_FLAG_NONBLOCK;
+        ret = tls_process_send_buffer(h);
+        if (ret < 0)
+            return ret;
+
         sspi_ret = ApplyControlToken(&c->ctxt_handle, &BuffDesc);
         if (sspi_ret != SEC_E_OK)
             av_log(h, AV_LOG_ERROR, "ApplyControlToken failed\n");
@@ -729,7 +767,6 @@ static int tls_shutdown_client(URLContext *h)
 
             if (outbuf.pvBuffer) {
                 if (outbuf.cbBuffer > 0) {
-                    uc->flags &= ~AVIO_FLAG_NONBLOCK;
                     ret = ffurl_write(uc, outbuf.pvBuffer, outbuf.cbBuffer);
                     if (ret < 0 || ret != outbuf.cbBuffer)
                         av_log(h, AV_LOG_ERROR, "Failed to send close message\n");
@@ -761,6 +798,9 @@ static int tls_close(URLContext *h)
     av_freep(&c->dec_buf);
     c->dec_buf_size = c->dec_buf_offset = 0;
 
+    av_freep(&c->send_buf);
+    c->send_buf_size = c->send_buf_offset = 0;
+
     if (s->is_dtls) {
         if (!s->external_sock)
             ffurl_closep(&c->tls_shared.udp);
@@ -1263,6 +1303,11 @@ static int tls_read(URLContext *h, uint8_t *buf, int len)
                     goto cleanup;
                 }
                 sspi_ret = SEC_E_OK;
+
+                /* if somehow any send data was left, it is now invalid */
+                av_freep(&c->send_buf);
+                c->send_buf_size = c->send_buf_offset = 0;
+
                 continue;
             } else if (sspi_ret == SEC_I_CONTEXT_EXPIRED) {
                 c->sspi_close_notify = 1;
@@ -1307,10 +1352,16 @@ static int tls_write(URLContext *h, const uint8_t *buf, int len)
     TLSShared *s = &c->tls_shared;
     URLContext *uc = s->is_dtls ? s->udp : s->tcp;
     SECURITY_STATUS sspi_ret;
-    int ret = 0, data_size;
-    uint8_t *data = NULL;
     SecBuffer outbuf[4];
     SecBufferDesc outbuf_desc;
+    int ret = 0;
+
+    uc->flags &= ~AVIO_FLAG_NONBLOCK;
+    uc->flags |= h->flags & AVIO_FLAG_NONBLOCK;
+
+    ret = tls_process_send_buffer(h);
+    if (ret < 0)
+        return ret;
 
     if (c->sizes.cbMaximumMessage == 0) {
         sspi_ret = QueryContextAttributes(&c->ctxt_handle, SECPKG_ATTR_STREAM_SIZES, &c->sizes);
@@ -1321,50 +1372,46 @@ static int tls_write(URLContext *h, const uint8_t *buf, int len)
     /* limit how much data we can consume */
     len = FFMIN(len, c->sizes.cbMaximumMessage - c->sizes.cbHeader - c->sizes.cbTrailer);
 
-    data_size = c->sizes.cbHeader + len + c->sizes.cbTrailer;
-    data = av_malloc(data_size);
-    if (data == NULL)
+    c->send_buf_size = c->sizes.cbHeader + len + c->sizes.cbTrailer;
+    c->send_buf = av_malloc(c->send_buf_size);
+    if (c->send_buf == NULL)
         return AVERROR(ENOMEM);
 
     init_sec_buffer(&outbuf[0], SECBUFFER_STREAM_HEADER,
-                  data, c->sizes.cbHeader);
+                    c->send_buf, c->sizes.cbHeader);
     init_sec_buffer(&outbuf[1], SECBUFFER_DATA,
-                  data + c->sizes.cbHeader, len);
+                    c->send_buf + c->sizes.cbHeader, len);
     init_sec_buffer(&outbuf[2], SECBUFFER_STREAM_TRAILER,
-                  data + c->sizes.cbHeader + len,
-                  c->sizes.cbTrailer);
+                    c->send_buf + c->sizes.cbHeader + len,
+                    c->sizes.cbTrailer);
     init_sec_buffer(&outbuf[3], SECBUFFER_EMPTY, NULL, 0);
     init_sec_buffer_desc(&outbuf_desc, outbuf, 4);
 
     memcpy(outbuf[1].pvBuffer, buf, len);
 
     sspi_ret = EncryptMessage(&c->ctxt_handle, 0, &outbuf_desc, 0);
-    if (sspi_ret == SEC_E_OK)  {
-        len = outbuf[0].cbBuffer + outbuf[1].cbBuffer + outbuf[2].cbBuffer;
-
-        uc->flags &= ~AVIO_FLAG_NONBLOCK;
-        uc->flags |= h->flags & AVIO_FLAG_NONBLOCK;
-
-        ret = ffurl_write(uc, data, len);
-        if (ret == AVERROR(EAGAIN)) {
-            goto done;
-        } else if (ret < 0 || ret != len) {
-            ret = AVERROR(EIO);
-            av_log(h, AV_LOG_ERROR, "Writing encrypted data to socket failed\n");
-            goto done;
-        }
-    } else {
+    if (sspi_ret != SEC_E_OK) {
+        av_freep(&c->send_buf);
         av_log(h, AV_LOG_ERROR, "Encrypting data failed\n");
         if (sspi_ret == SEC_E_INSUFFICIENT_MEMORY)
-            ret = AVERROR(ENOMEM);
-        else
-            ret = AVERROR(EIO);
-        goto done;
+            return AVERROR(ENOMEM);
+        return AVERROR(EIO);
+    }
+
+    c->send_buf_size = outbuf[0].cbBuffer + outbuf[1].cbBuffer + outbuf[2].cbBuffer;
+    c->send_buf_offset = 0;
+
+    ret = tls_process_send_buffer(h);
+    if (ret == AVERROR(EAGAIN)) {
+         /* We always need to signal that we consumed all (encryped) data since schannel must not
+            be fed the same data again. Sending will then be completed next call to this function,
+            and EAGAIN returned until all remaining buffer is sent. */
+        return outbuf[1].cbBuffer;
+    } else if (ret < 0) {
+        return ret;
     }
 
-done:
-    av_freep(&data);
-    return ret < 0 ? ret : outbuf[1].cbBuffer;
+    return outbuf[1].cbBuffer;
 }
 
 static int tls_get_file_handle(URLContext *h)
-- 
2.49.0

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [FFmpeg-devel] [PATCH v2 1/8] avformat/tls: move whip specific init out of generic tls code
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 1/8] avformat/tls: move whip specific init out of generic tls code Timo Rothenpieler
@ 2025-07-07  6:30   ` Jack Lau
  2025-07-07 11:26     ` Timo Rothenpieler
  0 siblings, 1 reply; 15+ messages in thread
From: Jack Lau @ 2025-07-07  6:30 UTC (permalink / raw)
  To: FFmpeg development discussions and patches



> On Jul 7, 2025, at 02:36, Timo Rothenpieler <timo@rothenpieler.org> wrote:
> 
> ---
> libavformat/tls.c         |  9 ---------
> libavformat/tls_openssl.c | 12 ++++++++----
> libavformat/whip.c        |  5 +++++
> 3 files changed, 13 insertions(+), 13 deletions(-)
> 
> diff --git a/libavformat/tls.c b/libavformat/tls.c
> index c0adaf61ce..bd9c05e6dc 100644
> --- a/libavformat/tls.c
> +++ b/libavformat/tls.c
> @@ -141,15 +141,6 @@ int ff_tls_open_underlying(TLSShared *c, URLContext *parent, const char *uri, AV
>     ret = ffurl_open_whitelist(c->is_dtls ? &c->udp : &c->tcp, buf, AVIO_FLAG_READ_WRITE,
>                                &parent->interrupt_callback, options,
>                                parent->protocol_whitelist, parent->protocol_blacklist, parent);
> -    if (c->is_dtls) {
> -        if (ret < 0) {
> -            av_log(c, AV_LOG_ERROR, "Failed to open udp://%s:%d\n", c->underlying_host, port);
> -            return ret;
> -        }
> -        /* Make the socket non-blocking, set to READ and WRITE mode after connected */
> -        ff_socket_nonblock(ffurl_get_file_handle(c->udp), 1);
> -        c->udp->flags |= AVIO_FLAG_READ | AVIO_FLAG_NONBLOCK;
> -    }
>     return ret;
> }
> 
> diff --git a/libavformat/tls_openssl.c b/libavformat/tls_openssl.c
> index 08527418b0..0c76f110e3 100644
> --- a/libavformat/tls_openssl.c
> +++ b/libavformat/tls_openssl.c
> @@ -1128,14 +1128,16 @@ static int tls_write(URLContext *h, const uint8_t *buf, int size)
> 
> static int tls_get_file_handle(URLContext *h)
> {
> -    TLSContext *c = h->priv_data;
> -    return ffurl_get_file_handle(c->tls_shared.tcp);
> +    TLSContext *p = h->priv_data;
> +    TLSShared *c = &p->tls_shared;
> +    return ffurl_get_file_handle(c->is_dtls ? c->udp : c->tcp);
> }
> 
> static int tls_get_short_seek(URLContext *h)
> {
> -    TLSContext *s = h->priv_data;
> -    return ffurl_get_short_seek(s->tls_shared.tcp);
> +    TLSContext *p = h->priv_data;
> +    TLSShared *c = &p->tls_shared;
> +    return ffurl_get_short_seek(c->is_dtls ? c->udp : c->tcp);
> }
> 
> static const AVOption options[] = {
> @@ -1177,6 +1179,8 @@ const URLProtocol ff_dtls_protocol = {
>     .url_close      = dtls_close,
>     .url_read       = tls_read,
>     .url_write      = tls_write,
> +    .url_get_file_handle = tls_get_file_handle,
> +    .url_get_short_seek  = tls_get_short_seek,
>     .priv_data_size = sizeof(TLSContext),
>     .flags          = URL_PROTOCOL_FLAG_NETWORK,
>     .priv_data_class = &dtls_class,
> diff --git a/libavformat/whip.c b/libavformat/whip.c
> index 84d4c5a1f3..4ac76e79f2 100644
> --- a/libavformat/whip.c
> +++ b/libavformat/whip.c
> @@ -388,6 +388,11 @@ static av_cold int dtls_initialize(AVFormatContext *s)
>     WHIPContext *whip = s->priv_data;
>     /* reuse the udp created by whip */
>     ff_dtls_set_udp(whip->dtls_uc, whip->udp);
> +
> +    /* Make the socket non-blocking */
> +    ff_socket_nonblock(ffurl_get_file_handle(whip->dtls_uc), 1);
> +    whip->dtls_uc->flags |= AVIO_FLAG_NONBLOCK;
> +
I think it’s redundant since udp_connect function has set these already.
>     return 0;
> }
> 
> -- 
> 2.49.0
> 
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
> 
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [FFmpeg-devel] [PATCH v2 2/8] avformat/udp: make recv addr of each packet available
  2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 2/8] avformat/udp: make recv addr of each packet available Timo Rothenpieler
@ 2025-07-07  8:03   ` Jack Lau
  2025-07-07 11:28     ` Timo Rothenpieler
  0 siblings, 1 reply; 15+ messages in thread
From: Jack Lau @ 2025-07-07  8:03 UTC (permalink / raw)
  To: FFmpeg development discussions and patches



> On Jul 7, 2025, at 02:36, Timo Rothenpieler <timo@rothenpieler.org> wrote:
> 
> ---
> libavformat/network.h |  2 ++
> libavformat/udp.c     | 25 +++++++++++++++++--------
> 2 files changed, 19 insertions(+), 8 deletions(-)
> 
> diff --git a/libavformat/network.h b/libavformat/network.h
> index ca214087fc..48bb75a758 100644
> --- a/libavformat/network.h
> +++ b/libavformat/network.h
> @@ -338,4 +338,6 @@ int ff_connect_parallel(struct addrinfo *addrs, int timeout_ms_per_address,
>                         int parallel, URLContext *h, int *fd,
>                         int (*customize_fd)(void *, int, int), void *customize_ctx);
> 
> +void ff_udp_get_last_recv_addr(URLContext *h, struct sockaddr_storage *addr);
> +
> #endif /* AVFORMAT_NETWORK_H */
> diff --git a/libavformat/udp.c b/libavformat/udp.c
> index 30f075de8e..7d64ff07ed 100644
> --- a/libavformat/udp.c
> +++ b/libavformat/udp.c
> @@ -107,7 +107,7 @@ typedef struct UDPContext {
>     pthread_cond_t cond;
>     int thread_started;
> #endif
> -    uint8_t tmp[UDP_MAX_PKT_SIZE+4];
> +    uint8_t tmp[UDP_MAX_PKT_SIZE + 4 + sizeof(struct sockaddr_storage)];
>     int remaining_in_dg;
>     char *localaddr;
>     int timeout;
> @@ -115,6 +115,7 @@ typedef struct UDPContext {
>     char *sources;
>     char *block;
>     IPSourceFilters filters;
> +    struct sockaddr_storage last_recv_addr;
> } UDPContext;
> 
> #define OFFSET(x) offsetof(UDPContext, x)
> @@ -467,6 +468,12 @@ int ff_udp_get_local_port(URLContext *h)
>     return s->local_port;
> }
> 
> +void ff_udp_get_last_recv_addr(URLContext *h, struct sockaddr_storage *addr)
> +{
> +    UDPContext *s = h->priv_data;
> +    *addr = s->last_recv_addr;
> +}
> +
> /**
>  * Return the udp file handle for select() usage to wait for several RTP
>  * streams at the same time.
> @@ -498,13 +505,14 @@ static void *circular_buffer_task_rx( void *_URLContext)
>         int len;
>         struct sockaddr_storage addr;
>         socklen_t addr_len = sizeof(addr);
> +        const int header_sz = 4 + addr_len;
> 
>         pthread_mutex_unlock(&s->mutex);
>         /* Blocking operations are always cancellation points;
>            see "General Information" / "Thread Cancelation Overview"
>            in Single Unix. */
>         pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_cancelstate);
> -        len = recvfrom(s->udp_fd, s->tmp+4, sizeof(s->tmp)-4, 0, (struct sockaddr *)&addr, &addr_len);
> +        len = recvfrom(s->udp_fd, s->tmp + header_sz, sizeof(s->tmp) - header_sz, 0, (struct sockaddr *)&addr, &addr_len);
>         pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelstate);
>         pthread_mutex_lock(&s->mutex);
>         if (len < 0) {
> @@ -517,8 +525,9 @@ static void *circular_buffer_task_rx( void *_URLContext)
>         if (ff_ip_check_source_lists(&addr, &s->filters))
>             continue;
>         AV_WL32(s->tmp, len);
> +        memcpy(s->tmp + 4, &addr, sizeof(addr));
> 
> -        if (av_fifo_can_write(s->fifo) < len + 4) {
> +        if (av_fifo_can_write(s->fifo) < len + header_sz) {
>             /* No Space left */
>             if (s->overrun_nonfatal) {
>                 av_log(h, AV_LOG_WARNING, "Circular buffer overrun. "
> @@ -532,7 +541,7 @@ static void *circular_buffer_task_rx( void *_URLContext)
>                 goto end;
>             }
>         }
> -        av_fifo_write(s->fifo, s->tmp, len + 4);
> +        av_fifo_write(s->fifo, s->tmp, len + header_sz);
>         pthread_cond_signal(&s->cond);
>     }
> 
> @@ -991,8 +1000,7 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
> {
>     UDPContext *s = h->priv_data;
>     int ret;
> -    struct sockaddr_storage addr;
> -    socklen_t addr_len = sizeof(addr);
> +    socklen_t addr_len = sizeof(s->last_recv_addr);
> #if HAVE_PTHREAD_CANCEL
>     int avail, nonblock = h->flags & AVIO_FLAG_NONBLOCK;
> 
> @@ -1004,6 +1012,7 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
>                 uint8_t tmp[4];
> 
>                 av_fifo_read(s->fifo, tmp, 4);
> +                av_fifo_read(s->fifo, &s->last_recv_addr, sizeof(s->last_recv_addr));
>                 avail = AV_RL32(tmp);
>                 if(avail > size){
>                     av_log(h, AV_LOG_WARNING, "Part of datagram lost due to insufficient buffer size\n");
> @@ -1043,10 +1052,10 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
>         if (ret < 0)
>             return ret;
>     }
> -    ret = recvfrom(s->udp_fd, buf, size, 0, (struct sockaddr *)&addr, &addr_len);
> +    ret = recvfrom(s->udp_fd, buf, size, 0, (struct sockaddr *)&s->last_recv_addr, &addr_len);
>     if (ret < 0)
>         return ff_neterrno();
> -    if (ff_ip_check_source_lists(&addr, &s->filters))
> +    if (ff_ip_check_source_lists(&s->last_recv_addr, &s->filters))
>         return AVERROR(EINTR);
>     return ret;
> }
> -- 
> 2.49.0
> 
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
> 
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe”.
I think it’s necessary to add "socklen_t last_recv_len".
I’ve send the fixes to your GitHub repo https://github.com/BtbN/FFmpeg/pull/3 because my fixes depends your patchset.

Best regards
Jack


_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [FFmpeg-devel] [PATCH v2 1/8] avformat/tls: move whip specific init out of generic tls code
  2025-07-07  6:30   ` Jack Lau
@ 2025-07-07 11:26     ` Timo Rothenpieler
  2025-07-07 12:33       ` Jack Lau
  0 siblings, 1 reply; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-07 11:26 UTC (permalink / raw)
  To: ffmpeg-devel

On 07/07/2025 08:30, Jack Lau wrote:
> 
> 
>> On Jul 7, 2025, at 02:36, Timo Rothenpieler <timo@rothenpieler.org> wrote:
>>
>> ---
>> libavformat/tls.c         |  9 ---------
>> libavformat/tls_openssl.c | 12 ++++++++----
>> libavformat/whip.c        |  5 +++++
>> 3 files changed, 13 insertions(+), 13 deletions(-)
>>
>> diff --git a/libavformat/tls.c b/libavformat/tls.c
>> index c0adaf61ce..bd9c05e6dc 100644
>> --- a/libavformat/tls.c
>> +++ b/libavformat/tls.c
>> @@ -141,15 +141,6 @@ int ff_tls_open_underlying(TLSShared *c, URLContext *parent, const char *uri, AV
>>      ret = ffurl_open_whitelist(c->is_dtls ? &c->udp : &c->tcp, buf, AVIO_FLAG_READ_WRITE,
>>                                 &parent->interrupt_callback, options,
>>                                 parent->protocol_whitelist, parent->protocol_blacklist, parent);
>> -    if (c->is_dtls) {
>> -        if (ret < 0) {
>> -            av_log(c, AV_LOG_ERROR, "Failed to open udp://%s:%d\n", c->underlying_host, port);
>> -            return ret;
>> -        }
>> -        /* Make the socket non-blocking, set to READ and WRITE mode after connected */
>> -        ff_socket_nonblock(ffurl_get_file_handle(c->udp), 1);
>> -        c->udp->flags |= AVIO_FLAG_READ | AVIO_FLAG_NONBLOCK;
>> -    }
>>      return ret;
>> }
>>
>> diff --git a/libavformat/tls_openssl.c b/libavformat/tls_openssl.c
>> index 08527418b0..0c76f110e3 100644
>> --- a/libavformat/tls_openssl.c
>> +++ b/libavformat/tls_openssl.c
>> @@ -1128,14 +1128,16 @@ static int tls_write(URLContext *h, const uint8_t *buf, int size)
>>
>> static int tls_get_file_handle(URLContext *h)
>> {
>> -    TLSContext *c = h->priv_data;
>> -    return ffurl_get_file_handle(c->tls_shared.tcp);
>> +    TLSContext *p = h->priv_data;
>> +    TLSShared *c = &p->tls_shared;
>> +    return ffurl_get_file_handle(c->is_dtls ? c->udp : c->tcp);
>> }
>>
>> static int tls_get_short_seek(URLContext *h)
>> {
>> -    TLSContext *s = h->priv_data;
>> -    return ffurl_get_short_seek(s->tls_shared.tcp);
>> +    TLSContext *p = h->priv_data;
>> +    TLSShared *c = &p->tls_shared;
>> +    return ffurl_get_short_seek(c->is_dtls ? c->udp : c->tcp);
>> }
>>
>> static const AVOption options[] = {
>> @@ -1177,6 +1179,8 @@ const URLProtocol ff_dtls_protocol = {
>>      .url_close      = dtls_close,
>>      .url_read       = tls_read,
>>      .url_write      = tls_write,
>> +    .url_get_file_handle = tls_get_file_handle,
>> +    .url_get_short_seek  = tls_get_short_seek,
>>      .priv_data_size = sizeof(TLSContext),
>>      .flags          = URL_PROTOCOL_FLAG_NETWORK,
>>      .priv_data_class = &dtls_class,
>> diff --git a/libavformat/whip.c b/libavformat/whip.c
>> index 84d4c5a1f3..4ac76e79f2 100644
>> --- a/libavformat/whip.c
>> +++ b/libavformat/whip.c
>> @@ -388,6 +388,11 @@ static av_cold int dtls_initialize(AVFormatContext *s)
>>      WHIPContext *whip = s->priv_data;
>>      /* reuse the udp created by whip */
>>      ff_dtls_set_udp(whip->dtls_uc, whip->udp);
>> +
>> +    /* Make the socket non-blocking */
>> +    ff_socket_nonblock(ffurl_get_file_handle(whip->dtls_uc), 1);
>> +    whip->dtls_uc->flags |= AVIO_FLAG_NONBLOCK;
>> +
> I think it’s redundant since udp_connect function has set these already.

No, it needs to be set on the tls URLContext, since all implementations 
forward their nonblocking flag (or lack thereof) to the underlying 
context each read/write.
_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [FFmpeg-devel] [PATCH v2 2/8] avformat/udp: make recv addr of each packet available
  2025-07-07  8:03   ` Jack Lau
@ 2025-07-07 11:28     ` Timo Rothenpieler
  2025-07-07 12:36       ` Jack Lau
  0 siblings, 1 reply; 15+ messages in thread
From: Timo Rothenpieler @ 2025-07-07 11:28 UTC (permalink / raw)
  To: ffmpeg-devel

On 07/07/2025 10:03, Jack Lau wrote:
> 
> 
>> On Jul 7, 2025, at 02:36, Timo Rothenpieler <timo@rothenpieler.org> wrote:
>>
>> ---
>> libavformat/network.h |  2 ++
>> libavformat/udp.c     | 25 +++++++++++++++++--------
>> 2 files changed, 19 insertions(+), 8 deletions(-)
>>
>> diff --git a/libavformat/network.h b/libavformat/network.h
>> index ca214087fc..48bb75a758 100644
>> --- a/libavformat/network.h
>> +++ b/libavformat/network.h
>> @@ -338,4 +338,6 @@ int ff_connect_parallel(struct addrinfo *addrs, int timeout_ms_per_address,
>>                          int parallel, URLContext *h, int *fd,
>>                          int (*customize_fd)(void *, int, int), void *customize_ctx);
>>
>> +void ff_udp_get_last_recv_addr(URLContext *h, struct sockaddr_storage *addr);
>> +
>> #endif /* AVFORMAT_NETWORK_H */
>> diff --git a/libavformat/udp.c b/libavformat/udp.c
>> index 30f075de8e..7d64ff07ed 100644
>> --- a/libavformat/udp.c
>> +++ b/libavformat/udp.c
>> @@ -107,7 +107,7 @@ typedef struct UDPContext {
>>      pthread_cond_t cond;
>>      int thread_started;
>> #endif
>> -    uint8_t tmp[UDP_MAX_PKT_SIZE+4];
>> +    uint8_t tmp[UDP_MAX_PKT_SIZE + 4 + sizeof(struct sockaddr_storage)];
>>      int remaining_in_dg;
>>      char *localaddr;
>>      int timeout;
>> @@ -115,6 +115,7 @@ typedef struct UDPContext {
>>      char *sources;
>>      char *block;
>>      IPSourceFilters filters;
>> +    struct sockaddr_storage last_recv_addr;
>> } UDPContext;
>>
>> #define OFFSET(x) offsetof(UDPContext, x)
>> @@ -467,6 +468,12 @@ int ff_udp_get_local_port(URLContext *h)
>>      return s->local_port;
>> }
>>
>> +void ff_udp_get_last_recv_addr(URLContext *h, struct sockaddr_storage *addr)
>> +{
>> +    UDPContext *s = h->priv_data;
>> +    *addr = s->last_recv_addr;
>> +}
>> +
>> /**
>>   * Return the udp file handle for select() usage to wait for several RTP
>>   * streams at the same time.
>> @@ -498,13 +505,14 @@ static void *circular_buffer_task_rx( void *_URLContext)
>>          int len;
>>          struct sockaddr_storage addr;
>>          socklen_t addr_len = sizeof(addr);
>> +        const int header_sz = 4 + addr_len;
>>
>>          pthread_mutex_unlock(&s->mutex);
>>          /* Blocking operations are always cancellation points;
>>             see "General Information" / "Thread Cancelation Overview"
>>             in Single Unix. */
>>          pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_cancelstate);
>> -        len = recvfrom(s->udp_fd, s->tmp+4, sizeof(s->tmp)-4, 0, (struct sockaddr *)&addr, &addr_len);
>> +        len = recvfrom(s->udp_fd, s->tmp + header_sz, sizeof(s->tmp) - header_sz, 0, (struct sockaddr *)&addr, &addr_len);
>>          pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelstate);
>>          pthread_mutex_lock(&s->mutex);
>>          if (len < 0) {
>> @@ -517,8 +525,9 @@ static void *circular_buffer_task_rx( void *_URLContext)
>>          if (ff_ip_check_source_lists(&addr, &s->filters))
>>              continue;
>>          AV_WL32(s->tmp, len);
>> +        memcpy(s->tmp + 4, &addr, sizeof(addr));
>>
>> -        if (av_fifo_can_write(s->fifo) < len + 4) {
>> +        if (av_fifo_can_write(s->fifo) < len + header_sz) {
>>              /* No Space left */
>>              if (s->overrun_nonfatal) {
>>                  av_log(h, AV_LOG_WARNING, "Circular buffer overrun. "
>> @@ -532,7 +541,7 @@ static void *circular_buffer_task_rx( void *_URLContext)
>>                  goto end;
>>              }
>>          }
>> -        av_fifo_write(s->fifo, s->tmp, len + 4);
>> +        av_fifo_write(s->fifo, s->tmp, len + header_sz);
>>          pthread_cond_signal(&s->cond);
>>      }
>>
>> @@ -991,8 +1000,7 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
>> {
>>      UDPContext *s = h->priv_data;
>>      int ret;
>> -    struct sockaddr_storage addr;
>> -    socklen_t addr_len = sizeof(addr);
>> +    socklen_t addr_len = sizeof(s->last_recv_addr);
>> #if HAVE_PTHREAD_CANCEL
>>      int avail, nonblock = h->flags & AVIO_FLAG_NONBLOCK;
>>
>> @@ -1004,6 +1012,7 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
>>                  uint8_t tmp[4];
>>
>>                  av_fifo_read(s->fifo, tmp, 4);
>> +                av_fifo_read(s->fifo, &s->last_recv_addr, sizeof(s->last_recv_addr));
>>                  avail = AV_RL32(tmp);
>>                  if(avail > size){
>>                      av_log(h, AV_LOG_WARNING, "Part of datagram lost due to insufficient buffer size\n");
>> @@ -1043,10 +1052,10 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
>>          if (ret < 0)
>>              return ret;
>>      }
>> -    ret = recvfrom(s->udp_fd, buf, size, 0, (struct sockaddr *)&addr, &addr_len);
>> +    ret = recvfrom(s->udp_fd, buf, size, 0, (struct sockaddr *)&s->last_recv_addr, &addr_len);
>>      if (ret < 0)
>>          return ff_neterrno();
>> -    if (ff_ip_check_source_lists(&addr, &s->filters))
>> +    if (ff_ip_check_source_lists(&s->last_recv_addr, &s->filters))
>>          return AVERROR(EINTR);
>>      return ret;
>> }
>> -- 
>> 2.49.0
>>
>> _______________________________________________
>> ffmpeg-devel mailing list
>> ffmpeg-devel@ffmpeg.org
>> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>>
>> To unsubscribe, visit link above, or email
>> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe”.
> I think it’s necessary to add "socklen_t last_recv_len".
> I’ve send the fixes to your GitHub repo https://github.com/BtbN/FFmpeg/pull/3 because my fixes depends your patchset.

Anything that needs the len can just get sizeof(sockaddr_storage), can't it?
Getting bigger than neccesary of a size won't break anything, as long as 
it isn't too small.
_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [FFmpeg-devel] [PATCH v2 1/8] avformat/tls: move whip specific init out of generic tls code
  2025-07-07 11:26     ` Timo Rothenpieler
@ 2025-07-07 12:33       ` Jack Lau
  0 siblings, 0 replies; 15+ messages in thread
From: Jack Lau @ 2025-07-07 12:33 UTC (permalink / raw)
  To: FFmpeg development discussions and patches



> On Jul 7, 2025, at 19:26, Timo Rothenpieler <timo@rothenpieler.org> wrote:
> 
> On 07/07/2025 08:30, Jack Lau wrote:
>>> On Jul 7, 2025, at 02:36, Timo Rothenpieler <timo@rothenpieler.org> wrote:
>>> 
>>> ---
>>> libavformat/tls.c         |  9 ---------
>>> libavformat/tls_openssl.c | 12 ++++++++----
>>> libavformat/whip.c        |  5 +++++
>>> 3 files changed, 13 insertions(+), 13 deletions(-)
>>> 
>>> diff --git a/libavformat/tls.c b/libavformat/tls.c
>>> index c0adaf61ce..bd9c05e6dc 100644
>>> --- a/libavformat/tls.c
>>> +++ b/libavformat/tls.c
>>> @@ -141,15 +141,6 @@ int ff_tls_open_underlying(TLSShared *c, URLContext *parent, const char *uri, AV
>>>     ret = ffurl_open_whitelist(c->is_dtls ? &c->udp : &c->tcp, buf, AVIO_FLAG_READ_WRITE,
>>>                                &parent->interrupt_callback, options,
>>>                                parent->protocol_whitelist, parent->protocol_blacklist, parent);
>>> -    if (c->is_dtls) {
>>> -        if (ret < 0) {
>>> -            av_log(c, AV_LOG_ERROR, "Failed to open udp://%s:%d\n", c->underlying_host, port);
>>> -            return ret;
>>> -        }
>>> -        /* Make the socket non-blocking, set to READ and WRITE mode after connected */
>>> -        ff_socket_nonblock(ffurl_get_file_handle(c->udp), 1);
>>> -        c->udp->flags |= AVIO_FLAG_READ | AVIO_FLAG_NONBLOCK;
>>> -    }
>>>     return ret;
>>> }
>>> 
>>> diff --git a/libavformat/tls_openssl.c b/libavformat/tls_openssl.c
>>> index 08527418b0..0c76f110e3 100644
>>> --- a/libavformat/tls_openssl.c
>>> +++ b/libavformat/tls_openssl.c
>>> @@ -1128,14 +1128,16 @@ static int tls_write(URLContext *h, const uint8_t *buf, int size)
>>> 
>>> static int tls_get_file_handle(URLContext *h)
>>> {
>>> -    TLSContext *c = h->priv_data;
>>> -    return ffurl_get_file_handle(c->tls_shared.tcp);
>>> +    TLSContext *p = h->priv_data;
>>> +    TLSShared *c = &p->tls_shared;
>>> +    return ffurl_get_file_handle(c->is_dtls ? c->udp : c->tcp);
>>> }
>>> 
>>> static int tls_get_short_seek(URLContext *h)
>>> {
>>> -    TLSContext *s = h->priv_data;
>>> -    return ffurl_get_short_seek(s->tls_shared.tcp);
>>> +    TLSContext *p = h->priv_data;
>>> +    TLSShared *c = &p->tls_shared;
>>> +    return ffurl_get_short_seek(c->is_dtls ? c->udp : c->tcp);
>>> }
>>> 
>>> static const AVOption options[] = {
>>> @@ -1177,6 +1179,8 @@ const URLProtocol ff_dtls_protocol = {
>>>     .url_close      = dtls_close,
>>>     .url_read       = tls_read,
>>>     .url_write      = tls_write,
>>> +    .url_get_file_handle = tls_get_file_handle,
>>> +    .url_get_short_seek  = tls_get_short_seek,
>>>     .priv_data_size = sizeof(TLSContext),
>>>     .flags          = URL_PROTOCOL_FLAG_NETWORK,
>>>     .priv_data_class = &dtls_class,
>>> diff --git a/libavformat/whip.c b/libavformat/whip.c
>>> index 84d4c5a1f3..4ac76e79f2 100644
>>> --- a/libavformat/whip.c
>>> +++ b/libavformat/whip.c
>>> @@ -388,6 +388,11 @@ static av_cold int dtls_initialize(AVFormatContext *s)
>>>     WHIPContext *whip = s->priv_data;
>>>     /* reuse the udp created by whip */
>>>     ff_dtls_set_udp(whip->dtls_uc, whip->udp);
>>> +
>>> +    /* Make the socket non-blocking */
>>> +    ff_socket_nonblock(ffurl_get_file_handle(whip->dtls_uc), 1);
>>> +    whip->dtls_uc->flags |= AVIO_FLAG_NONBLOCK;
>>> +
>> I think it’s redundant since udp_connect function has set these already.
> 
> No, it needs to be set on the tls URLContext, since all implementations forward their nonblocking flag (or lack thereof) to the underlying context each read/write.
Thanks for the explanation.
This patch Looks good to me.
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org <mailto:ffmpeg-devel@ffmpeg.org>
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
> 
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org <mailto:ffmpeg-devel-request@ffmpeg.org> with subject "unsubscribe".

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

* Re: [FFmpeg-devel] [PATCH v2 2/8] avformat/udp: make recv addr of each packet available
  2025-07-07 11:28     ` Timo Rothenpieler
@ 2025-07-07 12:36       ` Jack Lau
  0 siblings, 0 replies; 15+ messages in thread
From: Jack Lau @ 2025-07-07 12:36 UTC (permalink / raw)
  To: FFmpeg development discussions and patches



> On Jul 7, 2025, at 19:28, Timo Rothenpieler <timo@rothenpieler.org> wrote:
> 
> On 07/07/2025 10:03, Jack Lau wrote:
>>> On Jul 7, 2025, at 02:36, Timo Rothenpieler <timo@rothenpieler.org> wrote:
>>> 
>>> ---
>>> libavformat/network.h |  2 ++
>>> libavformat/udp.c     | 25 +++++++++++++++++--------
>>> 2 files changed, 19 insertions(+), 8 deletions(-)
>>> 
>>> diff --git a/libavformat/network.h b/libavformat/network.h
>>> index ca214087fc..48bb75a758 100644
>>> --- a/libavformat/network.h
>>> +++ b/libavformat/network.h
>>> @@ -338,4 +338,6 @@ int ff_connect_parallel(struct addrinfo *addrs, int timeout_ms_per_address,
>>>                         int parallel, URLContext *h, int *fd,
>>>                         int (*customize_fd)(void *, int, int), void *customize_ctx);
>>> 
>>> +void ff_udp_get_last_recv_addr(URLContext *h, struct sockaddr_storage *addr);
>>> +
>>> #endif /* AVFORMAT_NETWORK_H */
>>> diff --git a/libavformat/udp.c b/libavformat/udp.c
>>> index 30f075de8e..7d64ff07ed 100644
>>> --- a/libavformat/udp.c
>>> +++ b/libavformat/udp.c
>>> @@ -107,7 +107,7 @@ typedef struct UDPContext {
>>>     pthread_cond_t cond;
>>>     int thread_started;
>>> #endif
>>> -    uint8_t tmp[UDP_MAX_PKT_SIZE+4];
>>> +    uint8_t tmp[UDP_MAX_PKT_SIZE + 4 + sizeof(struct sockaddr_storage)];
>>>     int remaining_in_dg;
>>>     char *localaddr;
>>>     int timeout;
>>> @@ -115,6 +115,7 @@ typedef struct UDPContext {
>>>     char *sources;
>>>     char *block;
>>>     IPSourceFilters filters;
>>> +    struct sockaddr_storage last_recv_addr;
>>> } UDPContext;
>>> 
>>> #define OFFSET(x) offsetof(UDPContext, x)
>>> @@ -467,6 +468,12 @@ int ff_udp_get_local_port(URLContext *h)
>>>     return s->local_port;
>>> }
>>> 
>>> +void ff_udp_get_last_recv_addr(URLContext *h, struct sockaddr_storage *addr)
>>> +{
>>> +    UDPContext *s = h->priv_data;
>>> +    *addr = s->last_recv_addr;
>>> +}
>>> +
>>> /**
>>>  * Return the udp file handle for select() usage to wait for several RTP
>>>  * streams at the same time.
>>> @@ -498,13 +505,14 @@ static void *circular_buffer_task_rx( void *_URLContext)
>>>         int len;
>>>         struct sockaddr_storage addr;
>>>         socklen_t addr_len = sizeof(addr);
>>> +        const int header_sz = 4 + addr_len;
>>> 
>>>         pthread_mutex_unlock(&s->mutex);
>>>         /* Blocking operations are always cancellation points;
>>>            see "General Information" / "Thread Cancelation Overview"
>>>            in Single Unix. */
>>>         pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, &old_cancelstate);
>>> -        len = recvfrom(s->udp_fd, s->tmp+4, sizeof(s->tmp)-4, 0, (struct sockaddr *)&addr, &addr_len);
>>> +        len = recvfrom(s->udp_fd, s->tmp + header_sz, sizeof(s->tmp) - header_sz, 0, (struct sockaddr *)&addr, &addr_len);
>>>         pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &old_cancelstate);
>>>         pthread_mutex_lock(&s->mutex);
>>>         if (len < 0) {
>>> @@ -517,8 +525,9 @@ static void *circular_buffer_task_rx( void *_URLContext)
>>>         if (ff_ip_check_source_lists(&addr, &s->filters))
>>>             continue;
>>>         AV_WL32(s->tmp, len);
>>> +        memcpy(s->tmp + 4, &addr, sizeof(addr));
>>> 
>>> -        if (av_fifo_can_write(s->fifo) < len + 4) {
>>> +        if (av_fifo_can_write(s->fifo) < len + header_sz) {
>>>             /* No Space left */
>>>             if (s->overrun_nonfatal) {
>>>                 av_log(h, AV_LOG_WARNING, "Circular buffer overrun. "
>>> @@ -532,7 +541,7 @@ static void *circular_buffer_task_rx( void *_URLContext)
>>>                 goto end;
>>>             }
>>>         }
>>> -        av_fifo_write(s->fifo, s->tmp, len + 4);
>>> +        av_fifo_write(s->fifo, s->tmp, len + header_sz);
>>>         pthread_cond_signal(&s->cond);
>>>     }
>>> 
>>> @@ -991,8 +1000,7 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
>>> {
>>>     UDPContext *s = h->priv_data;
>>>     int ret;
>>> -    struct sockaddr_storage addr;
>>> -    socklen_t addr_len = sizeof(addr);
>>> +    socklen_t addr_len = sizeof(s->last_recv_addr);
>>> #if HAVE_PTHREAD_CANCEL
>>>     int avail, nonblock = h->flags & AVIO_FLAG_NONBLOCK;
>>> 
>>> @@ -1004,6 +1012,7 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
>>>                 uint8_t tmp[4];
>>> 
>>>                 av_fifo_read(s->fifo, tmp, 4);
>>> +                av_fifo_read(s->fifo, &s->last_recv_addr, sizeof(s->last_recv_addr));
>>>                 avail = AV_RL32(tmp);
>>>                 if(avail > size){
>>>                     av_log(h, AV_LOG_WARNING, "Part of datagram lost due to insufficient buffer size\n");
>>> @@ -1043,10 +1052,10 @@ static int udp_read(URLContext *h, uint8_t *buf, int size)
>>>         if (ret < 0)
>>>             return ret;
>>>     }
>>> -    ret = recvfrom(s->udp_fd, buf, size, 0, (struct sockaddr *)&addr, &addr_len);
>>> +    ret = recvfrom(s->udp_fd, buf, size, 0, (struct sockaddr *)&s->last_recv_addr, &addr_len);
>>>     if (ret < 0)
>>>         return ff_neterrno();
>>> -    if (ff_ip_check_source_lists(&addr, &s->filters))
>>> +    if (ff_ip_check_source_lists(&s->last_recv_addr, &s->filters))
>>>         return AVERROR(EINTR);
>>>     return ret;
>>> }
>>> -- 
>>> 2.49.0
>>> 
>>> _______________________________________________
>>> ffmpeg-devel mailing list
>>> ffmpeg-devel@ffmpeg.org
>>> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
>>> 
>>> To unsubscribe, visit link above, or email
>>> ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe”.
>> I think it’s necessary to add "socklen_t last_recv_len".
>> I’ve send the fixes to your GitHub repo https://github.com/BtbN/FFmpeg/pull/3 because my fixes depends your patchset.
> 
> Anything that needs the len can just get sizeof(sockaddr_storage), can't it?
> Getting bigger than neccesary of a size won't break anything, as long as it isn't too small.
Yes, it indeed won’t break anything, I just think use the real length is more clear.
But use the bigger length is also okay to me. 
I’ve update this patch:

diff --git a/libavformat/udp.c b/libavformat/udp.c
index 3657acb985..745fee4f96 100644
--- a/libavformat/udp.c
+++ b/libavformat/udp.c
@@ -1137,6 +1137,10 @@ static int udp_write(URLContext *h, const uint8_t *buf, int size)
     }
 
     if (!s->is_connected) {
+        if (!s->dest_addr_len && !s->dest_addr.ss_family) {
+            ff_udp_get_last_recv_addr(h, &s->dest_addr);
+            s->dest_addr_len = sizeof(s->dest_addr);
+        }
         ret = sendto (s->udp_fd, buf, size, 0,
                       (struct sockaddr *) &s->dest_addr,
                       s->dest_addr_len);

Do you have more comments about it?

Thanks
Jack
> _______________________________________________
> ffmpeg-devel mailing list
> ffmpeg-devel@ffmpeg.org <mailto:ffmpeg-devel@ffmpeg.org>
> https://ffmpeg.org/mailman/listinfo/ffmpeg-devel
> 
> To unsubscribe, visit link above, or email
> ffmpeg-devel-request@ffmpeg.org <mailto:ffmpeg-devel-request@ffmpeg.org> with subject "unsubscribe".

_______________________________________________
ffmpeg-devel mailing list
ffmpeg-devel@ffmpeg.org
https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

To unsubscribe, visit link above, or email
ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".

^ permalink raw reply	[flat|nested] 15+ messages in thread

end of thread, other threads:[~2025-07-07 12:37 UTC | newest]

Thread overview: 15+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-07-06 18:36 [FFmpeg-devel] [PATCH v2 0/8] WHIP + TLS + UDP fixes and SChannel DTLS support Timo Rothenpieler
2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 1/8] avformat/tls: move whip specific init out of generic tls code Timo Rothenpieler
2025-07-07  6:30   ` Jack Lau
2025-07-07 11:26     ` Timo Rothenpieler
2025-07-07 12:33       ` Jack Lau
2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 2/8] avformat/udp: make recv addr of each packet available Timo Rothenpieler
2025-07-07  8:03   ` Jack Lau
2025-07-07 11:28     ` Timo Rothenpieler
2025-07-07 12:36       ` Jack Lau
2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 3/8] avformat/udp: separate rx and tx fifo Timo Rothenpieler
2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 4/8] avformat/udp: add function to set remote address directly Timo Rothenpieler
2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 5/8] avformat/tls: make passing an external socket universal Timo Rothenpieler
2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 6/8] avformat/tls_schannel: add DTLS support Timo Rothenpieler
2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 7/8] avformat/tls_schannel: add option to load server certificate from store Timo Rothenpieler
2025-07-06 18:36 ` [FFmpeg-devel] [PATCH v2 8/8] avformat/tls_schannel: fix non-blocking write breaking TLS sessions Timo Rothenpieler

Git Inbox Mirror of the ffmpeg-devel mailing list - see https://ffmpeg.org/mailman/listinfo/ffmpeg-devel

This inbox may be cloned and mirrored by anyone:

	git clone --mirror https://master.gitmailbox.com/ffmpegdev/0 ffmpegdev/git/0.git

	# If you have public-inbox 1.1+ installed, you may
	# initialize and index your mirror using the following commands:
	public-inbox-init -V2 ffmpegdev ffmpegdev/ https://master.gitmailbox.com/ffmpegdev \
		ffmpegdev@gitmailbox.com
	public-inbox-index ffmpegdev

Example config snippet for mirrors.


AGPL code for this site: git clone https://public-inbox.org/public-inbox.git