Débogage HTTP/3 et QUIC : 5 modèles de production de la capture de paquets à l'analyse de performance

网络协议

Pourquoi le débogage HTTP/3 est 10x plus difficile que HTTP/2

HTTP/3 fonctionne sur QUIC via UDP, ce qui signifie que 20 ans d'expérience en débogage TCP sont essentiellement inutiles. Les paquets TCP sont directement lisibles dans Wireshark, les flux HTTP/2 sont visibles dans Chrome DevTools — mais le trafic QUIC est chiffré, rendant les paquets illisibles ; le multiplexage de flux HTTP/3 se produit au niveau transport, invisible aux outils de couche applicative.

En 2026, l'adoption mondiale d'HTTP/3 dépasse 45 % (données de Cloudflare Radar). Pourtant, lorsque les développeurs rencontrent des problèmes, ils ne voient souvent rien de plus que « connection failed » sans moyen d'enquêter. Cet article résume 5 modèles de débogage validés en production, de la capture décryptée à la surveillance en temps réel, vous aidant à construire une boîte à outils de débogage HTTP/3 complète.

Dimension de débogage Outil HTTP/2 Outil HTTP/3 Changement de difficulté
Capture de paquets Wireshark (lisible directement) Wireshark + SSLKEYLOG Nécessite le décryptage des clés
Journalisation de protocole Pas de standard qlog (format standardisé) Nouveau concept
Débogage client Chrome DevTools Chrome NetLog Niveau plus bas
Débogage CLI curl -v curl --http3 + variables d'environnement Nécessite compilation
Monitoring de production Métriques TCP Métriques spécifiques QUIC Système de métriques différent

Modèle 1 : Capture et dissection de paquets QUIC dans Wireshark

Problème central : QUIC est entièrement chiffré

QUIC intègre TLS 1.3 directement dans le protocole. Toutes les trames, y compris les en-têtes, sont chiffrées. Les captures QUIC dans Wireshark ne montrent que des payloads UDP — les trames HTTP/3 internes ne peuvent pas être analysées. Pour décrypter le trafic QUIC, vous devez obtenir les clés de session TLS.

Mécanisme SSLKEYLOGFILE

SSLKEYLOGFILE est le mécanisme standard de débogage TLS. Les clients qui le prennent en charge (Chrome, Firefox, curl, Go) écrivent les clés de session TLS dans un fichier spécifié. Wireshark lit ce fichier pour décrypter le trafic.

# Définir la variable d'environnement SSLKEYLOGFILE
export SSLKEYLOGFILE=/tmp/sslkeys.log

# Capturer le trafic HTTP/3 avec curl (clés écrites automatiquement)
curl --http3 https://example.com -v

# Accéder avec Chrome (clés écrites automatiquement)
google-chrome --ssl-key-log-file=/tmp/sslkeys.log

# Accéder avec Firefox
export MOZ_LOG="ssl:5"
export SSLKEYLOGFILE=/tmp/sslkeys.log
firefox

Configuration du décryptage QUIC dans Wireshark

# 1. Démarrer la capture Wireshark (filtrer le trafic QUIC)
# Filtre de capture :
udp port 443

# 2. Configurer le fichier journal de clés dans Wireshark
# Edit -> Preferences -> Protocols -> TLS -> (Pre)-Master-Secret log filename
# Saisir : /tmp/sslkeys.log

# 3. Vérifier le succès du décryptage QUIC
# Avant décryptage : les paquets QUIC affichent "Protected Payload, PKN: ..."
# Après décryptage : trames HTTP/3 visibles (HEADERS, DATA, SETTINGS, etc.)

Analyse de paquets en ligne de commande avec tshark

# Capturer le trafic QUIC avec décryptage
tshark -i eth0 -f "udp port 443" \
  -o "tls.keylog_file:/tmp/sslkeys.log" \
  -Y "quic" \
  -T fields \
  -e quic.packet_type \
  -e quic.frame_type \
  -e ip.src \
  -e ip.dst \
  -e quic.stream_id

# Extraire les en-têtes de requête HTTP/3
tshark -i eth0 -f "udp port 443" \
  -o "tls.keylog_file:/tmp/sslkeys.log" \
  -Y "http3" \
  -T fields \
  -e http3.header \
  -e http3.stream_id

# Analyser les informations de handshake de connexion QUIC
tshark -i eth0 -f "udp port 443" \
  -o "tls.keylog_file:/tmp/sslkeys.log" \
  -Y "quic.connection_id" \
  -T fields \
  -e quic.connection_id \
  -e quic.version

# Analyser la perte de paquets et la retransmission QUIC
tshark -i eth0 -f "udp port 443" \
  -o "tls.keylog_file:/tmp/sslkeys.log" \
  -Y "quic.frame_type == 0x02 || quic.frame_type == 0x03" \
  -T fields \
  -e quic.frame_type \
  -e quic.ack_range_count \
  -e quic.ack_delay

Export des clés sur serveur Go

package main

import (
	"crypto/tls"
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello HTTP/3!")
	})

	tlsConfig := &tls.Config{
		Certificates: []tls.Certificate{loadCert()},
		NextProtos:   []string{"h3"},
		KeyLogWriter: keyLogWriter(),
	}

	server := &http.Server{
		Addr:      ":443",
		Handler:   mux,
		TLSConfig: tlsConfig,
	}

	log.Fatal(server.ListenAndServeTLS("", ""))
}

func keyLogWriter() tls.KeyLogWriter {
	f, err := os.OpenFile("/tmp/server-sslkeys.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
	if err != nil {
		log.Printf("Failed to open keylog file: %v", err)
		return nil
	}
	return f
}

func loadCert() tls.Certificate {
	cert, err := tls.LoadX509KeyPair("server.crt", "server.key")
	if err != nil {
		log.Fatal(err)
	}
	return cert
}

Identification de version QUIC

# Numéros de version QUIC courants
# RFC 9000 (QUIC v1) :        0x00000001
# RFC 9369 (QUIC v2) :        0x6b3343cf
# Google QUIC (gQUIC) :       0x51303039 (Q039)
# IETF Draft 29 :             0xff00001d

# Filtrer une version spécifique avec tshark
tshark -i eth0 -f "udp port 443" \
  -Y "quic.version == 0x00000001" \
  -T fields \
  -e quic.connection_id \
  -e quic.version

Modèle 2 : Analyse et visualisation qlog

Qu'est-ce que qlog

qlog est le format de journalisation QUIC/HTTP/3 standardisé par l'IETF (RFC 9657). Il définit un modèle d'événements unifié permettant l'interopérabilité entre différentes implémentations QUIC. Contrairement aux formats propriétaires, qlog permet d'analyser les journaux de quiche, lsquic, quic-go, ngtcp2 et d'autres implémentations avec les mêmes outils.

{
  "qlog_version": "0.3",
  "title": "QUIC Connection Debug Log",
  "description": "HTTP/3 connection from client to example.com",
  "trace": [
    {
      "vantage_point": {
        "type": "client"
      },
      "title": "Client trace",
      "event_fields": [
        "relative_time",
        "category",
        "event_type",
        "data"
      ],
      "events": [
        [0, "transport", "packet_sent", {
          "packet_type": "initial",
          "header": {
            "packet_number": 0,
            "version": "0x00000001",
            "scid": "0x8394c8f03e515708",
            "dcid": "0x06b8a0b3a1914fc2"
          },
          "frames": [
            {
              "frame_type": "crypto",
              "offset": 0,
              "length": 387
            },
            {
              "frame_type": "padding"
            }
          ]
        }],
        [0.536, "transport", "packet_received", {
          "packet_type": "initial",
          "header": {
            "packet_number": 0
          },
          "frames": [
            {
              "frame_type": "crypto",
              "offset": 0,
              "length": 1256
            }
          ]
        }],
        [1.024, "http", "frame_created", {
          "stream_id": 0,
          "frame": {
            "frame_type": "headers",
            "headers": [
              {"name": ":method", "value": "GET"},
              {"name": ":path", "value": "/"},
              {"name": ":authority", "value": "example.com"},
              {"name": ":scheme", "value": "https"}
            ]
          }
        }]
      ]
    }
  ]
}

Collecte qlog côté serveur

# Nginx QUIC qlog configuration (1.27+)
http {
    quic_log_dir /var/log/nginx/quic;
    quic_log_level debug;

    server {
        listen 443 quic reuseport;
        server_name example.com;

        access_log /var/log/nginx/quic/access.log quic_format;
    }
}

Collecte qlog avec quic-go

package main

import (
	"context"
	"encoding/json"
	"log"
	"net"
	"os"
	"time"

	"github.com/quic-go/quic-go"
	"github.com/quic-go/quic-go/logging"
)

type qlogTracer struct {
	file *os.File
}

func newQlogTracer(filepath string) (*qlogTracer, error) {
	f, err := os.Create(filepath)
	if err != nil {
		return nil, err
	}
	return &qlogTracer{file: f}, nil
}

func (t *qlogTracer) Trace() *logging.ConnectionTracer {
	return &logging.ConnectionTracer{
		StartedConnection: func(local, remote net.Addr, srcConnID, destConnID logging.ConnectionID) {
			event := map[string]interface{}{
				"time":     time.Now().Format(time.RFC3339Nano),
				"category": "transport",
				"event":    "connection_started",
				"data": map[string]interface{}{
					"src_cid":  srcConnID.String(),
					"dest_cid": destConnID.String(),
				},
			}
			t.writeEvent(event)
		},
		ReceivedPacket: func(hdr *logging.PacketHeader, size logging.ByteCount, frames []logging.Frame) {
			event := map[string]interface{}{
				"time":     time.Now().Format(time.RFC3339Nano),
				"category": "transport",
				"event":    "packet_received",
				"data": map[string]interface{}{
					"packet_type": hdr.Type.String(),
					"packet_size": size,
					"frame_count": len(frames),
				},
			}
			t.writeEvent(event)
		},
		SentPacket: func(hdr *logging.PacketHeader, size logging.ByteCount, frames []logging.Frame) {
			event := map[string]interface{}{
				"time":     time.Now().Format(time.RFC3339Nano),
				"category": "transport",
				"event":    "packet_sent",
				"data": map[string]interface{}{
					"packet_type": hdr.Type.String(),
					"packet_size": size,
					"frame_count": len(frames),
				},
			}
			t.writeEvent(event)
		},
	}
}

func (t *qlogTracer) writeEvent(event map[string]interface{}) {
	data, _ := json.Marshal(event)
	t.file.Write(data)
	t.file.Write([]byte("\n"))
}

func main() {
	tracer, err := newQlogTracer("/tmp/quic-connection.qlog")
	if err != nil {
		log.Fatal(err)
	}
	defer tracer.file.Close()

	tlsConfig := &tls.Config{
		NextProtos: []string{"h3"},
	}

	quicConfig := &quic.Config{
		Tracer: func(ctx context.Context, p logging.Perspective, ci logging.ConnectionID) logging.ConnectionTracer {
			return *tracer.Trace()
		},
	}

	conn, err := quic.DialAddr(context.Background(), "example.com:443", tlsConfig, quicConfig)
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()

	log.Printf("Connected via QUIC, version: %s", conn.ConnectionState().Version)
}

Outils de visualisation qlog

# Visualisation qlog : qvis
# Outil en ligne : https://qvis.quictools.info/
# Téléverser le fichier .qlog pour la visualisation

# Installer qlog-converter
npm install -g qlog-converter

# Convertir un qlog binaire au format JSON
qlog-converter -i binary.qlog -o json.qlog --format JSON

# Convertir un JSON qlog en texte lisible
qlog-converter -i json.qlog -o readable.txt --format TEXT

# Générer un rapport de visualisation
npm install -g qlog-visualizer
qlog-visualizer -i connection.qlog -o report.html

Analyse des événements clés qlog

# Analyser la temporisation du handshake
cat connection.qlog | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data['trace'][0]['events']
handshake_events = [e for e in events if e[1] == 'transport' and 'packet' in e[2]]
for e in handshake_events[:10]:
    print(f't={e[0]:.3f}s  {e[2]}  type={e[3].get(\"packet_type\", \"?\")}')
"

# Analyser la perte de paquets et la retransmission
cat connection.qlog | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data['trace'][0]['events']
loss_events = [e for e in events if 'loss' in str(e[2]).lower() or 'retransmit' in str(e[2]).lower()]
for e in loss_events:
    print(f't={e[0]:.3f}s  {e[2]}  data={e[3]}')
print(f'Total loss events: {len(loss_events)}')
"

# Analyser la temporisation au niveau flux
cat connection.qlog | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data['trace'][0]['events']
stream_events = [e for e in events if e[1] == 'http' and 'stream' in str(e[3])]
for e in stream_events:
    stream_id = e[3].get('stream_id', '?')
    print(f't={e[0]:.3f}s  stream={stream_id}  {e[2]}')
"

Modèle 3 : Chrome NetLog pour le débogage HTTP/3

Pourquoi DevTools ne suffit pas

Le panneau Network de Chrome DevTools n'affiche que des informations de niveau applicatif (en-têtes de requête, corps de réponse, temporisation). Il ne peut pas voir les détails de la couche transport QUIC (migration de connexion, état 0-RTT, priorités de flux, récupération de perte). Pour déboguer les problèmes de transport HTTP/3, vous devez utiliser Chrome NetLog.

Lancer Chrome avec capture NetLog

# Méthode 1 : options de ligne de commande
google-chrome \
  --enable-logging=netlog \
  --net-log-capture-mode=Everything \
  --net-log=/tmp/chrome-netlog.json

# Méthode 2 : vue en temps réel chrome://net-internals
# 1. Naviguer vers chrome://net-internals/#export
# 2. Cliquer sur "Start logging to disk"
# 3. Effectuer l'opération à déboguer
# 4. Cliquer sur "Stop logging"

# Méthode 3 : Chrome DevTools Protocol
google-chrome --remote-debugging-port=9222

# Déclencher NetLog via CDP
curl -s http://localhost:9222/json/version | python3 -m json.tool

Analyse des événements NetLog

# La sortie NetLog est au format JSON contenant tous les événements réseau
# Structure typique :
# {
#   "constants": { ... },
#   "events": [
#     {"time": ..., "type": "HTTP3_SESSION_INITIALIZED", ...},
#     {"time": ..., "type": "QUIC_SESSION_PACKET_SENT", ...},
#     ...
#   ]
# }

# Extraire les événements liés à HTTP/3
cat /tmp/chrome-netlog.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data.get('events', [])
http3_events = [e for e in events if 'HTTP3' in e.get('type', '') or 'QUIC' in e.get('type', '')]
for e in http3_events[:50]:
    print(f't={e.get(\"time\",0)/1000000:.3f}s  type={e.get(\"type\",\"?\")}')
"

# Analyser la temporisation d'établissement de connexion QUIC
cat /tmp/chrome-netlog.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data.get('events', [])
connect_events = [e for e in events if any(k in e.get('type','') for k in ['QUIC_CONNECT', 'QUIC_SESSION_CONNECT', 'HTTP3_SESSION'])]
for e in connect_events:
    t = e.get('time', 0) / 1000000
    print(f't={t:.3f}s  {e.get(\"type\",\"?\")}')
    params = e.get('params', {})
    if params:
        for k, v in params.items():
            if k in ['host', 'quic_version', 'connection_id', 'error', 'is_alternative_service']:
                print(f'  {k}={v}')
"

# Vérifier l'état 0-RTT
cat /tmp/chrome-netlog.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data.get('events', [])
zrtt_events = [e for e in events if 'ZERO_RTT' in e.get('type', '') or 'early_data' in str(e.get('params', {})).lower()]
for e in zrtt_events:
    print(f't={e.get(\"time\",0)/1000000:.3f}s  {e.get(\"type\")}  params={e.get(\"params\",{})}')
if not zrtt_events:
    print('No 0-RTT events found')
"

# Analyser la migration de connexion
cat /tmp/chrome-netlog.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
events = data.get('events', [])
migration_events = [e for e in events if 'MIGRATION' in e.get('type', '') or 'CONNECTION_MIGRATION' in e.get('type', '')]
for e in migration_events:
    print(f't={e.get(\"time\",0)/1000000:.3f}s  {e.get(\"type\")}  params={e.get(\"params\",{})}')
if not migration_events:
    print('No connection migration events found')
"

Débogage en temps réel avec chrome://net-internals

Pages clés de chrome://net-internals :

#h3              - liste des sessions HTTP/3
#quic            - liste et configuration des connexions QUIC
#sockets         - état des sockets UDP
#dns             - résolution DNS (y compris les enregistrements HTTPS)
#httpCache       - état du cache
#altSvc          - contenu du cache Alt-Svc
# Via chrome://net-internals/#quic voir :
# - Connexions QUIC actives
# - Version, CID, état de chaque connexion
# - Paramètres de configuration QUIC
# - Informations d'erreur de connexion

# Via chrome://net-internals/#h3 voir :
# - État de session HTTP/3
# - Création et fermeture de flux
# - Relations de dépendance de priorité
# - État du push

Débogage du cache Alt-Svc

# Voir le cache Alt-Svc
# chrome://net-internals/#altSvc

# Problème courant : cache Alt-Svc expiré ou incorrect
# Effacer le cache Alt-Svc :
# 1. Ouvrir chrome://net-internals/#altSvc
# 2. Cliquer sur "Clear alt-svc cache"
# 3. Revisiter le site cible

# Forcer HTTP/3 (ignorer la découverte Alt-Svc)
google-chrome \
  --origin-to-force-quic-on=example.com:443 \
  --net-log=/tmp/chrome-forced-h3.json

Modèle 4 : Débogage curl HTTP/3

Compiler curl avec support HTTP/3

# Compiler curl avec HTTP/3 sur Ubuntu 22.04+
# Méthode : boringssl + nghttp3 + ngtcp2

# Installer les dépendances
sudo apt-get install -y build-essential cmake git

# Compiler boringssl
git clone https://boringssl.googlesource.com/boringssl
cd boringssl
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_POSITION_INDEPENDENT_CODE=on .
make -j$(nproc)
cd ..

# Compiler ngtcp2
git clone https://github.com/ngtcp2/ngtcp2
cd ngtcp2
autoreconf -fi
./configure --with-boringssl=$(pwd)/../boringssl \
  BORINGSSL_CFLAGS="-I$(pwd)/../boringssl/include" \
  BORINGSSL_LIBS="-L$(pwd)/../boringssl/build/ssl -L$(pwd)/../boringssl/build/crypto -lssl -lcrypto"
make -j$(nproc)
sudo make install
cd ..

# Compiler nghttp3
git clone https://github.com/ngtcp2/nghttp3
cd nghttp3
autoreconf -fi
./configure
make -j$(nproc)
sudo make install
cd ..

# Compiler curl avec HTTP/3
git clone https://github.com/curl/curl
cd curl
autoreconf -fi
./configure --with-openssl=$(pwd)/../boringssl \
  --with-ngtcp2=$(pwd)/../ngtcp2 \
  --with-nghttp3=$(pwd)/../nghttp3 \
  LDFLAGS="-Wl,-rpath,$(pwd)/../boringssl/build/ssl:$(pwd)/../boringssl/build/crypto"
make -j$(nproc)
sudo make install
cd ..

# Vérifier
curl --version | grep -i http3
# La sortie doit inclure : Features: ... HTTP3 ...

Débogage de requête HTTP/3 de base

# Envoyer une requête HTTP/3 avec sortie détaillée
curl --http3 https://example.com -v

# Exemple de sortie :
# *   Trying 93.184.216.34:443...
# * Connected to example.com (93.184.216.34) port 443
# * QUIC handshake successful
# * Connection #0 to host example.com left intact
# > GET / HTTP/3
# > Host: example.com
# > user-agent: curl/8.7.1
# > accept: */*
# >
# < HTTP/3 200
# < content-type: text/html; charset=UTF-8
# < date: Mon, 16 Jun 2026 10:00:00 GMT

# Forcer HTTP/3 uniquement (pas de fallback)
curl --http3-only https://example.com -v

# Tester 0-RTT
# Première requête : handshake normal
curl --http3 https://example.com -w "time_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_total: %{time_total}\n" -o /dev/null -s

# Seconde requête : 0-RTT (réutiliser la session)
curl --http3 https://example.com -w "time_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_total: %{time_total}\n" -o /dev/null -s

Analyse de temporisation curl

# Sortie de temporisation complète
curl --http3 https://example.com \
  -w "\n=== Timing Breakdown ===\n\
namelookup:    %{time_namelookup}s\n\
connect:       %{time_connect}s\n\
appconnect:    %{time_appconnect}s\n\
pretransfer:   %{time_pretransfer}s\n\
starttransfer: %{time_starttransfer}s\n\
total:         %{time_total}s\n\
\n=== Connection Info ===\n\
remote_ip:     %{remote_ip}\n\
remote_port:   %{remote_port}\n\
scheme:        %{scheme}\n\
http_version:  %{http_version}\n\
" -o /dev/null -s

# Comparer le temps de connexion HTTP/2 vs HTTP/3
echo "=== HTTP/2 ==="
curl --http2 https://example.com \
  -w "appconnect: %{time_appconnect}s  starttransfer: %{time_starttransfer}s  total: %{time_total}s\n" \
  -o /dev/null -s

echo "=== HTTP/3 ==="
curl --http3 https://example.com \
  -w "appconnect: %{time_appconnect}s  starttransfer: %{time_starttransfer}s  total: %{time_total}s\n" \
  -o /dev/null -s

# Test par lot (10 exécutions, moyenne)
for proto in h2 h3; do
  total=0
  for i in $(seq 1 10); do
    t=$(curl --http${proto} https://example.com -w "%{time_total}" -o /dev/null -s 2>/dev/null)
    total=$(echo "$total + $t" | bc)
  done
  avg=$(echo "scale=3; $total / 10" | bc)
  echo "HTTP/${proto} average total time: ${avg}s"
done

Débogage avec variables d'environnement curl

# Activer la journalisation interne QUIC
export SSLKEYLOGFILE=/tmp/curl-quic-keys.log

# Activer la journalisation détaillée ngtcp2
export NGTCP2_DEBUG_LOG=1

# Activer la journalisation détaillée nghttp3
export NGHTTP3_DEBUG_LOG=1

# Envoyer la requête
curl --http3 https://example.com -v 2>&1 | tee /tmp/curl-h3-debug.log

# Analyser le journal
grep -i "handshake\|0-rtt\|stream\|frame" /tmp/curl-h3-debug.log

# Tester une version QUIC spécifique
curl --http3 https://example.com -v \
  --quic-version v1    # RFC 9000 (QUIC v1)

curl --http3 https://example.com -v \
  --quic-version v2    # RFC 9369 (QUIC v2)

Simuler des problèmes réseau avec curl

# Simuler une latence élevée
curl --http3 https://example.com -v \
  --limit-rate 100k \
  --connect-timeout 5 \
  --max-time 30

# Tester la gestion du timeout de connexion
curl --http3-only https://unreachable.example.com -v \
  --connect-timeout 3 \
  --max-time 10

# Tester la découverte Alt-Svc
# D'abord obtenir l'en-tête Alt-Svc via HTTP/2
curl --http2 https://example.com -v -I 2>&1 | grep -i alt-svc

# Puis se connecter manuellement via HTTP/3
curl --http3 https://example.com -v

# Envoyer un grand fichier pour tester le contrôle de flux
dd if=/dev/urandom bs=1M count=100 2>/dev/null | \
  curl --http3 https://example.com/upload -v \
  -X POST \
  -H "Content-Type: application/octet-stream" \
  --data-binary @-

Modèle 5 : Monitoring QUIC de production

Collecte de métriques QUIC avec Prometheus

package main

import (
	"log"
	"net/http"

	"github.com/prometheus/client_golang/prometheus"
	"github.com/prometheus/client_golang/prometheus/promhttp"
)

var (
	quicConnectionsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_connections_total",
			Help: "Total number of QUIC connections",
		},
		[]string{"version", "status"},
	)

	quicConnectionDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "quic_connection_duration_seconds",
			Help:    "QUIC connection duration in seconds",
			Buckets: prometheus.ExponentialBuckets(0.1, 2, 15),
		},
		[]string{"version"},
	)

	quicHandshakeDuration = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "quic_handshake_duration_seconds",
			Help:    "QUIC handshake duration in seconds",
			Buckets: prometheus.ExponentialBuckets(0.001, 2, 15),
		},
		[]string{"version", "zero_rtt"},
	)

	quicStreamsTotal = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_streams_total",
			Help: "Total number of QUIC streams",
		},
		[]string{"direction", "stream_type"},
	)

	quicPacketsLost = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_packets_lost_total",
			Help: "Total number of QUIC packets lost",
		},
		[]string{"packet_type"},
	)

	quicRetransmitPackets = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_retransmit_packets_total",
			Help: "Total number of QUIC retransmit packets",
		},
		[]string{"packet_type"},
	)

	quicBytesTransferred = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_bytes_transferred_total",
			Help: "Total bytes transferred over QUIC",
		},
		[]string{"direction"},
	)

	quicConnectionMigrations = prometheus.NewCounter(
		prometheus.CounterOpts{
			Name: "quic_connection_migrations_total",
			Help: "Total number of QUIC connection migrations",
		},
	)

	quicZeroRTTAccepts = prometheus.NewCounterVec(
		prometheus.CounterOpts{
			Name: "quic_zero_rtt_accepts_total",
			Help: "Total number of 0-RTT connection attempts",
		},
		[]string{"status"},
	)

	quicCongestionWindow = prometheus.NewGaugeVec(
		prometheus.GaugeOpts{
			Name: "quic_congestion_window_bytes",
			Help: "Current QUIC congestion window size in bytes",
		},
		[]string{"connection_id"},
	)

	quicRtt = prometheus.NewHistogramVec(
		prometheus.HistogramOpts{
			Name:    "quic_rtt_seconds",
			Help:    "QUIC round trip time in seconds",
			Buckets: prometheus.ExponentialBuckets(0.001, 2, 15),
		},
		[]string{"rtt_type"},
	)
)

func init() {
	prometheus.MustRegister(
		quicConnectionsTotal,
		quicConnectionDuration,
		quicHandshakeDuration,
		quicStreamsTotal,
		quicPacketsLost,
		quicRetransmitPackets,
		quicBytesTransferred,
		quicConnectionMigrations,
		quicZeroRTTAccepts,
		quicCongestionWindow,
		quicRtt,
	)
}

func main() {
	http.Handle("/metrics", promhttp.Handler())

	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		if r.ProtoMajor == 3 {
			quicConnectionsTotal.WithLabelValues("v1", "active").Inc()
		}
		w.WriteHeader(http.StatusOK)
		w.Write([]byte("Hello HTTP/3!"))
	})

	log.Println("Starting server on :443 with metrics on :9090/metrics")

	go func() {
		log.Fatal(http.ListenAndServe(":9090", nil))
	}()

	log.Fatal(http.ListenAndServeTLS(":443", "server.crt", "server.key", mux))
}

Export des métriques QUIC dans Nginx

# nginx.conf - QUIC metrics via stub_status
http {
    server {
        listen 443 quic reuseport;
        listen 443 ssl;
        server_name example.com;

        location /nginx_status {
            stub_status;
            allow 127.0.0.1;
            deny all;
        }

        log_format quic_metrics
            '$remote_addr '
            '$quic_connection_id '
            '$quic_version '
            '$request_time '
            '$upstream_response_time '
            '$bytes_sent '
            '$bytes_received '
            '$status';

        access_log /var/log/nginx/quic-metrics.log quic_metrics;
    }
}
# Utiliser nginx-prometheus-exporter pour collecter les métriques Nginx
docker run -d --name nginx-exporter \
  -p 9113:9113 \
  nginx/nginx-prometheus-exporter:1.3 \
  --nginx.scrape-uri=http://nginx:80/nginx_status

# Analyseur de journal QUIC personnalisé (Python)
cat << 'EOF' > /usr/local/bin/quic-log-parser.py
import sys
from prometheus_client import Counter, Gauge, start_http_server

quic_connections = Counter('nginx_quic_connections', 'Nginx QUIC connections')
quic_request_duration = Gauge('nginx_quic_request_duration_seconds', 'QUIC request duration')

for line in sys.stdin:
    parts = line.strip().split()
    if len(parts) >= 8:
        quic_connections.inc()
        try:
            duration = float(parts[3])
            quic_request_duration.set(duration)
        except ValueError:
            pass

start_http_server(9114)
EOF

Configuration du tableau de bord Grafana

{
  "dashboard": {
    "title": "HTTP/3 & QUIC Production Monitoring",
    "panels": [
      {
        "title": "QUIC Connection Rate",
        "type": "timeseries",
        "targets": [
          {
            "expr": "rate(quic_connections_total[5m])",
            "legendFormat": "{{version}} {{status}}"
          }
        ]
      },
      {
        "title": "Handshake Duration",
        "type": "timeseries",
        "targets": [
          {
            "expr": "histogram_quantile(0.50, rate(quic_handshake_duration_seconds_bucket[5m]))",
            "legendFormat": "p50 {{version}} 0-rtt={{zero_rtt}}"
          },
          {
            "expr": "histogram_quantile(0.99, rate(quic_handshake_duration_seconds_bucket[5m]))",
            "legendFormat": "p99 {{version}} 0-rtt={{zero_rtt}}"
          }
        ]
      },
      {
        "title": "Packet Loss Rate",
        "type": "timeseries",
        "targets": [
          {
            "expr": "rate(quic_packets_lost_total[5m]) / rate(quic_connections_total[5m])",
            "legendFormat": "{{packet_type}} loss rate"
          }
        ]
      },
      {
        "title": "0-RTT Success Rate",
        "type": "gauge",
        "targets": [
          {
            "expr": "rate(quic_zero_rtt_accepts_total{status=\"accepted\"}[5m]) / rate(quic_zero_rtt_accepts_total[5m]) * 100",
            "legendFormat": "0-RTT Accept %"
          }
        ]
      },
      {
        "title": "Connection Migrations",
        "type": "stat",
        "targets": [
          {
            "expr": "rate(quic_connection_migrations_total[1h])",
            "legendFormat": "migrations/hour"
          }
        ]
      },
      {
        "title": "QUIC RTT Distribution",
        "type": "timeseries",
        "targets": [
          {
            "expr": "histogram_quantile(0.50, rate(quic_rtt_seconds_bucket[5m]))",
            "legendFormat": "p50 {{rtt_type}}"
          },
          {
            "expr": "histogram_quantile(0.95, rate(quic_rtt_seconds_bucket[5m]))",
            "legendFormat": "p95 {{rtt_type}}"
          }
        ]
      },
      {
        "title": "Throughput",
        "type": "timeseries",
        "targets": [
          {
            "expr": "rate(quic_bytes_transferred_total{direction=\"send\"}[5m]) * 8 / 1000000",
            "legendFormat": "Upload Mbps"
          },
          {
            "expr": "rate(quic_bytes_transferred_total{direction=\"receive\"}[5m]) * 8 / 1000000",
            "legendFormat": "Download Mbps"
          }
        ]
      },
      {
        "title": "Retransmit Rate",
        "type": "timeseries",
        "targets": [
          {
            "expr": "rate(quic_retransmit_packets_total[5m])",
            "legendFormat": "{{packet_type}} retransmits/s"
          }
        ]
      }
    ]
  }
}

Règles d'alerte

# Prometheus alerting rules for QUIC
groups:
  - name: quic_alerts
    rules:
      - alert: QUICHighPacketLoss
        expr: rate(quic_packets_lost_total[5m]) / rate(quic_connections_total[5m]) > 0.05
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "QUIC packet loss rate exceeds 5%"
          description: "QUIC packet loss rate is {{ $value | humanizePercentage }} on {{ $labels.packet_type }}"

      - alert: QUICHandshakeTimeout
        expr: histogram_quantile(0.99, rate(quic_handshake_duration_seconds_bucket[5m])) > 1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "QUIC handshake p99 latency exceeds 1s"
          description: "QUIC {{ $labels.version }} handshake p99 is {{ $value }}s"

      - alert: QUICZeroRTTRejectionHigh
        expr: rate(quic_zero_rtt_accepts_total{status="rejected"}[5m]) / rate(quic_zero_rtt_accepts_total[5m]) > 0.3
        for: 15m
        labels:
          severity: warning
        annotations:
          summary: "QUIC 0-RTT rejection rate exceeds 30%"
          description: "0-RTT rejection rate is {{ $value | humanizePercentage }}"

      - alert: QUICConnectionFailureSpike
        expr: rate(quic_connections_total{status="failed"}[5m]) > rate(quic_connections_total{status="active"}[5m]) * 0.1
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "QUIC connection failure rate exceeds 10%"
          description: "QUIC connection failures are {{ $value | humanizePercentage }} of successful connections"

      - alert: QUICHighRetransmitRate
        expr: rate(quic_retransmit_packets_total[5m]) / rate(quic_bytes_transferred_total[5m]) > 0.1
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "QUIC retransmit rate exceeds 10%"
          description: "QUIC retransmit rate is {{ $value | humanizePercentage }}"

Comparaison des 5 modèles de débogage

Modèle Cas d'usage Forces Limites Courbe d'apprentissage
Capture Wireshark Problèmes de niveau protocole, analyse de handshake La plus profonde, la plus complète Nécessite les clés, pas d'analyse live Élevée
Analyse qlog Comparaison entre implémentations, optimisation Standardisée, visualisable Nécessite le support d'implémentation Moyenne
Chrome NetLog Problèmes client, migration de connexion Native navigateur, temps réel Chrome uniquement, gros volume de données Moyenne
Débogage curl Validation rapide, intégration CI CLI, scriptable Nécessite compilation, fonctions limitées Faible
Monitoring de production Alertes de production, analyse de tendances Temps réel, vue globale Nécessite instrumentation, pas de rappel détaillé Moyenne-Élevée

Référence rapide de 10 scénarios de débogage courants

# Scénario Modèle recommandé Commande/outil clé
1 Échec de handshake QUIC Wireshark tshark -f "udp port 443" -o "tls.keylog_file:keys.log" -Y "quic.packet_type==initial"
2 0-RTT rejeté Chrome NetLog chrome://net-internals/#quic trouver les événements ZERO_RTT
3 Migration de connexion ne fonctionne pas Chrome NetLog --origin-to-force-quic-on + événements MIGRATION de NetLog
4 Récupération de perte lente qlog qvis visualiser les événements de détection et récupération de perte
5 Problèmes de priorité de flux Wireshark Déchiffrer et analyser les trames HTTP/3 PRIORITY
6 Alt-Svc ne fonctionne pas curl curl -I https://example.com vérifier l'en-tête Alt-Svc
7 Comparaison de performance curl comparaison de temporisation curl --http2 vs curl --http3
8 Perte de paquets anormale en production Prometheus alerte rate(quic_packets_lost_total[5m])
9 Latence de handshake élevée Prometheus histogram_quantile(0.99, quic_handshake_duration_seconds)
10 Risque de rejeu 0-RTT Wireshark + qlog Capturer et vérifier les données 0-RTT pour les requêtes non idempotentes

Outils recommandés

Lors du débogage HTTP/3, ces outils peuvent vous aider à analyser et vérifier :

  • Recherche de code de statut HTTP : Utilisez /fr/network/http-status pour rechercher les significations des codes de statut liés à QUIC/HTTP3
  • Encodeur Base64 : Utilisez /fr/encode/base64 pour encoder/décoder des certificats et des jetons
  • Calculateur de hachage : Utilisez /fr/encode/hash pour calculer des valeurs de hachage pour l'intégrité de la configuration QUIC

Résumé : Le défi central du débogage HTTP/3 réside dans le chiffrement complet et le transport UDP de QUIC. Les 5 modèles de débogage de production ont des focales différentes : capture Wireshark pour l'analyse approfondie de niveau protocole (nécessite le décryptage SSLKEYLOGFILE), qlog pour l'analyse de journal standardisée entre implémentations, Chrome NetLog pour le débogage temps réel côté client, curl pour la validation rapide et l'intégration CI, et Prometheus+Grafana pour le monitoring continu en production. Commencez par curl pour une validation rapide, passez à Wireshark/qlog pour les problèmes profonds, et construisez toujours un système complet de métriques QUIC de monitoring et d'alertes en production.

Essayez ces outils exécutés localement dans le navigateur — aucune inscription requise →

#HTTP/3调试#QUIC抓包#qlog#网络分析#Chrome NetLog#2026#网络协议