I’m a Senior Software Engineer living in Berlin. Shifting limits based on quality and robustness. Cutting-edge software development. Defining durable and flexible interfaces. Creating rich and intuitive user experiences.

Have a look at the 3-way TCP and the TLS handshake packets locally

Have you ever wanted to really understand what’s happening when you visit a secured website—down to each handshake and message exchanged? Here’s a developer-friendly, step-by-step lab that lets you stand up real HTTPS with your own CA, capture the raw TLS handshake, and dissect it packet-by-packet with modern CLI tools.

Generate a Local CA, Certificate, and Private Key

The first step to a realistic HTTPS lab is a certificate that your browser trusts. Rather than fighting with self-signed “unsafe” alerts, we’ll use mkcert, a simple tool that generates and installs a locally-trusted Certificate Authority (CA).

# Install dependencies
brew install mkcert
brew install nss # to support Firefox trust store
# Install the local CA in the system trust store so your browser will recognize its signatures
mkcert -install
mkcert -CAROOT # to see where your CA’s root cert and key are stored
# Create a wildcard cert and key for your domain (e.g., lab.local, *.lab.local)
# Note: X.509 wildcard certs only support one level of depth
mkdir ~/.mkcert
mkcert \
-cert-file ~/.mkcert/lab.local.crt \
-key-file ~/.mkcert/lab.local-key.pem \
"lab.local" "*.lab.local"
# Confirm your certificate contains all intended Subject Alternative Names (SANs)
openssl x509 -in ~/.mkcert/lab.local.crt -text -noout | grep -A 1 "Subject Alternative Name"
# Map lab.local and its subdomains to 127.0.0.1 for local development
echo "127.0.0.1 lab.local test.lab.local" | sudo tee -a /etc/hosts
# Double-check your hostnames resolve
ping -c 2 test.lab.local

If you need wildcard subdomains beyond /etc/hosts, set up dnsmasq for local DNS:

address=/.lab.local/127.0.0.1

This ensures any *.lab.local hostname points to your local machine.

Run a Simple Nginx Server on Port 443 with HTTPS

With your trusted cert and key ready, let’s create a minimal but functional Nginx HTTPS web server. We’ll use Docker to avoid polluting your host environment.

First, write a suitable nginx.conf:

cat <<EOF > ~/nginx.conf
events {}
http {
server {
listen 443 ssl;
server_name lab.local;
ssl_certificate /etc/nginx/certs/lab.local.crt;
ssl_certificate_key /etc/nginx/certs/lab.local-key.pem;
location / {
root /usr/share/nginx/html;
index index.html;
}
}
}
EOF

Now run the server—mounting both your config and your generated certs directory:

docker run --name nginx-https -p 443:443 \
-v ~/nginx.conf:/etc/nginx/nginx.conf:ro \
-v ~/.mkcert:/etc/nginx/certs:ro \
-d nginx
# (Optional) See what port is exposed
docker inspect --format='{{json .NetworkSettings.Ports}}' nginx-https | jq
# Test your HTTPS endpoint; you should see headers and no certificate warning
curl -I https://lab.local
# Clean up when finished
docker stop nginx-https
docker rm nginx-https
rm ~/nginx.conf

This configuration means any HTTPS request to lab.local on port 443 will be handled by nginx, using your trusted dev certificate for encryption.

Capture the TCP Traffic Using tcpdump

The next goal is to observe the raw network traffic, including both the TCP handshake and the start of TLS negotiation.

First, identify which interface represents your local network loopback (often lo0 on macOS, lo on Linux):

ifconfig -a

Then run tcpdump to show the traffic as it happens:

sudo tcpdump -i lo0 host lab.local and port 443
# Or save a copy to inspect later with Wireshark/tshark
sudo tcpdump -i lo0 host lab.local and port 443 -w lab-local-443.pcap

Now, by connecting to your server from a browser or curl, you’ll generate encrypted traffic—perfect for analysis!

Analyze the 3-way TCP and TLS Handshake Packets with tshark

With the capture file ready, it’s time for deep protocol inspection using tshark, the CLI-powered cousin of Wireshark. This is where all the magic becomes visible.

First, ensure tshark is installed and ready:

brew install wireshark
which tshark
tshark --version

1. Review the TCP 3-way handshake:

The handshake between client and server establishes a TCP connection:

echo -e "No.\tTime\t\tSource\t\tDestination\tProto\tLength\tInfo"
tshark -r lab-local-443.pcap -Y "tcp.stream==0" \
-T fields \
-e frame.number \
-e frame.time_relative \
-e ip.src \
-e ip.dst \
-e _ws.col.Protocol \
-e frame.len \
-e _ws.col.Info \
| head -n 3

Sample output:

No. Time Source Destination Proto Length Info
1 0.000000000 127.0.0.1 127.0.0.1 TCP 68 60630 → 443 [SYN] Seq=0 Win=65535 Len=0 MSS=16344 WS=64 TSval=775283515 TSecr=0 SACK_PERM
2 0.000285000 127.0.0.1 127.0.0.1 TCP 68 443 → 60630 [SYN, ACK] Seq=0 Ack=1 Win=65535 Len=0 MSS=16344 WS=64 TSval=4055471285 TSecr=775283515 SACK_PERM
3 0.000360000 127.0.0.1 127.0.0.1 TCP 56 60630 → 443 [ACK] Seq=1 Ack=1 Win=408320 Len=0 TSval=775283516 TSecr=4055471285

These are, respectively, the SYN, SYN-ACK, and ACK packets showing TCP successfully connecting your client and server.

2. Inspect the TLS handshake itself:

After TCP is established, tshark lets you follow the TLS handshake messages, revealing each phase of the session negotiation:

echo -e "No.\tTime\t\tSource\t\tDestination\tProto\tLength\tInfo"
tshark -r lab-local-443.pcap -Y "tcp.stream==0 && tls.handshake" \
-T fields \
-e frame.number \
-e frame.time_relative \
-e ip.src \
-e ip.dst \
-e _ws.col.Protocol \
-e frame.len \
-e _ws.col.Info

You might see:

No. Time Source Destination Proto Length Info
5 0.001147000 127.0.0.1 127.0.0.1 TLSv1 375 Client Hello (SNI=lab.local)
7 0.008839000 127.0.0.1 127.0.0.1 TLSv1.3 1697 Server Hello, Change Cipher Spec, Application Data, Application Data, Application Data, Application Data

This details the start of the encrypted negotiation. In modern TLS, most of the handshake moves into encrypted payloads (shown as Application Data).

Clean Up

Remove the packet capture to keep your system tidy:

rm lab-local-443.pcap

Conclusion

By walking through these steps, you’ve built a modern local HTTPS environment and unpacked the nitty-gritty of both TCP and TLS. You created your own CA, learned how browsers trust certificates, observed the mechanics of establishing encrypted connections, and witnessed the handshake as it truly occurs in the wild.

These skills are invaluable for debugging complex HTTPS issues, understanding how SSL/TLS works, and building your intuition about packets and protocols—all with just core open source tools. You can easily adapt this setup for more advanced experiments, from HTTP/2 over TLS to client certificates and beyond.

Happy packet hunting!