From 889b8ae256752105e71bbf0c75cd0a673d0a17c3 Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 10:19:03 +0300 Subject: [PATCH 01/19] log non success result of parse packet --- src/Core/LdapPacket.cs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/Core/LdapPacket.cs b/src/Core/LdapPacket.cs index 539196a..c7455a8 100644 --- a/src/Core/LdapPacket.cs +++ b/src/Core/LdapPacket.cs @@ -23,6 +23,7 @@ //OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE //SOFTWARE. +using Serilog; using System; using System.IO; using System.Linq; @@ -97,9 +98,11 @@ public static async Task ParsePacket(Stream stream) var packet = new LdapPacket(Tag.Parse(tagByte[0])); packet.ChildAttributes.AddRange(await ParseAttributes(contentBytes, 0, contentLength.Length)); - if (packet.ChildAttributes.Any(attr => attr.LdapOperation == Core.LdapOperation.SearchResultDone)) + var searchDone = packet.ChildAttributes.FirstOrDefault(attr => attr.LdapOperation == Core.LdapOperation.SearchResultDone); + if (searchDone != null) { - return null; //thats all, stop reading + LogNonSuccessResult(searchDone); + return null; } return packet; @@ -113,5 +116,20 @@ public static async Task ParsePacket(Stream stream) return null; } + + private static void LogNonSuccessResult(LdapAttribute searchDone) + { + var resultAttr = searchDone.ChildAttributes.FirstOrDefault(attr => attr.DataType == UniversalDataType.Enumerated); + if (resultAttr == null || resultAttr.Value.Length == 0) + { + return; + } + + var result = (LdapResult)resultAttr.GetValue(); + if (result != LdapResult.success) + { + Log.Logger.Warning("LDAP search operation completed with {result} result", result); + } + } } } From 5b5b480e579b31afd213baa08ac74312f2cb074d Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 10:27:31 +0300 Subject: [PATCH 02/19] add main logging --- src/Server/LdapProxy.cs | 45 ++++++++++++++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/Server/LdapProxy.cs b/src/Server/LdapProxy.cs index 71c1ee0..27d6afd 100644 --- a/src/Server/LdapProxy.cs +++ b/src/Server/LdapProxy.cs @@ -9,6 +9,7 @@ using Serilog; using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Sockets; @@ -39,6 +40,8 @@ public class LdapProxy private LdapProxyAuthenticationStatus _status; + private volatile bool _closing; + private static readonly ConcurrentDictionary _usersDn2Cn = new(); private static readonly ConcurrentDictionary _usersCn2Dn = new(); @@ -69,15 +72,22 @@ public async Task ProcessDataExchange() _logger.Information("Opened {client} => {server} client {clientName:l}", from, to, _clientConfig.Name); + var stopwatch = Stopwatch.StartNew(); + var requestStats = new ExchangeStats(); + var responseStats = new ExchangeStats(); + await Task.WhenAny( - DataExchange(_clientConnection, _clientStream, _serverConnection, _serverStream, ParseAndProcessRequest), - DataExchange(_serverConnection, _serverStream, _clientConnection, _clientStream, ParseAndProcessResponse)); - - _logger.Debug("Closed {client} => {server} client {clientName:l}", from, to, _clientConfig.Name); + DataExchange(_clientConnection, _clientStream, _serverConnection, _serverStream, ParseAndProcessRequest, requestStats, from, to), + DataExchange(_serverConnection, _serverStream, _clientConnection, _clientStream, ParseAndProcessResponse, responseStats, to, from)); + + _closing = true; + + _logger.Information("Closed {client} => {server} client {clientName:l} after {duration} ms: requests {requestPackets} packet(s) / {requestBytes} byte(s), responses {responsePackets} packet(s) / {responseBytes} byte(s)", + from, to, _clientConfig.Name, stopwatch.ElapsedMilliseconds, requestStats.Packets, requestStats.Bytes, responseStats.Packets, responseStats.Bytes); } - - private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient target, Stream targetStream, Func> process) + + private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient target, Stream targetStream, Func> process, ExchangeStats stats, string from, string to) { try { @@ -89,11 +99,17 @@ private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient ldapPacket = await streamReader.ReadLdapPacket(); if (ldapPacket.Data.Length == 0) { + _logger.Debug("Connection {from} => {to} finished, end of stream", from, to); break; } + + stats.Packets++; + stats.Bytes += ldapPacket.Data.Length; + if (!ldapPacket.PacketValid) { // bypass data + _logger.Warning("Bypassed {length} byte(s) of unparsed data from {from} to {to}", ldapPacket.Data.Length, from, to); await targetStream.WriteAsync(ldapPacket.Data, 0, ldapPacket.Data.Length); continue; } @@ -110,14 +126,17 @@ private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient } } while (ldapPacket.Data.Length > 0); } - catch (IOException) + catch (IOException) when (_closing) + { + //other side has finished and streams are being disposed, aborted read is expected here + } + catch (IOException ex) { - //connection closed unexpectly - //_logger.Debug(ioex, "proxy"); + _logger.Warning("Connection {from} => {to} closed unexpectedly: {message:l}", from, to, ex.Message); } catch (Exception ex) { - _logger.Error(ex, "Data exchange error from {client} to {server}", source.Client.RemoteEndPoint, target.Client.RemoteEndPoint); + _logger.Error(ex, "Data exchange error from {from} to {to}", from, to); } } @@ -459,6 +478,12 @@ private bool IsMemberOf(LdapProfile profile, string group) } } + internal class ExchangeStats + { + public long Packets; + public long Bytes; + } + public enum LdapProxyAuthenticationStatus { None, From db1065d9df451dff94a6a0a71a856918610d38ce Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 10:34:07 +0300 Subject: [PATCH 03/19] add try catch for read from bytes --- src/Server/LdapProxy.cs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Server/LdapProxy.cs b/src/Server/LdapProxy.cs index 27d6afd..0561093 100644 --- a/src/Server/LdapProxy.cs +++ b/src/Server/LdapProxy.cs @@ -142,7 +142,16 @@ private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient private async Task<(byte[], int)> ParseAndProcessRequest(byte[] data, int length) { - var request = await LdapRequest.FromBytesAsync(data); + LdapRequest request; + try + { + request = await LdapRequest.FromBytesAsync(data); + } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to parse {length} byte(s) request from {client}, bypassing as-is", length, _clientConnection.Client.RemoteEndPoint); + return await Task.FromResult((data, length)); + } if (request.RequestType == LdapRequestType.SearchRequest) { From 182127e150c5cb756d03b89f22e235b7be41050f Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 10:52:15 +0300 Subject: [PATCH 04/19] try catch for UserDnSearch --- src/Server/LdapProxy.cs | 31 ++++++++++++++++++++----------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/src/Server/LdapProxy.cs b/src/Server/LdapProxy.cs index 0561093..0642891 100644 --- a/src/Server/LdapProxy.cs +++ b/src/Server/LdapProxy.cs @@ -365,24 +365,33 @@ private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient if (_status == LdapProxyAuthenticationStatus.UserDnSearch) { - var packet = await LdapPacket.ParsePacket(data); - var searchResultEntry = packet.ChildAttributes.SingleOrDefault(c => c.LdapOperation == LdapOperation.SearchResultEntry); - - if (searchResultEntry != null) + try { - var userDn = searchResultEntry.ChildAttributes[0].GetValue(); + var packet = await LdapPacket.ParsePacket(data); + var searchResultEntry = packet.ChildAttributes.SingleOrDefault(c => c.LdapOperation == LdapOperation.SearchResultEntry); - if (_lookupUserName != null && userDn != null) + if (searchResultEntry != null) { - userDn = userDn.ToLower(); //becouse some apps do it + var userDn = searchResultEntry.ChildAttributes[0].GetValue(); + + if (_lookupUserName != null && userDn != null) + { + userDn = userDn.ToLower(); //becouse some apps do it - _usersDn2Cn.TryRemove(userDn, out _); - _usersDn2Cn.TryAdd(userDn, _lookupUserName); + _usersDn2Cn.TryRemove(userDn, out _); + _usersDn2Cn.TryAdd(userDn, _lookupUserName); - _usersCn2Dn.TryRemove(_lookupUserName, out _); - _usersCn2Dn.TryAdd(_lookupUserName, userDn); + _usersCn2Dn.TryRemove(_lookupUserName, out _); + _usersCn2Dn.TryAdd(_lookupUserName, userDn); + + _logger.Debug("Resolved user '{user:l}' DN: {dn:l}", _lookupUserName, userDn); + } } } + catch (Exception ex) + { + _logger.Warning(ex, "Failed to parse DN search response of {length} byte(s), bypassing as-is", length); + } _status = LdapProxyAuthenticationStatus.None; } From 741e30f5badcb6269cbbdbe2029c35e02b25459a Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 10:53:28 +0300 Subject: [PATCH 05/19] remove sw --- src/Server/LdapProxy.cs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Server/LdapProxy.cs b/src/Server/LdapProxy.cs index 0642891..b9a5193 100644 --- a/src/Server/LdapProxy.cs +++ b/src/Server/LdapProxy.cs @@ -72,7 +72,6 @@ public async Task ProcessDataExchange() _logger.Information("Opened {client} => {server} client {clientName:l}", from, to, _clientConfig.Name); - var stopwatch = Stopwatch.StartNew(); var requestStats = new ExchangeStats(); var responseStats = new ExchangeStats(); @@ -82,8 +81,8 @@ await Task.WhenAny( _closing = true; - _logger.Information("Closed {client} => {server} client {clientName:l} after {duration} ms: requests {requestPackets} packet(s) / {requestBytes} byte(s), responses {responsePackets} packet(s) / {responseBytes} byte(s)", - from, to, _clientConfig.Name, stopwatch.ElapsedMilliseconds, requestStats.Packets, requestStats.Bytes, responseStats.Packets, responseStats.Bytes); + _logger.Information("Closed {client} => {server} client {clientName:l} : requests {requestPackets} packet(s) / {requestBytes} byte(s), responses {responsePackets} packet(s) / {responseBytes} byte(s)", + from, to, _clientConfig.Name, requestStats.Packets, requestStats.Bytes, responseStats.Packets, responseStats.Bytes); } From bc118ee11b5345c4cef1f7afabee89eb12a3f51c Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 10:54:22 +0300 Subject: [PATCH 06/19] add ldap service logger --- src/Server/LdapProxy.cs | 3 ++- src/Services/LdapService.cs | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Server/LdapProxy.cs b/src/Server/LdapProxy.cs index b9a5193..be6f9df 100644 --- a/src/Server/LdapProxy.cs +++ b/src/Server/LdapProxy.cs @@ -61,7 +61,7 @@ public LdapProxy(TcpClient clientConnection, Stream clientStream, TcpClient serv _clientConfig = clientConfig ?? throw new ArgumentNullException(nameof(clientConfig)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _ldapService = new LdapService(clientConfig); + _ldapService = new LdapService(clientConfig, logger); _nameResolverService = nameResolverService; } @@ -223,6 +223,7 @@ private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient if (bound) //first factor authenticated { + _logger.Debug("User '{user:l}' first factor verified at {server}", _userName, _serverConnection.Client.RemoteEndPoint); var bypass = false; //apply login transformation users if any diff --git a/src/Services/LdapService.cs b/src/Services/LdapService.cs index b4154ee..e848af7 100644 --- a/src/Services/LdapService.cs +++ b/src/Services/LdapService.cs @@ -5,6 +5,7 @@ using MultiFactor.Ldap.Adapter.Configuration; using MultiFactor.Ldap.Adapter.Core; using MultiFactor.Ldap.Adapter.Core.NameResolving; +using Serilog; using System; using System.Collections.Generic; using System.IO; @@ -19,10 +20,12 @@ public class LdapService //must not repeat proxied messages ids private int _messageId = Int32.MaxValue - 9999; private readonly ClientConfiguration _config; + private readonly ILogger _logger; - public LdapService(ClientConfiguration config) + public LdapService(ClientConfiguration config, ILogger logger = null) { _config = config ?? throw new ArgumentNullException(nameof(config)); + _logger = logger ?? Log.Logger; } #region requests builders From 468b8212816429b8a10747366d23293e59555f11 Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 10:54:44 +0300 Subject: [PATCH 07/19] correct comm --- src/Services/LdapService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Services/LdapService.cs b/src/Services/LdapService.cs index e848af7..3798841 100644 --- a/src/Services/LdapService.cs +++ b/src/Services/LdapService.cs @@ -64,7 +64,7 @@ private LdapPacket BuildLoadProfileRequest(string userName, string baseDn) searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.OctetString, baseDn)); //base dn searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Enumerated, (byte)2)); //scope: subtree searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Enumerated, (byte)3)); //aliases: never - searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Integer, byte.MaxValue - 1)); //size limit: 127 + searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Integer, byte.MaxValue - 1)); //size limit: 254 searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Integer, (byte)60)); //time limit: 60 searchRequest.ChildAttributes.Add(new LdapAttribute(UniversalDataType.Boolean, false)); //typesOnly: false From c058d23c2b9ff2733d3dc21ce850777fcae34b12 Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 10:57:00 +0300 Subject: [PATCH 08/19] add debug logs --- src/Services/LdapService.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Services/LdapService.cs b/src/Services/LdapService.cs index 3798841..2904c7e 100644 --- a/src/Services/LdapService.cs +++ b/src/Services/LdapService.cs @@ -325,6 +325,8 @@ public async Task GetBaseDn(Stream ldapConnectedStream, string userName) } public async Task LoadProfile(Stream ldapConnectedStream, string userName, string baseDn) { + _logger.Debug("Loading profile of user '{user:l}' in {baseDn:l}", userName, baseDn); + var request = BuildLoadProfileRequest(userName, baseDn); var requestData = request.GetBytes(); @@ -380,6 +382,11 @@ public async Task LoadProfile(Stream ldapConnectedStream, string us if (profile != null) { profile.Email = GetMail(mailEntries); + _logger.Debug("Loaded profile of user '{user:l}' ({dn:l})", userName, profile.Dn); + } + else + { + _logger.Debug("Profile of user '{user:l}' was not found in {baseDn:l}", userName, baseDn); } return profile; @@ -408,6 +415,8 @@ public async Task> GetAllGroups(Stream ldapConnectedStream, LdapPro groups.AddRange(GetGroups(packet)); } + _logger.Debug("Loaded {count} group(s) of user {dn:l}", groups.Count, profile.Dn); + return groups; } From 2c79b43b764582a2201cce0c2cd883e842838205 Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 10:58:55 +0300 Subject: [PATCH 09/19] add logger to LdapStreamReader --- src/Server/LdapProxy.cs | 3 +-- src/Server/LdapStream/LdapStreamReader.cs | 10 +++++++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Server/LdapProxy.cs b/src/Server/LdapProxy.cs index be6f9df..bf20e47 100644 --- a/src/Server/LdapProxy.cs +++ b/src/Server/LdapProxy.cs @@ -9,7 +9,6 @@ using Serilog; using System; using System.Collections.Concurrent; -using System.Diagnostics; using System.IO; using System.Linq; using System.Net.Sockets; @@ -90,7 +89,7 @@ private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient { try { - var streamReader = new LdapStreamReader(sourceStream); + var streamReader = new LdapStreamReader(sourceStream, _logger); LdapPacketBuffer ldapPacket; do { diff --git a/src/Server/LdapStream/LdapStreamReader.cs b/src/Server/LdapStream/LdapStreamReader.cs index c1a1e9c..dd6b5a9 100644 --- a/src/Server/LdapStream/LdapStreamReader.cs +++ b/src/Server/LdapStream/LdapStreamReader.cs @@ -1,4 +1,5 @@ -using MultiFactor.Ldap.Adapter.Core; +using MultiFactor.Ldap.Adapter.Core; +using Serilog; using System; using System.IO; using System.Threading.Tasks; @@ -11,12 +12,15 @@ public class LdapStreamReader private byte[] _readBuffer; private Stream _inputStream; - public LdapStreamReader(Stream inputStream) : this(inputStream, DEFAULT_BUFFER_SIZE) + private readonly ILogger _logger; + + public LdapStreamReader(Stream inputStream, ILogger logger = null) : this(inputStream, DEFAULT_BUFFER_SIZE, logger) { } - public LdapStreamReader(Stream inputStream, int bufferSize) + public LdapStreamReader(Stream inputStream, int bufferSize, ILogger logger = null) { _readBuffer = new byte[bufferSize]; _inputStream = inputStream; + _logger = logger ?? Log.Logger; } private LdapPacketBuffer GetResultPacket(byte[] buffer, int totalRead, bool packetValid) From 2f8a12c9c1d625965101b5c04be544d86f02ad4b Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 11:04:22 +0300 Subject: [PATCH 10/19] add const with length below zero guard --- src/Server/LdapStream/LdapStreamReader.cs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Server/LdapStream/LdapStreamReader.cs b/src/Server/LdapStream/LdapStreamReader.cs index dd6b5a9..ce0fb7d 100644 --- a/src/Server/LdapStream/LdapStreamReader.cs +++ b/src/Server/LdapStream/LdapStreamReader.cs @@ -9,6 +9,7 @@ namespace MultiFactor.Ldap.Adapter.Server.LdapStream public class LdapStreamReader { private const int DEFAULT_BUFFER_SIZE = 32768; + private const int MAX_PACKET_SIZE = 64 * Constants.BYTES_IN_MB; private byte[] _readBuffer; private Stream _inputStream; @@ -41,15 +42,16 @@ public async Task ReadLdapPacket() { return GetResultPacket(_readBuffer, totalRead, false); } - // handle multi-octate BER LEN!! + // handle multi-octet BER LEN if (_readBuffer[1] >> 7 == 1) { totalRead += await _inputStream.ReadAsync(_readBuffer, totalRead, _readBuffer[1] & 127); } var berLen = await Utils.BerLengthToInt(_readBuffer, 1); int berLenWithHeading = berLen.Length + berLen.BerByteCount + 1; - if(berLenWithHeading > 64 * Constants.BYTES_IN_MB) + if (berLen.Length < 0 || berLenWithHeading > MAX_PACKET_SIZE) { + _logger.Warning("LDAP packet of {size} byte(s) exceeds the {limit} byte(s) limit and will be bypassed without processing", berLenWithHeading, MAX_PACKET_SIZE); return GetResultPacket(_readBuffer, totalRead, false); } if(berLenWithHeading >= _readBuffer.Length) From d440c0125f5ba1ab6526269cdde50e64146946da Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 11:05:38 +0300 Subject: [PATCH 11/19] add stream bytes tail log --- src/Server/LdapStream/LdapStreamReader.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Server/LdapStream/LdapStreamReader.cs b/src/Server/LdapStream/LdapStreamReader.cs index ce0fb7d..b52b0f9 100644 --- a/src/Server/LdapStream/LdapStreamReader.cs +++ b/src/Server/LdapStream/LdapStreamReader.cs @@ -40,6 +40,11 @@ public async Task ReadLdapPacket() int totalRead = await _inputStream.ReadAsync(_readBuffer, 0, 2); if (totalRead < 2) { + // 0 bytes: connection gracefully closed, 1 byte: stream ended in the middle of a packet header + if (totalRead > 0) + { + _logger.Warning("Unexpected end of stream while reading LDAP packet header"); + } return GetResultPacket(_readBuffer, totalRead, false); } // handle multi-octet BER LEN From bee491d2c7847dcf6b68b6e4d7b1d690fc235901 Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 12:09:29 +0300 Subject: [PATCH 12/19] add logging --- src/Server/LdapStream/LdapStreamReader.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Server/LdapStream/LdapStreamReader.cs b/src/Server/LdapStream/LdapStreamReader.cs index b52b0f9..3850584 100644 --- a/src/Server/LdapStream/LdapStreamReader.cs +++ b/src/Server/LdapStream/LdapStreamReader.cs @@ -43,14 +43,20 @@ public async Task ReadLdapPacket() // 0 bytes: connection gracefully closed, 1 byte: stream ended in the middle of a packet header if (totalRead > 0) { - _logger.Warning("Unexpected end of stream while reading LDAP packet header"); + _logger.Warning("Unexpected end of stream while reading LDAP packet header: got {read} of 2 byte(s)", totalRead); } return GetResultPacket(_readBuffer, totalRead, false); } // handle multi-octet BER LEN if (_readBuffer[1] >> 7 == 1) { - totalRead += await _inputStream.ReadAsync(_readBuffer, totalRead, _readBuffer[1] & 127); + var lengthOctets = _readBuffer[1] & 127; + var lengthRead = await _inputStream.ReadAsync(_readBuffer, totalRead, lengthOctets); + totalRead += lengthRead; + if (lengthRead < lengthOctets) + { + _logger.Warning("Unexpected end of stream while reading LDAP packet length: got {read} of {expected} length octet(s)", lengthRead, lengthOctets); + } } var berLen = await Utils.BerLengthToInt(_readBuffer, 1); int berLenWithHeading = berLen.Length + berLen.BerByteCount + 1; @@ -75,6 +81,7 @@ public async Task ReadLdapPacket() attempts++; if (attempts > 3) { + _logger.Warning("Unexpected end of stream while reading LDAP packet: got {read} of {expected} byte(s)", totalRead, berLenWithHeading); return GetResultPacket(_readBuffer, totalRead, false); } From fa7158c1af8bbbd7f1e13ff04f47eeae646f1ca7 Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 14:46:04 +0300 Subject: [PATCH 13/19] ref --- src/Server/LdapProxy.cs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Server/LdapProxy.cs b/src/Server/LdapProxy.cs index bf20e47..d05c820 100644 --- a/src/Server/LdapProxy.cs +++ b/src/Server/LdapProxy.cs @@ -75,8 +75,8 @@ public async Task ProcessDataExchange() var responseStats = new ExchangeStats(); await Task.WhenAny( - DataExchange(_clientConnection, _clientStream, _serverConnection, _serverStream, ParseAndProcessRequest, requestStats, from, to), - DataExchange(_serverConnection, _serverStream, _clientConnection, _clientStream, ParseAndProcessResponse, responseStats, to, from)); + DataExchange(_clientConnection, _clientStream, _serverConnection, _serverStream, ParseAndProcessRequest, requestStats), + DataExchange(_serverConnection, _serverStream, _clientConnection, _clientStream, ParseAndProcessResponse, responseStats)); _closing = true; @@ -85,8 +85,10 @@ await Task.WhenAny( } - private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient target, Stream targetStream, Func> process, ExchangeStats stats, string from, string to) + private async Task DataExchange(TcpClient source, Stream sourceStream, TcpClient target, Stream targetStream, Func> process, ExchangeStats stats) { + var from = source.Client.RemoteEndPoint.ToString(); + var to = target.Client.RemoteEndPoint.ToString(); try { var streamReader = new LdapStreamReader(sourceStream, _logger); From 3548882284c5736c6df1cb63d1c9bed6c92af305 Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 17:56:24 +0300 Subject: [PATCH 14/19] ReadExactlyAsync instead of ReadAsync --- src/Server/LdapStream/LdapStreamReader.cs | 30 ++++++++++++++--------- 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Server/LdapStream/LdapStreamReader.cs b/src/Server/LdapStream/LdapStreamReader.cs index 3850584..420f401 100644 --- a/src/Server/LdapStream/LdapStreamReader.cs +++ b/src/Server/LdapStream/LdapStreamReader.cs @@ -37,25 +37,30 @@ private LdapPacketBuffer GetResultPacket(byte[] buffer, int totalRead, bool pack public async Task ReadLdapPacket() { - int totalRead = await _inputStream.ReadAsync(_readBuffer, 0, 2); - if (totalRead < 2) + int totalRead; + try { - // 0 bytes: connection gracefully closed, 1 byte: stream ended in the middle of a packet header - if (totalRead > 0) - { - _logger.Warning("Unexpected end of stream while reading LDAP packet header: got {read} of 2 byte(s)", totalRead); - } - return GetResultPacket(_readBuffer, totalRead, false); + await _inputStream.ReadExactlyAsync(_readBuffer, 0, 2); + totalRead = 2; + } + catch (EndOfStreamException) + { + _logger.Warning("Unexpected end of stream while reading LDAP packet header"); + return GetResultPacket(_readBuffer, 0, false); } // handle multi-octet BER LEN if (_readBuffer[1] >> 7 == 1) { var lengthOctets = _readBuffer[1] & 127; - var lengthRead = await _inputStream.ReadAsync(_readBuffer, totalRead, lengthOctets); - totalRead += lengthRead; - if (lengthRead < lengthOctets) + try + { + await _inputStream.ReadExactlyAsync(_readBuffer, totalRead, lengthOctets); + totalRead += lengthOctets; + } + catch (EndOfStreamException) { - _logger.Warning("Unexpected end of stream while reading LDAP packet length: got {read} of {expected} length octet(s)", lengthRead, lengthOctets); + _logger.Warning("Unexpected end of stream while reading LDAP packet length: expected {expected} length octet(s)", lengthOctets); + return GetResultPacket(_readBuffer, 0, false); } } var berLen = await Utils.BerLengthToInt(_readBuffer, 1); @@ -71,6 +76,7 @@ public async Task ReadLdapPacket() Array.Copy(_readBuffer, newBuffer, totalRead); _readBuffer = newBuffer; } + // read packet until end int attempts = 0; while (totalRead < berLenWithHeading) From 67c7f006c051c75eb6bb5165e202751dae3580ce Mon Sep 17 00:00:00 2001 From: "a.andreev" Date: Fri, 3 Jul 2026 18:03:53 +0300 Subject: [PATCH 15/19] add regression tests --- tests/LdapStreamTests.cs | 70 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/tests/LdapStreamTests.cs b/tests/LdapStreamTests.cs index 3fea73c..d076806 100644 --- a/tests/LdapStreamTests.cs +++ b/tests/LdapStreamTests.cs @@ -64,6 +64,61 @@ public async Task LdapStream_ShouldNotOverflow() } } + [Fact] + public async Task LdapStream_ShouldReadFragmentedPackage() + { + // a packet split between tcp segments: each read returns a single byte + var packet = GetPacket(); + using (var stream = new ChunkedStream(packet, chunkSize: 1)) + { + var reader = new LdapStreamReader(stream); + var result = await reader.ReadLdapPacket(); + Assert.True(result.PacketValid); + Assert.Equal(packet.Length, result.Data.Length); + } + } + + [Fact] + public async Task LdapStream_EmptyStream_ShouldReturnEmptyPacket() + { + // closed connection: the proxy treats an empty packet as end of stream + using (var stream = new MemoryStream()) + { + var reader = new LdapStreamReader(stream); + var result = await reader.ReadLdapPacket(); + Assert.False(result.PacketValid); + Assert.Empty(result.Data); + } + } + + [Fact] + public async Task LdapStream_ShouldReadSequentialFragmentedPackets() + { + // several packets on one connection, each read returns a single byte: + // the reader must not lose the frame boundaries between packets + var packet = GetPacket(); + var twoPackets = new byte[packet.Length * 2]; + packet.CopyTo(twoPackets, 0); + packet.CopyTo(twoPackets, packet.Length); + + using (var stream = new ChunkedStream(twoPackets, chunkSize: 1)) + { + var reader = new LdapStreamReader(stream); + + var first = await reader.ReadLdapPacket(); + Assert.True(first.PacketValid); + Assert.Equal(packet.Length, first.Data.Length); + + var second = await reader.ReadLdapPacket(); + Assert.True(second.PacketValid); + Assert.Equal(packet.Length, second.Data.Length); + + var end = await reader.ReadLdapPacket(); + Assert.False(end.PacketValid); + Assert.Empty(end.Data); + } + } + [Fact] public async Task LdapStream_ShouldNotReadVeryBigPacket() { @@ -77,5 +132,20 @@ public async Task LdapStream_ShouldNotReadVeryBigPacket() Assert.False(result.Data.Length > 2048); } } + + private class ChunkedStream : MemoryStream + { + private readonly int _chunkSize; + + public ChunkedStream(byte[] data, int chunkSize) : base(data) + { + _chunkSize = chunkSize; + } + + public override Task ReadAsync(byte[] buffer, int offset, int count, System.Threading.CancellationToken cancellationToken) + { + return base.ReadAsync(buffer, offset, Math.Min(count, _chunkSize), cancellationToken); + } + } } } From 74c6678438af5b0c9cccdbdaa35f849076d19fed Mon Sep 17 00:00:00 2001 From: Andrei Andreev Date: Mon, 6 Jul 2026 11:16:31 +0300 Subject: [PATCH 16/19] add valuetask override --- tests/LdapStreamTests.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/LdapStreamTests.cs b/tests/LdapStreamTests.cs index d076806..6176b03 100644 --- a/tests/LdapStreamTests.cs +++ b/tests/LdapStreamTests.cs @@ -146,6 +146,11 @@ public override Task ReadAsync(byte[] buffer, int offset, int count, System { return base.ReadAsync(buffer, offset, Math.Min(count, _chunkSize), cancellationToken); } + + public override ValueTask ReadAsync(Memory buffer, System.Threading.CancellationToken cancellationToken = default) + { + return base.ReadAsync(buffer.Slice(0, Math.Min(buffer.Length, _chunkSize)), cancellationToken); + } } } } From 77a09028e42953f54e5245a05b5e790909261ddf Mon Sep 17 00:00:00 2001 From: Andrei Andreev Date: Mon, 6 Jul 2026 11:17:07 +0300 Subject: [PATCH 17/19] change log level of reading header --- src/Server/LdapStream/LdapStreamReader.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Server/LdapStream/LdapStreamReader.cs b/src/Server/LdapStream/LdapStreamReader.cs index 420f401..b23fbb5 100644 --- a/src/Server/LdapStream/LdapStreamReader.cs +++ b/src/Server/LdapStream/LdapStreamReader.cs @@ -45,7 +45,7 @@ public async Task ReadLdapPacket() } catch (EndOfStreamException) { - _logger.Warning("Unexpected end of stream while reading LDAP packet header"); + _logger.Debug("End of stream while reading LDAP packet header, connection closed"); return GetResultPacket(_readBuffer, 0, false); } // handle multi-octet BER LEN From 86d8f6e2bf31daf6a99d82267698a4af92af4349 Mon Sep 17 00:00:00 2001 From: Andrei Andreev Date: Mon, 6 Jul 2026 11:22:27 +0300 Subject: [PATCH 18/19] bypass some unsuccess results from warning logs --- src/Core/LdapPacket.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Core/LdapPacket.cs b/src/Core/LdapPacket.cs index c7455a8..d8336eb 100644 --- a/src/Core/LdapPacket.cs +++ b/src/Core/LdapPacket.cs @@ -126,10 +126,18 @@ private static void LogNonSuccessResult(LdapAttribute searchDone) } var result = (LdapResult)resultAttr.GetValue(); - if (result != LdapResult.success) + if (result == LdapResult.success) { - Log.Logger.Warning("LDAP search operation completed with {result} result", result); + return; + } + + if (result is LdapResult.referral or LdapResult.sizeLimitExceeded or LdapResult.timeLimitExceeded) + { + Log.Logger.Debug("LDAP search operation completed with {result} result", result); + return; } + + Log.Logger.Warning("LDAP search operation completed with {result} result", result); } } } From c4544b1fb1cabe9daf0ef5d97a0a2d96dc3f0ac4 Mon Sep 17 00:00:00 2001 From: Andrei Andreev Date: Tue, 7 Jul 2026 16:11:28 +0300 Subject: [PATCH 19/19] refactor profile not found block --- src/Services/LdapService.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Services/LdapService.cs b/src/Services/LdapService.cs index 2904c7e..171aa06 100644 --- a/src/Services/LdapService.cs +++ b/src/Services/LdapService.cs @@ -379,16 +379,16 @@ public async Task LoadProfile(Stream ldapConnectedStream, string us } } - if (profile != null) - { - profile.Email = GetMail(mailEntries); - _logger.Debug("Loaded profile of user '{user:l}' ({dn:l})", userName, profile.Dn); - } - else + if (profile == null) { _logger.Debug("Profile of user '{user:l}' was not found in {baseDn:l}", userName, baseDn); + + return null; } + profile.Email = GetMail(mailEntries); + _logger.Debug("Loaded profile of user '{user:l}' ({dn:l})", userName, profile.Dn); + return profile; }