Matching TCP Streams Between Client And Server Wireshark Packet Captures Using Powershell

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.

Wireshark TCP Session

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.

TCP 3 Way Handshake

But that’s not always the case, in Wireshark a TCP Stream can also be SYN packets with no response.

Unanswered SYN packets

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.

  1. IP Identification Field
  2. TCP Sequence Number

Unique Packet Identifiers

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.

TCP sessions with a proxy server

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)

  1. The path to TShark (Optional). Tshark is installed by default when you install Wireshark. If no path is required it will assume the default
  2. The path to the client side packet capture
  3. 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.

Matching TCP Steams 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.

Missing SYN Packets 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 👍


If you enjoyed this post consider sharing it on , , , or , and .