Matching TCP Streams Between Client And Server Wireshark Packet Captures Using Powershell
Posted on July 07, 2022
- and tagged as
- powershell,
- wireshark
If you’ve had to diagnose application issues where there may be a network fault (or the vendor is blaming the network 😡) you may have taken packet captures at both sides of the connection and then compared them. Sometimes this is relatively easy if you can trigger the traffic and start your Wireshark captures at the same time. Other times it can be a bit painful especially if another party is providing one of the capture files and your TCP sessions are all over the place.
I recently had so do something along these lines and found myself needing to map TCP sessions between the client and the server packet captures as well as find SYN packets from the client with which did not make it to the server. I created a quick PowerShell script to help with this and that’s what we’re going to discuss in this post.
But first a bit of theory.
Wireshark TCP Sessions/Streams
Wireshark keeps track of TCP sessions inside a packet capture and these can be accessed using the tcp.stream
display filter.
The session/stream index can be viewed in the TCP section of the packet under the Stream Index
label. Following a TCP Stream will set the display filter to only show that specific session. A TCP Stream begins with a 3 way TCP handshake (SYN
, SYN/ACK
, ACK
), contains some data, and then ends.
But that’s not always the case, in Wireshark a TCP Stream can also be SYN
packets with no response.
What we want to do in this post is get mapping of the TCP sessions between the client and server, for example, TCP Stream #12 on the client side may correspond with TCP Stream #16 in the server’s packet capture.
How to match packets between the client and server in packet captures?
There are two common ways to find the same packet in packet captures from two different sides of the connection.
- IP Identification Field
- TCP Sequence Number
If the traffic is UDP we would need to rely on the IP Identification field or something in the application data but as I’m dealing with TCP I’ve gone with TCP Sequence numbers in my code.
It’s important to note that if there are intermediary devices between the two endpoints that terminate the connection such as proxy or reverse proxy servers then neither of these methods would work as the client and server never directly exchange packets.
Other devices along the path which such as firewalls may also change these values but in my experience they’re generally reliable if the two endpoints are communicating directly, even if it’s across the Internet.
Processing Network Packet Data With PowerShell
I’ve written about a similar topic previously so if you’re interested in how this all works check out my post on Processing Tshark Streams With PowerShell.
TL;DR is we’re going to export the PCAP data to JSON using tshark
and then use PowerShell to convert the JSON into usable objects. From there we can apply whatever logic we need to get the desired result.
A note on capture size
I’ve used the code below with PCAPNG files ~100MB in size, I would recommend taking larger packet captures and using display filters in Wireshark to export the smallest size file possible for best performance and the smallest memory footprint.
Time for some code.
Mapping TCP Streams between two packet captures using PowerShell
This the function to map TCP sessions between two Wireshark captures. It takes 3 inputs (2 required)
- The path to TShark (Optional). Tshark is installed by default when you install Wireshark. If no path is required it will assume the default
- The path to the client side packet capture
- The path to the server side packet capture
For the sake of clarity, it is assumed that whichever side is initiating the connection (i.e., sending the initial SYN
packet) is the client.
Tshark is then invoked and we’re specifically looking to extract packets with either just the SYN
flag set which will have a value of 0x0002
in the TCP Flags field, or the SYN, ECN, CWR
flags which will have the value of 0x00c2
. ECN
and CWR
are used for congestion control and are sometimes found in the initial SYN
packet to indicate to the receiver that the sending party supports those features.
Finally we apply our logic by matching the TCP Sequence numbers between the captures.
function MapTCPStreams {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false)]
[ValidateScript({Test-Path $_})]
[string]$TsharkPath = "C:\Program Files\Wireshark\tshark.exe",
[Parameter(Mandatory)]
[ValidateScript({Test-Path $_})]
[ValidateScript({(Get-Item $_).Extension -match "\.pcap$|\.pcapng$"})]
[string]$ClientPCAP,
[Parameter(Mandatory)]
[ValidateScript({Test-Path $_})]
[ValidateScript({(Get-Item $_).Extension -match "\.pcap$|\.pcapng$"})]
[string]$ServerPCAP
)
$ClientSYNPackets = & $TsharkPath -r $ClientPCAP -T ek -e _ws.col.Protocol -e ip.proto -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e tcp.flags -e tcp.seq_raw -e tcp.stream -Y "tcp.flags==0x0002 or tcp.flags==0x00c2" | % {($_| ConvertFrom-Json).layers | ? {$null -ne $_}}
$ServerSYNPackets = & $TsharkPath -r $ServerPCAP -T ek -e _ws.col.Protocol -e ip.proto -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e tcp.flags -e tcp.seq_raw -e tcp.stream -Y "tcp.flags==0x0002 or tcp.flags==0x00c2" | % {($_| ConvertFrom-Json).layers | ? {$null -ne $_}}
$ClientSYNPackets | % {
$Client = $_
$ServerSYNPackets | ? tcp_seq_raw -eq $Client.tcp_seq_raw | Select @{L="ClientTCPStream"; E={($Client.tcp_stream)[0] -as [int]}}, @{L="ServerTCPStream"; E={($_.tcp_stream)[0] -as [int]}}, @{L="ClientTCPSeq";E={$Client.tcp_seq_raw}}, @{L="ServerTCPSeq";E={$_.tcp_seq_raw}}
} | Sort-Object -Unique -Property ClientTCPStream
}
What is sent to the output is an array of matched TCP flows including the TCP Sequence number of the initial SYN packet for that particular TCP session. Here is an example.
PS C:\> MapTCPStreams -ClientPCAP C:\temp\client1.pcapng -ServerPCAP C:\temp\server1.pcapng
ClientTCPStream ServerTCPStream ClientTCPSeq ServerTCPSeq
--------------- --------------- ------------ ------------
17 6 2013488348 2013488348
18 7 3235242357 3235242357
19 8 1593400366 1593400366
20 12 2405362910 2405362910
22 14 2485714300 2485714300
23 15 1884142422 1884142422
24 16 899349255 899349255
26 19 3367650905 3367650905
27 21 671080657 671080657
28 22 1804954561 1804954561
30 23 636807780 636807780
31 27 2246353286 2246353286
32 31 1585769473 1585769473
34 32 1503819969 1503819969
35 33 3450774594 3450774594
37 34 2452018063 2452018063
38 37 1229989904 1229989904
39 38 2512473281 2512473281
40 39 1316144821 1316144821
#truncated
We can see the TCP Stream at index 17 in the client PCAP file matches TCP Stream at index 6 in the server PCAP file, and the initial SYN
packet has a TCP Sequence number of 2013488348.
Let’s verify in Wireshark.
Looks good, both the IP Identification and TCP Sequence numbers match but they’re in different TCP streams within each capture. We can now analyse the TCP session from both ends of the connection side by side.
Moving on, let’s find TCP SYN packets in the client packet capture which aren’t present in the server capture. This may indicate packet loss or a firewall issue. If there are TCP Retransmits throughout the capture (not only SYN packets) this would indicate general packet loss, however, if only SYN packets seem to be missing this could indicate a firewall configuration issue possibly related to SYN flooding protection or a similar throttling mechanism. Here we’re only looking for SYN packets, general packet loss tends to be easy to spot in Wireshark.
The code below is mostly identical to the above, I could have combined everything into a single function and abstracted out the Tshark exports but the purpose here is to provide simple and clear examples which you can modify to suit your needs.
The notable difference apart from the logic to find missing packets is the updated Tshark export for the client side capture. In the previous example we were looking for matches but in this example we’re looking for the opposite - missing packets. This means we now need to provide an additional parameter for the remote Server IP and we want to only extract packets destined for that IP otherwise we could be returning SYN
packets destined for a completely different IP which obviously won’t be present in our server side capture.
There is no reason why this additional parameter could not have been included in the previous function and it would likely improve performance for capture files which aren’t already filtered to the required traffic flows but in this case omitting it could result in incorrect data being returned which is far worse.
function FindMissingSYNPackets {
[CmdletBinding()]
Param(
[Parameter(Mandatory=$false)]
[ValidateScript({Test-Path $_})]
[string]$TsharkPath = "C:\Program Files\Wireshark\tshark.exe",
[Parameter(Mandatory)]
[ValidateScript({Test-Path $_})]
[ValidateScript({(Get-Item $_).Extension -match "\.pcap$|\.pcapng$"})]
[string]$ClientPCAP,
[Parameter(Mandatory)]
[ValidateScript({Test-Path $_})]
[ValidateScript({(Get-Item $_).Extension -match "\.pcap$|\.pcapng$"})]
[string]$ServerPCAP,
[Parameter(Mandatory)]
[ValidateScript({ $_ -eq ([IPaddress]$_).IPAddressToString })]
[string]$ServerIP
)
$ClientSYNPackets = & $TsharkPath -r $ClientPCAP -T ek -e _ws.col.Protocol -e ip.proto -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e tcp.flags -e tcp.seq_raw -e tcp.stream -Y "ip.dst_host==$ServerIP and (tcp.flags==0x0002 or tcp.flags==0x00c2)" | % { ($_ | ConvertFrom-Json).layers | ? { $null -ne $_ } }
$ServerSYNPackets = & $TsharkPath -r $ServerPCAP -T ek -e _ws.col.Protocol -e ip.proto -e ip.src -e ip.dst -e tcp.srcport -e tcp.dstport -e tcp.flags -e tcp.seq_raw -e tcp.stream -Y "tcp.flags==0x0002 or tcp.flags==0x00c2" | % {($_| ConvertFrom-Json).layers | ? {$null -ne $_}}
$MissingSYNPackets = [System.Collections.Generic.List[Int64]]::new()
$ClientSYNPackets | % {
$SYNFound = $ServerSYNPackets.tcp_seq_raw -contains $_.tcp_seq_raw
if ($SYNFound -eq $false) {
$MissingSYNPackets.Add($_.tcp_seq_raw[0] -as [Int64])
}
}
$MissingSYNPackets | Group-Object -NoElement | Select Count, @{L="TCPSeqNo"; E={$_.Name}}
}
Let’s have a look at the output.
PS C:\> FindMissingSYNPackets -ClientPCAP C:\temp\client2.pcapng -ServerPCAP C:\temp\server2.pcapng
Count TCPSeqNo
----- --------
5 434253224
5 240224956
5 1524415270
5 2415391270
And let’s confirm in Wireshark.
There we have it, 5 SYN
packets sent from the client and none of them recorded in the server’s packet capture.
That’s all for this one, hope you got something useful out of it 👍