From 448ea905650983b4c9b88d7d4abddd3eee813985 Mon Sep 17 00:00:00 2001
From: Steffen Heil | secforge <steffen.heil@secforge.de>
Date: Wed, 13 May 2026 16:33:35 +0200
Subject: [PATCH 1/3] protocol: add LDAP server-side STARTTLS support

stunnel's ldap protocol helper only handled the client side (sending a
StartTLS ExtendedRequest to the backend). This adds server-side support
so stunnel can accept plain LDAP connections from legacy clients that
send a StartTLS request, perform the TLS upgrade with the client, and
forward to a backend that also speaks STARTTLS.

New functions:
- ldap_server_init: sets connect_before_ssl=1 so the backend TCP
  connection is established before the TLS handshake with the client
- ldap_server_middle: reads and validates the incoming STARTTLS
  ExtendedRequest (APPLICATION 23 / OID 1.3.6.1.4.1.1466.20037),
  then sends a success ExtendedResponse with the matching messageID

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
---
 src/protocol.c | 99 +++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 98 insertions(+), 1 deletion(-)

diff --git a/src/protocol.c b/src/protocol.c
index cddc381..c557fd1 100644
--- a/src/protocol.c
+++ b/src/protocol.c
@@ -78,6 +78,8 @@ NOEXPORT void imap_server_middle(CLI *);
 NOEXPORT void nntp_client_middle(CLI *);
 
 NOEXPORT void ldap_client_middle(CLI *);
+NOEXPORT const char *ldap_server_init(SERVICE_OPTIONS *);
+NOEXPORT void ldap_server_middle(CLI *);
 
 NOEXPORT void connect_server_early(CLI *);
 NOEXPORT const char *connect_client_init(SERVICE_OPTIONS *);
@@ -143,7 +145,8 @@ const char *protocol_init(SERVICE_OPTIONS *opt) {
         {.name="nntp",
             .client={.middle=nntp_client_middle}},
         {.name="ldap",
-            .client={.middle=ldap_client_middle}},
+            .client={.middle=ldap_client_middle},
+            .server={.init=ldap_server_init, .middle=ldap_server_middle}},
         {.name="connect",
             .client={.init=connect_client_init, .middle=connect_client_middle},
             .server={.early=connect_server_early}},
@@ -1334,6 +1337,100 @@ NOEXPORT void ldap_client_middle(CLI *c) {
     s_log(LOG_INFO, "LDAP Start TLS successfully negotiated");
 }
 
+#define LDAP_REQUEST_OP_APPLICATION_23          0x77
+#define LDAP_RESPONSE_MATCHED_DN_TAG            0x04
+#define LDAP_RESPONSE_NAME_CONTEXT_10           0x8a
+#define LDAP_STARTTLS_OID_LEN                   0x16
+#define LDAP_STARTTLS_RESPONSE_MSGID_OFFSET     4
+
+/* STARTTLS ExtendedResponse (success), messageID patched at offset 4 */
+static uint8_t ldap_starttls_response[38]={
+    0x30,                           /* UNIVERSAL SEQUENCE */
+    0x24,                           /* len = 36 */
+    0x02,                           /* INTEGER (messageID) */
+    0x01,                           /* len = 1 */
+    0x01,                           /* val = 1 (patched at runtime) */
+    0x78,                           /* APPLICATION 24 (ExtendedResponse) */
+    0x1f,                           /* len = 31 */
+    0x0a,                           /* ENUMERATED (resultCode) */
+    0x01,                           /* len = 1 */
+    0x00,                           /* val = 0 (success) */
+    0x04,                           /* OCTET STRING (matchedDN) */
+    0x00,                           /* len = 0 */
+    0x04,                           /* OCTET STRING (diagnosticMessage) */
+    0x00,                           /* len = 0 */
+    0x8a,                           /* CONTEXT [10] (responseName) */
+    0x16,                           /* len = 22 */
+    '1', '.', '3', '.', '6', '.', '1', '.', '4', '.', '1', '.',
+    '1', '4', '6', '6', '.', '2', '0', '0', '3', '7'
+};
+
+NOEXPORT const char *ldap_server_init(SERVICE_OPTIONS *opt) {
+    opt->option.connect_before_ssl=1; /* c->remote_fd needed */
+    return NULL;
+}
+
+NOEXPORT void ldap_server_middle(CLI *c) {
+    uint8_t buffer_8;
+    uint32_t buffer_32;
+    size_t req_len;
+    uint8_t ldap_request[128];
+    size_t req_idx;
+    uint8_t msg_id;
+    uint8_t response[sizeof(ldap_starttls_response)];
+
+    s_log(LOG_DEBUG, "Receiving LDAP Start TLS request tag");
+    s_read(c, c->local_rfd.fd, &buffer_8, 1);
+    if(buffer_8!=LDAP_UNIVERSAL_SEQUENCE) {
+        s_log(LOG_ERR, "LDAP request is not UNIVERSAL SEQUENCE");
+        throw_exception(c, 1);
+    }
+
+    s_log(LOG_DEBUG, "Receiving LDAP Start TLS request length");
+    s_read(c, c->local_rfd.fd, &buffer_8, 1);
+    if(buffer_8==LDAP_WINLDAP_FOUR_BYTE_LEN_FLAG) { /* WinLDAP */
+        s_read(c, c->local_rfd.fd, &buffer_32, 4);
+        req_len=ntohl(buffer_32);
+    } else {
+        req_len=buffer_8;
+    }
+    if(req_len>sizeof(ldap_request)) {
+        s_log(LOG_ERR, "LDAP request too long (%lu byte(s))",
+            (unsigned long)req_len);
+        throw_exception(c, 1);
+    }
+
+    s_log(LOG_DEBUG, "Receiving LDAP Start TLS request value (%lu byte(s))",
+        (unsigned long)req_len);
+    memset(ldap_request, 0, sizeof(ldap_request));
+    s_read(c, c->local_rfd.fd, ldap_request, req_len);
+
+    s_log(LOG_DEBUG, "Decoding LDAP Start TLS request");
+    req_idx=0;
+    if(ldap_request[req_idx++]!=LDAP_RESPONSE_MSG_ID_TAG_INTEGER) {
+        s_log(LOG_ERR, "LDAP request has an incorrect message ID type");
+        throw_exception(c, 1);
+    }
+    if(ldap_request[req_idx]<1 || ldap_request[req_idx]>4) {
+        s_log(LOG_ERR, "LDAP request has an unsupported message ID length (%u)",
+            ldap_request[req_idx]);
+        throw_exception(c, 1);
+    }
+    req_idx+=(size_t)ldap_request[req_idx]+1; /* skip length + value */
+    msg_id=ldap_request[req_idx-1]; /* last byte of messageID */
+    if(ldap_request[req_idx++]!=LDAP_REQUEST_OP_APPLICATION_23) {
+        s_log(LOG_ERR, "LDAP request protocol op is not ExtendedRequest");
+        throw_exception(c, 1);
+    }
+
+    s_log(LOG_DEBUG, "Sending LDAP Start TLS response (messageID=%u)", msg_id);
+    memcpy(response, ldap_starttls_response, sizeof(response));
+    response[LDAP_STARTTLS_RESPONSE_MSGID_OFFSET]=msg_id;
+    s_write(c, c->local_wfd.fd, response, sizeof(response));
+
+    s_log(LOG_INFO, "LDAP Start TLS accepted from client");
+}
+
 /**************************************** connect */
 
 NOEXPORT void connect_server_early(CLI *c) {
-- 
2.43.0

