diff --git a/.gitignore b/.gitignore
index a354f6e..66629db 100644
--- a/.gitignore
+++ b/.gitignore
@@ -36,3 +36,6 @@ $RECYCLE.BIN/
# Project private instructions
MITMPulse_project.md
+
+# Claude Code local settings
+.claude/
diff --git a/README.md b/README.md
index 75e8f3b..84034d3 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,9 @@ Website: [www.dpmworld.net](https://www.dpmworld.net)
- 👁️ **Red Eye (`Eye24`)**: Confirmed MITM SSL Inspection active (enterprise proxy re-signing detected).
- ⚠️ **Orange Warning (`Warning24`)**: Certificate expired or domain error on a public endpoint (direct connection).
- 🟢 **Green Checkmark (`CheckmarkCircle24`)**: Direct secure connection verified against trusted public Root CAs.
+- **DTLS over UDP Protocol Inspection**:
+ - Probes UDP target ports in parallel using standard RFC 6347 DTLS 1.2 `ClientHello` datagrams (with SNI).
+ - Displays a discrete green **DTLS** badge when DTLS transport (e.g., Citrix NetScaler Enlightened Data Transport - EDT) is verified.
- **GoDaddy R1/G2 Hierarchy & Cross-Certificate Diagnostics**:
- Native support for GoDaddy & Starfield Root CAs (`GoDaddy TLS Root CA - R1`, `R1v1` intermediate).
- Emits specific diagnostic warnings if a NetScaler or server is missing the `R1->G2` cross-certificate, preventing client trust false positives (referencing [Go Daddy TLS Certificate not trusted](https://www.dpmworld.net/2026/07/31/go-daddy-tls-certificate-not-trusted/)).
@@ -30,6 +33,7 @@ Website: [www.dpmworld.net](https://www.dpmworld.net)
- **TLS Version & Cipher Suite Metrics**: Displays negotiated protocol versions (TLS 1.2, TLS 1.3) and negotiated Cipher Suites.
- **Certificate Pinning**: Optional validation against expected SHA-1 certificate thumbprints with preset fallback.
- **Proxy Modes & PAC/WinHTTP Tunneling**: Supports **Direct** socket connections, **System Proxy** (WinINet / System with automatic **PAC script** execution & HTTP `CONNECT` tunneling), **WinHTTP Proxy** (`netsh winhttp` P/Invoke native system proxy reader), or **Custom** explicit HTTP/SOCKS proxies with mandatory configuration validation and authentication.
+- **Inspection History & Hardened CSV/JSON Export**: Stores persistent inspection records with built-in sanitization against CSV Formula Injection vulnerabilities.
### 🏢 Predefined Target Presets
Pre-populated with high-priority enterprise cloud endpoints and live certificate baselines:
diff --git a/src/MITMPulse/MITMPulse.csproj b/src/MITMPulse/MITMPulse.csproj
index 776133d..022e325 100644
--- a/src/MITMPulse/MITMPulse.csproj
+++ b/src/MITMPulse/MITMPulse.csproj
@@ -13,9 +13,9 @@
Francesco Dipietromaria
dpmworld.net
Copyright © 2026 Francesco Dipietromaria (www.dpmworld.net)
- 1.0.24
- 1.0.24.0
- 1.0.24.0
+ 1.0.26
+ 1.0.26.0
+ 1.0.26.0
Assets\icon.ico
diff --git a/src/MITMPulse/MainWindow.xaml b/src/MITMPulse/MainWindow.xaml
index 8b840c7..be82551 100644
--- a/src/MITMPulse/MainWindow.xaml
+++ b/src/MITMPulse/MainWindow.xaml
@@ -9,6 +9,10 @@
WindowStartupLocation="CenterScreen"
Icon="pack://application:,,,/Assets/icon.ico">
+
+
+
+
@@ -19,7 +23,8 @@
-
@@ -91,28 +96,52 @@
-
-
+
+
+
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/MITMPulse/MainWindow.xaml.cs b/src/MITMPulse/MainWindow.xaml.cs
index 1dd6dbc..45cfba8 100644
--- a/src/MITMPulse/MainWindow.xaml.cs
+++ b/src/MITMPulse/MainWindow.xaml.cs
@@ -9,6 +9,22 @@ public MainWindow()
InitializeComponent();
DataContext = App.ViewModel;
Loaded += MainWindow_Loaded;
+ MouseDown += MainWindow_MouseDown;
+ AppTitleBar.MouseLeftButtonDown += (s, e) =>
+ {
+ if (e.LeftButton == System.Windows.Input.MouseButtonState.Pressed)
+ {
+ DragMove();
+ }
+ };
+ }
+
+ private void MainWindow_MouseDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
+ {
+ if (e.ChangedButton == System.Windows.Input.MouseButton.Left && e.GetPosition(this).Y < 40)
+ {
+ DragMove();
+ }
}
private void MainWindow_Loaded(object sender, System.Windows.RoutedEventArgs e)
diff --git a/src/MITMPulse/Models/InspectionHistoryEntry.cs b/src/MITMPulse/Models/InspectionHistoryEntry.cs
index 6dab840..46fd3b5 100644
--- a/src/MITMPulse/Models/InspectionHistoryEntry.cs
+++ b/src/MITMPulse/Models/InspectionHistoryEntry.cs
@@ -18,4 +18,5 @@ public class InspectionHistoryEntry
public string StatusSummary { get; set; } = string.Empty;
public string ServerIssuer { get; set; } = string.Empty;
public string ServerThumbprint { get; set; } = string.Empty;
+ public bool IsDtlsSupported { get; set; }
}
diff --git a/src/MITMPulse/Models/SslInspectionResult.cs b/src/MITMPulse/Models/SslInspectionResult.cs
index 2962528..4d05b9c 100644
--- a/src/MITMPulse/Models/SslInspectionResult.cs
+++ b/src/MITMPulse/Models/SslInspectionResult.cs
@@ -24,4 +24,6 @@ public class SslInspectionResult
public string PacScriptUrl { get; set; } = string.Empty;
public string PacResolvedProxy { get; set; } = string.Empty;
public string TunnelStatus { get; set; } = string.Empty;
+ public bool IsDtlsSupported { get; set; }
+ public string DtlsDetails { get; set; } = string.Empty;
}
diff --git a/src/MITMPulse/Services/HistoryService.cs b/src/MITMPulse/Services/HistoryService.cs
index 4b5f158..e452e4e 100644
--- a/src/MITMPulse/Services/HistoryService.cs
+++ b/src/MITMPulse/Services/HistoryService.cs
@@ -71,7 +71,8 @@ public async Task SaveEntryAsync(SslInspectionResult result, CancellationToken c
IsSuccess = result.IsSuccess,
StatusSummary = statusSummary,
ServerIssuer = result.ServerCertificate?.Issuer ?? string.Empty,
- ServerThumbprint = result.ServerCertificate?.Thumbprint ?? string.Empty
+ ServerThumbprint = result.ServerCertificate?.Thumbprint ?? string.Empty,
+ IsDtlsSupported = result.IsDtlsSupported
};
history.Insert(0, entry); // Add newest at top
@@ -106,15 +107,27 @@ public async Task ExportToCsvAsync(string filePath, CancellationToken cancellati
{
var history = await GetHistoryAsync(cancellationToken);
var sb = new StringBuilder();
- sb.AppendLine("Timestamp,TargetHost,TargetPort,IsSuccess,IsSslInspectionDetected,TlsVersion,CipherSuite,ServerIssuer,ServerThumbprint,StatusSummary");
+ sb.AppendLine("Timestamp,TargetHost,TargetPort,IsSuccess,IsSslInspectionDetected,IsDtlsSupported,TlsVersion,CipherSuite,ServerIssuer,ServerThumbprint,StatusSummary");
foreach (var item in history)
{
- sb.AppendLine($"\"{item.Timestamp:yyyy-MM-dd HH:mm:ss}\",\"{EscapeCsv(item.TargetHost)}\",{item.TargetPort},{item.IsSuccess},{item.IsSslInspectionDetected},\"{EscapeCsv(item.TlsVersion)}\",\"{EscapeCsv(item.CipherSuite)}\",\"{EscapeCsv(item.ServerIssuer)}\",\"{EscapeCsv(item.ServerThumbprint)}\",\"{EscapeCsv(item.StatusSummary)}\"");
+ sb.AppendLine($"\"{item.Timestamp:yyyy-MM-dd HH:mm:ss}\",\"{EscapeCsv(item.TargetHost)}\",{item.TargetPort},{item.IsSuccess},{item.IsSslInspectionDetected},{item.IsDtlsSupported},\"{EscapeCsv(item.TlsVersion)}\",\"{EscapeCsv(item.CipherSuite)}\",\"{EscapeCsv(item.ServerIssuer)}\",\"{EscapeCsv(item.ServerThumbprint)}\",\"{EscapeCsv(item.StatusSummary)}\"");
}
await File.WriteAllTextAsync(filePath, sb.ToString(), Encoding.UTF8, cancellationToken);
}
- private static string EscapeCsv(string input) => input.Replace("\"", "\"\"");
+ private static string EscapeCsv(string input)
+ {
+ string escaped = input.Replace("\"", "\"\"");
+
+ // Prevent CSV/formula injection: neutralize values that Excel/LibreOffice
+ // may interpret as formulas (e.g. a malicious certificate Issuer field).
+ if (escaped.Length > 0 && (escaped[0] == '=' || escaped[0] == '+' || escaped[0] == '-' || escaped[0] == '@' || escaped[0] == '\t' || escaped[0] == '\r'))
+ {
+ escaped = "'" + escaped;
+ }
+
+ return escaped;
+ }
}
diff --git a/src/MITMPulse/Services/SslInspectionService.cs b/src/MITMPulse/Services/SslInspectionService.cs
index df2c01c..4671d98 100644
--- a/src/MITMPulse/Services/SslInspectionService.cs
+++ b/src/MITMPulse/Services/SslInspectionService.cs
@@ -41,6 +41,9 @@ public async Task InspectEndpointAsync(
return result;
}
+ // Launch DTLS over UDP test in parallel (with short timeout)
+ var dtlsTask = TestDtlsOverUdpAsync(targetHost, targetPort, cancellationToken);
+
try
{
using var tcpClient = new TcpClient();
@@ -170,6 +173,20 @@ public async Task InspectEndpointAsync(
result.ErrorMessage = ex.Message;
}
+ // Await DTLS UDP result (if not already completed)
+ try
+ {
+ result.IsDtlsSupported = await dtlsTask.ConfigureAwait(false);
+ if (result.IsDtlsSupported)
+ {
+ result.DtlsDetails = "DTLS over UDP (Datagram TLS) Active & Verified";
+ }
+ }
+ catch
+ {
+ result.IsDtlsSupported = false;
+ }
+
return result;
}
@@ -495,4 +512,171 @@ private static async Task EstablishConnectionAsync(
await tcpClient.ConnectAsync(targetHost, targetPort, cancellationToken).ConfigureAwait(false);
return tcpClient.GetStream();
}
+
+ ///
+ /// Tests if the remote endpoint speaks DTLS (Datagram Transport Layer Security) over UDP.
+ /// Sends a standard RFC 6347 DTLS 1.2 ClientHello datagram and checks for a valid DTLS response (e.g. HelloVerifyRequest or ServerHello).
+ ///
+ public static async Task TestDtlsOverUdpAsync(string host, int port, CancellationToken cancellationToken = default)
+ {
+ try
+ {
+ byte[] clientHello = BuildDtlsClientHello(host);
+ using var udpClient = new UdpClient();
+
+ using var timeoutCts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
+ timeoutCts.CancelAfter(TimeSpan.FromMilliseconds(2500));
+
+ // Resolve host IP
+ var addresses = await Dns.GetHostAddressesAsync(host, cancellationToken).ConfigureAwait(false);
+ if (addresses.Length == 0) return false;
+
+ var targetIp = addresses.FirstOrDefault(a => a.AddressFamily == AddressFamily.InterNetwork) ?? addresses[0];
+ var endpoint = new IPEndPoint(targetIp, port);
+
+ await udpClient.SendAsync(clientHello, clientHello.Length, endpoint).ConfigureAwait(false);
+
+ var receiveTask = udpClient.ReceiveAsync(timeoutCts.Token).AsTask();
+ var completedTask = await Task.WhenAny(receiveTask, Task.Delay(2500, timeoutCts.Token)).ConfigureAwait(false);
+
+ if (completedTask == receiveTask)
+ {
+ var response = await receiveTask.ConfigureAwait(false);
+ byte[] data = response.Buffer;
+
+ if (data.Length >= 13)
+ {
+ byte contentType = data[0];
+ byte versionMajor = data[1];
+
+ // DTLS record content types: 22 (Handshake), 21 (Alert)
+ // DTLS protocol version major is 0xFE (DTLS 1.0 = 0xFEFF, DTLS 1.2 = 0xFEFD, DTLS 1.3 = 0xFEFC)
+ if ((contentType == 22 || contentType == 21) && versionMajor == 0xFE)
+ {
+ return true;
+ }
+ }
+ }
+ }
+ catch
+ {
+ // UDP/DTLS check is non-fatal: return false on timeout/error
+ }
+
+ return false;
+ }
+
+ ///
+ /// Builds a well-formed RFC 6347 DTLS 1.2 ClientHello record datagram including SNI extension.
+ ///
+ public static byte[] BuildDtlsClientHello(string host)
+ {
+ using var ms = new MemoryStream();
+ using var bw = new BinaryWriter(ms);
+
+ using var bodyMs = new MemoryStream();
+ using var bodyBw = new BinaryWriter(bodyMs);
+
+ // Client Version (DTLS 1.2 = 0xFEFD)
+ bodyBw.Write(new byte[] { 0xfe, 0xfd });
+
+ // Random 32 bytes
+ byte[] randomBytes = new byte[32];
+ RandomNumberGenerator.Fill(randomBytes);
+ bodyBw.Write(randomBytes);
+
+ // Session ID Length = 0
+ bodyBw.Write((byte)0x00);
+
+ // Cookie Length = 0
+ bodyBw.Write((byte)0x00);
+
+ // Cipher Suites (20 bytes = 10 suites)
+ byte[] cipherSuites = new byte[]
+ {
+ 0x00, 0x14, // Length: 20 bytes
+ 0xc0, 0x2f, // TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
+ 0xc0, 0x30, // TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
+ 0xc0, 0x13, // TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
+ 0xc0, 0x14, // TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA
+ 0x00, 0x9c, // TLS_RSA_WITH_AES_128_GCM_SHA256
+ 0x00, 0x9d, // TLS_RSA_WITH_AES_256_GCM_SHA384
+ 0x00, 0x2f, // TLS_RSA_WITH_AES_128_CBC_SHA
+ 0x00, 0x35, // TLS_RSA_WITH_AES_256_CBC_SHA
+ 0x00, 0x0a, // TLS_RSA_WITH_3DES_EDE_CBC_SHA
+ 0x00, 0xff // TLS_EMPTY_RENEGOTIATION_INFO_SCSV
+ };
+ bodyBw.Write(cipherSuites);
+
+ // Compression Methods (1 byte length + 0x00 null)
+ bodyBw.Write(new byte[] { 0x01, 0x00 });
+
+ // Extensions (SNI)
+ if (!string.IsNullOrWhiteSpace(host) && !IPAddress.TryParse(host, out _))
+ {
+ byte[] hostBytes = Encoding.UTF8.GetBytes(host);
+ using var extMs = new MemoryStream();
+ using var extBw = new BinaryWriter(extMs);
+
+ // Extension: Server Name Indication (0x0000)
+ extBw.Write(new byte[] { 0x00, 0x00 });
+ int sniListLength = hostBytes.Length + 3;
+ int sniExtLength = sniListLength + 2;
+
+ extBw.Write((byte)(sniExtLength >> 8));
+ extBw.Write((byte)(sniExtLength & 0xFF));
+
+ extBw.Write((byte)(sniListLength >> 8));
+ extBw.Write((byte)(sniListLength & 0xFF));
+
+ extBw.Write((byte)0x00); // HostName type (0)
+ extBw.Write((byte)(hostBytes.Length >> 8));
+ extBw.Write((byte)(hostBytes.Length & 0xFF));
+ extBw.Write(hostBytes);
+
+ byte[] extData = extMs.ToArray();
+ bodyBw.Write((byte)(extData.Length >> 8));
+ bodyBw.Write((byte)(extData.Length & 0xFF));
+ bodyBw.Write(extData);
+ }
+
+ byte[] handshakeBody = bodyMs.ToArray();
+
+ // Handshake Header (12 bytes)
+ using var hsMs = new MemoryStream();
+ using var hsBw = new BinaryWriter(hsMs);
+ hsBw.Write((byte)0x01); // HandshakeType = ClientHello (1)
+
+ // 3 bytes length
+ hsBw.Write((byte)(handshakeBody.Length >> 16));
+ hsBw.Write((byte)((handshakeBody.Length >> 8) & 0xFF));
+ hsBw.Write((byte)(handshakeBody.Length & 0xFF));
+
+ // 2 bytes message_seq = 0
+ hsBw.Write(new byte[] { 0x00, 0x00 });
+
+ // 3 bytes fragment_offset = 0
+ hsBw.Write(new byte[] { 0x00, 0x00, 0x00 });
+
+ // 3 bytes fragment_length
+ hsBw.Write((byte)(handshakeBody.Length >> 16));
+ hsBw.Write((byte)((handshakeBody.Length >> 8) & 0xFF));
+ hsBw.Write((byte)(handshakeBody.Length & 0xFF));
+
+ // Body
+ hsBw.Write(handshakeBody);
+
+ byte[] handshakeRecord = hsMs.ToArray();
+
+ // DTLS Record Header (13 bytes)
+ bw.Write((byte)0x16); // ContentType: Handshake (22)
+ bw.Write(new byte[] { 0xfe, 0xfd }); // ProtocolVersion: DTLS 1.2
+ bw.Write(new byte[] { 0x00, 0x00 }); // Epoch: 0
+ bw.Write(new byte[] { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }); // SequenceNumber: 0
+ bw.Write((byte)(handshakeRecord.Length >> 8));
+ bw.Write((byte)(handshakeRecord.Length & 0xFF));
+ bw.Write(handshakeRecord);
+
+ return ms.ToArray();
+ }
}
diff --git a/src/MITMPulse/ViewModels/MainViewModel.cs b/src/MITMPulse/ViewModels/MainViewModel.cs
index f3ebff6..84f51f7 100644
--- a/src/MITMPulse/ViewModels/MainViewModel.cs
+++ b/src/MITMPulse/ViewModels/MainViewModel.cs
@@ -68,6 +68,7 @@ public string AppTitleWithVersion
public bool IsCertificateExpired => !IsLoading && InspectionResult != null && InspectionResult.IsSuccess && !InspectionResult.IsSslInspectionDetected && InspectionResult.ServerCertificate != null && (DateTime.UtcNow > InspectionResult.ServerCertificate.ValidTo || DateTime.UtcNow < InspectionResult.ServerCertificate.ValidFrom);
public bool IsDirectConnection => !IsLoading && InspectionResult != null && InspectionResult.IsSuccess && !InspectionResult.IsSslInspectionDetected && !IsCertificateExpired;
public bool IsConnectionError => !IsLoading && InspectionResult != null && !InspectionResult.IsSuccess;
+ public bool IsDtlsActive => !IsLoading && InspectionResult != null && InspectionResult.IsSuccess && InspectionResult.IsDtlsSupported;
partial void OnInspectionResultChanged(SslInspectionResult? value)
{
@@ -75,6 +76,7 @@ partial void OnInspectionResultChanged(SslInspectionResult? value)
OnPropertyChanged(nameof(IsCertificateExpired));
OnPropertyChanged(nameof(IsDirectConnection));
OnPropertyChanged(nameof(IsConnectionError));
+ OnPropertyChanged(nameof(IsDtlsActive));
}
partial void OnIsLoadingChanged(bool value)
@@ -83,6 +85,7 @@ partial void OnIsLoadingChanged(bool value)
OnPropertyChanged(nameof(IsCertificateExpired));
OnPropertyChanged(nameof(IsDirectConnection));
OnPropertyChanged(nameof(IsConnectionError));
+ OnPropertyChanged(nameof(IsDtlsActive));
}
public MainViewModel(
diff --git a/tests/MITMPulse.Tests/Services/SslInspectionServiceTests.cs b/tests/MITMPulse.Tests/Services/SslInspectionServiceTests.cs
index 243226e..39c63a3 100644
--- a/tests/MITMPulse.Tests/Services/SslInspectionServiceTests.cs
+++ b/tests/MITMPulse.Tests/Services/SslInspectionServiceTests.cs
@@ -42,4 +42,34 @@ public async Task InspectEndpointAsync_InvalidHost_ReturnsFailureResult()
Assert.False(result.IsSuccess);
Assert.NotEmpty(result.ErrorMessage);
}
+
+ [Fact]
+ public void BuildDtlsClientHello_ValidHost_ReturnsWellFormedDtlsRecord()
+ {
+ // Arrange
+ string host = "citrix.gateway.example.com";
+
+ // Act
+ byte[] datagram = SslInspectionService.BuildDtlsClientHello(host);
+
+ // Assert
+ Assert.NotNull(datagram);
+ Assert.True(datagram.Length >= 13);
+ Assert.Equal(0x16, datagram[0]); // ContentType = Handshake (22)
+ Assert.Equal(0xFE, datagram[1]); // Major Version = DTLS (0xFE)
+ Assert.Equal(0xFD, datagram[2]); // Minor Version = DTLS 1.2 (0xFD)
+ }
+
+ [Fact]
+ public async Task TestDtlsOverUdpAsync_InvalidHost_ReturnsFalse()
+ {
+ // Arrange
+ string invalidHost = "invalid.nonexistent.domain.xyz12345";
+
+ // Act
+ bool isSupported = await SslInspectionService.TestDtlsOverUdpAsync(invalidHost, 443);
+
+ // Assert
+ Assert.False(isSupported);
+ }
}
diff --git a/tests/MITMPulse.Tests/ViewModels/MainViewModelTests.cs b/tests/MITMPulse.Tests/ViewModels/MainViewModelTests.cs
index 6614861..1803821 100644
--- a/tests/MITMPulse.Tests/ViewModels/MainViewModelTests.cs
+++ b/tests/MITMPulse.Tests/ViewModels/MainViewModelTests.cs
@@ -75,4 +75,26 @@ public async Task InspectEndpointAsync_ValidHost_CallsSslInspectionService()
Assert.Equal("DIRECT CONNECTION (No SSL Inspection Detected)", _sut.StatusMessage);
_mockHistoryService.Verify(h => h.SaveEntryAsync(expectedResult, It.IsAny()), Times.Once);
}
+
+ [Fact]
+ public void Constructor_IsDtlsActive_IsFalseInitially()
+ {
+ // Assert
+ Assert.Null(_sut.InspectionResult);
+ Assert.False(_sut.IsDtlsActive);
+ }
+
+ [Fact]
+ public void IsDtlsActive_WhenDtlsSupportedAndSuccess_ReturnsTrue()
+ {
+ // Arrange
+ _sut.InspectionResult = new SslInspectionResult
+ {
+ IsSuccess = true,
+ IsDtlsSupported = true
+ };
+
+ // Assert
+ Assert.True(_sut.IsDtlsActive);
+ }
}