HTTP/2 and QUIC
HTTP/2 replaced HTTP/1.1 as the dominant web protocol and runs exclusively over TLS (except in very unusual deployments). QUIC is Google's protocol that combines TLS 1.3 and HTTP/3 over UDP, replacing TCP for a growing fraction of internet traffic. Both protocols present challenges for network forensics: they multiplex many requests over a single connection, use binary framing instead of text, and are increasingly difficult to inspect at the network level. Understanding their structure is necessary to extract meaningful forensic evidence.
You're analyzing an attacker's C2 communication that uses HTTP/2 over TLS to blend with Google Chrome traffic. The attacker's implant opens one persistent HTTP/2 connection and multiplexes check-in frames (HEADERS + DATA frames) over that single connection. Unlike HTTP/1.1 where each request creates a new TCP connection, this single connection carries all C2 traffic. In PCAP you see: one TLS connection to an IP, then sustained TCP session with regular DATA frames — the multiplexed C2 stream looks like a single TLS session to a legitimate service.
HTTP/2 Structure for Forensics
HTTP/2 Frame Structure ═══════════════════════════════════════════════════════════════════ One TCP connection can carry many HTTP/2 streams. Frame header (9 bytes per frame): ├── Length (3 bytes): payload length ├── Type (1 byte): HEADERS=1, DATA=0, SETTINGS=4, PUSH_PROMISE=5 │ RST_STREAM=3, GOAWAY=7, WINDOW_UPDATE=8 ├── Flags (1 byte): END_STREAM=0x1, END_HEADERS=0x4 └── Stream ID (4 bytes): odd=client, even=server, 0=connection-level Forensic-relevant frame types: ├── HEADERS (type=1): HTTP request/response headers │ Compressed with HPACK — Wireshark decompresses automatically │ Key fields: :method, :path, :authority, :status, user-agent └── DATA (type=0): HTTP body (request/response payload) HTTP/2 vs HTTP/1.1 forensics: ├── HTTP/1.1: one request per TCP stream → easy to follow per-request ├── HTTP/2: many requests interleaved → filter by stream ID └── Both: SNI visible in TLS ClientHello regardless of HTTP version QUIC (HTTP/3): ├── UDP port 443 (not TCP) ├── TLS 1.3 handshake embedded in QUIC Initial packets ├── SNI visible in QUIC ClientHello (CRYPTO frames in Initial) ├── After handshake: application data fully encrypted └── Wireshark dissects QUIC with SSLKEYLOGFILE
HTTP/2 Analysis with tshark
#!/bin/bash
PCAP="$1"
# If TLS is decryptable, set SSLKEYLOGFILE environment variable before capture
echo "=== HTTP/2 connections (TLS + HTTP/2 ALPN) ==="
tshark -r "$PCAP" -n \
-Y "tls.handshake.extensions_alpn_str == h2" \
-T fields \
-E separator="\t" \
-e ip.src -e ip.dst -e tcp.dstport \
-e tls.handshake.extensions_server_name \
| sort | uniq -c | sort -rn | head -20
echo ""
echo "=== QUIC connections (UDP 443) ==="
tshark -r "$PCAP" -n -Y "udp.dstport==443 or udp.srcport==443" \
-T fields \
-E separator="\t" \
-e ip.src -e ip.dst \
-e quic.version \
| sort | uniq -c | sort -rn | head -20
# If decryption keys are available
if [ -f "$2" ]; then
KEYLOG="$2"
echo ""
echo "=== HTTP/2 request paths (after TLS decryption) ==="
tshark -r "$PCAP" -n \
-o "tls.keylog_file:${KEYLOG}" \
-Y "http2.headers" \
-T fields \
-E separator="\t" \
-e frame.time_epoch \
-e ip.src \
-e ip.dst \
-e http2.headers.method \
-e http2.headers.path \
-e http2.headers.authority \
-e http2.headers.user_agent \
| head -50
fi
echo ""
echo "=== HTTP/2 stream data sizes (large DATA frames = potential exfil) ==="
tshark -r "$PCAP" -n -Y "http2.type==0 and http2.length > 10000" \
-T fields \
-E separator="\t" \
-e frame.time_epoch -e ip.src -e ip.dst -e http2.streamid -e http2.length \
| head -20
In HTTP/1.1, "one TCP connection = one HTTP request" (with pipelining being a rare exception). Every time you follow a TCP stream in Wireshark, you get one HTTP request/response pair. In HTTP/2, "one TCP connection = many HTTP requests" — a browser opens one connection to a server and multiplexes dozens of parallel requests over it, each with a different stream ID. For forensics: when you follow a TCP stream in an HTTP/2 capture, you see all the multiplexed frames interleaved. To analyze a specific HTTP/2 request, you need to filter by stream ID: http2.streamid == 7. This makes Wireshark's "Follow Stream" less useful for HTTP/2 — use the HTTP/2 statistics and filter by stream ID instead. tshark handles this better with the -e http2.headers.path field extraction approach.
Q & A
Q: 30% of our network traffic is QUIC (UDP 443). Our current detection rules only look at TCP. What do I lose?
For QUIC traffic you lose: full packet payload inspection (application data is encrypted with TLS 1.3 from the start), HTTP method/path/header visibility (all encrypted in QUIC application data), and the ability to use most HTTP-specific signatures. What you retain for QUIC: source/destination IP and port, the SNI in the QUIC ClientHello (CRYPTO frames in Initial packets, visible with Wireshark's QUIC dissector), certificate SANs (visible during the handshake), JA3-equivalent fingerprinting (QUIC version, cipher suites), and connection-level metadata (packet sizes, timing, frequency). Practical recommendation: (1) Enable QUIC dissection in Zeek (zeek-quic package) to get SNI and connection metadata in logs, (2) Block QUIC from internal hosts to unknown IPs at the firewall — this forces clients to fall back to TLS/TCP where you have better visibility, (3) Use JARM fingerprinting on the QUIC TLS layer to fingerprint C2 servers using QUIC.