Commit 6cbb3f950a for openssl.org

commit 6cbb3f950a5f66ba7bb3e573c36aa598821fded7
Author: Alexandr Nedvedicky <sashan@openssl.org>
Date:   Fri Aug 7 15:39:15 2026 +0200

    allow dgram pair to dump datagrams into libpcapp

    to enable the feature use ./Configure enable-pcap to prepare
    your OpenSSL build.

    use SSLPCAPDIR env veriable to tell library where to write pcap
    format files with dumped datagrams. the names in directory
    represent memory address of bio object in dgram pair.

    alternatively you can use SSLPCAPFILE env. variable which
    sets desired name of file where to write the pcap dump.
    The library then adds index (YourFileName-1.pcap). So every
    dgram pair gets its own unique file.

    Co-authored-by: Jakub Zelenka <bukka@php.net>
    Reviewed-by: Norbert Pocs <norbertp@openssl.org>
    Reviewed-by: Neil Horman <nhorman@openssl.org>
    Merge-date: Thu Oct 29 11:29:36 2026
    Merged-from: https://github.com/openssl/openssl/pull/32266

diff --git a/.github/workflows/run-checker-ci.yml b/.github/workflows/run-checker-ci.yml
index 89186ff03c..c46561a679 100644
--- a/.github/workflows/run-checker-ci.yml
+++ b/.github/workflows/run-checker-ci.yml
@@ -59,6 +59,7 @@ jobs:
           no-tls1_2,
           no-tls1_3,
           enable-trace enable-fips,
+          no-pcap,
           no-quic,
           -DOPENSSL_USE_IPV6=0
         ]
diff --git a/.github/workflows/run-checker-daily.yml b/.github/workflows/run-checker-daily.yml
index 171692d8fb..7516a4c9ba 100644
--- a/.github/workflows/run-checker-daily.yml
+++ b/.github/workflows/run-checker-daily.yml
@@ -119,6 +119,7 @@ jobs:
           no-multiblock,
           no-nextprotoneg,
           no-ocb,
+          no-pcap,
           no-pic,
           no-poly1305,
           no-posix-io,
diff --git a/Configure b/Configure
index dba37111de..6c3ea33623 100755
--- a/Configure
+++ b/Configure
@@ -561,6 +561,7 @@ my @disablables_features = (
     "pinshared",
     "posix-io",
     "unstable-qlog",
+    "pcap",
     "rdrand",
     "rfc3779",
     "secure-memory",
@@ -586,7 +587,6 @@ my @disablables_features = (
     "weak-ssl-ciphers",
     "zlib-dynamic",
     "zstd-dynamic",
-
 );

 my @disablables = sort (@disablables_protocols,@disablables_algorithms,@disablables_features);
@@ -651,6 +651,7 @@ our %disabled = ( # "what"         => "comment"
                   "md2"                 => "default",
                   "msan"                => "default",
                   "rc5"                 => "default",
+                  "pcap"                => "default",
                   "sctp"                => "default",
                   "sslkeylog"           => "default",
                   "static-vcruntime"    => "default",
diff --git a/crypto/bio/bss_dgram_pair.c b/crypto/bio/bss_dgram_pair.c
index 4e631342ff..0eeff94cd4 100644
--- a/crypto/bio/bss_dgram_pair.c
+++ b/crypto/bio/bss_dgram_pair.c
@@ -14,6 +14,16 @@
 #include "internal/safe_math.h"
 #include "internal/threads_common.h"

+#ifndef OPENSSL_NO_PCAP
+#include <openssl/rand.h>
+#include <stdio.h>
+
+#include "internal/time.h"
+#include "internal/bio_addr.h"
+#include "internal/thread.h"
+
+#endif
+
 #if !defined(OPENSSL_NO_DGRAM) && !defined(OPENSSL_NO_SOCK)

 #define is_dgram_pair(b) (b->pair != NULL)
@@ -331,6 +341,12 @@ struct bio_dgram_pair_st {
     unsigned int local_addr_enable : 1; /* Can use BIO_MSG->local? */
     unsigned int role : 1; /* Determines lock order */
     unsigned int grows_on_write : 1; /* Set for BIO_s_dgram_mem only */
+#ifndef OPENSSL_NO_PCAP
+    FILE *tcpdump_f;
+    CRYPTO_MUTEX *pcap_mutex;
+    int owner;
+    uint64_t packet_num;
+#endif
 };

 /*
@@ -424,6 +440,7 @@ static int dgram_pair_init(BIO *bio)
     }

     bio->ptr = b;
+
     return 1;
 }

@@ -465,6 +482,10 @@ static int dgram_pair_free(BIO *bio)
     ring_buf_destroy(&b->rbuf);
     BIO_ADDR_free(b->local_addr);
     CRYPTO_THREAD_lock_free(b->lock);
+#ifndef OPENSSL_NO_PCAP
+    if (b->tcpdump_f != NULL)
+        fclose(b->tcpdump_f);
+#endif
     OPENSSL_free(b);
     return 1;
 }
@@ -576,9 +597,70 @@ static int dgram_pair_ctrl_make_bio_pair(BIO *bio1, BIO *bio2)
     b2->role = 1;
     bio1->init = 1;
     bio2->init = 1;
+
     return 1;
 }

+/* ARGSUSED */
+static int dgram_pair_ctrl_set_pcap_file(BIO *bio, void *ptr)
+{
+    int ret = 0;
+
+#ifndef OPENSSL_NO_PCAP
+#define LINKTYPE_IPV4 0x00e4 /* IANA type */
+    struct bio_dgram_pair_st *b1, *b2;
+    char *filename = (char *)ptr;
+    static struct {
+        uint32_t pcap_magic;
+        uint16_t pcap_maj;
+        uint16_t pcap_min;
+        uint32_t pcap_r1;
+        uint32_t pcap_r2;
+        uint32_t pcap_snap;
+        uint32_t pcap_ltype;
+    } pcap_f_header = {
+        .pcap_magic = 0xA1B2C3D4,
+        .pcap_maj = 2, /* https://www.ietf.org/archive/id/draft-gharris-opsawg-pcap-01.html */
+        .pcap_min = 4, /* ---"--- */
+        .pcap_r1 = 0,
+        .pcap_r2 = 0,
+        .pcap_snap = 65536,
+        .pcap_ltype = LINKTYPE_IPV4,
+    };
+
+    b1 = (struct bio_dgram_pair_st *)bio->ptr;
+    if (b1 == NULL)
+        return 0;
+
+    if (b1->peer == NULL)
+        return 0;
+
+    b2 = (struct bio_dgram_pair_st *)b1->peer->ptr;
+    if (b2 == NULL)
+        return 0;
+
+    if (filename != NULL) {
+        if (b1->tcpdump_f == NULL) {
+            b1->pcap_mutex = ossl_crypto_mutex_new();
+            if (b1->pcap_mutex != NULL) {
+                b1->tcpdump_f = fopen(filename, "wb");
+                if (b1->tcpdump_f != NULL) {
+                    b1->owner = 1;
+                    b2->tcpdump_f = b1->tcpdump_f;
+                    b2->pcap_mutex = b1->pcap_mutex;
+                    ret = fwrite(&pcap_f_header, sizeof(pcap_f_header), 1,
+                        b1->tcpdump_f);
+                } else {
+                    ossl_crypto_mutex_free(&b1->pcap_mutex);
+                }
+            }
+        }
+    }
+#endif
+
+    return ret;
+}
+
 /* BIO_destroy_bio_pair (BIO_C_DESTROY_BIO_PAIR) */
 static int dgram_pair_ctrl_destroy_bio_pair(BIO *bio1)
 {
@@ -610,6 +692,20 @@ static int dgram_pair_ctrl_destroy_bio_pair(BIO *bio1)
          * The last half of the pair is leaving, clean up the
          * shared data
          */
+#ifndef OPENSSL_NO_PCAP
+        if (b1->tcpdump_f != NULL || b2->tcpdump_f != NULL) {
+            if (b1->owner && b1->tcpdump_f) {
+                fclose(b1->tcpdump_f);
+                ossl_crypto_mutex_free(&b1->pcap_mutex);
+            }
+            if (b2->owner && b2->tcpdump_f) {
+                fclose(b2->tcpdump_f);
+                ossl_crypto_mutex_free(&b2->pcap_mutex);
+            }
+            b1->tcpdump_f = NULL;
+            b2->tcpdump_f = NULL;
+        }
+#endif
         CRYPTO_FREE_REF(&b1->pair->ref_cnt);
         CRYPTO_THREAD_lock_free(b1->pair->map[0].lock);
         CRYPTO_THREAD_lock_free(b1->pair->map[1].lock);
@@ -1037,6 +1133,10 @@ static long dgram_pair_ctrl(BIO *bio, int cmd, long num, void *ptr)
         ret = (long)dgram_pair_ctrl_get_effective_caps(bio);
         break;

+    case BIO_CTRL_DGRAM_PAIR_SET_PCAP_FILE:
+        ret = dgram_pair_ctrl_set_pcap_file(bio, ptr);
+        break;
+
     default:
         ret = dgram_mem_ctrl(bio, cmd, num, ptr);
         break;
@@ -1446,6 +1546,81 @@ static ossl_inline size_t compute_rbuf_growth(size_t target, size_t current)
     return current;
 }

+#ifndef OPENSSL_NO_PCAP
+static void dgram_pcap(struct bio_dgram_pair_st *b, const char *buf, size_t sz)
+{
+    struct {
+        uint32_t pcap_secs;
+        uint32_t pcap_usecs;
+        uint32_t pcap_plen;
+        uint32_t pcap_olen;
+    } pcap_hdr;
+    struct {
+        uint8_t ip_hv;
+        uint8_t ip_tos;
+        uint16_t ip_len;
+        uint16_t ip_id;
+        uint8_t ip_frag;
+        uint8_t ip_foff;
+        uint8_t ip_ttl;
+        uint8_t ip_proto;
+        uint16_t ip_csum;
+        uint32_t ip_src;
+        uint32_t ip_dst;
+    } ip_hdr;
+    struct {
+        uint16_t uh_sport;
+        uint16_t uh_dport;
+        uint16_t uh_len;
+        uint16_t uh_csum;
+    } udp_hdr;
+    uint16_t len;
+    OSSL_TIME now;
+    struct bio_dgram_pair_st *peer;
+
+    if (b->tcpdump_f == NULL || sz == 0)
+        return;
+
+    if ((sz + sizeof(ip_hdr) + sizeof(udp_hdr)) > 65535)
+        return;
+    len = (uint16_t)(sz + sizeof(ip_hdr) + sizeof(udp_hdr));
+
+    now = ossl_time_now();
+
+    pcap_hdr.pcap_secs = ossl_time2seconds(now);
+    pcap_hdr.pcap_usecs = (uint32_t)(ossl_time2us(now) % 1000000);
+    pcap_hdr.pcap_plen = (uint32_t)(sz + sizeof(ip_hdr) + sizeof(udp_hdr));
+    pcap_hdr.pcap_olen = pcap_hdr.pcap_plen;
+
+    ip_hdr.ip_hv = 0x45;
+    ip_hdr.ip_tos = 0;
+    ip_hdr.ip_len = htons(len);
+    RAND_bytes((unsigned char *)&ip_hdr.ip_id, 2);
+    ip_hdr.ip_frag = 0x40; /* don't fragment */
+    ip_hdr.ip_foff = 0;
+    ip_hdr.ip_ttl = 64;
+    ip_hdr.ip_proto = 17;
+    ip_hdr.ip_csum = 0; /* wireshark will complain with ?chksum offload? */
+    ip_hdr.ip_src = (b->local_addr == NULL) ? htonl(0x7f000001) : b->local_addr->s_in.sin_addr.s_addr;
+    peer = (struct bio_dgram_pair_st *)b->peer->ptr;
+    ip_hdr.ip_dst = (peer == NULL || peer->local_addr == NULL) ? htonl(0x7f000001) : peer->local_addr->s_in.sin_addr.s_addr;
+
+    /*
+     * use some fake port numbers
+     */
+    udp_hdr.uh_sport = (b->local_addr == NULL) ? htons(8080) : b->local_addr->s_in.sin_port;
+    udp_hdr.uh_dport = (peer == NULL || peer->local_addr == NULL) ? htons(4040) : peer->local_addr->s_in.sin_port;
+    udp_hdr.uh_csum = 0;
+    udp_hdr.uh_len = htons((uint16_t)(sz + sizeof(udp_hdr)));
+    ossl_crypto_mutex_lock(b->pcap_mutex);
+    fwrite(&pcap_hdr, sizeof(pcap_hdr), 1, b->tcpdump_f);
+    fwrite(&ip_hdr, sizeof(ip_hdr), 1, b->tcpdump_f);
+    fwrite(&udp_hdr, sizeof(udp_hdr), 1, b->tcpdump_f);
+    fwrite(buf, sz, 1, b->tcpdump_f);
+    ossl_crypto_mutex_unlock(b->pcap_mutex);
+}
+#endif
+
 /* Must hold local write lock */
 static size_t dgram_pair_write_inner(struct bio_dgram_pair_st *b,
     const uint8_t *buf, size_t sz)
@@ -1561,6 +1736,10 @@ static ossl_ssize_t dgram_pair_write_actual(BIO *bio, const char *buf, size_t sz
         return -BIO_R_NON_FATAL;
     }

+#ifndef OPENSSL_NO_PCAP
+    dgram_pcap(b, buf, sz);
+#endif
+
     return sz;
 }

diff --git a/doc/man3/BIO_s_dgram_pair.pod b/doc/man3/BIO_s_dgram_pair.pod
index 0df30f0099..1d2893c86a 100644
--- a/doc/man3/BIO_s_dgram_pair.pod
+++ b/doc/man3/BIO_s_dgram_pair.pod
@@ -5,7 +5,7 @@
 BIO_s_dgram_pair, BIO_new_bio_dgram_pair, BIO_dgram_set_no_trunc,
 BIO_dgram_get_no_trunc, BIO_dgram_get_effective_caps, BIO_dgram_get_caps,
 BIO_dgram_set_caps, BIO_dgram_set_mtu, BIO_dgram_get_mtu,
-BIO_dgram_set0_local_addr - datagram pair BIO
+BIO_dgram_set0_local_addr, BIO_dgram_pair_set0_pcap_file  - datagram pair BIO

 =head1 SYNOPSIS

@@ -23,6 +23,7 @@ BIO_dgram_set0_local_addr - datagram pair BIO
  int BIO_dgram_set_mtu(BIO *bio, unsigned int mtu);
  unsigned int BIO_dgram_get_mtu(BIO *bio);
  int BIO_dgram_set0_local_addr(BIO *bio, BIO_ADDR *addr);
+ int BIO_dgram_pair_set0_pcap_file(BIO *bio, char *filename);

 =head1 DESCRIPTION

@@ -115,6 +116,11 @@ explicitly specified local address takes precedence. The reference to the
 BIO_ADDR is passed to the BIO by this call and will be freed automatically when
 the BIO is freed.

+BIO_dgram_pair_set0_pcap_file() creates a packet capture file with desired filename
+and writes all packets transmitted over BIO dgram pair. The file is truncated
+if it exists already. To enable the feature you must configure your OpenSSL
+library build with B<enable-pcap> option.
+
 L<BIO_flush(3)> is a no-op.

 =head1 NOTES
@@ -221,6 +227,8 @@ operation is not supported.

 BIO_dgram_set0_local_addr() returns 1 on success and <= 0 otherwise.

+BIO_dgram_pair_set0_pcap_file() returns 1 on success and 0 otherwise.
+
 =head1 SEE ALSO

 L<BIO_s_bio(3)>, L<bio(7)>
diff --git a/include/openssl/bio.h.in b/include/openssl/bio.h.in
index a2a5972980..0b44b7a815 100644
--- a/include/openssl/bio.h.in
+++ b/include/openssl/bio.h.in
@@ -198,6 +198,8 @@ extern "C" {
 #define BIO_CTRL_DGRAM_DETECT_PEER_ADDR 93
 #define BIO_CTRL_DGRAM_SET0_LOCAL_ADDR 94

+#define BIO_CTRL_DGRAM_PAIR_SET_PCAP_FILE 95
+
 #define BIO_DGRAM_CAP_NONE 0U
 #define BIO_DGRAM_CAP_HANDLES_SRC_ADDR (1U << 0)
 #define BIO_DGRAM_CAP_HANDLES_DST_ADDR (1U << 1)
@@ -685,6 +687,8 @@ int BIO_ctrl_reset_read_request(BIO *b);
     (int)BIO_ctrl((b), BIO_CTRL_DGRAM_SET_MTU, (mtu), NULL)
 #define BIO_dgram_set0_local_addr(b, addr) \
     (int)BIO_ctrl((b), BIO_CTRL_DGRAM_SET0_LOCAL_ADDR, 0, (addr))
+#define BIO_dgram_pair_set0_pcap_file(b, addr) \
+    (int)BIO_ctrl((b), BIO_CTRL_DGRAM_PAIR_SET_PCAP_FILE, 0, (addr))

 /* ctrl macros for BIO_f_prefix */
 #define BIO_set_prefix(b, p) BIO_ctrl((b), BIO_CTRL_SET_PREFIX, 0, (void *)(p))