Build a QUIC Chat Server with Wireshark

Build a QUIC chat server and inspect encrypted packets with Wireshark.

Introduction

30 Second Summary

Every time you load a webpage, stream a video, or send a message, data travels between computers using a transport protocol. For decades that protocol was TCP, but a newer protocol called QUIC now carries most of Google, Cloudflare, and Meta's web traffic because it's faster and more secure by design.

In this project, you will build a QUIC-based chat server and client from scratch, then use Wireshark to decrypt and inspect the packets at the wire level.

What You'll Build

A local QUIC chat system where a server accepts client connections, echoes messages back over multiplexed streams, and logs encryption secrets so you can see exactly what happens inside each encrypted packet.

By the end of this project, you'll have:

  • A working QUIC echo server and client exchanging messages over the QUIC protocol.
  • Self-signed TLS certificates and a secrets log file that lets Wireshark decrypt QUIC's mandatory encryption.
  • A Wireshark packet capture showing decrypted handshake frames, stream data, and connection IDs.
  • Secret Mission: Add stream multiplexing where your client opens multiple simultaneous streams and observe how QUIC avoids head-of-line blocking.

Are there any prerequisites?

You should be comfortable with basic Python (functions and async/await concepts) and familiar with using a terminal. No prior networking knowledge is required.

Install Python and OpenSSL

Before you can build a QUIC server, you need the right tools on your machine. The aioquic library requires Python 3.10 or higher, and on Windows it needs OpenSSL headers to compile its cryptographic components.

In this step, you'll install Python, set up OpenSSL via Chocolatey, and create an isolated project environment with everything aioquic needs.

In this step, get ready to:
  • Install Python 3.10+ and verify it's on your PATH.
  • Install OpenSSL via Chocolatey so aioquic can build its TLS components.
  • Create a project directory with a virtual environment and install aioquic.
Install Python 3.10+

The aioquic library requires Python 3.10 or higher. Let's check whether you already have it installed.

  • Open PowerShell by pressing the Windows key, typing PowerShell, and pressing Enter.
  • Check your Python version by running this command:
python --version

✔️ I see version 3.10 or higher

Your Python version is ready to go. Move on to the next substep.

ⓧ I see an older version

You have Python installed, but it's older than 3.10. You need to install a newer version.

  • Go to python.org/downloads and download the latest Python 3 installer for Windows (64-bit).
  • Run the installer.
  • On the first page of the installer, check the box labelled Add Python to PATH.
  • Click Install Now and wait for the installation to complete.
  • Close and reopen PowerShell, then run python --version again to confirm you see 3.10 or higher.

Multiple Python versions

If python --version still shows the old version after installing, try python3 --version. The new installer sometimes registers as python3 instead.

If that shows the right version, use python3 in place of python for the rest of this project.

ⓧ Command not found

Python isn't installed on your system yet. Let's install it.

  • Go to python.org/downloads and download the latest Python 3 installer for Windows (64-bit).
  • Run the installer.
  • On the first page of the installer, check the box labelled Add Python to PATH.
  • Click Install Now and wait for the installation to complete.
  • Close and reopen PowerShell, then run python --version to confirm you see 3.10 or higher.

Why Add Python to PATH?

The PATH is a list of folders your operating system searches when you type a command. Without adding Python to PATH, typing python in PowerShell wouldn't find anything. Checking this box during install saves you from configuring it manually later.

Install OpenSSL via Chocolatey

The aioquic library depends on OpenSSL headers to build its TLS cryptography layer. On Windows, the easiest way to get OpenSSL is through Chocolatey, a package manager for Windows.

  • Open PowerShell as Administrator by pressing the Windows key, typing PowerShell, right-clicking Windows PowerShell, and selecting Run as administrator.
  • Install OpenSSL by running this command:
choco install openssl

What does this command do?

Chocolatey downloads and installs the OpenSSL library and its development headers to C:\Program Files\OpenSSL. The aioquic build process needs these headers to compile its cryptographic bindings.

  • When prompted to confirm the installation, type Y and press Enter.
  • After the install finishes, set the environment variables that tell aioquic where to find OpenSSL by running these commands:
$Env:INCLUDE = "C:\Progra~1\OpenSSL\include"
$Env:LIB = "C:\Progra~1\OpenSSL\lib"

What do these environment variables do?

The INCLUDE variable tells the compiler where to find OpenSSL's header files. The LIB variable tells it where to find the compiled libraries.

The path C:\Progra~1 is the short name for C:\Program Files. Using the short name avoids issues with spaces in the path.

Seeing 'choco' is not recognized?

This means Chocolatey is not installed on your system. Install it first by running this command in your Administrator PowerShell:

Set-ExecutionPolicy Bypass -Scope Process -Force; [System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072; iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))

After Chocolatey installs, close and reopen your Administrator PowerShell, then run choco install openssl again. Help me install Chocolatey on Windows.

Create a project directory and install aioquic

Now that your tools are installed, you'll create a dedicated folder for the project and set up a virtual environment to keep your dependencies isolated.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir quic-chat
cd quic-chat
  • Create a virtual environment by running:
python -m venv venv

What is a virtual environment?

A virtual environment is an isolated Python installation inside your project folder. Packages you install here won't affect your system Python.

Different projects can use different package versions without conflict. This keeps everything clean and reproducible.

  • Activate the virtual environment by running:
.\venv\Scripts\Activate.ps1

You should see (venv) appear at the beginning of your PowerShell prompt. This confirms the virtual environment is active.

  • Install aioquic by running:
pip install aioquic==1.3.0

What is aioquic?

The aioquic library is a Python implementation of the QUIC transport protocol (RFC 9000). It provides a high-level asyncio API to build QUIC servers and clients.

It also supports TLS secrets logging for Wireshark decryption, which you'll use later to inspect your packets.

  • Verify the installation by running:
pip show aioquic

You should see output including Name: aioquic and Version: 1.3.0. This confirms aioquic is installed and ready.

Installation failed with a build error?

If pip install aioquic==1.3.0 fails with errors about missing headers or compilation, double-check that you set the INCLUDE and LIB environment variables in this same PowerShell session.

The variables only persist for the current session. If you opened a new PowerShell window since setting them, run the two $Env: commands from the previous substep again before retrying the install. Help me fix aioquic installation errors on Windows.

✔️ Awesome, I've got everything!

Great. Double check you see (venv) in your prompt and Version: 1.3.0 in the pip output. You're all set.

ⓧ I'd like to double check the full setup

Here's a summary of what your environment should look like at this point:

  • PowerShell prompt starts with (venv).
  • Running python --version shows 3.10 or higher.
  • Running pip show aioquic shows Version: 1.3.0.
  • Your current directory is ~/Desktop/quic-chat.

Your environment is fully configured. Next up, you'll install Wireshark and generate the TLS certificates that your QUIC server needs for its encrypted handshake.

Install Wireshark and Generate TLS Certificates

Your QUIC server needs TLS certificates before it can accept any connections. QUIC mandates TLS 1.3 at the protocol level, so every connection starts with a cryptographic handshake.

You also need a way to see what is happening at the packet level. Wireshark is the industry-standard tool for inspecting network traffic, and you will use it later to decrypt and analyze your QUIC packets.

In this step, get ready to:
  • Install Wireshark for live packet capture.
  • Generate self-signed TLS certificates for your QUIC server.
  • Understand why QUIC mandates encryption at the transport layer.
Download and install Wireshark

Wireshark lets you capture and inspect individual network packets. You will use it to see your QUIC traffic at the byte level.

  • Open your browser and navigate to the Wireshark download page.
  • Download the Windows x64 Installer for version 4.6.6.
  • Run the installer.
  • Accept the default options on each screen.
  • When prompted to install Npcap, keep the checkbox selected and accept its defaults too.

What is Npcap?

Npcap is the Windows packet capture driver that Wireshark needs to intercept live network traffic. Without it, Wireshark cannot see any interfaces to capture from.

Npcap also provides a loopback adapter that lets you capture traffic between processes on the same machine. You will use this loopback adapter to capture your QUIC packets between the server and client running on localhost.

  • Once the installer finishes, open Wireshark by pressing Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Wireshark and press Enter to open it.

You should see the Wireshark start page with a list of capture interfaces. Look for an entry called Adapter for loopback traffic capture or Npcap Loopback Adapter in the list.

Not seeing any capture interfaces?

If Wireshark shows no interfaces, the Npcap service may not be running. Open an elevated Command Prompt and run sc start npcap to start the service, then relaunch Wireshark.

If that does not help, try uninstalling Npcap, rebooting, then reinstalling the latest Npcap from the official website.

Help me troubleshoot Wireshark not showing capture interfaces on Windows.

Generate a self-signed TLS certificate

QUIC requires a TLS certificate for every connection. You will create a self-signed certificate that your server presents during the handshake to prove its identity.

  • Switch back to your PowerShell terminal where your virtual environment is active.
  • Make sure you are inside the quic-chat folder.
  • Generate the certificate and private key by running this command:
openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost"

What does this command do?

  • req -x509 tells OpenSSL to generate a self-signed certificate (not a certificate signing request).
  • -newkey rsa:2048 creates a fresh 2048-bit RSA private key.
  • -keyout key.pem saves the private key to a file called key.pem.
  • -out cert.pem saves the certificate to a file called cert.pem.
  • -days 365 makes the certificate valid for one year.
  • -nodes skips encrypting the private key with a passphrase, so your server can read it without prompting.
  • -subj "/CN=localhost" sets the Common Name to localhost, which matches the hostname your client connects to.
  • Confirm the files were created by running:
ls cert.pem, key.pem

You should see both cert.pem and key.pem listed with today's date.

Seeing a path conversion error?

If you are running this command in Git Bash instead of PowerShell, the forward slash in the -subj argument gets converted into a Windows file path. The fix is to run the command in PowerShell, which does not perform this conversion.

If openssl is not recognized, make sure the OpenSSL environment variables from Step 1 are still set in your current terminal session.

Help me fix an OpenSSL error when generating certificates on Windows.

Understand why QUIC mandates TLS

Why does QUIC require TLS?

With TCP, encryption is optional. TLS is a separate layer bolted on top, which is why you can have unencrypted HTTP over TCP. This separation means an extra round trip for the TLS handshake on top of TCP's own handshake.

QUIC takes a different approach. TLS 1.3 is baked directly into the protocol itself. Every QUIC connection is encrypted from the very first packet.

The certificate you just created is what the server presents during that handshake to prove its identity to connecting clients. Because TLS is integrated, QUIC can complete its handshake in fewer round trips than TCP+TLS.

You now have Wireshark ready for packet capture and TLS certificates ready for your QUIC server. Next up, you will write the server code that uses these certificates to accept encrypted QUIC connections.

Build the QUIC Echo Server

Your TLS certificates are ready and your tools are installed. Now it's time to write the server that will accept QUIC connections and respond to messages.

The server listens for incoming QUIC connections on a UDP port. When a client sends a message on a stream, the server reads it, prints it to the terminal, and echoes it back with a prefix.

In this step, get ready to:
  • Create the server file and define an echo protocol class.
  • Handle incoming stream data and echo messages back to clients.
  • Configure TLS, ALPN, and secrets logging for Wireshark decryption.
Create the server file

The aioquic library uses an event-driven model. You subclass a protocol and override a method that fires every time something happens on the connection. This keeps the server lightweight and asynchronous.

  • In your code editor, create a new file called server.py inside your quic-chat folder.

What is a protocol class in aioquic?

In aioquic, you create a server by subclassing QuicConnectionProtocol. The library calls your quic_event_received() method whenever a QUIC event occurs (like data arriving on a stream, or a handshake completing).

This is similar to how a web framework calls your route handler when a request arrives. You define the behaviour, and the framework handles the plumbing.

Implement the echo protocol
  • Add the following code to server.py to define the imports and the echo protocol class:
import asyncio
import ssl

from aioquic.asyncio import QuicConnectionProtocol, serve
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.events import QuicEvent, StreamDataReceived


class EchoServerProtocol(QuicConnectionProtocol):
    def quic_event_received(self, event: QuicEvent) -> None:
        if isinstance(event, StreamDataReceived):
            message = event.data.decode("utf-8")
            stream_id = event.stream_id
            print(f"[Server] Received on stream {stream_id}: {message}")

            response = f"Echo: {message}"
            self._quic.send_stream_data(
                stream_id, response.encode("utf-8"), end_stream=event.end_stream
            )
            self.transmit()

What does this code do?

  • The imports bring in QuicConnectionProtocol (the base class for your server), serve (to start listening), and the event types we need to handle.
  • EchoServerProtocol subclasses QuicConnectionProtocol and overrides quic_event_received() to react when data arrives.
  • When a StreamDataReceived event fires, the handler decodes the bytes into a string, prints it, and sends back a response prefixed with Echo: .
  • self._quic.send_stream_data() queues the response on the same stream the client used. self.transmit() flushes the queued data onto the wire.

You won't see a change yet. The next chunk adds the server startup logic that makes this protocol run.

Configure the server and start listening

The protocol class defines behaviour, but you still need to tell aioquic where to listen, which certificates to use, and where to log TLS secrets for Wireshark.

  • Add the following code below your EchoServerProtocol class in server.py:
async def main():
    configuration = QuicConfiguration(
        is_client=False,
        alpn_protocols=["echo"],
    )
    configuration.load_cert_chain(certfile="cert.pem", keyfile="key.pem")

    # Log TLS secrets for Wireshark decryption
    configuration.secrets_log_file = open("secrets.log", "a")

    print("[Server] Starting QUIC echo server on localhost:4433...")
    await serve(
        host="localhost",
        port=4433,
        configuration=configuration,
        create_protocol=EchoServerProtocol,
    )
    await asyncio.Future()  # Run forever


if __name__ == "__main__":
    asyncio.run(main())

What does this code do?

  • QuicConfiguration(is_client=False) sets up server-mode QUIC settings. alpn_protocols=["echo"] tells the TLS handshake that this server speaks the "echo" application protocol.
  • load_cert_chain() loads the TLS certificate and private key you generated in the previous step. QUIC requires TLS 1.3 for every connection.
  • secrets_log_file writes TLS session keys to secrets.log in the SSLKEYLOGFILE format. Wireshark reads this file to decrypt your QUIC packets.
  • serve() binds to localhost:4433 over UDP and uses your EchoServerProtocol class for each new connection.
  • await asyncio.Future() keeps the server running indefinitely until you press Ctrl+C.

Why ALPN protocols?

ALPN (Application-Layer Protocol Negotiation) lets the client and server agree on which application protocol to use during the TLS handshake. Both sides must list the same protocol name. In this project, both will use "echo" so they can find each other.

In production, HTTP/3 servers advertise "h3" as their ALPN protocol. Your custom protocol uses "echo" to keep things simple.

  • Save server.py.
  • Test that the server starts by running this command in your terminal (with your virtual environment activated):
python server.py
  • You should see [Server] Starting QUIC echo server on localhost:4433... printed in the terminal. The server is now listening for QUIC connections.
  • Press Ctrl+C to stop the server for now.

Server won't start?

  • If you see ModuleNotFoundError: No module named 'aioquic', your virtual environment is not activated. Run .\venv\Scripts\Activate.ps1 first.
  • If you see FileNotFoundError for cert.pem or key.pem, make sure you are running the command from inside the quic-chat folder where you generated the certificates.
  • If you see a SyntaxError, double-check that your indentation uses spaces (not tabs) and that the f"" strings have matching quotes.

Help me debug my QUIC server startup error.

✔️ Awesome, I've got everything!

Great. Double check you've saved server.py and confirmed it starts without errors.

ⓧ I'd like to double check the full code

Here is the complete server.py file:

import asyncio
import ssl

from aioquic.asyncio import QuicConnectionProtocol, serve
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.events import QuicEvent, StreamDataReceived


class EchoServerProtocol(QuicConnectionProtocol):
    def quic_event_received(self, event: QuicEvent) -> None:
        if isinstance(event, StreamDataReceived):
            message = event.data.decode("utf-8")
            stream_id = event.stream_id
            print(f"[Server] Received on stream {stream_id}: {message}")

            response = f"Echo: {message}"
            self._quic.send_stream_data(
                stream_id, response.encode("utf-8"), end_stream=event.end_stream
            )
            self.transmit()


async def main():
    configuration = QuicConfiguration(
        is_client=False,
        alpn_protocols=["echo"],
    )
    configuration.load_cert_chain(certfile="cert.pem", keyfile="key.pem")

    # Log TLS secrets for Wireshark decryption
    configuration.secrets_log_file = open("secrets.log", "a")

    print("[Server] Starting QUIC echo server on localhost:4433...")
    await serve(
        host="localhost",
        port=4433,
        configuration=configuration,
        create_protocol=EchoServerProtocol,
    )
    await asyncio.Future()  # Run forever


if __name__ == "__main__":
    asyncio.run(main())

Your QUIC server is ready to accept connections. Next up, you'll build the client that connects to this server, sends a message, and reads the echoed response.

Build the QUIC Client

Your server is ready and waiting for connections on port 4433. Now you need a client that speaks the same QUIC protocol, connects to the server, sends a message, and reads back the echo response.

The client mirrors the server's structure. It has its own protocol class for handling incoming events and a main() function that manages the connection lifecycle. The key difference is that the client initiates the connection and opens a stream to start the conversation.

In this step, get ready to:
  • Create a client protocol class that handles server responses.
  • Write connection logic that opens a stream, sends a message, and reads the echo.
  • Configure TLS secrets logging so Wireshark can decrypt client-side traffic.
Create the client protocol handler

Just like the server, the client needs a protocol class to handle QUIC events. When the server sends data back, the client protocol class receives it and resolves a waiting future so the main function can read the response.

  • Create a new file called client.py in your quic-chat folder and add the following imports and protocol class:
import asyncio
import ssl

from aioquic.asyncio import QuicConnectionProtocol, connect
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.events import QuicEvent, StreamDataReceived


class EchoClientProtocol(QuicConnectionProtocol):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._response_waiter = None

    def quic_event_received(self, event: QuicEvent) -> None:
        if isinstance(event, StreamDataReceived):
            message = event.data.decode("utf-8")
            print(f"[Client] Received: {message}")
            if self._response_waiter and not self._response_waiter.done():
                self._response_waiter.set_result(message)

What does this code do?

The imports bring in connect (instead of serve which the server used) because this side initiates the connection.

  • self._response_waiter is a placeholder for a future that the main function will set before reading a response.
  • quic_event_received() fires when data arrives from the server. It decodes the bytes, prints the message, and resolves the waiting future so the caller knows the response is ready.
  • Save client.py.
  • You won't see a visible change yet. The next chunk adds the logic that actually connects to the server.

Seeing a red underline on the imports?

Make sure your virtual environment is activated in the terminal. If your editor shows import errors, it might be using a different Python interpreter. Select the interpreter inside your quic-chat\venv folder.

Help me fix my Python interpreter so aioquic imports resolve

Write the connection and messaging logic

Now add the main() function that configures the QUIC connection, connects to the server, opens a bidirectional stream, and sends a message.

  • Add the following code below your EchoClientProtocol class in client.py:
async def main():
    configuration = QuicConfiguration(
        is_client=True,
        alpn_protocols=["echo"],
    )
    configuration.verify_mode = ssl.CERT_NONE

    # Log TLS secrets for Wireshark decryption
    configuration.secrets_log_file = open("secrets.log", "a")

    print("[Client] Connecting to localhost:4433...")
    async with connect(
        host="localhost",
        port=4433,
        configuration=configuration,
        create_protocol=EchoClientProtocol,
    ) as client:
        print("[Client] Connected! Opening stream...")

        reader, writer = await client.create_stream()

        message = "Hello, QUIC!"
        print(f"[Client] Sending: {message}")
        writer.write(message.encode("utf-8"))
        writer.write_eof()

        response = await reader.read()
        print(f"[Client] Got response: {response.decode('utf-8')}")

    print("[Client] Connection closed.")


if __name__ == "__main__":
    asyncio.run(main())

What does this code do?

  • QuicConfiguration(is_client=True) tells aioquic this is a client connection. The alpn_protocols=["echo"] must match the server's ALPN setting so both sides agree on the application protocol.
  • verify_mode = ssl.CERT_NONE disables certificate verification because you're using a self-signed certificate. In production, you would verify the server's certificate against a trusted authority.
  • secrets_log_file writes TLS secrets to the same secrets.log file the server uses. Wireshark reads this file to decrypt QUIC packets from both sides.
  • client.create_stream() opens a bidirectional stream and returns a reader/writer pair. The client writes the message, calls write_eof() to signal the end of the message, then reads the server's response.

Why does the client skip loading a certificate?

The server calls load_cert_chain() because it needs to prove its identity during the TLS 1.3 handshake. The client does not present a certificate. It only verifies the server's identity (or in this case, skips verification with CERT_NONE since the certificate is self-signed).

  • Save client.py.
  • Confirm the file exists by running:
ls client.py
  • You should see client.py listed in the output.

File not found?

Make sure your terminal is in the quic-chat directory. Run cd quic-chat if you navigated somewhere else. Also double check you saved the file as client.py (not client.py.txt or similar).

Help me find my client.py file

Double check the full code

✔️ Awesome, I've got everything!

Great. Double check you've saved client.py in your quic-chat folder.

ⓧ I'd like to double check the full code

Here is the complete client.py file. Compare it with yours:

import asyncio
import ssl

from aioquic.asyncio import QuicConnectionProtocol, connect
from aioquic.quic.configuration import QuicConfiguration
from aioquic.quic.events import QuicEvent, StreamDataReceived


class EchoClientProtocol(QuicConnectionProtocol):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._response_waiter = None

    def quic_event_received(self, event: QuicEvent) -> None:
        if isinstance(event, StreamDataReceived):
            message = event.data.decode("utf-8")
            print(f"[Client] Received: {message}")
            if self._response_waiter and not self._response_waiter.done():
                self._response_waiter.set_result(message)


async def main():
    configuration = QuicConfiguration(
        is_client=True,
        alpn_protocols=["echo"],
    )
    configuration.verify_mode = ssl.CERT_NONE

    # Log TLS secrets for Wireshark decryption
    configuration.secrets_log_file = open("secrets.log", "a")

    print("[Client] Connecting to localhost:4433...")
    async with connect(
        host="localhost",
        port=4433,
        configuration=configuration,
        create_protocol=EchoClientProtocol,
    ) as client:
        print("[Client] Connected! Opening stream...")

        reader, writer = await client.create_stream()

        message = "Hello, QUIC!"
        print(f"[Client] Sending: {message}")
        writer.write(message.encode("utf-8"))
        writer.write_eof()

        response = await reader.read()
        print(f"[Client] Got response: {response.decode('utf-8')}")

    print("[Client] Connection closed.")


if __name__ == "__main__":
    asyncio.run(main())

Your client is ready to connect. In the next step, you'll run both the server and client together and capture the live QUIC traffic in Wireshark.

Run the Chat System and Capture Packets

Your server and client code are ready. Now it is time to see QUIC in action by running both sides of the conversation while Wireshark captures every packet on the wire.

You will need two terminal windows for this step. One runs the server (listening for connections), and the other runs the client (sending a message). Meanwhile, Wireshark captures every UDP datagram exchanged between them.

In this step, get ready to:
  • Start a Wireshark capture on the loopback adapter.
  • Run the QUIC server and client to exchange messages.
  • Verify QUIC packets appear in Wireshark.
Start a Wireshark capture on the loopback adapter

QUIC sends all its traffic over UDP. To capture localhost traffic on Windows, Wireshark uses a special virtual interface provided by Npcap.

  • Press the Windows key to open your search bar.
  • Type Wireshark and press Enter to open it.
  • On the Welcome Screen, find the interface called Adapter for loopback traffic capture in the interface list.
  • Double-click Adapter for loopback traffic capture to begin capturing packets.

You should see the packet list start populating (it may be empty for now since no QUIC traffic is flowing yet).

  • In the display filter bar at the top of the Wireshark window, type the following filter and press Enter:
udp.port == 4433

What does this filter do?

This is a display filter that tells Wireshark to only show packets traveling to or from UDP port 4433. Your QUIC server listens on that port, so this filters out all unrelated traffic and shows only your QUIC packets.

The packet list should now be empty, waiting for QUIC traffic to appear.

Don't see the loopback adapter?

The interface may also appear as Npcap Loopback Adapter on some systems. Look for any interface with "loopback" in its name.

If no loopback interface appears at all, Npcap may not have installed correctly. Reinstall Wireshark and make sure you accept the Npcap component during installation.

help me find the loopback capture interface in Wireshark

Run the server and client

With Wireshark capturing in the background, you will now start the server in one terminal and the client in another. The server waits for connections, and the client sends a message and receives the echo response.

  • Switch back to your terminal where the virtual environment is already activated.
  • Start the server by running this command:
python server.py

You should see this output in the terminal:

[Server] Starting QUIC echo server on localhost:4433...

What is the server doing?

The server is now listening on UDP port 4433, waiting for incoming QUIC connections. It will keep running until you stop it manually.

  • Open a second terminal window. Press the Windows key, type PowerShell, and press Enter.
  • Navigate to your project folder and activate the virtual environment by running:
cd ~/Desktop/quic-chat
.\venv\Scripts\Activate.ps1
  • Run the client by running this command:
python client.py

You should see the full message exchange in the client terminal:

[Client] Connecting to localhost:4433...
[Client] Connected! Opening stream...
[Client] Sending: Hello, QUIC!
[Client] Received: Echo: Hello, QUIC!
[Client] Got response: Echo: Hello, QUIC!
[Client] Connection closed.

In the server terminal, you should also see:

[Server] Received on stream 0: Hello, QUIC!

What just happened?

The client connected to the server over QUIC, opened a bidirectional stream, sent the message "Hello, QUIC!", and received the echoed response. All of this happened over a single encrypted UDP connection.

Both the server and client wrote their TLS session secrets into secrets.log as the handshake completed. You will use this file in the next step to decrypt the captured packets in Wireshark.

Seeing a connection error?

Make sure the server is still running in the first terminal before you start the client. If you see an error about the port being in use, another process is occupying port 4433. Close it and try again.

If you see an import error mentioning aioquic, make sure the virtual environment is activated in both terminals. You should see (venv) at the beginning of your terminal prompt.

help me debug my QUIC server or client connection error

Stop the capture and verify QUIC packets

The message exchange is complete. Now you will stop the Wireshark capture and confirm that QUIC packets were recorded.

  • Switch back to Wireshark.
  • Click Capture in the menu bar, then click Stop.

You should now see a list of captured packets in the main window. Look at the Protocol column. The packets should show QUIC as their protocol because Wireshark recognizes the QUIC header format.

  • Confirm that secrets.log was created in your project folder by running this in your second terminal:
ls secrets.log

You should see secrets.log listed in the output. This file contains the TLS session keys that Wireshark needs to decrypt your QUIC traffic.

Why are the packets labeled QUIC?

Even though QUIC runs over UDP, Wireshark can identify QUIC packets by their distinctive header structure. The first byte of a QUIC packet contains a header form bit and version information that Wireshark uses to classify the traffic.

The packet contents are still encrypted at this point. You cannot read the actual stream data yet. In the next step, you will load secrets.log into Wireshark to decrypt everything.

No packets showing in Wireshark?

Make sure you selected the loopback adapter (not your Wi-Fi or Ethernet interface). QUIC traffic between localhost and localhost only appears on the loopback interface.

Check that your display filter is udp.port == 4433 with no typos. Try clearing the filter to see if any packets were captured at all.

help me troubleshoot missing packets in my Wireshark QUIC capture

Your QUIC chat system is working and you have the raw packets captured. Next up, you will decrypt those packets and examine the QUIC handshake, connection IDs, and stream frames at the byte level.

Decrypt and Analyze QUIC Packets in Wireshark

You captured live QUIC traffic in the last step, and you can see UDP packets labeled as QUIC in the Protocol column.

The packet contents are still hidden behind encryption. To peek inside the handshake and see your actual chat messages, you need to feed Wireshark the TLS secrets that your server and client logged during the connection.

In this step, get ready to:
  • Load TLS secrets into Wireshark to decrypt QUIC packets.
  • Examine the QUIC handshake and identify Connection IDs.
  • Find STREAM frames containing your chat messages.
Load TLS secrets into Wireshark

QUIC encrypts everything by default. Without the session keys, Wireshark can only read the packet headers. The secrets.log file your server and client wrote contains the per-session TLS secrets that unlock the encrypted frames.

  • Switch back to Wireshark with your capture still open.
  • Click Edit in the top menu bar.
  • Click Preferences.
  • In the left-hand tree, expand Protocols.
  • Scroll down and select TLS.
  • In the right panel, find the field labeled (Pre)-Master-Secret log filename.
  • Click Browse and navigate to your quic-chat folder on your Desktop.
  • Select secrets.log and click Open.
  • Click OK to close Preferences.

Your packet list should immediately refresh. Packets that previously showed only encrypted payloads now display decrypted QUIC frame types in the Info column.

Why does this work?

When you set secrets_log_file in your Python code, aioquic wrote TLS session keys to secrets.log in the standard SSLKEYLOGFILE format.

Wireshark reads those keys and uses them to decrypt each QUIC packet's payload. This is the same mechanism browsers use when you set the SSLKEYLOGFILE environment variable.

Packets still showing as encrypted?

Make sure you selected the correct secrets.log file inside your quic-chat folder. If the file is empty, re-run the server and client from Step 5 to regenerate it.

Confirm that you expanded Protocols and selected TLS (not the top-level Preferences page). The field only appears under the TLS protocol settings.

Why aren't my QUIC packets decrypting after loading secrets.log?

Examine the QUIC handshake

Now that your packets are decrypted, you can see the TLS 1.3 handshake that QUIC performs inside its own transport layer. In TCP, this handshake happens separately on top of the connection. In QUIC, it is woven directly into the first packets.

  • In the packet list, look for the first packet with a long Info line that mentions Initial. This is QUIC's Initial packet type, which uses a long header.
  • Click that packet to select it.
  • In the packet details pane (middle section), expand the QUIC layer.
  • Expand the QUIC Connection information section. Notice the Destination Connection ID and Source Connection ID fields.
  • Look for CRYPTO frames inside the packet. Expand them to find the TLS 1.3 ClientHello message embedded directly in the QUIC packet.
  • Find the server's response packet (also an Initial or Handshake type). Expand its CRYPTO frame to see the ServerHello message.

What are Connection IDs?

TCP identifies a connection by the combination of source IP, source port, destination IP, and destination port. If any of those change (like switching from Wi-Fi to mobile data), the connection breaks.

QUIC uses Connection IDs instead. Each endpoint picks its own ID, and packets carry it in the header. If your IP address changes, the Connection ID stays the same, and the connection survives. This is what enables connection migration in QUIC.

Add Connection ID as a column

Right-click the Destination Connection ID field in the packet details pane and select Apply as Column. This adds the Connection ID to your packet list, making it easy to track which packets belong to the same QUIC connection.

Why is TLS inside QUIC packets?

With TCP, TLS is a separate layer added on top. The TCP handshake happens first (1 round trip), then the TLS handshake happens (another 1-2 round trips). QUIC embeds the TLS 1.3 handshake into its own Initial packets, completing both transport setup and encryption in a single round trip.

Find your stream data

The handshake proved how QUIC bakes TLS into the protocol. Now find your actual chat messages flowing through the encrypted tunnel.

  • Scroll through the packet list and look for packets whose Info column mentions STREAM. These are the frames that carry application data.
  • Click on a STREAM packet to select it.
  • In the packet details pane, expand the QUIC layer and find the STREAM frame.
  • Expand the STREAM frame. You should see a Stream ID field and the decoded stream data containing your message.
  • Look for the string Hello, QUIC! in the data payload. This is the message your client sent.
  • Find the server's response frame containing Echo: Hello, QUIC! on the same Stream ID.

What is stream multiplexing?

Each STREAM frame has a unique Stream ID. In QUIC, multiple streams can run independently inside a single connection. If a packet carrying data for Stream 0 is lost, only Stream 0 stalls while waiting for retransmission. Other streams keep flowing.

TCP cannot do this. In TCP, a single lost packet blocks everything behind it in the byte sequence, even if that data belongs to a completely unrelated request. This is called head-of-line blocking. QUIC's independent streams eliminate this problem entirely.

Secret mission

Stream Multiplexing in Action

Your current client opens just one stream per connection. Real QUIC applications open many streams simultaneously, and each one flows independently. In this secret mission, you'll modify the client to open 3 concurrent streams and watch them interleave in Wireshark without blocking each other.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources available, pause them to come back later, or delete them entirely.

Resources you used:

  • The quic-chat project folder (contains server.py, client.py, cert.pem, key.pem, secrets.log, and the venv virtual environment).
  • Wireshark 4.6.6 (installed with Npcap).
  • OpenSSL (installed via Chocolatey).

Keep everything running

No action needed. Choose this if you plan to continue experimenting with QUIC, adding new features, or running more Wireshark captures.

  • Your quic-chat folder stays on your Desktop with all files intact.
  • Wireshark and OpenSSL remain installed and available for other networking projects.
  • The virtual environment stays deactivated until you reactivate it with .\venv\Scripts\Activate.ps1.

Pause - I'll come back to this later

Shut down any running processes to free up resources. Your files and installations stay put for when you return.

  • If the server is still running in a terminal, press Ctrl+C to stop it.
  • Deactivate your virtual environment by running deactivate in the terminal.
  • Close any open Wireshark capture windows.

Delete - I don't want to use this again

Remove all project resources and installed tools to start completely fresh.

  • Stop the server if it is still running by pressing Ctrl+C in its terminal.
  • Delete the project folder. On macOS/Linux, run:
rm -rf ~/Desktop/quic-chat

On Windows (PowerShell), run:

Remove-Item -Recurse -Force ~\Desktop\quic-chat
  • Uninstall OpenSSL via Chocolatey. Open PowerShell as Administrator and run:
choco uninstall openssl
  • Uninstall Wireshark. Open Settings, select Apps, then Installed apps.
  • Find Wireshark in the list, click the three-dot menu, and select Uninstall.
  • Find Npcap in the list and uninstall it separately (it is not removed with Wireshark).

Nice Work!

Nice Work!

You've built a working QUIC transport system from scratch and seen its internals at the packet level. You now understand how modern internet protocols like HTTP/3 deliver speed, security, and resilience in a single layer.

You've learned how to:

  • Built a QUIC echo server and client using aioquic's asyncio API that exchange messages over encrypted, multiplexed streams.
  • Generated TLS certificates for QUIC's mandatory TLS 1.3 handshake and logged encryption secrets for packet analysis.
  • Captured and decrypted live QUIC packets in Wireshark, identifying Connection IDs, CRYPTO frames, and STREAM frames to understand how QUIC differs from TCP.
  • Added multi-stream communication to see QUIC's head-of-line blocking avoidance in action.

Ready to quiz yourself?