Toxiproxy ist ein kleiner TCP-Proxy von Shopify, der zwischen deinem MQTT-Client und dem Broker sitzt und die Verbindung gezielt kaputt macht: Latenz und Jitter hinzufügen, Bandbreite drosseln, den Verkehr einfrieren oder den Socket zurücksetzen. Gesteuert wird alles über eine HTTP-API, sodass deine Tests Fehler im Code ein- und ausschalten können.
Diese Anleitung stellt Toxiproxy vor einen MQTT-Broker, geht die für MQTT relevanten Toxics durch und macht daraus automatisierte Tests für Keep-Alive, Reconnect und QoS-Verhalten.
Warum ein Proxy statt Kabel ziehen
WLAN trennen oder den Broker killen sind grobe Werkzeuge. Sie lassen sich schlecht timen, kaum in CI wiederholen und simulieren nur einen Fehler: „alles weg“. Echte Netze versagen subtiler – ein Mobilfunklink mit zwei Sekunden zusätzlicher Latenz, eine NAT-Box, die einen inaktiven Flow still verwirft, ein überlasteter Uplink mit 5 KB pro Sekunde. Genau diese Bedingungen decken Bugs in Reconnect-Schleifen, Inflight-Fenstern und Timeouts auf.
Toxiproxy liefert jeden dieser Fehler als benanntes, umkehrbares „Toxic“ auf einer einzelnen TCP-Verbindung. Broker und andere Clients bleiben unberührt, und der Test kann genau prüfen, was dein Code getan hat, während das Toxic aktiv war.
| Reales Problem | Toxic | Was es testet |
|---|---|---|
| Langsames oder weit entferntes Netz | latency (mit jitter) | Timeouts, Warten auf CONNACK, PUBACK-Latenz |
| Schwacher Uplink / volumenbasierter Tarif | bandwidth | Große Payloads, Publish-Rückstau, Inflight-Limits |
| Halboffene Verbindung (toter NAT-Eintrag) | timeout mit timeout=0 | Erkennung per Keep-Alive |
| Verbindung wird nach einer Weile gekappt | timeout mit Wert, reset_peer | Reconnect und Session-Wiederaufnahme |
| Fragmentierte TCP-Segmente | slicer | Paket-Parsing über mehrere Reads |
| Verbindung bricht mitten in der Übertragung ab | limit_data | Teilpakete, QoS-Wiederholung |
Toxiproxy vor dem Broker starten
Am schnellsten geht es mit dem offiziellen Container. Port 8474 ist die Steuer-API; jeder Proxy, den du anlegst, bekommt einen eigenen Listen-Port.
docker run -d --name toxiproxy \
-p 8474:8474 -p 21883:21883 \
ghcr.io/shopify/toxiproxyLege einen Proxy an, der auf 21883 lauscht und an den Broker weiterleitet:
curl -s -X POST localhost:8474/proxies -d '{
"name": "mqtt",
"listen": "0.0.0.0:21883",
"upstream": "broker.connectmq.com:1883"
}'Richte einen beliebigen MQTT-Client auf localhost:21883 statt auf den Broker und prüfe, dass weiterhin Daten fließen:
mosquitto_sub -h localhost -p 21883 -t 'testmqtt/toxiproxy/#' -v &
mosquitto_pub -h localhost -p 21883 -t 'testmqtt/toxiproxy/hello' -m 'through the proxy'Denselben Topic kannst du im Online-MQTT-Client beobachten, der direkt mit dem Broker verbunden ist: Über den Proxy veröffentlichte Nachrichten erscheinen auch dort, sodass du leicht siehst, wann ein Toxic sie verzögert oder blockiert.
Für einen Test-Stack deklarierst du den Proxy in einer Konfigurationsdatei, damit er existiert, sobald der Container startet:
services:
toxiproxy:
image: ghcr.io/shopify/toxiproxy
command: ["-host=0.0.0.0", "-config=/config/toxiproxy.json"]
volumes:
- ./toxiproxy.json:/config/toxiproxy.json:ro
ports:
- "8474:8474"
- "21883:21883"[
{ "name": "mqtt", "listen": "0.0.0.0:21883", "upstream": "broker.connectmq.com:1883", "enabled": true }
]Läuft der Broker in derselben Compose-Datei (etwa ein Mosquitto-Container mit Testcontainers oder ein Service namens mosquitto), setze den Upstream auf mosquitto:1883.
Die für MQTT relevanten Toxics
Toxics werden pro Proxy und pro Richtung hinzugefügt. downstream ist Broker → Client (CONNACK, PUBACK, zugestellte Nachrichten), upstream ist Client → Broker (CONNECT, PUBLISH, PINGREQ). Standard ist downstream.
# 800 ms ± 200 ms on everything the broker sends back
curl -s -X POST localhost:8474/proxies/mqtt/toxics -d '{
"name": "slow", "type": "latency", "stream": "downstream",
"attributes": { "latency": 800, "jitter": 200 }
}'
# Throttle the client's uplink to 4 KB/s
curl -s -X POST localhost:8474/proxies/mqtt/toxics -d '{
"name": "thin", "type": "bandwidth", "stream": "upstream",
"attributes": { "rate": 4 }
}'
curl -s localhost:8474/proxies/mqtt/toxics # list active toxics
curl -s -X DELETE localhost:8474/proxies/mqtt/toxics/slow
curl -s -X POST localhost:8474/reset # remove all toxics, re-enable proxiesEinige MQTT-spezifische Hinweise:
- latency verzögert jeden Datenblock. Downstream angewendet verlängert es jeden Roundtrip: CONNACK, SUBACK und PUBACK kommen verspätet an. Damit findest du zu knappe Timeouts – ein Connect-Timeout von 1 s schlägt fehl, sobald die Latenz darüber liegt.
- bandwidth (
ratein KB/s) zeigt, was passiert, wenn du schneller publishst, als die Leitung transportieren kann. Bei QoS 1 stauen sich offene Nachrichten im Inflight-Fenster des Clients; siehe MQTT Flow Control. - timeout mit
timeout: 0stoppt alle Daten, lässt den Socket aber offen. Keine Seite bekommt einen TCP-Fehler – die klassische halboffene Verbindung, die nur MQTT Keep Alive bemerkt. - reset_peer schließt die Verbindung nach
timeoutms mit einem TCP-RST, der Client siehtECONNRESET. - slicer zerlegt die Daten in kleine Segmente mit Pausen dazwischen. Ein korrekter Client setzt MQTT-Pakete über mehrere Reads zusammen; ein naiver Embedded-Parser vielleicht nicht.
- limit_data schließt die Verbindung nach
bytesBytes – praktisch, um ein großes PUBLISH in der Mitte abzuschneiden.
Um die Verbindung komplett zu trennen, deaktivierst du den Proxy: curl -X POST localhost:8474/proxies/mqtt -d '{"enabled": false}'. Bestehende Verbindungen werden geschlossen, neue abgelehnt, bis du ihn wieder aktivierst.
Automatisieren: ein Keep-Alive-Test in Python
Die Steuer-API ist reines HTTP, eine Client-Bibliothek brauchst du nicht. Dieser Test (paho-mqtt 2.x) verbindet sich mit 5 Sekunden Keep-Alive über den Proxy, friert die Leitung ein und prüft, dass der Client das innerhalb des Keep-Alive-Fensters bemerkt.
import json, threading, time, urllib.request
import paho.mqtt.client as mqtt
API = "http://localhost:8474"
def toxiproxy(method, path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(API + path, data=data, method=method)
with urllib.request.urlopen(req) as resp:
return resp.read()
def test_client_detects_half_open_connection():
toxiproxy("POST", "/reset")
connected, disconnected = threading.Event(), threading.Event()
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id="toxi-keepalive-test")
client.on_connect = lambda c, u, f, rc, p: connected.set()
client.on_disconnect = lambda c, u, f, rc, p: disconnected.set()
client.connect("localhost", 21883, keepalive=5)
client.loop_start()
try:
assert connected.wait(5), "no CONNACK through the proxy"
# Verbindung in beide Richtungen einfrieren, ohne sie zu schließen
for stream in ("upstream", "downstream"):
toxiproxy("POST", "/proxies/mqtt/toxics", {
"name": f"freeze_{stream}", "type": "timeout",
"stream": stream, "attributes": {"timeout": 0},
})
started = time.monotonic()
assert disconnected.wait(15), "client never noticed the dead link"
print(f"dead link detected after {time.monotonic() - started:.1f} s")
finally:
toxiproxy("POST", "/reset")
client.loop_stop()
client.disconnect()Starte ihn mit pytest -s test_keepalive.py. Die Erkennung sollte etwa ein bis zwei Keep-Alive-Intervalle dauern, je nachdem, wo im Ping-Zyklus das Einfrieren passiert. Dauert es deutlich länger oder passiert es nie, setzt dein Client Keep-Alive nicht durch – ein Bug, der echte Geräte stundenlang mit nichts „verbunden“ lässt. Eine sauberere Fixture-Struktur zeigt pytest-Fixtures für MQTT.
Dasselbe Muster deckt weitere Szenarien ab:
- Reconnect – Proxy deaktivieren, warten, wieder aktivieren und prüfen, dass der Client sich neu verbindet und neu abonniert (Details in MQTT-Reconnect-Tests).
- QoS-1-Wiederholung –
limit_dataauf upstream setzen, eine große QoS-1-Nachricht publishen, das Toxic entfernen und prüfen, dass der Subscriber sie genau einmal bzw. mindestens einmal erhält, je nachdem, was dein Design erwartet. - Timeout-Budget – 3 s Downstream-Latenz hinzufügen und prüfen, dass dein Connect- oder Request-Response-Timeout sauber mit einer klaren Fehlermeldung scheitert, statt zu hängen.
MQTT über TLS und WebSockets durch Toxiproxy
Toxiproxy leitet rohe Bytes weiter, daher funktionieren TLS (8883) und WebSockets (8083, 8084 mit Pfad /mqtt) ohne Änderungen: Lege einen Proxy mit Upstream broker.connectmq.com:8883 an, und der Handshake läuft direkt durch.
Nur die Hostname-Prüfung musst du lösen. Der Client verbindet sich mit localhost, das Zertifikat nennt aber den Broker. Möglichkeiten:
- Behalte den Broker-Hostnamen im Client und leite ihn per
/etc/hostsoder Docker-extra_hostsauf dem Test-Runner auf den Proxy um. - Setze den TLS-Servernamen explizit – in MQTT.js über die Option
servername, in Python, indem du den Socket mitserver_hostnamegleich dem Broker-Namen wrappst. - Nutze für Netzwerkfehler-Tests reines TCP und halte TLS für einen separaten TLS-Verbindungstest bereit.
Deaktiviere nie die Zertifikatsprüfung, nur damit der Proxy in einem Test funktioniert, der auch mit Produktionseinstellungen läuft – diese Gewohnheit landet gern im echten Code.
Häufige Fehler
- Latenz nur upstream hinzufügen. Viele interessante Timeouts warten auf die Antwort des Brokers. Teste beide Richtungen, oder zuerst downstream.
timeout: 0mit dem Deaktivieren des Proxys verwechseln. Deaktivieren schließt die Sockets (der Client sieht sofort einen Disconnect);timeout: 0friert sie ein (nur Keep-Alive rettet dich).- Nur mit QoS 0 testen. QoS 0 verbirgt, was mit unbestätigten Nachrichten passiert. Wiederhole kritische Szenarien mit QoS 1.
- Über einen öffentlichen Broker messen. Geteilte Sandboxes bringen eigene Schwankungen mit. Für alles, worauf du asserten willst, nutze einen Broker unter deiner Kontrolle.
Nachrichten über den Proxy im Browser beobachten
Toxiproxy macht aus „im Feld bricht es manchmal ab“ einen Test, der bei jedem Commit läuft. Starte mit einem Proxy, einem Keep-Alive-Test mit eingefrorener Leitung und einem Reconnect-Test; sobald die grün sind, ergänze Latenz- und Bandbreitenprofile, die den Netzen deiner Geräte entsprechen, etwa die Mobilfunkprofile aus MQTT in langsamen, instabilen Mobilfunknetzen testen.
Häufig gestellte Fragen
Versteht Toxiproxy MQTT?
Nein. Toxiproxy ist ein TCP-Proxy und arbeitet mit Bytes, nicht mit MQTT-Paketen. Genau das brauchst du für Netzwerksimulation: Er verzögert, drosselt, zerstückelt oder kappt den Datenstrom, und Client und Broker reagieren wie auf einer echten schlechten Leitung.
Funktioniert Toxiproxy mit MQTT über TLS?
Ja, TLS läuft unverändert durch, weil Toxiproxy nur Bytes weiterleitet. Der Haken ist die Hostname-Prüfung: Der Client verbindet sich mit localhost, das Zertifikat lautet aber auf den Broker-Namen. Leite den Broker-Namen auf den Proxy um oder setze den TLS-Servernamen explizit.
Welches Toxic simuliert eine tote Verbindung, die nie geschlossen wird?
Das timeout-Toxic mit timeout=0. Es stoppt alle Daten, ohne den Socket zu schließen – die halboffene Verbindung, die nur MQTT Keep-Alive erkennen kann. Ein Wert ungleich null schließt die Verbindung nach so vielen Millisekunden.


